Skip to content
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