Skip to content

refactor: restructure mcp tools fetching with options object pattern #296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 11, 2025
Merged
5 changes: 5 additions & 0 deletions .changeset/seven-rice-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/agents-core': patch
---

refactor: restructure mcp tools fetching with options object pattern
6 changes: 6 additions & 0 deletions examples/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ pnpm -F mcp start:stdio
```bash
pnpm -F mcp start:tool-filter
```

`get-all-mcp-tools-example.ts` demonstrates how to use the `getAllMcpTools` function to fetch tools from multiple MCP servers:

```bash
pnpm -F mcp start:get-all-tools
```
110 changes: 110 additions & 0 deletions examples/mcp/get-all-mcp-tools-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
Agent,
run,
MCPServerStdio,
getAllMcpTools,
withTrace,
} from '@openai/agents';
import * as path from 'node:path';

async function main() {
const samplesDir = path.join(__dirname, 'sample_files');

// Create multiple MCP servers to demonstrate getAllMcpTools
const filesystemServer = new MCPServerStdio({
name: 'Filesystem Server',
fullCommand: `npx -y @modelcontextprotocol/server-filesystem ${samplesDir}`,
});

// Note: This example shows how to use multiple servers
// In practice, you would have different servers with different tools
const servers = [filesystemServer];

// Connect all servers
for (const server of servers) {
await server.connect();
}

try {
await withTrace('getAllMcpTools Example', async () => {
console.log('=== Using getAllMcpTools to fetch all tools ===\n');

// Method 1: Simple array of servers
const allTools = await getAllMcpTools(servers);
console.log(
`Found ${allTools.length} tools from ${servers.length} server(s):`,
);
allTools.forEach((tool) => {
const description =
tool.type === 'function' ? tool.description : 'No description';
console.log(`- ${tool.name}: ${description}`);
});

console.log('\n=== Using getAllMcpTools with options object ===\n');

// Method 2: Using options object (recommended for more control)
const allToolsWithOptions = await getAllMcpTools({
mcpServers: servers,
convertSchemasToStrict: true, // Convert schemas to strict mode
});

console.log(
`Found ${allToolsWithOptions.length} tools with strict schemas:`,
);
allToolsWithOptions.forEach((tool) => {
const description =
tool.type === 'function' ? tool.description : 'No description';
console.log(`- ${tool.name}: ${description}`);
});

console.log('\n=== Creating agent with pre-fetched tools ===\n');

// Create agent using the pre-fetched tools
const agent = new Agent({
name: 'MCP Assistant with Pre-fetched Tools',
instructions:
'Use the available tools to help the user with file operations.',
tools: allTools, // Use pre-fetched tools instead of mcpServers
});

// Test the agent
const message = 'List the available files and read one of them.';
console.log(`Running: ${message}\n`);
const result = await run(agent, message);
console.log(result.finalOutput);

console.log(
'\n=== Demonstrating tool filtering with getAllMcpTools ===\n',
);

// Add tool filter to one of the servers
filesystemServer.toolFilter = {
allowedToolNames: ['read_file'], // Only allow read_file tool
};

// Note: For callable filters to work, you need to pass runContext and agent
// This is typically done internally when the agent runs
const filteredTools = await getAllMcpTools({
mcpServers: servers,
convertSchemasToStrict: false,
// runContext and agent would normally be provided by the agent runtime
// For demo purposes, we're showing the structure
});

console.log(`After filtering, found ${filteredTools.length} tools:`);
filteredTools.forEach((tool) => {
console.log(`- ${tool.name}`);
});
});
} finally {
// Clean up - close all servers
for (const server of servers) {
await server.close();
}
}
}

