Skip to content

Commit b94432b

Browse files
authored
fix: #683 Failing to run MCP servers when deserializing run state data (#685)
1 parent d84976a commit b94432b

File tree

4 files changed

+185
-55
lines changed

4 files changed

+185
-55
lines changed

.changeset/loud-emus-rule.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+
fix: #683 Failing to run MCP servers when deserializing run state data

packages/agents-core/src/mcp.ts

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import {
55
MCPServerStreamableHttp as UnderlyingMCPServerStreamableHttp,
66
MCPServerSSE as UnderlyingMCPServerSSE,
77
} from '@openai/agents-core/_shims';
8-
import { getCurrentSpan, withMCPListToolsSpan } from './tracing';
8+
import {
9+
getCurrentSpan,
10+
getCurrentTrace,
11+
withMCPListToolsSpan,
12+
type MCPListToolsSpanData,
13+
type Span,
14+
} from './tracing';
915
import { logger as globalLogger, getLogger, Logger } from './logger';
1016
import debug from 'debug';
1117
import { z } from 'zod';
@@ -314,68 +320,78 @@ async function getFunctionToolsFromServer<TContext = UnknownContext>({
314320
mcpToFunctionTool(t, server, convertSchemasToStrict),
315321
);
316322
}
317-
return withMCPListToolsSpan(
318-
async (span) => {
319-
const fetchedMcpTools = await server.listTools();
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;
323+
324+
const listToolsForServer = async (
325+
span?: Span<MCPListToolsSpanData>,
326+
): Promise<FunctionTool<TContext, any, unknown>[]> => {
327+
const fetchedMcpTools = await server.listTools();
328+
let mcpTools: MCPTool[] = fetchedMcpTools;
329+
330+
if (runContext && agent) {
331+
const context = { runContext, agent, serverName: server.name };
332+
const filteredTools: MCPTool[] = [];
333+
for (const tool of fetchedMcpTools) {
334+
const filter = server.toolFilter;
335+
if (filter) {
336+
if (typeof filter === 'function') {
337+
const filtered = await filter(context, tool);
338+
if (!filtered) {
339+
globalLogger.debug(
340+
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the callable filter.`,
341+
);
342+
continue;
343+
}
344+
} else {
345+
const allowedToolNames = filter.allowedToolNames ?? [];
346+
const blockedToolNames = filter.blockedToolNames ?? [];
347+
if (allowedToolNames.length > 0 || blockedToolNames.length > 0) {
348+
const allowed =
349+
allowedToolNames.length > 0
350+
? allowedToolNames.includes(tool.name)
351+
: true;
352+
const blocked =
353+
blockedToolNames.length > 0
354+
? blockedToolNames.includes(tool.name)
355+
: false;
356+
if (!allowed || blocked) {
357+
if (blocked) {
358+
globalLogger.debug(
359+
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is blocked by the static filter.`,
360+
);
361+
} else if (!allowed) {
362+
globalLogger.debug(
363+
`MCP Tool (server: ${server.name}, tool: ${tool.name}) is not allowed by the static filter.`,
364+
);
359365
}
366+
continue;
360367
}
361368
}
362369
}
363-
filteredTools.push(tool);
364370
}
365-
mcpTools = filteredTools;
371+
filteredTools.push(tool);
366372
}
373+
mcpTools = filteredTools;
374+
}
367375

376+
if (span) {
368377
span.spanData.result = mcpTools.map((t) => t.name);
369-
const tools: FunctionTool<TContext, any, string>[] = mcpTools.map((t) =>
370-
mcpToFunctionTool(t, server, convertSchemasToStrict),
371-
);
372-
if (server.cacheToolsList) {
373-
_cachedTools[server.name] = mcpTools;
374-
}
375-
return tools;
376-
},
377-
{ data: { server: server.name } },
378-
);
378+
}
379+
const tools: FunctionTool<TContext, any, string>[] = mcpTools.map((t) =>
380+
mcpToFunctionTool(t, server, convertSchemasToStrict),
381+
);
382+
if (server.cacheToolsList) {
383+
_cachedTools[server.name] = mcpTools;
384+
}
385+
return tools;
386+
};
387+
388+
if (!getCurrentTrace()) {
389+
return listToolsForServer();
390+
}
391+
392+
return withMCPListToolsSpan(listToolsForServer, {
393+
data: { server: server.name },
394+
});
379395
}
380396

