From 7439cc0adc0cfb1bf17e5b5b820f7376cf7b4189 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 11 Nov 2025 18:50:07 +0000 Subject: [PATCH 01/11] Implement SEP 1577 (Sampling w/ Tools) --- src/types.test.ts | 464 +++++++++++++++++++++++++++++++++++++++++++++- src/types.ts | 177 +++++++++++++++++- 2 files changed, 631 insertions(+), 10 deletions(-) diff --git a/src/types.test.ts b/src/types.test.ts index cd8cc0711..1141b3205 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -5,7 +5,16 @@ import { ContentBlockSchema, PromptMessageSchema, CallToolResultSchema, - CompleteRequestSchema + CompleteRequestSchema, + ToolUseContentSchema, + ToolResultContentSchema, + ToolChoiceSchema, + UserMessageSchema, + AssistantMessageSchema, + SamplingMessageSchema, + CreateMessageRequestSchema, + CreateMessageResultSchema, + ClientCapabilitiesSchema, } from './types.js'; describe('Types', () => { @@ -311,4 +320,457 @@ describe('Types', () => { } }); }); + + describe("ToolUseContent", () => { + test("should validate a tool call content", () => { + const toolCall = { + type: "tool_use", + id: "call_123", + name: "get_weather", + input: { city: "San Francisco", units: "celsius" } + }; + + const result = ToolUseContentSchema.safeParse(toolCall); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe("tool_use"); + expect(result.data.id).toBe("call_123"); + expect(result.data.name).toBe("get_weather"); + expect(result.data.input).toEqual({ city: "San Francisco", units: "celsius" }); + } + }); + + test("should validate tool call with _meta", () => { + const toolCall = { + type: "tool_use", + id: "call_456", + name: "search", + input: { query: "test" }, + _meta: { custom: "data" } + }; + + const result = ToolUseContentSchema.safeParse(toolCall); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data._meta).toEqual({ custom: "data" }); + } + }); + + test("should fail validation for missing required fields", () => { + const invalidToolCall = { + type: "tool_use", + name: "test" + // missing id and input + }; + + const result = ToolUseContentSchema.safeParse(invalidToolCall); + expect(result.success).toBe(false); + }); + }); + + describe("ToolResultContent", () => { + test("should validate a tool result content", () => { + const toolResult = { + type: "tool_result", + toolUseId: "call_123", + content: { temperature: 72, condition: "sunny" } + }; + + const result = ToolResultContentSchema.safeParse(toolResult); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe("tool_result"); + expect(result.data.toolUseId).toBe("call_123"); + expect(result.data.content).toEqual({ temperature: 72, condition: "sunny" }); + } + }); + + test("should validate tool result with error in content", () => { + const toolResult = { + type: "tool_result", + toolUseId: "call_456", + content: { error: "API_ERROR", message: "Service unavailable" } + }; + + const result = ToolResultContentSchema.safeParse(toolResult); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.content).toEqual({ error: "API_ERROR", message: "Service unavailable" }); + } + }); + + test("should fail validation for missing required fields", () => { + const invalidToolResult = { + type: "tool_result", + content: { data: "test" } + // missing toolUseId + }; + + const result = ToolResultContentSchema.safeParse(invalidToolResult); + expect(result.success).toBe(false); + }); + }); + + describe("ToolChoice", () => { + test("should validate tool choice with mode auto", () => { + const toolChoice = { + mode: "auto" + }; + + const result = ToolChoiceSchema.safeParse(toolChoice); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.mode).toBe("auto"); + } + }); + + test("should validate tool choice with mode required", () => { + const toolChoice = { + mode: "required", + disable_parallel_tool_use: true + }; + + const result = ToolChoiceSchema.safeParse(toolChoice); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.mode).toBe("required"); + expect(result.data.disable_parallel_tool_use).toBe(true); + } + }); + + test("should validate empty tool choice", () => { + const toolChoice = {}; + + const result = ToolChoiceSchema.safeParse(toolChoice); + expect(result.success).toBe(true); + }); + + test("should fail validation for invalid mode", () => { + const invalidToolChoice = { + mode: "invalid" + }; + + const result = ToolChoiceSchema.safeParse(invalidToolChoice); + expect(result.success).toBe(false); + }); + }); + + describe("UserMessage and AssistantMessage", () => { + test("should validate user message with text", () => { + const userMessage = { + role: "user", + content: { type: "text", text: "What's the weather?" } + }; + + const result = UserMessageSchema.safeParse(userMessage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe("user"); + expect(result.data.content.type).toBe("text"); + } + }); + + test("should validate user message with tool result", () => { + const userMessage = { + role: "user", + content: { + type: "tool_result", + toolUseId: "call_123", + content: { temperature: 72 } + } + }; + + const result = UserMessageSchema.safeParse(userMessage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.content.type).toBe("tool_result"); + } + }); + + test("should validate assistant message with text", () => { + const assistantMessage = { + role: "assistant", + content: { type: "text", text: "I'll check the weather for you." } + }; + + const result = AssistantMessageSchema.safeParse(assistantMessage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe("assistant"); + } + }); + + test("should validate assistant message with tool call", () => { + const assistantMessage = { + role: "assistant", + content: { + type: "tool_use", + id: "call_123", + name: "get_weather", + input: { city: "SF" } + } + }; + + const result = AssistantMessageSchema.safeParse(assistantMessage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.content.type).toBe("tool_use"); + } + }); + + test("should fail validation for assistant with tool result", () => { + const invalidMessage = { + role: "assistant", + content: { + type: "tool_result", + toolUseId: "call_123", + content: {} + } + }; + + const result = AssistantMessageSchema.safeParse(invalidMessage); + expect(result.success).toBe(false); + }); + + test("should fail validation for user with tool call", () => { + const invalidMessage = { + role: "user", + content: { + type: "tool_use", + id: "call_123", + name: "test", + input: {} + } + }; + + const result = UserMessageSchema.safeParse(invalidMessage); + expect(result.success).toBe(false); + }); + }); + + describe("SamplingMessage", () => { + test("should validate user message via discriminated union", () => { + const message = { + role: "user", + content: { type: "text", text: "Hello" } + }; + + const result = SamplingMessageSchema.safeParse(message); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe("user"); + } + }); + + test("should validate assistant message via discriminated union", () => { + const message = { + role: "assistant", + content: { type: "text", text: "Hi there!" } + }; + + const result = SamplingMessageSchema.safeParse(message); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.role).toBe("assistant"); + } + }); + }); + + describe("CreateMessageRequest", () => { + test("should validate request without tools", () => { + const request = { + method: "sampling/createMessage", + params: { + messages: [ + { role: "user", content: { type: "text", text: "Hello" } } + ], + maxTokens: 1000 + } + }; + + const result = CreateMessageRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.tools).toBeUndefined(); + } + }); + + test("should validate request with tools", () => { + const request = { + method: "sampling/createMessage", + params: { + messages: [ + { role: "user", content: { type: "text", text: "What's the weather?" } } + ], + maxTokens: 1000, + tools: [ + { + name: "get_weather", + description: "Get weather for a location", + inputSchema: { + type: "object", + properties: { + location: { type: "string" } + }, + required: ["location"] + } + } + ], + tool_choice: { + mode: "auto" + } + } + }; + + const result = CreateMessageRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.tools).toHaveLength(1); + expect(result.data.params.tool_choice?.mode).toBe("auto"); + } + }); + + test("should validate request with includeContext (soft-deprecated)", () => { + const request = { + method: "sampling/createMessage", + params: { + messages: [ + { role: "user", content: { type: "text", text: "Help" } } + ], + maxTokens: 1000, + includeContext: "thisServer" + } + }; + + const result = CreateMessageRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.includeContext).toBe("thisServer"); + } + }); + }); + + describe("CreateMessageResult", () => { + test("should validate result with text content", () => { + const result = { + model: "claude-3-5-sonnet-20241022", + role: "assistant", + content: { type: "text", text: "Here's the answer." }, + stopReason: "endTurn" + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.role).toBe("assistant"); + expect(parseResult.data.stopReason).toBe("endTurn"); + } + }); + + test("should validate result with tool call", () => { + const result = { + model: "claude-3-5-sonnet-20241022", + role: "assistant", + content: { + type: "tool_use", + id: "call_123", + name: "get_weather", + input: { city: "SF" } + }, + stopReason: "toolUse" + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.stopReason).toBe("toolUse"); + expect(parseResult.data.content.type).toBe("tool_use"); + } + }); + + test("should validate all new stop reasons", () => { + const stopReasons = ["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]; + + stopReasons.forEach(stopReason => { + const result = { + model: "test", + role: "assistant", + content: { type: "text", text: "test" }, + stopReason + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + }); + }); + + test("should allow custom stop reason string", () => { + const result = { + model: "test", + role: "assistant", + content: { type: "text", text: "test" }, + stopReason: "custom_provider_reason" + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + }); + + test("should fail for user role in result", () => { + const result = { + model: "test", + role: "user", + content: { type: "text", text: "test" } + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(false); + }); + }); + + describe("ClientCapabilities with sampling", () => { + test("should validate capabilities with sampling.tools", () => { + const capabilities = { + sampling: { + tools: {} + } + }; + + const result = ClientCapabilitiesSchema.safeParse(capabilities); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sampling?.tools).toBeDefined(); + } + }); + + test("should validate capabilities with sampling.context", () => { + const capabilities = { + sampling: { + context: {} + } + }; + + const result = ClientCapabilitiesSchema.safeParse(capabilities); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sampling?.context).toBeDefined(); + } + }); + + test("should validate capabilities with both", () => { + const capabilities = { + sampling: { + context: {}, + tools: {} + } + }; + + const result = ClientCapabilitiesSchema.safeParse(capabilities); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sampling?.context).toBeDefined(); + expect(result.data.sampling?.tools).toBeDefined(); + } + }); + }); }); diff --git a/src/types.ts b/src/types.ts index 78fa81d54..1496ff892 100644 --- a/src/types.ts +++ b/src/types.ts @@ -310,7 +310,19 @@ export const ClientCapabilitiesSchema = z.object({ /** * Present if the client supports sampling from an LLM. */ - sampling: AssertObjectSchema.optional(), + sampling: z + .object({ + /** + * Present if the client supports non-'none' values for includeContext parameter. + */ + context: AssertObjectSchema.optional(), + /** + * Present if the client supports tools and tool_choice parameters in sampling requests. + * Presence indicates full tool calling support. + */ + tools: AssertObjectSchema.optional(), + }) + .optional(), /** * Present if the client supports eliciting user input. */ @@ -832,6 +844,36 @@ export const AudioContentSchema = z.object({ _meta: z.record(z.string(), z.unknown()).optional() }); +/** + * A tool call request from an assistant (LLM). + * Represents the assistant's request to use a tool. + */ +export const ToolUseContentSchema = z + .object({ + type: z.literal("tool_use"), + /** + * The name of the tool to invoke. + * Must match a tool name from the request's tools array. + */ + name: z.string(), + /** + * Unique identifier for this tool call. + * Used to correlate with ToolResultContent in subsequent messages. + */ + id: z.string(), + /** + * Arguments to pass to the tool. + * Must conform to the tool's inputSchema. + */ + input: z.object({}).passthrough(), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); + /** * The contents of a resource, embedded into a prompt or tool call result. */ @@ -1158,13 +1200,99 @@ export const ModelPreferencesSchema = z.object({ intelligencePriority: z.optional(z.number().min(0).max(1)) }); +/** + * Controls tool usage behavior in sampling requests. + */ +export const ToolChoiceSchema = z + .object({ + /** + * Controls when tools are used: + * - "auto": Model decides whether to use tools (default) + * - "required": Model MUST use at least one tool before completing + */ + mode: z.optional(z.enum(["auto", "required", "none"])), + /** + * If true, model should not use multiple tools in parallel. + * Some models may ignore this hint. + * Default: false + */ + disable_parallel_tool_use: z.optional(z.boolean()), + }) + .passthrough(); + +/** + * The result of a tool execution, provided by the user (server). + * Represents the outcome of invoking a tool requested via ToolUseContent. + */ +export const ToolResultContentSchema = z + .object({ + type: z.literal("tool_result"), + toolUseId: z.string().describe("The unique identifier for the corresponding tool call."), + content: z.array(ContentBlockSchema).default([]), + structuredContent: z.object({}).passthrough().optional(), + isError: z.optional(z.boolean()), + + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); + +export const UserMessageContentSchema = z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolResultContentSchema, +]); + +/** + * A message from the user (server) in a sampling conversation. + */ +export const UserMessageSchema = z + .object({ + role: z.literal("user"), + content: z.union([UserMessageContentSchema, z.array(UserMessageContentSchema)]), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); + +export const AssistantMessageContentSchema = z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolUseContentSchema, +]); + +/** + * A message from the assistant (LLM) in a sampling conversation. + */ +export const AssistantMessageSchema = z + .object({ + role: z.literal("assistant"), + content: z.union([AssistantMessageContentSchema, z.array(AssistantMessageContentSchema)]), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); + /** * Describes a message issued to or received from an LLM API. + * This is a discriminated union of UserMessage and AssistantMessage, where + * each role has its own set of allowed content types. */ -export const SamplingMessageSchema = z.object({ - role: z.enum(['user', 'assistant']), - content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]) -}); +export const SamplingMessageSchema = z.discriminatedUnion("role", [ + UserMessageSchema, + AssistantMessageSchema, +]); /** * Parameters for a `sampling/createMessage` request. @@ -1181,6 +1309,7 @@ export const CreateMessageRequestParamsSchema = BaseRequestParamsSchema.extend({ systemPrompt: z.string().optional(), /** * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. + * Values different from 'none' require clientCapabilities.sampling.context */ includeContext: z.enum(['none', 'thisServer', 'allServers']).optional(), temperature: z.number().optional(), @@ -1194,7 +1323,17 @@ export const CreateMessageRequestParamsSchema = BaseRequestParamsSchema.extend({ /** * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. */ - metadata: AssertObjectSchema.optional() + metadata: AssertObjectSchema.optional(), + /** + * Tool definitions for the LLM to use. + * Requires clientCapabilities.sampling.tools. + */ + tools: z.optional(z.array(ToolSchema)), + /** + * Controls tool usage behavior. + * Requires clientCapabilities.sampling.tools and tools parameter. + */ + toolChoice: z.optional(ToolChoiceSchema), }); /** * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. @@ -1214,10 +1353,23 @@ export const CreateMessageResultSchema = ResultSchema.extend({ model: z.string(), /** * The reason why sampling stopped. + * Standard values: + * - "endTurn": Natural end of the assistant's turn + * - "stopSequence": A stop sequence was encountered + * - "maxTokens": Maximum token limit was reached + * - "toolUse": The model wants to use one or more tools + */ + stopReason: z.optional( + z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse"]).or(z.string()), + ), + /** + * The role is always "assistant" in responses from the LLM. */ - stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens']).or(z.string())), - role: z.enum(['user', 'assistant']), - content: z.discriminatedUnion('type', [TextContentSchema, ImageContentSchema, AudioContentSchema]) + role: z.literal("assistant"), + /** + * Response content. May be ToolUseContent if stopReason is "toolUse". + */ + content: z.union([AssistantMessageContentSchema, z.array(AssistantMessageContentSchema)]), }); /* Elicitation */ @@ -1813,6 +1965,8 @@ export type GetPromptRequest = Infer; export type TextContent = Infer; export type ImageContent = Infer; export type AudioContent = Infer; +export type ToolUseContent = Infer; +export type ToolResultContent = Infer; export type EmbeddedResource = Infer; export type ResourceLink = Infer; export type ContentBlock = Infer; @@ -1839,12 +1993,17 @@ export type LoggingMessageNotificationParams = Infer; /* Sampling */ +export type ToolChoice = Infer; +export type UserMessage = Infer; +export type AssistantMessage = Infer; export type ModelHint = Infer; export type ModelPreferences = Infer; export type SamplingMessage = Infer; export type CreateMessageRequestParams = Infer; export type CreateMessageRequest = Infer; export type CreateMessageResult = Infer; +export type AssistantMessageContent = Infer; +export type UserMessageContent = Infer; /* Elicitation */ export type BooleanSchema = Infer; From d8a3924f54bba16e1eab29eb7ceeb50edb67c82e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 17 Nov 2025 14:53:32 +0000 Subject: [PATCH 02/11] simplify SamplingMessage --- src/types.test.ts | 41 ++++++++++++++-------------- src/types.ts | 69 +++++++++++++++-------------------------------- 2 files changed, 42 insertions(+), 68 deletions(-) diff --git a/src/types.test.ts b/src/types.test.ts index 1141b3205..cf0ab68ad 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -9,8 +9,6 @@ import { ToolUseContentSchema, ToolResultContentSchema, ToolChoiceSchema, - UserMessageSchema, - AssistantMessageSchema, SamplingMessageSchema, CreateMessageRequestSchema, CreateMessageResultSchema, @@ -455,18 +453,20 @@ describe('Types', () => { }); }); - describe("UserMessage and AssistantMessage", () => { + describe("SamplingMessage content types", () => { test("should validate user message with text", () => { const userMessage = { role: "user", content: { type: "text", text: "What's the weather?" } }; - const result = UserMessageSchema.safeParse(userMessage); + const result = SamplingMessageSchema.safeParse(userMessage); expect(result.success).toBe(true); if (result.success) { expect(result.data.role).toBe("user"); - expect(result.data.content.type).toBe("text"); + if (!Array.isArray(result.data.content)) { + expect(result.data.content.type).toBe("text"); + } } }); @@ -476,13 +476,13 @@ describe('Types', () => { content: { type: "tool_result", toolUseId: "call_123", - content: { temperature: 72 } + content: [] } }; - const result = UserMessageSchema.safeParse(userMessage); + const result = SamplingMessageSchema.safeParse(userMessage); expect(result.success).toBe(true); - if (result.success) { + if (result.success && !Array.isArray(result.data.content)) { expect(result.data.content.type).toBe("tool_result"); } }); @@ -493,7 +493,7 @@ describe('Types', () => { content: { type: "text", text: "I'll check the weather for you." } }; - const result = AssistantMessageSchema.safeParse(assistantMessage); + const result = SamplingMessageSchema.safeParse(assistantMessage); expect(result.success).toBe(true); if (result.success) { expect(result.data.role).toBe("assistant"); @@ -511,29 +511,28 @@ describe('Types', () => { } }; - const result = AssistantMessageSchema.safeParse(assistantMessage); + const result = SamplingMessageSchema.safeParse(assistantMessage); expect(result.success).toBe(true); - if (result.success) { + if (result.success && !Array.isArray(result.data.content)) { expect(result.data.content.type).toBe("tool_use"); } }); - test("should fail validation for assistant with tool result", () => { - const invalidMessage = { + test("should validate any content type for any role", () => { + // The simplified schema allows any content type for any role + const assistantWithToolResult = { role: "assistant", content: { type: "tool_result", toolUseId: "call_123", - content: {} + content: [] } }; - const result = AssistantMessageSchema.safeParse(invalidMessage); - expect(result.success).toBe(false); - }); + const result1 = SamplingMessageSchema.safeParse(assistantWithToolResult); + expect(result1.success).toBe(true); - test("should fail validation for user with tool call", () => { - const invalidMessage = { + const userWithToolUse = { role: "user", content: { type: "tool_use", @@ -543,8 +542,8 @@ describe('Types', () => { } }; - const result = UserMessageSchema.safeParse(invalidMessage); - expect(result.success).toBe(false); + const result2 = SamplingMessageSchema.safeParse(userWithToolUse); + expect(result2.success).toBe(true); }); }); diff --git a/src/types.ts b/src/types.ts index 1496ff892..1711a2383 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1209,6 +1209,7 @@ export const ToolChoiceSchema = z * Controls when tools are used: * - "auto": Model decides whether to use tools (default) * - "required": Model MUST use at least one tool before completing + * - "none": Model MUST NOT use any tools */ mode: z.optional(z.enum(["auto", "required", "none"])), /** @@ -1240,59 +1241,33 @@ export const ToolResultContentSchema = z }) .passthrough(); -export const UserMessageContentSchema = z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolResultContentSchema, -]); - /** - * A message from the user (server) in a sampling conversation. + * Content block types allowed in sampling messages. + * This includes text, image, audio, tool use requests, and tool results. */ -export const UserMessageSchema = z - .object({ - role: z.literal("user"), - content: z.union([UserMessageContentSchema, z.array(UserMessageContentSchema)]), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); - -export const AssistantMessageContentSchema = z.discriminatedUnion("type", [ +export const SamplingMessageContentBlockSchema = z.discriminatedUnion("type", [ TextContentSchema, ImageContentSchema, AudioContentSchema, ToolUseContentSchema, + ToolResultContentSchema, ]); -/** - * A message from the assistant (LLM) in a sampling conversation. - */ -export const AssistantMessageSchema = z - .object({ - role: z.literal("assistant"), - content: z.union([AssistantMessageContentSchema, z.array(AssistantMessageContentSchema)]), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); - /** * Describes a message issued to or received from an LLM API. - * This is a discriminated union of UserMessage and AssistantMessage, where - * each role has its own set of allowed content types. */ -export const SamplingMessageSchema = z.discriminatedUnion("role", [ - UserMessageSchema, - AssistantMessageSchema, -]); +export const SamplingMessageSchema = z.object({ + role: z.enum(['user', 'assistant']), + content: z.union([ + SamplingMessageContentBlockSchema, + z.array(SamplingMessageContentBlockSchema) + ]), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); /** * Parameters for a `sampling/createMessage` request. @@ -1369,7 +1344,10 @@ export const CreateMessageResultSchema = ResultSchema.extend({ /** * Response content. May be ToolUseContent if stopReason is "toolUse". */ - content: z.union([AssistantMessageContentSchema, z.array(AssistantMessageContentSchema)]), + content: z.union([ + SamplingMessageContentBlockSchema, + z.array(SamplingMessageContentBlockSchema) + ]), }); /* Elicitation */ @@ -1994,16 +1972,13 @@ export type LoggingMessageNotification = Infer; -export type UserMessage = Infer; -export type AssistantMessage = Infer; export type ModelHint = Infer; export type ModelPreferences = Infer; +export type SamplingMessageContentBlock = Infer; export type SamplingMessage = Infer; export type CreateMessageRequestParams = Infer; export type CreateMessageRequest = Infer; export type CreateMessageResult = Infer; -export type AssistantMessageContent = Infer; -export type UserMessageContent = Infer; /* Elicitation */ export type BooleanSchema = Infer; From 42aaffa536f7de1ed1c9a3a7a3d78a5d93137e18 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 17 Nov 2025 14:56:32 +0000 Subject: [PATCH 03/11] Update toolWithSampleServer.ts --- src/examples/server/toolWithSampleServer.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/examples/server/toolWithSampleServer.ts b/src/examples/server/toolWithSampleServer.ts index ad5a01bdc..2bb1094dc 100644 --- a/src/examples/server/toolWithSampleServer.ts +++ b/src/examples/server/toolWithSampleServer.ts @@ -33,13 +33,12 @@ mcpServer.registerTool( maxTokens: 500 }); + const contents = Array.isArray(response.content) ? response.content : [response.content]; return { - content: [ - { - type: 'text', - text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary' - } - ] + content: contents.map(content => ({ + type: 'text', + text: content.type === 'text' ? content.text : 'Unable to generate summary' + })) }; } ); From dcb68e18c2bfc54ec93ca6432bfadb302faf7da0 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 17 Nov 2025 15:04:35 +0000 Subject: [PATCH 04/11] Fix failing tests for ToolResultContent and CreateMessageRequest schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use structuredContent instead of content for plain object results in ToolResultContent tests - Use camelCase toolChoice instead of snake_case tool_choice in CreateMessageRequest test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/types.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/types.test.ts b/src/types.test.ts index cf0ab68ad..0186df706 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -371,7 +371,7 @@ describe('Types', () => { const toolResult = { type: "tool_result", toolUseId: "call_123", - content: { temperature: 72, condition: "sunny" } + structuredContent: { temperature: 72, condition: "sunny" } }; const result = ToolResultContentSchema.safeParse(toolResult); @@ -379,7 +379,7 @@ describe('Types', () => { if (result.success) { expect(result.data.type).toBe("tool_result"); expect(result.data.toolUseId).toBe("call_123"); - expect(result.data.content).toEqual({ temperature: 72, condition: "sunny" }); + expect(result.data.structuredContent).toEqual({ temperature: 72, condition: "sunny" }); } }); @@ -387,13 +387,15 @@ describe('Types', () => { const toolResult = { type: "tool_result", toolUseId: "call_456", - content: { error: "API_ERROR", message: "Service unavailable" } + structuredContent: { error: "API_ERROR", message: "Service unavailable" }, + isError: true }; const result = ToolResultContentSchema.safeParse(toolResult); expect(result.success).toBe(true); if (result.success) { - expect(result.data.content).toEqual({ error: "API_ERROR", message: "Service unavailable" }); + expect(result.data.structuredContent).toEqual({ error: "API_ERROR", message: "Service unavailable" }); + expect(result.data.isError).toBe(true); } }); @@ -615,7 +617,7 @@ describe('Types', () => { } } ], - tool_choice: { + toolChoice: { mode: "auto" } } @@ -625,7 +627,7 @@ describe('Types', () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.params.tools).toHaveLength(1); - expect(result.data.params.tool_choice?.mode).toBe("auto"); + expect(result.data.params.toolChoice?.mode).toBe("auto"); } }); From aead5c19c9dc22b2f63c8c276a6e2fbd90f2c35a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 17 Nov 2025 15:13:10 +0000 Subject: [PATCH 05/11] Update types.ts --- src/types.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/types.ts b/src/types.ts index 1711a2383..ae96ac112 100644 --- a/src/types.ts +++ b/src/types.ts @@ -313,12 +313,12 @@ export const ClientCapabilitiesSchema = z.object({ sampling: z .object({ /** - * Present if the client supports non-'none' values for includeContext parameter. + * Present if the client supports context inclusion via includeContext parameter. + * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). */ context: AssertObjectSchema.optional(), /** - * Present if the client supports tools and tool_choice parameters in sampling requests. - * Presence indicates full tool calling support. + * Present if the client supports tool use via tools and toolChoice parameters. */ tools: AssertObjectSchema.optional(), }) @@ -1283,8 +1283,11 @@ export const CreateMessageRequestParamsSchema = BaseRequestParamsSchema.extend({ */ systemPrompt: z.string().optional(), /** - * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. - * Values different from 'none' require clientCapabilities.sampling.context + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + * The client MAY ignore this request. + * + * Default is "none". Values "thisServer" and "allServers" are soft-deprecated. Servers SHOULD only use these values if the client + * declares ClientCapabilities.sampling.context. These values may be removed in future spec releases. */ includeContext: z.enum(['none', 'thisServer', 'allServers']).optional(), temperature: z.number().optional(), @@ -1300,13 +1303,14 @@ export const CreateMessageRequestParamsSchema = BaseRequestParamsSchema.extend({ */ metadata: AssertObjectSchema.optional(), /** - * Tool definitions for the LLM to use. - * Requires clientCapabilities.sampling.tools. + * Tools that the model may use during generation. + * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. */ tools: z.optional(z.array(ToolSchema)), /** - * Controls tool usage behavior. - * Requires clientCapabilities.sampling.tools and tools parameter. + * Controls how the model uses tools. + * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. + * Default is `{ mode: "auto" }`. */ toolChoice: z.optional(ToolChoiceSchema), }); @@ -1327,12 +1331,15 @@ export const CreateMessageResultSchema = ResultSchema.extend({ */ model: z.string(), /** - * The reason why sampling stopped. + * The reason why sampling stopped, if known. + * * Standard values: * - "endTurn": Natural end of the assistant's turn * - "stopSequence": A stop sequence was encountered * - "maxTokens": Maximum token limit was reached * - "toolUse": The model wants to use one or more tools + * + * This field is an open string to allow for provider-specific stop reasons. */ stopReason: z.optional( z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse"]).or(z.string()), From 79c79c8604887716b55f29e805f41bcb23b7853a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 17 Nov 2025 15:22:48 +0000 Subject: [PATCH 06/11] update spec types changes for sampling w/ tools sep --- src/spec.types.ts | 148 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 4 deletions(-) diff --git a/src/spec.types.ts b/src/spec.types.ts index 307884fa0..da1d4cf47 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -307,7 +307,17 @@ export interface ClientCapabilities { /** * Present if the client supports sampling from an LLM. */ - sampling?: object; + sampling?: { + /** + * Whether the client supports context inclusion via includeContext parameter. + * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + */ + context?: object; + /** + * Whether the client supports tool use via tools and toolChoice parameters. + */ + tools?: object; + }; /** * Present if the client supports elicitation from the server. */ @@ -1255,7 +1265,11 @@ export interface CreateMessageRequestParams extends RequestParams { */ systemPrompt?: string; /** - * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + * The client MAY ignore this request. + * + * Default is "none". Values "thisServer" and "allServers" are soft-deprecated. Servers SHOULD only use these values if the client + * declares ClientCapabilities.sampling.context. These values may be removed in future spec releases. */ includeContext?: "none" | "thisServer" | "allServers"; /** @@ -1273,6 +1287,32 @@ export interface CreateMessageRequestParams extends RequestParams { * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. */ metadata?: object; + /** + * Tools that the model may use during generation. + * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. + */ + tools?: Tool[]; + /** + * Controls how the model uses tools. + * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. + * Default is `{ mode: "auto" }`. + */ + toolChoice?: ToolChoice; +} + +/** + * Controls tool selection behavior for sampling requests. + * + * @category `sampling/createMessage` + */ +export interface ToolChoice { + /** + * Controls the tool use ability of the model: + * - "auto": Model decides whether to use tools (default) + * - "required": Model MUST use at least one tool before completing + * - "none": Model MUST NOT use any tools + */ + mode?: "auto" | "required" | "none"; } /** @@ -1295,10 +1335,19 @@ export interface CreateMessageResult extends Result, SamplingMessage { * The name of the model that generated the message. */ model: string; + /** * The reason why sampling stopped, if known. + * + * Standard values: + * - "endTurn": Natural end of the assistant's turn + * - "stopSequence": A stop sequence was encountered + * - "maxTokens": Maximum token limit was reached + * - "toolUse": The model wants to use one or more tools + * + * This field is an open string to allow for provider-specific stop reasons. */ - stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | string; } /** @@ -1308,8 +1357,18 @@ export interface CreateMessageResult extends Result, SamplingMessage { */ export interface SamplingMessage { role: Role; - content: TextContent | ImageContent | AudioContent; + content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; + /** + * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; } +export type SamplingMessageContentBlock = + | TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent; /** * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed @@ -1444,6 +1503,87 @@ export interface AudioContent { _meta?: { [key: string]: unknown }; } +/** + * A request from the assistant to call a tool. + * + * @category `sampling/createMessage` + */ +export interface ToolUseContent { + type: "tool_use"; + + /** + * A unique identifier for this tool use. + * + * This ID is used to match tool results to their corresponding tool uses. + */ + id: string; + + /** + * The name of the tool to call. + */ + name: string; + + /** + * The arguments to pass to the tool, conforming to the tool's input schema. + */ + input: object; + + /** + * Optional metadata about the tool use. Clients SHOULD preserve this field when + * including tool uses in subsequent sampling requests to enable caching optimizations. + * + * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + +/** + * The result of a tool use, provided by the user back to the assistant. + * + * @category `sampling/createMessage` + */ +export interface ToolResultContent { + type: "tool_result"; + + /** + * The ID of the tool use this result corresponds to. + * + * This MUST match the ID from a previous ToolUseContent. + */ + toolUseId: string; + + /** + * The unstructured result content of the tool use. + * + * This has the same format as CallToolResult.content and can include text, images, + * audio, resource links, and embedded resources. + */ + content: ContentBlock[]; + + /** + * An optional structured result object. + * + * If the tool defined an outputSchema, this SHOULD conform to that schema. + */ + structuredContent?: object; + + /** + * Whether the tool use resulted in an error. + * + * If true, the content typically describes the error that occurred. + * Default: false + */ + isError?: boolean; + + /** + * Optional metadata about the tool result. Clients SHOULD preserve this field when + * including tool results in subsequent sampling requests to enable caching optimizations. + * + * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. + */ + _meta?: { [key: string]: unknown }; +} + /** * The server's preferences for model selection, requested of the client during sampling. * From d5cc7364acc56bde207feb1a34afd405fccb0047 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 17 Nov 2025 15:26:04 +0000 Subject: [PATCH 07/11] Add compatibility tests for new sampling tool types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add type checks for ToolChoice, ToolUseContent, ToolResultContent, SamplingMessageContentBlock - Update expected spec types count from 119 to 123 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/spec.types.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 3df41bfc5..976430c1b 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -602,6 +602,22 @@ const sdkTypeChecks = { ModelPreferences: (sdk: SDKTypes.ModelPreferences, spec: SpecTypes.ModelPreferences) => { sdk = spec; spec = sdk; + }, + ToolChoice: (sdk: SDKTypes.ToolChoice, spec: SpecTypes.ToolChoice) => { + sdk = spec; + spec = sdk; + }, + ToolUseContent: (sdk: SDKTypes.ToolUseContent, spec: SpecTypes.ToolUseContent) => { + sdk = spec; + spec = sdk; + }, + ToolResultContent: (sdk: SDKTypes.ToolResultContent, spec: SpecTypes.ToolResultContent) => { + sdk = spec; + spec = sdk; + }, + SamplingMessageContentBlock: (sdk: SDKTypes.SamplingMessageContentBlock, spec: SpecTypes.SamplingMessageContentBlock) => { + sdk = spec; + spec = sdk; } }; From a39bd7826e6a9145d759838091abfb7b139bd583 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 18 Nov 2025 18:30:13 -0500 Subject: [PATCH 08/11] Fix SDK types to align with MCP spec for sampling tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Remove `disable_parallel_tool_use` from ToolChoiceSchema (not in MCP spec) - Remove unnecessary `.passthrough()` from ToolChoiceSchema - Change CreateMessageResultSchema.role from z.literal("assistant") to z.enum(["user", "assistant"]) to match spec's SamplingMessage.role - Update spec type count from 123 to 127 (4 new sampling tool types) - Fix test accessing .type on content union (could be array) - Add test for CreateMessageResult with array content - Remove test expecting user role to fail (spec allows both roles) Note: 7 type compatibility errors remain due to upstream spec issue where ToolUseContent.input and ToolResultContent.structuredContent use `object` type instead of `{ [key: string]: unknown }`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/spec.types.test.ts | 2 +- src/types.test.ts | 328 +++++++++++++++++++++-------------------- src/types.ts | 133 ++++++++--------- 3 files changed, 231 insertions(+), 232 deletions(-) diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 976430c1b..544a70049 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -647,7 +647,7 @@ describe('Spec Types', () => { it('should define some expected types', () => { expect(specTypes).toContain('JSONRPCNotification'); expect(specTypes).toContain('ElicitResult'); - expect(specTypes).toHaveLength(123); + expect(specTypes).toHaveLength(127); }); it('should have up to date list of missing sdk types', () => { diff --git a/src/types.test.ts b/src/types.test.ts index 0186df706..a5ad66717 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -12,7 +12,7 @@ import { SamplingMessageSchema, CreateMessageRequestSchema, CreateMessageResultSchema, - ClientCapabilitiesSchema, + ClientCapabilitiesSchema } from './types.js'; describe('Types', () => { @@ -319,45 +319,45 @@ describe('Types', () => { }); }); - describe("ToolUseContent", () => { - test("should validate a tool call content", () => { + describe('ToolUseContent', () => { + test('should validate a tool call content', () => { const toolCall = { - type: "tool_use", - id: "call_123", - name: "get_weather", - input: { city: "San Francisco", units: "celsius" } + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'San Francisco', units: 'celsius' } }; const result = ToolUseContentSchema.safeParse(toolCall); expect(result.success).toBe(true); if (result.success) { - expect(result.data.type).toBe("tool_use"); - expect(result.data.id).toBe("call_123"); - expect(result.data.name).toBe("get_weather"); - expect(result.data.input).toEqual({ city: "San Francisco", units: "celsius" }); + expect(result.data.type).toBe('tool_use'); + expect(result.data.id).toBe('call_123'); + expect(result.data.name).toBe('get_weather'); + expect(result.data.input).toEqual({ city: 'San Francisco', units: 'celsius' }); } }); - test("should validate tool call with _meta", () => { + test('should validate tool call with _meta', () => { const toolCall = { - type: "tool_use", - id: "call_456", - name: "search", - input: { query: "test" }, - _meta: { custom: "data" } + type: 'tool_use', + id: 'call_456', + name: 'search', + input: { query: 'test' }, + _meta: { custom: 'data' } }; const result = ToolUseContentSchema.safeParse(toolCall); expect(result.success).toBe(true); if (result.success) { - expect(result.data._meta).toEqual({ custom: "data" }); + expect(result.data._meta).toEqual({ custom: 'data' }); } }); - test("should fail validation for missing required fields", () => { + test('should fail validation for missing required fields', () => { const invalidToolCall = { - type: "tool_use", - name: "test" + type: 'tool_use', + name: 'test' // missing id and input }; @@ -366,43 +366,43 @@ describe('Types', () => { }); }); - describe("ToolResultContent", () => { - test("should validate a tool result content", () => { + describe('ToolResultContent', () => { + test('should validate a tool result content', () => { const toolResult = { - type: "tool_result", - toolUseId: "call_123", - structuredContent: { temperature: 72, condition: "sunny" } + type: 'tool_result', + toolUseId: 'call_123', + structuredContent: { temperature: 72, condition: 'sunny' } }; const result = ToolResultContentSchema.safeParse(toolResult); expect(result.success).toBe(true); if (result.success) { - expect(result.data.type).toBe("tool_result"); - expect(result.data.toolUseId).toBe("call_123"); - expect(result.data.structuredContent).toEqual({ temperature: 72, condition: "sunny" }); + expect(result.data.type).toBe('tool_result'); + expect(result.data.toolUseId).toBe('call_123'); + expect(result.data.structuredContent).toEqual({ temperature: 72, condition: 'sunny' }); } }); - test("should validate tool result with error in content", () => { + test('should validate tool result with error in content', () => { const toolResult = { - type: "tool_result", - toolUseId: "call_456", - structuredContent: { error: "API_ERROR", message: "Service unavailable" }, + type: 'tool_result', + toolUseId: 'call_456', + structuredContent: { error: 'API_ERROR', message: 'Service unavailable' }, isError: true }; const result = ToolResultContentSchema.safeParse(toolResult); expect(result.success).toBe(true); if (result.success) { - expect(result.data.structuredContent).toEqual({ error: "API_ERROR", message: "Service unavailable" }); + expect(result.data.structuredContent).toEqual({ error: 'API_ERROR', message: 'Service unavailable' }); expect(result.data.isError).toBe(true); } }); - test("should fail validation for missing required fields", () => { + test('should fail validation for missing required fields', () => { const invalidToolResult = { - type: "tool_result", - content: { data: "test" } + type: 'tool_result', + content: { data: 'test' } // missing toolUseId }; @@ -411,43 +411,41 @@ describe('Types', () => { }); }); - describe("ToolChoice", () => { - test("should validate tool choice with mode auto", () => { + describe('ToolChoice', () => { + test('should validate tool choice with mode auto', () => { const toolChoice = { - mode: "auto" + mode: 'auto' }; const result = ToolChoiceSchema.safeParse(toolChoice); expect(result.success).toBe(true); if (result.success) { - expect(result.data.mode).toBe("auto"); + expect(result.data.mode).toBe('auto'); } }); - test("should validate tool choice with mode required", () => { + test('should validate tool choice with mode required', () => { const toolChoice = { - mode: "required", - disable_parallel_tool_use: true + mode: 'required' }; const result = ToolChoiceSchema.safeParse(toolChoice); expect(result.success).toBe(true); if (result.success) { - expect(result.data.mode).toBe("required"); - expect(result.data.disable_parallel_tool_use).toBe(true); + expect(result.data.mode).toBe('required'); } }); - test("should validate empty tool choice", () => { + test('should validate empty tool choice', () => { const toolChoice = {}; const result = ToolChoiceSchema.safeParse(toolChoice); expect(result.success).toBe(true); }); - test("should fail validation for invalid mode", () => { + test('should fail validation for invalid mode', () => { const invalidToolChoice = { - mode: "invalid" + mode: 'invalid' }; const result = ToolChoiceSchema.safeParse(invalidToolChoice); @@ -455,29 +453,29 @@ describe('Types', () => { }); }); - describe("SamplingMessage content types", () => { - test("should validate user message with text", () => { + describe('SamplingMessage content types', () => { + test('should validate user message with text', () => { const userMessage = { - role: "user", - content: { type: "text", text: "What's the weather?" } + role: 'user', + content: { type: 'text', text: "What's the weather?" } }; const result = SamplingMessageSchema.safeParse(userMessage); expect(result.success).toBe(true); if (result.success) { - expect(result.data.role).toBe("user"); + expect(result.data.role).toBe('user'); if (!Array.isArray(result.data.content)) { - expect(result.data.content.type).toBe("text"); + expect(result.data.content.type).toBe('text'); } } }); - test("should validate user message with tool result", () => { + test('should validate user message with tool result', () => { const userMessage = { - role: "user", + role: 'user', content: { - type: "tool_result", - toolUseId: "call_123", + type: 'tool_result', + toolUseId: 'call_123', content: [] } }; @@ -485,48 +483,48 @@ describe('Types', () => { const result = SamplingMessageSchema.safeParse(userMessage); expect(result.success).toBe(true); if (result.success && !Array.isArray(result.data.content)) { - expect(result.data.content.type).toBe("tool_result"); + expect(result.data.content.type).toBe('tool_result'); } }); - test("should validate assistant message with text", () => { + test('should validate assistant message with text', () => { const assistantMessage = { - role: "assistant", - content: { type: "text", text: "I'll check the weather for you." } + role: 'assistant', + content: { type: 'text', text: "I'll check the weather for you." } }; const result = SamplingMessageSchema.safeParse(assistantMessage); expect(result.success).toBe(true); if (result.success) { - expect(result.data.role).toBe("assistant"); + expect(result.data.role).toBe('assistant'); } }); - test("should validate assistant message with tool call", () => { + test('should validate assistant message with tool call', () => { const assistantMessage = { - role: "assistant", + role: 'assistant', content: { - type: "tool_use", - id: "call_123", - name: "get_weather", - input: { city: "SF" } + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'SF' } } }; const result = SamplingMessageSchema.safeParse(assistantMessage); expect(result.success).toBe(true); if (result.success && !Array.isArray(result.data.content)) { - expect(result.data.content.type).toBe("tool_use"); + expect(result.data.content.type).toBe('tool_use'); } }); - test("should validate any content type for any role", () => { + test('should validate any content type for any role', () => { // The simplified schema allows any content type for any role const assistantWithToolResult = { - role: "assistant", + role: 'assistant', content: { - type: "tool_result", - toolUseId: "call_123", + type: 'tool_result', + toolUseId: 'call_123', content: [] } }; @@ -535,11 +533,11 @@ describe('Types', () => { expect(result1.success).toBe(true); const userWithToolUse = { - role: "user", + role: 'user', content: { - type: "tool_use", - id: "call_123", - name: "test", + type: 'tool_use', + id: 'call_123', + name: 'test', input: {} } }; @@ -549,42 +547,40 @@ describe('Types', () => { }); }); - describe("SamplingMessage", () => { - test("should validate user message via discriminated union", () => { + describe('SamplingMessage', () => { + test('should validate user message via discriminated union', () => { const message = { - role: "user", - content: { type: "text", text: "Hello" } + role: 'user', + content: { type: 'text', text: 'Hello' } }; const result = SamplingMessageSchema.safeParse(message); expect(result.success).toBe(true); if (result.success) { - expect(result.data.role).toBe("user"); + expect(result.data.role).toBe('user'); } }); - test("should validate assistant message via discriminated union", () => { + test('should validate assistant message via discriminated union', () => { const message = { - role: "assistant", - content: { type: "text", text: "Hi there!" } + role: 'assistant', + content: { type: 'text', text: 'Hi there!' } }; const result = SamplingMessageSchema.safeParse(message); expect(result.success).toBe(true); if (result.success) { - expect(result.data.role).toBe("assistant"); + expect(result.data.role).toBe('assistant'); } }); }); - describe("CreateMessageRequest", () => { - test("should validate request without tools", () => { + describe('CreateMessageRequest', () => { + test('should validate request without tools', () => { const request = { - method: "sampling/createMessage", + method: 'sampling/createMessage', params: { - messages: [ - { role: "user", content: { type: "text", text: "Hello" } } - ], + messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }], maxTokens: 1000 } }; @@ -596,29 +592,27 @@ describe('Types', () => { } }); - test("should validate request with tools", () => { + test('should validate request with tools', () => { const request = { - method: "sampling/createMessage", + method: 'sampling/createMessage', params: { - messages: [ - { role: "user", content: { type: "text", text: "What's the weather?" } } - ], + messages: [{ role: 'user', content: { type: 'text', text: "What's the weather?" } }], maxTokens: 1000, tools: [ { - name: "get_weather", - description: "Get weather for a location", + name: 'get_weather', + description: 'Get weather for a location', inputSchema: { - type: "object", + type: 'object', properties: { - location: { type: "string" } + location: { type: 'string' } }, - required: ["location"] + required: ['location'] } } ], toolChoice: { - mode: "auto" + mode: 'auto' } } }; @@ -627,76 +621,108 @@ describe('Types', () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.params.tools).toHaveLength(1); - expect(result.data.params.toolChoice?.mode).toBe("auto"); + expect(result.data.params.toolChoice?.mode).toBe('auto'); } }); - test("should validate request with includeContext (soft-deprecated)", () => { + test('should validate request with includeContext (soft-deprecated)', () => { const request = { - method: "sampling/createMessage", + method: 'sampling/createMessage', params: { - messages: [ - { role: "user", content: { type: "text", text: "Help" } } - ], + messages: [{ role: 'user', content: { type: 'text', text: 'Help' } }], maxTokens: 1000, - includeContext: "thisServer" + includeContext: 'thisServer' } }; const result = CreateMessageRequestSchema.safeParse(request); expect(result.success).toBe(true); if (result.success) { - expect(result.data.params.includeContext).toBe("thisServer"); + expect(result.data.params.includeContext).toBe('thisServer'); } }); }); - describe("CreateMessageResult", () => { - test("should validate result with text content", () => { + describe('CreateMessageResult', () => { + test('should validate result with text content', () => { const result = { - model: "claude-3-5-sonnet-20241022", - role: "assistant", - content: { type: "text", text: "Here's the answer." }, - stopReason: "endTurn" + model: 'claude-3-5-sonnet-20241022', + role: 'assistant', + content: { type: 'text', text: "Here's the answer." }, + stopReason: 'endTurn' }; const parseResult = CreateMessageResultSchema.safeParse(result); expect(parseResult.success).toBe(true); if (parseResult.success) { - expect(parseResult.data.role).toBe("assistant"); - expect(parseResult.data.stopReason).toBe("endTurn"); + expect(parseResult.data.role).toBe('assistant'); + expect(parseResult.data.stopReason).toBe('endTurn'); } }); - test("should validate result with tool call", () => { + test('should validate result with tool call', () => { const result = { - model: "claude-3-5-sonnet-20241022", - role: "assistant", + model: 'claude-3-5-sonnet-20241022', + role: 'assistant', content: { - type: "tool_use", - id: "call_123", - name: "get_weather", - input: { city: "SF" } + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'SF' } }, - stopReason: "toolUse" + stopReason: 'toolUse' + }; + + const parseResult = CreateMessageResultSchema.safeParse(result); + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.stopReason).toBe('toolUse'); + const content = parseResult.data.content; + expect(Array.isArray(content)).toBe(false); + if (!Array.isArray(content)) { + expect(content.type).toBe('tool_use'); + } + } + }); + + test('should validate result with array content', () => { + const result = { + model: 'claude-3-5-sonnet-20241022', + role: 'assistant', + content: [ + { type: 'text', text: 'Let me check the weather.' }, + { + type: 'tool_use', + id: 'call_123', + name: 'get_weather', + input: { city: 'SF' } + } + ], + stopReason: 'toolUse' }; const parseResult = CreateMessageResultSchema.safeParse(result); expect(parseResult.success).toBe(true); if (parseResult.success) { - expect(parseResult.data.stopReason).toBe("toolUse"); - expect(parseResult.data.content.type).toBe("tool_use"); + expect(parseResult.data.stopReason).toBe('toolUse'); + const content = parseResult.data.content; + expect(Array.isArray(content)).toBe(true); + if (Array.isArray(content)) { + expect(content).toHaveLength(2); + expect(content[0].type).toBe('text'); + expect(content[1].type).toBe('tool_use'); + } } }); - test("should validate all new stop reasons", () => { - const stopReasons = ["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]; + test('should validate all new stop reasons', () => { + const stopReasons = ['endTurn', 'stopSequence', 'maxTokens', 'toolUse', 'refusal', 'other']; stopReasons.forEach(stopReason => { const result = { - model: "test", - role: "assistant", - content: { type: "text", text: "test" }, + model: 'test', + role: 'assistant', + content: { type: 'text', text: 'test' }, stopReason }; @@ -705,32 +731,22 @@ describe('Types', () => { }); }); - test("should allow custom stop reason string", () => { + test('should allow custom stop reason string', () => { const result = { - model: "test", - role: "assistant", - content: { type: "text", text: "test" }, - stopReason: "custom_provider_reason" + model: 'test', + role: 'assistant', + content: { type: 'text', text: 'test' }, + stopReason: 'custom_provider_reason' }; const parseResult = CreateMessageResultSchema.safeParse(result); expect(parseResult.success).toBe(true); }); - test("should fail for user role in result", () => { - const result = { - model: "test", - role: "user", - content: { type: "text", text: "test" } - }; - - const parseResult = CreateMessageResultSchema.safeParse(result); - expect(parseResult.success).toBe(false); - }); - }); + }); - describe("ClientCapabilities with sampling", () => { - test("should validate capabilities with sampling.tools", () => { + describe('ClientCapabilities with sampling', () => { + test('should validate capabilities with sampling.tools', () => { const capabilities = { sampling: { tools: {} @@ -744,7 +760,7 @@ describe('Types', () => { } }); - test("should validate capabilities with sampling.context", () => { + test('should validate capabilities with sampling.context', () => { const capabilities = { sampling: { context: {} @@ -758,7 +774,7 @@ describe('Types', () => { } }); - test("should validate capabilities with both", () => { + test('should validate capabilities with both', () => { const capabilities = { sampling: { context: {}, diff --git a/src/types.ts b/src/types.ts index ae96ac112..2cf897ed9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -320,7 +320,7 @@ export const ClientCapabilitiesSchema = z.object({ /** * Present if the client supports tool use via tools and toolChoice parameters. */ - tools: AssertObjectSchema.optional(), + tools: AssertObjectSchema.optional() }) .optional(), /** @@ -849,30 +849,30 @@ export const AudioContentSchema = z.object({ * Represents the assistant's request to use a tool. */ export const ToolUseContentSchema = z - .object({ - type: z.literal("tool_use"), - /** - * The name of the tool to invoke. - * Must match a tool name from the request's tools array. - */ - name: z.string(), - /** - * Unique identifier for this tool call. - * Used to correlate with ToolResultContent in subsequent messages. - */ - id: z.string(), - /** - * Arguments to pass to the tool. - * Must conform to the tool's inputSchema. - */ - input: z.object({}).passthrough(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + .object({ + type: z.literal('tool_use'), + /** + * The name of the tool to invoke. + * Must match a tool name from the request's tools array. + */ + name: z.string(), + /** + * Unique identifier for this tool call. + * Used to correlate with ToolResultContent in subsequent messages. + */ + id: z.string(), + /** + * Arguments to pass to the tool. + * Must conform to the tool's inputSchema. + */ + input: z.object({}).passthrough(), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.object({}).passthrough()) + }) + .passthrough(); /** * The contents of a resource, embedded into a prompt or tool call result. @@ -1203,23 +1203,15 @@ export const ModelPreferencesSchema = z.object({ /** * Controls tool usage behavior in sampling requests. */ -export const ToolChoiceSchema = z - .object({ - /** - * Controls when tools are used: - * - "auto": Model decides whether to use tools (default) - * - "required": Model MUST use at least one tool before completing - * - "none": Model MUST NOT use any tools - */ - mode: z.optional(z.enum(["auto", "required", "none"])), - /** - * If true, model should not use multiple tools in parallel. - * Some models may ignore this hint. - * Default: false - */ - disable_parallel_tool_use: z.optional(z.boolean()), - }) - .passthrough(); +export const ToolChoiceSchema = z.object({ + /** + * Controls when tools are used: + * - "auto": Model decides whether to use tools (default) + * - "required": Model MUST use at least one tool before completing + * - "none": Model MUST NOT use any tools + */ + mode: z.optional(z.enum(['auto', 'required', 'none'])) +}); /** * The result of a tool execution, provided by the user (server). @@ -1227,8 +1219,8 @@ export const ToolChoiceSchema = z */ export const ToolResultContentSchema = z .object({ - type: z.literal("tool_result"), - toolUseId: z.string().describe("The unique identifier for the corresponding tool call."), + type: z.literal('tool_result'), + toolUseId: z.string().describe('The unique identifier for the corresponding tool call.'), content: z.array(ContentBlockSchema).default([]), structuredContent: z.object({}).passthrough().optional(), isError: z.optional(z.boolean()), @@ -1237,7 +1229,7 @@ export const ToolResultContentSchema = z * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.object({}).passthrough()) }) .passthrough(); @@ -1245,29 +1237,28 @@ export const ToolResultContentSchema = z * Content block types allowed in sampling messages. * This includes text, image, audio, tool use requests, and tool results. */ -export const SamplingMessageContentBlockSchema = z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolUseContentSchema, - ToolResultContentSchema, +export const SamplingMessageContentBlockSchema = z.discriminatedUnion('type', [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolUseContentSchema, + ToolResultContentSchema ]); /** * Describes a message issued to or received from an LLM API. */ -export const SamplingMessageSchema = z.object({ - role: z.enum(['user', 'assistant']), - content: z.union([ - SamplingMessageContentBlockSchema, - z.array(SamplingMessageContentBlockSchema) - ]), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); +export const SamplingMessageSchema = z + .object({ + role: z.enum(['user', 'assistant']), + content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)]), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.object({}).passthrough()) + }) + .passthrough(); /** * Parameters for a `sampling/createMessage` request. @@ -1312,7 +1303,7 @@ export const CreateMessageRequestParamsSchema = BaseRequestParamsSchema.extend({ * The client MUST return an error if this field is provided but ClientCapabilities.sampling.tools is not declared. * Default is `{ mode: "auto" }`. */ - toolChoice: z.optional(ToolChoiceSchema), + toolChoice: z.optional(ToolChoiceSchema) }); /** * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. @@ -1341,20 +1332,12 @@ export const CreateMessageResultSchema = ResultSchema.extend({ * * This field is an open string to allow for provider-specific stop reasons. */ - stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse"]).or(z.string()), - ), - /** - * The role is always "assistant" in responses from the LLM. - */ - role: z.literal("assistant"), + stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens', 'toolUse']).or(z.string())), + role: z.enum(['user', 'assistant']), /** * Response content. May be ToolUseContent if stopReason is "toolUse". */ - content: z.union([ - SamplingMessageContentBlockSchema, - z.array(SamplingMessageContentBlockSchema) - ]), + content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)]) }); /* Elicitation */ From 2009833647272b8d2e9619bf01a62d2415b36b04 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 19 Nov 2025 18:37:26 +0000 Subject: [PATCH 09/11] updated spec types related to sampling sep --- src/spec.types.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/spec.types.ts b/src/spec.types.ts index da1d4cf47..6ce24059e 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -1526,7 +1526,7 @@ export interface ToolUseContent { /** * The arguments to pass to the tool, conforming to the tool's input schema. */ - input: object; + input: { [key: string]: unknown }; /** * Optional metadata about the tool use. Clients SHOULD preserve this field when @@ -1565,7 +1565,7 @@ export interface ToolResultContent { * * If the tool defined an outputSchema, this SHOULD conform to that schema. */ - structuredContent?: object; + structuredContent?: { [key: string]: unknown }; /** * Whether the tool use resulted in an error. @@ -1902,7 +1902,6 @@ export interface ElicitRequest extends JSONRPCRequest { params: ElicitRequestParams; } -/** /** * Restricted schema definitions that only allow primitive types * without nested objects or arrays. From 53b81f8f17e6a3dc2ba68cb1aa753c3941271933 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 19 Nov 2025 20:37:11 +0000 Subject: [PATCH 10/11] fix spec type checks --- src/spec.types.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 544a70049..5a9986450 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -141,7 +141,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - CreateMessageRequestParams: (sdk: SDKTypes.CreateMessageRequestParams, spec: SpecTypes.CreateMessageRequestParams) => { + CreateMessageRequestParams: (sdk: RemovePassthrough, spec: SpecTypes.CreateMessageRequestParams) => { sdk = spec; spec = sdk; }, @@ -351,11 +351,11 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - SamplingMessage: (sdk: SDKTypes.SamplingMessage, spec: SpecTypes.SamplingMessage) => { + SamplingMessage: (sdk: RemovePassthrough, spec: SpecTypes.SamplingMessage) => { sdk = spec; spec = sdk; }, - CreateMessageResult: (sdk: SDKTypes.CreateMessageResult, spec: SpecTypes.CreateMessageResult) => { + CreateMessageResult: (sdk: RemovePassthrough, spec: SpecTypes.CreateMessageResult) => { sdk = spec; spec = sdk; }, @@ -535,7 +535,7 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - CreateMessageRequest: (sdk: WithJSONRPCRequest, spec: SpecTypes.CreateMessageRequest) => { + CreateMessageRequest: (sdk: RemovePassthrough>, spec: SpecTypes.CreateMessageRequest) => { sdk = spec; spec = sdk; }, @@ -607,15 +607,15 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - ToolUseContent: (sdk: SDKTypes.ToolUseContent, spec: SpecTypes.ToolUseContent) => { + ToolUseContent: (sdk: RemovePassthrough, spec: SpecTypes.ToolUseContent) => { sdk = spec; spec = sdk; }, - ToolResultContent: (sdk: SDKTypes.ToolResultContent, spec: SpecTypes.ToolResultContent) => { + ToolResultContent: (sdk: RemovePassthrough, spec: SpecTypes.ToolResultContent) => { sdk = spec; spec = sdk; }, - SamplingMessageContentBlock: (sdk: SDKTypes.SamplingMessageContentBlock, spec: SpecTypes.SamplingMessageContentBlock) => { + SamplingMessageContentBlock: (sdk: RemovePassthrough, spec: SpecTypes.SamplingMessageContentBlock) => { sdk = spec; spec = sdk; } From 9b40c7f436e9542a0f9330290b8091d9ab4ae761 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 19 Nov 2025 21:56:22 +0000 Subject: [PATCH 11/11] npm run lint:fix --- src/spec.types.test.ts | 5 ++++- src/types.test.ts | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index c7825bb57..1c0b6ab5d 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -627,7 +627,10 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, - SamplingMessageContentBlock: (sdk: RemovePassthrough, spec: SpecTypes.SamplingMessageContentBlock) => { + SamplingMessageContentBlock: ( + sdk: RemovePassthrough, + spec: SpecTypes.SamplingMessageContentBlock + ) => { sdk = spec; spec = sdk; } diff --git a/src/types.test.ts b/src/types.test.ts index a5ad66717..3f6f83a14 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -742,8 +742,7 @@ describe('Types', () => { const parseResult = CreateMessageResultSchema.safeParse(result); expect(parseResult.success).toBe(true); }); - - }); + }); describe('ClientCapabilities with sampling', () => { test('should validate capabilities with sampling.tools', () => {