Skip to content

Commit 0c4abde

Browse files
feat(realtime-twilio): add MCP demo tools (#427)
1 parent 2c43bcc commit 0c4abde

File tree

2 files changed

+115
-25
lines changed

2 files changed

+115
-25
lines changed

examples/realtime-twilio/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ The script in `index.ts` starts a Fastify server that serves TwiML for incoming
55
endpoint for streaming audio. When a call connects, the audio stream is forwarded through a
66
`TwilioRealtimeTransportLayer` to a `RealtimeSession` so the `RealtimeAgent` can respond in real time.
77

8+
The demo agent mirrors the [realtime-next](../realtime-next) example. It includes the same MCP integrations
9+
(`dnd` and `deepwiki`) as well as local sample tools for weather lookups and a secret number helper. Ask the
10+
agent to "look that up in Deep Wiki" or "roll a Dungeons and Dragons character" to try the hosted MCP tools.
11+
812
To try it out you must have a Twilio phone number.
913
Expose your localhost with a tunneling service such as ngrok and set the phone number's incoming call URL to `https://<your-tunnel-url>/incoming-call`.
1014

@@ -13,4 +17,3 @@ Start the server with:
1317
```bash
1418
pnpm -F realtime-twilio start
1519
```
16-

examples/realtime-twilio/index.ts

Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import Fastify from 'fastify';
2+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
23
import dotenv from 'dotenv';
34
import fastifyFormBody from '@fastify/formbody';
45
import fastifyWs from '@fastify/websocket';
5-
import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
6+
import {
7+
RealtimeAgent,
8+
RealtimeSession,
9+
backgroundResult,
10+
tool,
11+
} from '@openai/agents/realtime';
612
import { TwilioRealtimeTransportLayer } from '@openai/agents-extensions';
13+
import { hostedMcpTool } from '@openai/agents';
14+
import { z } from 'zod';
15+
import process from 'node:process';
716

817
// Load environment variables from .env file
918
dotenv.config();
@@ -21,50 +30,128 @@ const fastify = Fastify();
2130
fastify.register(fastifyFormBody);
2231
fastify.register(fastifyWs);
2332

33+
const weatherTool = tool({
34+
name: 'weather',
35+
description: 'Get the weather in a given location.',
36+
parameters: z.object({
37+
location: z.string(),
38+
}),
39+
execute: async ({ location }: { location: string }) => {
40+
return backgroundResult(`The weather in ${location} is sunny.`);
41+
},
42+
});
43+
44+
const secretTool = tool({
45+
name: 'secret',
46+
description: 'A secret tool to tell the special number.',
47+
parameters: z.object({
48+
question: z
49+
.string()
50+
.describe(
51+
'The question to ask the secret tool; mainly about the special number.',
52+
),
53+
}),
54+
execute: async ({ question }: { question: string }) => {
55+
return `The answer to ${question} is 42.`;
56+
},
57+
needsApproval: true,
58+
});
59+
2460
const agent = new RealtimeAgent({
25-
name: 'Triage Agent',
61+
name: 'Greeter',
2662
instructions:
27-
'You are a helpful assistant that starts every conversation with a creative greeting.',
63+
'You are a friendly assistant. When you use a tool always first say what you are about to do.',
64+
tools: [
65+
hostedMcpTool({
66+
serverLabel: 'dnd',
67+
}),
68+
hostedMcpTool({
69+
serverLabel: 'deepwiki',
70+
}),
71+
secretTool,
72+
weatherTool,
73+
],
2874
});
2975

3076
// Root Route
31-
fastify.get('/', async (request, reply) => {
77+
fastify.get('/', async (_request: FastifyRequest, reply: FastifyReply) => {
3278
reply.send({ message: 'Twilio Media Stream Server is running!' });
3379
});
3480

3581
// Route for Twilio to handle incoming and outgoing calls
3682
// <Say> punctuation to improve text-to-speech translation
37-
fastify.all('/incoming-call', async (request, reply) => {
38-
const twimlResponse = `
83+
fastify.all(
84+
'/incoming-call',
85+
async (request: FastifyRequest, reply: FastifyReply) => {
86+
const twimlResponse = `
3987
<?xml version="1.0" encoding="UTF-8"?>
4088
<Response>
4189
<Say>O.K. you can start talking!</Say>
4290
<Connect>
4391
<Stream url="wss://${request.headers.host}/media-stream" />
4492
</Connect>
4593
</Response>`.trim();
46-
reply.type('text/xml').send(twimlResponse);
47-
});
94+
reply.type('text/xml').send(twimlResponse);
95+
},
96+
);
4897

4998
// WebSocket route for media-stream
50-
fastify.register(async (fastify) => {
51-
fastify.get('/media-stream', { websocket: true }, async (connection) => {
52-
const twilioTransportLayer = new TwilioRealtimeTransportLayer({
53-
twilioWebSocket: connection,
54-
});
55-
56-
const session = new RealtimeSession(agent, {
57-
transport: twilioTransportLayer,
58-
});
59-
60-
await session.connect({
61-
apiKey: OPENAI_API_KEY,
62-
});
63-
console.log('Connected to the OpenAI Realtime API');
64-
});
99+
fastify.register(async (scopedFastify: FastifyInstance) => {
100+
scopedFastify.get(
101+
'/media-stream',
102+
{ websocket: true },
103+
async (connection: any) => {
104+
const twilioTransportLayer = new TwilioRealtimeTransportLayer({
105+
twilioWebSocket: connection,
106+
});
107+
108+
const session = new RealtimeSession(agent, {
109+
transport: twilioTransportLayer,
110+
model: 'gpt-realtime',
111+
config: {
112+
audio: {
113+
output: {
114+
voice: 'verse',
115+
},
116+
},
117+
},
118+
});
119+
120+
session.on('mcp_tools_changed', (tools: { name: string }[]) => {
121+
const toolNames = tools.map((tool) => tool.name).join(', ');
122+
console.log(`Available MCP tools: ${toolNames || 'None'}`);
123+
});
124+
125+
session.on(
126+
'tool_approval_requested',
127+
(_context: unknown, _agent: unknown, approvalRequest: any) => {
128+
console.log(
129+
`Approving tool call for ${approvalRequest.approvalItem.rawItem.name}.`,
130+
);
131+
session
132+
.approve(approvalRequest.approvalItem)
133+
.catch((error: unknown) =>
134+
console.error('Failed to approve tool call.', error),
135+
);
136+
},
137+
);
138+
139+
session.on(
140+
'mcp_tool_call_completed',
141+
(_context: unknown, _agent: unknown, toolCall: unknown) => {
142+
console.log('MCP tool call completed.', toolCall);
143+
},
144+
);
145+
146+
await session.connect({
147+
apiKey: OPENAI_API_KEY,
148+
});
149+
console.log('Connected to the OpenAI Realtime API');
150+
},
151+
);
65152
});
66153

67-
fastify.listen({ port: PORT }, (err) => {
154+
fastify.listen({ port: PORT }, (err: Error | null) => {
68155
if (err) {
69156
console.error(err);
70157
process.exit(1);

0 commit comments

Comments
 (0)