Skip to content

Commit eede6cf

Browse files
committed
Fix #23 Add Hosted MCP server tool support
1 parent e62bf56 commit eede6cf

File tree

15 files changed

+712
-63
lines changed

15 files changed

+712
-63
lines changed

examples/mcp/hosted-mcp-approvals.ts

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
@@ -99,6 +99,8 @@ export {
9999
HostedTool,
100100
ComputerTool,
101101
computerTool,
102+
HostedMCPTool,
103+
hostedMcpTool,
102104
FunctionTool,
103105
FunctionToolResult,
104106
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
hasToolsOrApprovalsToRun(): boolean;
5770
};
@@ -69,21 +82,78 @@ export function processModelResponse<TContext>(
6982
const runHandoffs: ToolRunHandoff[] = [];
7083
const runFunctions: ToolRunFunction<TContext>[] = [];
7184
const runComputerActions: ToolRunComputer[] = [];
85+
const runMCPApprovalRequests: ToolRunMCPApprovalRequest[] = [];
7286
const toolsUsed: string[] = [];
7387
const handoffMap = new Map(handoffs.map((h) => [h.toolName, h]));
7488
const functionMap = new Map(
7589
tools.filter((t) => t.type === 'function').map((t) => [t.name, t]),
7690
);
7791
const computerTool = tools.find((t) => t.type === 'computer');
92+
const mcpToolMap = new Map(
93+
tools
94+
.filter((t) => t.type === 'hosted_tool' && t.providerData?.type === 'mcp')
95+
.map((t) => t as HostedMCPTool)
96+
.map((t) => [t.providerData.serverLabel, t]),
97+
);
7898

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

418+
// run hosted MCP approval requests
419+
if (processedResponse.mcpApprovalRequests.length > 0) {
420+
for (const approvalRequest of processedResponse.mcpApprovalRequests) {
421+
const toolData = approvalRequest.mcpTool
422+
.providerData as ProviderData.HostedMCPTool<TContext>;
423+
if (!toolData.onApproval) {
424+
throw new UserError(
425+
`Hosted remote MCP server tool (${toolData.serverLabel}) does not have an onApproval function`,
426+
);
427+
}
428+
const approvalResult = await toolData.onApproval(
429+
state._context,
430+
approvalRequest.requestItem,
431+
);
432+
const requestData = approvalRequest.requestItem.rawItem
433+
.providerData as ProviderData.HostedMCPApprovalRequest;
434+
const approvalResponseData: ProviderData.HostedMCPApprovalResponse = {
435+
approve: approvalResult.approve,
436+
approvalRequestId: requestData.id,
437+
reason: approvalResult.reason,
438+
};
439+
newItems.push(
440+
new RunToolCallItem(
441+
{
442+
type: 'hosted_tool_call',
443+
name: 'mcp_approval_response',
444+
providerData: approvalResponseData,
445+
},
446+
agent as Agent<unknown, 'text'>,
447+
),
448+
);
449+
}
450+
}
451+
347452
// process handoffs
348453
if (processedResponse.handoffs.length > 0) {
349454
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,6 +740,15 @@ 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

739754
return {

0 commit comments

Comments
 (0)