381397
/**

packages/agents-core/src/tracing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {
1919
export { NoopSpan, Span } from './spans';
2020
export { NoopTrace, Trace } from './traces';
2121
export { generateGroupId, generateSpanId, generateTraceId } from './utils';
22+
export type { MCPListToolsSpanData } from './spans';
2223

2324
/**
2425
* Add a processor to the list of processors. Each processor will receive all traces/spans.

packages/agents-core/test/runState.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import {
2020
FakeShell,
2121
FakeEditor,
2222
} from './stubs';
23+
import { createAgentSpan } from '../src/tracing';
24+
import { getGlobalTraceProvider } from '../src/tracing/provider';
25+
import type { MCPServer, MCPTool } from '../src/mcp';
2326

2427
describe('RunState', () => {
2528
it('initializes with default values', () => {
@@ -353,6 +356,111 @@ describe('deserialize helpers', () => {
353356
).toBe(editorTool);
354357
});
355358

359+
it('fromString tolerates agents gaining MCP servers after serialization', async () => {
360+
const agentWithoutMcp = new Agent({ name: 'McpLite' });
361+
const state = new RunState(new RunContext(), 'input', agentWithoutMcp, 1);
362+
state._lastProcessedResponse = {
363+
newItems: [],
364+
functions: [],
365+
handoffs: [],
366+
computerActions: [],
367+
shellActions: [],
368+
applyPatchActions: [],
369+
mcpApprovalRequests: [],
370+
toolsUsed: [],
371+
hasToolsOrApprovalsToRun: () => false,
372+
};
373+
374+
const serialized = state.toString();
375+
376+
const stubMcpServer: MCPServer = {
377+
name: 'stub-server',
378+
cacheToolsList: false,
379+
toolFilter: undefined,
380+
async connect() {},
381+
async close() {},
382+
async listTools() {
383+
return [];
384+
},
385+
async callTool() {
386+
return [];
387+
},
388+
async invalidateToolsCache() {},
389+
};
390+
391+
const agentWithMcp = new Agent({
392+
name: 'McpLite',
393+
mcpServers: [stubMcpServer],
394+
});
395+
396+
const restored = await RunState.fromString(agentWithMcp, serialized);
397+
expect(restored._currentAgent.mcpServers).toHaveLength(1);
398+
expect(restored._lastProcessedResponse?.hasToolsOrApprovalsToRun()).toBe(
399+
false,
400+
);
401+
});
402+
403+
it('fromString tolerates serialized traces with new MCP servers', async () => {
404+
const traceProvider = getGlobalTraceProvider();
405+
const trace = traceProvider.createTrace({ name: 'restore-with-trace' });
406+
const agentWithoutMcp = new Agent({ name: 'McpTracey' });
407+
const state = new RunState(new RunContext(), 'input', agentWithoutMcp, 1);
408+
state._trace = trace;
409+
state._currentAgentSpan = createAgentSpan(
410+
{ data: { name: agentWithoutMcp.name } },
411+
trace,
412+
);
413+
state._lastProcessedResponse = {
414+
newItems: [],
415+
functions: [],
416+
handoffs: [],
417+
computerActions: [],
418+
shellActions: [],
419+
applyPatchActions: [],
420+
mcpApprovalRequests: [],
421+
toolsUsed: [],
422+
hasToolsOrApprovalsToRun: () => false,
423+
};
424+
425+
const serialized = state.toString();
426+
427+
let listCalled = false;
428+
const stubMcpTool: MCPTool = {
429+
name: 'sample_tool',
430+
description: '',
431+
inputSchema: {
432+
type: 'object',
433+
properties: {},
434+
required: [],
435+
additionalProperties: false,
436+
},
437+
};
438+
const stubMcpServer: MCPServer = {
439+
name: 'stub-traced-server',
440+
cacheToolsList: false,
441+
toolFilter: undefined,
442+
async connect() {},
443+
async close() {},
444+
async listTools() {
445+
listCalled = true;
446+
return [stubMcpTool];
447+
},
448+
async callTool() {
449+
return [];
450+
},
451+
async invalidateToolsCache() {},
452+
};
453+
454+
const agentWithMcp = new Agent({
455+
name: 'McpTracey',
456+
mcpServers: [stubMcpServer],
457+
});
458+
459+
const restored = await RunState.fromString(agentWithMcp, serialized);
460+
expect(restored._currentAgent.mcpServers).toHaveLength(1);
461+
expect(listCalled).toBe(true);
462+
});
463+
356464
it('deserializeProcessedResponse restores currentStep', async () => {
357465
const tool = computerTool({ computer: new FakeComputer() });
358466
const agent = new Agent({ name: 'Comp', tools: [tool] });

0 commit comments

Comments
 (0)