main().catch((err) => {
console.error('Error:', err);
process.exit(1);
});
3 changes: 2 additions & 1 deletion examples/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"start:hosted-mcp-human-in-the-loop": "tsx hosted-mcp-human-in-the-loop.ts",
"start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts",
"start:tool-filter": "tsx tool-filter-example.ts",
"start:sse": "tsx sse-example.ts"
"start:sse": "tsx sse-example.ts",
"start:get-all-tools": "tsx get-all-mcp-tools-example.ts"
}
}
7 changes: 6 additions & 1 deletion packages/agents-core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,12 @@ export class Agent<
runContext: RunContext<TContext>,
): Promise<Tool<TContext>[]> {
if (this.mcpServers.length > 0) {
return getAllMcpTools(this.mcpServers, runContext, this, false);
return getAllMcpTools({
mcpServers: this.mcpServers,
runContext,
agent: this,
convertSchemasToStrict: false,
});
}

return [];
Expand Down
2 changes: 2 additions & 0 deletions packages/agents-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ export { getLogger } from './logger';
export {
getAllMcpTools,
invalidateServerToolsCache,
mcpToFunctionTool,
MCPServer,
MCPServerStdio,
MCPServerStreamableHttp,
MCPServerSSE,
GetAllMcpToolsOptions,
} from './mcp';
export {
MCPToolFilterCallable,
Expand Down
191 changes: 109 additions & 82 deletions packages/agents-core/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,35 +285,6 @@ export class MCPServerSSE extends BaseMCPServerSSE {
* Fetches and flattens all tools from multiple MCP servers.
* Logs and skips any servers that fail to respond.
*/
export async function getAllMcpFunctionTools<TContext = UnknownContext>(
mcpServers: MCPServer[],
runContext: RunContext<TContext>,
agent: Agent<any, any>,
convertSchemasToStrict = false,
): Promise<Tool<TContext>[]> {
const allTools: Tool<TContext>[] = [];
const toolNames = new Set<string>();
for (const server of mcpServers) {
const serverTools = await getFunctionToolsFromServer(
server,
runContext,
agent,
convertSchemasToStrict,
);
const serverToolNames = new Set(serverTools.map((t) => t.name));
const intersection = [...serverToolNames].filter((n) => toolNames.has(n));
if (intersection.length > 0) {
throw new UserError(
`Duplicate tool names found across MCP servers: ${intersection.join(', ')}`,
);
}
for (const t of serverTools) {
toolNames.add(t.name);
allTools.push(t);
}
}
return allTools;
}

const _cachedTools: Record<string, MCPTool[]> = {};
/**
Expand All @@ -327,12 +298,17 @@ export async function invalidateServerToolsCache(serverName: string) {
/**
* Fetches all function tools from a single MCP server.
*/
async function getFunctionToolsFromServer<TContext = UnknownContext>(
server: MCPServer,
runContext: RunContext<TContext>,
agent: Agent<any, any>,
convertSchemasToStrict: boolean,
): Promise<FunctionTool<TContext, any, unknown>[]> {
async function getFunctionToolsFromServer<TContext = UnknownContext>({
server,
convertSchemasToStrict,
runContext,
agent,
}: {
server: MCPServer;
convertSchemasToStrict: boolean;
runContext?: RunContext<TContext>;
agent?: Agent<any, any>;
}): Promise<FunctionTool<TContext, any, unknown>[]> {
if (server.cacheToolsList && _cachedTools[server.name]) {
return _cachedTools[server.name].map((t) =>
mcpToFunctionTool(t, server, convertSchemasToStrict),
Expand All @@ -341,52 +317,54 @@ async function getFunctionToolsFromServer<TContext = UnknownContext>(
return withMCPListToolsSpan(
async (span) => {
const fetchedMcpTools = await server.listTools();
const mcpTools: MCPTool[] = [];
const context = {
runContext,
agent,
serverName: server.name,
};
for (const tool of fetchedMcpTools) {
const filter = server.toolFilter;
if (filter) {
if (filter && typeof filter === 'function') {
const filtered = await filter(context, tool);
if (!filtered) {
globalLogger.debug(
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the callable filter.`,
);
continue; // skip this tool
}
} else {
const allowedToolNames = filter.allowedToolNames ?? [];
const blockedToolNames = filter.blockedToolNames ?? [];
if (allowedToolNames.length > 0 || blockedToolNames.length > 0) {
const allowed =
allowedToolNames.length > 0
? allowedToolNames.includes(tool.name)
: true;
const blocked =
blockedToolNames.length > 0
? blockedToolNames.includes(tool.name)
: false;
if (!allowed || blocked) {
if (blocked) {
globalLogger.debug(
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the static filter.`,
);
} else if (!allowed) {
globalLogger.debug(
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is not allowed by the static filter.`,
);
let mcpTools: MCPTool[] = fetchedMcpTools;

if (runContext && agent) {
const context = { runContext, agent, serverName: server.name };
const filteredTools: MCPTool[] = [];
for (const tool of fetchedMcpTools) {
const filter = server.toolFilter;
if (filter) {
if (typeof filter === 'function') {
const filtered = await filter(context, tool);
if (!filtered) {
globalLogger.debug(
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the callable filter.`,
);
continue;
}
} else {
const allowedToolNames = filter.allowedToolNames ?? [];
const blockedToolNames = filter.blockedToolNames ?? [];
if (allowedToolNames.length > 0 || blockedToolNames.length > 0) {
const allowed =
allowedToolNames.length > 0
? allowedToolNames.includes(tool.name)
: true;
const blocked =
blockedToolNames.length > 0
? blockedToolNames.includes(tool.name)
: false;
if (!allowed || blocked) {
if (blocked) {
globalLogger.debug(
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the static filter.`,
);
} else if (!allowed) {
globalLogger.debug(
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is not allowed by the static filter.`,
);
}
continue;
}
continue; // skip this tool
}
}
}
filteredTools.push(tool);
}
mcpTools.push(tool);
mcpTools = filteredTools;
}

span.spanData.result = mcpTools.map((t) => t.name);
const tools: FunctionTool<TContext, any, string>[] = mcpTools.map((t) =>
mcpToFunctionTool(t, server, convertSchemasToStrict),
Expand All @@ -400,21 +378,70 @@ async function getFunctionToolsFromServer<TContext = UnknownContext>(
);
}

/**
* Options for fetching MCP tools.
*/
export type GetAllMcpToolsOptions<TContext> = {
mcpServers: MCPServer[];
convertSchemasToStrict?: boolean;
runContext?: RunContext<TContext>;
agent?: Agent<TContext, any>;
};

/**
* Returns all MCP tools from the provided servers, using the function tool conversion.
* If runContext and agent are provided, callable tool filters will be applied.
*/
export async function getAllMcpTools<TContext = UnknownContext>(
mcpServers: MCPServer[],
runContext: RunContext<TContext>,
agent: Agent<TContext, any>,
): Promise<Tool<TContext>[]>;
export async function getAllMcpTools<TContext = UnknownContext>(
opts: GetAllMcpToolsOptions<TContext>,
): Promise<Tool<TContext>[]>;
export async function getAllMcpTools<TContext = UnknownContext>(
mcpServersOrOpts: MCPServer[] | GetAllMcpToolsOptions<TContext>,
runContext?: RunContext<TContext>,
agent?: Agent<TContext, any>,
convertSchemasToStrict = false,
): Promise<Tool<TContext>[]> {
return getAllMcpFunctionTools(
const opts = Array.isArray(mcpServersOrOpts)
? {
mcpServers: mcpServersOrOpts,
runContext,
agent,
convertSchemasToStrict,
}
: mcpServersOrOpts;

const {
mcpServers,
runContext,
agent,
convertSchemasToStrict,
);
convertSchemasToStrict: convertSchemasToStrictFromOpts = false,
runContext: runContextFromOpts,
agent: agentFromOpts,
} = opts;
const allTools: Tool<TContext>[] = [];
const toolNames = new Set<string>();

for (const server of mcpServers) {
const serverTools = await getFunctionToolsFromServer({
server,
convertSchemasToStrict: convertSchemasToStrictFromOpts,
runContext: runContextFromOpts,
agent: agentFromOpts,
});
const serverToolNames = new Set(serverTools.map((t) => t.name));
const intersection = [...serverToolNames].filter((n) => toolNames.has(n));
if (intersection.length > 0) {
throw new UserError(
`Duplicate tool names found across MCP servers: ${intersection.join(', ')}`,
);
}
for (const t of serverTools) {
toolNames.add(t.name);
allTools.push(t);
}
}
return allTools;
}

/**
Expand Down
Loading