Skip to content

Commit c42a0a9

Browse files
authored
refactor: restructure mcp tools fetching with options object pattern (#296)
1 parent 4bfd911 commit c42a0a9

File tree

8 files changed

+265
-109
lines changed

8 files changed

+265
-109
lines changed

.changeset/seven-rice-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openai/agents-core': patch
3+
---
4+
5+
refactor: restructure mcp tools fetching with options object pattern

examples/mcp/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ pnpm -F mcp start:stdio
1818
```bash
1919
pnpm -F mcp start:tool-filter
2020
```
21+
22+
`get-all-mcp-tools-example.ts` demonstrates how to use the `getAllMcpTools` function to fetch tools from multiple MCP servers:
23+
24+
```bash
25+
pnpm -F mcp start:get-all-tools
26+
```
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
Agent,
3+
run,
4+
MCPServerStdio,
5+
getAllMcpTools,
6+
withTrace,
7+
} from '@openai/agents';
8+
import * as path from 'node:path';
9+
10+
async function main() {
11+
const samplesDir = path.join(__dirname, 'sample_files');
12+
13+
// Create multiple MCP servers to demonstrate getAllMcpTools
14+
const filesystemServer = new MCPServerStdio({
15+
name: 'Filesystem Server',
16+
fullCommand: `npx -y @modelcontextprotocol/server-filesystem ${samplesDir}`,
17+
});
18+
19+
// Note: This example shows how to use multiple servers
20+
// In practice, you would have different servers with different tools
21+
const servers = [filesystemServer];
22+
23+
// Connect all servers
24+
for (const server of servers) {
25+
await server.connect();
26+
}
27+
28+
try {
29+
await withTrace('getAllMcpTools Example', async () => {
30+
console.log('=== Using getAllMcpTools to fetch all tools ===\n');
31+
32+
// Method 1: Simple array of servers
33+
const allTools = await getAllMcpTools(servers);
34+
console.log(
35+
`Found ${allTools.length} tools from ${servers.length} server(s):`,
36+
);
37+
allTools.forEach((tool) => {
38+
const description =
39+
tool.type === 'function' ? tool.description : 'No description';
40+
console.log(`- ${tool.name}: ${description}`);
41+
});
42+
43+
console.log('\n=== Using getAllMcpTools with options object ===\n');
44+
45+
// Method 2: Using options object (recommended for more control)
46+
const allToolsWithOptions = await getAllMcpTools({
47+
mcpServers: servers,
48+
convertSchemasToStrict: true, // Convert schemas to strict mode
49+
});
50+
51+
console.log(
52+
`Found ${allToolsWithOptions.length} tools with strict schemas:`,
53+
);
54+
allToolsWithOptions.forEach((tool) => {
55+
const description =
56+
tool.type === 'function' ? tool.description : 'No description';
57+
console.log(`- ${tool.name}: ${description}`);
58+
});
59+
60+
console.log('\n=== Creating agent with pre-fetched tools ===\n');
61+
62+
// Create agent using the pre-fetched tools
63+
const agent = new Agent({
64+
name: 'MCP Assistant with Pre-fetched Tools',
65+
instructions:
66+
'Use the available tools to help the user with file operations.',
67+
tools: allTools, // Use pre-fetched tools instead of mcpServers
68+
});
69+
70+
// Test the agent
71+
const message = 'List the available files and read one of them.';
72+
console.log(`Running: ${message}\n`);
73+
const result = await run(agent, message);
74+
console.log(result.finalOutput);
75+
76+
console.log(
77+
'\n=== Demonstrating tool filtering with getAllMcpTools ===\n',
78+
);
79+
80+
// Add tool filter to one of the servers
81+
filesystemServer.toolFilter = {
82+
allowedToolNames: ['read_file'], // Only allow read_file tool
83+
};
84+
85+
// Note: For callable filters to work, you need to pass runContext and agent
86+
// This is typically done internally when the agent runs
87+
const filteredTools = await getAllMcpTools({
88+
mcpServers: servers,
89+
convertSchemasToStrict: false,
90+
// runContext and agent would normally be provided by the agent runtime
91+
// For demo purposes, we're showing the structure
92+
});
93+
94+
console.log(`After filtering, found ${filteredTools.length} tools:`);
95+
filteredTools.forEach((tool) => {
96+
console.log(`- ${tool.name}`);
97+
});
98+
});
99+
} finally {
100+
// Clean up - close all servers
101+
for (const server of servers) {
102+
await server.close();
103+
}
104+
}
105+
}
106+
107+
main().catch((err) => {
108+
console.error('Error:', err);
109+
process.exit(1);
110+
});

examples/mcp/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"start:hosted-mcp-human-in-the-loop": "tsx hosted-mcp-human-in-the-loop.ts",
1515
"start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts",
1616
"start:tool-filter": "tsx tool-filter-example.ts",
17-
"start:sse": "tsx sse-example.ts"
17+
"start:sse": "tsx sse-example.ts",
18+
"start:get-all-tools": "tsx get-all-mcp-tools-example.ts"
1819
}
1920
}

