Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/mcp-structured-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@openai/agents-core': patch
---

feat(mcp): add structuredContent support behind `useStructuredContent`; return full CallToolResult from `callTool`

- `MCPServer#callTool` now returns the full `CallToolResult` (was `content[]`), exposing optional `structuredContent`.
- Add `useStructuredContent` option to MCP servers (stdio/streamable-http/SSE), default `false` to avoid duplicate data by default.
- When enabled, function tool outputs return JSON strings for consistency with Python SDK implementation.
86 changes: 77 additions & 9 deletions packages/agents-core/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,35 @@ export const DEFAULT_SSE_MCP_CLIENT_LOGGER_NAME =
export interface MCPServer {
cacheToolsList: boolean;
toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic;

/**
* Whether to include structuredContent in tool outputs when available.
*/
useStructuredContent?: boolean;
connect(): Promise<void>;
readonly name: string;
close(): Promise<void>;
listTools(): Promise<MCPTool[]>;
callTool(
toolName: string,
args: Record<string, unknown> | null,
): Promise<CallToolResultContent>;
): Promise<CallToolResult>;
invalidateToolsCache(): Promise<void>;
}

export abstract class BaseMCPServerStdio implements MCPServer {
public cacheToolsList: boolean;
protected _cachedTools: any[] | undefined = undefined;
public toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic;
public useStructuredContent?: boolean;

protected logger: Logger;
constructor(options: MCPServerStdioOptions) {
this.logger =
options.logger ?? getLogger(DEFAULT_STDIO_MCP_CLIENT_LOGGER_NAME);
this.cacheToolsList = options.cacheToolsList ?? false;
this.toolFilter = options.toolFilter;
this.useStructuredContent = options.useStructuredContent ?? false;
}

abstract get name(): string;
Expand All @@ -66,7 +73,7 @@ export abstract class BaseMCPServerStdio implements MCPServer {
abstract callTool(
_toolName: string,
_args: Record<string, unknown> | null,
): Promise<CallToolResultContent>;
): Promise<CallToolResult>;
abstract invalidateToolsCache(): Promise<void>;

/**
Expand All @@ -85,6 +92,7 @@ export abstract class BaseMCPServerStreamableHttp implements MCPServer {
public cacheToolsList: boolean;
protected _cachedTools: any[] | undefined = undefined;
public toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic;
public useStructuredContent?: boolean;

protected logger: Logger;
constructor(options: MCPServerStreamableHttpOptions) {
Expand All @@ -93,6 +101,7 @@ export abstract class BaseMCPServerStreamableHttp implements MCPServer {
getLogger(DEFAULT_STREAMABLE_HTTP_MCP_CLIENT_LOGGER_NAME);
this.cacheToolsList = options.cacheToolsList ?? false;
this.toolFilter = options.toolFilter;
this.useStructuredContent = options.useStructuredContent ?? false;
}

abstract get name(): string;
Expand All @@ -102,7 +111,7 @@ export abstract class BaseMCPServerStreamableHttp implements MCPServer {
abstract callTool(
_toolName: string,
_args: Record<string, unknown> | null,
): Promise<CallToolResultContent>;
): Promise<CallToolResult>;
abstract invalidateToolsCache(): Promise<void>;

/**
Expand All @@ -121,13 +130,15 @@ export abstract class BaseMCPServerSSE implements MCPServer {
public cacheToolsList: boolean;
protected _cachedTools: any[] | undefined = undefined;
public toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic;
public useStructuredContent?: boolean;

protected logger: Logger;
constructor(options: MCPServerSSEOptions) {
this.logger =
options.logger ?? getLogger(DEFAULT_SSE_MCP_CLIENT_LOGGER_NAME);
this.cacheToolsList = options.cacheToolsList ?? false;
this.toolFilter = options.toolFilter;
this.useStructuredContent = options.useStructuredContent ?? false;
}

abstract get name(): string;
Expand All @@ -137,7 +148,7 @@ export abstract class BaseMCPServerSSE implements MCPServer {
abstract callTool(
_toolName: string,
_args: Record<string, unknown> | null,
): Promise<CallToolResultContent>;
): Promise<CallToolResult>;
abstract invalidateToolsCache(): Promise<void>;

/**
Expand Down Expand Up @@ -201,7 +212,7 @@ export class MCPServerStdio extends BaseMCPServerStdio {
callTool(
toolName: string,
args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
return this.underlying.callTool(toolName, args);
}
invalidateToolsCache(): Promise<void> {
Expand Down Expand Up @@ -237,7 +248,7 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp {
callTool(
toolName: string,
args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
return this.underlying.callTool(toolName, args);
}
invalidateToolsCache(): Promise<void> {
Expand Down Expand Up @@ -273,7 +284,7 @@ export class MCPServerSSE extends BaseMCPServerSSE {
callTool(
toolName: string,
args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
return this.underlying.callTool(toolName, args);
}
invalidateToolsCache(): Promise<void> {
Expand Down Expand Up @@ -446,6 +457,7 @@ export async function getAllMcpTools<TContext = UnknownContext>(

/**
* Converts an MCP tool definition to a function tool for the Agents SDK.
* When useStructuredContent is enabled, returns JSON strings for consistency with Python SDK.
*/
export function mcpToFunctionTool(
mcpTool: MCPTool,
Expand All @@ -463,8 +475,36 @@ export function mcpToFunctionTool(
if (currentSpan) {
currentSpan.spanData['mcp_data'] = { server: server.name };
}
const content = await server.callTool(mcpTool.name, args);
return content.length === 1 ? content[0] : content;
const result = await server.callTool(mcpTool.name, args);

if (result.content && result.content.length === 1) {
if (
server.useStructuredContent &&
'structuredContent' in result &&
result.structuredContent !== undefined
) {
return JSON.stringify([result.content[0], result.structuredContent]);
}
return result.content[0];
} else if (result.content && result.content.length > 1) {
if (
server.useStructuredContent &&
'structuredContent' in result &&
result.structuredContent !== undefined
) {
const outputs = [...result.content, result.structuredContent];
return JSON.stringify(outputs);
}
return result.content;
} else if (
server.useStructuredContent &&
'structuredContent' in result &&
result.structuredContent !== undefined
) {
return JSON.stringify(result.structuredContent);
}
// Preserve backward compatibility: return empty array when no content
return result.content || [];
}

const schema: JsonObjectSchema<any> = {
Expand Down Expand Up @@ -533,6 +573,11 @@ export interface BaseMCPServerStdioOptions {
encodingErrorHandler?: 'strict' | 'ignore' | 'replace';
logger?: Logger;
toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic;

/**
* Whether to include structuredContent in tool outputs when available.
*/
useStructuredContent?: boolean;
timeout?: number;
}
export interface DefaultMCPServerStdioOptions
Expand All @@ -555,6 +600,11 @@ export interface MCPServerStreamableHttpOptions {
name?: string;
logger?: Logger;
toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic;

/**
* Whether to include structuredContent in tool outputs when available.
*/
useStructuredContent?: boolean;
timeout?: number;

// ----------------------------------------------------
Expand All @@ -579,6 +629,11 @@ export interface MCPServerSSEOptions {
name?: string;
logger?: Logger;
toolFilter?: MCPToolFilterCallable | MCPToolFilterStatic;

/**
* Whether to include structuredContent in tool outputs when available.
*/
useStructuredContent?: boolean;
timeout?: number;

// ----------------------------------------------------
Expand Down Expand Up @@ -621,9 +676,22 @@ export interface JsonRpcResponse {
error?: any;
}

/**
* Structured content that can be returned by MCP tools.
* Supports various data types including objects, arrays, primitives, and null.
*/
export type StructuredContent =
| Record<string, unknown>
| unknown[]
| string
| number
| boolean
| null;

export interface CallToolResponse extends JsonRpcResponse {
result: {
content: { type: string; text: string }[];
structuredContent?: StructuredContent;
};
}
export type CallToolResult = CallToolResponse['result'];
Expand Down
8 changes: 4 additions & 4 deletions packages/agents-core/src/shims/mcp-server/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
BaseMCPServerSSE,
BaseMCPServerStdio,
BaseMCPServerStreamableHttp,
CallToolResultContent,
CallToolResult,
MCPServerSSEOptions,
MCPServerStdioOptions,
MCPServerStreamableHttpOptions,
Expand All @@ -28,7 +28,7 @@ export class MCPServerStdio extends BaseMCPServerStdio {
callTool(
_toolName: string,
_args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
throw new Error('Method not implemented.');
}
invalidateToolsCache(): Promise<void> {
Expand All @@ -55,7 +55,7 @@ export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp {
callTool(
_toolName: string,
_args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
throw new Error('Method not implemented.');
}
invalidateToolsCache(): Promise<void> {
Expand Down Expand Up @@ -84,7 +84,7 @@ export class MCPServerSSE extends BaseMCPServerSSE {
callTool(
_toolName: string,
_args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
throw new Error('Method not implemented.');
}

Expand Down
26 changes: 13 additions & 13 deletions packages/agents-core/src/shims/mcp-server/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
BaseMCPServerStdio,
BaseMCPServerStreamableHttp,
BaseMCPServerSSE,
CallToolResultContent,
CallToolResult,
DefaultMCPServerStdioOptions,
InitializeResult,
MCPServerStdioOptions,
Expand Down Expand Up @@ -124,7 +124,7 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio {
async callTool(
toolName: string,
args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
const { CallToolResultSchema } = await import(
'@modelcontextprotocol/sdk/types.js'
).catch(failedToImport);
Expand All @@ -144,12 +144,12 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio {
},
);
const parsed = CallToolResultSchema.parse(response);
const result = parsed.content;
const result = parsed;
this.debugLog(
() =>
`Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`,
`Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result.content)})`,
);
return result as CallToolResultContent;
return result as CallToolResult;
}

get name() {
Expand Down Expand Up @@ -245,7 +245,7 @@ export class NodeMCPServerSSE extends BaseMCPServerSSE {
async callTool(
toolName: string,
args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
const { CallToolResultSchema } = await import(
'@modelcontextprotocol/sdk/types.js'
).catch(failedToImport);
Expand All @@ -265,12 +265,12 @@ export class NodeMCPServerSSE extends BaseMCPServerSSE {
},
);
const parsed = CallToolResultSchema.parse(response);
const result = parsed.content;
const result = parsed;
this.debugLog(
() =>
`Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`,
`Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result.content)})`,
);
return result as CallToolResultContent;
return result as CallToolResult;
}

get name() {
Expand Down Expand Up @@ -371,7 +371,7 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp {
async callTool(
toolName: string,
args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
): Promise<CallToolResult> {
const { CallToolResultSchema } = await import(
'@modelcontextprotocol/sdk/types.js'
).catch(failedToImport);
Expand All @@ -391,12 +391,12 @@ export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp {
},
);
const parsed = CallToolResultSchema.parse(response);
const result = parsed.content;
const result = parsed;
this.debugLog(
() =>
`Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`,
`Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result.content)})`,
);
return result as CallToolResultContent;
return result as CallToolResult;
}

get name() {
Expand Down
6 changes: 3 additions & 3 deletions packages/agents-core/test/mcpCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getAllMcpTools } from '../src/mcp';
import type { FunctionTool } from '../src/tool';
import { withTrace } from '../src/tracing';
import { NodeMCPServerStdio } from '../src/shims/mcp-server/node';
import type { CallToolResultContent } from '../src/mcp';
import type { CallToolResult } from '../src/mcp';
import { RunContext } from '../src/runContext';
import { Agent } from '../src/agent';

Expand All @@ -27,8 +27,8 @@ class StubServer extends NodeMCPServerStdio {
async callTool(
_toolName: string,
_args: Record<string, unknown> | null,
): Promise<CallToolResultContent> {
return [];
): Promise<CallToolResult> {
return { content: [] };
}
}

Expand Down
Loading