diff --git a/.changeset/gentle-snakes-flash.md b/.changeset/gentle-snakes-flash.md new file mode 100644 index 000000000000..603c6c699b0a --- /dev/null +++ b/.changeset/gentle-snakes-flash.md @@ -0,0 +1,5 @@ +--- +"@ai-sdk/mcp": patch +--- + +feat(mcp): surface 'serverInfo' exposed from the MCP server diff --git a/examples/mcp/src/server-info/client.ts b/examples/mcp/src/server-info/client.ts new file mode 100644 index 000000000000..9a0c624a3faf --- /dev/null +++ b/examples/mcp/src/server-info/client.ts @@ -0,0 +1,16 @@ +import { createMCPClient } from '@ai-sdk/mcp'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +async function main() { + const client = await createMCPClient({ + transport: new StreamableHTTPClientTransport( + new URL('http://localhost:3000/mcp'), + ), + }); + + console.log('serverInfo:', client.serverInfo); + + await client.close(); +} + +main().catch(console.error); diff --git a/examples/mcp/src/server-info/server.ts b/examples/mcp/src/server-info/server.ts new file mode 100644 index 000000000000..679d4abf0e80 --- /dev/null +++ b/examples/mcp/src/server-info/server.ts @@ -0,0 +1,31 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from 'express'; + +const app = express(); +app.use(express.json()); + +app.post('/mcp', async (req, res) => { + const server = new McpServer({ + name: 'my-dumb-server', + version: '2.000.0', + }); + + server.tool('ping', 'A simple ping tool', async () => { + return { content: [{ type: 'text', text: 'pong' }] }; + }); + + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + res.on('close', () => { + transport.close(); + server.close(); + }); +}); + +app.listen(3000, () => { + console.log('server-info example server listening on port 3000'); +}); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index b61988a3d6ef..76c6b05db438 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -14,6 +14,7 @@ export { } from './tool/mcp-client'; export { ElicitationRequestSchema, ElicitResultSchema } from './tool/types'; export type { + Configuration, ElicitationRequest, ElicitResult, ListToolsResult, diff --git a/packages/mcp/src/tool/mcp-client.test.ts b/packages/mcp/src/tool/mcp-client.test.ts index 17edde3d0cba..1b2d29ed2e20 100644 --- a/packages/mcp/src/tool/mcp-client.test.ts +++ b/packages/mcp/src/tool/mcp-client.test.ts @@ -924,6 +924,48 @@ describe('MCPClient', () => { } }); + it('should expose serverInfo from initialization', async () => { + createMockTransport.mockImplementation( + () => + new MockMCPTransport({ + initializeResult: { + protocolVersion: '2025-11-25', + serverInfo: { + name: 'my-server', + version: '2.0.0', + title: 'My Server', + }, + capabilities: { tools: {} }, + }, + }), + ); + + client = await createMCPClient({ + transport: { type: 'sse', url: 'https://example.com/sse' }, + }); + + expect(client.serverInfo).toMatchInlineSnapshot(` + { + "name": "my-server", + "title": "My Server", + "version": "2.0.0", + } + `); + }); + + it('should expose serverInfo without title when server omits it', async () => { + client = await createMCPClient({ + transport: { type: 'sse', url: 'https://example.com/sse' }, + }); + + expect(client.serverInfo).toMatchInlineSnapshot(` + { + "name": "mock-mcp-server", + "version": "1.0.0", + } + `); + }); + it('should close transport when client is closed', async () => { const mockTransport = new MockMCPTransport(); const closeSpy = vi.spyOn(mockTransport, 'close'); diff --git a/packages/mcp/src/tool/mcp-client.ts b/packages/mcp/src/tool/mcp-client.ts index 0ae5d202f668..9159267e04d6 100644 --- a/packages/mcp/src/tool/mcp-client.ts +++ b/packages/mcp/src/tool/mcp-client.ts @@ -29,6 +29,7 @@ import { CallToolResult, CallToolResultSchema, ClientCapabilities, + Configuration, Configuration as ClientConfiguration, ElicitationRequest, ElicitationRequestSchema, @@ -119,6 +120,12 @@ export async function createMCPClient( } export interface MCPClient { + /** + * Information about the connected MCP server, as reported during initialization. + * @see https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation + */ + readonly serverInfo: Configuration; + tools(options?: { schemas?: TOOL_SCHEMAS; }): Promise>; @@ -201,6 +208,7 @@ class DefaultMCPClient implements MCPClient { (response: JSONRPCResponse | Error) => void > = new Map(); private serverCapabilities: ServerCapabilities = {}; + private _serverInfo: Configuration = { name: '', version: '' }; private isClosed = true; private elicitationRequestHandler?: ( request: ElicitationRequest, @@ -247,6 +255,10 @@ class DefaultMCPClient implements MCPClient { }; } + get serverInfo(): Configuration { + return this._serverInfo; + } + async init(): Promise { try { await this.transport.start(); @@ -277,6 +289,7 @@ class DefaultMCPClient implements MCPClient { } this.serverCapabilities = result.capabilities; + this._serverInfo = result.serverInfo; // Complete initialization handshake: await this.notification({ diff --git a/packages/mcp/src/tool/types.ts b/packages/mcp/src/tool/types.ts index a9439a0b96ea..e05be410d0b0 100644 --- a/packages/mcp/src/tool/types.ts +++ b/packages/mcp/src/tool/types.ts @@ -56,8 +56,10 @@ export type McpToolSet = const ClientOrServerImplementationSchema = z.looseObject({ name: z.string(), version: z.string(), + title: z.optional(z.string()), }); +// Maps to `Implementation` in the MCP specification export type Configuration = z.infer; export const BaseParamsSchema = z.looseObject({