packages/agents-core/src/agent.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,12 @@ export class Agent<
518518
runContext: RunContext<TContext>,
519519
): Promise<Tool<TContext>[]> {
520520
if (this.mcpServers.length > 0) {
521-
return getAllMcpTools(this.mcpServers, runContext, this, false);
521+
return getAllMcpTools({
522+
mcpServers: this.mcpServers,
523+
runContext,
524+
agent: this,
525+
convertSchemasToStrict: false,
526+
});
522527
}
523528

524529
return [];

packages/agents-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@ export { getLogger } from './logger';
7070
export {
7171
getAllMcpTools,
7272
invalidateServerToolsCache,
73+
mcpToFunctionTool,
7374
MCPServer,
7475
MCPServerStdio,
7576
MCPServerStreamableHttp,
7677
MCPServerSSE,
78+
GetAllMcpToolsOptions,
7779
} from './mcp';
7880
export {
7981
MCPToolFilterCallable,

packages/agents-core/src/mcp.ts

Lines changed: 109 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -285,35 +285,6 @@ export class MCPServerSSE extends BaseMCPServerSSE {
285285
* Fetches and flattens all tools from multiple MCP servers.
286286
* Logs and skips any servers that fail to respond.
287287
*/
288-
export async function getAllMcpFunctionTools<TContext = UnknownContext>(
289-
mcpServers: MCPServer[],
290-
runContext: RunContext<TContext>,
291-
agent: Agent<any, any>,
292-
convertSchemasToStrict = false,
293-
): Promise<Tool<TContext>[]> {
294-
const allTools: Tool<TContext>[] = [];
295-
const toolNames = new Set<string>();
296-
for (const server of mcpServers) {
297-
const serverTools = await getFunctionToolsFromServer(
298-
server,
299-
runContext,
300-
agent,
301-
convertSchemasToStrict,
302-
);
303-
const serverToolNames = new Set(serverTools.map((t) => t.name));
304-
const intersection = [...serverToolNames].filter((n) => toolNames.has(n));
305-
if (intersection.length > 0) {
306-
throw new UserError(
307-
`Duplicate tool names found across MCP servers: ${intersection.join(', ')}`,
308-
);
309-
}
310-
for (const t of serverTools) {
311-
toolNames.add(t.name);
312-
allTools.push(t);
313-
}
314-
}
315-
return allTools;
316-
}
317288

318289
const _cachedTools: Record<string, MCPTool[]> = {};
319290
/**
@@ -327,12 +298,17 @@ export async function invalidateServerToolsCache(serverName: string) {
327298
/**
328299
* Fetches all function tools from a single MCP server.
329300
*/
330-
async function getFunctionToolsFromServer<TContext = UnknownContext>(
331-
server: MCPServer,
332-
runContext: RunContext<TContext>,
333-
agent: Agent<any, any>,
334-
convertSchemasToStrict: boolean,
335-
): Promise<FunctionTool<TContext, any, unknown>[]> {
301+
async function getFunctionToolsFromServer<TContext = UnknownContext>({
302+
server,
303+
convertSchemasToStrict,
304+
runContext,
305+
agent,
306+
}: {
307+
server: MCPServer;
308+
convertSchemasToStrict: boolean;
309+
runContext?: RunContext<TContext>;
310+
agent?: Agent<any, any>;
311+
}): Promise<FunctionTool<TContext, any, unknown>[]> {
336312
if (server.cacheToolsList && _cachedTools[server.name]) {
337313
return _cachedTools[server.name].map((t) =>
338314
mcpToFunctionTool(t, server, convertSchemasToStrict),
@@ -341,52 +317,54 @@ async function getFunctionToolsFromServer<TContext = UnknownContext>(
341317
return withMCPListToolsSpan(
342318
async (span) => {
343319
const fetchedMcpTools = await server.listTools();
344-
const mcpTools: MCPTool[] = [];
345-
const context = {
346-
runContext,
347-
agent,
348-
serverName: server.name,
349-
};
350-
for (const tool of fetchedMcpTools) {
351-
const filter = server.toolFilter;
352-
if (filter) {
353-
if (filter && typeof filter === 'function') {
354-
const filtered = await filter(context, tool);
355-
if (!filtered) {
356-
globalLogger.debug(
357-
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the callable filter.`,
358-
);
359-
continue; // skip this tool
360-
}
361-
} else {
362-
const allowedToolNames = filter.allowedToolNames ?? [];
363-
const blockedToolNames = filter.blockedToolNames ?? [];
364-
if (allowedToolNames.length > 0 || blockedToolNames.length > 0) {
365-
const allowed =
366-
allowedToolNames.length > 0
367-
? allowedToolNames.includes(tool.name)
368-
: true;
369-
const blocked =
370-
blockedToolNames.length > 0
371-
? blockedToolNames.includes(tool.name)
372-
: false;
373-
if (!allowed || blocked) {
374-
if (blocked) {
375-
globalLogger.debug(
376-
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the static filter.`,
377-
);
378-
} else if (!allowed) {
379-
globalLogger.debug(
380-
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is not allowed by the static filter.`,
381-
);
320+
let mcpTools: MCPTool[] = fetchedMcpTools;
321+
322+
if (runContext && agent) {
323+
const context = { runContext, agent, serverName: server.name };
324+
const filteredTools: MCPTool[] = [];
325+
for (const tool of fetchedMcpTools) {
326+
const filter = server.toolFilter;
327+
if (filter) {
328+
if (typeof filter === 'function') {
329+
const filtered = await filter(context, tool);
330+
if (!filtered) {
331+
globalLogger.debug(
332+
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the callable filter.`,
333+
);
334+
continue;
335+
}
336+
} else {
337+
const allowedToolNames = filter.allowedToolNames ?? [];
338+
const blockedToolNames = filter.blockedToolNames ?? [];
339+
if (allowedToolNames.length > 0 || blockedToolNames.length > 0) {
340+
const allowed =
341+
allowedToolNames.length > 0
342+
? allowedToolNames.includes(tool.name)
343+
: true;
344+
const blocked =
345+
blockedToolNames.length > 0
346+
? blockedToolNames.includes(tool.name)
347+
: false;
348+
if (!allowed || blocked) {
349+
if (blocked) {
350+
globalLogger.debug(
351+
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the static filter.`,
352+
);
353+
} else if (!allowed) {
354+
globalLogger.debug(
355+
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is not allowed by the static filter.`,
356+
);
357+
}
358+
continue;
382359
}
383-
continue; // skip this tool
384360
}
385361
}
386362
}
363+
filteredTools.push(tool);
387364
}
388-
mcpTools.push(tool);
365+
mcpTools = filteredTools;
389366
}
367+
390368
span.spanData.result = mcpTools.map((t) => t.name);
391369
const tools: FunctionTool<TContext, any, string>[] = mcpTools.map((t) =>
392370
mcpToFunctionTool(t, server, convertSchemasToStrict),
@@ -400,21 +378,70 @@ async function getFunctionToolsFromServer<TContext = UnknownContext>(
400378
);
401379
}
402380

381+
/**
382+
* Options for fetching MCP tools.
383+
*/
384+
export type GetAllMcpToolsOptions<TContext> = {
385+
mcpServers: MCPServer[];
386+
convertSchemasToStrict?: boolean;
387+
runContext?: RunContext<TContext>;
388+
agent?: Agent<TContext, any>;
389+
};
390+
403391
/**
404392
* Returns all MCP tools from the provided servers, using the function tool conversion.
393+
* If runContext and agent are provided, callable tool filters will be applied.
405394
*/
406395
export async function getAllMcpTools<TContext = UnknownContext>(
407396
mcpServers: MCPServer[],
408-
runContext: RunContext<TContext>,
409-
agent: Agent<TContext, any>,
397+
): Promise<Tool<TContext>[]>;
398+
export async function getAllMcpTools<TContext = UnknownContext>(
399+
opts: GetAllMcpToolsOptions<TContext>,
400+
): Promise<Tool<TContext>[]>;
401+
export async function getAllMcpTools<TContext = UnknownContext>(
402+
mcpServersOrOpts: MCPServer[] | GetAllMcpToolsOptions<TContext>,
403+
runContext?: RunContext<TContext>,
404+
agent?: Agent<TContext, any>,
410405
convertSchemasToStrict = false,
411406
): Promise<Tool<TContext>[]> {
412-
return getAllMcpFunctionTools(
407+
const opts = Array.isArray(mcpServersOrOpts)
408+
? {
409+
mcpServers: mcpServersOrOpts,
410+
runContext,
411+
agent,
412+
convertSchemasToStrict,
413+
}
414+
: mcpServersOrOpts;
415+
416+
const {
413417
mcpServers,
414-
runContext,
415-
agent,
416-
convertSchemasToStrict,
417-
);
418+
convertSchemasToStrict: convertSchemasToStrictFromOpts = false,
419+
runContext: runContextFromOpts,
420+
agent: agentFromOpts,
421+
} = opts;
422+
const allTools: Tool<TContext>[] = [];
423+
const toolNames = new Set<string>();
424+
425+
for (const server of mcpServers) {
426+
const serverTools = await getFunctionToolsFromServer({
427+
server,
428+
convertSchemasToStrict: convertSchemasToStrictFromOpts,
429+
runContext: runContextFromOpts,
430+
agent: agentFromOpts,
431+
});
432+
const serverToolNames = new Set(serverTools.map((t) => t.name));
433+
const intersection = [...serverToolNames].filter((n) => toolNames.has(n));
434+
if (intersection.length > 0) {
435+
throw new UserError(
436+
`Duplicate tool names found across MCP servers: ${intersection.join(', ')}`,
437+
);
438+
}
439+
for (const t of serverTools) {
440+
toolNames.add(t.name);
441+
allTools.push(t);
442+
}
443+
}
444+
return allTools;
418445
}
419446

420447
/**

0 commit comments

Comments
 (0)