From 217756edf7b0f0c9dd796857e56b994ff4cb090d Mon Sep 17 00:00:00 2001 From: Jesse Lumarie Date: Tue, 9 Sep 2025 07:21:03 -0400 Subject: [PATCH 1/7] mcp: update SDK for SEP 973 + add to example server (#904) Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com> --- src/examples/server/simpleStreamableHttp.ts | 4 +- src/types.ts | 48 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 3271e6213..6f1e20080 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -21,7 +21,9 @@ const strictOAuth = process.argv.includes('--oauth-strict'); const getServer = () => { const server = new McpServer({ name: 'simple-streamable-http-server', - version: '1.0.0' + version: '1.0.0', + icons: [{src: './mcp.svg', sizes: '512x512', mimeType: 'image/svg+xml'}], + websiteUrl: 'https://github.com/modelcontextprotocol/typescript-sdk', }, { capabilities: { logging: {} } }); // Register a simple tool that returns a greeting diff --git a/src/types.ts b/src/types.ts index 323e37389..262e3b623 100644 --- a/src/types.ts +++ b/src/types.ts @@ -200,6 +200,26 @@ export const CancelledNotificationSchema = NotificationSchema.extend({ }); /* Base Metadata */ +/** + * Icon schema for use in tools, prompts, resources, and implementations. + */ +export const IconSchema = z + .object({ + /** + * URL or data URI for the icon. + */ + src: z.string(), + /** + * Optional MIME type for the icon. + */ + mimeType: z.optional(z.string()), + /** + * Optional string specifying icon dimensions (e.g., "48x48 96x96"). + */ + sizes: z.optional(z.string()), + }) + .passthrough(); + /** * Base metadata interface for common properties across resources, tools, prompts, and implementations. */ @@ -225,6 +245,19 @@ export const BaseMetadataSchema = z */ export const ImplementationSchema = BaseMetadataSchema.extend({ version: z.string(), + /** + * An optional URL of the website for this implementation. + */ + websiteUrl: z.optional(z.string()), + /** + * An optional list of icons for this implementation. + * This can be used by clients to display the implementation in a user interface. + * Each icon should have a `kind` property that specifies whether it is a data representation or a URL source, a `src` property that points to the icon file or data representation, and may also include a `mimeType` and `sizes` property. + * The `mimeType` property should be a valid MIME type for the icon file, such as "image/png" or "image/svg+xml". + * The `sizes` property should be a string that specifies one or more sizes at which the icon file can be used, such as "48x48" or "any" for scalable formats like SVG. + * The `sizes` property is optional, and if not provided, the client should assume that the icon can be used at any size. + */ + icons: z.optional(z.array(IconSchema)), }); /** @@ -506,6 +539,11 @@ export const ResourceSchema = BaseMetadataSchema.extend({ */ mimeType: z.optional(z.string()), + /** + * An optional list of icons for this resource. + */ + icons: z.optional(z.array(IconSchema)), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -672,6 +710,10 @@ export const PromptSchema = BaseMetadataSchema.extend({ * A list of arguments to use for templating the prompt. */ arguments: z.optional(z.array(PromptArgumentSchema)), + /** + * An optional list of icons for this prompt. + */ + icons: z.optional(z.array(IconSchema)), /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -930,6 +972,11 @@ export const ToolSchema = BaseMetadataSchema.extend({ */ annotations: z.optional(ToolAnnotationsSchema), + /** + * An optional list of icons for this tool. + */ + icons: z.optional(z.array(IconSchema)), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1535,6 +1582,7 @@ export type EmptyResult = Infer; export type CancelledNotification = Infer; /* Base Metadata */ +export type Icon = Infer; export type BaseMetadata = Infer; /* Initialization */ From 4de54fed0c6b85217da98e10dacf62696a4bbad4 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 9 Sep 2025 14:30:40 +0100 Subject: [PATCH 2/7] Add the checkIcon test, so that tests aren't failing anymore --- src/spec.types.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 09e411894..ae47d9de7 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -663,6 +663,13 @@ function checkLoggingLevel( sdk = spec; spec = sdk; } +function checkIcon( + sdk: RemovePassthrough, + spec: SpecTypes.Icon +) { + sdk = spec; + spec = sdk; +} // This file is .gitignore'd, and fetched by `npm run fetch:spec-types` (called by `npm run test`) const SPEC_TYPES_FILE = 'spec.types.ts'; @@ -692,7 +699,7 @@ describe('Spec Types', () => { it('should define some expected types', () => { expect(specTypes).toContain('JSONRPCNotification'); expect(specTypes).toContain('ElicitResult'); - expect(specTypes).toHaveLength(92); + expect(specTypes).toHaveLength(93); }); it('should have up to date list of missing sdk types', () => { From 5dd7a2b700bb286e1d479befefff4635f02d5a6a Mon Sep 17 00:00:00 2001 From: Khanh Nguyen <163906601+knguyen-figma@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:39:52 -0400 Subject: [PATCH 3/7] feat: add _meta field support to tool definitions (#922) Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com> --- src/server/mcp.test.ts | 93 ++++++++++++++++++++++++++++++++++++++++++ src/server/mcp.ts | 12 +++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 10e550df4..d9142702f 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1633,6 +1633,99 @@ describe("tool()", () => { ), ).rejects.toThrow(/Tool nonexistent-tool not found/); }); + + /*** + * Test: Tool Registration with _meta field + */ + test("should register tool with _meta field and include it in list response", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + const metaData = { + author: "test-author", + version: "1.2.3", + category: "utility", + tags: ["test", "example"] + }; + + mcpServer.registerTool( + "test-with-meta", + { + description: "A tool with _meta field", + inputSchema: { name: z.string() }, + _meta: metaData, + }, + async ({ name }) => ({ + content: [{ type: "text", text: `Hello, ${name}!` }] + }) + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + const result = await client.request( + { method: "tools/list" }, + ListToolsResultSchema, + ); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe("test-with-meta"); + expect(result.tools[0].description).toBe("A tool with _meta field"); + expect(result.tools[0]._meta).toEqual(metaData); + }); + + /*** + * Test: Tool Registration without _meta field should have undefined _meta + */ + test("should register tool without _meta field and have undefined _meta in response", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.registerTool( + "test-without-meta", + { + description: "A tool without _meta field", + inputSchema: { name: z.string() }, + }, + async ({ name }) => ({ + content: [{ type: "text", text: `Hello, ${name}!` }] + }) + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + const result = await client.request( + { method: "tools/list" }, + ListToolsResultSchema, + ); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe("test-without-meta"); + expect(result.tools[0]._meta).toBeUndefined(); + }); }); describe("resource()", () => { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index fb797a8b4..ac4880c99 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -123,6 +123,7 @@ export class McpServer { }) as Tool["inputSchema"]) : EMPTY_OBJECT_JSON_SCHEMA, annotations: tool.annotations, + _meta: tool._meta, }; if (tool.outputSchema) { @@ -773,6 +774,7 @@ export class McpServer { inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, + _meta: Record | undefined, callback: ToolCallback ): RegisteredTool { const registeredTool: RegisteredTool = { @@ -783,6 +785,7 @@ export class McpServer { outputSchema: outputSchema === undefined ? undefined : z.object(outputSchema), annotations, + _meta, callback, enabled: true, disable: () => registeredTool.update({ enabled: false }), @@ -798,6 +801,7 @@ export class McpServer { if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema) if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations + if (typeof updates._meta !== "undefined") registeredTool._meta = updates._meta if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled this.sendToolListChanged() }, @@ -915,7 +919,7 @@ export class McpServer { } const callback = rest[0] as ToolCallback; - return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, callback) + return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, undefined, callback) } /** @@ -929,6 +933,7 @@ export class McpServer { inputSchema?: InputArgs; outputSchema?: OutputArgs; annotations?: ToolAnnotations; + _meta?: Record; }, cb: ToolCallback ): RegisteredTool { @@ -936,7 +941,7 @@ export class McpServer { throw new Error(`Tool ${name} is already registered`); } - const { title, description, inputSchema, outputSchema, annotations } = config; + const { title, description, inputSchema, outputSchema, annotations, _meta } = config; return this._createRegisteredTool( name, @@ -945,6 +950,7 @@ export class McpServer { inputSchema, outputSchema, annotations, + _meta, cb as ToolCallback ); } @@ -1173,6 +1179,7 @@ export type RegisteredTool = { inputSchema?: AnyZodObject; outputSchema?: AnyZodObject; annotations?: ToolAnnotations; + _meta?: Record; callback: ToolCallback; enabled: boolean; enable(): void; @@ -1185,6 +1192,7 @@ export type RegisteredTool = { paramsSchema?: InputArgs, outputSchema?: OutputArgs, annotations?: ToolAnnotations, + _meta?: Record, callback?: ToolCallback, enabled?: boolean }): void From ebf39330f3a90fe9e74512788c11533a9be720e1 Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Tue, 9 Sep 2025 16:35:08 -0400 Subject: [PATCH 4/7] Fix automatic log level handling for sessionless connections (#917) --- src/server/index.test.ts | 156 ++++++++++++++++++++++++++++++++++++++- src/server/index.ts | 8 +- 2 files changed, 158 insertions(+), 6 deletions(-) diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 46205d726..664ed4520 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -15,7 +15,8 @@ import { ListResourcesRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, - ErrorCode + ErrorCode, + LoggingMessageNotification } from "../types.js"; import { Transport } from "../shared/transport.js"; import { InMemoryTransport } from "../inMemory.js"; @@ -569,7 +570,7 @@ test("should allow elicitation reject and cancel without validation", async () = action: "decline", }); - // Test cancel - should not validate + // Test cancel - should not validate await expect( server.elicitInput({ message: "Please provide your name", @@ -861,3 +862,154 @@ test("should handle request timeout", async () => { code: ErrorCode.RequestTimeout, }); }); + +/* + Test automatic log level handling for transports with and without sessionId + */ +test("should respect log level for transport without sessionId", async () => { + + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + expect(clientTransport.sessionId).toEqual(undefined); + + // Client sets logging level to warning + await client.setLoggingLevel("warning"); + + // This one will make it through + const warningParams: LoggingMessageNotification["params"] = { + level: "warning", + logger: "test server", + data: "Warning message", + }; + + // This one will not + const debugParams: LoggingMessageNotification["params"] = { + level: "debug", + logger: "test server", + data: "Debug message", + }; + + // Test the one that makes it through + clientTransport.onmessage = jest.fn().mockImplementation((message) => { + expect(message).toEqual({ + jsonrpc: "2.0", + method: "notifications/message", + params: warningParams + }); + }); + + // This one will not make it through + await server.sendLoggingMessage(debugParams); + expect(clientTransport.onmessage).not.toHaveBeenCalled(); + + // This one will, triggering the above test in clientTransport.onmessage + await server.sendLoggingMessage(warningParams); + expect(clientTransport.onmessage).toHaveBeenCalled(); + +}); + +test("should respect log level for transport with sessionId", async () => { + + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + // Add a session id to the transports + const SESSION_ID = "test-session-id"; + clientTransport.sessionId = SESSION_ID; + serverTransport.sessionId = SESSION_ID; + + expect(clientTransport.sessionId).toBeDefined(); + expect(serverTransport.sessionId).toBeDefined(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + + // Client sets logging level to warning + await client.setLoggingLevel("warning"); + + // This one will make it through + const warningParams: LoggingMessageNotification["params"] = { + level: "warning", + logger: "test server", + data: "Warning message", + }; + + // This one will not + const debugParams: LoggingMessageNotification["params"] = { + level: "debug", + logger: "test server", + data: "Debug message", + }; + + // Test the one that makes it through + clientTransport.onmessage = jest.fn().mockImplementation((message) => { + expect(message).toEqual({ + jsonrpc: "2.0", + method: "notifications/message", + params: warningParams + }); + }); + + // This one will not make it through + await server.sendLoggingMessage(debugParams, SESSION_ID); + expect(clientTransport.onmessage).not.toHaveBeenCalled(); + + // This one will, triggering the above test in clientTransport.onmessage + await server.sendLoggingMessage(warningParams, SESSION_ID); + expect(clientTransport.onmessage).toHaveBeenCalled(); + +}); + diff --git a/src/server/index.ts b/src/server/index.ts index b1f71ea28..970657358 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -117,7 +117,7 @@ export class Server< const transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string || undefined; const { level } = request.params; const parseResult = LoggingLevelSchema.safeParse(level); - if (transportSessionId && parseResult.success) { + if (parseResult.success) { this._loggingLevels.set(transportSessionId, parseResult.data); } return {}; @@ -126,7 +126,7 @@ export class Server< } // Map log levels by session id - private _loggingLevels = new Map(); + private _loggingLevels = new Map(); // Map LogLevelSchema to severity index private readonly LOG_LEVEL_SEVERITY = new Map( @@ -134,7 +134,7 @@ export class Server< ); // Is a message with the given level ignored in the log level set for the given session id? - private isMessageIgnored = (level: LoggingLevel, sessionId: string): boolean => { + private isMessageIgnored = (level: LoggingLevel, sessionId?: string): boolean => { const currentLevel = this._loggingLevels.get(sessionId); return (currentLevel) ? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)! @@ -398,7 +398,7 @@ export class Server< */ async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) { if (this._capabilities.logging) { - if (!sessionId || !this.isMessageIgnored(params.level, sessionId)) { + if (!this.isMessageIgnored(params.level, sessionId)) { return this.notification({method: "notifications/message", params}) } } From 68baf63583df8b7e2dab835a15c9cf81e285c174 Mon Sep 17 00:00:00 2001 From: Inna Harper Date: Thu, 11 Sep 2025 18:38:05 +0100 Subject: [PATCH 5/7] 1.17.6 (#936) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9380e731..b737f02ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.17.5", + "version": "1.17.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.17.5", + "version": "1.17.6", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/package.json b/package.json index b421b6eac..3586f1f23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.17.5", + "version": "1.17.6", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 835286894c0cd80c5cd6580e8096a40ce2f28755 Mon Sep 17 00:00:00 2001 From: Inna Harper Date: Thu, 11 Sep 2025 18:40:16 +0100 Subject: [PATCH 6/7] 1.18.0 (#937) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b737f02ea..14b3a9f35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.17.6", + "version": "1.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.17.6", + "version": "1.18.0", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/package.json b/package.json index 3586f1f23..fda002f99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.17.6", + "version": "1.18.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From b28c297184cb0cb64611a3357d6438dd1b0824c6 Mon Sep 17 00:00:00 2001 From: Inna Harper Date: Thu, 11 Sep 2025 20:34:09 +0100 Subject: [PATCH 7/7] ignore icons for now (#938) --- src/spec.types.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index ae47d9de7..5aa497f4a 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -685,6 +685,7 @@ const MISSING_SDK_TYPES = [ 'Annotations', 'ModelHint', 'ModelPreferences', + 'Icons', ] function extractExportedTypes(source: string): string[] { @@ -699,7 +700,7 @@ describe('Spec Types', () => { it('should define some expected types', () => { expect(specTypes).toContain('JSONRPCNotification'); expect(specTypes).toContain('ElicitResult'); - expect(specTypes).toHaveLength(93); + expect(specTypes).toHaveLength(94); }); it('should have up to date list of missing sdk types', () => {