Skip to content

Commit 32726f6

Browse files
committed
Fix #23 Add Hosted MCP server tool support
1 parent be52a88 commit 32726f6

File tree

15 files changed

+712
-63
lines changed

15 files changed

+712
-63
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as readline from 'readline/promises';
2+
import { stdin, stdout } from 'node:process';
3+
import { Agent, run, hostedMcpTool, RunToolApprovalItem } from '@openai/agents';
4+
5+
async function promptApproval(item: RunToolApprovalItem): Promise<boolean> {
6+
const rl = readline.createInterface({ input: stdin, output: stdout });
7+
const name = item.rawItem.name;
8+
const params = JSON.parse(item.rawItem.providerData?.arguments || '{}');
9+
const answer = await rl.question(
10+
`Approve running tool (mcp: ${name}, params: ${JSON.stringify(params)})? (y/n) `,
11+
);
12+
rl.close();
13+
return answer.toLowerCase().trim() === 'y';
14+
}
15+
16+
async function main(verbose: boolean, stream: boolean): Promise<void> {
17+
const agent = new Agent({
18+
name: 'MCP Assistant',
19+
instructions: 'You must always use the MCP tools to answer questions.',
20+
tools: [
21+
hostedMcpTool({
22+
serverLabel: 'gitmcp',
23+
serverUrl: 'https://gitmcp.io/openai/codex',
24+
requireApproval: 'always',
25+
onApproval: async (_, data) => {
26+
return { approve: await promptApproval(data) };
27+
},
28+
}),
29+
],
30+
});
31+
32+
const input = 'Which language is this repo written in?';
33+
34+
if (stream) {
35+
// Streaming
36+
const result = await run(agent, input, { stream: true });
37+
for await (const event of result) {
38+
if (verbose) {
39+
console.log(JSON.stringify(event, null, 2));
40+
} else {
41+
if (
42+
event.type === 'raw_model_stream_event' &&
43+
event.data.type === 'model'
44+
) {
45+
console.log(event.data.event.type);
46+
}
47+
}
48+
}
49+
console.log(`Done streaming; final result: ${result.finalOutput}`);
50+
} else {
51+
// Non-streaming
52+
let result = await run(agent, input);
53+
while (result.interruptions && result.interruptions.length) {
54+
result = await run(agent, result.state);
55+
}
56+
console.log(result.finalOutput);
57+
58+
if (verbose) {
59+
console.log('----------------------------------------------------------');
60+
console.log(JSON.stringify(result.newItems, null, 2));
61+
console.log('----------------------------------------------------------');
62+
}
63+
}
64+
}
65+
66+
const args = process.argv.slice(2);
67+
const verbose = args.includes('--verbose');
68+
const stream = args.includes('--stream');
69+
70+
main(verbose, stream).catch((err) => {
71+
console.error(err);
72+
process.exit(1);
73+
});

examples/mcp/hosted-mcp-simple.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Agent, run, hostedMcpTool, withTrace } from '@openai/agents';
2+
3+
async function main(verbose: boolean, stream: boolean): Promise<void> {
4+
withTrace('Hosted MCP Example', async () => {
5+
const agent = new Agent({
6+
name: 'MCP Assistant',
7+
instructions: 'You must always use the MCP tools to answer questions.',
8+
tools: [
9+
hostedMcpTool({
10+
serverLabel: 'gitmcp',
11+
serverUrl: 'https://gitmcp.io/openai/codex',
12+
requireApproval: 'never',
13+
}),
14+
],
15+
});
16+
17+
const input =
18+
'Which language is the repo I pointed in the MCP tool settings written in?';
19+
if (stream) {
20+
const result = await run(agent, input, { stream: true });
21+
for await (const event of result) {
22+
if (
23+
event.type === 'raw_model_stream_event' &&
24+
event.data.type === 'model' &&
25+
event.data.event.type !== 'response.mcp_call_arguments.delta' &&
26+
event.data.event.type !== 'response.output_text.delta'
27+
) {
28+
console.log(`Got event of type ${JSON.stringify(event.data)}`);
29+
}
30+
}
31+
for (const item of result.newItems) {
32+
console.log(JSON.stringify(item, null, 2));
33+
}
34+
console.log(`Done streaming; final result: ${result.finalOutput}`);
35+
} else {
36+
const res = await run(agent, input);
37+
// The repository is primarily written in multiple languages, including Rust and TypeScript...
38+
if (verbose) {
39+
for (const item of res.output) {
40+
console.log(JSON.stringify(item, null, 2));
41+
}
42+
}
43+
console.log(res.finalOutput);
44+
}
45+
});
46+
}
47+
48+
const args = process.argv.slice(2);
49+
const verbose = args.includes('--verbose');
50+
const stream = args.includes('--stream');
51+
52+
main(verbose, stream).catch((err) => {
53+
console.error(err);
54+
process.exit(1);
55+
});

