From 48b4834cc815e214f3a7f033dafbd30496ca6557 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 11 Jun 2025 14:52:40 +0900 Subject: [PATCH 1/5] Add remote MCP server (Streamable HTTP) support; fixes #78 --- examples/mcp/streamable-http-example.ts | 31 +++++ packages/agents-core/src/index.ts | 1 + packages/agents-core/src/mcp.ts | 94 +++++++++++++- .../{mcp-stdio => mcp-server}/browser.ts | 26 ++++ .../shims/{mcp-stdio => mcp-server}/node.ts | 119 ++++++++++++++++++ .../agents-core/src/shims/shims-browser.ts | 2 +- packages/agents-core/src/shims/shims-node.ts | 5 +- .../agents-core/src/shims/shims-workerd.ts | 2 +- 8 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 examples/mcp/streamable-http-example.ts rename packages/agents-core/src/shims/{mcp-stdio => mcp-server}/browser.ts (50%) rename packages/agents-core/src/shims/{mcp-stdio => mcp-server}/node.ts (54%) diff --git a/examples/mcp/streamable-http-example.ts b/examples/mcp/streamable-http-example.ts new file mode 100644 index 00000000..2f04bbae --- /dev/null +++ b/examples/mcp/streamable-http-example.ts @@ -0,0 +1,31 @@ +import { Agent, run, MCPServerStreamableHttp, withTrace } from '@openai/agents'; + +async function main() { + const mcpServer = new MCPServerStreamableHttp({ + url: 'https://gitmcp.io/openai/codex', + name: 'GitMCP Documentation Server', + }); + const agent = new Agent({ + name: 'GitMCP Assistant', + instructions: 'Use the tools to respond to user requests.', + mcpServers: [mcpServer], + }); + + try { + await withTrace('GitMCP Documentation Server Example', async () => { + await mcpServer.connect(); + const result = await run( + agent, + 'Which language is this repo written in?', + ); + console.log(result.finalOutput ?? result.output ?? result); + }); + } finally { + await mcpServer.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/agents-core/src/index.ts b/packages/agents-core/src/index.ts index e2cfc772..00c5f0b4 100644 --- a/packages/agents-core/src/index.ts +++ b/packages/agents-core/src/index.ts @@ -71,6 +71,7 @@ export { invalidateServerToolsCache, MCPServer, MCPServerStdio, + MCPServerStreamableHttp, } from './mcp'; export { Model, diff --git a/packages/agents-core/src/mcp.ts b/packages/agents-core/src/mcp.ts index 416bfc87..680b791c 100644 --- a/packages/agents-core/src/mcp.ts +++ b/packages/agents-core/src/mcp.ts @@ -1,6 +1,9 @@ import { FunctionTool, tool, Tool } from './tool'; import { UserError } from './errors'; -import { MCPServerStdio as UnderlyingMCPServerStdio } from '@openai/agents-core/_shims'; +import { + MCPServerStdio as UnderlyingMCPServerStdio, + MCPServerStreamableHttp as UnderlyingMCPServerStreamableHttp, +} from '@openai/agents-core/_shims'; import { getCurrentSpan, withMCPListToolsSpan } from './tracing'; import { logger as globalLogger, getLogger, Logger } from './logger'; import debug from 'debug'; @@ -15,6 +18,9 @@ import { export const DEFAULT_STDIO_MCP_CLIENT_LOGGER_NAME = 'openai-agents:stdio-mcp-client'; +export const DEFAULT_STREAMABLE_HTTP_MCP_CLIENT_LOGGER_NAME = + 'openai-agents:streamable-http-mcp-client'; + /** * Interface for MCP server implementations. * Provides methods for connecting, listing tools, calling tools, and cleanup. @@ -63,6 +69,39 @@ export abstract class BaseMCPServerStdio implements MCPServer { } } +export abstract class BaseMCPServerStreamableHttp implements MCPServer { + public cacheToolsList: boolean; + protected _cachedTools: any[] | undefined = undefined; + + protected logger: Logger; + constructor(options: MCPServerStreamableHttpOptions) { + this.logger = + options.logger ?? + getLogger(DEFAULT_STREAMABLE_HTTP_MCP_CLIENT_LOGGER_NAME); + this.cacheToolsList = options.cacheToolsList ?? true; + } + + abstract get name(): string; + abstract connect(): Promise; + abstract close(): Promise; + abstract listTools(): Promise; + abstract callTool( + _toolName: string, + _args: Record | null, + ): Promise; + + /** + * Logs a debug message when debug logging is enabled. + * @param buildMessage A function that returns the message to log. + */ + protected debugLog(buildMessage: () => string): void { + if (debug.enabled(this.logger.namespace)) { + // only when this is true, the function to build the string is called + this.logger.debug(buildMessage()); + } + } +} + /** * Minimum MCP tool data definition. * This type definition does not intend to cover all possible properties. @@ -116,6 +155,40 @@ export class MCPServerStdio extends BaseMCPServerStdio { return this.underlying.callTool(toolName, args); } } + +export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { + private underlying: UnderlyingMCPServerStreamableHttp; + constructor(options: MCPServerStreamableHttpOptions) { + super(options); + this.underlying = new UnderlyingMCPServerStreamableHttp(options); + } + get name(): string { + return this.underlying.name; + } + connect(): Promise { + return this.underlying.connect(); + } + close(): Promise { + return this.underlying.close(); + } + async listTools(): Promise { + if (this.cacheToolsList && this._cachedTools) { + return this._cachedTools; + } + const tools = await this.underlying.listTools(); + if (this.cacheToolsList) { + this._cachedTools = tools; + } + return tools; + } + callTool( + toolName: string, + args: Record | null, + ): Promise { + return this.underlying.callTool(toolName, args); + } +} + /** * Fetches and flattens all tools from multiple MCP servers. * Logs and skips any servers that fail to respond. @@ -292,6 +365,25 @@ export type MCPServerStdioOptions = | DefaultMCPServerStdioOptions | FullCommandMCPServerStdioOptions; +export interface MCPServerStreamableHttpOptions { + url: string; + cacheToolsList?: boolean; + clientSessionTimeoutSeconds?: number; + name?: string; + logger?: Logger; + + // ---------------------------------------------------- + // OAuth + // import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; + authProvider?: any; + // RequestInit + requestInit?: any; + // import { StreamableHTTPReconnectionOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + reconnectionOptions?: any; + sessionId?: string; + // ---------------------------------------------------- +} + /** * Represents a JSON-RPC request message. */ diff --git a/packages/agents-core/src/shims/mcp-stdio/browser.ts b/packages/agents-core/src/shims/mcp-server/browser.ts similarity index 50% rename from packages/agents-core/src/shims/mcp-stdio/browser.ts rename to packages/agents-core/src/shims/mcp-server/browser.ts index 91859259..7d9e7fbc 100644 --- a/packages/agents-core/src/shims/mcp-stdio/browser.ts +++ b/packages/agents-core/src/shims/mcp-server/browser.ts @@ -1,7 +1,9 @@ import { BaseMCPServerStdio, + BaseMCPServerStreamableHttp, CallToolResultContent, MCPServerStdioOptions, + MCPServerStreamableHttpOptions, MCPTool, } from '../../mcp'; @@ -28,3 +30,27 @@ export class MCPServerStdio extends BaseMCPServerStdio { throw new Error('Method not implemented.'); } } + +export class MCPServerStreamableHttp extends BaseMCPServerStreamableHttp { + constructor(params: MCPServerStreamableHttpOptions) { + super(params); + } + get name(): string { + return 'MCPServerStdio'; + } + connect(): Promise { + throw new Error('Method not implemented.'); + } + close(): Promise { + throw new Error('Method not implemented.'); + } + listTools(): Promise { + throw new Error('Method not implemented.'); + } + callTool( + _toolName: string, + _args: Record | null, + ): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/agents-core/src/shims/mcp-stdio/node.ts b/packages/agents-core/src/shims/mcp-server/node.ts similarity index 54% rename from packages/agents-core/src/shims/mcp-stdio/node.ts rename to packages/agents-core/src/shims/mcp-server/node.ts index 9ede6d5c..42fc6707 100644 --- a/packages/agents-core/src/shims/mcp-stdio/node.ts +++ b/packages/agents-core/src/shims/mcp-server/node.ts @@ -2,10 +2,12 @@ import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { BaseMCPServerStdio, + BaseMCPServerStreamableHttp, CallToolResultContent, DefaultMCPServerStdioOptions, InitializeResult, MCPServerStdioOptions, + MCPServerStreamableHttpOptions, MCPTool, invalidateServerToolsCache, } from '../../mcp'; @@ -154,3 +156,120 @@ export class NodeMCPServerStdio extends BaseMCPServerStdio { } } } + +export class NodeMCPServerStreamableHttp extends BaseMCPServerStreamableHttp { + protected session: Client | null = null; + protected _cacheDirty = true; + protected _toolsList: any[] = []; + protected serverInitializeResult: InitializeResult | null = null; + protected clientSessionTimeoutSeconds?: number; + + params: MCPServerStreamableHttpOptions; + private _name: string; + private transport: any = null; + + constructor(params: MCPServerStreamableHttpOptions) { + super(params); + this.clientSessionTimeoutSeconds = params.clientSessionTimeoutSeconds ?? 5; + this.params = params; + this._name = params.name || `streamable-http: ${this.params.url}`; + } + + async connect(): Promise { + try { + const { StreamableHTTPClientTransport } = await import( + '@modelcontextprotocol/sdk/client/streamableHttp.js' + ).catch(failedToImport); + const { Client } = await import( + '@modelcontextprotocol/sdk/client/index.js' + ).catch(failedToImport); + this.transport = new StreamableHTTPClientTransport( + new URL(this.params.url), + { + authProvider: this.params.authProvider, + requestInit: this.params.requestInit, + reconnectionOptions: this.params.reconnectionOptions, + sessionId: this.params.sessionId, + }, + ); + this.session = new Client({ + name: this._name, + version: '1.0.0', // You may want to make this configurable + }); + await this.session.connect(this.transport); + this.serverInitializeResult = { + serverInfo: { name: this._name, version: '1.0.0' }, + } as InitializeResult; + } catch (e) { + this.logger.error('Error initializing MCP server:', e); + await this.close(); + throw e; + } + this.debugLog(() => `Connected to MCP server: ${this._name}`); + } + + invalidateToolsCache() { + invalidateServerToolsCache(this.name); + this._cacheDirty = true; + } + + // The response element type is intentionally left as `any` to avoid explosing MCP SDK type dependencies. + async listTools(): Promise { + const { ListToolsResultSchema } = await import( + '@modelcontextprotocol/sdk/types.js' + ).catch(failedToImport); + if (!this.session) { + throw new Error( + 'Server not initialized. Make sure you call connect() first.', + ); + } + if (this.cacheToolsList && !this._cacheDirty && this._toolsList) { + return this._toolsList; + } + this._cacheDirty = false; + const response = await this.session.listTools(); + this.debugLog(() => `Listed tools: ${JSON.stringify(response)}`); + this._toolsList = ListToolsResultSchema.parse(response).tools; + return this._toolsList; + } + + async callTool( + toolName: string, + args: Record | null, + ): Promise { + const { CallToolResultSchema } = await import( + '@modelcontextprotocol/sdk/types.js' + ).catch(failedToImport); + if (!this.session) { + throw new Error( + 'Server not initialized. Make sure you call connect() first.', + ); + } + const response = await this.session.callTool({ + name: toolName, + arguments: args ?? {}, + }); + const parsed = CallToolResultSchema.parse(response); + const result = parsed.content; + this.debugLog( + () => + `Called tool ${toolName} (args: ${JSON.stringify(args)}, result: ${JSON.stringify(result)})`, + ); + return result as CallToolResultContent; + } + + get name() { + return this._name; + } + + async close(): Promise { + if (this.transport) { + await this.transport.close(); + this.transport = null; + } + if (this.session) { + await this.session.close(); + this.session = null; + } + } +} diff --git a/packages/agents-core/src/shims/shims-browser.ts b/packages/agents-core/src/shims/shims-browser.ts index 19bbec04..d265af56 100644 --- a/packages/agents-core/src/shims/shims-browser.ts +++ b/packages/agents-core/src/shims/shims-browser.ts @@ -109,7 +109,7 @@ export function isTracingLoopRunningByDefault(): boolean { return false; } -export { MCPServerStdio } from './mcp-stdio/browser'; +export { MCPServerStdio, MCPServerStreamableHttp } from './mcp-server/browser'; class BrowserTimer implements Timer { constructor() {} diff --git a/packages/agents-core/src/shims/shims-node.ts b/packages/agents-core/src/shims/shims-node.ts index a0035585..ec0608bf 100644 --- a/packages/agents-core/src/shims/shims-node.ts +++ b/packages/agents-core/src/shims/shims-node.ts @@ -41,7 +41,10 @@ export function isTracingLoopRunningByDefault(): boolean { export function isBrowserEnvironment(): boolean { return false; } -export { NodeMCPServerStdio as MCPServerStdio } from './mcp-stdio/node'; +export { + NodeMCPServerStdio as MCPServerStdio, + NodeMCPServerStreamableHttp as MCPServerStreamableHttp, +} from './mcp-server/node'; export { clearTimeout } from 'node:timers'; diff --git a/packages/agents-core/src/shims/shims-workerd.ts b/packages/agents-core/src/shims/shims-workerd.ts index e77a0095..ffdb03e7 100644 --- a/packages/agents-core/src/shims/shims-workerd.ts +++ b/packages/agents-core/src/shims/shims-workerd.ts @@ -54,7 +54,7 @@ export function isTracingLoopRunningByDefault(): boolean { /** * Right now Cloudflare Workers does not support MCP */ -export { MCPServerStdio } from './mcp-stdio/browser'; +export { MCPServerStdio, MCPServerStreamableHttp } from './mcp-server/browser'; export { clearTimeout, setTimeout } from 'node:timers'; From 86e0c041d18a09ac920d5ab853d6af2aed55a0f6 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 11 Jun 2025 14:59:07 +0900 Subject: [PATCH 2/5] Fix tests --- packages/agents-core/test/mcpCache.test.ts | 19 +++++++++++++++---- .../{mcp-stdio => mcp-server}/browser.test.ts | 2 +- .../{mcp-stdio => mcp-server}/node.test.ts | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) rename packages/agents-core/test/shims/{mcp-stdio => mcp-server}/browser.test.ts (82%) rename packages/agents-core/test/shims/{mcp-stdio => mcp-server}/node.test.ts (96%) diff --git a/packages/agents-core/test/mcpCache.test.ts b/packages/agents-core/test/mcpCache.test.ts index ddb12146..67d789f5 100644 --- a/packages/agents-core/test/mcpCache.test.ts +++ b/packages/agents-core/test/mcpCache.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { getAllMcpTools } from '../src/mcp'; import { withTrace } from '../src/tracing'; -import { NodeMCPServerStdio } from '../src/shims/mcp-stdio/node'; +import { NodeMCPServerStdio } from '../src/shims/mcp-server/node'; import type { CallToolResultContent } from '../src/mcp'; class StubServer extends NodeMCPServerStdio { @@ -20,7 +20,10 @@ class StubServer extends NodeMCPServerStdio { this._toolsList = this.toolList; return this.toolList; } - async callTool(_toolName: string, _args: Record | null): Promise { + async callTool( + _toolName: string, + _args: Record | null, + ): Promise { return []; } } @@ -29,10 +32,18 @@ describe('MCP tools cache invalidation', () => { it('fetches fresh tools after cache invalidation', async () => { await withTrace('test', async () => { const toolsA = [ - { name: 'a', description: '', inputSchema: { type: 'object', properties: {} } }, + { + name: 'a', + description: '', + inputSchema: { type: 'object', properties: {} }, + }, ]; const toolsB = [ - { name: 'b', description: '', inputSchema: { type: 'object', properties: {} } }, + { + name: 'b', + description: '', + inputSchema: { type: 'object', properties: {} }, + }, ]; const server = new StubServer('server', toolsA); diff --git a/packages/agents-core/test/shims/mcp-stdio/browser.test.ts b/packages/agents-core/test/shims/mcp-server/browser.test.ts similarity index 82% rename from packages/agents-core/test/shims/mcp-stdio/browser.test.ts rename to packages/agents-core/test/shims/mcp-server/browser.test.ts index 632669ed..98b43ad2 100644 --- a/packages/agents-core/test/shims/mcp-stdio/browser.test.ts +++ b/packages/agents-core/test/shims/mcp-server/browser.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { MCPServerStdio } from '../../../src/shims/mcp-stdio/browser'; +import { MCPServerStdio } from '../../../src/shims/mcp-server/browser'; describe('MCPServerStdio', () => { test('should be available', async () => { diff --git a/packages/agents-core/test/shims/mcp-stdio/node.test.ts b/packages/agents-core/test/shims/mcp-server/node.test.ts similarity index 96% rename from packages/agents-core/test/shims/mcp-stdio/node.test.ts rename to packages/agents-core/test/shims/mcp-server/node.test.ts index 762ff348..13709bf6 100644 --- a/packages/agents-core/test/shims/mcp-stdio/node.test.ts +++ b/packages/agents-core/test/shims/mcp-server/node.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, vi, afterAll, beforeAll } from 'vitest'; -import { NodeMCPServerStdio } from '../../../src/shims/mcp-stdio/node'; +import { NodeMCPServerStdio } from '../../../src/shims/mcp-server/node'; import { TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport'; import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types'; From 5c00a8680d02a144d3c3c668b009c41352982a98 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 11 Jun 2025 15:00:09 +0900 Subject: [PATCH 3/5] Add npm command --- examples/mcp/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 92a08322..045ad4a0 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -9,6 +9,7 @@ "scripts": { "build-check": "tsc --noEmit", "start:stdio": "tsx filesystem-example.ts", + "start:streamable-http": "tsx streamable-http-example.ts", "start:hosted-mcp-on-approval": "tsx hosted-mcp-on-approval.ts", "start:hosted-mcp-human-in-the-loop": "tsx hosted-mcp-human-in-the-loop.ts", "start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts" From df4fdfe6947a926ff9f0f88c60bb6176d2b9a10b Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 11 Jun 2025 15:02:46 +0900 Subject: [PATCH 4/5] Add changeset --- .changeset/crazy-coats-give.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/crazy-coats-give.md diff --git a/.changeset/crazy-coats-give.md b/.changeset/crazy-coats-give.md new file mode 100644 index 00000000..35d0bdf0 --- /dev/null +++ b/.changeset/crazy-coats-give.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +Add remote MCP server (Streamable HTTP) support From dc6843ea6c10fb3bfd087983215fe8559f82d63f Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 11 Jun 2025 17:59:16 +0900 Subject: [PATCH 5/5] simplify the example --- examples/mcp/streamable-http-example.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mcp/streamable-http-example.ts b/examples/mcp/streamable-http-example.ts index 2f04bbae..5a685337 100644 --- a/examples/mcp/streamable-http-example.ts +++ b/examples/mcp/streamable-http-example.ts @@ -18,7 +18,7 @@ async function main() { agent, 'Which language is this repo written in?', ); - console.log(result.finalOutput ?? result.output ?? result); + console.log(result.finalOutput); }); } finally { await mcpServer.close();