Skip to content

Commit 2fae25c

Browse files
authored
Fix #23 Add Hosted MCP server tool support (#33)
1 parent e62bf56 commit 2fae25c

24 files changed

+1037
-127
lines changed

.changeset/wild-stars-teach.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@openai/agents-openai': patch
3+
'@openai/agents-core': patch
4+
---
5+
6+
Add hosted MCP server support
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
// 'always' |
18+
// 'never' |
19+
// { never?: { toolNames: string[] }; always?: { toolNames: string[] } }
20+
const requireApproval = {
21+
never: { toolNames: ['search_codex_code', 'fetch_codex_documentation'] },
22+
always: { toolNames: ['fetch_generic_url_content'] },
23+
};
24+
const agent = new Agent({
25+
name: 'MCP Assistant',
26+
instructions: 'You must always use the MCP tools to answer questions.',
27+
tools: [
28+
hostedMcpTool({
29+
serverLabel: 'gitmcp',
30+
serverUrl: 'https://gitmcp.io/openai/codex',
31+
requireApproval,
32+
// when you don't pass onApproval, the agent loop will handle the approval process
33+
}),
34+
],
35+
});
36+
37+
const input = 'Which language is this repo written in?';
38+
39+
if (stream) {
40+
// Streaming
41+
const result = await run(agent, input, { stream: true, maxTurns: 100 });
42+
for await (const event of result) {
43+
if (verbose) {
44+
console.log(JSON.stringify(event, null, 2));
45+
} else {
46+
if (
47+
event.type === 'raw_model_stream_event' &&
48+
event.data.type === 'model'
49+
) {
50+
console.log(event.data.event.type);
51+
}
52+
}
53+
}
54+
console.log(`Done streaming; final result: ${result.finalOutput}`);
55+
} else {
56+
// Non-streaming
57+
let result = await run(agent, input, { maxTurns: 100 });
58+
while (result.interruptions && result.interruptions.length) {
59+
for (const interruption of result.interruptions) {
60+
const approval = await promptApproval(interruption);
61+
if (approval) {
62+
result.state.approve(interruption);
63+
} else {
64+
result.state.reject(interruption);
65+
}
66+
}
67+
result = await run(agent, result.state, { maxTurns: 100 });
68+
}
69+
console.log(result.finalOutput);
70+
71+
if (verbose) {
72+
console.log('----------------------------------------------------------');
73+
console.log(JSON.stringify(result.newItems, null, 2));
74+
console.log('----------------------------------------------------------');
75+
}
76+
}
77+
}
78+
79+
const args = process.argv.slice(2);
80+
const verbose = args.includes('--verbose');
81+
const stream = args.includes('--stream');
82+
83+
main(verbose, stream).catch((err) => {
84+
console.error(err);
85+
process.exit(1);
86+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
// 'always' |
18+
// 'never' |
19+
// { never?: { toolNames: string[] }; always?: { toolNames: string[] } }
20+
const requireApproval = {
21+
never: {
22+
toolNames: ['fetch_codex_documentation', 'fetch_generic_url_content'],
23+
},
24+
always: {
25+
toolNames: ['search_codex_code'],
26+
},
27+
};
28+
const agent = new Agent({
29+
name: 'MCP Assistant',
30+
instructions: 'You must always use the MCP tools to answer questions.',
31+
tools: [
32+
hostedMcpTool({
33+
serverLabel: 'gitmcp',
34+
serverUrl: 'https://gitmcp.io/openai/codex',
35+
requireApproval,
36+
onApproval: async (_context, item) => {
37+
const approval = await promptApproval(item);
38+
return { approve: approval, reason: undefined };
39+
},
40+
}),
41+
],
42+
});
43+
44+
const input = 'Which language is this repo written in?';
45+
46+
if (stream) {
47+
// Streaming
48+
const result = await run(agent, input, { stream: true });
49+
for await (const event of result) {
50+
if (verbose) {
51+
console.log(JSON.stringify(event, null, 2));
52+
} else {
53+
if (
54+
event.type === 'raw_model_stream_event' &&
55+
event.data.type === 'model'
56+
) {
57+
console.log(event.data.event.type);
58+
}
59+
}
60+
}
61+
console.log(`Done streaming; final result: ${result.finalOutput}`);
62+
} else {
63+
// Non-streaming
64+
let result = await run(agent, input);
65+
while (result.interruptions && result.interruptions.length) {
66+
result = await run(agent, result.state);
67+
}
68+
console.log(result.finalOutput);
69+
70+
if (verbose) {
71+
console.log('----------------------------------------------------------');
72+
console.log(JSON.stringify(result.newItems, null, 2));
73+
console.log('----------------------------------------------------------');
74+
}
75+
}
76+
}
77+
78+
const args = process.argv.slice(2);
79+
const verbose = args.includes('--verbose');
80+
const stream = args.includes('--stream');
81+
82+
main(verbose, stream).catch((err) => {
83+
console.error(err);
84+
process.exit(1);
85+
});

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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
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-on-approval": "tsx hosted-mcp-on-approval.ts",
13+
"start:hosted-mcp-human-in-the-loop": "tsx hosted-mcp-human-in-the-loop.ts",
14+
"start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts"
1215
}
1316
}

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/items.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export class RunToolApprovalItem extends RunItemBase {
142142
public readonly type = 'tool_approval_item' as const;
143143

144144
constructor(
145-
public rawItem: protocol.FunctionCallItem,
145+
public rawItem: protocol.FunctionCallItem | protocol.HostedToolCallItem,
146146
public agent: Agent<any, any>,
147147
) {
148148
super();

packages/agents-core/src/runContext.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ export class RunContext<TContext = UnknownContext> {
117117
rejected: [],
118118
};
119119
if (Array.isArray(approvalEntry.approved)) {
120-
approvalEntry.approved.push(approvalItem.rawItem.callId);
120+
// function tool has call_id, hosted tool call has id
121+
const callId =
122+
'callId' in approvalItem.rawItem
123+
? approvalItem.rawItem.callId // function tools
124+
: approvalItem.rawItem.id!; // hosted tools
125+
approvalEntry.approved.push(callId);
121126
}
122127
this.#approvals.set(toolName, approvalEntry);
123128
}
@@ -146,7 +151,12 @@ export class RunContext<TContext = UnknownContext> {
146151
};
147152

148153
if (Array.isArray(approvalEntry.rejected)) {
149-
approvalEntry.rejected.push(approvalItem.rawItem.callId);
154+
// function tool has call_id, hosted tool call has id
155+
const callId =
156+
'callId' in approvalItem.rawItem
157+
? approvalItem.rawItem.callId // function tools
158+
: approvalItem.rawItem.id!; // hosted tools
159+
approvalEntry.rejected.push(callId);
150160
}
151161
this.#approvals.set(toolName, approvalEntry);
152162
}

0 commit comments

Comments
 (0)