examples/mcp/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
},
99
"scripts": {
1010
"build-check": "tsc --noEmit",
11-
"start:stdio": "tsx filesystem-example.ts"
11+
"start:stdio": "tsx filesystem-example.ts",
12+
"start:hosted-mcp-approvals": "tsx hosted-mcp-approvals.ts",
13+
"start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts"
1214
}
1315
}

packages/agents-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export {
101101
HostedTool,
102102
ComputerTool,
103103
computerTool,
104+
HostedMCPTool,
105+
hostedMcpTool,
104106
FunctionTool,
105107
FunctionToolResult,
106108
Tool,

packages/agents-core/src/runImplementation.ts

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ import {
1414
} from './items';
1515
import logger, { Logger } from './logger';
1616
import { ModelResponse, ModelSettings } from './model';
17-
import { ComputerTool, FunctionTool, Tool, FunctionToolResult } from './tool';
17+
import {
18+
ComputerTool,
19+
FunctionTool,
20+
Tool,
21+
FunctionToolResult,
22+
HostedMCPTool,
23+
} from './tool';
1824
import { AgentInputItem, UnknownContext } from './types';
1925
import { Runner } from './run';
2026
import { RunContext } from './runContext';
@@ -31,6 +37,7 @@ import * as protocol from './types/protocol';
3137
import { Computer } from './computer';
3238
import { RunState } from './runState';
3339
import { isZodObject } from './utils';
40+
import * as ProviderData from './types/providerData';
3441

3542
type ToolRunHandoff = {
3643
toolCall: protocol.FunctionCallItem;
@@ -47,11 +54,17 @@ type ToolRunComputer = {
4754
computer: ComputerTool;
4855
};
4956

57+
type ToolRunMCPApprovalRequest = {
58+
requestItem: RunToolApprovalItem;
59+
mcpTool: HostedMCPTool;
60+
};
61+
5062
export type ProcessedResponse<TContext = UnknownContext> = {
5163
newItems: RunItem[];
5264
handoffs: ToolRunHandoff[];
5365
functions: ToolRunFunction<TContext>[];
5466
computerActions: ToolRunComputer[];
67+
mcpApprovalRequests: ToolRunMCPApprovalRequest[];
5568
toolsUsed: string[];
5669
};
5770

@@ -68,21 +81,78 @@ export function processModelResponse<TContext>(
6881
const runHandoffs: ToolRunHandoff[] = [];
6982
const runFunctions: ToolRunFunction<TContext>[] = [];
7083
const runComputerActions: ToolRunComputer[] = [];
84+
const runMCPApprovalRequests: ToolRunMCPApprovalRequest[] = [];
7185
const toolsUsed: string[] = [];
7286
const handoffMap = new Map(handoffs.map((h) => [h.toolName, h]));
7387
const functionMap = new Map(
7488
tools.filter((t) => t.type === 'function').map((t) => [t.name, t]),
7589
);
7690
const computerTool = tools.find((t) => t.type === 'computer');
91+
const mcpToolMap = new Map(
92+
tools
93+
.filter((t) => t.type === 'hosted_tool' && t.providerData?.type === 'mcp')
94+
.map((t) => t as HostedMCPTool)
95+
.map((t) => [t.providerData.serverLabel, t]),
96+
);
7797

7898
for (const output of modelResponse.output) {
7999
if (output.type === 'message') {
80100
if (output.role === 'assistant') {
81101
items.push(new RunMessageOutputItem(output, agent));
82102
}
83103
} else if (output.type === 'hosted_tool_call') {
84-
items.push(new RunToolCallItem(output, agent));
85-
toolsUsed.push(output.name);
104+
if (
105+
output.providerData?.type === 'mcp_approval_request' ||
106+
output.name === 'mcp_approval_request'
107+
) {
108+
// Hosted remote MCP server support
109+
const providerData =
110+
output.providerData as ProviderData.HostedMCPApprovalRequest;
111+
const mcpServerLabel = providerData.serverLabel;
112+
const mcpServerTool = mcpToolMap.get(mcpServerLabel);
113+
if (mcpServerTool !== undefined) {
114+
const toolName = JSON.stringify({
115+
server: providerData.serverLabel,
116+
name: providerData.name,
117+
});
118+
// Do this approval later
119+
runMCPApprovalRequests.push({
120+
requestItem: new RunToolApprovalItem(
121+
{
122+
type: 'function_call',
123+
name: toolName,
124+
callId: providerData.id,
125+
arguments: providerData.arguments || '',
126+
status: 'in_progress',
127+
providerData,
128+
},
129+
agent,
130+
),
131+
mcpTool: mcpServerTool,
132+
});
133+
items.push(new RunToolCallItem(output, agent));
134+
toolsUsed.push(toolName);
135+
} else {
136+
const message = `MCP server (${mcpServerLabel}) not found in Agent (${agent.name})`;
137+
addErrorToCurrentSpan({
138+
message,
139+
data: { mcp_server_label: mcpServerLabel },
140+
});
141+
throw new ModelBehaviorError(message);
142+
}
143+
} else {
144+
// the rest of the hosted
145+
items.push(new RunToolCallItem(output, agent));
146+
const toolName = output.providerData?.serverLabel
147+
? // hosted MCP tool
148+
JSON.stringify({
149+
server: output.providerData.serverLabel,
150+
name: output.name,
151+
})
152+
: // other hosted tools
153+
output.name;
154+
toolsUsed.push(toolName);
155+
}
86156
} else if (output.type === 'reasoning') {
87157
items.push(new RunReasoningItem(output, agent));
88158
} else if (output.type === 'computer_call') {
@@ -146,6 +216,7 @@ export function processModelResponse<TContext>(
146216
handoffs: runHandoffs,
147217
functions: runFunctions,
148218
computerActions: runComputerActions,
219+
mcpApprovalRequests: runMCPApprovalRequests,
149220
toolsUsed: toolsUsed,
150221
};
151222
}
@@ -336,6 +407,40 @@ export async function executeToolsAndSideEffects<TContext>(
336407
newItems = newItems.concat(functionResults.map((r) => r.runItem));
337408
newItems = newItems.concat(computerResults);
338409

410+
// run hosted MCP approval requests
411+
if (processedResponse.mcpApprovalRequests.length > 0) {
412+
for (const approvalRequest of processedResponse.mcpApprovalRequests) {
413+
const toolData = approvalRequest.mcpTool
414+
.providerData as ProviderData.HostedMCPTool<TContext>;
415+
if (!toolData.onApproval) {
416+
throw new UserError(
417+
`Hosted remote MCP server tool (${toolData.serverLabel}) does not have an onApproval function`,
418+
);
419+
}
420+
const approvalResult = await toolData.onApproval(
421+
state._context,
422+
approvalRequest.requestItem,
423+
);
424+
const requestData = approvalRequest.requestItem.rawItem
425+
.providerData as ProviderData.HostedMCPApprovalRequest;
426+
const approvalResponseData: ProviderData.HostedMCPApprovalResponse = {
427+
approve: approvalResult.approve,
428+
approvalRequestId: requestData.id,
429+
reason: approvalResult.reason,
430+
};
431+
newItems.push(
432+
new RunToolCallItem(
433+
{
434+
type: 'hosted_tool_call',
435+
name: 'mcp_approval_response',
436+
providerData: approvalResponseData,
437+
},
438+
agent as Agent<unknown, 'text'>,
439+
),
440+
);
441+
}
442+
}
443+
339444
// process handoffs
340445
if (processedResponse.handoffs.length > 0) {
341446
return await executeHandoffCalls(

packages/agents-core/src/runState.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ const serializedProcessedResponseSchema = z.object({
152152
computer: z.any(),
153153
}),
154154
),
155+
mcpApprovalRequests: z.array(
156+
z.object({
157+
requestItem: z.any(),
158+
mcpTool: z.any(),
159+
}),
160+
),
155161
});
156162

157163
const guardrailFunctionOutputSchema = z.object({
@@ -734,5 +740,14 @@ async function deserializeProcessedResponse<TContext = UnknownContext>(
734740
};
735741
},
736742
),
743+
mcpApprovalRequests: serializedProcessedResponse.mcpApprovalRequests.map(
744+
(approvalRequest) => ({
745+
requestItem: new RunToolApprovalItem(
746+
approvalRequest.requestItem.rawItem,
747+
currentAgent,
748+
),
749+
mcpTool: approvalRequest.mcpTool,
750+
}),
751+
),
737752
};
738753
}

0 commit comments

Comments
 (0)