From 9286b2578b8148ca1f0df755e0705685e95a4dbc Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 14:28:10 +0100 Subject: [PATCH 01/33] feat(types): Implement SEP-1577 - Sampling with Tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive tool calling support to MCP sampling: - New content types: ToolCallContent and ToolResultContent - Split SamplingMessage into role-specific UserMessage/AssistantMessage - Add ToolChoice schema for controlling tool usage behavior - Update CreateMessageRequest with tools and tool_choice parameters - Update CreateMessageResult with new stop reasons (toolUse, refusal, other) - Enhance ClientCapabilities.sampling with context and tools sub-capabilities - Mark includeContext as soft-deprecated in favor of explicit tools - Add comprehensive unit tests (27 new test cases covering all new schemas) All tests pass (47/47 in types.test.ts). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/types.test.ts | 468 +++++++++++++++++- src/types.ts | 182 ++++++- tmp/client.mjs | 43 ++ tmp/client.py | 66 +++ tmp/package-lock.json | 1051 +++++++++++++++++++++++++++++++++++++++++ tmp/package.json | 15 + 6 files changed, 1816 insertions(+), 9 deletions(-) create mode 100644 tmp/client.mjs create mode 100755 tmp/client.py create mode 100644 tmp/package-lock.json create mode 100644 tmp/package.json diff --git a/src/types.test.ts b/src/types.test.ts index 0aee62a93..f4bc2c886 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -5,7 +5,16 @@ import { ContentBlockSchema, PromptMessageSchema, CallToolResultSchema, - CompleteRequestSchema + CompleteRequestSchema, + ToolCallContentSchema, + ToolResultContentSchema, + ToolChoiceSchema, + UserMessageSchema, + AssistantMessageSchema, + SamplingMessageSchema, + CreateMessageRequestSchema, + CreateMessageResultSchema, + ClientCapabilitiesSchema, } from "./types.js"; describe("Types", () => { @@ -312,4 +321,461 @@ describe("Types", () => { } }); }); + + describe("SEP-1577: Sampling with Tools", () => { + describe("ToolCallContent", () => { + 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 = ToolCallContentSchema.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 = ToolCallContentSchema.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 = ToolCallContentSchema.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" }); + expect(result.data.isError).toBeUndefined(); + } + }); + + test("should validate tool result with error", () => { + const toolResult = { + type: "tool_result", + toolUseId: "call_456", + content: { error: "API_ERROR", message: "Service unavailable" }, + isError: true + }; + + const result = ToolResultContentSchema.safeParse(toolResult); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.isError).toBe(true); + } + }); + + 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 ee2ceb5ed..3b9085e71 100644 --- a/src/types.ts +++ b/src/types.ts @@ -285,7 +285,22 @@ export const ClientCapabilitiesSchema = z /** * Present if the client supports sampling from an LLM. */ - sampling: z.optional(z.object({}).passthrough()), + sampling: z.optional( + z + .object({ + /** + * Present if the client supports non-'none' values for includeContext parameter. + * SOFT-DEPRECATED: New implementations should use tools parameter instead. + */ + context: z.optional(z.object({}).passthrough()), + /** + * Present if the client supports tools and tool_choice parameters in sampling requests. + * Presence indicates full tool calling support. + */ + tools: z.optional(z.object({}).passthrough()), + }) + .passthrough(), + ), /** * Present if the client supports eliciting user input. */ @@ -821,6 +836,67 @@ export const AudioContentSchema = z }) .passthrough(); +/** + * A tool call request from an assistant (LLM). + * Represents the assistant's request to use a tool. + */ +export const ToolCallContentSchema = 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 result of a tool execution, provided by the user (server). + * Represents the outcome of invoking a tool requested via ToolCallContent. + */ +export const ToolResultContentSchema = z + .object({ + type: z.literal("tool_result"), + /** + * The ID of the tool call this result corresponds to. + * Must match a ToolCallContent.id from a previous assistant message. + */ + toolUseId: z.string(), + /** + * The result of the tool execution. + * Can be any JSON-serializable object. + * May include error information if the tool failed. + */ + content: z.object({}).passthrough(), + /** + * If true, indicates the tool execution failed. + * The content should contain error details. + */ + 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(); + /** * The contents of a resource, embedded into a prompt or tool call result. */ @@ -1147,15 +1223,75 @@ export const ModelPreferencesSchema = z .passthrough(); /** - * Describes a message issued to or received from an LLM API. + * Controls tool usage behavior in sampling requests. */ -export const SamplingMessageSchema = z +export const ToolChoiceSchema = z .object({ - role: z.enum(["user", "assistant"]), - content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), + /** + * 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"])), + /** + * 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(); + +/** + * A message from the user (server) in a sampling conversation. + */ +export const UserMessageSchema = z + .object({ + role: z.literal("user"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolResultContentSchema, + ]), + /** + * 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(); +/** + * A message from the assistant (LLM) in a sampling conversation. + */ +export const AssistantMessageSchema = z + .object({ + role: z.literal("assistant"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), + /** + * 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, +]); + /** * 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. */ @@ -1168,7 +1304,9 @@ export const CreateMessageRequestSchema = RequestSchema.extend({ */ systemPrompt: z.optional(z.string()), /** + * SOFT-DEPRECATED: Use tools parameter instead. * 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. + * Requires clientCapabilities.sampling.context. */ includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), temperature: z.optional(z.number()), @@ -1185,6 +1323,16 @@ export const CreateMessageRequestSchema = RequestSchema.extend({ * The server's preferences for which model to select. */ modelPreferences: z.optional(ModelPreferencesSchema), + /** + * 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. + */ + tool_choice: z.optional(ToolChoiceSchema), }), }); @@ -1198,15 +1346,28 @@ export const CreateMessageResultSchema = ResultSchema.extend({ model: z.string(), /** * The reason why sampling stopped. + * - "endTurn": Model completed naturally + * - "stopSequence": Hit a stop sequence + * - "maxTokens": Reached token limit + * - "toolUse": Model wants to use a tool + * - "refusal": Model refused the request + * - "other": Other provider-specific reason */ stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens"]).or(z.string()), + z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]).or(z.string()), ), - role: z.enum(["user", "assistant"]), + /** + * The role is always "assistant" in responses from the LLM. + */ + role: z.literal("assistant"), + /** + * Response content. May be ToolCallContent if stopReason is "toolUse". + */ content: z.discriminatedUnion("type", [ TextContentSchema, ImageContentSchema, - AudioContentSchema + AudioContentSchema, + ToolCallContentSchema, ]), }); @@ -1630,6 +1791,8 @@ export type GetPromptRequest = Infer; export type TextContent = Infer; export type ImageContent = Infer; export type AudioContent = Infer; +export type ToolCallContent = Infer; +export type ToolResultContent = Infer; export type EmbeddedResource = Infer; export type ResourceLink = Infer; export type ContentBlock = Infer; @@ -1653,6 +1816,9 @@ export type SetLevelRequest = Infer; export type LoggingMessageNotification = Infer; /* Sampling */ +export type ToolChoice = Infer; +export type UserMessage = Infer; +export type AssistantMessage = Infer; export type SamplingMessage = Infer; export type CreateMessageRequest = Infer; export type CreateMessageResult = Infer; diff --git a/tmp/client.mjs b/tmp/client.mjs new file mode 100644 index 000000000..c32572836 --- /dev/null +++ b/tmp/client.mjs @@ -0,0 +1,43 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +const transport = new StdioClientTransport({ + command: "uvx", + args:[ + "--quiet", + "--refresh", + "git+https://github.com/emsi/slow-mcp", + "--transport", + "stdio", +] +}); + +const client = new Client( + { + name: "example-client", + version: "1.0.0" + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {} + } + } +); + +await client.connect(transport); + +const tools = await client.listTools(); + +console.log(tools); + +// Call a tool +const result = await client.callTool({ + name: "run_command", +}, { + timeout: 300000, +}); + + +console.log(result); diff --git a/tmp/client.py b/tmp/client.py new file mode 100755 index 000000000..cecaffc8f --- /dev/null +++ b/tmp/client.py @@ -0,0 +1,66 @@ +#!/usr/bin/env uv run +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "mcp", +# ] +# /// +import datetime +from mcp import ClientSession, StdioServerParameters, types +from mcp.client.stdio import stdio_client + +# Create server parameters for stdio connection +server_params = StdioServerParameters( + command="uvx", # Executable + args=[ + "--quiet", + "--refresh", + "git+https://github.com/emsi/slow-mcp", + "--transport", + "stdio", + ], + env=None, # Optional environment variables +) + + +# Optional: create a sampling callback +async def handle_sampling_message( + message: types.CreateMessageRequestParams, +) -> types.CreateMessageResult: + return types.CreateMessageResult( + role="assistant", + content=types.TextContent( + type="text", + text="Hello, world! from model", + ), + model="gpt-3.5-turbo", + stopReason="endTurn", + ) + + +async def run(): + async with stdio_client(server_params) as (read, write): + async with ClientSession( + read, write, #sampling_callback=handle_sampling_message + read_timeout_seconds=datetime.timedelta(seconds=60), + ) as session: + # Initialize the connection + await session.initialize() + + resources = await session.list_resources() + + # List available tools + tools = await session.list_tools() + + print(f"Tools: {tools}") + + # Call a tool + result = await session.call_tool("run_command") + + print(f"Result: {result}") + + +if __name__ == "__main__": + import asyncio + + asyncio.run(run()) diff --git a/tmp/package-lock.json b/tmp/package-lock.json new file mode 100644 index 000000000..a1991ddb4 --- /dev/null +++ b/tmp/package-lock.json @@ -0,0 +1,1051 @@ +{ + "name": "tmp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tmp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.5" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/wrappy/1.0.2/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/tmp/package.json b/tmp/package.json new file mode 100644 index 000000000..8959cf3b0 --- /dev/null +++ b/tmp/package.json @@ -0,0 +1,15 @@ +{ + "name": "tmp", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.5" + } +} From 58ea5dc8d3c07692729016a90e83a5d741a06a2c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 15:12:17 +0100 Subject: [PATCH 02/33] rm tmp --- tmp/client.mjs | 43 -- tmp/client.py | 66 --- tmp/package-lock.json | 1051 ----------------------------------------- tmp/package.json | 15 - 4 files changed, 1175 deletions(-) delete mode 100644 tmp/client.mjs delete mode 100755 tmp/client.py delete mode 100644 tmp/package-lock.json delete mode 100644 tmp/package.json diff --git a/tmp/client.mjs b/tmp/client.mjs deleted file mode 100644 index c32572836..000000000 --- a/tmp/client.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; - -const transport = new StdioClientTransport({ - command: "uvx", - args:[ - "--quiet", - "--refresh", - "git+https://github.com/emsi/slow-mcp", - "--transport", - "stdio", -] -}); - -const client = new Client( - { - name: "example-client", - version: "1.0.0" - }, - { - capabilities: { - prompts: {}, - resources: {}, - tools: {} - } - } -); - -await client.connect(transport); - -const tools = await client.listTools(); - -console.log(tools); - -// Call a tool -const result = await client.callTool({ - name: "run_command", -}, { - timeout: 300000, -}); - - -console.log(result); diff --git a/tmp/client.py b/tmp/client.py deleted file mode 100755 index cecaffc8f..000000000 --- a/tmp/client.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env uv run -# /// script -# requires-python = ">=3.11" -# dependencies = [ -# "mcp", -# ] -# /// -import datetime -from mcp import ClientSession, StdioServerParameters, types -from mcp.client.stdio import stdio_client - -# Create server parameters for stdio connection -server_params = StdioServerParameters( - command="uvx", # Executable - args=[ - "--quiet", - "--refresh", - "git+https://github.com/emsi/slow-mcp", - "--transport", - "stdio", - ], - env=None, # Optional environment variables -) - - -# Optional: create a sampling callback -async def handle_sampling_message( - message: types.CreateMessageRequestParams, -) -> types.CreateMessageResult: - return types.CreateMessageResult( - role="assistant", - content=types.TextContent( - type="text", - text="Hello, world! from model", - ), - model="gpt-3.5-turbo", - stopReason="endTurn", - ) - - -async def run(): - async with stdio_client(server_params) as (read, write): - async with ClientSession( - read, write, #sampling_callback=handle_sampling_message - read_timeout_seconds=datetime.timedelta(seconds=60), - ) as session: - # Initialize the connection - await session.initialize() - - resources = await session.list_resources() - - # List available tools - tools = await session.list_tools() - - print(f"Tools: {tools}") - - # Call a tool - result = await session.call_tool("run_command") - - print(f"Result: {result}") - - -if __name__ == "__main__": - import asyncio - - asyncio.run(run()) diff --git a/tmp/package-lock.json b/tmp/package-lock.json deleted file mode 100644 index a1991ddb4..000000000 --- a/tmp/package-lock.json +++ /dev/null @@ -1,1051 +0,0 @@ -{ - "name": "tmp", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "tmp", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.5" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", - "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.6", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/wrappy/1.0.2/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "license": "ISC" - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - } - } -} diff --git a/tmp/package.json b/tmp/package.json deleted file mode 100644 index 8959cf3b0..000000000 --- a/tmp/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "tmp", - "version": "1.0.0", - "description": "", - "license": "ISC", - "author": "", - "type": "commonjs", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.5" - } -} From 353789a4458470037ace3cd85766832eb0793bc9 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 15:12:54 +0100 Subject: [PATCH 03/33] refactor: Remove isError from ToolResultContent to align with Claude/OpenAI APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the non-standard isError field from ToolResultContentSchema. Errors should be represented in the content object itself, matching the standard behavior of Claude and OpenAI tool result APIs. Updated tests to validate error content directly without isError flag. All tests pass (47/47 in types.test.ts, 683/683 in full suite). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/types.test.ts | 8 +++----- src/types.ts | 7 +------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/types.test.ts b/src/types.test.ts index f4bc2c886..2bc1be00d 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -384,22 +384,20 @@ describe("Types", () => { 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.isError).toBeUndefined(); } }); - test("should validate tool result with error", () => { + test("should validate tool result with error in content", () => { const toolResult = { type: "tool_result", toolUseId: "call_456", - content: { error: "API_ERROR", message: "Service unavailable" }, - isError: true + content: { error: "API_ERROR", message: "Service unavailable" } }; const result = ToolResultContentSchema.safeParse(toolResult); expect(result.success).toBe(true); if (result.success) { - expect(result.data.isError).toBe(true); + expect(result.data.content).toEqual({ error: "API_ERROR", message: "Service unavailable" }); } }); diff --git a/src/types.ts b/src/types.ts index 3b9085e71..6085d760e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -881,14 +881,9 @@ export const ToolResultContentSchema = z /** * The result of the tool execution. * Can be any JSON-serializable object. - * May include error information if the tool failed. + * Error information should be included in the content itself. */ content: z.object({}).passthrough(), - /** - * If true, indicates the tool execution failed. - * The content should contain error details. - */ - 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. From 3c287c8b6f4fe2f7113ad004afadaff03c87cf0f Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 15:16:33 +0100 Subject: [PATCH 04/33] add sampling backfill example --- package-lock.json | 22 ++ package.json | 1 + src/examples/backfill/backfillSampling.ts | 236 ++++++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 src/examples/backfill/backfillSampling.ts diff --git a/package-lock.json b/package-lock.json index c94ab5c9c..75e963d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "zod-to-json-schema": "^3.24.1" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.65.0", "@eslint/js": "^9.8.0", "@jest-mock/express": "^3.0.0", "@types/content-type": "^1.1.8", @@ -61,6 +62,27 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.65.0.tgz", + "integrity": "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", diff --git a/package.json b/package.json index b5b9b8ec9..b1eaa902d 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "zod-to-json-schema": "^3.24.1" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.65.0", "@eslint/js": "^9.8.0", "@jest-mock/express": "^3.0.0", "@types/content-type": "^1.1.8", diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts new file mode 100644 index 000000000..24a780523 --- /dev/null +++ b/src/examples/backfill/backfillSampling.ts @@ -0,0 +1,236 @@ +/* + This example implements an stdio MCP proxy that backfills sampling requests using the Claude API. + + Usage: + npx -y @modelcontextprotocol/inspector \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ + npx -y --silent @modelcontextprotocol/server-everything +*/ + +import { Anthropic } from "@anthropic-ai/sdk"; +import { Base64ImageSource, ContentBlock, ContentBlockParam, MessageParam } from "@anthropic-ai/sdk/resources/messages.js"; +import { StdioServerTransport } from '../../server/stdio.js'; +import { StdioClientTransport } from '../../client/stdio.js'; +import { + CancelledNotification, + CancelledNotificationSchema, + isInitializeRequest, + isJSONRPCRequest, + ElicitRequest, + ElicitRequestSchema, + CreateMessageRequest, + CreateMessageRequestSchema, + CreateMessageResult, + JSONRPCResponse, + isInitializedNotification, + CallToolRequest, + CallToolRequestSchema, + isJSONRPCNotification, +} from "../../types.js"; +import { Transport } from "../../shared/transport.js"; + +// TODO: move to SDK + +const isCancelledNotification: (value: unknown) => value is CancelledNotification = + ((value: any) => CancelledNotificationSchema.safeParse(value).success) as any; + +const isCallToolRequest: (value: unknown) => value is CallToolRequest = + ((value: any) => CallToolRequestSchema.safeParse(value).success) as any; + +const isElicitRequest: (value: unknown) => value is ElicitRequest = + ((value: any) => ElicitRequestSchema.safeParse(value).success) as any; + +const isCreateMessageRequest: (value: unknown) => value is CreateMessageRequest = + ((value: any) => CreateMessageRequestSchema.safeParse(value).success) as any; + + +function contentToMcp(content: ContentBlock): CreateMessageResult['content'][number] { + switch (content.type) { + case 'text': + return {type: 'text', text: content.text}; + default: + throw new Error(`Unsupported content type: ${content.type}`); + } +} + +function contentFromMcp(content: CreateMessageRequest['params']['messages'][number]['content']): ContentBlockParam { + switch (content.type) { + case 'text': + return {type: 'text', text: content.text}; + case 'image': + return { + type: 'image', + source: { + data: content.data, + media_type: content.mimeType as Base64ImageSource['media_type'], + type: 'base64', + }, + }; + case 'audio': + default: + throw new Error(`Unsupported content type: ${content.type}`); + } +} + +export type NamedTransport = { + name: 'client' | 'server', + transport: T, +} + +export async function setupBackfill(client: NamedTransport, server: NamedTransport, api: Anthropic) { + const backfillMeta = await (async () => { + const models = new Set(); + let defaultModel: string | undefined; + for await (const info of api.models.list()) { + models.add(info.id); + if (info.id.indexOf('sonnet') >= 0 && defaultModel === undefined) { + defaultModel = info.id; + } + } + if (defaultModel === undefined) { + if (models.size === 0) { + throw new Error("No models available from the API"); + } + defaultModel = models.values().next().value; + } + return { + sampling_models: Array.from(models), + sampling_default_model: defaultModel, + }; + })(); + + function pickModel(preferences: CreateMessageRequest['params']['modelPreferences'] | undefined): string { + if (preferences?.hints) { + for (const hint of Object.values(preferences.hints)) { + if (hint.name !== undefined && backfillMeta.sampling_models.includes(hint.name)) { + return hint.name; + } + } + } + // TODO: linear model on preferences?.{intelligencePriority, speedPriority, costPriority} to pick betwen haiku, sonnet, opus. + return backfillMeta.sampling_default_model!; + } + + let clientSupportsSampling: boolean | undefined; + // let clientSupportsElicitation: boolean | undefined; + + const propagateMessage = (source: NamedTransport, target: NamedTransport) => { + source.transport.onmessage = async (message, extra) => { + console.error(`[proxy]: Message from ${source.name} transport: ${JSON.stringify(message)}; extra: ${JSON.stringify(extra)}`); + + if (isJSONRPCRequest(message)) { + if (isInitializeRequest(message)) { + if (!(clientSupportsSampling = !!message.params.capabilities.sampling)) { + message.params.capabilities.sampling = {} + message.params._meta = {...(message.params._meta ?? {}), ...backfillMeta}; + } + } else if (isCreateMessageRequest(message) && !clientSupportsSampling) { + if (message.params.includeContext !== 'none') { + source.transport.send({ + jsonrpc: "2.0", + id: message.id, + error: { + code: -32601, // Method not found + message: "includeContext != none not supported by MCP sampling backfill", + }, + }, {relatedRequestId: message.id}); + return; + } + + message.params.metadata; + message.params.modelPreferences; + + try { + // message.params. + const msg = await api.messages.create({ + model: pickModel(message.params.modelPreferences), + system: message.params.systemPrompt === undefined ? undefined : [ + { + type: "text", + text: message.params.systemPrompt + }, + ], + messages: message.params.messages.map(({role, content}) => ({ + role, + content: [contentFromMcp(content)] + })), + max_tokens: message.params.maxTokens, + temperature: message.params.temperature, + stop_sequences: message.params.stopSequences, + }); + + if (msg.content.length !== 1) { + throw new Error(`Expected exactly one content item in the response, got ${msg.content.length}`); + } + + source.transport.send({ + jsonrpc: "2.0", + id: message.id, + result: { + model: msg.model, + stopReason: msg.stop_reason, + role: msg.role, + content: contentToMcp(msg.content[0]), + }, + }); + } catch (error) { + source.transport.send({ + jsonrpc: "2.0", + id: message.id, + error: { + code: -32601, // Method not found + message: `Error processing message: ${(error as Error).message}`, + }, + }, {relatedRequestId: message.id}); + } + return; + // } else if (isElicitRequest(message) && !clientSupportsElicitation) { + // // TODO: form + // return; + } + } else if (isJSONRPCNotification(message)) { + if (isInitializedNotification(message) && source.name === 'server') { + if (!clientSupportsSampling) { + message.params = {...(message.params ?? {}), _meta: {...(message.params?._meta ?? {}), ...backfillMeta}}; + } + } + } + + try { + const relatedRequestId = isCancelledNotification(message)? message.params.requestId : undefined; + await target.transport.send(message, {relatedRequestId}); + } catch (error) { + console.error(`[proxy]: Error sending message to ${target.name}:`, error); + } + }; + }; + propagateMessage(server, client); + propagateMessage(client, server); + + const addErrorHandler = (transport: NamedTransport) => { + transport.transport.onerror = async (error: Error) => { + console.error(`[proxy]: Error from ${transport.name} transport:`, error); + }; + }; + + addErrorHandler(client); + addErrorHandler(server); + + await server.transport.start(); + await client.transport.start(); +} + +async function main() { + const args = process.argv.slice(2); + const client: NamedTransport = {name: 'client', transport: new StdioClientTransport({command: args[0], args: args.slice(1)})}; + const server: NamedTransport = {name: 'server', transport: new StdioServerTransport()}; + + const api = new Anthropic(); + await setupBackfill(client, server, api); + console.error("[proxy]: Transports started."); +} + +main().catch((error) => { + console.error("[proxy]: Fatal error:", error); + process.exit(1); +}); \ No newline at end of file From 744c83defd18171c2734558bcc17b314df92ad8d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 15:23:48 +0100 Subject: [PATCH 05/33] Update backfillSampling.ts --- src/examples/backfill/backfillSampling.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 24a780523..979511f66 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -2,9 +2,10 @@ This example implements an stdio MCP proxy that backfills sampling requests using the Claude API. Usage: - npx -y @modelcontextprotocol/inspector \ - npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ - npx -y --silent @modelcontextprotocol/server-everything + npx -y @modelcontextprotocol/inspector \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ + npx -y --silent @modelcontextprotocol/server-everything + /Users/ochafik/code/modelcontextprotocol-servers/src/everything/run-stdio.sh */ import { Anthropic } from "@anthropic-ai/sdk"; From cc030973f806a51c3aff0c6f94904522c9ba88b9 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 15:27:56 +0100 Subject: [PATCH 06/33] feat(examples): Add tool calling support to sampling backfill proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update backfillSampling.ts to support SEP-1577 tool calling: **New Conversions:** - MCP Tool → Claude API tool format (toolToClaudeFormat) - MCP ToolChoice → Claude tool_choice (toolChoiceToClaudeFormat) - Claude tool_use → MCP ToolCallContent (in contentToMcp) - MCP ToolResultContent → Claude tool_result (in contentFromMcp) **Message Handling:** - Extract and convert tools/tool_choice from CreateMessageRequest - Pass tools to Claude API messages.create - Handle multi-content responses (prioritize tool_use over text) - Map stop reasons: tool_use → toolUse, end_turn → endTurn, etc. **Flow Support:** The proxy now fully supports agentic tool calling loops: 1. Server sends request with tools 2. Claude responds with tool_use 3. Server executes tool and sends tool_result 4. Claude provides final answer All conversions maintain type safety with proper MCP types (UserMessage, AssistantMessage, ToolCallContent, ToolResultContent). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/examples/backfill/backfillSampling.ts | 100 +++++++++++++++++++--- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 979511f66..964e9322b 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -9,7 +9,7 @@ */ import { Anthropic } from "@anthropic-ai/sdk"; -import { Base64ImageSource, ContentBlock, ContentBlockParam, MessageParam } from "@anthropic-ai/sdk/resources/messages.js"; +import { Base64ImageSource, ContentBlock, ContentBlockParam, MessageParam, Tool as ClaudeTool, ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool } from "@anthropic-ai/sdk/resources/messages.js"; import { StdioServerTransport } from '../../server/stdio.js'; import { StdioClientTransport } from '../../client/stdio.js'; import { @@ -27,6 +27,11 @@ import { CallToolRequest, CallToolRequestSchema, isJSONRPCNotification, + Tool, + ToolCallContent, + ToolResultContent, + UserMessage, + AssistantMessage, } from "../../types.js"; import { Transport } from "../../shared/transport.js"; @@ -44,20 +49,53 @@ const isElicitRequest: (value: unknown) => value is ElicitRequest = const isCreateMessageRequest: (value: unknown) => value is CreateMessageRequest = ((value: any) => CreateMessageRequestSchema.safeParse(value).success) as any; +/** + * Converts MCP Tool definition to Claude API tool format + */ +function toolToClaudeFormat(tool: Tool): ClaudeTool { + return { + name: tool.name, + description: tool.description || "", + input_schema: tool.inputSchema, + }; +} + +/** + * Converts MCP ToolChoice to Claude API tool_choice format + */ +function toolChoiceToClaudeFormat(toolChoice: CreateMessageRequest['params']['tool_choice']): ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool | undefined { + if (!toolChoice) { + return undefined; + } + + if (toolChoice.mode === "required") { + return { type: "any", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; + } + + // "auto" or undefined defaults to auto + return { type: "auto", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; +} -function contentToMcp(content: ContentBlock): CreateMessageResult['content'][number] { +function contentToMcp(content: ContentBlock): CreateMessageResult['content'] { switch (content.type) { case 'text': - return {type: 'text', text: content.text}; + return { type: 'text', text: content.text }; + case 'tool_use': + return { + type: 'tool_use', + id: content.id, + name: content.name, + input: content.input, + } as ToolCallContent; default: - throw new Error(`Unsupported content type: ${content.type}`); + throw new Error(`Unsupported content type: ${(content as any).type}`); } } -function contentFromMcp(content: CreateMessageRequest['params']['messages'][number]['content']): ContentBlockParam { +function contentFromMcp(content: UserMessage['content'] | AssistantMessage['content']): ContentBlockParam { switch (content.type) { case 'text': - return {type: 'text', text: content.text}; + return { type: 'text', text: content.text }; case 'image': return { type: 'image', @@ -67,9 +105,16 @@ function contentFromMcp(content: CreateMessageRequest['params']['messages'][numb type: 'base64', }, }; + case 'tool_result': + // MCP ToolResultContent to Claude API tool_result + return { + type: 'tool_result', + tool_use_id: content.toolUseId, + content: JSON.stringify(content.content), + }; case 'audio': default: - throw new Error(`Unsupported content type: ${content.type}`); + throw new Error(`Unsupported content type: ${(content as any).type}`); } } @@ -142,7 +187,10 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo message.params.modelPreferences; try { - // message.params. + // Convert MCP tools to Claude API format if provided + const tools = message.params.tools?.map(toolToClaudeFormat); + const tool_choice = toolChoiceToClaudeFormat(message.params.tool_choice); + const msg = await api.messages.create({ model: pickModel(message.params.modelPreferences), system: message.params.systemPrompt === undefined ? undefined : [ @@ -158,10 +206,36 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo max_tokens: message.params.maxTokens, temperature: message.params.temperature, stop_sequences: message.params.stopSequences, + // Add tool calling support + tools: tools && tools.length > 0 ? tools : undefined, + tool_choice: tool_choice, }); - if (msg.content.length !== 1) { - throw new Error(`Expected exactly one content item in the response, got ${msg.content.length}`); + // Claude can return multiple content blocks (e.g., text + tool_use) + // MCP currently supports single content block per message + // Priority: tool_use > text > other + let responseContent: CreateMessageResult['content']; + const toolUseBlock = msg.content.find(block => block.type === 'tool_use'); + if (toolUseBlock) { + responseContent = contentToMcp(toolUseBlock); + } else { + // Fall back to first content block (typically text) + if (msg.content.length === 0) { + throw new Error('Claude API returned no content blocks'); + } + responseContent = contentToMcp(msg.content[0]); + } + + // Map stop reasons from Claude to MCP format + let stopReason: CreateMessageResult['stopReason'] = msg.stop_reason as any; + if (msg.stop_reason === 'tool_use') { + stopReason = 'toolUse'; + } else if (msg.stop_reason === 'max_tokens') { + stopReason = 'maxTokens'; + } else if (msg.stop_reason === 'end_turn') { + stopReason = 'endTurn'; + } else if (msg.stop_reason === 'stop_sequence') { + stopReason = 'stopSequence'; } source.transport.send({ @@ -169,9 +243,9 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo id: message.id, result: { model: msg.model, - stopReason: msg.stop_reason, - role: msg.role, - content: contentToMcp(msg.content[0]), + stopReason: stopReason, + role: 'assistant', // Always assistant in MCP responses + content: responseContent, }, }); } catch (error) { From e5c5df2de601d737e13df2abfb0e12b5013b006a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 16:17:20 +0100 Subject: [PATCH 07/33] feat(examples): Add toolLoopSampling example demonstrating agentic file search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a comprehensive example showing how to use MCP sampling with a tool loop. The server exposes a fileSearch tool that uses an LLM with locally-defined ripgrep and read tools to intelligently search and read files. Key features: - Implements a full agentic tool loop pattern - Uses systemPrompt parameter for proper LLM instruction - Validates tool inputs using Zod schemas - Ensures path safety with canonicalization and CWD constraints - Demonstrates recursive tool use (LLM decides which tools to call) - Proper error handling throughout the tool loop - Includes iteration limit to prevent infinite loops This example demonstrates SEP-1577 tool calling support in MCP sampling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/examples/server/toolLoopSampling.ts | 353 ++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 src/examples/server/toolLoopSampling.ts diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts new file mode 100644 index 000000000..d83a89389 --- /dev/null +++ b/src/examples/server/toolLoopSampling.ts @@ -0,0 +1,353 @@ +/* + This example demonstrates a tool loop using MCP sampling with locally defined tools. + + It exposes a "fileSearch" tool that uses an LLM with ripgrep and read capabilities + to intelligently search and read files in the current directory. + + Usage: + npx -y --silent tsx src/examples/server/toolLoopSampling.ts + + Then connect with an MCP client and call the "fileSearch" tool with a query like: + "Find all TypeScript files that export a Server class" +*/ + +import { McpServer } from "../../server/mcp.js"; +import { StdioServerTransport } from "../../server/stdio.js"; +import { z } from "zod"; +import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { resolve, relative } from "node:path"; +import type { + SamplingMessage, + Tool, + ToolCallContent, + CreateMessageResult, +} from "../../types.js"; + +const CWD = process.cwd(); + +/** + * Zod schemas for validating tool inputs + */ +const RipgrepInputSchema = z.object({ + pattern: z.string(), + path: z.string(), +}); + +const ReadInputSchema = z.object({ + path: z.string(), +}); + +/** + * Ensures a path is canonical and within the current working directory. + * Throws an error if the path attempts to escape CWD. + */ +function ensureSafePath(inputPath: string): string { + const resolved = resolve(CWD, inputPath); + const rel = relative(CWD, resolved); + + // Check if the path escapes CWD (starts with .. or is absolute outside CWD) + if (rel.startsWith("..") || resolve(CWD, rel) !== resolved) { + throw new Error(`Path "${inputPath}" is outside the current directory`); + } + + return resolved; +} + +/** + * Executes ripgrep to search for a pattern in files. + * Returns search results as a string. + */ +async function executeRipgrep( + pattern: string, + path: string +): Promise<{ output?: string; error?: string }> { + try { + const safePath = ensureSafePath(path); + + return new Promise((resolve) => { + const rg = spawn("rg", [ + "--json", + "--max-count", "50", + "--", + pattern, + safePath, + ]); + + let stdout = ""; + let stderr = ""; + + rg.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + rg.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + rg.on("close", (code) => { + if (code === 0 || code === 1) { + // code 1 means no matches, which is fine + resolve({ output: stdout || "No matches found" }); + } else { + resolve({ error: stderr || `ripgrep exited with code ${code}` }); + } + }); + + rg.on("error", (err) => { + resolve({ error: `Failed to execute ripgrep: ${err.message}` }); + }); + }); + } catch (error) { + return { + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} + +/** + * Reads a file from the filesystem. + * Returns file contents as a string. + */ +async function executeRead( + path: string +): Promise<{ content?: string; error?: string }> { + try { + const safePath = ensureSafePath(path); + const content = await readFile(safePath, "utf-8"); + return { content }; + } catch (error) { + return { + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} + +/** + * Defines the local tools available to the LLM during sampling. + */ +const LOCAL_TOOLS: Tool[] = [ + { + name: "ripgrep", + description: + "Search for a pattern in files using ripgrep. Returns matching lines with file paths and line numbers.", + inputSchema: { + type: "object", + properties: { + pattern: { + type: "string", + description: "The regex pattern to search for", + }, + path: { + type: "string", + description: "The file or directory path to search in (relative to current directory)", + }, + }, + required: ["pattern", "path"], + }, + }, + { + name: "read", + description: + "Read the contents of a file. Use this to examine files found by ripgrep.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "The file path to read (relative to current directory)", + }, + }, + required: ["path"], + }, + }, +]; + +/** + * Executes a local tool and returns the result. + */ +async function executeLocalTool( + toolName: string, + toolInput: Record +): Promise> { + try { + switch (toolName) { + case "ripgrep": { + const validated = RipgrepInputSchema.parse(toolInput); + return await executeRipgrep(validated.pattern, validated.path); + } + case "read": { + const validated = ReadInputSchema.parse(toolInput); + return await executeRead(validated.path); + } + default: + return { error: `Unknown tool: ${toolName}` }; + } + } catch (error) { + if (error instanceof z.ZodError) { + return { + error: `Invalid input for tool '${toolName}': ${error.errors.map(e => e.message).join(", ")}`, + }; + } + return { + error: error instanceof Error ? error.message : "Unknown error during tool execution", + }; + } +} + +/** + * Runs a tool loop using sampling. + * Continues until the LLM provides a final answer. + */ +async function runToolLoop( + server: McpServer, + initialQuery: string +): Promise { + const messages: SamplingMessage[] = [ + { + role: "user", + content: { + type: "text", + text: initialQuery, + }, + }, + ]; + + const MAX_ITERATIONS = 10; + let iteration = 0; + + const systemPrompt = + "You are a helpful assistant that searches through files to answer questions. " + + "You have access to ripgrep (for searching) and read (for reading file contents). " + + "Use ripgrep to find relevant files, then read them to provide accurate answers. " + + "All paths are relative to the current working directory. " + + "Be concise and focus on providing the most relevant information."; + + while (iteration < MAX_ITERATIONS) { + iteration++; + + // Request message from LLM with available tools + const response: CreateMessageResult = await server.server.createMessage({ + messages, + systemPrompt, + maxTokens: 4000, + tools: LOCAL_TOOLS, + tool_choice: { mode: "auto" }, + }); + + // Add assistant's response to message history + messages.push({ + role: "assistant", + content: response.content, + }); + + // Check if LLM wants to use a tool + if (response.stopReason === "toolUse") { + const toolCall = response.content as ToolCallContent; + + console.error( + `[toolLoop] LLM requested tool: ${toolCall.name} with input:`, + JSON.stringify(toolCall.input, null, 2) + ); + + // Execute the requested tool locally + const toolResult = await executeLocalTool(toolCall.name, toolCall.input); + + console.error( + `[toolLoop] Tool result:`, + JSON.stringify(toolResult, null, 2) + ); + + // Add tool result to message history + messages.push({ + role: "user", + content: { + type: "tool_result", + toolUseId: toolCall.id, + content: toolResult, + }, + }); + + // Continue the loop to get next response + continue; + } + + // LLM provided final answer + if (response.content.type === "text") { + return response.content.text; + } + + // Unexpected response type + throw new Error( + `Unexpected response content type: ${response.content.type}` + ); + } + + throw new Error(`Tool loop exceeded maximum iterations (${MAX_ITERATIONS})`); +} + +// Create and configure MCP server +const mcpServer = new McpServer({ + name: "tool-loop-sampling-server", + version: "1.0.0", +}); + +// Register the fileSearch tool that uses sampling with a tool loop +mcpServer.registerTool( + "fileSearch", + { + description: + "Search for information in files using an AI assistant with ripgrep and file reading capabilities. " + + "The assistant will intelligently use ripgrep to find relevant files and read them to answer your query.", + inputSchema: { + query: z + .string() + .describe( + "A natural language query describing what to search for (e.g., 'Find all TypeScript files that export a Server class')" + ), + }, + }, + async ({ query }) => { + try { + console.error(`[fileSearch] Processing query: ${query}`); + + const result = await runToolLoop(mcpServer, query); + + console.error(`[fileSearch] Final result: ${result.substring(0, 200)}...`); + + return { + content: [ + { + type: "text", + text: result, + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error(`[fileSearch] Error: ${errorMessage}`); + + return { + content: [ + { + type: "text", + text: `Error performing file search: ${errorMessage}`, + }, + ], + }; + } + } +); + +async function main() { + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); + console.error("MCP Tool Loop Sampling Server is running..."); + console.error(`Working directory: ${CWD}`); +} + +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); +}); From c85618719b2dca89e67472052aa8207d4c054bac Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 16:46:06 +0100 Subject: [PATCH 08/33] docs: Add comprehensive test patterns analysis for sampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Analyzed existing test files and examples to document: - How to set up Client with StdioClientTransport for testing - How to implement sampling request handlers - Proper test structure and cleanup patterns - Example code snippets for sampling handlers - How to simulate a tool loop conversation in tests - Common pitfalls and solutions This analysis covers both unit testing (InMemoryTransport) and integration testing (StdioClientTransport) patterns for servers that use MCP sampling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../test-patterns-analysis.md | 794 ++++++++++++++++++ 1 file changed, 794 insertions(+) create mode 100644 intermediate-findings/test-patterns-analysis.md diff --git a/intermediate-findings/test-patterns-analysis.md b/intermediate-findings/test-patterns-analysis.md new file mode 100644 index 000000000..989b66f0f --- /dev/null +++ b/intermediate-findings/test-patterns-analysis.md @@ -0,0 +1,794 @@ +# MCP TypeScript SDK Test Patterns for Sampling + +## Overview + +This document analyzes test patterns in the MCP TypeScript SDK codebase to understand how to write tests for servers that use sampling (LLM requests). Based on analysis of existing test files and examples. + +## Key Testing Components + +### 1. Transport Types for Testing + +#### InMemoryTransport (Recommended for Unit Tests) +- **Location**: `/src/inMemory.ts` +- **Use case**: Testing client-server interactions within the same process +- **Advantages**: + - Synchronous, fast execution + - No external process spawning + - Full control over both sides of the connection + - Easy to mock and test error conditions + +```typescript +import { InMemoryTransport } from "../inMemory.js"; +import { Client } from "../client/index.js"; +import { Server } from "../server/index.js"; + +// Create linked pair - one for client, one for server +const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + +// Connect both sides +await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), +]); +``` + +#### StdioClientTransport (For Integration Tests) +- **Location**: `/src/client/stdio.ts` +- **Use case**: Testing real server processes via stdio +- **Pattern**: Spawn actual server process and communicate via stdin/stdout + +```typescript +import { StdioClientTransport } from "./stdio.js"; + +const transport = new StdioClientTransport({ + command: "/path/to/server", + args: ["arg1", "arg2"], + env: { CUSTOM_VAR: "value" } +}); + +await transport.start(); +// Use with Client instance +``` + +### 2. Setting Up Sampling Request Handlers + +#### On the Client Side (Simulating LLM) + +The client needs to implement a handler for `sampling/createMessage` requests to simulate LLM responses: + +```typescript +import { Client } from "../client/index.js"; +import { CreateMessageRequestSchema } from "../types.js"; + +const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + sampling: {}, // MUST declare sampling capability + }, + } +); + +// Set up handler for sampling requests from server +client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + return { + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "This is a mock LLM response", + }, + }; +}); +``` + +#### Pattern from `src/server/index.test.ts` (lines 237-248): + +```typescript +// Server declares it will call sampling +const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, +); + +// Client provides sampling capability +const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + sampling: {}, + }, + }, +); + +// Implement request handler for sampling/createMessage +client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + // Mock implementation of createMessage + return { + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "This is a test response", + }, + }; +}); + +const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + +await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), +]); + +// Now server can call createMessage +const response = await server.createMessage({ + messages: [], + maxTokens: 10, +}); +``` + +### 3. Tool Loop Testing Pattern + +Based on `toolLoopSampling.ts` example, here's how to test a server that uses sampling with tools: + +```typescript +import { McpServer } from "../server/mcp.js"; +import { Client } from "../client/index.js"; +import { InMemoryTransport } from "../inMemory.js"; +import { CreateMessageRequestSchema, ToolCallContent } from "../types.js"; + +describe("Server with sampling tool loop", () => { + test("should handle tool loop with local tools", async () => { + const mcpServer = new McpServer({ + name: "tool-loop-server", + version: "1.0.0", + }); + + // Register a tool that uses sampling + mcpServer.registerTool( + "fileSearch", + { + description: "Search files using AI", + inputSchema: { + query: z.string(), + }, + }, + async ({ query }) => { + // Tool implementation calls createMessage + const response = await mcpServer.server.createMessage({ + messages: [ + { + role: "user", + content: { + type: "text", + text: query, + }, + }, + ], + maxTokens: 1000, + tools: [ + { + name: "ripgrep", + description: "Search files", + inputSchema: { + type: "object", + properties: { + pattern: { type: "string" }, + }, + required: ["pattern"], + }, + }, + ], + tool_choice: { mode: "auto" }, + }); + + return { + content: [ + { + type: "text", + text: response.content.type === "text" + ? response.content.text + : "Tool result", + }, + ], + }; + } + ); + + // Set up client that simulates LLM with tool calling + const client = new Client( + { + name: "test-client", + version: "1.0.0", + }, + { + capabilities: { + sampling: {}, + }, + } + ); + + let callCount = 0; + client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + callCount++; + + // First call: LLM decides to use tool + if (callCount === 1) { + return { + model: "test-model", + role: "assistant", + stopReason: "toolUse", + content: { + type: "tool_use", + id: "tool-call-1", + name: "ripgrep", + input: { pattern: "test" }, + } as ToolCallContent, + }; + } + + // Second call: LLM provides final answer after tool result + return { + model: "test-model", + role: "assistant", + stopReason: "endTurn", + content: { + type: "text", + text: "Found 5 matches in the files", + }, + }; + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + // Test the tool + const result = await client.callTool({ + name: "fileSearch", + arguments: { query: "Find TypeScript files" }, + }); + + expect(result.content[0]).toMatchObject({ + type: "text", + text: expect.stringContaining("matches"), + }); + expect(callCount).toBe(2); // Tool loop made 2 LLM calls + }); +}); +``` + +### 4. Simulating Multi-Turn Conversations + +To test a tool loop or conversation: + +```typescript +client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + + // Check if this is a tool result + if (lastMessage.role === "user" && lastMessage.content.type === "tool_result") { + // LLM processes tool result and provides final answer + return { + model: "test-model", + role: "assistant", + stopReason: "endTurn", + content: { + type: "text", + text: "Based on the tool result, here's my answer...", + }, + }; + } + + // Initial request - ask to use a tool + return { + model: "test-model", + role: "assistant", + stopReason: "toolUse", + content: { + type: "tool_use", + id: `tool-call-${Date.now()}`, + name: "some_tool", + input: { arg: "value" }, + } as ToolCallContent, + }; +}); +``` + +### 5. Test Structure and Cleanup Patterns + +#### Basic Test Structure + +```typescript +describe("Server with sampling", () => { + let server: Server; + let client: Client; + let clientTransport: InMemoryTransport; + let serverTransport: InMemoryTransport; + + beforeEach(async () => { + server = new Server( + { name: "test-server", version: "1.0" }, + { capabilities: {} } + ); + + client = new Client( + { name: "test-client", version: "1.0" }, + { capabilities: { sampling: {} } } + ); + + // Set up sampling handler + client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + return { + model: "test-model", + role: "assistant", + content: { type: "text", text: "Mock response" }, + }; + }); + + [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await Promise.all([ + clientTransport.close(), + serverTransport.close(), + ]); + }); + + test("should make sampling request", async () => { + const result = await server.createMessage({ + messages: [ + { + role: "user", + content: { type: "text", text: "Hello" }, + }, + ], + maxTokens: 100, + }); + + expect(result.content.type).toBe("text"); + expect(result.role).toBe("assistant"); + }); +}); +``` + +#### Cleanup Pattern + +From `process-cleanup.test.ts`: + +```typescript +test("should exit cleanly after closing transport", async () => { + const server = new Server( + { name: "test-server", version: "1.0.0" }, + { capabilities: {} } + ); + + const transport = new StdioServerTransport(); + await server.connect(transport); + + // Close the transport + await transport.close(); + + // Test passes if we reach here without hanging + expect(true).toBe(true); +}); +``` + +## 6. Testing with StdioClientTransport + +For integration tests that spawn real server processes: + +```typescript +import { StdioClientTransport } from "../client/stdio.js"; +import { Client } from "../client/index.js"; + +describe("Integration test with real server", () => { + let client: Client; + let transport: StdioClientTransport; + + beforeEach(() => { + client = new Client( + { name: "test-client", version: "1.0" }, + { capabilities: { sampling: {} } } + ); + + // Set up handler for sampling requests from server + client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + // Simulate LLM response + return { + model: "claude-3-sonnet", + role: "assistant", + content: { + type: "text", + text: "Mock LLM response for integration test", + }, + }; + }); + + transport = new StdioClientTransport({ + command: "npx", + args: ["-y", "tsx", "path/to/your/server.ts"], + }); + }); + + afterEach(async () => { + await transport.close(); + }); + + test("should communicate with real server", async () => { + await client.connect(transport); + + // Test server capabilities + const serverCapabilities = client.getServerCapabilities(); + expect(serverCapabilities).toBeDefined(); + + // Call a tool that uses sampling + const result = await client.callTool({ + name: "ai-powered-tool", + arguments: { query: "test query" }, + }); + + expect(result.content).toBeDefined(); + }); +}); +``` + +## 7. Key Patterns from Existing Tests + +### Pattern 1: Parallel Connection Setup +```typescript +// Always connect client and server in parallel +await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), +]); +``` + +### Pattern 2: Capability Declaration +```typescript +// Client MUST declare sampling capability to handle requests +const client = new Client( + { name: "test-client", version: "1.0" }, + { capabilities: { sampling: {} } } // Required! +); + +// Server checks client capabilities before making sampling requests +expect(server.getClientCapabilities()).toEqual({ sampling: {} }); +``` + +### Pattern 3: Request Handler Registration +```typescript +// Register handler BEFORE connecting +client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + // Handler implementation +}); + +// Then connect +await client.connect(transport); +``` + +### Pattern 4: Error Handling in Tests +```typescript +test("should throw when capability missing", async () => { + const clientWithoutSampling = new Client( + { name: "no-sampling", version: "1.0" }, + { capabilities: {} } // No sampling! + ); + + await clientWithoutSampling.connect(clientTransport); + + // Server should reject sampling request + await expect( + server.createMessage({ messages: [], maxTokens: 10 }) + ).rejects.toThrow(/Client does not support/); +}); +``` + +## 8. Testing Tool Loops - Complete Example + +```typescript +describe("Tool loop with sampling", () => { + test("should execute multi-turn tool loop", async () => { + const mcpServer = new McpServer({ + name: "tool-loop-test", + version: "1.0.0", + }); + + // Track tool executions + const toolExecutions: Array<{ name: string; input: any }> = []; + + // Register local tools that the LLM can call + const localTools = [ + { + name: "search", + description: "Search for information", + inputSchema: { + type: "object" as const, + properties: { + query: { type: "string" as const }, + }, + required: ["query"], + }, + }, + { + name: "read", + description: "Read a file", + inputSchema: { + type: "object" as const, + properties: { + path: { type: "string" as const }, + }, + required: ["path"], + }, + }, + ]; + + // Register a server tool that uses the tool loop + mcpServer.registerTool( + "ai_assistant", + { + description: "AI assistant with tool access", + inputSchema: { task: z.string() }, + }, + async ({ task }) => { + const messages: SamplingMessage[] = [ + { + role: "user", + content: { type: "text", text: task }, + }, + ]; + + let iteration = 0; + const MAX_ITERATIONS = 5; + + while (iteration < MAX_ITERATIONS) { + iteration++; + + const response = await mcpServer.server.createMessage({ + messages, + maxTokens: 1000, + tools: localTools, + tool_choice: { mode: "auto" }, + }); + + messages.push({ + role: "assistant", + content: response.content, + }); + + if (response.stopReason === "toolUse") { + const toolCall = response.content as ToolCallContent; + + toolExecutions.push({ + name: toolCall.name, + input: toolCall.input, + }); + + // Simulate tool execution + const toolResult = { + result: `Mock result for ${toolCall.name}`, + }; + + messages.push({ + role: "user", + content: { + type: "tool_result", + toolUseId: toolCall.id, + content: toolResult, + }, + }); + + continue; + } + + // Final answer + if (response.content.type === "text") { + return { + content: [{ type: "text", text: response.content.text }], + }; + } + } + + throw new Error("Max iterations exceeded"); + } + ); + + // Set up client to simulate LLM + const client = new Client( + { name: "test-client", version: "1.0" }, + { capabilities: { sampling: {} } } + ); + + let samplingCallCount = 0; + client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + samplingCallCount++; + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + + // First call: use search tool + if (samplingCallCount === 1) { + return { + model: "test-model", + role: "assistant", + stopReason: "toolUse", + content: { + type: "tool_use", + id: "call-1", + name: "search", + input: { query: "typescript files" }, + }, + }; + } + + // Second call: use read tool + if (samplingCallCount === 2) { + return { + model: "test-model", + role: "assistant", + stopReason: "toolUse", + content: { + type: "tool_use", + id: "call-2", + name: "read", + input: { path: "file.ts" }, + }, + }; + } + + // Third call: provide final answer + return { + model: "test-model", + role: "assistant", + stopReason: "endTurn", + content: { + type: "text", + text: "Found and analyzed the files", + }, + }; + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + // Execute the tool + const result = await client.callTool({ + name: "ai_assistant", + arguments: { task: "Find TypeScript files" }, + }); + + // Verify + expect(samplingCallCount).toBe(3); + expect(toolExecutions).toHaveLength(2); + expect(toolExecutions[0].name).toBe("search"); + expect(toolExecutions[1].name).toBe("read"); + expect(result.content[0]).toMatchObject({ + type: "text", + text: expect.stringContaining("Found"), + }); + }); +}); +``` + +## 9. Common Pitfalls and Solutions + +### Pitfall 1: Not Declaring Capabilities +```typescript +// ❌ WRONG - will throw error +const client = new Client({ name: "test", version: "1.0" }); +client.setRequestHandler(CreateMessageRequestSchema, ...); // Throws! + +// ✅ CORRECT +const client = new Client( + { name: "test", version: "1.0" }, + { capabilities: { sampling: {} } } // Declare first! +); +client.setRequestHandler(CreateMessageRequestSchema, ...); +``` + +### Pitfall 2: Registering Handler After Connect +```typescript +// ❌ WRONG - handler not available during initialization +await client.connect(transport); +client.setRequestHandler(CreateMessageRequestSchema, ...); // Too late! + +// ✅ CORRECT +client.setRequestHandler(CreateMessageRequestSchema, ...); +await client.connect(transport); +``` + +### Pitfall 3: Not Handling Tool Loop Properly +```typescript +// ❌ WRONG - doesn't handle tool results +client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + // Always returns tool use, causing infinite loop + return { + model: "test", + role: "assistant", + stopReason: "toolUse", + content: { type: "tool_use", ... }, + }; +}); + +// ✅ CORRECT - check message history +client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + + if (lastMessage.content.type === "tool_result") { + // Provide final answer after tool use + return { + model: "test", + role: "assistant", + stopReason: "endTurn", + content: { type: "text", text: "Final answer" }, + }; + } + + // Initial request - use tool + return { ... }; +}); +``` + +## 10. File Locations Reference + +Key files for understanding test patterns: + +- **Client Tests**: `/src/client/index.test.ts` (lines 583-636 for sampling handler examples) +- **Server Tests**: `/src/server/index.test.ts` (lines 208-270, 728-864 for sampling tests) +- **InMemory Transport**: `/src/inMemory.ts` +- **Stdio Transport Tests**: + - `/src/client/stdio.test.ts` + - `/src/client/cross-spawn.test.ts` +- **Tool Loop Example**: `/src/examples/server/toolLoopSampling.ts` +- **Backfill Proxy Example**: `/src/examples/backfill/backfillSampling.ts` +- **McpServer Tests**: `/src/server/mcp.test.ts` + +## Summary + +**For unit tests of servers with sampling:** +1. Use `InMemoryTransport.createLinkedPair()` +2. Create `Client` with `capabilities: { sampling: {} }` +3. Register `CreateMessageRequestSchema` handler on client before connecting +4. Connect both client and server in parallel +5. Simulate LLM responses in the handler +6. For tool loops, track message history and alternate between tool use and final answer + +**For integration tests:** +1. Use `StdioClientTransport` to spawn real server process +2. Still need to provide sampling handler on client side +3. Test actual server behavior with realistic scenarios +4. Ensure proper cleanup of spawned processes From b688779d9b61d144b60ae0cfac46c8a602b76b17 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 16:51:37 +0100 Subject: [PATCH 09/33] test(examples): Add comprehensive tests for toolLoopSampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds integration tests for toolLoopSampling server that verify: - Complete tool loop flow (ripgrep → read → final answer) - Path validation and security (prevents directory traversal) - Error handling for invalid tool names - Input validation with malformed tool inputs - Maximum iteration limit enforcement Tests use StdioClientTransport to spawn actual server process and implement sampling handlers that simulate LLM behavior with tool calls and responses. All 5 tests pass successfully, providing solid coverage of the agentic tool loop pattern. Also updates toolLoopSampling.ts with linter formatting fixes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/examples/server/toolLoopSampling.test.ts | 471 +++++++++++++++++++ src/examples/server/toolLoopSampling.ts | 4 +- 2 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 src/examples/server/toolLoopSampling.test.ts diff --git a/src/examples/server/toolLoopSampling.test.ts b/src/examples/server/toolLoopSampling.test.ts new file mode 100644 index 000000000..20f072011 --- /dev/null +++ b/src/examples/server/toolLoopSampling.test.ts @@ -0,0 +1,471 @@ +/** + * Tests for toolLoopSampling.ts + * + * These tests verify that the fileSearch tool correctly implements a tool loop + * by simulating an LLM that makes ripgrep and read tool calls. + */ + +import { Client } from "../../client/index.js"; +import { StdioClientTransport } from "../../client/stdio.js"; +import { + CreateMessageRequestSchema, + CreateMessageResult, + CallToolResultSchema, + ToolCallContent, + SamplingMessage, +} from "../../types.js"; +import { resolve } from "node:path"; + +describe("toolLoopSampling server", () => { + jest.setTimeout(30000); // 30 second timeout for integration tests + + let client: Client; + let transport: StdioClientTransport; + + beforeEach(() => { + // Create client with sampling capability + client = new Client( + { + name: "test-client", + version: "1.0.0", + }, + { + capabilities: { + sampling: { + tools: {}, // Indicate we support tool calling in sampling + }, + }, + } + ); + + // Create transport that spawns the toolLoopSampling server + transport = new StdioClientTransport({ + command: "npx", + args: [ + "-y", + "--silent", + "tsx", + resolve(__dirname, "toolLoopSampling.ts"), + ], + }); + }); + + afterEach(async () => { + await transport.close(); + }); + + test("should handle a tool loop with ripgrep and read", async () => { + // Track sampling request count to simulate different LLM responses + let samplingCallCount = 0; + + // Set up sampling handler that simulates an LLM + client.setRequestHandler( + CreateMessageRequestSchema, + async (request): Promise => { + samplingCallCount++; + + // Extract the last message to understand context + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + + console.error( + `[test] Sampling call ${samplingCallCount}, messages: ${messages.length}, last message type: ${lastMessage.content.type}` + ); + + // First call: Return tool_use for ripgrep + if (samplingCallCount === 1) { + return { + model: "test-model", + role: "assistant", + content: { + type: "tool_use", + id: "call_1", + name: "ripgrep", + input: { + pattern: "McpServer", + path: "src", + }, + } as ToolCallContent, + stopReason: "toolUse", + }; + } + + // Second call: After getting ripgrep results, return tool_use for read + if (samplingCallCount === 2) { + // Verify we got a tool result + expect(lastMessage.content.type).toBe("tool_result"); + + return { + model: "test-model", + role: "assistant", + content: { + type: "tool_use", + id: "call_2", + name: "read", + input: { + path: "src/server/mcp.ts", + }, + } as ToolCallContent, + stopReason: "toolUse", + }; + } + + // Third call: After getting read results, return final answer + if (samplingCallCount === 3) { + // Verify we got another tool result + expect(lastMessage.content.type).toBe("tool_result"); + + return { + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "I found the McpServer class in src/server/mcp.ts. It's the main server class for MCP.", + }, + stopReason: "endTurn", + }; + } + + // Should not reach here + throw new Error( + `Unexpected sampling call count: ${samplingCallCount}` + ); + } + ); + + // Connect client to server + await client.connect(transport); + + // Call the fileSearch tool + const result = await client.request( + { + method: "tools/call", + params: { + name: "fileSearch", + arguments: { + query: "Find the McpServer class definition", + }, + }, + }, + CallToolResultSchema + ); + + // Verify the result + expect(result.content).toBeDefined(); + expect(result.content.length).toBeGreaterThan(0); + expect(result.content[0].type).toBe("text"); + + // Verify we got the expected response + if (result.content[0].type === "text") { + expect(result.content[0].text).toContain("McpServer"); + } + + // Verify we made exactly 3 sampling calls (tool loop worked correctly) + expect(samplingCallCount).toBe(3); + }); + + test("should handle errors in tool execution", async () => { + let samplingCallCount = 0; + + // Set up sampling handler that requests an invalid path + client.setRequestHandler( + CreateMessageRequestSchema, + async (request): Promise => { + samplingCallCount++; + + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + + // First call: Return tool_use for ripgrep with path outside CWD + if (samplingCallCount === 1) { + return { + model: "test-model", + role: "assistant", + content: { + type: "tool_use", + id: "call_1", + name: "ripgrep", + input: { + pattern: "test", + path: "../../etc/passwd", // Try to escape CWD + }, + } as ToolCallContent, + stopReason: "toolUse", + }; + } + + // Second call: Should receive error in tool result + if (samplingCallCount === 2) { + expect(lastMessage.content.type).toBe("tool_result"); + if (lastMessage.content.type === "tool_result") { + // Verify error is present in tool result + const content = lastMessage.content.content as Record< + string, + unknown + >; + expect(content.error).toBeDefined(); + expect( + typeof content.error === "string" && + content.error.includes("outside the current directory") + ).toBe(true); + } + + // Return final answer acknowledging the error + return { + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "I encountered an error: the path is outside the current directory.", + }, + stopReason: "endTurn", + }; + } + + throw new Error( + `Unexpected sampling call count: ${samplingCallCount}` + ); + } + ); + + await client.connect(transport); + + // Call the fileSearch tool + const result = await client.request( + { + method: "tools/call", + params: { + name: "fileSearch", + arguments: { + query: "Search outside current directory", + }, + }, + }, + CallToolResultSchema + ); + + // Verify we got a response (even though there was an error) + expect(result.content).toBeDefined(); + expect(result.content.length).toBeGreaterThan(0); + expect(result.content[0].type).toBe("text"); + }); + + test("should handle invalid tool names", async () => { + let samplingCallCount = 0; + + // Set up sampling handler that requests an unknown tool + client.setRequestHandler( + CreateMessageRequestSchema, + async (request): Promise => { + samplingCallCount++; + + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + + // First call: Return tool_use for unknown tool + if (samplingCallCount === 1) { + return { + model: "test-model", + role: "assistant", + content: { + type: "tool_use", + id: "call_1", + name: "unknown_tool", + input: { + foo: "bar", + }, + } as ToolCallContent, + stopReason: "toolUse", + }; + } + + // Second call: Should receive error in tool result + if (samplingCallCount === 2) { + expect(lastMessage.content.type).toBe("tool_result"); + if (lastMessage.content.type === "tool_result") { + const content = lastMessage.content.content as Record< + string, + unknown + >; + expect(content.error).toBeDefined(); + expect( + typeof content.error === "string" && + content.error.includes("Unknown tool") + ).toBe(true); + } + + return { + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "The requested tool does not exist.", + }, + stopReason: "endTurn", + }; + } + + throw new Error( + `Unexpected sampling call count: ${samplingCallCount}` + ); + } + ); + + await client.connect(transport); + + const result = await client.request( + { + method: "tools/call", + params: { + name: "fileSearch", + arguments: { + query: "Use unknown tool", + }, + }, + }, + CallToolResultSchema + ); + + expect(result.content).toBeDefined(); + expect(samplingCallCount).toBe(2); + }); + + test("should handle malformed tool inputs", async () => { + let samplingCallCount = 0; + + // Set up sampling handler that sends malformed input + client.setRequestHandler( + CreateMessageRequestSchema, + async (request): Promise => { + samplingCallCount++; + + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + + // First call: Return tool_use with missing required fields + if (samplingCallCount === 1) { + return { + model: "test-model", + role: "assistant", + content: { + type: "tool_use", + id: "call_1", + name: "ripgrep", + input: { + // Missing 'pattern' and 'path' required fields + foo: "bar", + }, + } as ToolCallContent, + stopReason: "toolUse", + }; + } + + // Second call: Should receive validation error + if (samplingCallCount === 2) { + expect(lastMessage.content.type).toBe("tool_result"); + if (lastMessage.content.type === "tool_result") { + const content = lastMessage.content.content as Record< + string, + unknown + >; + expect(content.error).toBeDefined(); + // Verify it's a validation error + expect( + typeof content.error === "string" && + content.error.includes("Invalid input") + ).toBe(true); + } + + return { + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "I provided invalid input to the tool.", + }, + stopReason: "endTurn", + }; + } + + throw new Error( + `Unexpected sampling call count: ${samplingCallCount}` + ); + } + ); + + await client.connect(transport); + + const result = await client.request( + { + method: "tools/call", + params: { + name: "fileSearch", + arguments: { + query: "Test malformed input", + }, + }, + }, + CallToolResultSchema + ); + + expect(result.content).toBeDefined(); + expect(samplingCallCount).toBe(2); + }); + + test("should respect maximum iteration limit", async () => { + let samplingCallCount = 0; + + // Set up sampling handler that keeps requesting tools indefinitely + client.setRequestHandler( + CreateMessageRequestSchema, + async (request): Promise => { + samplingCallCount++; + + // Always return tool calls (never final answer) + return { + model: "test-model", + role: "assistant", + content: { + type: "tool_use", + id: `call_${samplingCallCount}`, + name: "ripgrep", + input: { + pattern: "test", + path: "src", + }, + } as ToolCallContent, + stopReason: "toolUse", + }; + } + ); + + await client.connect(transport); + + // Call fileSearch with infinite loop scenario + const result = await client.request( + { + method: "tools/call", + params: { + name: "fileSearch", + arguments: { + query: "Infinite loop test", + }, + }, + }, + CallToolResultSchema + ); + + // Verify we got an error response (not a throw) + expect(result.content).toBeDefined(); + expect(result.content.length).toBeGreaterThan(0); + expect(result.content[0].type).toBe("text"); + + // Verify the error message mentions the iteration limit + if (result.content[0].type === "text") { + expect(result.content[0].text).toContain("Tool loop exceeded maximum iterations"); + } + + // Verify we hit the iteration limit (10 iterations as defined in toolLoopSampling.ts) + expect(samplingCallCount).toBe(10); + }); +}); diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index d83a89389..413ecbe3b 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -5,7 +5,9 @@ to intelligently search and read files in the current directory. Usage: - npx -y --silent tsx src/examples/server/toolLoopSampling.ts + npx -y @modelcontextprotocol/inspector \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ + npx -y --silent tsx src/examples/server/toolLoopSampling.ts Then connect with an MCP client and call the "fileSearch" tool with a query like: "Find all TypeScript files that export a Server class" From 48cc38c83ef3fb703e23fabe4662e75f7166bf3c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 17:01:25 +0100 Subject: [PATCH 10/33] Update backfillSampling.ts --- src/examples/backfill/backfillSampling.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 979511f66..7e3a8a3af 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -5,7 +5,6 @@ npx -y @modelcontextprotocol/inspector \ npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ npx -y --silent @modelcontextprotocol/server-everything - /Users/ochafik/code/modelcontextprotocol-servers/src/everything/run-stdio.sh */ import { Anthropic } from "@anthropic-ai/sdk"; From 751d10273f3badfde2cdb47a9da218b28a6e3cb4 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 17:35:28 +0100 Subject: [PATCH 11/33] Update backfillSampling.ts --- src/examples/backfill/backfillSampling.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 7e3a8a3af..c377021cd 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -3,7 +3,7 @@ Usage: npx -y @modelcontextprotocol/inspector \ - npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \ npx -y --silent @modelcontextprotocol/server-everything */ @@ -29,6 +29,8 @@ import { } from "../../types.js"; import { Transport } from "../../shared/transport.js"; +const DEFAULT_MAX_TOKENS = process.env.DEFAULT_MAX_TOKENS ? parseInt(process.env.DEFAULT_MAX_TOKENS) : 1000; + // TODO: move to SDK const isCancelledNotification: (value: unknown) => value is CancelledNotification = @@ -124,22 +126,21 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo message.params.capabilities.sampling = {} message.params._meta = {...(message.params._meta ?? {}), ...backfillMeta}; } - } else if (isCreateMessageRequest(message) && !clientSupportsSampling) { - if (message.params.includeContext !== 'none') { + } else if (isCreateMessageRequest(message)) { + if ((message.params.includeContext ?? 'none') !== 'none') { + const errorMessage = "includeContext != none not supported by MCP sampling backfill" + console.error(`[proxy]: ${errorMessage}`); source.transport.send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, // Method not found - message: "includeContext != none not supported by MCP sampling backfill", + message: errorMessage, }, }, {relatedRequestId: message.id}); return; } - message.params.metadata; - message.params.modelPreferences; - try { // message.params. const msg = await api.messages.create({ @@ -154,9 +155,10 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo role, content: [contentFromMcp(content)] })), - max_tokens: message.params.maxTokens, + max_tokens: message.params.maxTokens ?? DEFAULT_MAX_TOKENS, temperature: message.params.temperature, stop_sequences: message.params.stopSequences, + ...(message.params.metadata ?? {}), }); if (msg.content.length !== 1) { @@ -174,6 +176,8 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo }, }); } catch (error) { + console.error(`[proxy]: Error processing message: ${(error as Error).message}`); + source.transport.send({ jsonrpc: "2.0", id: message.id, @@ -233,4 +237,4 @@ async function main() { main().catch((error) => { console.error("[proxy]: Fatal error:", error); process.exit(1); -}); \ No newline at end of file +}); From 265c22782c11f4effd341782c822c1f782fbab04 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:26:02 +0100 Subject: [PATCH 12/33] fix(examples): Handle CreateMessageResult.content as array in toolLoopSampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated toolLoopSampling.ts to properly handle CreateMessageResult.content as both single content blocks and arrays: - Changed runToolLoop return type to include both answer and transcript - Extract and execute ALL tool_use blocks in parallel using Promise.all() - Concatenate all text content blocks for final answer - Return full message transcript for debugging This ensures the tool loop works correctly when the LLM returns multiple content blocks (text + tool calls, or multiple tool calls in one turn). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- intermediate-findings/client-sampling-api.md | 776 +++++++++ intermediate-findings/issue-876-analysis.md | 101 ++ .../protocol-spec-research.md | 586 +++++++ intermediate-findings/sampling-analysis.md | 750 +++++++++ .../sampling-examples-review.md | 794 +++++++++ .../sampling-tool-additions.md | 632 ++++++++ intermediate-findings/sep-1577-spec.md | 1429 +++++++++++++++++ intermediate-findings/test-analysis.md | 664 ++++++++ .../toolLoopSampling-review.md | 542 +++++++ .../toolLoopSampling-test-review.md | 249 +++ intermediate-findings/transport-analysis.md | 960 +++++++++++ protocol-debugger.ts | 9 + .../backfill/backfillSampling.merge.ts | 380 +++++ src/examples/server/toolLoopSampling.ts | 93 +- src/shared/transport-validator.ts | 115 ++ tmp2/client.mjs | 43 + tmp2/client.py | 66 + tmp2/issue766.ts | 165 ++ tmp2/package-lock.json | 1051 ++++++++++++ tmp2/package.json | 15 + 20 files changed, 9390 insertions(+), 30 deletions(-) create mode 100644 intermediate-findings/client-sampling-api.md create mode 100644 intermediate-findings/issue-876-analysis.md create mode 100644 intermediate-findings/protocol-spec-research.md create mode 100644 intermediate-findings/sampling-analysis.md create mode 100644 intermediate-findings/sampling-examples-review.md create mode 100644 intermediate-findings/sampling-tool-additions.md create mode 100644 intermediate-findings/sep-1577-spec.md create mode 100644 intermediate-findings/test-analysis.md create mode 100644 intermediate-findings/toolLoopSampling-review.md create mode 100644 intermediate-findings/toolLoopSampling-test-review.md create mode 100644 intermediate-findings/transport-analysis.md create mode 100644 protocol-debugger.ts create mode 100644 src/examples/backfill/backfillSampling.merge.ts create mode 100644 src/shared/transport-validator.ts create mode 100644 tmp2/client.mjs create mode 100755 tmp2/client.py create mode 100644 tmp2/issue766.ts create mode 100644 tmp2/package-lock.json create mode 100644 tmp2/package.json diff --git a/intermediate-findings/client-sampling-api.md b/intermediate-findings/client-sampling-api.md new file mode 100644 index 000000000..6ce239d18 --- /dev/null +++ b/intermediate-findings/client-sampling-api.md @@ -0,0 +1,776 @@ +# Client Sampling API Documentation + +## Overview + +This document describes how to use the MCP TypeScript SDK Client API to handle `sampling/createMessage` requests. The sampling capability allows MCP servers to request language model completions from clients, enabling servers to use AI capabilities without directly accessing LLM APIs. + +## Table of Contents + +1. [Setup and Configuration](#setup-and-configuration) +2. [Request Handler Registration](#request-handler-registration) +3. [Handler Signature and Parameters](#handler-signature-and-parameters) +4. [Request Structure](#request-structure) +5. [Response Construction](#response-construction) +6. [Content Types](#content-types) +7. [Tool Calling Support](#tool-calling-support) +8. [Complete Examples](#complete-examples) +9. [Best Practices and Gotchas](#best-practices-and-gotchas) + +--- + +## Setup and Configuration + +### 1. Declare Sampling Capability + +To handle sampling requests, you must declare the `sampling` capability when creating the client: + +```typescript +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; + +const client = new Client( + { + name: "my-client", + version: "1.0.0", + }, + { + capabilities: { + sampling: {}, // Required to handle sampling/createMessage requests + }, + } +); +``` + +**Important:** Without declaring the `sampling` capability, calling `setRequestHandler` with `CreateMessageRequestSchema` will throw an error: +``` +Error: Client does not support sampling capability (required for sampling/createMessage) +``` + +--- + +## Request Handler Registration + +### Method: `client.setRequestHandler()` + +The `setRequestHandler` method is used to register a handler for incoming `sampling/createMessage` requests. + +```typescript +import { CreateMessageRequestSchema, CreateMessageResult } from "@modelcontextprotocol/sdk/types.js"; + +client.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + // Handler implementation + return result; // CreateMessageResult + } +); +``` + +### Parameters + +1. **Schema**: `CreateMessageRequestSchema` - Zod schema defining the request structure +2. **Handler**: Async function with signature: + ```typescript + (request: CreateMessageRequest, extra: RequestHandlerExtra) => Promise | CreateMessageResult + ``` + +--- + +## Handler Signature and Parameters + +### Handler Function Signature + +```typescript +async function handler( + request: CreateMessageRequest, + extra: RequestHandlerExtra +): Promise +``` + +### Request Parameter (`CreateMessageRequest`) + +The request object contains: + +```typescript +interface CreateMessageRequest { + method: "sampling/createMessage"; + params: { + // Required: Array of conversation messages + messages: SamplingMessage[]; + + // Required: Maximum tokens to generate + maxTokens: number; + + // Optional: System prompt for the LLM + systemPrompt?: string; + + // Optional: Temperature parameter (0-1) + temperature?: number; + + // Optional: Stop sequences + stopSequences?: string[]; + + // Optional: Tools available to the LLM + tools?: Tool[]; + + // Optional: Tool choice configuration + tool_choice?: ToolChoice; + + // Optional: Model preferences/hints + modelPreferences?: ModelPreferences; + + // Optional: Metadata + metadata?: Record; + + // DEPRECATED: Context inclusion preference + includeContext?: "none" | "thisServer" | "allServers"; + + // Internal metadata + _meta?: Record; + }; +} +``` + +### Extra Parameter (`RequestHandlerExtra`) + +The `extra` object provides additional context and utilities: + +```typescript +interface RequestHandlerExtra { + // Abort signal for cancellation + signal: AbortSignal; + + // JSON-RPC request ID + requestId: RequestId; + + // Session ID from transport (if available) + sessionId?: string; + + // Authentication info (if available) + authInfo?: AuthInfo; + + // Request metadata + _meta?: RequestMeta; + + // Original HTTP request info (if applicable) + requestInfo?: RequestInfo; + + // Send a notification related to this request + sendNotification: (notification: SendNotificationT) => Promise; + + // Send a request related to this request + sendRequest: >( + request: SendRequestT, + resultSchema: U, + options?: RequestOptions + ) => Promise>; + + // Elicit input from user (if elicitation capability enabled) + elicitInput?: (request: { + message: string; + requestedSchema?: object; + }) => Promise; +} +``` + +**Key fields:** +- `signal`: Use to detect if the request was cancelled +- `requestId`: Useful for logging/tracking +- `sendNotification`: Send progress updates or other notifications +- `sendRequest`: Make requests back to the server (if needed) + +--- + +## Response Construction + +### CreateMessageResult Structure + +```typescript +interface CreateMessageResult { + // Required: Name of the model used + model: string; + + // Required: Response role (always "assistant") + role: "assistant"; + + // Required: Response content (discriminated union) + content: TextContent | ImageContent | AudioContent | ToolCallContent; + + // Optional: Why sampling stopped + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | "refusal" | "other" | string; +} +``` + +### Basic Text Response Example + +```typescript +const result: CreateMessageResult = { + model: "claude-3-5-sonnet-20241022", + role: "assistant", + content: { + type: "text", + text: "This is the LLM's response" + }, + stopReason: "endTurn" +}; +``` + +--- + +## Content Types + +### 1. TextContent + +Plain text response from the LLM. + +```typescript +interface TextContent { + type: "text"; + text: string; + _meta?: Record; +} +``` + +**Example:** +```typescript +content: { + type: "text", + text: "The capital of France is Paris." +} +``` + +### 2. ImageContent + +Image data (base64 encoded). + +```typescript +interface ImageContent { + type: "image"; + data: string; // Base64 encoded image data + mimeType: string; // e.g., "image/png", "image/jpeg" + _meta?: Record; +} +``` + +**Example:** +```typescript +content: { + type: "image", + data: "iVBORw0KGgoAAAANSUhEUgAA...", + mimeType: "image/png" +} +``` + +### 3. AudioContent + +Audio data (base64 encoded). + +```typescript +interface AudioContent { + type: "audio"; + data: string; // Base64 encoded audio data + mimeType: string; // e.g., "audio/wav", "audio/mp3" + _meta?: Record; +} +``` + +### 4. ToolCallContent (Tool Use) + +Request to call a tool. Used when the LLM wants to invoke a tool. + +```typescript +interface ToolCallContent { + type: "tool_use"; + id: string; // Unique ID for this tool call + name: string; // Tool name + input: Record; // Tool arguments + _meta?: Record; +} +``` + +**Example:** +```typescript +content: { + type: "tool_use", + id: "toolu_01A09q90qw90lq917835lq9", + name: "get_weather", + input: { + location: "San Francisco, CA", + unit: "celsius" + } +} +``` + +When returning `ToolCallContent`, you should typically set `stopReason: "toolUse"`. + +--- + +## Tool Calling Support + +### Overview + +The sampling API supports tool calling, allowing the LLM to use tools provided by the server. This enables agentic behavior where the LLM can: +1. Decide to use a tool +2. Return a tool call request +3. Receive tool results +4. Continue the conversation + +### Tool Definition + +Tools are provided in the request's `params.tools` array: + +```typescript +interface Tool { + name: string; + description?: string; + inputSchema: { + type: "object"; + properties: Record; + required?: string[]; + // JSON Schema for tool inputs + }; + outputSchema?: { + // Optional JSON Schema for tool outputs + }; +} +``` + +### Tool Choice Configuration + +The `tool_choice` parameter controls how the LLM should use tools: + +```typescript +interface ToolChoice { + mode: "auto" | "required" | "tool"; + disable_parallel_tool_use?: boolean; + toolName?: string; // Required when mode is "tool" +} +``` + +**Modes:** +- `"auto"`: LLM decides whether to use tools +- `"required"`: LLM must use at least one tool +- `"tool"`: LLM must use a specific tool (specified by `toolName`) + +### Tool Use Flow + +1. **Server sends request with tools:** +```typescript +{ + messages: [...], + maxTokens: 1000, + tools: [ + { + name: "get_weather", + description: "Get current weather", + inputSchema: { + type: "object", + properties: { + location: { type: "string" } + } + } + } + ], + tool_choice: { mode: "auto" } +} +``` + +2. **Client returns tool use response:** +```typescript +{ + model: "claude-3-5-sonnet-20241022", + role: "assistant", + content: { + type: "tool_use", + id: "toolu_123", + name: "get_weather", + input: { location: "Paris" } + }, + stopReason: "toolUse" +} +``` + +3. **Server executes tool and adds result to messages:** +```typescript +{ + role: "user", + content: { + type: "tool_result", + toolUseId: "toolu_123", + content: { temperature: 20, condition: "sunny" } + } +} +``` + +4. **Server sends another request with updated messages** (tool loop continues until LLM provides final answer) + +--- + +## Complete Examples + +### Example 1: Basic Text Response Handler + +```typescript +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { CreateMessageRequestSchema, CreateMessageResult } from "@modelcontextprotocol/sdk/types.js"; + +const client = new Client( + { name: "basic-client", version: "1.0.0" }, + { capabilities: { sampling: {} } } +); + +client.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + // Check if cancelled + if (extra.signal.aborted) { + throw new Error("Request was cancelled"); + } + + console.log(`Handling sampling request with ${request.params.messages.length} messages`); + + // In a real implementation, call your LLM API here + const response = await callYourLLMAPI({ + messages: request.params.messages, + maxTokens: request.params.maxTokens, + systemPrompt: request.params.systemPrompt, + temperature: request.params.temperature, + }); + + const result: CreateMessageResult = { + model: response.model, + role: "assistant", + content: { + type: "text", + text: response.text, + }, + stopReason: response.stopReason, + }; + + return result; + } +); +``` + +### Example 2: Tool Calling Handler + +```typescript +import { Anthropic } from "@anthropic-ai/sdk"; + +const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + +client.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + // Convert MCP messages to Anthropic format + const messages = request.params.messages.map(msg => ({ + role: msg.role, + content: convertContent(msg.content) + })); + + // Convert tools to Anthropic format + const tools = request.params.tools?.map(tool => ({ + name: tool.name, + description: tool.description || "", + input_schema: tool.inputSchema, + })); + + // Call Anthropic API + const response = await anthropic.messages.create({ + model: "claude-3-5-sonnet-20241022", + system: request.params.systemPrompt, + messages: messages, + max_tokens: request.params.maxTokens, + temperature: request.params.temperature, + tools: tools, + }); + + // Convert response to MCP format + let content: CreateMessageResult['content']; + let stopReason: CreateMessageResult['stopReason']; + + // Check if LLM wants to use a tool + const toolUseBlock = response.content.find(block => block.type === 'tool_use'); + if (toolUseBlock) { + content = { + type: "tool_use", + id: toolUseBlock.id, + name: toolUseBlock.name, + input: toolUseBlock.input, + }; + stopReason = "toolUse"; + } else { + // Regular text response + const textBlock = response.content.find(block => block.type === 'text'); + content = { + type: "text", + text: textBlock?.text || "", + }; + stopReason = response.stop_reason === "end_turn" ? "endTurn" : response.stop_reason; + } + + return { + model: response.model, + role: "assistant", + content, + stopReason, + }; + } +); +``` + +### Example 3: Handler with Cancellation Support + +```typescript +client.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + // Set up cancellation handling + const controller = new AbortController(); + extra.signal.addEventListener('abort', () => { + controller.abort(); + }); + + try { + const response = await fetch('https://your-llm-api.com/v1/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: request.params.messages, + max_tokens: request.params.maxTokens, + }), + signal: controller.signal, + }); + + const data = await response.json(); + + return { + model: data.model, + role: "assistant", + content: { + type: "text", + text: data.text, + }, + stopReason: "endTurn", + }; + } catch (error) { + if (error.name === 'AbortError') { + throw new Error('Request was cancelled'); + } + throw error; + } + } +); +``` + +### Example 4: Handler with Progress Notifications + +```typescript +client.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + // Send progress notification + await extra.sendNotification({ + method: "notifications/progress", + params: { + progressToken: extra.requestId, + progress: 0.5, + total: 1.0, + } + }); + + // Perform LLM call... + const response = await callLLM(request.params); + + // Send completion notification + await extra.sendNotification({ + method: "notifications/progress", + params: { + progressToken: extra.requestId, + progress: 1.0, + total: 1.0, + } + }); + + return { + model: response.model, + role: "assistant", + content: { + type: "text", + text: response.text, + }, + stopReason: "endTurn", + }; + } +); +``` + +--- + +## Best Practices and Gotchas + +### Best Practices + +1. **Always Declare Capabilities** + - Declare the `sampling` capability in client options before calling `setRequestHandler` + - Failure to do so will throw an error at registration time + +2. **Validate Input** + - The SDK automatically validates the request structure via Zod schemas + - Additional validation of your own business logic should be added + +3. **Handle Cancellation** + - Always check `extra.signal.aborted` before expensive operations + - Forward the abort signal to your LLM API calls + - Clean up resources when cancelled + +4. **Use Appropriate Stop Reasons** + - `"endTurn"`: Natural completion + - `"stopSequence"`: Hit a stop sequence + - `"maxTokens"`: Reached token limit + - `"toolUse"`: When returning ToolCallContent + - `"refusal"`: Model refused the request + - `"other"`: Provider-specific reasons + +5. **Tool Calling Patterns** + - When returning `ToolCallContent`, set `stopReason: "toolUse"` + - Generate unique IDs for each tool call (e.g., using UUID) + - The server will handle executing tools and continuing the conversation + +6. **Error Handling** + - Throw descriptive errors that will be returned to the server + - The Protocol layer will automatically convert thrown errors to JSON-RPC error responses + - Use `McpError` for MCP-specific errors with error codes + +7. **Model Selection** + - Use `request.params.modelPreferences` to select appropriate models + - Fall back to a default model if preferences don't match available models + - Return the actual model name used in the response + +### Common Gotchas + +1. **Missing Capability Declaration** + ```typescript + // ❌ Wrong - will throw error + const client = new Client({ name: "client", version: "1.0.0" }); + client.setRequestHandler(CreateMessageRequestSchema, handler); + + // ✅ Correct + const client = new Client( + { name: "client", version: "1.0.0" }, + { capabilities: { sampling: {} } } + ); + client.setRequestHandler(CreateMessageRequestSchema, handler); + ``` + +2. **Wrong Content Type for Stop Reason** + ```typescript + // ❌ Wrong - stopReason doesn't match content type + return { + content: { type: "text", text: "..." }, + stopReason: "toolUse" // Should be "endTurn" for text + }; + + // ✅ Correct + return { + content: { type: "tool_use", id: "...", name: "...", input: {} }, + stopReason: "toolUse" + }; + ``` + +3. **Not Handling All Message Types** + - `SamplingMessage` can be either `UserMessage` or `AssistantMessage` + - `UserMessage.content` can be: `TextContent`, `ImageContent`, `AudioContent`, or `ToolResultContent` + - `AssistantMessage.content` can be: `TextContent`, `ImageContent`, `AudioContent`, or `ToolCallContent` + - Make sure your LLM API supports all content types in the messages + +4. **Forgetting Role Field** + ```typescript + // ❌ Wrong - missing role + return { + model: "claude-3-5-sonnet-20241022", + content: { type: "text", text: "..." } + }; + + // ✅ Correct + return { + model: "claude-3-5-sonnet-20241022", + role: "assistant", // Always "assistant" + content: { type: "text", text: "..." } + }; + ``` + +5. **Not Propagating Tool Definitions** + - When tools are provided in `request.params.tools`, pass them to your LLM API + - Tools must be in the format expected by your LLM provider + - Convert between MCP and provider-specific tool formats + +6. **Incorrect Tool Result Format** + - Tool results come as `ToolResultContent` in user messages + - The `content` field is an object (not an array) + - Match `toolUseId` with the `id` from `ToolCallContent` + +7. **Handler Registration Order** + - Register handlers before calling `client.connect()` + - Handlers can only be set once per method (subsequent calls replace the handler) + - Use `client.removeRequestHandler(method)` to remove a handler + +8. **Message History Management** + - The `messages` array contains the full conversation history + - Each message has a `role` ("user" or "assistant") and `content` + - Tool use creates a cycle: assistant tool_use → user tool_result → assistant response + +### Type Safety + +The SDK uses Zod schemas for runtime validation and TypeScript for compile-time type safety: + +```typescript +// Request is automatically typed +client.setRequestHandler( + CreateMessageRequestSchema, + async (request, extra) => { + // ✅ TypeScript knows the structure + request.params.messages.forEach(msg => { + if (msg.role === "user") { + // msg.content can be text, image, audio, or tool_result + } else { + // msg.content can be text, image, audio, or tool_use + } + }); + + // ✅ Return type is validated + return { + model: "...", + role: "assistant", + content: { type: "text", text: "..." }, + stopReason: "endTurn", + }; + } +); +``` + +--- + +## Related Resources + +- **MCP Specification**: [https://modelcontextprotocol.io/docs/specification](https://modelcontextprotocol.io/docs/specification) +- **Client API Source**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` +- **Protocol Base**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.ts` +- **Type Definitions**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` +- **Example: Tool Loop Sampling**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.ts` +- **Example: Backfill Proxy**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/backfill/backfillSampling.ts` + +--- + +## Summary + +The Client Sampling API allows MCP clients to handle `sampling/createMessage` requests from servers, enabling servers to use LLM capabilities without direct API access. Key points: + +1. Declare `sampling` capability in client options +2. Register handler using `setRequestHandler(CreateMessageRequestSchema, handler)` +3. Handler receives request with messages, tools, and parameters +4. Return `CreateMessageResult` with model, role, content, and stopReason +5. Support text responses and tool calling +6. Handle cancellation via `extra.signal` +7. Match content types with stop reasons + +This API enables powerful patterns like tool loops, agent-based search, and delegated LLM access in MCP architectures. diff --git a/intermediate-findings/issue-876-analysis.md b/intermediate-findings/issue-876-analysis.md new file mode 100644 index 000000000..8f32121e0 --- /dev/null +++ b/intermediate-findings/issue-876-analysis.md @@ -0,0 +1,101 @@ +# GitHub Issue #876 Analysis: SSE Connection 5-Minute Timeout + +## Issue Summary + +**Problem**: SSE (Server-Sent Events) connections always close after 5 minutes, despite attempts to configure longer timeouts. + +**Root Cause**: Operating system-level network connection timeouts kill inactive connections after 5 minutes. This is an OS-level limitation, not an application-level issue. + +**User's Experience**: +- The user reported that `res.on('close')` is triggered after exactly 5 minutes +- They attempted to set a longer timeout using `callTool(xx, undefined, {timeout: 20mins})` but this did not prevent the 5-minute disconnect +- The timeout configuration did not work as expected because the OS kills the connection at the network layer + +## Technical Details from GitHub Issue + +1. **The Problem**: SSE connections terminate after 5 minutes of inactivity +2. **Failed Solution**: Setting application-level timeouts (e.g., `{timeout: 20mins}`) doesn't prevent OS-level network timeouts +3. **Provided Solution**: MCP team member (antonpk1) provided a comprehensive workaround + +## Workaround Solution Provided + +**Key Insight**: Send periodic "heartbeat" messages to keep the connection alive and prevent OS timeout. + +**Implementation Strategy**: +1. Send regular "notifications/progress" messages during long-running operations +2. Use periodic notifications (e.g., every 30 seconds) to maintain connection activity +3. Successfully demonstrated a 20-minute task with periodic progress updates + +**Sample Code Pattern** (from the GitHub issue): +- Implement periodic progress notifications to prevent connection timeout +- Send notifications every 30 seconds during long operations +- This keeps the SSE connection active and prevents the 5-minute OS timeout + +## Current SDK Implementation Analysis + +### Timeout Handling in Protocol Layer + +From `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.ts`: + +1. **Default Timeout**: `DEFAULT_REQUEST_TIMEOUT_MSEC = 60000` (60 seconds) +2. **Request Options**: Support for timeout configuration: + - `timeout?: number` - Request-specific timeout in milliseconds + - `resetTimeoutOnProgress?: boolean` - Reset timeout when progress notifications are received + - `maxTotalTimeout?: number` - Maximum total time regardless of progress + +3. **Progress Support**: The SDK has built-in support for progress notifications: + - `onprogress?: ProgressCallback` - Callback for progress notifications + - `ProgressNotification` handling in the protocol layer + - Automatic timeout reset when `resetTimeoutOnProgress` is enabled + +### SSE Implementation + +From `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/sse.ts` and `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/sse.ts`: + +1. **Client SSE Transport**: Uses EventSource API for receiving messages, HTTP POST for sending +2. **Server SSE Transport**: Sends messages via SSE stream, receives via HTTP POST handlers +3. **No Built-in Keepalive**: The current SSE implementation does not include automatic keepalive/heartbeat functionality + +### Current Client Implementation Note + +In `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` (line 436): +```typescript +console.error("Calling tool", params, options, options?.timeout); +``` +This debug log shows the client does receive and process timeout options. + +## Gap Analysis + +**What's Missing**: +1. **No automatic keepalive mechanism** in SSE transport implementations +2. **No built-in progress notification sending** for long-running operations +3. **Documentation** about the 5-minute OS timeout limitation and workarounds + +**What Exists**: +1. **Progress notification support** in the protocol layer +2. **Timeout reset on progress** functionality (`resetTimeoutOnProgress`) +3. **Flexible timeout configuration** per request + +## Recommended Implementation + +Based on the GitHub issue resolution, the SDK should: + +1. **Add automatic keepalive option** to SSE transport classes +2. **Provide helper utilities** for sending periodic progress notifications +3. **Document the 5-minute limitation** and workaround patterns +4. **Include example code** showing how to implement progress notifications for long-running operations + +## Impact Assessment + +- **Current State**: Users experience unexpected disconnects after 5 minutes +- **Workaround Exists**: Manual progress notification implementation works +- **SDK Enhancement Needed**: Built-in keepalive and better documentation would improve developer experience + +## Test Coverage + +The test suite in `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.test.ts` includes: +- Timeout error handling tests +- Progress notification preservation tests +- But no specific tests for long-duration connections or keepalive functionality + +This analysis confirms that GitHub issue #876 identifies a real OS-level limitation that affects SSE connections, and the MCP team has provided a viable workaround using progress notifications to maintain connection activity. \ No newline at end of file diff --git a/intermediate-findings/protocol-spec-research.md b/intermediate-findings/protocol-spec-research.md new file mode 100644 index 000000000..8c870ab28 --- /dev/null +++ b/intermediate-findings/protocol-spec-research.md @@ -0,0 +1,586 @@ +# Model Context Protocol (MCP) Specification Research + +## Research Date: 2025-10-01 + +## Executive Summary + +This document provides comprehensive research on the Model Context Protocol (MCP) specification, focusing on protocol requirements, message format specifications, JSON-RPC 2.0 compliance, validation requirements, and security considerations. The research reveals significant gaps in current validation implementation, particularly around JSON-RPC 2.0 message format validation at the transport/protocol level. + +--- + +## 1. Official Protocol Specification + +### Primary Sources + +- **Official Specification**: https://modelcontextprotocol.io/specification/2025-06-18 +- **GitHub Repository**: https://github.com/modelcontextprotocol/modelcontextprotocol +- **TypeScript SDK**: https://github.com/modelcontextprotocol/typescript-sdk +- **Security Best Practices**: https://modelcontextprotocol.io/specification/draft/basic/security_best_practices + +### Protocol Overview + +MCP is an open protocol that enables seamless integration between LLM applications and external data sources and tools. The protocol: + +- **Built on JSON-RPC 2.0**: All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification +- **Stateful Session Protocol**: Focuses on context exchange and sampling coordination between clients and servers +- **Component-Based Architecture**: Defines multiple optional components (resources, prompts, tools, sampling, roots, elicitation) +- **Transport-Agnostic**: Supports multiple transport mechanisms (stdio, SSE, Streamable HTTP, WebSocket) + +--- + +## 2. JSON-RPC 2.0 Compliance Requirements + +### Core JSON-RPC 2.0 Specification + +Source: https://www.jsonrpc.org/specification + +All MCP implementations MUST comply with the JSON-RPC 2.0 specification, which defines three fundamental message types: + +#### 2.1 Request Object Requirements + +A valid JSON-RPC 2.0 Request **MUST** contain: + +1. **`jsonrpc`**: A String specifying the version of the JSON-RPC protocol. **MUST** be exactly `"2.0"`. + +2. **`method`**: A String containing the name of the method to be invoked. + - Method names beginning with `rpc.` are reserved for JSON-RPC internal methods and extensions + - **MUST NOT** be used for application-specific methods + +3. **`params`**: A Structured value (Array or Object) holding parameter values. + - This member **MAY** be omitted. + +4. **`id`**: An identifier established by the Client. + - **MUST** contain a String, Number, or NULL value if included + - If not included, the message is assumed to be a **Notification** + - **MCP Deviation**: The MCP specification states that the ID **MUST NOT** be null + +#### 2.2 Response Object Requirements + +When an RPC call is made, the Server **MUST** reply with a Response, except for Notifications. + +A valid Response **MUST** contain: + +1. **`jsonrpc`**: **MUST** be exactly `"2.0"`. + +2. **`result`**: This member is **REQUIRED** on success. + - This member **MUST NOT** exist if there was an error. + +3. **`error`**: This member is **REQUIRED** on error. + - This member **MUST NOT** exist if there was no error. + - **MUST** contain an error object with: + - `code` (Number): Integer error code + - `message` (String): Short error description + - `data` (Any, optional): Additional error details + +4. **`id`**: This member is **REQUIRED**. + - It **MUST** be the same as the value of the `id` member in the Request Object. + - If there was an error in detecting the id in the Request object, it MUST be Null. + +#### 2.3 Notification Requirements + +A Notification is a Request object without an `id` member. + +- The receiver **MUST NOT** send a response to a Notification. +- Notifications **MUST NOT** include an `id` member. +- Used for one-way messages that do not expect a response. + +#### 2.4 Batch Request Support + +JSON-RPC 2.0 supports sending multiple Request objects in an Array: + +- MCP implementations **MAY** support sending JSON-RPC batches +- MCP implementations **MUST** support receiving JSON-RPC batches +- The Server **MAY** process batch requests concurrently +- Responses can be returned in any order +- No Response is sent for Notifications in a batch + +#### 2.5 Standard Error Codes + +The JSON-RPC 2.0 specification defines the following standard error codes: + +| Code | Message | Meaning | +|------|---------|---------| +| -32700 | Parse error | Invalid JSON was received by the server | +| -32600 | Invalid Request | The JSON sent is not a valid Request object | +| -32601 | Method not found | The method does not exist / is not available | +| -32602 | Invalid params | Invalid method parameter(s) | +| -32603 | Internal error | Internal JSON-RPC error | +| -32000 to -32099 | Server error | Reserved for implementation-defined server-errors | + +--- + +## 3. MCP Protocol Requirements + +### 3.1 Message Format Requirements + +All messages between MCP clients and servers **MUST**: + +1. Follow the JSON-RPC 2.0 specification +2. Use JSON (RFC 4627) as the data format +3. Include the `jsonrpc: "2.0"` version field +4. Follow the appropriate structure for requests, responses, or notifications + +### 3.2 Response Requirements + +- Responses **MUST** include the same ID as the request they correspond to +- Either a `result` or an `error` **MUST** be set +- A response **MUST NOT** set both `result` and `error` +- Results **MAY** follow any JSON object structure +- Errors **MUST** include an error code (integer) and message (string) at minimum + +### 3.3 Transport Requirements + +From the specification: + +> Implementers choosing to support custom transport mechanisms must ensure they preserve the JSON-RPC message format and lifecycle requirements defined by MCP. + +All implementations **MUST**: +- Support the base protocol and lifecycle management components +- Preserve JSON-RPC message format across all transports +- Support receiving JSON-RPC batches (even if sending batches is not supported) + +### 3.4 Core Component Requirements + +- **Base Protocol**: All implementations **MUST** support +- **Lifecycle Management**: All implementations **MUST** support +- **Other Components** (Resources, Prompts, Tools, etc.): **MAY** be implemented based on application needs + +--- + +## 4. Validation Requirements + +### 4.1 Protocol-Level Validation + +Based on the specification and best practices, MCP implementations should rigorously validate: + +#### Message Structure Validation + +1. **JSON-RPC Version**: Verify `jsonrpc === "2.0"` +2. **Required Fields**: Ensure all required fields are present +3. **Field Types**: Validate that fields have correct types +4. **ID Consistency**: Ensure response IDs match request IDs +5. **Mutual Exclusivity**: Verify responses don't have both `result` and `error` +6. **Notification Structure**: Ensure notifications don't have `id` field + +#### Parameter Validation + +From the specification: +> Use JSON Schema validation on both client and server sides to catch type mismatches early and provide helpful error messages. + +#### Security-Related Validation + +From security best practices: +> Servers should rigorously validate incoming MCP messages against the protocol specification (structure, field consistency, recursion depth) to prevent malformed request attacks. + +### 4.2 Error Handling Requirements + +**Standard JSON-RPC Errors**: Servers should return standard JSON-RPC errors for common failure cases: + +- **-32700 (Parse error)**: Invalid JSON received +- **-32600 (Invalid Request)**: Missing required fields or invalid structure +- **-32601 (Method not found)**: Unknown method +- **-32602 (Invalid params)**: Parameter validation failed +- **-32603 (Internal error)**: Server-side processing error + +**Error Message Strategy**: +- Parameter validation with detailed error messages +- Error messages should help the LLM understand what went wrong +- Include suggestions for corrective actions + +**Timeout Requirements**: +> Implementations should implement appropriate timeouts for all requests, to prevent hung connections and resource exhaustion. + +--- + +## 5. Security Implications of Protocol Validation + +### 5.1 Critical Security Requirements + +From the official security best practices documentation: + +#### Authentication and Authorization + +**MUST Requirements**: +- MCP servers that implement authorization **MUST** verify all inbound requests +- MCP Servers **MUST NOT** use sessions for authentication +- MCP servers **MUST NOT** accept any tokens that were not explicitly issued for the MCP server +- "Token passthrough" (accepting tokens without validation) is explicitly **FORBIDDEN** + +#### Session Security + +- MCP servers **MUST** use secure, non-deterministic session IDs +- Generated session IDs (e.g., UUIDs) **SHOULD** use secure random number generators + +#### Input/Output Validation + +From security guidelines: +> Security for agent-tool protocols must start with strong auth, scoped permissions, and input/output validation. Developers should implement allow-lists, schema validations, and content filters. + +### 5.2 Attack Vectors and Mitigation + +#### Identified Vulnerabilities + +1. **Malformed Request Attacks** + - **Risk**: Servers that don't validate message structure can be exploited + - **Mitigation**: Rigorous validation of structure, field consistency, recursion depth + +2. **Prompt Injection** + - **Risk**: AI systems accepting untrusted user input with hidden prompts + - **Mitigation**: Input validation, content filtering, careful handling of user data + - **Note**: Modern exploits center on "lethal trifecta": privileged access + untrusted input + exfiltration channel + +3. **Confused Deputy Problem** + - **Risk**: MCP server acts on behalf of wrong principal + - **Mitigation**: Strict authorization checks, proper token validation + +4. **Session Hijacking** + - **Risk**: Attacker takes over legitimate session + - **Mitigation**: Secure session ID generation, session binding to user information + +5. **OAuth Phishing (Issue #544)** + - **Risk**: Insufficient authentication mechanisms allow fake MCP servers + - **Recommendation**: Add "resource" parameter to OAuth flow, validate server addresses + - **Note**: Security flaws should be addressed "at the protocol level" rather than relying on user awareness + +### 5.3 Best Practices for Validation + +#### Development Security + +- **SAST/SCA**: Build on pipelines implementing Static Application Security Testing and Software Composition Analysis +- **Dependency Management**: Identify and fix known vulnerabilities in dependencies + +#### Logging and Monitoring + +> Every time the AI uses a tool via MCP, the system should log who/what invoked it, which tool, with what parameters, and what result came back, with logs stored securely. + +#### User Consent + +From the specification: +> Users must explicitly consent to and understand all data access and operations + +Implementations **MUST** obtain explicit user consent before: +- Exposing user data to servers +- Invoking any tools +- Performing LLM sampling + +#### Local Server Configuration Safeguards + +- Display full command details before execution +- Require explicit user consent +- Highlight potentially dangerous command patterns +- Sandbox server execution +- Restrict system/file system access +- Limit network privileges + +--- + +## 6. Current TypeScript SDK Implementation Analysis + +### 6.1 Existing Validation + +The TypeScript SDK currently implements: + +1. **Zod-based Type Validation**: Uses Zod schemas for runtime type checking +2. **Message Type Guards**: Functions like `isJSONRPCRequest()`, `isJSONRPCResponse()`, etc. +3. **Application-Level Validation**: Input schema validation for tools, resources, and prompts +4. **Error Code Support**: Defines standard JSON-RPC error codes in `ErrorCode` enum + +### 6.2 Identified Gaps + +#### Critical Issue: Invalid JSON-RPC Validation (Issue #563) + +**Problem**: Some invalid JSON-RPC requests do not generate error responses as specified in the JSON-RPC 2.0 specification. + +**Example**: A request with an incorrect method property (e.g., `"method_"` instead of `"method"`) returns nothing instead of an error. + +**Expected Behavior**: +- For malformed requests: Return error code -32600 (Invalid Request) +- For invalid params: Return error code -32602 (Invalid params) + +**Current Limitation**: +- Invalid requests do not reach application error handling +- Developers cannot implement validation logic due to lack of error responses + +#### Transport-Level Validation Issues + +1. **No Validation at Transport Boundary**: The `Transport` interface doesn't enforce JSON-RPC validation +2. **Protocol Class Assumes Valid Messages**: The `Protocol` class (in `protocol.ts`) uses type guards but doesn't respond with errors for invalid messages +3. **Missing Parse Error Handling**: No handling for -32700 (Parse error) when invalid JSON is received + +#### Message Handling in Protocol Class + +Looking at `protocol.ts` lines 314-328: + +```typescript +this._transport.onmessage = (message, extra) => { + _onmessage?.(message, extra); + if (isJSONRPCResponse(message) || isJSONRPCError(message)) { + this._onresponse(message); + } else if (isJSONRPCRequest(message)) { + this._onrequest(message, extra); + } else if (isJSONRPCNotification(message)) { + this._onnotification(message); + } else { + this._onerror( + new Error(`Unknown message type: ${JSON.stringify(message)}`), + ); + } +}; +``` + +**Issue**: When a message doesn't match any type guard, it calls `_onerror()` but doesn't send a JSON-RPC error response back to the sender. + +### 6.3 Incomplete Implementation: transport-validator.ts + +The file `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/transport-validator.ts` exists but is incomplete: + +- Contains a proposal for validation +- Has a `ProtocolValidator` class that wraps Transport +- Implements logging but not actual validation +- The `close()` method throws "Method not implemented" +- No actual protocol checkers are implemented + +This suggests validation work was started but not completed. + +--- + +## 7. Recommendations for Implementation + +### 7.1 Priority 1: JSON-RPC Message Validation + +Implement comprehensive JSON-RPC 2.0 message validation at the transport/protocol boundary: + +1. **Validate All Incoming Messages**: + - Check for valid JSON structure (catch parse errors) + - Verify `jsonrpc === "2.0"` + - Validate required fields are present + - Check field types match specification + - Ensure proper Request/Response/Notification structure + +2. **Return Proper Error Responses**: + - -32700 for parse errors (invalid JSON) + - -32600 for invalid request structure (missing/wrong fields) + - -32601 for method not found + - -32602 for invalid parameters + +3. **Implementation Location**: + - Option A: At the Transport level (validate before passing to Protocol) + - Option B: As a Transport wrapper (like proposed `ProtocolValidator`) + - Option C: In the Protocol class's `onmessage` handler + +### 7.2 Priority 2: Security-Focused Validation + +1. **Malformed Request Protection**: + - Validate message structure depth (prevent deeply nested objects) + - Implement size limits on messages + - Validate recursion depth + +2. **Token Validation**: + - Ensure proper token validation (no passthrough) + - Verify token audience and claims + - Implement proper session binding + +3. **Input Sanitization**: + - Validate all user inputs against schemas + - Implement content filtering for prompt injection + - Use allow-lists where appropriate + +### 7.3 Priority 3: Comprehensive Testing + +1. **JSON-RPC Compliance Tests**: + - Test all invalid request formats + - Verify proper error responses + - Test batch request handling + - Test notification handling (no responses) + +2. **Security Tests**: + - Test malformed request handling + - Test deeply nested structures + - Test oversized messages + - Test invalid tokens + +3. **Transport-Specific Tests**: + - Test validation across all transport types + - Ensure consistent behavior + +### 7.4 Priority 4: Documentation and Guidelines + +1. **Security Documentation**: + - Document validation requirements for implementers + - Provide security best practices + - Include threat model documentation + +2. **Error Handling Guide**: + - Document all error codes + - Provide examples of proper error responses + - Include debugging guidance + +--- + +## 8. Related Issues and Discussions + +### GitHub Issues + +1. **Issue #563** - Invalid JSON RPC requests do not respond with an error + - https://github.com/modelcontextprotocol/typescript-sdk/issues/563 + - Status: Open + - Priority: High (directly impacts JSON-RPC compliance) + +2. **Issue #544** - The MCP protocol exhibits insufficient security design + - https://github.com/modelcontextprotocol/modelcontextprotocol/issues/544 + - Concerns: OAuth phishing, protocol-level security + - Recommendation: Address at protocol level, not just implementation + +3. **Various Transport Issues** - Multiple issues related to SSE, Streamable HTTP, and validation errors + - Indicates validation is a recurring concern across transport implementations + +### Security Discussions + +Multiple security researchers have identified concerns: +- Prompt injection vulnerabilities +- OAuth security issues +- Token passthrough anti-patterns +- Confused deputy problems + +The consensus is that **security must be addressed at the protocol level**, not left to individual implementations. + +--- + +## 9. Conclusion + +### Key Findings + +1. **JSON-RPC 2.0 Compliance is Mandatory**: MCP explicitly requires full JSON-RPC 2.0 compliance, including proper error handling for invalid messages. + +2. **Current Implementation Has Gaps**: The TypeScript SDK does not properly validate JSON-RPC message format at the protocol level, leading to non-compliant behavior (Issue #563). + +3. **Security Requires Validation**: Proper protocol-level validation is critical for security, protecting against malformed requests, prompt injection, and other attack vectors. + +4. **Incomplete Implementation Exists**: The `transport-validator.ts` file suggests validation work was started but not completed. + +### Critical Requirements Summary + +**MUST Implement**: +- ✅ JSON-RPC 2.0 message format validation +- ✅ Standard error code responses (-32700, -32600, -32601, -32602, -32603) +- ✅ Proper handling of invalid requests, responses, and notifications +- ✅ Batch request support (receiving) +- ✅ Token validation (no passthrough) +- ✅ Secure session ID generation + +**SHOULD Implement**: +- ✅ JSON Schema validation for parameters +- ✅ Recursion depth limits +- ✅ Message size limits +- ✅ Comprehensive error messages +- ✅ Logging and monitoring + +**MAY Implement**: +- JSON-RPC batch sending (receiving is MUST) +- Additional validation beyond spec requirements +- Custom transport mechanisms (must preserve JSON-RPC format) + +### Next Steps + +1. **Design Decision**: Choose validation implementation approach (Transport level, wrapper, or Protocol level) +2. **Implementation**: Build comprehensive JSON-RPC validation with proper error responses +3. **Testing**: Create comprehensive test suite for JSON-RPC compliance +4. **Documentation**: Update docs with validation requirements and security guidelines +5. **Security Review**: Conduct security review of validation implementation + +--- + +## References + +### Specifications +- JSON-RPC 2.0 Specification: https://www.jsonrpc.org/specification +- MCP Specification (2025-06-18): https://modelcontextprotocol.io/specification/2025-06-18 +- MCP Security Best Practices: https://modelcontextprotocol.io/specification/draft/basic/security_best_practices + +### Repositories +- MCP Specification Repository: https://github.com/modelcontextprotocol/modelcontextprotocol +- MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk + +### Security Resources +- RedHat MCP Security: https://www.redhat.com/en/blog/model-context-protocol-mcp-understanding-security-risks-and-controls +- Cisco MCP Security: https://community.cisco.com/t5/security-blogs/ai-model-context-protocol-mcp-and-security/ba-p/5274394 +- Writer MCP Security: https://writer.com/engineering/mcp-security-considerations/ +- Pillar Security MCP Risks: https://www.pillar.security/blog/the-security-risks-of-model-context-protocol-mcp +- Simon Willison on MCP Prompt Injection: https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/ +- Microsoft MCP Security: https://techcommunity.microsoft.com/blog/microsoftdefendercloudblog/plug-play-and-prey-the-security-risks-of-the-model-context-protocol/4410829 +- Windows MCP Security Architecture: https://blogs.windows.com/windowsexperience/2025/05/19/securing-the-model-context-protocol-building-a-safer-agentic-future-on-windows/ + +### Related Issues +- TypeScript SDK Issue #563: https://github.com/modelcontextprotocol/typescript-sdk/issues/563 +- MCP Issue #544: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/544 +- Invariant GitHub MCP Vulnerability: https://invariantlabs.ai/blog/mcp-github-vulnerability + +--- + +## Appendix: Current TypeScript SDK Code Structure + +### Relevant Files +- `/src/types.ts` - JSON-RPC type definitions and Zod schemas +- `/src/shared/protocol.ts` - Protocol class implementing message handling +- `/src/shared/transport.ts` - Transport interface definition +- `/src/shared/transport-validator.ts` - Incomplete validation implementation +- Various transport implementations (stdio, sse, streamableHttp, websocket) + +### Type Definitions (from types.ts) + +```typescript +// JSON-RPC Version +export const JSONRPC_VERSION = "2.0"; + +// Error Codes +export enum ErrorCode { + // SDK error codes + ConnectionClosed = -32000, + RequestTimeout = -32001, + + // Standard JSON-RPC error codes + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, +} + +// Type Guards +export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => + JSONRPCRequestSchema.safeParse(value).success; + +export const isJSONRPCNotification = (value: unknown): value is JSONRPCNotification => + JSONRPCNotificationSchema.safeParse(value).success; + +export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => + JSONRPCResponseSchema.safeParse(value).success; + +export const isJSONRPCError = (value: unknown): value is JSONRPCError => + JSONRPCErrorSchema.safeParse(value).success; +``` + +### Current Message Handling (from protocol.ts, lines 314-328) + +```typescript +this._transport.onmessage = (message, extra) => { + _onmessage?.(message, extra); + if (isJSONRPCResponse(message) || isJSONRPCError(message)) { + this._onresponse(message); + } else if (isJSONRPCRequest(message)) { + this._onrequest(message, extra); + } else if (isJSONRPCNotification(message)) { + this._onnotification(message); + } else { + this._onerror( + new Error(`Unknown message type: ${JSON.stringify(message)}`), + ); + } +}; +``` + +**Gap**: When message doesn't match any type, it calls `_onerror()` but doesn't send a proper JSON-RPC error response (-32600) back to the sender. + +--- + +*End of Research Document* diff --git a/intermediate-findings/sampling-analysis.md b/intermediate-findings/sampling-analysis.md new file mode 100644 index 000000000..4a8b2b491 --- /dev/null +++ b/intermediate-findings/sampling-analysis.md @@ -0,0 +1,750 @@ +# MCP Sampling Analysis: Current Implementation & Tools Support Requirements + +## Executive Summary + +This document analyzes the current sampling implementation in the MCP TypeScript SDK to understand how to add tools support. The analysis covers: + +1. Current sampling API structure +2. Message content type system +3. Existing tool infrastructure +4. Gaps that need to be filled to add tools to sampling + +--- + +## 1. Current Sampling API Structure + +### 1.1 CreateMessageRequest + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1162-1189) + +```typescript +export const CreateMessageRequestSchema = RequestSchema.extend({ + method: z.literal("sampling/createMessage"), + params: BaseRequestParamsSchema.extend({ + messages: z.array(SamplingMessageSchema), + systemPrompt: z.optional(z.string()), + includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), + temperature: z.optional(z.number()), + maxTokens: z.number().int(), + stopSequences: z.optional(z.array(z.string())), + metadata: z.optional(z.object({}).passthrough()), + modelPreferences: z.optional(ModelPreferencesSchema), + }), +}); +``` + +**Key Parameters:** +- `messages`: Array of SamplingMessage objects (user/assistant conversation history) +- `systemPrompt`: Optional system prompt string +- `includeContext`: Optional context inclusion from MCP servers +- `temperature`: Optional temperature for sampling +- `maxTokens`: Maximum tokens to generate (required) +- `stopSequences`: Optional array of stop sequences +- `metadata`: Optional provider-specific metadata +- `modelPreferences`: Optional model selection preferences + +**Note:** Currently NO support for tools parameter. + +### 1.2 CreateMessageResult + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1194-1211) + +```typescript +export const CreateMessageResultSchema = ResultSchema.extend({ + model: z.string(), + stopReason: z.optional( + z.enum(["endTurn", "stopSequence", "maxTokens"]).or(z.string()), + ), + role: z.enum(["user", "assistant"]), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema + ]), +}); +``` + +**Key Fields:** +- `model`: Name of the model that generated the message +- `stopReason`: Why sampling stopped (endTurn, stopSequence, maxTokens, or custom) +- `role`: Role of the message (user or assistant) +- `content`: Single content block (text, image, or audio) + +**Note:** Content is currently a single content block, NOT an array. Also NO support for tool_use or tool_result content types. + +### 1.3 SamplingMessage + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1152-1157) + +```typescript +export const SamplingMessageSchema = z + .object({ + role: z.enum(["user", "assistant"]), + content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), + }) + .passthrough(); +``` + +**Structure:** +- `role`: Either "user" or "assistant" +- `content`: Single content block (text, image, or audio) + +**Note:** Messages in the conversation history also only support single content blocks, not arrays. + +### 1.4 How Sampling is Invoked + +#### From Server (requesting sampling from client): + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/index.ts` (lines 332-341) + +```typescript +async createMessage( + params: CreateMessageRequest["params"], + options?: RequestOptions, +) { + return this.request( + { method: "sampling/createMessage", params }, + CreateMessageResultSchema, + options, + ); +} +``` + +#### From Client (handling sampling request): + +Client must set a request handler for sampling: + +```typescript +client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + // Client implementation to call LLM + return { + model: "test-model", + role: "assistant", + content: { + type: "text", + text: "This is a test response", + }, + }; +}); +``` + +### 1.5 Sampling Capabilities + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 279-308) + +```typescript +export const ClientCapabilitiesSchema = z + .object({ + experimental: z.optional(z.object({}).passthrough()), + sampling: z.optional(z.object({}).passthrough()), + elicitation: z.optional(z.object({}).passthrough()), + roots: z.optional( + z.object({ + listChanged: z.optional(z.boolean()), + }).passthrough(), + ), + }) + .passthrough(); +``` + +The `sampling` capability is currently just an empty object. There's no granular capability for "supports tools" or similar. + +--- + +## 2. Message Content Type System + +### 2.1 ContentBlock (Used in Prompts & Tool Results) + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 851-857) + +```typescript +export const ContentBlockSchema = z.union([ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ResourceLinkSchema, + EmbeddedResourceSchema, +]); +``` + +ContentBlock is used in: +- Prompt messages (`PromptMessageSchema`) +- Tool call results (`CallToolResultSchema`) + +### 2.2 Available Content Types + +#### TextContent + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 762-776) + +```typescript +export const TextContentSchema = z + .object({ + type: z.literal("text"), + text: z.string(), + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); +``` + +#### ImageContent + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 781-799) + +```typescript +export const ImageContentSchema = z + .object({ + type: z.literal("image"), + data: Base64Schema, + mimeType: z.string(), + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); +``` + +#### AudioContent + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 804-822) + +```typescript +export const AudioContentSchema = z + .object({ + type: z.literal("audio"), + data: Base64Schema, + mimeType: z.string(), + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); +``` + +#### EmbeddedResource + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 827-837) + +```typescript +export const EmbeddedResourceSchema = z + .object({ + type: z.literal("resource"), + resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + _meta: z.optional(z.object({}).passthrough()), + }) + .passthrough(); +``` + +#### ResourceLink + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 844-846) + +```typescript +export const ResourceLinkSchema = ResourceSchema.extend({ + type: z.literal("resource_link"), +}); +``` + +### 2.3 Content Type Differences + +**Important Distinction:** + +1. **SamplingMessage content**: Single content block (text, image, or audio only) +2. **ContentBlock**: Used in prompts & tool results (includes resource types) +3. **CallToolResult content**: Array of ContentBlock + +--- + +## 3. Tool Infrastructure + +### 3.1 Tool Definition + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 947-984) + +```typescript +export const ToolSchema = BaseMetadataSchema.extend({ + description: z.optional(z.string()), + inputSchema: z + .object({ + type: z.literal("object"), + properties: z.optional(z.object({}).passthrough()), + required: z.optional(z.array(z.string())), + }) + .passthrough(), + outputSchema: z.optional( + z.object({ + type: z.literal("object"), + properties: z.optional(z.object({}).passthrough()), + required: z.optional(z.array(z.string())), + }) + .passthrough() + ), + annotations: z.optional(ToolAnnotationsSchema), + _meta: z.optional(z.object({}).passthrough()), +}).merge(IconsSchema); +``` + +**Key Fields:** +- `name`: Tool name (from BaseMetadataSchema) +- `title`: Optional display title +- `description`: Tool description +- `inputSchema`: JSON Schema for tool input +- `outputSchema`: Optional JSON Schema for tool output +- `annotations`: Optional hints about tool behavior +- `icons`: Optional icons + +### 3.2 CallToolRequest + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1048-1054) + +```typescript +export const CallToolRequestSchema = RequestSchema.extend({ + method: z.literal("tools/call"), + params: BaseRequestParamsSchema.extend({ + name: z.string(), + arguments: z.optional(z.record(z.unknown())), + }), +}); +``` + +**Structure:** +- `name`: Tool name to call +- `arguments`: Optional record of arguments + +### 3.3 CallToolResult + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1003-1034) + +```typescript +export const CallToolResultSchema = ResultSchema.extend({ + content: z.array(ContentBlockSchema).default([]), + structuredContent: z.object({}).passthrough().optional(), + isError: z.optional(z.boolean()), +}); +``` + +**Key Fields:** +- `content`: Array of ContentBlock (text, image, audio, resource, resource_link) +- `structuredContent`: Optional structured output (if outputSchema defined) +- `isError`: Whether the tool call resulted in an error + +### 3.4 How Tools are Used + +#### Server Side (providing tools): + +**Example from:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/mcp.ts` + +```typescript +mcpServer.registerTool( + "summarize", + { + description: "Summarize any text using an LLM", + inputSchema: { + text: z.string().describe("Text to summarize"), + }, + }, + async ({ text }) => { + // Tool implementation + return { + content: [ + { + type: "text", + text: "Summary result", + }, + ], + }; + } +); +``` + +#### Client Side (calling tools): + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` (lines 429-479) + +```typescript +async callTool( + params: CallToolRequest["params"], + resultSchema: + | typeof CallToolResultSchema + | typeof CompatibilityCallToolResultSchema = CallToolResultSchema, + options?: RequestOptions, +) { + const result = await this.request( + { method: "tools/call", params }, + resultSchema, + options, + ); + + // Validate structuredContent against outputSchema if present + const validator = this.getToolOutputValidator(params.name); + if (validator) { + if (!result.structuredContent && !result.isError) { + throw new McpError( + ErrorCode.InvalidRequest, + `Tool ${params.name} has an output schema but did not return structured content` + ); + } + + if (result.structuredContent) { + const isValid = validator(result.structuredContent); + if (!isValid) { + throw new McpError( + ErrorCode.InvalidParams, + `Structured content does not match the tool's output schema` + ); + } + } + } + + return result; +} +``` + +The client caches tool output schemas from `listTools()` and validates results. + +### 3.5 Tool Capabilities + +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 377-388) + +```typescript +tools: z.optional( + z + .object({ + listChanged: z.optional(z.boolean()), + }) + .passthrough(), +), +``` + +Server advertises `tools` capability to indicate it provides tools. + +--- + +## 4. Gaps to Fill for Tools in Sampling + +### 4.1 Missing Content Types + +The current sampling system lacks content types for tool usage: + +**Need to add:** + +1. **ToolUseContent** - Represents a tool call from the LLM + - Should include: tool name, tool call ID, arguments + +2. **ToolResultContent** - Represents the result of a tool call + - Should include: tool call ID, result content, error status + +**Example structure (based on Anthropic's API):** + +```typescript +// Tool use content +{ + type: "tool_use", + id: "tool_call_123", + name: "get_weather", + input: { city: "San Francisco" } +} + +// Tool result content +{ + type: "tool_result", + tool_use_id: "tool_call_123", + content: "Weather is sunny, 72°F" +} +``` + +### 4.2 Content Array Support + +**Current Issue:** +- `SamplingMessage.content` is a single content block +- `CreateMessageResult.content` is a single content block + +**Need to change:** +- Support array of content blocks to allow multiple tool calls in one message +- Or support both single and array (discriminated union based on whether tools are used) + +**Example:** +```typescript +// Assistant message with multiple tool calls +{ + role: "assistant", + content: [ + { type: "text", text: "Let me check the weather..." }, + { type: "tool_use", id: "1", name: "get_weather", input: { city: "SF" } }, + { type: "tool_use", id: "2", name: "get_weather", input: { city: "NYC" } } + ] +} + +// User response with tool results +{ + role: "user", + content: [ + { type: "tool_result", tool_use_id: "1", content: "72°F, sunny" }, + { type: "tool_result", tool_use_id: "2", content: "65°F, cloudy" } + ] +} +``` + +### 4.3 Tools Parameter in Request + +**Current Issue:** +`CreateMessageRequestSchema` has no `tools` parameter. + +**Need to add:** +```typescript +tools: z.optional(z.array(ToolSchema)) +``` + +This allows the server to specify which tools are available to the LLM during sampling. + +### 4.4 Tool Use in Stop Reason + +**Current Issue:** +`stopReason` enum is: `["endTurn", "stopSequence", "maxTokens"]` + +**Need to add:** +`"tool_use"` as a valid stop reason to indicate the LLM wants to call tools. + +### 4.5 Tool Choice Parameter + +**Missing Feature:** +No way to control whether/how tools are used. + +**Should consider adding:** +```typescript +tool_choice: z.optional( + z.union([ + z.literal("auto"), // LLM decides + z.literal("required"), // Must use a tool + z.literal("none"), // Don't use tools + z.object({ // Force specific tool + type: z.literal("tool"), + name: z.string() + }) + ]) +) +``` + +### 4.6 Example Flow with Tools + +**1. Server requests sampling with tools:** +```typescript +await server.createMessage({ + messages: [ + { + role: "user", + content: { type: "text", text: "What's the weather in SF?" } + } + ], + tools: [ + { + name: "get_weather", + description: "Get current weather", + inputSchema: { + type: "object", + properties: { + city: { type: "string" } + }, + required: ["city"] + } + } + ], + maxTokens: 1000 +}) +``` + +**2. Client/LLM responds with tool use:** +```typescript +{ + model: "claude-3-5-sonnet", + role: "assistant", + stopReason: "tool_use", + content: [ + { + type: "text", + text: "I'll check the weather for you." + }, + { + type: "tool_use", + id: "tool_123", + name: "get_weather", + input: { city: "San Francisco" } + } + ] +} +``` + +**3. Server calls the tool and continues conversation:** +```typescript +// Server calls its own tool +const toolResult = await callTool({ + name: "get_weather", + arguments: { city: "San Francisco" } +}); + +// Continue the conversation with tool result +await server.createMessage({ + messages: [ + { + role: "user", + content: { type: "text", text: "What's the weather in SF?" } + }, + { + role: "assistant", + content: [ + { type: "text", text: "I'll check the weather for you." }, + { type: "tool_use", id: "tool_123", name: "get_weather", input: { city: "San Francisco" } } + ] + }, + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "tool_123", content: "72°F, sunny" } + ] + } + ], + tools: [...], + maxTokens: 1000 +}) +``` + +**4. Final LLM response:** +```typescript +{ + model: "claude-3-5-sonnet", + role: "assistant", + stopReason: "endTurn", + content: { + type: "text", + text: "The weather in San Francisco is currently 72°F and sunny!" + } +} +``` + +--- + +## 5. Implementation Considerations + +### 5.1 Backward Compatibility + +The changes need to maintain backward compatibility with existing implementations that don't use tools. + +**Approach:** +1. Make `tools` parameter optional +2. Support both single content and array content (discriminated union or always array) +3. Add new content types without breaking existing ones +4. Ensure existing code without tools continues to work + +### 5.2 Content Structure Decision + +**Option A: Always use array** +```typescript +content: z.array( + z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolUseContentSchema, + ToolResultContentSchema + ]) +) +``` + +**Option B: Union of single or array** +```typescript +content: z.union([ + // Single content (backward compatible) + z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ]), + // Array content (for tools) + z.array( + z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolUseContentSchema, + ToolResultContentSchema + ]) + ) +]) +``` + +**Recommendation:** Option A (always array) is cleaner but requires migration. Option B maintains perfect backward compatibility. + +### 5.3 Validation + +The client will need to validate: +1. Tool definitions match expected schema +2. Tool use IDs are unique +3. Tool result IDs match previous tool uses +4. Tool names in tool_use match provided tools + +### 5.4 Error Handling + +Need to define behavior for: +1. Tool not found +2. Invalid tool arguments +3. Tool execution errors +4. Missing tool results +5. Mismatched tool_use_id references + +--- + +## 6. Related Code Paths + +### 6.1 Test Coverage + +**Key test files:** +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/index.test.ts` - Server sampling tests (lines 208-270) +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.test.ts` - Client tool validation tests (lines 834-1303) + +The client already has extensive tests for tool output schema validation. Similar tests will be needed for tool usage in sampling. + +### 6.2 Example Usage + +**Current example:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolWithSampleServer.ts` + +This shows a tool that uses sampling internally. With tools support in sampling, this pattern becomes more powerful - a tool can call the LLM which can in turn call other tools. + +--- + +## 7. Summary + +### What Currently Works: +- Basic sampling (text, image, audio) +- Tool definitions and tool calling (separate from sampling) +- Tool output schema validation +- Message history with roles + +### What Needs to be Added: +1. **New content types:** ToolUseContent, ToolResultContent +2. **Array content support:** Messages need to support multiple content blocks +3. **Tools parameter:** CreateMessageRequest needs tools array +4. **Tool choice parameter:** Optional control over tool usage +5. **Stop reason:** Add "tool_use" to valid stop reasons +6. **Validation logic:** Ensure tool use/result consistency +7. **Documentation:** Update examples and guides + +### Critical Design Decisions: +1. Content array vs union approach for backward compatibility +2. Tool_use_id generation: client or server responsibility? +3. Error handling strategy for tool-related errors +4. Capability negotiation: extend sampling capability or add new sub-capabilities? + +--- + +## 8. Next Steps + +1. Review MCP specification for tools in sampling (if exists) +2. Decide on content structure approach (array vs union) +3. Define new Zod schemas for tool content types +4. Update CreateMessageRequest and CreateMessageResult schemas +5. Implement validation logic +6. Write comprehensive tests +7. Update documentation and examples +8. Consider migration guide for existing users + +--- + +**Document created:** 2025-10-01 +**SDK Version:** Based on commit 856d9ec (post v1.18.2) +**Analysis completed by:** Claude (AI Assistant) diff --git a/intermediate-findings/sampling-examples-review.md b/intermediate-findings/sampling-examples-review.md new file mode 100644 index 000000000..cd0aa8391 --- /dev/null +++ b/intermediate-findings/sampling-examples-review.md @@ -0,0 +1,794 @@ +# MCP TypeScript SDK - Sampling Examples Review + +## Overview + +This document provides a comprehensive review of sampling examples in the MCP TypeScript SDK, covering their structure, patterns, dependencies, and best practices for implementation. + +--- + +## Key Sampling Examples + +### 1. **backfillSampling.ts** (Proxy Pattern) +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/backfill/backfillSampling.ts` + +**Purpose:** Implements an MCP proxy that backfills sampling requests using the Claude API when a client doesn't support native sampling. + +**Key Features:** +- Acts as a middleware proxy between client and server +- Detects client sampling capabilities during initialization +- Intercepts `sampling/createMessage` requests +- Translates MCP requests to Claude API format +- Handles tool calling support +- Converts responses back to MCP format + +**Dependencies:** +```typescript +import { Anthropic } from "@anthropic-ai/sdk"; +import { StdioServerTransport } from '../../server/stdio.js'; +import { StdioClientTransport } from '../../client/stdio.js'; +``` + +**Usage Pattern:** +```bash +npx -y @modelcontextprotocol/inspector \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ + npx -y --silent @modelcontextprotocol/server-everything +``` + +--- + +### 2. **toolWithSampleServer.ts** (Server-Side Sampling) +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolWithSampleServer.ts` + +**Purpose:** Demonstrates how a server can use LLM sampling to implement intelligent tools. + +**Key Features:** +- Registers tools that internally use sampling +- Simple `summarize` tool that uses `mcpServer.server.createMessage()` +- Shows how to call LLM through MCP sampling API +- Demonstrates proper response handling + +**Core Pattern:** +```typescript +mcpServer.registerTool( + "summarize", + { + description: "Summarize any text using an LLM", + inputSchema: { + text: z.string().describe("Text to summarize"), + }, + }, + async ({ text }) => { + // Call the LLM through MCP sampling + const response = await mcpServer.server.createMessage({ + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please summarize the following text concisely:\n\n${text}`, + }, + }, + ], + maxTokens: 500, + }); + + return { + content: [ + { + type: "text", + text: response.content.type === "text" ? response.content.text : "Unable to generate summary", + }, + ], + }; + } +); +``` + +**Transport Setup:** +```typescript +const transport = new StdioServerTransport(); +await mcpServer.connect(transport); +``` + +--- + +## Common Server Example Patterns + +### 3. **simpleStreamableHttp.ts** (Full-Featured Server) +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/simpleStreamableHttp.ts` + +**Key Patterns:** +- Express-based HTTP server setup +- Session management with in-memory event store +- Tool registration with `registerTool()` or `tool()` +- Prompt registration with `registerPrompt()` +- Resource registration with `registerResource()` +- Notification handling via `sendLoggingMessage()` +- OAuth support (optional) + +**Server Initialization:** +```typescript +const getServer = () => { + const server = new McpServer({ + name: 'simple-streamable-http-server', + version: '1.0.0', + icons: [{src: './mcp.svg', sizes: ['512x512'], mimeType: 'image/svg+xml'}], + websiteUrl: 'https://github.com/modelcontextprotocol/typescript-sdk', + }, { capabilities: { logging: {} } }); + + // Register tools, prompts, resources... + return server; +}; +``` + +**Transport Management:** +```typescript +const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + +// For new sessions +const eventStore = new InMemoryEventStore(); +transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore, // Enable resumability + onsessioninitialized: (sessionId) => { + console.log(`Session initialized with ID: ${sessionId}`); + transports[sessionId] = transport; + } +}); + +// Connect and handle requests +const server = getServer(); +await server.connect(transport); +await transport.handleRequest(req, res, req.body); +``` + +--- + +### 4. **simpleSseServer.ts** (SSE Transport Pattern) +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/simpleSseServer.ts` + +**Key Patterns:** +- Deprecated HTTP+SSE transport (protocol version 2024-11-05) +- Separate endpoints for SSE stream and messages +- Session tracking by transport + +**Transport Setup:** +```typescript +const transports: Record = {}; + +app.get('/mcp', async (req: Request, res: Response) => { + const transport = new SSEServerTransport('/messages', res); + const sessionId = transport.sessionId; + transports[sessionId] = transport; + + transport.onclose = () => { + delete transports[sessionId]; + }; + + const server = getServer(); + await server.connect(transport); +}); + +app.post('/messages', async (req: Request, res: Response) => { + const sessionId = req.query.sessionId as string; + const transport = transports[sessionId]; + await transport.handlePostMessage(req, res, req.body); +}); +``` + +--- + +## Client Example Patterns + +### 5. **parallelToolCallsClient.ts** +**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/client/parallelToolCallsClient.ts` + +**Key Patterns:** +- Client initialization with capabilities +- Transport connection +- Notification handlers +- Parallel tool execution +- Request handling with schemas + +**Client Setup:** +```typescript +const client = new Client({ + name: 'parallel-tool-calls-client', + version: '1.0.0' +}); + +client.onerror = (error) => { + console.error('Client error:', error); +}; + +const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); +await client.connect(transport); + +// Set up notification handlers +client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { + console.log(`Notification: ${notification.params.data}`); +}); +``` + +**Tool Calling:** +```typescript +const result = await client.request({ + method: 'tools/call', + params: { + name: 'tool-name', + arguments: { /* args */ } + } +}, CallToolResultSchema); +``` + +--- + +## Type System Structure + +### Sampling Types (from types.ts) + +**CreateMessageRequest:** +```typescript +export const CreateMessageRequestSchema = RequestSchema.extend({ + method: z.literal("sampling/createMessage"), + params: BaseRequestParamsSchema.extend({ + messages: z.array(SamplingMessageSchema), + systemPrompt: z.optional(z.string()), + includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), + temperature: z.optional(z.number()), + maxTokens: z.number().int(), + stopSequences: z.optional(z.array(z.string())), + metadata: z.optional(z.object({}).passthrough()), + modelPreferences: z.optional(ModelPreferencesSchema), + tools: z.optional(z.array(ToolSchema)), // Tool definitions + tool_choice: z.optional(ToolChoiceSchema), // Tool usage control + }), +}); +``` + +**CreateMessageResult:** +```typescript +export const CreateMessageResultSchema = ResultSchema.extend({ + model: z.string(), + stopReason: z.optional( + z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]).or(z.string()), + ), + role: z.literal("assistant"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), +}); +``` + +**Message Types:** +```typescript +// User message (from server to LLM) +export const UserMessageSchema = z.object({ + role: z.literal("user"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolResultContentSchema, + ]), + _meta: z.optional(z.object({}).passthrough()), +}); + +// Assistant message (from LLM to server) +export const AssistantMessageSchema = z.object({ + role: z.literal("assistant"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), + _meta: z.optional(z.object({}).passthrough()), +}); +``` + +--- + +## Server Class Methods (from server/index.ts) + +### Sampling Methods + +**createMessage:** +```typescript +async createMessage( + params: CreateMessageRequest["params"], + options?: RequestOptions, +) { + return this.request( + { method: "sampling/createMessage", params }, + CreateMessageResultSchema, + options, + ); +} +``` + +**elicitInput:** +```typescript +async elicitInput( + params: ElicitRequest["params"], + options?: RequestOptions, +): Promise { + const result = await this.request( + { method: "elicitation/create", params }, + ElicitResultSchema, + options, + ); + // Validates response content against requested schema + return result; +} +``` + +### Capability Assertions + +The Server class validates capabilities before allowing methods: + +```typescript +protected assertCapabilityForMethod(method: RequestT["method"]): void { + switch (method as ServerRequest["method"]) { + case "sampling/createMessage": + if (!this._clientCapabilities?.sampling) { + throw new Error( + `Client does not support sampling (required for ${method})`, + ); + } + break; + + case "elicitation/create": + if (!this._clientCapabilities?.elicitation) { + throw new Error( + `Client does not support elicitation (required for ${method})`, + ); + } + break; + } +} +``` + +--- + +## Dependencies + +### Core Dependencies +- **zod**: Schema validation (v3.23.8) +- **express**: HTTP server framework (v5.0.1) +- **cors**: CORS middleware (v2.8.5) + +### Sampling-Specific Dependencies +- **@anthropic-ai/sdk**: Claude API client (v0.65.0) - devDependency + - Used in backfillSampling.ts example + - Provides types and API client for Claude integration + +### Transport Dependencies +- **eventsource**: SSE client support (v3.0.2) +- **eventsource-parser**: SSE parsing (v3.0.0) +- **cross-spawn**: Process spawning for stdio (v7.0.5) + +### Other Utilities +- **ajv**: JSON Schema validation (v6.12.6) +- **zod-to-json-schema**: Convert Zod to JSON Schema (v3.24.1) + +--- + +## Tool Registration Patterns + +### Pattern 1: registerTool (with metadata) +```typescript +mcpServer.registerTool( + 'tool-name', + { + title: 'Tool Display Name', + description: 'Tool description', + inputSchema: { + param1: z.string().describe('Parameter description'), + param2: z.number().optional().describe('Optional parameter'), + }, + }, + async (args): Promise => { + // Tool implementation + return { + content: [ + { + type: 'text', + text: 'Result text', + }, + ], + }; + } +); +``` + +### Pattern 2: tool (shorthand) +```typescript +mcpServer.tool( + 'tool-name', + 'Tool description', + { + param1: z.string().describe('Parameter description'), + }, + { + title: 'Tool Display Name', + readOnlyHint: true, + openWorldHint: false + }, + async (args, extra): Promise => { + // Access session ID via extra.sessionId + return { + content: [{ type: 'text', text: 'Result' }], + }; + } +); +``` + +--- + +## Notification Patterns + +### Sending Notifications from Server +```typescript +// In tool handler +async ({ name }, extra): Promise => { + // Send logging notification + await server.sendLoggingMessage({ + level: "info", + data: `Processing request for ${name}` + }, extra.sessionId); + + // Process... + + return { + content: [{ type: 'text', text: 'Done' }], + }; +} +``` + +### Receiving Notifications in Client +```typescript +client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { + console.log(`[${notification.params.level}] ${notification.params.data}`); +}); +``` + +--- + +## Error Handling Patterns + +### Server-Side Error Handling +```typescript +try { + const result = await someOperation(); + return { + content: [{ type: 'text', text: result }], + }; +} catch (error) { + return { + content: [ + { + type: 'text', + text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + ], + }; +} +``` + +### Client-Side Error Handling +```typescript +client.onerror = (error) => { + console.error('Client error:', error); +}; + +try { + const result = await client.request(request, schema); +} catch (error) { + console.error('Request failed:', error); +} +``` + +--- + +## Best Practices + +### 1. **Server Setup** +- Use `getServer()` pattern to create fresh server instances +- Register capabilities at initialization: `{ capabilities: { logging: {}, sampling: {} } }` +- Set up proper session management with unique IDs +- Implement proper cleanup in `onclose` handlers + +### 2. **Tool Implementation** +- Use Zod schemas for input validation +- Provide clear descriptions for all parameters +- Return proper `CallToolResult` format +- Handle errors gracefully and return user-friendly messages +- Use `extra.sessionId` when sending notifications + +### 3. **Sampling Integration** +- Check client capabilities before calling `createMessage()` +- Provide clear system prompts +- Set appropriate `maxTokens` limits +- Handle all possible `stopReason` values +- Check response `content.type` before accessing type-specific fields + +### 4. **Transport Management** +- Store transports by session ID in a map +- Clean up closed transports +- Support resumability with EventStore +- Handle reconnection scenarios + +### 5. **Type Safety** +- Use Zod schemas for request/response validation +- Use type guards for message discrimination +- Validate schemas with `.safeParse()` when needed +- Export and reuse schema definitions + +### 6. **Error Handling** +- Set up `onerror` handlers +- Validate capabilities before making requests +- Handle transport errors gracefully +- Provide meaningful error messages + +--- + +## File Structure Convention + +``` +src/examples/ +├── client/ # Client implementations +│ ├── simpleStreamableHttp.ts +│ ├── parallelToolCallsClient.ts +│ └── ... +├── server/ # Server implementations +│ ├── simpleStreamableHttp.ts +│ ├── simpleSseServer.ts +│ ├── toolWithSampleServer.ts +│ └── ... +├── backfill/ # Proxy/middleware implementations +│ └── backfillSampling.ts +└── shared/ # Shared utilities + └── inMemoryEventStore.ts +``` + +--- + +## Testing Patterns + +### Running Examples + +**Server:** +```bash +npx tsx src/examples/server/simpleStreamableHttp.ts +npx tsx src/examples/server/toolWithSampleServer.ts +``` + +**Client:** +```bash +npx tsx src/examples/client/simpleStreamableHttp.ts +``` + +**Proxy:** +```bash +npx -y @modelcontextprotocol/inspector \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ + npx -y --silent @modelcontextprotocol/server-everything +``` + +### Test Suite Pattern +- Co-locate tests with source: `*.test.ts` +- Use descriptive test names +- Test both success and error cases +- Validate schemas with Zod +- Mock transports for unit tests + +--- + +## Code Style + +- **TypeScript**: Strict mode, explicit return types +- **Naming**: PascalCase for classes/types, camelCase for functions/variables +- **Files**: Lowercase with hyphens, test files with `.test.ts` suffix +- **Imports**: ES module style, include `.js` extension +- **Formatting**: 2-space indentation, semicolons required, single quotes preferred +- **Comments**: JSDoc for public APIs + +--- + +## Adaptable Code Snippets + +### Basic Server Setup +```typescript +import { McpServer } from "../../server/mcp.js"; +import { StdioServerTransport } from "../../server/stdio.js"; +import { z } from "zod"; + +const mcpServer = new McpServer({ + name: "my-server", + version: "1.0.0", +}, { capabilities: { sampling: {} } }); + +mcpServer.registerTool( + "my-tool", + { + description: "My tool description", + inputSchema: { + input: z.string().describe("Input parameter"), + }, + }, + async ({ input }) => { + // Tool logic here + return { + content: [ + { + type: "text", + text: `Processed: ${input}`, + }, + ], + }; + } +); + +async function main() { + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); + console.log("Server running..."); +} + +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); +}); +``` + +### Sampling Tool Pattern +```typescript +mcpServer.registerTool( + "llm-powered-tool", + { + description: "Tool that uses LLM sampling", + inputSchema: { + query: z.string().describe("Query to process"), + }, + }, + async ({ query }) => { + try { + const response = await mcpServer.server.createMessage({ + messages: [ + { + role: "user", + content: { + type: "text", + text: query, + }, + }, + ], + maxTokens: 1000, + }); + + return { + content: [ + { + type: "text", + text: response.content.type === "text" + ? response.content.text + : "Unable to generate response", + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + ], + }; + } + } +); +``` + +### HTTP Server with Tools +```typescript +import express from 'express'; +import { McpServer } from '../../server/mcp.js'; +import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; +import { randomUUID } from 'node:crypto'; +import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; + +const app = express(); +app.use(express.json()); + +const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + +const getServer = () => { + const server = new McpServer({ + name: 'my-http-server', + version: '1.0.0', + }, { capabilities: { logging: {} } }); + + // Register tools... + + return server; +}; + +app.post('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + let transport: StreamableHTTPServerTransport; + if (sessionId && transports[sessionId]) { + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + const eventStore = new InMemoryEventStore(); + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore, + onsessioninitialized: (sid) => { + transports[sid] = transport; + } + }); + + transport.onclose = () => { + const sid = transport.sessionId; + if (sid && transports[sid]) { + delete transports[sid]; + } + }; + + const server = getServer(); + await server.connect(transport); + } else { + res.status(400).json({ + jsonrpc: '2.0', + error: { code: -32000, message: 'Bad Request' }, + id: null, + }); + return; + } + + await transport.handleRequest(req, res, req.body); +}); + +const PORT = 3000; +app.listen(PORT, () => { + console.log(`Server listening on port ${PORT}`); +}); +``` + +--- + +## Summary + +The MCP TypeScript SDK provides a robust framework for implementing sampling-enabled MCP servers. Key takeaways: + +1. **Two Main Sampling Patterns:** + - **Proxy/Backfill**: Intercept and handle sampling for non-supporting clients + - **Server-Side Tools**: Implement tools that use sampling internally + +2. **Core Components:** + - `McpServer` for server implementation + - `Server.createMessage()` for sampling requests + - Transport abstractions (Stdio, HTTP, SSE) + - Zod-based schema validation + +3. **Best Practices:** + - Check capabilities before making sampling requests + - Provide clear tool descriptions and schemas + - Handle errors gracefully + - Clean up resources properly + - Use TypeScript strict mode + +4. **Examples Structure:** + - Simple examples for learning (toolWithSampleServer.ts) + - Complex examples for reference (backfillSampling.ts) + - Full-featured servers (simpleStreamableHttp.ts) + - Client implementations for testing + +This foundation enables building sophisticated MCP servers that leverage LLM capabilities while maintaining proper protocol compliance and type safety. diff --git a/intermediate-findings/sampling-tool-additions.md b/intermediate-findings/sampling-tool-additions.md new file mode 100644 index 000000000..1f1725277 --- /dev/null +++ b/intermediate-findings/sampling-tool-additions.md @@ -0,0 +1,632 @@ +# Sampling Tool Call Additions Analysis (SEP-1577) + +## Summary of Changes + +The branch `ochafik/sep1577` implements comprehensive tool calling support for MCP sampling (SEP-1577). This enables agentic workflows where LLMs can request tool execution during sampling operations. The changes include: + +1. **New content types** for tool calls and results in messages +2. **Role-specific message types** (UserMessage, AssistantMessage) with appropriate content types +3. **Tool choice controls** to specify when/how tools should be used +4. **Extended sampling requests** with tools and tool_choice parameters +5. **Extended sampling responses** with new stop reasons including "toolUse" +6. **Client capabilities** signaling for tool support +7. **Complete example implementation** in the backfill sampling proxy + +## Key Types and Interfaces Added + +### 1. ToolCallContent (Assistant → User) + +Represents the LLM's request to use a tool. This appears in assistant messages. + +```typescript +export const ToolCallContentSchema = 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(), + /** + * Optional metadata + */ + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +export type ToolCallContent = z.infer; +``` + +**Example:** +```typescript +{ + type: "tool_use", + id: "call_123", + name: "get_weather", + input: { city: "San Francisco", units: "celsius" } +} +``` + +### 2. ToolResultContent (User → Assistant) + +Represents the result of executing a tool. This appears in user messages to provide tool execution results back to the LLM. + +```typescript +export const ToolResultContentSchema = z.object({ + type: z.literal("tool_result"), + /** + * The ID of the tool call this result corresponds to. + * Must match a ToolCallContent.id from a previous assistant message. + */ + toolUseId: z.string(), + /** + * The result of the tool execution. + * Can be any JSON-serializable object. + * Error information should be included in the content itself. + */ + content: z.object({}).passthrough(), + /** + * Optional metadata + */ + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +export type ToolResultContent = z.infer; +``` + +**Example (success):** +```typescript +{ + type: "tool_result", + toolUseId: "call_123", + content: { temperature: 72, condition: "sunny", units: "fahrenheit" } +} +``` + +**Example (error in content):** +```typescript +{ + type: "tool_result", + toolUseId: "call_123", + content: { error: "API_ERROR", message: "Service unavailable" } +} +``` + +**Important:** Errors are represented directly in the `content` object, not via a separate `isError` field. This aligns with Claude and OpenAI APIs. + +### 3. ToolChoice + +Controls when and how tools are used during sampling. + +```typescript +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"])), + /** + * 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 type ToolChoice = z.infer; +``` + +**Examples:** +```typescript +// Let model decide +{ mode: "auto" } + +// Force tool use +{ mode: "required" } + +// Sequential tool calls only +{ mode: "auto", disable_parallel_tool_use: true } +``` + +### 4. Role-Specific Message Types + +Messages are now split by role, with each role allowing specific content types: + +#### UserMessage +```typescript +export const UserMessageSchema = z.object({ + role: z.literal("user"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolResultContentSchema, // NEW: Users provide tool results + ]), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +export type UserMessage = z.infer; +``` + +#### AssistantMessage +```typescript +export const AssistantMessageSchema = z.object({ + role: z.literal("assistant"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, // NEW: Assistants request tool use + ]), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +export type AssistantMessage = z.infer; +``` + +#### SamplingMessage +```typescript +export const SamplingMessageSchema = z.discriminatedUnion("role", [ + UserMessageSchema, + AssistantMessageSchema, +]); + +export type SamplingMessage = z.infer; +``` + +### 5. CreateMessageRequest (Extended) + +The sampling request now supports tools: + +```typescript +export const CreateMessageRequestSchema = RequestSchema.extend({ + method: z.literal("sampling/createMessage"), + params: BaseRequestParamsSchema.extend({ + messages: z.array(SamplingMessageSchema), + systemPrompt: z.optional(z.string()), + temperature: z.optional(z.number()), + maxTokens: z.number().int(), + stopSequences: z.optional(z.array(z.string())), + metadata: z.optional(z.object({}).passthrough()), + modelPreferences: z.optional(ModelPreferencesSchema), + + // NEW: Tool support + /** + * 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. + */ + tool_choice: z.optional(ToolChoiceSchema), + + // SOFT-DEPRECATED: Use tools parameter instead + includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), + }), +}); + +export type CreateMessageRequest = z.infer; +``` + +**Example request:** +```typescript +{ + method: "sampling/createMessage", + params: { + messages: [ + { + role: "user", + content: { type: "text", text: "What's the weather in San Francisco?" } + } + ], + maxTokens: 1000, + tools: [ + { + name: "get_weather", + description: "Get current weather for a location", + inputSchema: { + type: "object", + properties: { + city: { type: "string" }, + units: { type: "string", enum: ["celsius", "fahrenheit"] } + }, + required: ["city"] + } + } + ], + tool_choice: { mode: "auto" } + } +} +``` + +### 6. CreateMessageResult (Extended) + +The sampling response now supports tool use stop reasons and tool call content: + +```typescript +export const CreateMessageResultSchema = ResultSchema.extend({ + /** + * The name of the model that generated the message. + */ + model: z.string(), + + /** + * The reason why sampling stopped. + * - "endTurn": Model completed naturally + * - "stopSequence": Hit a stop sequence + * - "maxTokens": Reached token limit + * - "toolUse": Model wants to use a tool // NEW + * - "refusal": Model refused the request // NEW + * - "other": Other provider-specific reason // NEW + */ + stopReason: z.optional( + z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]) + .or(z.string()) + ), + + /** + * Always "assistant" for sampling responses + */ + role: z.literal("assistant"), + + /** + * Response content. May be ToolCallContent if stopReason is "toolUse". + */ + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, // NEW + ]), +}); + +export type CreateMessageResult = z.infer; +``` + +**Example response with tool call:** +```typescript +{ + model: "claude-3-5-sonnet-20241022", + role: "assistant", + content: { + type: "tool_use", + id: "call_abc123", + name: "get_weather", + input: { city: "San Francisco", units: "celsius" } + }, + stopReason: "toolUse" +} +``` + +### 7. Client Capabilities + +Signal tool support in capabilities: + +```typescript +export const ClientCapabilitiesSchema = z.object({ + sampling: z.optional( + z.object({ + /** + * Present if the client supports non-'none' values for includeContext. + * SOFT-DEPRECATED: New implementations should use tools parameter instead. + */ + context: z.optional(z.object({}).passthrough()), + + /** + * Present if the client supports tools and tool_choice parameters. + * Presence indicates full tool calling support. + */ + tools: z.optional(z.object({}).passthrough()), // NEW + }).passthrough() + ), + // ... other capabilities +}).passthrough(); +``` + +**Example:** +```typescript +{ + sampling: { + tools: {} // Indicates client supports tool calling + } +} +``` + +## How the Tool Call Loop Works + +The tool calling flow follows this pattern: + +### 1. Initial Request with Tools + +Server sends a sampling request with available tools: + +```typescript +{ + method: "sampling/createMessage", + params: { + messages: [ + { + role: "user", + content: { type: "text", text: "What's the weather in SF?" } + } + ], + maxTokens: 1000, + tools: [ + { + name: "get_weather", + description: "Get weather for a location", + inputSchema: { + type: "object", + properties: { + city: { type: "string" }, + units: { type: "string", enum: ["celsius", "fahrenheit"] } + }, + required: ["city"] + } + } + ], + tool_choice: { mode: "auto" } + } +} +``` + +### 2. LLM Responds with Tool Call + +Client/LLM decides to use a tool and responds: + +```typescript +{ + model: "claude-3-5-sonnet-20241022", + role: "assistant", + content: { + type: "tool_use", + id: "toolu_01A2B3C4D5", + name: "get_weather", + input: { city: "San Francisco", units: "celsius" } + }, + stopReason: "toolUse" +} +``` + +### 3. Server Executes Tool + +Server receives tool call, executes the tool (e.g., calls weather API), and sends another request with the result: + +```typescript +{ + method: "sampling/createMessage", + params: { + messages: [ + // Original user message + { + role: "user", + content: { type: "text", text: "What's the weather in SF?" } + }, + // Assistant's tool call + { + role: "assistant", + content: { + type: "tool_use", + id: "toolu_01A2B3C4D5", + name: "get_weather", + input: { city: "San Francisco", units: "celsius" } + } + }, + // Tool result from server + { + role: "user", + content: { + type: "tool_result", + toolUseId: "toolu_01A2B3C4D5", + content: { + temperature: 18, + condition: "partly cloudy", + humidity: 65 + } + } + } + ], + maxTokens: 1000, + tools: [...], // Same tools as before + tool_choice: { mode: "auto" } + } +} +``` + +### 4. LLM Provides Final Answer + +Client/LLM uses the tool result to provide a final answer: + +```typescript +{ + model: "claude-3-5-sonnet-20241022", + role: "assistant", + content: { + type: "text", + text: "The weather in San Francisco is currently 18°C and partly cloudy with 65% humidity." + }, + stopReason: "endTurn" +} +``` + +## Implementation Example + +The `backfillSampling.ts` example demonstrates a complete implementation. Key conversion functions: + +### Tool Definition Conversion +```typescript +function toolToClaudeFormat(tool: Tool): ClaudeTool { + return { + name: tool.name, + description: tool.description || "", + input_schema: tool.inputSchema, + }; +} +``` + +### Tool Choice Conversion +```typescript +function toolChoiceToClaudeFormat( + toolChoice: CreateMessageRequest['params']['tool_choice'] +): ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool | undefined { + if (!toolChoice) return undefined; + + if (toolChoice.mode === "required") { + return { + type: "any", + disable_parallel_tool_use: toolChoice.disable_parallel_tool_use + }; + } + + return { + type: "auto", + disable_parallel_tool_use: toolChoice.disable_parallel_tool_use + }; +} +``` + +### Content Conversion (Claude → MCP) +```typescript +function contentToMcp(content: ContentBlock): CreateMessageResult['content'] { + switch (content.type) { + case 'text': + return { type: 'text', text: content.text }; + case 'tool_use': + return { + type: 'tool_use', + id: content.id, + name: content.name, + input: content.input, + } as ToolCallContent; + default: + throw new Error(`Unsupported content type: ${(content as any).type}`); + } +} +``` + +### Content Conversion (MCP → Claude) +```typescript +function contentFromMcp( + content: UserMessage['content'] | AssistantMessage['content'] +): ContentBlockParam { + switch (content.type) { + case 'text': + return { type: 'text', text: content.text }; + case 'image': + return { + type: 'image', + source: { + data: content.data, + media_type: content.mimeType as Base64ImageSource['media_type'], + type: 'base64', + }, + }; + case 'tool_result': + return { + type: 'tool_result', + tool_use_id: content.toolUseId, + content: JSON.stringify(content.content), + }; + default: + throw new Error(`Unsupported content type: ${(content as any).type}`); + } +} +``` + +### Stop Reason Mapping +```typescript +let stopReason: CreateMessageResult['stopReason'] = msg.stop_reason as any; +if (msg.stop_reason === 'tool_use') { + stopReason = 'toolUse'; +} else if (msg.stop_reason === 'max_tokens') { + stopReason = 'maxTokens'; +} else if (msg.stop_reason === 'end_turn') { + stopReason = 'endTurn'; +} else if (msg.stop_reason === 'stop_sequence') { + stopReason = 'stopSequence'; +} +``` + +## Testing + +The implementation includes comprehensive tests in `src/types.test.ts`: + +- ToolCallContent validation (with/without _meta, error cases) +- ToolResultContent validation (success, errors in content, missing fields) +- ToolChoice validation (auto, required, parallel control) +- UserMessage/AssistantMessage with tool content types +- CreateMessageRequest with tools and tool_choice +- CreateMessageResult with tool calls and new stop reasons +- All new stop reasons: endTurn, stopSequence, maxTokens, toolUse, refusal, other +- Custom stop reason strings + +Total: 27 new test cases added, all passing (47/47 in types.test.ts, 683/683 overall). + +## Key Design Decisions + +1. **No `isError` field**: Errors are represented in the `content` object itself, matching Claude/OpenAI APIs +2. **Role-specific content types**: UserMessage can have tool_result, AssistantMessage can have tool_use +3. **Discriminated unions**: Both messages and content use discriminated unions for type safety +4. **Soft deprecation**: `includeContext` is soft-deprecated in favor of explicit `tools` parameter +5. **Extensible stop reasons**: Stop reasons are an enum but also allow arbitrary strings for provider-specific reasons +6. **Tool correlation**: Tool calls and results are linked via unique IDs (id/toolUseId) + +## Client Capabilities Check + +Before using tools in sampling requests, verify the client supports them: + +```typescript +// In server code +if (client.getServerCapabilities()?.sampling?.tools) { + // Client supports tool calling + // Can send CreateMessageRequest with tools parameter +} +``` + +## Migration Notes + +For existing code using sampling without tools: +- No breaking changes - tools are optional +- `includeContext` still works but is soft-deprecated +- All existing sampling requests continue to work unchanged +- To add tool support: + 1. Add `sampling.tools = {}` to client capabilities + 2. Include `tools` array in CreateMessageRequest.params + 3. Optionally include `tool_choice` to control tool usage + 4. Handle ToolCallContent in responses + 5. Send ToolResultContent in follow-up requests + +## Related Files + +- **Type definitions**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` +- **Client implementation**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` +- **Protocol base**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.ts` +- **Example implementation**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/backfill/backfillSampling.ts` +- **Tests**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.test.ts` + +## Summary + +SEP-1577 adds comprehensive, type-safe tool calling support to MCP sampling. The implementation: +- ✅ Introduces new content types (ToolCallContent, ToolResultContent) +- ✅ Splits messages by role with appropriate content types +- ✅ Adds tool choice controls +- ✅ Extends sampling request/response schemas +- ✅ Includes client capability signaling +- ✅ Provides complete example implementation +- ✅ Has comprehensive test coverage +- ✅ Maintains backward compatibility +- ✅ Aligns with Claude and OpenAI API conventions + +The tool loop enables agentic workflows where servers can provide tools to LLMs, have the LLM request tool execution, execute those tools, and provide results back to the LLM for final answers. diff --git a/intermediate-findings/sep-1577-spec.md b/intermediate-findings/sep-1577-spec.md new file mode 100644 index 000000000..10fafd313 --- /dev/null +++ b/intermediate-findings/sep-1577-spec.md @@ -0,0 +1,1429 @@ +# SEP-1577: Sampling With Tools - Complete Technical Specification + +**Research Date:** 2025-10-01 +**Status:** Draft SEP +**Source:** https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577 +**Author:** Olivier Chafik (@ochafik) +**Sponsor:** @bhosmer-ant +**Target Spec Version:** MCP 2025-06-18 + +--- + +## Executive Summary + +SEP-1577 introduces tool calling support to MCP's `sampling/createMessage` request, enabling MCP servers to run agentic loops using client LLM tokens. This enhancement addresses three key issues: +1. Lack of tool calling support in current sampling implementation +2. Ambiguous definition of context inclusion parameters +3. Low adoption of sampling features by MCP clients + +The specification soft-deprecates the `includeContext` parameter in favor of explicit tool definitions and introduces new capability negotiation requirements. + +--- + +## 1. Motivation and Background + +### Current Problems + +1. **No Tool Support**: Current `sampling/createMessage` lacks tool calling capability, limiting agentic workflows +2. **Ambiguous Context Inclusion**: The `includeContext` parameter's behavior is poorly defined and inconsistently implemented +3. **Low Client Adoption**: Complex and ambiguous requirements have led to minimal client support + +### Goals + +- Enable servers to orchestrate multi-step tool-based workflows using client LLM access +- Standardize tool calling across different AI model providers +- Simplify client implementation requirements +- Maintain backwards compatibility with existing implementations + +### Related Discussions + +- Discussion #124: "Improve sampling in the protocol" +- Issue #503: "Reframe sampling as a basis for bidirectional agent-to-agent communication" +- Discussion #314: "Task semantics and multi-turn interactions with tools" + +--- + +## 2. Type Definitions + +### 2.1 Client Capabilities + +**Updated Schema:** + +```typescript +interface ClientCapabilities { + sampling?: { + /** + * If present, client supports non-'none' values for includeContext parameter. + * Soft-deprecated - new implementations should use tools parameter instead. + */ + context?: object; + + /** + * If present, client supports tools and tool_choice parameters. + * Presence of this capability indicates full tool calling support. + */ + tools?: object; + }; + // ... other capabilities +} +``` + +**Capability Negotiation Rules:** + +1. If `sampling.tools` is NOT present: + - Server MUST NOT include `tools` or `tool_choice` in `CreateMessageRequest` + - Server MUST throw error if it requires tool support + +2. If `sampling.context` is NOT present: + - Server MUST NOT use `includeContext` with values `"thisServer"` or `"allServers"` + - Server MAY use `includeContext: "none"` (default behavior) + +3. Servers SHOULD prefer `tools` over `includeContext` when both are available + +--- + +### 2.2 Tool-Related Types + +#### ToolChoice + +```typescript +interface ToolChoice { + /** + * Controls when tools are used: + * - "auto": Model decides whether to use tools (default) + * - "required": Model MUST use at least one tool before completing + */ + mode?: "auto" | "required"; + + /** + * If true, model should not use multiple tools in parallel. + * Some models may ignore this hint. + * Default: false + */ + disable_parallel_tool_use?: boolean; +} +``` + +**Notes:** +- `mode` defaults to `"auto"` if not specified +- `disable_parallel_tool_use` is a hint, not a guarantee +- Future extensions may add tool-specific selection (e.g., `{"type": "tool", "name": "search"}`) + +#### Tool (Reference) + +The existing `Tool` type from `tools/list` is reused: + +```typescript +interface Tool { + name: string; + title?: string; + description?: string; + inputSchema: { + type: "object"; + properties?: Record; + required?: string[]; + }; + outputSchema?: { + type: "object"; + properties?: Record; + required?: string[]; + }; + annotations?: ToolAnnotations; + _meta?: Record; + icons?: Icon[]; +} +``` + +**Important:** Tools passed in sampling requests use the same schema as `tools/list` responses. + +--- + +### 2.3 New Content Types + +#### ToolCallContent + +Represents a tool invocation request from the assistant. + +```typescript +interface ToolCallContent { + /** + * Discriminator for content type + */ + type: "tool_use"; + + /** + * The name of the tool to invoke. + * Must match a tool name from the request's tools array. + */ + name: string; + + /** + * Unique identifier for this tool call. + * Used to correlate with ToolResultContent in subsequent messages. + */ + id: string; + + /** + * Arguments to pass to the tool. + * Must conform to the tool's inputSchema. + */ + input: object; + + /** + * Optional metadata + */ + _meta?: Record; +} +``` + +**Validation Rules:** +- `name` MUST reference a tool from the request's `tools` array +- `id` MUST be unique within the conversation +- `input` MUST validate against the tool's `inputSchema` +- `id` format is provider-specific (commonly UUIDs or sequential IDs) + +**Zod Schema:** + +```typescript +const ToolCallContentSchema = z.object({ + type: z.literal("tool_use"), + name: z.string(), + id: z.string(), + input: z.object({}).passthrough(), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); +``` + +#### ToolResultContent + +Represents the result of a tool execution, sent by the user (server). + +```typescript +interface ToolResultContent { + /** + * Discriminator for content type + */ + type: "tool_result"; + + /** + * The ID of the tool call this result corresponds to. + * Must match a ToolCallContent.id from a previous assistant message. + */ + toolUseId: string; + + /** + * The result of the tool execution. + * Can be any JSON-serializable object. + * May include error information if the tool failed. + */ + content: object; + + /** + * If true, indicates the tool execution failed. + * The content should contain error details. + * Default: false + */ + isError?: boolean; + + /** + * Optional metadata + */ + _meta?: Record; +} +``` + +**Validation Rules:** +- `toolUseId` MUST reference a previous `ToolCallContent.id` in the conversation +- All `ToolCallContent` instances MUST have corresponding `ToolResultContent` responses +- `content` SHOULD validate against the tool's `outputSchema` if defined +- If `isError` is true, `content` SHOULD contain error explanation + +**Zod Schema:** + +```typescript +const ToolResultContentSchema = z.object({ + type: z.literal("tool_result"), + toolUseId: z.string(), + content: z.object({}).passthrough(), + isError: z.optional(z.boolean()), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); +``` + +--- + +### 2.4 Message Types + +#### SamplingMessage (Updated) + +```typescript +type SamplingMessage = UserMessage | AssistantMessage; + +interface UserMessage { + role: "user"; + content: + | TextContent + | ImageContent + | AudioContent + | ToolResultContent; // NEW + _meta?: Record; +} + +interface AssistantMessage { + role: "assistant"; + content: + | TextContent + | ImageContent + | AudioContent + | ToolCallContent; // NEW + _meta?: Record; +} +``` + +**Key Changes from Current Implementation:** + +1. **Split Message Types**: `SamplingMessage` is now a discriminated union of `UserMessage` and `AssistantMessage` + - Current: Single type with both roles + - New: Separate types with role-specific content + +2. **New Content Types**: + - `UserMessage` can contain `ToolResultContent` + - `AssistantMessage` can contain `ToolCallContent` + +3. **Content Structure**: + - Current: `content` is a single union type + - New: `content` is role-specific union type + +**Backwards Compatibility:** +- Existing messages without tool content remain valid +- Parsers MUST handle both old and new content types +- Servers MUST validate role-content compatibility + +**Zod Schemas:** + +```typescript +const UserMessageSchema = z.object({ + role: z.literal("user"), + content: z.union([ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolResultContentSchema, + ]), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +const AssistantMessageSchema = z.object({ + role: z.literal("assistant"), + content: z.union([ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +const SamplingMessageSchema = z.union([ + UserMessageSchema, + AssistantMessageSchema, +]); +``` + +--- + +### 2.5 Request and Result Updates + +#### CreateMessageRequest (Updated) + +```typescript +interface CreateMessageRequest { + method: "sampling/createMessage"; + params: { + messages: SamplingMessage[]; + + /** + * System prompt for the LLM. + * Client MAY modify or omit this. + */ + systemPrompt?: string; + + /** + * SOFT-DEPRECATED: Use tools parameter instead. + * Request to include context from MCP servers. + * Requires clientCapabilities.sampling.context. + */ + includeContext?: "none" | "thisServer" | "allServers"; + + /** + * Temperature for sampling (0.0 to 1.0+) + */ + temperature?: number; + + /** + * Maximum tokens to generate + */ + maxTokens: number; + + /** + * Stop sequences + */ + stopSequences?: string[]; + + /** + * Provider-specific metadata + */ + metadata?: object; + + /** + * Model selection preferences + */ + modelPreferences?: ModelPreferences; + + /** + * NEW: Tool definitions for the LLM to use. + * Requires clientCapabilities.sampling.tools. + */ + tools?: Tool[]; + + /** + * NEW: Controls tool usage behavior. + * Requires clientCapabilities.sampling.tools. + */ + tool_choice?: ToolChoice; + + /** + * Request metadata + */ + _meta?: { + progressToken?: string | number; + }; + }; +} +``` + +**Parameter Requirements:** + +| Parameter | Required | Capability Required | Notes | +|-----------|----------|-------------------|-------| +| `messages` | Yes | None | Must be non-empty | +| `maxTokens` | Yes | None | Must be positive integer | +| `systemPrompt` | No | None | Client may override | +| `temperature` | No | None | Typically 0.0-1.0 | +| `stopSequences` | No | None | | +| `metadata` | No | None | Provider-specific | +| `modelPreferences` | No | None | | +| `includeContext` | No | `sampling.context` | Soft-deprecated | +| `tools` | No | `sampling.tools` | New in SEP-1577 | +| `tool_choice` | No | `sampling.tools` | Requires `tools` | + +#### CreateMessageResult (Updated) + +```typescript +interface CreateMessageResult { + /** + * The model that generated the response + */ + model: string; + + /** + * Why sampling stopped. + * NEW VALUES: "toolUse", "refusal", "other" + */ + stopReason?: + | "endTurn" // Model completed naturally + | "stopSequence" // Hit a stop sequence + | "maxTokens" // Reached token limit (RENAMED from "maxToken") + | "toolUse" // NEW: Model wants to use a tool + | "refusal" // NEW: Model refused the request + | "other" // NEW: Other provider-specific reason + | string; // Allow extension + + /** + * Role is always "assistant" in responses + */ + role: "assistant"; + + /** + * Response content. + * May be ToolCallContent if stopReason is "toolUse" + */ + content: + | TextContent + | ImageContent + | AudioContent + | ToolCallContent; // NEW + + /** + * Result metadata + */ + _meta?: Record; +} +``` + +**Stop Reason Semantics:** + +| Stop Reason | Meaning | Expected Content Type | Server Action | +|-------------|---------|---------------------|---------------| +| `endTurn` | Natural completion | Text/Image/Audio | Conversation complete | +| `stopSequence` | Hit stop sequence | Text | Conversation may continue | +| `maxTokens` | Token limit reached | Text/Image/Audio | May be incomplete | +| `toolUse` | Tool call requested | ToolCallContent | Server MUST execute tool | +| `refusal` | Request refused | Text (explanation) | Handle refusal | +| `other` | Provider-specific | Any | Check provider docs | + +**Key Changes:** + +1. `stopReason` expanded with 3 new values +2. `maxToken` renamed to `maxTokens` (note the 's') +3. `content` can now be `ToolCallContent` +4. `role` is fixed as `"assistant"` (no longer enum with both) + +--- + +## 3. Protocol Requirements + +### 3.1 Server Requirements + +#### Capability Validation + +Servers MUST validate capabilities before using features: + +```typescript +// Pseudocode +function validateCreateMessageRequest( + request: CreateMessageRequest, + clientCapabilities: ClientCapabilities +): void { + // Check context capability + if (request.params.includeContext && + request.params.includeContext !== "none") { + if (!clientCapabilities.sampling?.context) { + throw new McpError( + ErrorCode.InvalidRequest, + `Client does not support includeContext parameter. ` + + `Client must advertise sampling.context capability.` + ); + } + } + + // Check tools capability + if (request.params.tools || request.params.tool_choice) { + if (!clientCapabilities.sampling?.tools) { + throw new McpError( + ErrorCode.InvalidRequest, + `Client does not support tools parameter. ` + + `Client must advertise sampling.tools capability.` + ); + } + } + + // tool_choice requires tools + if (request.params.tool_choice && !request.params.tools) { + throw new McpError( + ErrorCode.InvalidParams, + `tool_choice requires tools parameter to be set` + ); + } +} +``` + +#### Message Balancing + +Servers MUST ensure tool calls and results are balanced: + +```typescript +// Pseudocode validation +function validateMessageBalance(messages: SamplingMessage[]): void { + const toolCallIds = new Set(); + const toolResultIds = new Set(); + + for (const message of messages) { + if (message.content.type === "tool_use") { + if (toolCallIds.has(message.content.id)) { + throw new Error(`Duplicate tool call ID: ${message.content.id}`); + } + toolCallIds.add(message.content.id); + } + + if (message.content.type === "tool_result") { + toolResultIds.add(message.content.toolUseId); + } + } + + // Every tool call must have a result + for (const callId of toolCallIds) { + if (!toolResultIds.has(callId)) { + throw new Error(`Tool call ${callId} has no corresponding result`); + } + } + + // Every result must reference a valid call + for (const resultId of toolResultIds) { + if (!toolCallIds.has(resultId)) { + throw new Error(`Tool result references unknown call: ${resultId}`); + } + } +} +``` + +#### Tool Execution Loop + +Servers implementing agentic loops SHOULD: + +```typescript +async function agenticLoop( + client: McpClient, + initialMessages: SamplingMessage[], + tools: Tool[] +): Promise { + let messages = [...initialMessages]; + + while (true) { + // Request completion from LLM + const result = await client.request(CreateMessageRequestSchema, { + method: "sampling/createMessage", + params: { + messages, + tools, + maxTokens: 4096, + } + }, CreateMessageResultSchema); + + // Check if tool use is required + if (result.stopReason === "toolUse" && + result.content.type === "tool_use") { + + // Add assistant message with tool call + messages.push({ + role: "assistant", + content: result.content + }); + + // Execute tool locally + const toolResult = await executeToolLocally( + result.content.name, + result.content.input + ); + + // Add user message with tool result + messages.push({ + role: "user", + content: { + type: "tool_result", + toolUseId: result.content.id, + content: toolResult, + isError: toolResult.error ? true : undefined + } + }); + + // Continue loop + continue; + } + + // Completion - return final result + return result; + } +} +``` + +### 3.2 Client Requirements + +#### Capability Advertisement + +Clients MUST advertise capabilities accurately: + +```typescript +const clientCapabilities: ClientCapabilities = { + sampling: { + // Advertise context support if implemented + context: supportContextInclusion ? {} : undefined, + + // Advertise tools support if implemented + tools: supportToolCalling ? {} : undefined, + }, + // ... other capabilities +}; +``` + +#### Tool Execution + +Clients MUST: + +1. Validate tool definitions in requests +2. Provide tools to LLM in provider-specific format +3. Handle tool calls in LLM responses +4. Return results with correct `stopReason` + +Clients MAY: + +1. Filter or modify tool definitions for safety +2. Request user approval before tool use +3. Implement tool execution client-side +4. Convert between provider-specific tool formats + +#### Error Handling + +Clients MUST return appropriate errors: + +| Condition | Error Code | Message | +|-----------|-----------|---------| +| Unsupported capability used | `InvalidRequest` | "Client does not support [feature]" | +| Invalid tool definition | `InvalidParams` | "Invalid tool schema: [details]" | +| Tool execution failed | N/A | Return success with `isError: true` | +| Request refused by LLM | N/A | Return success with `stopReason: "refusal"` | + +--- + +## 4. Backwards Compatibility + +### 4.1 Compatibility Strategy + +**Soft Deprecation:** +- `includeContext` is marked soft-deprecated but remains functional +- Implementations SHOULD prefer `tools` over `includeContext` +- Both MAY coexist in transition period +- `includeContext` MAY be removed in future spec version + +**Version Detection:** + +```typescript +function supportsToolCalling(capabilities: ClientCapabilities): boolean { + return capabilities.sampling?.tools !== undefined; +} + +function supportsContextInclusion(capabilities: ClientCapabilities): boolean { + return capabilities.sampling?.context !== undefined; +} +``` + +### 4.2 Migration Path + +**For Server Implementations:** + +1. Check client capabilities in negotiation +2. Prefer `tools` parameter if available +3. Fall back to `includeContext` for older clients +4. Validate capabilities before sending requests + +**For Client Implementations:** + +1. Add `sampling.tools` capability when ready +2. Continue supporting `sampling.context` for existing servers +3. Implement tool calling according to provider's API +4. Update to handle new content types and stop reasons + +### 4.3 Breaking Changes + +**Type Changes:** + +| Old Type | New Type | Breaking? | Migration | +|----------|----------|-----------|-----------| +| `SamplingMessage` | Split into `UserMessage` / `AssistantMessage` | Yes | Use discriminated union | +| `stopReason: "maxToken"` | `stopReason: "maxTokens"` | Yes | Support both for transition | +| Content types | Added `ToolCallContent`, `ToolResultContent` | Additive | Extend parsers | + +**Validation Changes:** + +- Parsers MUST handle new content types +- Message role validation is now stricter (role-specific content) +- Tool call/result balancing is required when tools used + +--- + +## 5. Implementation Checklist + +### 5.1 TypeScript SDK Changes Required + +#### src/types.ts + +- [ ] Add `ToolCallContentSchema` and `ToolCallContent` type +- [ ] Add `ToolResultContentSchema` and `ToolResultContent` type +- [ ] Split `SamplingMessageSchema` into `UserMessageSchema` and `AssistantMessageSchema` +- [ ] Add `ToolChoiceSchema` and `ToolChoice` type +- [ ] Update `CreateMessageRequestSchema` to include `tools` and `tool_choice` +- [ ] Update `CreateMessageResultSchema`: + - [ ] Add new stop reason values: `"toolUse"`, `"refusal"`, `"other"` + - [ ] Rename `"maxToken"` to `"maxTokens"` (keep both for transition) + - [ ] Update content type to include `ToolCallContent` + - [ ] Fix role to be `"assistant"` only +- [ ] Update `ClientCapabilitiesSchema` to include `sampling.context` and `sampling.tools` +- [ ] Add validation helpers for message balancing +- [ ] Export all new types and schemas + +#### src/client/index.ts + +- [ ] Add capability advertisement for `sampling.tools` +- [ ] Add request validation for tool capabilities +- [ ] Add helper methods for tool calling workflow +- [ ] Update example code / documentation comments + +#### src/server/index.ts + +- [ ] Add validation for client capabilities before using tools +- [ ] Add helper for building tool-enabled sampling requests +- [ ] Add validation for message balance +- [ ] Add error handling for unsupported capabilities + +### 5.2 Test Requirements + +#### Unit Tests + +- [ ] Test `ToolCallContent` schema validation +- [ ] Test `ToolResultContent` schema validation +- [ ] Test `UserMessage` and `AssistantMessage` schemas +- [ ] Test `ToolChoice` schema validation +- [ ] Test updated `CreateMessageRequest` schema +- [ ] Test updated `CreateMessageResult` schema +- [ ] Test capability negotiation logic +- [ ] Test message balance validation +- [ ] Test error conditions: + - [ ] Using tools without capability + - [ ] Using context without capability + - [ ] Unbalanced tool calls/results + - [ ] Invalid tool choice with no tools + +#### Integration Tests + +- [ ] Test full agentic loop with tool calling +- [ ] Test client-server capability negotiation +- [ ] Test backwards compatibility with old clients +- [ ] Test error propagation +- [ ] Test tool execution with various content types +- [ ] Test multi-turn conversations with tools + +### 5.3 Documentation Requirements + +- [ ] Update API documentation for new types +- [ ] Add migration guide for existing implementations +- [ ] Add examples of agentic workflows +- [ ] Document capability negotiation +- [ ] Add troubleshooting guide for common errors +- [ ] Update changelog with breaking changes + +--- + +## 6. Test Scenarios + +### 6.1 Basic Tool Calling + +**Scenario:** Client supports tools, server uses single tool + +```typescript +// Client advertises capability +const clientCaps: ClientCapabilities = { + sampling: { tools: {} } +}; + +// Server sends request +const request: CreateMessageRequest = { + method: "sampling/createMessage", + params: { + messages: [ + { + role: "user", + content: { type: "text", text: "What's the weather in SF?" } + } + ], + tools: [ + { + name: "get_weather", + description: "Get weather for a location", + inputSchema: { + type: "object", + properties: { + location: { type: "string" } + }, + required: ["location"] + } + } + ], + maxTokens: 1000 + } +}; + +// Expected response +const response: CreateMessageResult = { + model: "claude-3-5-sonnet-20241022", + role: "assistant", + stopReason: "toolUse", + content: { + type: "tool_use", + id: "tool_1", + name: "get_weather", + input: { location: "San Francisco, CA" } + } +}; +``` + +### 6.2 Tool Result Submission + +**Scenario:** Server provides tool result in follow-up + +```typescript +// Server adds tool result +const followUp: CreateMessageRequest = { + method: "sampling/createMessage", + params: { + messages: [ + { + role: "user", + content: { type: "text", text: "What's the weather in SF?" } + }, + { + role: "assistant", + content: { + type: "tool_use", + id: "tool_1", + name: "get_weather", + input: { location: "San Francisco, CA" } + } + }, + { + role: "user", + content: { + type: "tool_result", + toolUseId: "tool_1", + content: { + temperature: 65, + condition: "Partly cloudy", + humidity: 70 + } + } + } + ], + tools: [/* same tools */], + maxTokens: 1000 + } +}; + +// Expected final response +const finalResponse: CreateMessageResult = { + model: "claude-3-5-sonnet-20241022", + role: "assistant", + stopReason: "endTurn", + content: { + type: "text", + text: "The weather in San Francisco is currently 65°F and partly cloudy with 70% humidity." + } +}; +``` + +### 6.3 Tool Error Handling + +**Scenario:** Tool execution fails + +```typescript +const errorResult: CreateMessageRequest = { + method: "sampling/createMessage", + params: { + messages: [ + /* ... previous messages ... */ + { + role: "user", + content: { + type: "tool_result", + toolUseId: "tool_1", + content: { + error: "API_ERROR", + message: "Weather service unavailable" + }, + isError: true + } + } + ], + tools: [/* same tools */], + maxTokens: 1000 + } +}; + +// LLM should handle error gracefully +const errorResponse: CreateMessageResult = { + model: "claude-3-5-sonnet-20241022", + role: "assistant", + stopReason: "endTurn", + content: { + type: "text", + text: "I apologize, but I'm unable to fetch the weather data right now due to a service issue. Please try again later." + } +}; +``` + +### 6.4 Capability Rejection + +**Scenario:** Client doesn't support tools + +```typescript +// Client without tools capability +const limitedClientCaps: ClientCapabilities = { + sampling: {} // No tools property +}; + +// Server attempts to use tools +const request: CreateMessageRequest = { + method: "sampling/createMessage", + params: { + messages: [/* ... */], + tools: [/* ... */], // ERROR: Not supported + maxTokens: 1000 + } +}; + +// Expected error response +// Client should return JSON-RPC error: +{ + jsonrpc: "2.0", + id: 1, + error: { + code: ErrorCode.InvalidRequest, + message: "Client does not support tools parameter. Client must advertise sampling.tools capability." + } +} +``` + +### 6.5 Parallel Tool Use + +**Scenario:** Model uses multiple tools in one response (if supported) + +```typescript +// Note: Not all models/providers support parallel tool use +// When supported, response might contain multiple tool calls + +const parallelRequest: CreateMessageRequest = { + method: "sampling/createMessage", + params: { + messages: [ + { + role: "user", + content: { type: "text", text: "Compare weather in SF and NYC" } + } + ], + tools: [ + { + name: "get_weather", + description: "Get weather for a location", + inputSchema: { + type: "object", + properties: { + location: { type: "string" } + }, + required: ["location"] + } + } + ], + tool_choice: { + mode: "auto", + disable_parallel_tool_use: false // Allow parallel use + }, + maxTokens: 1000 + } +}; + +// IMPLEMENTATION NOTE: +// Current spec shows single content per message. +// Parallel tool use may require: +// 1. Multiple assistant messages, OR +// 2. Array of content blocks (not in current spec), OR +// 3. Sequential tool calls in separate turns +// Clarification needed in specification. +``` + +### 6.6 Backwards Compatibility + +**Scenario:** Old client without sampling capabilities + +```typescript +// Legacy client +const legacyClientCaps: ClientCapabilities = { + // No sampling property at all +}; + +// Server checks capabilities +if (!clientCaps.sampling?.tools) { + // Fall back to alternative implementation + // or return error if tools are required + throw new Error("This server requires tool support"); +} +``` + +--- + +## 7. Open Questions and Ambiguities + +### 7.1 Specification Gaps + +1. **Parallel Tool Use Implementation:** + - How should multiple tool calls be represented in a single response? + - Should `content` be an array of content blocks? + - Or should each tool call be a separate assistant message? + +2. **Tool Result Content Schema:** + - Should `content` validate against `outputSchema`? + - How should validation errors be reported? + - What's the expected format for error content? + +3. **Message Ordering:** + - Must tool results immediately follow tool calls? + - Can multiple tool calls be batched before results? + - Can user messages be interleaved? + +4. **Context + Tools Interaction:** + - How do `includeContext` and `tools` interact when both present? + - Should tools override context, or vice versa? + - Migration strategy unclear + +5. **Stop Reason Semantics:** + - What's the difference between `"refusal"` and returning text explaining refusal? + - When should `"other"` be used vs extending the enum? + - Should `"toolUse"` be used even if `tool_choice.mode` was `"required"`? + +### 7.2 Implementation Questions + +1. **Tool Validation:** + - Should SDK validate tool schemas before sending? + - Should SDK validate tool inputs against schemas? + - Who validates outputSchema compliance? + +2. **Error Handling:** + - Should tool execution errors be MCP errors or tool results with `isError: true`? + - What error codes should be used for tool-related failures? + - How should schema validation failures be reported? + +3. **Type Safety:** + - How to enforce role-content compatibility at compile time? + - Should content be a discriminated union per role? + - How to prevent `ToolCallContent` in `UserMessage`? + +4. **Testing:** + - How to test multi-model compatibility? + - Should SDK include mock LLM for testing? + - How to validate different provider tool formats? + +### 7.3 Recommendations for Clarification + +1. **Add explicit examples** for: + - Parallel tool use (if supported) + - Error handling patterns + - Multi-turn conversations with mixed content + +2. **Clarify validation requirements:** + - Who validates what and when + - Expected error responses for each validation failure + - Schema compliance requirements + +3. **Define message sequencing rules:** + - Allowed message patterns + - Prohibited patterns + - Ordering requirements + +4. **Document provider-specific behavior:** + - How to handle provider-specific tool formats + - Dealing with capability mismatches + - Fallback strategies + +--- + +## 8. Related Resources + +### Primary Documents + +- **SEP-1577 Issue:** https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577 +- **MCP Specification:** https://spec.modelcontextprotocol.io/ +- **TypeScript SDK:** https://github.com/modelcontextprotocol/typescript-sdk + +### Related SEPs + +- **SEP-973:** Icons and metadata support (merged) +- **SEP-835:** Authorization scope management +- **SEP-1299:** Server-side authorization management +- **SEP-1502:** MCP extension specification + +### Related Discussions + +- **Discussion #124:** Improve sampling in the protocol + - https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/124 +- **Issue #503:** Reframe sampling for agent-to-agent communication + - https://github.com/modelcontextprotocol/modelcontextprotocol/issues/503 +- **Discussion #314:** Task semantics and multi-turn interactions + - https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/314 +- **Discussion #315:** Suggested response format proposal + - https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/315 + +### External References + +- **Anthropic Claude API:** Tool use documentation +- **OpenAI API:** Function calling documentation +- **JSON Schema:** https://json-schema.org/ +- **RFC 9110:** HTTP Semantics + +--- + +## 9. Implementation Timeline + +### Phase 1: Type Definitions (Week 1) +- Add new content type schemas +- Update message type schemas +- Update request/result schemas +- Add capability schemas +- Write unit tests for schemas + +### Phase 2: Validation (Week 1-2) +- Implement capability checking +- Implement message balance validation +- Implement error handling +- Write validation tests + +### Phase 3: Client/Server Integration (Week 2) +- Update client implementation +- Update server implementation +- Add helper methods +- Write integration tests + +### Phase 4: Documentation and Examples (Week 2-3) +- Update API documentation +- Write migration guide +- Create example implementations +- Write user guides + +### Phase 5: Review and Polish (Week 3) +- Code review +- Documentation review +- Performance testing +- Bug fixes + +--- + +## 10. Security Considerations + +### 10.1 Tool Definition Validation + +Servers SHOULD validate tool definitions from untrusted sources: +- Validate schemas are well-formed JSON Schema +- Limit tool definition size +- Sanitize tool names and descriptions +- Prevent schema injection attacks + +### 10.2 Tool Execution Safety + +Clients MUST implement safety measures: +- Validate tool inputs before execution +- Sandbox tool execution when possible +- Request user approval for sensitive operations +- Log all tool executions +- Implement rate limiting + +### 10.3 Content Validation + +Both sides SHOULD validate content: +- Check content size limits +- Validate base64 encoding for binary data +- Sanitize text content for display +- Validate JSON structure +- Prevent injection attacks + +### 10.4 Capability-Based Security + +Implementations MUST: +- Enforce capability checks strictly +- Reject requests using unsupported features +- Never assume capabilities without negotiation +- Log capability violations + +--- + +## Appendix A: Current vs New Type Comparison + +### A.1 SamplingMessage + +**Current (pre-SEP-1577):** +```typescript +interface SamplingMessage { + role: "user" | "assistant"; + content: TextContent | ImageContent | AudioContent; +} +``` + +**New (SEP-1577):** +```typescript +type SamplingMessage = UserMessage | AssistantMessage; + +interface UserMessage { + role: "user"; + content: TextContent | ImageContent | AudioContent | ToolResultContent; +} + +interface AssistantMessage { + role: "assistant"; + content: TextContent | ImageContent | AudioContent | ToolCallContent; +} +``` + +### A.2 CreateMessageResult + +**Current (pre-SEP-1577):** +```typescript +interface CreateMessageResult { + model: string; + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; + role: "user" | "assistant"; + content: TextContent | ImageContent | AudioContent; +} +``` + +**New (SEP-1577):** +```typescript +interface CreateMessageResult { + model: string; + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | "refusal" | "other" | string; + role: "assistant"; // Fixed, not union + content: TextContent | ImageContent | AudioContent | ToolCallContent; +} +``` + +### A.3 ClientCapabilities + +**Current (pre-SEP-1577):** +```typescript +interface ClientCapabilities { + sampling?: object; + // ... other capabilities +} +``` + +**New (SEP-1577):** +```typescript +interface ClientCapabilities { + sampling?: { + context?: object; + tools?: object; + }; + // ... other capabilities +} +``` + +--- + +## Appendix B: Complete Zod Schema Definitions + +```typescript +// Content types +export const ToolCallContentSchema = z.object({ + type: z.literal("tool_use"), + name: z.string(), + id: z.string(), + input: z.object({}).passthrough(), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +export const ToolResultContentSchema = z.object({ + type: z.literal("tool_result"), + toolUseId: z.string(), + content: z.object({}).passthrough(), + isError: z.optional(z.boolean()), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +// Message types +export const UserMessageSchema = z.object({ + role: z.literal("user"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolResultContentSchema, + ]), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +export const AssistantMessageSchema = z.object({ + role: z.literal("assistant"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), + _meta: z.optional(z.object({}).passthrough()), +}).passthrough(); + +export const SamplingMessageSchema = z.discriminatedUnion("role", [ + UserMessageSchema, + AssistantMessageSchema, +]); + +// Tool choice +export const ToolChoiceSchema = z.object({ + mode: z.optional(z.enum(["auto", "required"])), + disable_parallel_tool_use: z.optional(z.boolean()), +}).passthrough(); + +// Updated request +export const CreateMessageRequestSchema = RequestSchema.extend({ + method: z.literal("sampling/createMessage"), + params: BaseRequestParamsSchema.extend({ + messages: z.array(SamplingMessageSchema), + systemPrompt: z.optional(z.string()), + includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), + temperature: z.optional(z.number()), + maxTokens: z.number().int(), + stopSequences: z.optional(z.array(z.string())), + metadata: z.optional(z.object({}).passthrough()), + modelPreferences: z.optional(ModelPreferencesSchema), + tools: z.optional(z.array(ToolSchema)), + tool_choice: z.optional(ToolChoiceSchema), + }), +}); + +// Updated result +export const CreateMessageResultSchema = ResultSchema.extend({ + model: z.string(), + stopReason: z.optional( + z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]).or(z.string()) + ), + role: z.literal("assistant"), + content: z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), +}); + +// Updated capabilities +export const ClientCapabilitiesSchema = z.object({ + sampling: z.optional(z.object({ + context: z.optional(z.object({}).passthrough()), + tools: z.optional(z.object({}).passthrough()), + }).passthrough()), + // ... other capabilities +}).passthrough(); +``` + +--- + +## Appendix C: TypeScript Type Definitions + +```typescript +// Inferred types +export type ToolCallContent = z.infer; +export type ToolResultContent = z.infer; +export type UserMessage = z.infer; +export type AssistantMessage = z.infer; +export type SamplingMessage = z.infer; +export type ToolChoice = z.infer; +export type CreateMessageRequest = z.infer; +export type CreateMessageResult = z.infer; +export type ClientCapabilities = z.infer; +``` + +--- + +## Document Revision History + +| Version | Date | Author | Changes | +|---------|------|--------|---------| +| 1.0 | 2025-10-01 | Research Agent | Initial comprehensive analysis | + +--- + +**END OF DOCUMENT** diff --git a/intermediate-findings/test-analysis.md b/intermediate-findings/test-analysis.md new file mode 100644 index 000000000..cbe7aa148 --- /dev/null +++ b/intermediate-findings/test-analysis.md @@ -0,0 +1,664 @@ +# MCP TypeScript SDK Test Suite Analysis + +## Executive Summary + +This document provides a comprehensive analysis of the existing test suite in the MCP TypeScript SDK, examining testing patterns, coverage, and identifying gaps relevant to transport validation work. + +## Test File Organization + +### Test Files Discovered (35 total) + +**Client Tests:** +- `/src/client/auth.test.ts` - OAuth client authentication +- `/src/client/cross-spawn.test.ts` - Process spawning +- `/src/client/index.test.ts` - Main client functionality +- `/src/client/middleware.test.ts` - Fetch middleware (OAuth, logging) +- `/src/client/sse.test.ts` - SSE client transport +- `/src/client/stdio.test.ts` - Stdio client transport +- `/src/client/streamableHttp.test.ts` - StreamableHTTP client transport + +**Server Tests:** +- `/src/server/index.test.ts` - Main server functionality +- `/src/server/mcp.test.ts` - MCP server +- `/src/server/sse.test.ts` - SSE server transport +- `/src/server/stdio.test.ts` - Stdio server transport +- `/src/server/streamableHttp.test.ts` - StreamableHTTP server transport +- `/src/server/completable.test.ts` - Completable behavior +- `/src/server/title.test.ts` - Title handling +- `/src/server/auth/` - Multiple auth-related tests (7 files) + +**Shared/Protocol Tests:** +- `/src/shared/protocol-transport-handling.test.ts` - Protocol transport bug fixes +- `/src/shared/protocol.test.ts` - Core protocol tests +- `/src/shared/stdio.test.ts` - Shared stdio utilities +- `/src/shared/auth-utils.test.ts` - Auth utilities +- `/src/shared/auth.test.ts` - Auth functionality +- `/src/shared/uriTemplate.test.ts` - URI templates + +**Type/Integration Tests:** +- `/src/inMemory.test.ts` - In-memory transport +- `/src/types.test.ts` - Type validation +- `/src/spec.types.test.ts` - Spec type compatibility +- `/src/integration-tests/` - Integration tests (3 files) + +## Test Patterns and Structure + +### 1. Testing Framework & Tools + +**Jest Configuration:** +- Uses Jest as the primary test runner +- `@jest/globals` for describe/test/expect/beforeEach/afterEach +- Mock implementations with `jest.fn()` and `jest.spyOn()` +- Timer mocking with `jest.useFakeTimers()` for timeout testing + +**Common Patterns:** +```typescript +describe("Component name", () => { + let variable: Type; + + beforeEach(() => { + // Setup + }); + + test("should do something specific", async () => { + // Arrange - Act - Assert + }); +}); +``` + +### 2. Mock Transport Pattern + +**Consistent Mock Transport Implementation:** +```typescript +class MockTransport implements Transport { + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: unknown) => void; + + async start(): Promise {} + async close(): Promise { this.onclose?.(); } + async send(_message: unknown): Promise {} +} +``` + +**Used extensively in:** +- `/src/shared/protocol.test.ts` - Basic mock transport +- `/src/shared/protocol-transport-handling.test.ts` - Enhanced with ID tracking +- Client/server tests - For isolating protocol logic from transport details + +### 3. In-Memory Transport for Integration + +**InMemoryTransport Pattern:** +- Used for testing full client-server interactions +- Creates linked pairs for bidirectional communication +- Examples in `/src/inMemory.test.ts` and `/src/client/index.test.ts` + +```typescript +const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); +await client.connect(clientTransport); +await server.connect(serverTransport); +``` + +### 4. HTTP Server Mocking for Network Transports + +**Pattern for SSE/HTTP tests:** +- Creates actual Node.js HTTP servers on random ports +- Uses AddressInfo to get assigned port +- Simulates real HTTP interactions + +```typescript +const server = createServer((req, res) => { + // Handle requests +}); + +server.listen(0, "127.0.0.1", () => { + const addr = server.address() as AddressInfo; + const baseUrl = new URL(`http://127.0.0.1:${addr.port}`); + // Run tests +}); +``` + +## Transport Layer Coverage + +### 1. Stdio Transport Tests + +**Client-side (`/src/client/stdio.test.ts`):** +- ✅ Basic lifecycle (start, close) +- ✅ Message reading (JSON-RPC over newline-delimited) +- ✅ Process management (child process PID tracking) +- ✅ Cross-platform testing (Windows vs Unix commands) + +**Server-side (`/src/server/stdio.test.ts`):** +- ✅ Start/close lifecycle +- ✅ Message buffering (doesn't read until started) +- ✅ Multiple message handling +- ✅ Stream handling (Readable/Writable) + +**Shared (`/src/shared/stdio.test.ts`):** +- ✅ ReadBuffer implementation +- ✅ Newline-delimited message parsing +- ✅ Buffer clearing and reuse + +**Coverage: HIGH** - Well-tested for message framing and process management + +### 2. SSE Transport Tests + +**Client-side (`/src/client/sse.test.ts` - 1450 lines):** +- ✅ Connection establishment and endpoint discovery +- ✅ Message sending/receiving (GET for events, POST for messages) +- ✅ Error handling (malformed JSON, server errors) +- ✅ HTTP status code handling (401, 403, 500) +- ✅ Custom headers and fetch implementation +- ✅ **Extensive OAuth authentication flow testing:** + - Token attachment to requests + - 401 retry with token refresh + - Authorization code flow + - Error handling (InvalidClientError, InvalidGrantError, etc.) + - Custom fetch middleware integration +- ✅ DNS rebinding protection validation + +**Server-side (`/src/server/sse.test.ts` - 717 lines):** +- ✅ Session ID management and endpoint generation +- ✅ Query parameter handling (existing params, hash fragments) +- ✅ POST message validation (content-type, JSON schema) +- ✅ Request info propagation to handlers +- ✅ **DNS rebinding protection:** + - Host header validation + - Origin header validation + - Content-Type validation + - Combined validation scenarios + +**Coverage: VERY HIGH** - Comprehensive testing including security features + +### 3. StreamableHTTP Transport Tests + +**Found in:** +- `/src/client/streamableHttp.test.ts` +- `/src/server/streamableHttp.test.ts` + +**Note:** Not read in detail during this analysis, but appears to follow similar patterns to SSE tests + +### 4. In-Memory Transport Tests + +**Location:** `/src/inMemory.test.ts` + +**Coverage:** +- ✅ Linked pair creation +- ✅ Bidirectional message sending +- ✅ Auth info propagation +- ✅ Connection lifecycle (start, close) +- ✅ Error handling (send after close) +- ✅ Message queueing (before start) + +**Coverage: GOOD** - Covers basic functionality for testing purposes + +## Protocol Layer Tests + +### 1. Core Protocol Tests (`/src/shared/protocol.test.ts` - 741 lines) + +**Message Handling:** +- ✅ Request timeouts +- ✅ Connection close handling +- ✅ Hook preservation (onclose, onerror, onmessage) + +**Progress Notifications:** +- ✅ _meta preservation when adding progressToken +- ✅ Progress notification handling with timeout reset +- ✅ maxTotalTimeout enforcement +- ✅ Multiple progress updates +- ✅ Progress with message field + +**Debounced Notifications:** +- ✅ Notification debouncing (params-based conditions) +- ✅ Non-debounced notifications (with relatedRequestId) +- ✅ Clearing pending on close +- ✅ Multiple synchronous calls +- ✅ Sequential batches + +**Capabilities Merging:** +- ✅ Client capability merging +- ✅ Server capability merging +- ✅ Value overriding +- ✅ Empty object handling + +**Coverage: HIGH** - Comprehensive protocol behavior testing + +### 2. Transport Handling Bug Tests (`/src/shared/protocol-transport-handling.test.ts`) + +**Specific Bug Scenario:** +- ✅ Multiple client connections with proper response routing +- ✅ Timing issues with rapid connections +- ✅ Transport reference management + +**Context:** This file tests a specific bug where responses were sent to wrong transports when multiple clients connected + +**Coverage: TARGETED** - Focuses on specific multi-client scenario + +## Client/Server Integration Tests + +### 1. Client Tests (`/src/client/index.test.ts` - 1304 lines) + +**Protocol Negotiation:** +- ✅ Latest protocol version acceptance +- ✅ Older supported version acceptance +- ✅ Unsupported version rejection +- ✅ Version negotiation (old client, new server) + +**Capabilities:** +- ✅ Server capability respect (resources, tools, prompts, logging) +- ✅ Client notification capability validation +- ✅ Request handler capability validation +- ✅ Strict capability enforcement + +**Request Management:** +- ✅ Request cancellation (AbortController) +- ✅ Request timeout handling +- ✅ Custom request/notification schemas (type checking) + +**Output Schema Validation:** +- ✅ Tool output schema validation +- ✅ Complex JSON schema validation +- ✅ Additional properties validation +- ✅ Missing structuredContent detection + +**Coverage: VERY HIGH** - Comprehensive client behavior + +### 2. Server Tests (`/src/server/index.test.ts` - 1016 lines) + +**Protocol Support:** +- ✅ Latest protocol version handling +- ✅ Older version support +- ✅ Unsupported version handling (auto-negotiation) + +**Capabilities:** +- ✅ Client capability respect (sampling, elicitation) +- ✅ Server notification capability validation +- ✅ Request handler capability validation + +**Elicitation Feature:** +- ✅ Schema validation for accept action +- ✅ Invalid data rejection +- ✅ Decline/cancel without validation + +**Logging:** +- ✅ Log level filtering per transport (with/without sessionId) + +**Coverage: VERY HIGH** - Comprehensive server behavior + +### 3. Middleware Tests (`/src/client/middleware.test.ts` - 1214 lines) + +**OAuth Middleware:** +- ✅ Authorization header injection +- ✅ Token retrieval and usage +- ✅ 401 retry with auth flow +- ✅ Persistent 401 handling +- ✅ Request preservation (method, body, headers) +- ✅ Non-401 error pass-through +- ✅ URL object handling + +**Logging Middleware:** +- ✅ Default logger (console) +- ✅ Custom logger support +- ✅ Request/response header inclusion +- ✅ Status level filtering +- ✅ Duration measurement +- ✅ Network error logging + +**Middleware Composition:** +- ✅ Single middleware +- ✅ Multiple middleware in order +- ✅ Error propagation through middleware +- ✅ Real-world transport patterns (SSE, StreamableHTTP) + +**CreateMiddleware Helper:** +- ✅ Cleaner syntax for middleware creation +- ✅ Conditional logic support +- ✅ Short-circuit responses +- ✅ Response transformation +- ✅ Error handling and retry + +**Coverage: VERY HIGH** - Comprehensive middleware testing + +## Type and Schema Tests + +### 1. Types Test (`/src/types.test.ts`) + +**Basic Types:** +- ✅ Protocol version constants +- ✅ ResourceLink validation +- ✅ ContentBlock types (text, image, audio, resource_link, embedded resource) + +**Message Types:** +- ✅ PromptMessage with ContentBlock +- ✅ CallToolResult with ContentBlock arrays + +**Completion:** +- ✅ CompleteRequest without context +- ✅ CompleteRequest with resolved arguments +- ✅ Multiple resolved variables + +**Coverage: GOOD** - Schema validation coverage + +### 2. Spec Types Test (`/src/spec.types.test.ts` - 725 lines) + +**Type Compatibility:** +- ✅ Static type checks for SDK vs Spec types +- ✅ Runtime verification of type coverage +- ✅ 94 type compatibility checks +- ✅ Missing SDK types tracking + +**Pattern:** +```typescript +const sdkTypeChecks = { + TypeName: (sdk: SDKType, spec: SpecType) => { + sdk = spec; // Mutual assignability + spec = sdk; + }, +}; +``` + +**Coverage: COMPREHENSIVE** - Ensures SDK types match spec + +## Edge Cases and Error Handling + +### Well-Covered Edge Cases: + +1. **Timeout Scenarios:** + - Request timeouts with immediate (0ms) expiry + - Progress notification timeout reset + - Maximum total timeout enforcement + - Timeout cancellation on abort + +2. **Multi-Client Scenarios:** + - Multiple clients connecting to same server + - Transport reference management + - Response routing to correct client + +3. **Authentication:** + - Token expiry and refresh + - Invalid client/grant errors + - Authorization redirect flows + - Persistent 401 after auth + +4. **Message Framing:** + - Newline-delimited message parsing + - Buffer management + - Malformed JSON handling + +5. **DNS Rebinding Protection:** + - Host header validation + - Origin header validation + - Content-Type validation + - Combined validation rules + +6. **Protocol Negotiation:** + - Version mismatch handling + - Old client, new server scenarios + - Unsupported version rejection + +## Testing Gaps and Opportunities + +### 1. Transport Validation (MISSING - Primary Gap) + +**No dedicated tests for:** +- Message structure validation at transport layer +- Invalid JSON-RPC format detection +- Required field validation (jsonrpc, method, id) +- Type validation (id must be string/number, method must be string) +- Extra field rejection in strict mode +- Batch message handling validation + +**Current Situation:** +- Validation happens implicitly through Zod schemas in Protocol +- No explicit transport-level validation tests +- Error handling tested but not validation edge cases + +### 2. Message Boundary Tests (LIMITED) + +**Could be improved:** +- Partial message handling in buffers +- Very large message handling +- Concurrent message sends +- Message interleaving scenarios + +### 3. Transport Error Recovery (PARTIAL) + +**Covered for:** +- Network errors +- Connection drops +- Auth failures + +**Not explicitly covered:** +- Partial write failures +- Buffer overflow scenarios +- Transport-specific error conditions + +### 4. Performance and Load Testing (ABSENT) + +**No tests for:** +- High message throughput +- Large payload handling +- Memory usage under load +- Connection pool management + +### 5. Security Testing (GOOD but could be enhanced) + +**Well covered:** +- OAuth flows +- DNS rebinding protection +- Header validation + +**Could add:** +- Message injection attacks +- Buffer overflow attempts +- Resource exhaustion scenarios + +## Test Patterns to Follow + +### 1. Mock Transport Pattern + +```typescript +class MockTransport implements Transport { + id: string; + sentMessages: JSONRPCMessage[] = []; + + constructor(id: string) { + this.id = id; + } + + async send(message: JSONRPCMessage): Promise { + this.sentMessages.push(message); + } + + // Simulate receiving a message + simulateMessage(message: unknown) { + this.onmessage?.(message); + } +} +``` + +**Use for:** Isolating protocol logic from transport implementation + +### 2. Integration Test Pattern + +```typescript +const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + +const client = new Client(clientInfo, capabilities); +const server = new Server(serverInfo, capabilities); + +await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport) +]); + +// Test full round-trip behavior +``` + +**Use for:** Testing complete protocol interactions + +### 3. HTTP Server Pattern (for network transports) + +```typescript +const server = createServer((req, res) => { + // Handle requests, simulate responses +}); + +await new Promise(resolve => { + server.listen(0, "127.0.0.1", () => { + const addr = server.address() as AddressInfo; + baseUrl = new URL(`http://127.0.0.1:${addr.port}`); + resolve(); + }); +}); + +// Run tests with real HTTP +await server.close(); +``` + +**Use for:** Testing SSE, StreamableHTTP transports + +### 4. Timer Testing Pattern + +```typescript +beforeEach(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.useRealTimers(); +}); + +test("should timeout", async () => { + const promise = someTimedOperation(); + jest.advanceTimersByTime(1001); + await expect(promise).rejects.toThrow("timeout"); +}); +``` + +**Use for:** Testing timeout behavior without waiting + +### 5. Spy Pattern for Callbacks + +```typescript +const mockCallback = jest.fn(); +transport.onmessage = mockCallback; + +// Trigger message +await transport.simulateMessage(message); + +expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ method: "test" }) +); +``` + +**Use for:** Verifying callback invocations + +## Testing Infrastructure + +### 1. Test Utilities + +**Location:** Embedded in test files, no centralized utility module found + +**Common utilities:** +- Mock transport creation +- Message builders +- Server creation helpers +- Flush microtasks helper + +**Opportunity:** Could centralize common test utilities + +### 2. Test Data Builders + +**Current approach:** Inline object creation + +```typescript +const testMessage: JSONRPCMessage = { + jsonrpc: "2.0", + id: 1, + method: "test" +}; +``` + +**Opportunity:** Could create test data builders for common scenarios + +### 3. Assertion Helpers + +**Current approach:** Direct Jest matchers + +**Opportunity:** Could create custom matchers for: +- Valid JSON-RPC structure +- Message format validation +- Transport state assertions + +## Recommendations for Transport Validation Testing + +### 1. Create Dedicated Transport Validator Tests + +**New file:** `/src/shared/transport-validator.test.ts` + +**Test cases to add:** +- Valid message acceptance +- Invalid JSON rejection +- Missing required fields +- Invalid field types +- Extra field handling (strict vs permissive) +- Batch message validation +- Edge cases (empty arrays, null values, etc.) + +### 2. Integration with Existing Tests + +**Enhance existing transport tests with validation:** +- Add invalid message test cases to stdio tests +- Add malformed message handling to SSE tests +- Test validation errors are properly propagated + +### 3. Error Message Quality + +**Test that validation errors provide:** +- Clear error messages +- Field-specific errors +- Helpful suggestions +- Proper error codes + +### 4. Performance Testing + +**Add basic performance tests:** +- Validation overhead measurement +- Large message handling +- Batch validation performance + +## Summary + +### Strengths of Current Test Suite: + +1. **Comprehensive Protocol Testing** - Well-covered protocol behavior, capabilities, negotiation +2. **Strong Transport Implementation Tests** - Good coverage of stdio, SSE with security features +3. **Excellent Integration Tests** - Full client-server scenarios well tested +4. **Type Safety** - Comprehensive type compatibility verification +5. **Authentication** - Extensive OAuth flow testing +6. **Edge Cases** - Good coverage of timeouts, multi-client scenarios, error handling + +### Primary Gaps: + +1. **Transport Validation** - No dedicated message validation tests at transport layer +2. **Message Boundary Handling** - Limited testing of partial messages, large payloads +3. **Performance** - No load or performance testing +4. **Centralized Test Utilities** - Opportunities for DRY improvements + +### Test Quality Indicators: + +- **Total test files:** 35+ +- **Largest test files:** + - client/sse.test.ts: 1450 lines + - client/middleware.test.ts: 1214 lines + - client/index.test.ts: 1304 lines + - server/index.test.ts: 1016 lines +- **Test framework:** Jest with comprehensive mocking +- **Pattern consistency:** HIGH - Consistent use of beforeEach, mock patterns +- **Documentation:** Some tests include helpful comments +- **Maintainability:** GOOD - Clear test structure, logical grouping + +### Overall Assessment: + +The MCP TypeScript SDK has a **strong, comprehensive test suite** with excellent coverage of protocol behavior, transport implementations, and integration scenarios. The primary gap is **transport-level message validation**, which is the focus of the current work. The existing test patterns provide excellent examples to follow when implementing validation tests. diff --git a/intermediate-findings/toolLoopSampling-review.md b/intermediate-findings/toolLoopSampling-review.md new file mode 100644 index 000000000..62f7a2f98 --- /dev/null +++ b/intermediate-findings/toolLoopSampling-review.md @@ -0,0 +1,542 @@ +# Tool Loop Sampling Review + +## Summary + +The `toolLoopSampling.ts` file implements a sophisticated MCP server that demonstrates a tool loop pattern using sampling. The server exposes a `fileSearch` tool that uses an LLM (via MCP sampling) with locally-defined `ripgrep` and `read` tools to intelligently search and read files in the current directory. + +### Architecture + +The implementation follows a proxy pattern where: +1. Client calls the `fileSearch` tool with a natural language query +2. The server runs a tool loop that: + - Sends the query to an LLM via `server.createMessage()` + - The LLM decides to use `ripgrep` or `read` tools (defined locally) + - The server executes these tools locally + - Results are fed back to the LLM + - Process repeats until the LLM provides a final answer +3. The final answer is returned to the client + +--- + +## What's Done Well + +### 1. **Clear Separation of Concerns** +- Path safety logic is isolated in `ensureSafePath()` +- Tool execution is separated from tool loop orchestration +- Local tool definitions are cleanly defined in `LOCAL_TOOLS` constant + +### 2. **Proper Error Handling** +- Path validation with security checks +- Graceful handling of ripgrep exit codes (0 and 1 are both success) +- Error handling in tool execution functions returns structured error objects +- Top-level try-catch in the `fileSearch` handler + +### 3. **Good Documentation** +- Comprehensive file-level comments explaining the purpose +- Clear usage instructions +- Function-level JSDoc comments + +### 4. **Security Considerations** +- Path canonicalization to prevent directory traversal attacks +- Working directory constraint enforcement + +### 5. **Tool Loop Pattern** +- Implements a proper agentic loop with iteration limits +- Correctly handles tool use responses +- Properly constructs message history + +--- + +## Issues Found + +### 1. **Critical: Incorrect Content Type Handling** ⚠️ + +**Location:** Lines 214-215 + +```typescript +if (response.stopReason === "toolUse" && response.content.type === "tool_use") { + const toolCall = response.content as ToolCallContent; +``` + +**Problem:** According to the MCP protocol types (lines 1361-1366 in `types.ts`), `CreateMessageResult.content` is a **discriminated union**, not an array. The code correctly handles this as a single content block. However, the condition checks both `stopReason` and `content.type`, which is redundant. + +**Impact:** This works but is redundant. When `stopReason === "toolUse"`, the content type is guaranteed to be `"tool_use"`. + +**Fix:** Simplify the condition: +```typescript +if (response.stopReason === "toolUse") { + const toolCall = response.content as ToolCallContent; +``` + +### 2. **Type Safety Issue: Message Content Structure** ⚠️ + +**Location:** Lines 183-191, 208-211 + +```typescript +const messages: SamplingMessage[] = [ + { + role: "user", + content: { + type: "text", + text: initialQuery, + }, + }, +]; +``` + +**Problem:** According to `SamplingMessageSchema` (lines 1285-1288 in `types.ts`), the schema uses a discriminated union. The code structure is correct, but TypeScript may not enforce this properly without explicit type annotations. + +**Impact:** Currently works, but could lead to type errors if the content structure changes. + +**Fix:** Add explicit type annotation: +```typescript +const messages: SamplingMessage[] = [ + { + role: "user", + content: { + type: "text", + text: initialQuery, + } as TextContent, + } as UserMessage, +]; +``` + +### 3. **Edge Case: Empty Content Block** ⚠️ + +**Location:** Lines 244-247 + +```typescript +// LLM provided final answer +if (response.content.type === "text") { + return response.content.text; +} +``` + +**Problem:** The code assumes that if the content type is "text", it has a valid `text` field. While this should always be true according to the protocol, there's no null/empty check. + +**Impact:** Could potentially return an empty string if the LLM returns empty text content. + +**Fix:** Add validation: +```typescript +if (response.content.type === "text") { + const text = response.content.text; + if (!text) { + throw new Error("LLM returned empty text content"); + } + return text; +} +``` + +### 4. **Potential Race Condition: System Prompt Injection** + +**Location:** Lines 283-290 + +```typescript +const systemPrompt = + "You are a helpful assistant that searches through files to answer questions. " + + "You have access to ripgrep (for searching) and read (for reading file contents). " + + "Use ripgrep to find relevant files, then read them to provide accurate answers. " + + "All paths are relative to the current working directory. " + + "Be concise and focus on providing the most relevant information."; + +const fullQuery = `${systemPrompt}\n\nUser query: ${query}`; +``` + +**Problem:** The system prompt is injected into the user message rather than using the `systemPrompt` parameter of `createMessage()`. This is not following MCP best practices. + +**Impact:** +- The LLM sees this as part of the user message, not as a system instruction +- Less effective instruction following +- Deviates from the protocol design + +**Fix:** Use the proper parameter: +```typescript +const response: CreateMessageResult = await server.server.createMessage({ + messages, + systemPrompt: "You are a helpful assistant that searches through files...", + maxTokens: 4000, + tools: LOCAL_TOOLS, + tool_choice: { mode: "auto" }, +}); +``` + +And remove the system prompt from the initial query: +```typescript +const messages: SamplingMessage[] = [ + { + role: "user", + content: { + type: "text", + text: initialQuery, // Just the query, not the system prompt + }, + }, +]; +``` + +### 5. **Missing: Tool Input Validation** + +**Location:** Lines 157-173 + +```typescript +async function executeLocalTool( + toolName: string, + toolInput: Record +): Promise> { + switch (toolName) { + case "ripgrep": { + const { pattern, path } = toolInput as { pattern: string; path: string }; + return await executeRipgrep(pattern, path); + } + case "read": { + const { path } = toolInput as { path: string }; + return await executeRead(path); + } +``` + +**Problem:** No validation that the input actually contains the required fields or that they are strings. + +**Impact:** Could crash with unhelpful errors if the LLM provides malformed input. + +**Fix:** Add validation: +```typescript +case "ripgrep": { + if (typeof toolInput.pattern !== 'string' || typeof toolInput.path !== 'string') { + return { error: 'Invalid input: pattern and path must be strings' }; + } + const { pattern, path } = toolInput as { pattern: string; path: string }; + return await executeRipgrep(pattern, path); +} +``` + +### 6. **Inconsistent Logging** + +**Location:** Lines 217-228, 281, 294 + +**Problem:** Some operations log detailed information, others don't. The logging uses `console.error` inconsistently. + +**Impact:** Makes debugging harder; inconsistent user experience. + +**Fix:** Add consistent logging at key points: +- Before and after LLM calls +- Tool execution start/end +- Error conditions + +### 7. **Tool Result Structure Mismatch** + +**Location:** Lines 230-238 + +```typescript +// Add tool result to message history +messages.push({ + role: "user", + content: { + type: "tool_result", + toolUseId: toolCall.id, + content: toolResult, + }, +}); +``` + +**Problem:** According to `ToolResultContentSchema` (lines 873-893 in `types.ts`), the `content` field should be a passthrough object. The current implementation passes `toolResult` which is `Record`, which is correct. However, the tool execution functions return objects with `{ output?: string; error?: string }` or `{ content?: string; error?: string }`, which is inconsistent. + +**Impact:** This works but creates an inconsistent structure for tool results. + +**Fix:** Standardize tool result format: +```typescript +interface ToolExecutionResult { + success: boolean; + data?: unknown; + error?: string; +} +``` + +--- + +## Suggested Improvements + +### 1. **Use Zod for Input Validation** + +The code already imports `z` from zod but only uses it for the tool registration. Consider using Zod schemas to validate tool inputs: + +```typescript +const RipgrepInputSchema = z.object({ + pattern: z.string(), + path: z.string(), +}); + +const ReadInputSchema = z.object({ + path: z.string(), +}); + +async function executeLocalTool( + toolName: string, + toolInput: Record +): Promise> { + try { + switch (toolName) { + case "ripgrep": { + const validated = RipgrepInputSchema.parse(toolInput); + return await executeRipgrep(validated.pattern, validated.path); + } + case "read": { + const validated = ReadInputSchema.parse(toolInput); + return await executeRead(validated.path); + } + default: + return { error: `Unknown tool: ${toolName}` }; + } + } catch (error) { + return { + error: error instanceof Error ? error.message : "Validation error", + }; + } +} +``` + +### 2. **Add Configurable Parameters** + +Consider making some hardcoded values configurable: + +```typescript +interface ToolLoopConfig { + maxIterations?: number; + maxTokens?: number; + workingDirectory?: string; + ripgrepMaxCount?: number; +} + +async function runToolLoop( + server: McpServer, + initialQuery: string, + config: ToolLoopConfig = {} +): Promise { + const MAX_ITERATIONS = config.maxIterations ?? 10; + const maxTokens = config.maxTokens ?? 4000; + // ... +} +``` + +### 3. **Improve Error Messages** + +Currently, tool errors are opaque. Consider adding more context: + +```typescript +return { + error: `Failed to read file '${inputPath}': ${error.message}`, + errorCode: 'FILE_READ_ERROR', + filePath: inputPath, +}; +``` + +### 4. **Add Tool Execution Timeout** + +Long-running ripgrep searches could hang. Consider adding timeouts: + +```typescript +const TOOL_EXECUTION_TIMEOUT = 30000; // 30 seconds + +async function executeRipgrep( + pattern: string, + path: string +): Promise<{ output?: string; error?: string }> { + return Promise.race([ + actualRipgrepExecution(pattern, path), + new Promise<{ error: string }>((resolve) => + setTimeout( + () => resolve({ error: 'Tool execution timeout' }), + TOOL_EXECUTION_TIMEOUT + ) + ), + ]); +} +``` + +### 5. **Better Comparison with Existing Examples** + +Compared to `toolWithSampleServer.ts`: +- ✅ **More sophisticated**: Implements a full agentic loop vs simple one-shot sampling +- ✅ **Better demonstrates tool calling**: Shows recursive tool use +- ⚠️ **More complex**: Could be harder for users to understand + +Compared to `backfillSampling.ts`: +- ✅ **Simpler scope**: Focused on server-side tool loop vs full proxy +- ✅ **Better demonstrates local tools**: Shows how to define and execute tools locally +- ✅ **More practical example**: Real-world use case (file search) + +### 6. **Consider Using `tool_choice: { mode: "required" }` Initially** + +For the first iteration, you might want to ensure the LLM uses a tool: + +```typescript +const response: CreateMessageResult = await server.server.createMessage({ + messages, + maxTokens: 4000, + tools: LOCAL_TOOLS, + tool_choice: iteration === 1 ? { mode: "required" } : { mode: "auto" }, +}); +``` + +This ensures the LLM doesn't try to answer without searching first. + +### 7. **Add Debug Mode** + +```typescript +const DEBUG = process.env.DEBUG === 'true'; + +function debug(...args: unknown[]) { + if (DEBUG) { + console.error('[toolLoop DEBUG]', ...args); + } +} +``` + +--- + +## Path Safety Analysis + +The path canonicalization logic is **mostly correct** but has a subtle issue: + +```typescript +function ensureSafePath(inputPath: string): string { + const resolved = resolve(CWD, inputPath); + const rel = relative(CWD, resolved); + + // Check if the path escapes CWD (starts with .. or is absolute outside CWD) + if (rel.startsWith("..") || resolve(CWD, rel) !== resolved) { + throw new Error(`Path "${inputPath}" is outside the current directory`); + } + + return resolved; +} +``` + +**Issue:** The second condition `resolve(CWD, rel) !== resolved` is always false if the first condition is false, making it redundant. + +**Better approach:** + +```typescript +function ensureSafePath(inputPath: string): string { + const resolved = resolve(CWD, inputPath); + const rel = relative(CWD, resolved); + + // Check if the path escapes CWD + if (rel.startsWith("..") || path.isAbsolute(rel)) { + throw new Error(`Path "${inputPath}" is outside the current directory`); + } + + return resolved; +} +``` + +**Edge cases to consider:** +- Symlinks could bypass this check (consider using `realpath`) +- Windows paths with drive letters +- UNC paths on Windows + +--- + +## Best Practices Compliance + +### TypeScript Best Practices +- ✅ Uses strict type checking +- ✅ Explicit return types on functions +- ⚠️ Could use more type guards and validation +- ✅ Good use of const and let +- ✅ Proper async/await usage + +### MCP SDK Best Practices +- ⚠️ **System prompt should use `systemPrompt` parameter, not be in user message** +- ✅ Proper message history management +- ✅ Correct tool result format +- ✅ Proper use of `createMessage()` API +- ✅ Good tool schema definitions + +### Error Handling +- ✅ Try-catch blocks in appropriate places +- ✅ Structured error objects +- ⚠️ Could benefit from custom error types +- ⚠️ Some error messages could be more descriptive + +### Code Quality +- ✅ Well-structured and readable +- ✅ Good function decomposition +- ✅ Appropriate use of constants +- ⚠️ Could benefit from more validation +- ⚠️ Some minor type safety improvements needed + +--- + +## Recommendations + +### Priority 1 (Must Fix Before Commit) + +1. **Fix system prompt handling** - Use the `systemPrompt` parameter in `createMessage()` instead of prepending to user query +2. **Add input validation** - Validate tool inputs before execution +3. **Simplify redundant conditions** - Remove redundant check on line 214 + +### Priority 2 (Should Fix) + +1. **Add empty content validation** - Check for empty text responses +2. **Improve path safety** - Consider symlink handling +3. **Standardize tool result format** - Use consistent structure for all tool results +4. **Add better logging** - Consistent, structured logging throughout + +### Priority 3 (Nice to Have) + +1. **Add configuration options** - Make hardcoded values configurable +2. **Add timeouts** - Prevent hung tool executions +3. **Add debug mode** - Better debugging experience +4. **Consider using `tool_choice: required` initially** - Ensure LLM searches before answering + +--- + +## Code Quality Assessment + +| Aspect | Rating | Notes | +|--------|--------|-------| +| Correctness | 8/10 | Works but has minor issues with system prompt handling | +| Security | 8/10 | Good path validation but could handle symlinks | +| Error Handling | 7/10 | Good structure but needs more validation | +| Type Safety | 7/10 | Good but could be stricter with type guards | +| Readability | 9/10 | Well-documented and structured | +| Best Practices | 7/10 | Good but system prompt handling needs fix | +| **Overall** | **7.5/10** | Good quality, needs minor fixes before commit | + +--- + +## Verdict + +**Status: Needs Minor Changes Before Commit** + +The code demonstrates a sophisticated understanding of the MCP sampling API and implements a useful, practical example. However, there are a few issues that should be addressed: + +1. **Must fix:** System prompt handling (use `systemPrompt` parameter) +2. **Should fix:** Add input validation +3. **Should fix:** Simplify redundant conditions + +Once these issues are addressed, this will be an excellent example of MCP tool loop patterns and should be committed. + +--- + +## Testing Recommendations + +Before committing, test the following scenarios: + +1. **Happy path**: Query that requires multiple tool calls +2. **Edge cases**: + - Empty query + - Path traversal attempts (`../`, `../../`, etc.) + - Non-existent files + - Very large search results + - Binary files +3. **Error conditions**: + - Malformed tool inputs from LLM + - ripgrep not installed + - Permission denied errors + - Max iterations exceeded +4. **Security**: + - Symlink handling + - Absolute paths + - Special characters in paths + +Consider adding a unit test file (`toolLoopSampling.test.ts`) to cover these scenarios. diff --git a/intermediate-findings/toolLoopSampling-test-review.md b/intermediate-findings/toolLoopSampling-test-review.md new file mode 100644 index 000000000..7e668ab38 --- /dev/null +++ b/intermediate-findings/toolLoopSampling-test-review.md @@ -0,0 +1,249 @@ +# Test Review: toolLoopSampling.test.ts + +## Executive Summary + +The test file `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.test.ts` is **ready to commit with minor improvements recommended**. The tests are well-structured, provide good coverage of the tool loop functionality, and follow project conventions. However, there are opportunities for simplification and improved maintainability. + +## Test Coverage Analysis + +### Covered Scenarios ✓ + +1. **Happy Path**: Tool loop with sequential ripgrep and read operations (lines 57-165) +2. **Error Handling**: + - Path validation errors (lines 167-251) + - Invalid tool names (lines 253-331) + - Malformed tool inputs (lines 333-413) +3. **Edge Cases**: + - Maximum iteration limit enforcement (lines 415-470) + +### Coverage Assessment + +**Score: 8/10** + +**Strengths:** +- Tests the complete tool loop flow from initial query to final answer +- Validates error propagation and handling at multiple levels +- Confirms iteration limit protection against infinite loops +- Tests input validation (both tool name and tool parameters) + +**Gaps:** +- No test for successful multi-iteration loops (e.g., ripgrep → read → ripgrep again → answer) +- Missing test for stopReason variants beyond "toolUse" and "endTurn" +- No explicit test for tool result error handling when tool execution fails but returns gracefully +- No test for empty/edge case responses from ripgrep (e.g., "No matches found") + +## Code Quality Assessment + +### Structure & Organization + +**Score: 9/10** + +- Consistent test structure across all test cases +- Good use of `beforeEach` and `afterEach` for setup/teardown +- Clear test names that describe the scenario being tested +- Proper Jest timeout configuration for integration tests + +### Best Practices Adherence + +**Comparison with project patterns:** + +| Pattern | toolLoopSampling.test.ts | server/index.test.ts | client/index.test.ts | Assessment | +|---------|--------------------------|----------------------|----------------------|------------| +| Transport management | Uses StdioClientTransport | Uses InMemoryTransport | Uses mock Transport | ✓ Appropriate for integration | +| Client/Server setup | Creates real client+server | Creates real client+server | Uses mocks | ✓ Correct pattern | +| Handler setup | Uses setRequestHandler | Uses setRequestHandler | Uses setRequestHandler | ✓ Consistent | +| Assertions | Uses expect().toBe/toContain | Uses expect().toBe/toEqual | Uses expect().toBe | ✓ Standard Jest | +| Error testing | Tests via result content | Uses rejects.toThrow | Uses rejects.toThrow | ⚠️ Could be more explicit | + +**Observations:** +- **Positive**: The test properly spawns the actual server using `npx tsx`, making it a true integration test +- **Positive**: Uses `resolve(__dirname, ...)` for reliable path resolution +- **Concern**: Heavy reliance on `console.error` output for debugging (lines 71-73, etc.) +- **Concern**: Sampling handler complexity increases with each test case + +## Duplication Analysis + +### Identified Duplication + +1. **Client/Transport Setup** (repeated in every `beforeEach`): + ```typescript + // Lines 25-51 - repeated setup code + client = new Client(...) + transport = new StdioClientTransport(...) + ``` + +2. **Sampling Handler Pattern** (repeated 5 times): + ```typescript + // Lines 62-134, 171-229, 258-312, 338-394, 419-439 + client.setRequestHandler(CreateMessageRequestSchema, async (request) => { + samplingCallCount++; + const messages = request.params.messages; + const lastMessage = messages[messages.length - 1]; + // ... different logic per test + }) + ``` + +3. **Connection and Tool Call** (repeated 5 times): + ```typescript + // Lines 136-151, 231-245, 314-327, 396-409, 442-456 + await client.connect(transport); + const result = await client.request({ method: "tools/call", ... }) + ``` + +### Duplication Impact + +**Score: 6/10** (significant duplication, but manageable) + +## Simplification Opportunities + +### High Priority + +1. **Extract Helper Function for Sampling Handler Setup** + ```typescript + // Proposed helper + function createMockSamplingHandler(responses: Array) { + let callIndex = 0; + return async (request: CreateMessageRequest): Promise => { + const response = responses[callIndex]; + callIndex++; + if (!response) { + throw new Error(`Unexpected sampling call count: ${callIndex}`); + } + return { + model: "test-model", + role: "assistant", + content: response, + stopReason: response.type === "tool_use" ? "toolUse" : "endTurn", + }; + }; + } + ``` + **Benefit**: Reduce 150+ lines of duplicated handler setup code + +2. **Extract Helper for Common Test Flow** + ```typescript + // Proposed helper + async function executeFileSearchTest( + client: Client, + transport: StdioClientTransport, + query: string, + expectedSamplingCalls: number + ) { + await client.connect(transport); + const result = await client.request( + { method: "tools/call", params: { name: "fileSearch", arguments: { query } } }, + CallToolResultSchema + ); + return { result, samplingCallCount: /* tracked value */ }; + } + ``` + **Benefit**: Reduce boilerplate in each test + +3. **Simplify Error Assertions** + ```typescript + // Current (lines 199-210): + expect(lastMessage.content.type).toBe("tool_result"); + if (lastMessage.content.type === "tool_result") { + const content = lastMessage.content.content as Record; + expect(content.error).toBeDefined(); + expect(typeof content.error === "string" && content.error.includes("...")).toBe(true); + } + + // Proposed: + expectToolResultError(lastMessage, "outside the current directory"); + ``` + **Benefit**: Improve readability and maintainability + +### Medium Priority + +4. **Remove Verbose Console Logging** + - Lines 71-73: Console.error statements should use a debug flag or be removed + - These logs are helpful during development but clutter test output + +5. **Consolidate Type Assertions** + - Lines 159-161, 200-210, 284-295: Repeated type narrowing patterns + - Create a helper: `assertTextContent(content)` or use type guards + +### Low Priority + +6. **Use Test.each for Similar Tests** + - Tests for "invalid tool names" and "malformed tool inputs" follow similar patterns + - Could be parameterized to reduce code + +## Clarity Assessment + +**Score: 8/10** + +### Strengths +- Test names clearly describe what's being tested +- Comments explain the test scenario (lines 2-6, 57, etc.) +- Logical flow is easy to follow +- Good use of descriptive variable names + +### Areas for Improvement +1. **Overly Complex Inline Handlers**: The sampling handlers contain significant logic that makes tests harder to understand at a glance +2. **Mixed Concerns**: Tests verify both the sampling call count AND the result content, which could be split +3. **Implicit Behavior**: The interaction between samplingCallCount and handler logic requires mental tracking + +## Comparison with Project Test Patterns + +### server/index.test.ts Patterns +- ✓ Uses descriptive test names +- ✓ Groups related tests with `describe` blocks +- ✓ Uses `beforeEach`/`afterEach` consistently +- ✓ Tests both success and error cases +- ⚠️ toolLoopSampling uses more complex integration setup (spawning process) + +### client/index.test.ts Patterns +- ✓ Similar assertion patterns +- ✓ Uses `expect().toBe()`, `expect().toContain()` +- ✓ Tests timeout and cancellation scenarios +- ⚠️ toolLoopSampling has more verbose test bodies + +### protocol.test.ts Patterns +- ✓ Good use of jest.fn() for mocking +- ✓ Clean setup/teardown +- ✓ Focused test cases +- ⚠️ toolLoopSampling could benefit from similar spy usage + +## Recommendations + +### Must Fix (Before Commit) +None - the tests are functional and pass. + +### Should Fix (High Value) +1. **Extract sampling handler helper** - Reduces duplication by ~40% +2. **Add helper for error content assertions** - Improves readability +3. **Remove or gate console.error debug statements** - Cleaner test output + +### Nice to Have (Future Improvements) +1. Add test for successful multi-step tool loop (ripgrep → read → ripgrep → answer) +2. Add test for edge cases in tool results (empty results, no matches) +3. Consider parameterized tests for error scenarios +4. Add JSDoc comments to explain the test strategy + +## Verdict + +**Status: READY TO COMMIT AS-IS** + +The tests are well-designed, follow project conventions, and provide solid coverage of the tool loop sampling functionality. While there are opportunities for refactoring to reduce duplication and improve clarity, the current implementation is: + +- ✓ Functional and passing +- ✓ Covers main scenarios adequately +- ✓ Follows TypeScript and Jest best practices +- ✓ Provides good error coverage +- ✓ Tests important edge cases (iteration limit, validation errors) + +**Recommended Action:** +1. Commit the tests as-is to establish baseline coverage +2. Create a follow-up task to implement the suggested refactorings +3. Consider adding the recommended test cases for multi-step loops in a future iteration + +## Files Analyzed + +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.test.ts` (main test file) +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.ts` (implementation) +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/index.test.ts` (reference patterns) +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.test.ts` (reference patterns) +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.test.ts` (reference patterns) +- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/demoInMemoryOAuthProvider.test.ts` (reference patterns) diff --git a/intermediate-findings/transport-analysis.md b/intermediate-findings/transport-analysis.md new file mode 100644 index 000000000..14c47778e --- /dev/null +++ b/intermediate-findings/transport-analysis.md @@ -0,0 +1,960 @@ +# MCP TypeScript SDK Transport Architecture Analysis + +## Executive Summary + +This document provides a comprehensive analysis of the transport layer implementation in the MCP TypeScript SDK, focusing on how messages flow through the system, validation mechanisms, error handling, and potential areas for improvement. + +## 1. Architecture Overview + +### 1.1 Core Transport Architecture + +The MCP SDK uses a **layered architecture** with clear separation of concerns: + +``` +┌─────────────────────────────────────────────────────────┐ +│ Client / Server (High-level API) │ +│ - Client class (src/client/index.ts) │ +│ - Server class (src/server/index.ts) │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Protocol Layer │ +│ - Protocol class (src/shared/protocol.ts) │ +│ - Request/Response handling │ +│ - Progress tracking │ +│ - Capability enforcement │ +│ - Timeout management │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Transport Interface │ +│ (src/shared/transport.ts) │ +│ - start(), send(), close() │ +│ - onmessage, onerror, onclose callbacks │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Transport Implementations │ +│ - StdioClientTransport │ +│ - SSEClientTransport / SSEServerTransport │ +│ - StreamableHTTPClientTransport / Server │ +│ - InMemoryTransport (testing) │ +└─────────────────────────────────────────────────────────┘ +``` + +### 1.2 Key Design Principles + +1. **Transport Agnostic**: The Protocol layer is completely independent of the transport mechanism +2. **Callback-Based**: Transport implementations use callbacks (`onmessage`, `onerror`, `onclose`) to push data up +3. **Async Operations**: All transport operations return Promises +4. **Schema Validation**: Zod schemas validate messages at the protocol boundary + +## 2. Key Files and Their Purposes + +### 2.1 Core Transport Files + +#### `/src/shared/transport.ts` (86 lines) +**Purpose**: Defines the Transport interface contract + +**Key Types**: +- `Transport` interface: The contract all transport implementations must fulfill +- `TransportSendOptions`: Options for sending messages (relatedRequestId, resumption tokens) +- `FetchLike`: Type for custom fetch implementations + +**Core Interface**: +```typescript +interface Transport { + start(): Promise; // Initialize connection + send(message: JSONRPCMessage, options?: TransportSendOptions): Promise; + close(): Promise; + + // Callbacks + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; + + // Optional features + sessionId?: string; + setProtocolVersion?: (version: string) => void; +} +``` + +#### `/src/shared/protocol.ts` (785 lines) +**Purpose**: Implements the MCP protocol layer on top of any transport + +**Key Responsibilities**: +1. **Request/Response Correlation**: Maps request IDs to response handlers +2. **Progress Tracking**: Handles progress notifications via progress tokens +3. **Timeout Management**: Implements per-request timeouts with optional progress-based reset +4. **Capability Enforcement**: Validates that requests match advertised capabilities +5. **Request Cancellation**: Supports AbortSignal-based cancellation +6. **Notification Debouncing**: Can coalesce multiple notifications in the same tick +7. **Request Handler Management**: Routes incoming requests to registered handlers + +**Key Data Structures**: +```typescript +private _requestMessageId = 0; // Monotonic counter for request IDs +private _requestHandlers: Map +private _notificationHandlers: Map +private _responseHandlers: Map +private _progressHandlers: Map +private _timeoutInfo: Map +private _pendingDebouncedNotifications: Set +``` + +**Critical Design Pattern - Transport Capture**: +The protocol uses a **transport capture pattern** in `_onrequest()` to ensure responses go to the correct client when multiple connections exist: + +```typescript +// Capture the current transport at request time +const capturedTransport = this._transport; + +// Use capturedTransport for sending responses, not this._transport +``` + +This prevents a race condition where reconnections could send responses to the wrong client. + +### 2.2 Transport Implementations + +#### Stdio Transport (`/src/shared/stdio.ts`, `/src/client/stdio.ts`) +- **Communication**: Line-delimited JSON over stdin/stdout +- **Serialization**: `serializeMessage()` adds newline, `deserializeMessage()` uses Zod validation +- **Buffering**: `ReadBuffer` class handles partial message buffering +- **Use Case**: Local process communication, MCP servers as child processes + +#### SSE Transport (Client: `/src/client/sse.ts`, Server: `/src/server/sse.ts`) +- **Client Receives**: Via Server-Sent Events (GET request with EventSource) +- **Client Sends**: Via POST requests to endpoint provided by server +- **Server Receives**: POST requests with JSON body +- **Server Sends**: SSE stream with `event: message` format +- **Authentication**: Integrated OAuth support with UnauthorizedError handling +- **Security**: DNS rebinding protection (optional, configurable) + +#### Streamable HTTP Transport +**Client** (`/src/client/streamableHttp.ts` - 570 lines): +- Most complex transport implementation +- Bidirectional HTTP with SSE for server-to-client messages +- **Reconnection Logic**: Exponential backoff with configurable parameters +- **Resumability**: Last-Event-ID header for resuming interrupted streams +- **Session Management**: Tracks session ID across reconnections + +**Server** (`/src/server/streamableHttp.ts`): +- Stateful (session-based) and stateless modes +- Event store interface for resumability support +- Stream management for multiple concurrent requests + +#### InMemory Transport (`/src/inMemory.ts` - 64 lines) +- Testing-only transport +- Direct in-memory message passing +- Useful for unit tests and integration tests + +### 2.3 Protocol Types + +#### `/src/types.ts` (1500+ lines) +**Purpose**: Central type definitions using Zod schemas + +**Key Message Types**: +```typescript +// Base types +JSONRPCRequest // { jsonrpc: "2.0", id, method, params } +JSONRPCNotification // { jsonrpc: "2.0", method, params } +JSONRPCResponse // { jsonrpc: "2.0", id, result } +JSONRPCError // { jsonrpc: "2.0", id, error: { code, message, data? }} +``` + +**Error Codes**: +```typescript +enum ErrorCode { + // SDK-specific + ConnectionClosed = -32000, + RequestTimeout = -32001, + + // JSON-RPC standard + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, +} +``` + +**MessageExtraInfo** (line 1551): +```typescript +interface MessageExtraInfo { + requestInfo?: RequestInfo; // HTTP headers, etc. + authInfo?: AuthInfo; // OAuth token info +} +``` + +## 3. Message Flow + +### 3.1 Outgoing Request Flow (Client -> Server) + +``` +┌──────────────────┐ +│ Client.request()│ +└────────┬─────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Protocol.request() │ +│ - Generate message ID │ +│ - Add progress token if requested │ +│ - Register response handler │ +│ - Set up timeout │ +│ - Set up cancellation handler │ +└────────┬───────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Transport.send(JSONRPCRequest) │ +│ - Serialize message │ +│ - Send over wire (HTTP/SSE/stdio) │ +└────────────────────────────────────────┘ +``` + +### 3.2 Incoming Request Flow (Server <- Client) + +``` +┌────────────────────────────────────────┐ +│ Transport receives data │ +│ - Parse JSON │ +│ - Validate with Zod schema │ +└────────┬───────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Transport.onmessage(message, extra) │ +│ - extra contains authInfo, headers │ +└────────┬───────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Protocol._onrequest() │ +│ - Capture current transport │ +│ - Look up handler │ +│ - Create AbortController │ +│ - Build RequestHandlerExtra context │ +│ - Execute handler │ +└────────┬───────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Request Handler │ +│ - Business logic │ +│ - Can send notifications │ +│ - Can make requests │ +│ - Returns Result │ +└────────┬───────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Send Response │ +│ - Use capturedTransport.send() │ +│ - Send JSONRPCResponse or Error │ +└────────────────────────────────────────┘ +``` + +### 3.3 Progress Notification Flow + +``` +Client Request (with onprogress) + │ + ▼ +Request includes _meta: { progressToken: messageId } + │ + ▼ +Server Handler receives request + │ + ▼ +Server sends: notifications/progress + { params: { progressToken, progress, total, message? }} + │ + ▼ +Client Protocol._onprogress() + - Look up handler by progressToken + - Optionally reset timeout + - Call onprogress callback +``` + +## 4. Validation Mechanisms + +### 4.1 Schema Validation (Zod) + +**Where**: At the transport boundary when messages are received + +**Implementation**: +```typescript +// In shared/stdio.ts +export function deserializeMessage(line: string): JSONRPCMessage { + return JSONRPCMessageSchema.parse(JSON.parse(line)); +} + +// In SSE transports +const message = JSONRPCMessageSchema.parse(JSON.parse(messageEvent.data)); + +// In protocol request handlers +this._requestHandlers.set(method, (request, extra) => { + return Promise.resolve(handler(requestSchema.parse(request), extra)); +}); +``` + +**Validation Levels**: +1. **Message Structure**: JSONRPCMessageSchema validates basic JSON-RPC structure +2. **Request Params**: Individual request schemas validate params structure +3. **Result Schema**: Response results are validated against expected schemas +4. **Type Guards**: Helper functions like `isJSONRPCRequest()`, `isJSONRPCResponse()` + +### 4.2 Capability Validation + +**Where**: In Client and Server classes before sending requests + +**Methods**: +- `assertCapabilityForMethod()`: Checks remote side supports the request method +- `assertNotificationCapability()`: Checks local side advertised notification support +- `assertRequestHandlerCapability()`: Checks local side advertised handler support + +**Example from Client**: +```typescript +protected assertCapabilityForMethod(method: RequestT["method"]): void { + switch (method as ClientRequest["method"]) { + case "tools/call": + case "tools/list": + if (!this._serverCapabilities?.tools) { + throw new Error( + `Server does not support tools (required for ${method})` + ); + } + break; + // ... more cases + } +} +``` + +**Enforcement**: Optional via `enforceStrictCapabilities` option (defaults to false for backwards compatibility) + +### 4.3 Client-Side Tool Output Validation + +**Special Case**: Client validates tool call results against declared output schemas + +**Implementation** (`/src/client/index.ts`, lines 429-479): +```typescript +async callTool(params, resultSchema, options) { + const result = await this.request(...); + + const validator = this.getToolOutputValidator(params.name); + if (validator) { + // Tool with outputSchema MUST return structuredContent + if (!result.structuredContent && !result.isError) { + throw new McpError(...); + } + + // Validate structured content against schema + if (result.structuredContent) { + const isValid = validator(result.structuredContent); + if (!isValid) { + throw new McpError(...); + } + } + } + + return result; +} +``` + +**Why This Exists**: Ensures servers respect their own tool output schemas + +### 4.4 Transport-Specific Validation + +#### DNS Rebinding Protection +Both SSE and StreamableHTTP server transports support optional DNS rebinding protection: + +```typescript +private validateRequestHeaders(req: IncomingMessage): string | undefined { + if (!this._enableDnsRebindingProtection) return undefined; + + // Validate Host header + if (this._allowedHosts && !this._allowedHosts.includes(req.headers.host)) { + return `Invalid Host header`; + } + + // Validate Origin header + if (this._allowedOrigins && !this._allowedOrigins.includes(req.headers.origin)) { + return `Invalid Origin header`; + } + + return undefined; +} +``` + +#### Session Validation (StreamableHTTP stateful mode) +- Validates session ID in headers matches expected session +- Returns 404 if session not found +- Returns 400 if non-initialization request lacks session ID + +## 5. Error Handling + +### 5.1 Error Types and Hierarchy + +``` +Error (JavaScript base) + │ + ├─ McpError (general MCP errors with error codes) + │ - ConnectionClosed + │ - RequestTimeout + │ - ParseError + │ - InvalidRequest + │ - MethodNotFound + │ - InvalidParams + │ - InternalError + │ + ├─ TransportError (transport-specific errors) + │ ├─ SseError (SSE transport errors) + │ ├─ StreamableHTTPError (Streamable HTTP errors) + │ └─ UnauthorizedError (authentication failures) + │ + └─ OAuthError (authentication/authorization errors) + └─ [Many specific OAuth error types] +``` + +### 5.2 Error Handling Patterns + +#### Protocol Layer Error Handling + +**Request Handler Errors**: +```typescript +Promise.resolve() + .then(() => handler(request, fullExtra)) + .then( + (result) => capturedTransport?.send({ result, ... }), + (error) => capturedTransport?.send({ + error: { + code: Number.isSafeInteger(error["code"]) ? error["code"] : ErrorCode.InternalError, + message: error.message ?? "Internal error" + } + }) + ) + .catch((error) => this._onerror(new Error(`Failed to send response: ${error}`))) +``` + +**Key Behaviors**: +1. Errors are caught and converted to JSON-RPC error responses +2. If error has numeric `code`, it's preserved; otherwise defaults to InternalError +3. If sending the error response fails, it's reported via `onerror` callback +4. Aborted requests don't send responses + +#### Transport Layer Error Handling + +**Stdio Transport**: +- Zod validation errors reported via `onerror` +- Process spawn errors reject start() promise +- Stream errors reported via `onerror` + +**SSE/StreamableHTTP Transports**: +- Network errors caught and reported via `onerror` +- 401 responses trigger authentication flow (if authProvider present) +- Connection close triggers cleanup and `onclose` callback +- Reconnection with exponential backoff (StreamableHTTP only) + +### 5.3 Error Propagation + +``` +Transport Layer + └─> onerror(error) callback + │ + ▼ +Protocol Layer + └─> this.onerror(error) callback + │ + ▼ +Client/Server Layer + └─> User-defined onerror handler +``` + +**Request/Response Errors**: +- Returned as rejected Promise from `request()` method +- Either McpError (from remote) or Error (local/transport) + +**Notification Handler Errors**: +- Caught and reported via `onerror` +- Do not affect other operations + +## 6. Gaps and Potential Issues + +### 6.1 Validation Gaps + +#### 6.1.1 Incomplete Message Validation +**Issue**: While JSON-RPC structure is validated, there's limited validation of: +- Message content before sending (only validated on receive) +- Semantic correctness of method names +- Parameter structure matching method requirements + +**Evidence**: +- No validation in `Protocol.request()` that `request.method` is valid before sending +- Transport implementations assume messages are well-formed + +**Impact**: Invalid messages can be sent and only caught by the receiver + +#### 6.1.2 No Validation of TransportSendOptions +**Issue**: `TransportSendOptions` (relatedRequestId, resumptionToken) are not validated + +**Potential Problems**: +- Invalid resumption tokens could cause server errors +- Wrong relatedRequestId could break request association + +#### 6.1.3 Missing Protocol Version Validation During Send +**Issue**: After negotiation, protocol version is stored but not validated on every message + +**Current State**: +- `setProtocolVersion()` called after initialization +- No validation that subsequent messages conform to negotiated version + +### 6.2 Error Handling Gaps + +#### 6.2.1 Limited Error Context +**Issue**: Errors often lose context as they propagate + +**Example**: When `JSONRPCMessageSchema.parse()` fails: +```typescript +try { + message = JSONRPCMessageSchema.parse(JSON.parse(messageEvent.data)); +} catch (error) { + this.onerror?.(error as Error); // Original message content is lost + return; +} +``` + +**Better Approach**: Include the raw message in error context + +#### 6.2.2 Silent Failures in Some Paths +**Issue**: Some error paths don't propagate errors effectively + +**Example in Protocol**: +```typescript +this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`)); +// Message is dropped silently if no onerror handler +``` + +**Better Approach**: Throw error or have required error handling + +#### 6.2.3 No Error Recovery Mechanisms +**Issue**: Most errors are fatal to the connection + +**Missing**: +- Ability to recover from transient errors +- Retry logic for failed sends (except StreamableHTTP reconnection) +- Graceful degradation when features are unavailable + +### 6.3 Transport-Specific Issues + +#### 6.3.1 Race Conditions in Protocol Connection +**Evidence**: The `protocol-transport-handling.test.ts` shows a bug where multiple rapid connections can send responses to the wrong client + +**Fix Applied**: Transport capture pattern in `_onrequest()` + +**Remaining Risk**: Similar issues could occur with: +- Progress notifications sent to wrong client +- Notification handlers accessing wrong transport + +#### 6.3.2 No Backpressure Handling +**Issue**: None of the transports implement backpressure or flow control + +**Potential Problems**: +- Stdio: If stdin write buffer fills, could block +- HTTP: No limit on concurrent requests +- No queuing or rate limiting + +#### 6.3.3 Incomplete Resumability Implementation +**Status**: Resumability API exists but: +- Only StreamableHTTP client supports it +- Server-side EventStore is an interface with no default implementation +- No automatic resumability without custom EventStore + +### 6.4 Protocol Layer Issues + +#### 6.4.1 Timeout Reset Logic Complexity +**Issue**: The timeout reset logic (for progress notifications) is complex and error-prone + +**Code** (lines 268-285): +```typescript +private _resetTimeout(messageId: number): boolean { + const info = this._timeoutInfo.get(messageId); + if (!info) return false; + + const totalElapsed = Date.now() - info.startTime; + if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { + this._timeoutInfo.delete(messageId); + throw new McpError(...); // Throws from unexpected context + } + + clearTimeout(info.timeoutId); + info.timeoutId = setTimeout(info.onTimeout, info.timeout); + return true; +} +``` + +**Problem**: Throws exception that gets caught in progress handler, but the flow is not obvious + +#### 6.4.2 Request Handler Memory Leak Risk +**Issue**: AbortControllers are stored in `_requestHandlerAbortControllers` map + +**Risk**: If request handling never completes (handler hangs), entries never cleaned up + +**Mitigation**: Cleanup happens in `finally` block, but what if handler never returns? + +#### 6.4.3 No Rate Limiting or Request Queue Management +**Issue**: Unlimited concurrent requests are allowed + +**Problems**: +- Memory usage can grow unbounded +- No prioritization of requests +- No limit on message IDs (though it's just a counter) + +### 6.5 Testing and Observability Gaps + +#### 6.5.1 Limited Error Testing +**Observation**: Test files focus on happy paths + +**Missing Tests**: +- Malformed JSON handling +- Invalid protocol version negotiation +- Capability violations +- Concurrent request/connection scenarios + +#### 6.5.2 No Built-in Logging or Tracing +**Issue**: No standardized way to trace messages through the system + +**Current State**: Each component can report via `onerror`, but: +- No structured logging +- No message IDs in logs +- No performance metrics + +#### 6.5.3 Proposed Transport Validator Not Integrated +**Evidence**: `src/shared/transport-validator.ts` exists with a `ProtocolValidator` class + +**Status**: +- Not used anywhere in codebase +- Implements logging but not enforcement +- Could be the foundation for protocol validation + +**Potential**: +```typescript +class ProtocolValidator implements Transport { + private log: ProtocolLog = { events: [] } + + // Wraps a transport and logs all events + // Can run checkers on the log +} +``` + +This could validate: +- Message ordering (initialize must be first) +- Request/response pairing +- Capability usage +- Protocol version conformance + +## 7. Existing Validation Infrastructure + +### 7.1 Zod Schema System + +**Strengths**: +- Comprehensive type definitions +- Runtime validation +- Type inference for TypeScript +- Good error messages + +**Coverage**: +- All JSON-RPC message types +- All MCP-specific request/response types +- Capability structures +- Metadata structures + +### 7.2 Type Guards + +**Available Functions**: +```typescript +isJSONRPCRequest(value) +isJSONRPCResponse(value) +isJSONRPCError(value) +isJSONRPCNotification(value) +isInitializeRequest(value) +isInitializedNotification(value) +``` + +**Usage Pattern**: +```typescript +if (isJSONRPCResponse(message) || isJSONRPCError(message)) { + this._onresponse(message); +} else if (isJSONRPCRequest(message)) { + this._onrequest(message, extra); +} else if (isJSONRPCNotification(message)) { + this._onnotification(message); +} +``` + +### 7.3 Test Infrastructure + +**Available**: +- InMemoryTransport for testing +- MockTransport in test files +- Protocol test suite with various scenarios +- Transport-specific test suites + +**Good Coverage Of**: +- Basic message flow +- Error scenarios +- Timeout behavior +- Progress notifications +- Debounced notifications + +## 8. Recommendations for Protocol Validation Improvements + +### 8.1 High Priority + +#### 8.1.1 Integrate Transport Validator +**Action**: Complete and integrate the ProtocolValidator class + +**Benefits**: +- Centralized validation logic +- Protocol conformance checking +- Better debugging and testing + +**Implementation**: +```typescript +// Wrap any transport with validation +const validatedTransport = new ProtocolValidator( + rawTransport, + [ + checkInitializeFirst, + checkRequestResponsePairing, + checkCapabilityUsage, + ] +); +``` + +#### 8.1.2 Enhanced Error Context +**Action**: Add message context to validation errors + +**Example**: +```typescript +catch (error) { + this.onerror?.(new Error( + `Failed to parse message: ${error}\nRaw message: ${rawMessage}` + )); +} +``` + +#### 8.1.3 Pre-Send Validation +**Action**: Validate messages before sending + +**Implementation**: +```typescript +async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { + // Validate message structure + const parseResult = JSONRPCMessageSchema.safeParse(message); + if (!parseResult.success) { + throw new Error(`Invalid message: ${parseResult.error}`); + } + + // Validate options + if (options?.resumptionToken && typeof options.resumptionToken !== 'string') { + throw new Error('Invalid resumption token'); + } + + // Send validated message + return this._actualSend(message, options); +} +``` + +### 8.2 Medium Priority + +#### 8.2.1 Structured Logging +**Action**: Add optional structured logging throughout + +**Example**: +```typescript +interface TransportLogger { + logMessageSent(message: JSONRPCMessage, options?: TransportSendOptions): void; + logMessageReceived(message: JSONRPCMessage, extra?: MessageExtraInfo): void; + logError(error: Error, context?: Record): void; +} +``` + +#### 8.2.2 Backpressure Support +**Action**: Add flow control to prevent overwhelming the transport + +**API**: +```typescript +interface Transport { + // ...existing methods + canSend?(): boolean; // Check if ready to accept messages + onready?: () => void; // Called when ready to send after backpressure +} +``` + +#### 8.2.3 Request Queue Management +**Action**: Add limits and prioritization + +**Options**: +```typescript +interface ProtocolOptions { + // ...existing options + maxConcurrentRequests?: number; + requestQueueSize?: number; +} +``` + +### 8.3 Low Priority (Future Enhancements) + +#### 8.3.1 Automatic Retry Logic +**Action**: Add configurable retry for failed requests + +**Options**: +```typescript +interface RequestOptions { + // ...existing options + retry?: { + maxAttempts: number; + backoff: 'exponential' | 'linear'; + retryableErrors?: number[]; // Error codes that should be retried + }; +} +``` + +#### 8.3.2 Message Compression +**Action**: Support compressed message payloads for large transfers + +#### 8.3.3 Message Batching +**Action**: Allow batching multiple requests in one transport send + +## 9. How Protocol Validation Could Be Improved + +### 9.1 State Machine Based Validation + +**Concept**: Track protocol state and validate allowed transitions + +```typescript +enum ProtocolState { + Disconnected, + Connecting, + Initializing, + Initialized, + Closing, + Closed, +} + +class StatefulProtocolValidator { + private state: ProtocolState = ProtocolState.Disconnected; + + validateMessage(message: JSONRPCMessage): ValidationResult { + if (this.state === ProtocolState.Connecting && + !isInitializeRequest(message)) { + return { valid: false, error: 'Must send initialize first' }; + } + + // ...more state-based validation + + return { valid: true }; + } +} +``` + +### 9.2 Message Sequence Validation + +**Track**: +- Initialize must be first request +- Initialized notification must follow initialize response +- No requests before initialized (except cancel/ping) +- Request IDs must be unique per direction +- Response IDs must match request IDs + +### 9.3 Capability-Based Validation + +**Enhance**: +```typescript +class CapabilityValidator { + constructor( + private localCapabilities: Capabilities, + private remoteCapabilities: Capabilities + ) {} + + canSendRequest(method: string): ValidationResult { + // Check if remote side advertised support + } + + canHandleRequest(method: string): ValidationResult { + // Check if local side advertised support + } + + canSendNotification(method: string): ValidationResult { + // Check if local side advertised capability + } +} +``` + +### 9.4 Schema-Based Request Validation + +**Validate request params match method schema**: +```typescript +class RequestValidator { + private schemas = new Map(); + + validateRequest(request: JSONRPCRequest): ValidationResult { + const schema = this.schemas.get(request.method); + if (!schema) { + return { valid: false, error: 'Unknown method' }; + } + + const result = schema.safeParse(request.params); + if (!result.success) { + return { valid: false, error: result.error }; + } + + return { valid: true }; + } +} +``` + +## 10. Summary + +### Strengths +1. ✅ Clean separation between protocol and transport layers +2. ✅ Comprehensive Zod-based type system +3. ✅ Good test coverage for basic scenarios +4. ✅ Flexible transport interface supports multiple implementations +5. ✅ Robust timeout and cancellation support +6. ✅ Authentication integration for HTTP-based transports + +### Weaknesses +1. ❌ Limited pre-send validation +2. ❌ Error context often lost during propagation +3. ❌ No backpressure or flow control +4. ❌ ProtocolValidator class exists but not integrated +5. ❌ Limited observability (logging, tracing) +6. ❌ Some race conditions with multiple connections + +### Opportunities +1. 💡 Integrate and expand ProtocolValidator +2. 💡 Add state machine based protocol validation +3. 💡 Improve error context and recovery +4. 💡 Add structured logging/tracing +5. 💡 Implement backpressure handling +6. 💡 Add request queue management and prioritization + +### Protocol Validation Specific +The codebase has excellent **foundations** for protocol validation: +- Comprehensive schemas +- Type guards +- Capability system +- Unused ProtocolValidator class + +What's **missing**: +- Pre-send validation +- State transition validation +- Message sequence validation +- Integration of validator infrastructure + +**Recommendation**: Focus on integrating the existing ProtocolValidator and expanding it with state machine validation before building entirely new validation systems. diff --git a/protocol-debugger.ts b/protocol-debugger.ts new file mode 100644 index 000000000..38e93c992 --- /dev/null +++ b/protocol-debugger.ts @@ -0,0 +1,9 @@ +import { Transport } from './src/shared/transport.js'; + +interface SessionDebugger { + +} + +class Debugger { + constructor(private transport: Transport) {} +} \ No newline at end of file diff --git a/src/examples/backfill/backfillSampling.merge.ts b/src/examples/backfill/backfillSampling.merge.ts new file mode 100644 index 000000000..678c1791e --- /dev/null +++ b/src/examples/backfill/backfillSampling.merge.ts @@ -0,0 +1,380 @@ +/* + This example implements an stdio MCP proxy that backfills sampling requests using the Claude API. + + Usage: + npx -y @modelcontextprotocol/inspector \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \ + npx -y --silent @modelcontextprotocol/server-everything +*/ + +import { Anthropic } from "@anthropic-ai/sdk"; +<<<<<<< Updated upstream +import { Base64ImageSource, ContentBlock, ContentBlockParam, MessageParam } from "@anthropic-ai/sdk/resources/messages.js"; +======= +import { + Base64ImageSource, + ContentBlock, + ContentBlockParam, + MessageParam, + Tool as ClaudeTool, + ToolChoiceAuto, + ToolChoiceAny, + ToolChoiceTool, + MessageCreateParamsBase, + ToolUseBlockParam +} from "@anthropic-ai/sdk/resources/messages.js"; +>>>>>>> Stashed changes +import { StdioServerTransport } from '../../server/stdio.js'; +import { StdioClientTransport } from '../../client/stdio.js'; +import { + CancelledNotification, + CancelledNotificationSchema, + isInitializeRequest, + isJSONRPCRequest, + ElicitRequest, + ElicitRequestSchema, + CreateMessageRequest, + CreateMessageRequestSchema, + CreateMessageResult, + JSONRPCResponse, + isInitializedNotification, + CallToolRequest, + CallToolRequestSchema, + isJSONRPCNotification, +<<<<<<< Updated upstream +======= + Tool, + ToolCallContent, + UserMessage, + AssistantMessage, + SamplingMessage, + ToolResultContent, +>>>>>>> Stashed changes +} from "../../types.js"; +import { Transport } from "../../shared/transport.js"; + +const DEFAULT_MAX_TOKENS = process.env.DEFAULT_MAX_TOKENS ? parseInt(process.env.DEFAULT_MAX_TOKENS) : 1000; + +// TODO: move to SDK + +const isCancelledNotification: (value: unknown) => value is CancelledNotification = + ((value: any) => CancelledNotificationSchema.safeParse(value).success) as any; + +const isCallToolRequest: (value: unknown) => value is CallToolRequest = + ((value: any) => CallToolRequestSchema.safeParse(value).success) as any; + +const isElicitRequest: (value: unknown) => value is ElicitRequest = + ((value: any) => ElicitRequestSchema.safeParse(value).success) as any; + +const isCreateMessageRequest: (value: unknown) => value is CreateMessageRequest = + ((value: any) => CreateMessageRequestSchema.safeParse(value).success) as any; + + +<<<<<<< Updated upstream +function contentToMcp(content: ContentBlock): CreateMessageResult['content'][number] { + switch (content.type) { + case 'text': + return {type: 'text', text: content.text}; + default: + throw new Error(`Unsupported content type: ${content.type}`); +======= +/** + * Converts MCP ToolChoice to Claude API tool_choice format + */ +function toolChoiceToClaudeFormat(toolChoice: CreateMessageRequest['params']['tool_choice']): ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool | undefined { + if (!toolChoice) { + return undefined; + } + + if (toolChoice.mode === "required") { + return { type: "any", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; + } + + // "auto" or undefined defaults to auto + return { type: "auto", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; +} + +function contentToMcp(content: ContentBlockParam): CreateMessageResult['content'] | SamplingMessage['content'] { + switch (content.type) { + case 'text': + return { type: 'text', text: content.text }; + case 'tool_result': + return { + type: 'tool_result', + toolUseId: content.tool_use_id, + content: content.content as any, + }; + case 'tool_use': + return { + type: 'tool_use', + id: content.id, + name: content.name, + input: content.input, + } as ToolCallContent; + default: + throw new Error(`[contentToMcp] Unsupported content type: ${(content as any).type}`); + } +} + +function stopReasonToMcp(reason: string | null): CreateMessageResult['stopReason'] { + switch (reason) { + case 'max_tokens': + return 'maxTokens'; + case 'stop_sequence': + return 'stopSequence'; + case 'tool_use': + return 'toolUse'; + case 'end_turn': + return 'endTurn'; + default: + return 'other'; +>>>>>>> Stashed changes + } +} + +function contentFromMcp(content: CreateMessageRequest['params']['messages'][number]['content']): ContentBlockParam { + switch (content.type) { + case 'text': + return {type: 'text', text: content.text}; + case 'image': + return { + type: 'image', + source: { + data: content.data, + media_type: content.mimeType as Base64ImageSource['media_type'], + type: 'base64', + }, + }; +<<<<<<< Updated upstream + case 'audio': + default: + throw new Error(`Unsupported content type: ${content.type}`); +======= + case 'tool_result': + // MCP ToolResultContent to Claude API tool_result + return { + type: 'tool_result', + tool_use_id: content.toolUseId, + content: JSON.stringify(content.content), // TODO + }; + case 'tool_use': + // MCP ToolCallContent to Claude API tool_use + return { + type: 'tool_use', + id: content.id, + name: content.name, + input: content.input, + }; + case 'audio': + default: + throw new Error(`[contentFromMcp] Unsupported content type: ${(content as any).type}`); +>>>>>>> Stashed changes + } +} + +export type NamedTransport = { + name: 'client' | 'server', + transport: T, +} + +export async function setupBackfill(client: NamedTransport, server: NamedTransport, api: Anthropic) { + const backfillMeta = await (async () => { + const models = new Set(); + let defaultModel: string | undefined; + for await (const info of api.models.list()) { + models.add(info.id); + if (info.id.indexOf('sonnet') >= 0 && defaultModel === undefined) { + defaultModel = info.id; + } + } + if (defaultModel === undefined) { + if (models.size === 0) { + throw new Error("No models available from the API"); + } + defaultModel = models.values().next().value; + } + return { + sampling_models: Array.from(models), + sampling_default_model: defaultModel, + }; + })(); + + function pickModel(preferences: CreateMessageRequest['params']['modelPreferences'] | undefined): string { + if (preferences?.hints) { + for (const hint of Object.values(preferences.hints)) { + if (hint.name !== undefined && backfillMeta.sampling_models.includes(hint.name)) { + return hint.name; + } + } + } + // TODO: linear model on preferences?.{intelligencePriority, speedPriority, costPriority} to pick betwen haiku, sonnet, opus. + return backfillMeta.sampling_default_model!; + } + + let clientSupportsSampling: boolean | undefined; + // let clientSupportsElicitation: boolean | undefined; + + const propagateMessage = (source: NamedTransport, target: NamedTransport) => { + source.transport.onmessage = async (message, extra) => { + console.error(`[proxy]: Message from ${source.name} transport: ${JSON.stringify(message)}; extra: ${JSON.stringify(extra)}`); + + if (isJSONRPCRequest(message)) { + console.error(`[proxy]: Detected JSON-RPC request: ${JSON.stringify(message)}`); + if (isInitializeRequest(message)) { + console.error(`[proxy]: Detected Initialize request: ${JSON.stringify(message)}`); + if (!(clientSupportsSampling = !!message.params.capabilities.sampling)) { + message.params.capabilities.sampling = {} + message.params._meta = {...(message.params._meta ?? {}), ...backfillMeta}; + } + } else if (isCreateMessageRequest(message)) {// && !clientSupportsSampling) { + console.error(`[proxy]: Detected CreateMessage request: ${JSON.stringify(message)}`); + if ((message.params.includeContext ?? 'none') !== 'none') { + const errorMessage = "includeContext != none not supported by MCP sampling backfill" + console.error(`[proxy]: ${errorMessage}`); + source.transport.send({ + jsonrpc: "2.0", + id: message.id, + error: { + code: -32601, // Method not found + message: errorMessage, + }, + }, {relatedRequestId: message.id}); + return; + } + + try { +<<<<<<< Updated upstream + // message.params. + const msg = await api.messages.create({ +======= + // Convert MCP tools to Claude API format if provided + const tools = message.params.tools?.map(toolToClaudeFormat); + const tool_choice = toolChoiceToClaudeFormat(message.params.tool_choice); + + const request = { +>>>>>>> Stashed changes + model: pickModel(message.params.modelPreferences), + system: message.params.systemPrompt === undefined ? undefined : [ + { + type: "text", + text: message.params.systemPrompt + }, + ], + messages: message.params.messages.map(({role, content}) => ({ + role, + content: [contentFromMcp(content)] + })), + max_tokens: message.params.maxTokens ?? DEFAULT_MAX_TOKENS, + temperature: message.params.temperature, + stop_sequences: message.params.stopSequences, +<<<<<<< Updated upstream + }); + + if (msg.content.length !== 1) { + throw new Error(`Expected exactly one content item in the response, got ${msg.content.length}`); +======= + // Add tool calling support + tools: tools && tools.length > 0 ? tools : undefined, + tool_choice: tool_choice, + ...(message.params.metadata ?? {}), + }; + console.error(`[proxy]: Claude API request: ${JSON.stringify(request)}`); + const msg = await api.messages.create(request); + + console.error(`[proxy]: Claude API response: ${JSON.stringify(msg)}`); + + // Claude can return multiple content blocks (e.g., text + tool_use) + // MCP currently supports single content block per message + // Priority: tool_use > text > other + let responseContent: CreateMessageResult['content']; + const toolUseBlock = msg.content.find(block => block.type === 'tool_use'); + if (toolUseBlock) { + responseContent = contentToMcp(toolUseBlock); + } else { + // Fall back to first content block (typically text) + if (msg.content.length === 0) { + throw new Error('Claude API returned no content blocks'); + } + responseContent = contentToMcp(msg.content[0]); +>>>>>>> Stashed changes + } + + source.transport.send({ + jsonrpc: "2.0", + id: message.id, + result: { + model: msg.model, +<<<<<<< Updated upstream + stopReason: msg.stop_reason, + role: msg.role, + content: contentToMcp(msg.content[0]), +======= + stopReason: stopReasonToMcp(msg.stop_reason), + role: 'assistant', // Always assistant in MCP responses + content: responseContent, +>>>>>>> Stashed changes + }, + }); + } catch (error) { + console.error(`[proxy]: Error processing message: ${(error as Error).message}`); + + source.transport.send({ + jsonrpc: "2.0", + id: message.id, + error: { + code: -32601, // Method not found + message: `Error processing message: ${(error as Error).message}`, + }, + }, {relatedRequestId: message.id}); + } + return; + // } else if (isElicitRequest(message) && !clientSupportsElicitation) { + // // TODO: form + // return; + } + } else if (isJSONRPCNotification(message)) { + if (isInitializedNotification(message) && source.name === 'server') { + if (!clientSupportsSampling) { + message.params = {...(message.params ?? {}), _meta: {...(message.params?._meta ?? {}), ...backfillMeta}}; + } + } + } + + try { + const relatedRequestId = isCancelledNotification(message)? message.params.requestId : undefined; + await target.transport.send(message, {relatedRequestId}); + } catch (error) { + console.error(`[proxy]: Error sending message to ${target.name}:`, error); + } + }; + }; + propagateMessage(server, client); + propagateMessage(client, server); + + const addErrorHandler = (transport: NamedTransport) => { + transport.transport.onerror = async (error: Error) => { + console.error(`[proxy]: Error from ${transport.name} transport:`, error); + }; + }; + + addErrorHandler(client); + addErrorHandler(server); + + await server.transport.start(); + await client.transport.start(); +} + +async function main() { + const args = process.argv.slice(2); + const client: NamedTransport = {name: 'client', transport: new StdioClientTransport({command: args[0], args: args.slice(1)})}; + const server: NamedTransport = {name: 'server', transport: new StdioServerTransport()}; + + const api = new Anthropic(); + await setupBackfill(client, server, api); + console.error("[proxy]: Transports started."); +} + +main().catch((error) => { + console.error("[proxy]: Fatal error:", error); + process.exit(1); +}); diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 413ecbe3b..36281feb2 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -204,7 +204,7 @@ async function executeLocalTool( async function runToolLoop( server: McpServer, initialQuery: string -): Promise { +): Promise<{ answer: string; transcript: SamplingMessage[] }> { const messages: SamplingMessage[] = [ { role: "user", @@ -238,50 +238,78 @@ async function runToolLoop( }); // Add assistant's response to message history - messages.push({ - role: "assistant", - content: response.content, - }); + // Note that SamplingMessage.content doesn't yet support arrays, so we flatten the content into multiple messages. + for (const content of (Array.isArray(response.content) ? response.content : [response.content])) { + messages.push({ + role: "assistant", + content, + }); + } - // Check if LLM wants to use a tool + // Check if LLM wants to use tools if (response.stopReason === "toolUse") { - const toolCall = response.content as ToolCallContent; + // Extract all tool_use content blocks + const contentArray = Array.isArray(response.content) ? response.content : [response.content]; + const toolCalls = contentArray.filter( + (content): content is ToolCallContent => content.type === "tool_use" + ); console.error( - `[toolLoop] LLM requested tool: ${toolCall.name} with input:`, - JSON.stringify(toolCall.input, null, 2) + `[toolLoop] LLM requested ${toolCalls.length} tool(s):`, + toolCalls.map(tc => `${tc.name}`).join(", ") ); - // Execute the requested tool locally - const toolResult = await executeLocalTool(toolCall.name, toolCall.input); + // Execute all tools in parallel + const toolResultPromises = toolCalls.map(async (toolCall) => { + console.error( + `[toolLoop] Executing tool: ${toolCall.name} with input:`, + JSON.stringify(toolCall.input, null, 2) + ); - console.error( - `[toolLoop] Tool result:`, - JSON.stringify(toolResult, null, 2) - ); + const result = await executeLocalTool(toolCall.name, toolCall.input); - // Add tool result to message history - messages.push({ - role: "user", - content: { - type: "tool_result", - toolUseId: toolCall.id, - content: toolResult, - }, + console.error( + `[toolLoop] Tool ${toolCall.name} result:`, + JSON.stringify(result, null, 2) + ); + + return { toolCall, result }; }); + const toolResults = await Promise.all(toolResultPromises); + + // Add all tool results to message history + for (const { toolCall, result } of toolResults) { + messages.push({ + role: "user", + content: { + type: "tool_result", + toolUseId: toolCall.id, + content: result, + }, + }); + } + // Continue the loop to get next response continue; } - // LLM provided final answer - if (response.content.type === "text") { - return response.content.text; + // LLM provided final answer (no tool use) + // Extract all text content blocks and concatenate them + const contentArray = Array.isArray(response.content) ? response.content : [response.content]; + const textBlocks = contentArray.filter( + (content): content is { type: "text"; text: string } => content.type === "text" + ); + + if (textBlocks.length > 0) { + const answer = textBlocks.map(block => block.text).join("\n\n"); + return { answer, transcript: messages }; } // Unexpected response type + const contentTypes = contentArray.map(c => c.type).join(", "); throw new Error( - `Unexpected response content type: ${response.content.type}` + `Unexpected response content types: ${contentTypes}` ); } @@ -313,15 +341,20 @@ mcpServer.registerTool( try { console.error(`[fileSearch] Processing query: ${query}`); - const result = await runToolLoop(mcpServer, query); + const { answer, transcript } = await runToolLoop(mcpServer, query); - console.error(`[fileSearch] Final result: ${result.substring(0, 200)}...`); + console.error(`[fileSearch] Final answer: ${answer.substring(0, 200)}...`); + console.error(`[fileSearch] Transcript length: ${transcript.length} messages`); return { content: [ { type: "text", - text: result, + text: answer, + }, + { + type: "text", + text: `\n\n--- Debug Transcript (${transcript.length} messages) ---\n${JSON.stringify(transcript, null, 2)}`, }, ], }; diff --git a/src/shared/transport-validator.ts b/src/shared/transport-validator.ts new file mode 100644 index 000000000..725e71f95 --- /dev/null +++ b/src/shared/transport-validator.ts @@ -0,0 +1,115 @@ +/* + +Proposal: +- Validate streamable http inside transport code itself. +- Validate protocol-level messages as a wrapper around Transport interface. + +*/ + +import { JSONRPCMessage, MessageExtraInfo } from "src/types.js" +import { Transport, TransportSendOptions } from "./transport.js" + +export type ProtocolLog = { + version?: string, + // startTimestamp?: number, + // endTimestamp?: number, + events: ({ + type: 'sent', + timestamp: number, + message: JSONRPCMessage, + options?: TransportSendOptions, + } | { + type: 'received', + timestamp: number, + message: JSONRPCMessage, + extra?: MessageExtraInfo, + } | { + type: 'start' | 'close', + timestamp: number, + } | { + type: 'error', + timestamp: number, + error: Error, + })[], +}; + +export type ProtocolChecker = (log: ProtocolLog) => void; + +// type StreamableHttpLog = { + +// } + + +class ProtocolValidator implements Transport { + private log: ProtocolLog = { + events: [] + } + + constructor(private transport: Transport, private checkers: ProtocolChecker[], private now = () => Date.now()) { + transport.onmessage = (message, extra) => { + this.addEvent({ + type: 'received', + timestamp: this.now(), + message, + extra, + }); + this.onmessage?.(message, extra); + }; + transport.onerror = (error) => { + this.addEvent({ + type: 'error', + timestamp: this.now(), + error, + }); + this.onerror?.(error); + }; + transport.onclose = () => { + this.addEvent({ + type: 'close', + timestamp: this.now(), + }); + this.onclose?.(); + }; + } + + private check() { + for (const checker of this.checkers) { + checker(this.log); + } + } + + private addEvent(event: ProtocolLog['events'][number]) { + this.log.events.push(event); + this.check(); + } + + start(): Promise { + this.addEvent({ + type: 'start', + timestamp: this.now(), + }); + return this.transport.start() + } + + send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { + this.addEvent({ + type: 'sent', + timestamp: this.now(), + message, + options, + }); + return this.transport.send(message, options) + } + + close(): Promise { + throw new Error("Method not implemented.") + } + + onclose?: (() => void) | undefined + onerror?: ((error: Error) => void) | undefined + onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined + + sessionId?: string | undefined + + setProtocolVersion?: ((version: string) => void) | undefined +} \ No newline at end of file diff --git a/tmp2/client.mjs b/tmp2/client.mjs new file mode 100644 index 000000000..c3a383b12 --- /dev/null +++ b/tmp2/client.mjs @@ -0,0 +1,43 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +const transport = new StdioClientTransport({ + command: "uvx", + args:[ + "--quiet", + "--refresh", + "git+https://github.com/emsi/slow-mcp", + "--transport", + "stdio", +] +}); + +const client = new Client( + { + name: "example-client", + version: "1.0.0" + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {} + } + } +); + +await client.connect(transport); + +const tools = await client.listTools(); + +console.log(tools); + +// Call a tool +const result = await client.callTool({ + name: "run_command", +}, undefined, { + timeout: 300000, +}); + + +console.log(result); diff --git a/tmp2/client.py b/tmp2/client.py new file mode 100755 index 000000000..cecaffc8f --- /dev/null +++ b/tmp2/client.py @@ -0,0 +1,66 @@ +#!/usr/bin/env uv run +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "mcp", +# ] +# /// +import datetime +from mcp import ClientSession, StdioServerParameters, types +from mcp.client.stdio import stdio_client + +# Create server parameters for stdio connection +server_params = StdioServerParameters( + command="uvx", # Executable + args=[ + "--quiet", + "--refresh", + "git+https://github.com/emsi/slow-mcp", + "--transport", + "stdio", + ], + env=None, # Optional environment variables +) + + +# Optional: create a sampling callback +async def handle_sampling_message( + message: types.CreateMessageRequestParams, +) -> types.CreateMessageResult: + return types.CreateMessageResult( + role="assistant", + content=types.TextContent( + type="text", + text="Hello, world! from model", + ), + model="gpt-3.5-turbo", + stopReason="endTurn", + ) + + +async def run(): + async with stdio_client(server_params) as (read, write): + async with ClientSession( + read, write, #sampling_callback=handle_sampling_message + read_timeout_seconds=datetime.timedelta(seconds=60), + ) as session: + # Initialize the connection + await session.initialize() + + resources = await session.list_resources() + + # List available tools + tools = await session.list_tools() + + print(f"Tools: {tools}") + + # Call a tool + result = await session.call_tool("run_command") + + print(f"Result: {result}") + + +if __name__ == "__main__": + import asyncio + + asyncio.run(run()) diff --git a/tmp2/issue766.ts b/tmp2/issue766.ts new file mode 100644 index 000000000..b4aea6beb --- /dev/null +++ b/tmp2/issue766.ts @@ -0,0 +1,165 @@ +import express from 'express'; +import { z } from 'zod'; +import cors from 'cors'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { isInitializeRequest, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { randomUUID } from 'node:crypto'; + +const LogLevelMap = { + emergency: 0, + alert: 1, + critical: 2, + error: 3, + warning: 4, + notice: 5, + info: 6, + debug: 7, +} +const validLogLevels = Object.keys(LogLevelMap); + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +// Store transports by session ID to send notifications +const transports = {}; + +const getServer = () => { + // Create an MCP server with implementation details + const server = new McpServer({ + name: 'stateless-streamable-http-server', + version: '1.0.0', + }, { capabilities: { logging: {} } }); + + server.tool( + "sum", + { a: z.number(), b: z.number() }, + async ({ a, b }) => { + server.server.sendLoggingMessage({ level: "debug", data: { message: "Received input", a, b } }); + await sleep(1000); + const result = a + b; + server.server.sendLoggingMessage({ level: "info", data: { message: "Sum calculated", result } }); + return { + content: [{ type: "text", text: "Result: " + result }], + }; + } + ); + server.server.setRequestHandler( + SetLevelRequestSchema, + async (request) => { + const levelName = request.params.level; + if (validLogLevels.includes(levelName)) { + server.server.sendLoggingMessage({ level: "debug", data: { message: "Set root log level to " + levelName } }); + } else { + server.server.sendLoggingMessage({ level: "warning", data: { message: "Invalid log level " + levelName + " received" } }); + } + return {}; + } + ); + + return server; +} + +const app = express(); +app.use(express.json()); + +// Configure CORS to expose Mcp-Session-Id header for browser-based clients +app.use(cors({ + origin: '*', // Allow all origins - adjust as needed for production + exposedHeaders: ['Mcp-Session-Id'], +})); + +const server = getServer(); +app.post('/mcp', async (req, res) => { + console.log('Received MCP POST request:', req.body); + try { + // Check for existing session ID + const sessionId = req.headers['mcp-session-id']; + let transport; + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId) => { + // Store the transport by session ID when session is initialized + // This avoids race conditions where requests might come in before the session is stored + console.log("Session initialized with ID: ", sessionId); + transports[sessionId] = transport; + } + }); + + // Connect the transport to the MCP server + await server.connect(transport); + + // Handle the request - the onsessioninitialized callback will store the transport + await transport.handleRequest(req, res, req.body); + return; // Already handled + } else { + // Invalid request - no session ID or not initialization request + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }); + return; + } + + // Handle the request with existing transport + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } +}); + +// Handle GET requests for SSE streams (now using built-in support from StreamableHTTP) +app.get('/mcp', async (req, res) => { + console.log('Received MCP GET request:', req); + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + console.log("Establishing SSE stream for session", sessionId); + const transport = transports[sessionId]; + await transport.handleRequest(req, res); +}); + +// Start the server +const PORT = 3000; +app.listen(PORT, (error) => { + if (error) { + console.error('Failed to start server:', error); + process.exit(1); + } + console.log("MCP Streamable HTTP Server listening on port", PORT); +}); + +// Handle server shutdown +process.on('SIGINT', async () => { + console.log('Shutting down server...'); + process.exit(0); +}); + +/* +Refs: + +https://www.mcpevals.io/blog/mcp-logging-tutorial +https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/server/standaloneSseWithGetStreamableHttp.ts + +*/` \ No newline at end of file diff --git a/tmp2/package-lock.json b/tmp2/package-lock.json new file mode 100644 index 000000000..a1991ddb4 --- /dev/null +++ b/tmp2/package-lock.json @@ -0,0 +1,1051 @@ +{ + "name": "tmp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tmp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.5" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/wrappy/1.0.2/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/tmp2/package.json b/tmp2/package.json new file mode 100644 index 000000000..8959cf3b0 --- /dev/null +++ b/tmp2/package.json @@ -0,0 +1,15 @@ +{ + "name": "tmp", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.5" + } +} From 2ed11ea587a3987ca4088391496d6d391faad96d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:29:46 +0100 Subject: [PATCH 13/33] updated result content to be a content or content array --- .../backfill/backfillSampling.merge.ts | 380 ------------------ src/examples/backfill/backfillSampling.ts | 111 ++--- src/examples/server/toolWithSampleServer.ts | 12 +- src/types.ts | 20 +- 4 files changed, 84 insertions(+), 439 deletions(-) delete mode 100644 src/examples/backfill/backfillSampling.merge.ts diff --git a/src/examples/backfill/backfillSampling.merge.ts b/src/examples/backfill/backfillSampling.merge.ts deleted file mode 100644 index 678c1791e..000000000 --- a/src/examples/backfill/backfillSampling.merge.ts +++ /dev/null @@ -1,380 +0,0 @@ -/* - This example implements an stdio MCP proxy that backfills sampling requests using the Claude API. - - Usage: - npx -y @modelcontextprotocol/inspector \ - npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \ - npx -y --silent @modelcontextprotocol/server-everything -*/ - -import { Anthropic } from "@anthropic-ai/sdk"; -<<<<<<< Updated upstream -import { Base64ImageSource, ContentBlock, ContentBlockParam, MessageParam } from "@anthropic-ai/sdk/resources/messages.js"; -======= -import { - Base64ImageSource, - ContentBlock, - ContentBlockParam, - MessageParam, - Tool as ClaudeTool, - ToolChoiceAuto, - ToolChoiceAny, - ToolChoiceTool, - MessageCreateParamsBase, - ToolUseBlockParam -} from "@anthropic-ai/sdk/resources/messages.js"; ->>>>>>> Stashed changes -import { StdioServerTransport } from '../../server/stdio.js'; -import { StdioClientTransport } from '../../client/stdio.js'; -import { - CancelledNotification, - CancelledNotificationSchema, - isInitializeRequest, - isJSONRPCRequest, - ElicitRequest, - ElicitRequestSchema, - CreateMessageRequest, - CreateMessageRequestSchema, - CreateMessageResult, - JSONRPCResponse, - isInitializedNotification, - CallToolRequest, - CallToolRequestSchema, - isJSONRPCNotification, -<<<<<<< Updated upstream -======= - Tool, - ToolCallContent, - UserMessage, - AssistantMessage, - SamplingMessage, - ToolResultContent, ->>>>>>> Stashed changes -} from "../../types.js"; -import { Transport } from "../../shared/transport.js"; - -const DEFAULT_MAX_TOKENS = process.env.DEFAULT_MAX_TOKENS ? parseInt(process.env.DEFAULT_MAX_TOKENS) : 1000; - -// TODO: move to SDK - -const isCancelledNotification: (value: unknown) => value is CancelledNotification = - ((value: any) => CancelledNotificationSchema.safeParse(value).success) as any; - -const isCallToolRequest: (value: unknown) => value is CallToolRequest = - ((value: any) => CallToolRequestSchema.safeParse(value).success) as any; - -const isElicitRequest: (value: unknown) => value is ElicitRequest = - ((value: any) => ElicitRequestSchema.safeParse(value).success) as any; - -const isCreateMessageRequest: (value: unknown) => value is CreateMessageRequest = - ((value: any) => CreateMessageRequestSchema.safeParse(value).success) as any; - - -<<<<<<< Updated upstream -function contentToMcp(content: ContentBlock): CreateMessageResult['content'][number] { - switch (content.type) { - case 'text': - return {type: 'text', text: content.text}; - default: - throw new Error(`Unsupported content type: ${content.type}`); -======= -/** - * Converts MCP ToolChoice to Claude API tool_choice format - */ -function toolChoiceToClaudeFormat(toolChoice: CreateMessageRequest['params']['tool_choice']): ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool | undefined { - if (!toolChoice) { - return undefined; - } - - if (toolChoice.mode === "required") { - return { type: "any", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; - } - - // "auto" or undefined defaults to auto - return { type: "auto", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; -} - -function contentToMcp(content: ContentBlockParam): CreateMessageResult['content'] | SamplingMessage['content'] { - switch (content.type) { - case 'text': - return { type: 'text', text: content.text }; - case 'tool_result': - return { - type: 'tool_result', - toolUseId: content.tool_use_id, - content: content.content as any, - }; - case 'tool_use': - return { - type: 'tool_use', - id: content.id, - name: content.name, - input: content.input, - } as ToolCallContent; - default: - throw new Error(`[contentToMcp] Unsupported content type: ${(content as any).type}`); - } -} - -function stopReasonToMcp(reason: string | null): CreateMessageResult['stopReason'] { - switch (reason) { - case 'max_tokens': - return 'maxTokens'; - case 'stop_sequence': - return 'stopSequence'; - case 'tool_use': - return 'toolUse'; - case 'end_turn': - return 'endTurn'; - default: - return 'other'; ->>>>>>> Stashed changes - } -} - -function contentFromMcp(content: CreateMessageRequest['params']['messages'][number]['content']): ContentBlockParam { - switch (content.type) { - case 'text': - return {type: 'text', text: content.text}; - case 'image': - return { - type: 'image', - source: { - data: content.data, - media_type: content.mimeType as Base64ImageSource['media_type'], - type: 'base64', - }, - }; -<<<<<<< Updated upstream - case 'audio': - default: - throw new Error(`Unsupported content type: ${content.type}`); -======= - case 'tool_result': - // MCP ToolResultContent to Claude API tool_result - return { - type: 'tool_result', - tool_use_id: content.toolUseId, - content: JSON.stringify(content.content), // TODO - }; - case 'tool_use': - // MCP ToolCallContent to Claude API tool_use - return { - type: 'tool_use', - id: content.id, - name: content.name, - input: content.input, - }; - case 'audio': - default: - throw new Error(`[contentFromMcp] Unsupported content type: ${(content as any).type}`); ->>>>>>> Stashed changes - } -} - -export type NamedTransport = { - name: 'client' | 'server', - transport: T, -} - -export async function setupBackfill(client: NamedTransport, server: NamedTransport, api: Anthropic) { - const backfillMeta = await (async () => { - const models = new Set(); - let defaultModel: string | undefined; - for await (const info of api.models.list()) { - models.add(info.id); - if (info.id.indexOf('sonnet') >= 0 && defaultModel === undefined) { - defaultModel = info.id; - } - } - if (defaultModel === undefined) { - if (models.size === 0) { - throw new Error("No models available from the API"); - } - defaultModel = models.values().next().value; - } - return { - sampling_models: Array.from(models), - sampling_default_model: defaultModel, - }; - })(); - - function pickModel(preferences: CreateMessageRequest['params']['modelPreferences'] | undefined): string { - if (preferences?.hints) { - for (const hint of Object.values(preferences.hints)) { - if (hint.name !== undefined && backfillMeta.sampling_models.includes(hint.name)) { - return hint.name; - } - } - } - // TODO: linear model on preferences?.{intelligencePriority, speedPriority, costPriority} to pick betwen haiku, sonnet, opus. - return backfillMeta.sampling_default_model!; - } - - let clientSupportsSampling: boolean | undefined; - // let clientSupportsElicitation: boolean | undefined; - - const propagateMessage = (source: NamedTransport, target: NamedTransport) => { - source.transport.onmessage = async (message, extra) => { - console.error(`[proxy]: Message from ${source.name} transport: ${JSON.stringify(message)}; extra: ${JSON.stringify(extra)}`); - - if (isJSONRPCRequest(message)) { - console.error(`[proxy]: Detected JSON-RPC request: ${JSON.stringify(message)}`); - if (isInitializeRequest(message)) { - console.error(`[proxy]: Detected Initialize request: ${JSON.stringify(message)}`); - if (!(clientSupportsSampling = !!message.params.capabilities.sampling)) { - message.params.capabilities.sampling = {} - message.params._meta = {...(message.params._meta ?? {}), ...backfillMeta}; - } - } else if (isCreateMessageRequest(message)) {// && !clientSupportsSampling) { - console.error(`[proxy]: Detected CreateMessage request: ${JSON.stringify(message)}`); - if ((message.params.includeContext ?? 'none') !== 'none') { - const errorMessage = "includeContext != none not supported by MCP sampling backfill" - console.error(`[proxy]: ${errorMessage}`); - source.transport.send({ - jsonrpc: "2.0", - id: message.id, - error: { - code: -32601, // Method not found - message: errorMessage, - }, - }, {relatedRequestId: message.id}); - return; - } - - try { -<<<<<<< Updated upstream - // message.params. - const msg = await api.messages.create({ -======= - // Convert MCP tools to Claude API format if provided - const tools = message.params.tools?.map(toolToClaudeFormat); - const tool_choice = toolChoiceToClaudeFormat(message.params.tool_choice); - - const request = { ->>>>>>> Stashed changes - model: pickModel(message.params.modelPreferences), - system: message.params.systemPrompt === undefined ? undefined : [ - { - type: "text", - text: message.params.systemPrompt - }, - ], - messages: message.params.messages.map(({role, content}) => ({ - role, - content: [contentFromMcp(content)] - })), - max_tokens: message.params.maxTokens ?? DEFAULT_MAX_TOKENS, - temperature: message.params.temperature, - stop_sequences: message.params.stopSequences, -<<<<<<< Updated upstream - }); - - if (msg.content.length !== 1) { - throw new Error(`Expected exactly one content item in the response, got ${msg.content.length}`); -======= - // Add tool calling support - tools: tools && tools.length > 0 ? tools : undefined, - tool_choice: tool_choice, - ...(message.params.metadata ?? {}), - }; - console.error(`[proxy]: Claude API request: ${JSON.stringify(request)}`); - const msg = await api.messages.create(request); - - console.error(`[proxy]: Claude API response: ${JSON.stringify(msg)}`); - - // Claude can return multiple content blocks (e.g., text + tool_use) - // MCP currently supports single content block per message - // Priority: tool_use > text > other - let responseContent: CreateMessageResult['content']; - const toolUseBlock = msg.content.find(block => block.type === 'tool_use'); - if (toolUseBlock) { - responseContent = contentToMcp(toolUseBlock); - } else { - // Fall back to first content block (typically text) - if (msg.content.length === 0) { - throw new Error('Claude API returned no content blocks'); - } - responseContent = contentToMcp(msg.content[0]); ->>>>>>> Stashed changes - } - - source.transport.send({ - jsonrpc: "2.0", - id: message.id, - result: { - model: msg.model, -<<<<<<< Updated upstream - stopReason: msg.stop_reason, - role: msg.role, - content: contentToMcp(msg.content[0]), -======= - stopReason: stopReasonToMcp(msg.stop_reason), - role: 'assistant', // Always assistant in MCP responses - content: responseContent, ->>>>>>> Stashed changes - }, - }); - } catch (error) { - console.error(`[proxy]: Error processing message: ${(error as Error).message}`); - - source.transport.send({ - jsonrpc: "2.0", - id: message.id, - error: { - code: -32601, // Method not found - message: `Error processing message: ${(error as Error).message}`, - }, - }, {relatedRequestId: message.id}); - } - return; - // } else if (isElicitRequest(message) && !clientSupportsElicitation) { - // // TODO: form - // return; - } - } else if (isJSONRPCNotification(message)) { - if (isInitializedNotification(message) && source.name === 'server') { - if (!clientSupportsSampling) { - message.params = {...(message.params ?? {}), _meta: {...(message.params?._meta ?? {}), ...backfillMeta}}; - } - } - } - - try { - const relatedRequestId = isCancelledNotification(message)? message.params.requestId : undefined; - await target.transport.send(message, {relatedRequestId}); - } catch (error) { - console.error(`[proxy]: Error sending message to ${target.name}:`, error); - } - }; - }; - propagateMessage(server, client); - propagateMessage(client, server); - - const addErrorHandler = (transport: NamedTransport) => { - transport.transport.onerror = async (error: Error) => { - console.error(`[proxy]: Error from ${transport.name} transport:`, error); - }; - }; - - addErrorHandler(client); - addErrorHandler(server); - - await server.transport.start(); - await client.transport.start(); -} - -async function main() { - const args = process.argv.slice(2); - const client: NamedTransport = {name: 'client', transport: new StdioClientTransport({command: args[0], args: args.slice(1)})}; - const server: NamedTransport = {name: 'server', transport: new StdioServerTransport()}; - - const api = new Anthropic(); - await setupBackfill(client, server, api); - console.error("[proxy]: Transports started."); -} - -main().catch((error) => { - console.error("[proxy]: Fatal error:", error); - process.exit(1); -}); diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 3255f3369..1b9621500 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -8,7 +8,15 @@ */ import { Anthropic } from "@anthropic-ai/sdk"; -import { Base64ImageSource, ContentBlock, ContentBlockParam, MessageParam, Tool as ClaudeTool, ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool } from "@anthropic-ai/sdk/resources/messages.js"; +import { + Base64ImageSource, + ContentBlock, + ContentBlockParam, + Tool as ClaudeTool, + ToolChoiceAuto, + ToolChoiceAny, + ToolChoiceTool, +} from "@anthropic-ai/sdk/resources/messages.js"; import { StdioServerTransport } from '../../server/stdio.js'; import { StdioClientTransport } from '../../client/stdio.js'; import { @@ -28,9 +36,6 @@ import { isJSONRPCNotification, Tool, ToolCallContent, - ToolResultContent, - UserMessage, - AssistantMessage, } from "../../types.js"; import { Transport } from "../../shared/transport.js"; @@ -89,14 +94,29 @@ function contentToMcp(content: ContentBlock): CreateMessageResult['content'] { input: content.input, } as ToolCallContent; default: - throw new Error(`Unsupported content type: ${(content as any).type}`); + throw new Error(`[contentToMcp] Unsupported content type: ${(content as any).type}`); + } +} + +function stopReasonToMcp(reason: string | null): CreateMessageResult['stopReason'] { + switch (reason) { + case 'max_tokens': + return 'maxTokens'; + case 'stop_sequence': + return 'stopSequence'; + case 'tool_use': + return 'toolUse'; + case 'end_turn': + return 'endTurn'; + default: + return 'other'; } } -function contentFromMcp(content: UserMessage['content'] | AssistantMessage['content']): ContentBlockParam { +function contentFromMcp(content: CreateMessageRequest['params']['messages'][number]['content']): ContentBlockParam { switch (content.type) { case 'text': - return { type: 'text', text: content.text }; + return {type: 'text', text: content.text}; case 'image': return { type: 'image', @@ -107,15 +127,21 @@ function contentFromMcp(content: UserMessage['content'] | AssistantMessage['cont }, }; case 'tool_result': - // MCP ToolResultContent to Claude API tool_result return { type: 'tool_result', tool_use_id: content.toolUseId, - content: JSON.stringify(content.content), + content: JSON.stringify(content.content), // TODO + }; + case 'tool_use': + return { + type: 'tool_use', + id: content.id, + name: content.name, + input: content.input, }; case 'audio': default: - throw new Error(`Unsupported content type: ${(content as any).type}`); + throw new Error(`[contentFromMcp] Unsupported content type: ${(content as any).type}`); } } @@ -166,23 +192,30 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo console.error(`[proxy]: Message from ${source.name} transport: ${JSON.stringify(message)}; extra: ${JSON.stringify(extra)}`); if (isJSONRPCRequest(message)) { + + const sendInternalError = (errorMessage: string) => { + console.error(`[proxy -> ${source.name}]: Error: ${errorMessage}`); + source.transport.send({ + jsonrpc: "2.0", + id: message.id, + error: { + code: -32603, // Internal error + message: errorMessage, + }, + }, {relatedRequestId: message.id}); + }; + + console.error(`[proxy]: Detected JSON-RPC request: ${JSON.stringify(message)}`); if (isInitializeRequest(message)) { + console.error(`[proxy]: Detected Initialize request: ${JSON.stringify(message)}`); if (!(clientSupportsSampling = !!message.params.capabilities.sampling)) { message.params.capabilities.sampling = {} message.params._meta = {...(message.params._meta ?? {}), ...backfillMeta}; } - } else if (isCreateMessageRequest(message)) { + } else if (isCreateMessageRequest(message)) {// && !clientSupportsSampling) { + console.error(`[proxy]: Detected CreateMessage request: ${JSON.stringify(message)}`); if ((message.params.includeContext ?? 'none') !== 'none') { - const errorMessage = "includeContext != none not supported by MCP sampling backfill" - console.error(`[proxy]: ${errorMessage}`); - source.transport.send({ - jsonrpc: "2.0", - id: message.id, - error: { - code: -32601, // Method not found - message: errorMessage, - }, - }, {relatedRequestId: message.id}); + sendInternalError("includeContext != none not supported by MCP sampling backfill"); return; } @@ -199,7 +232,7 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo text: message.params.systemPrompt }, ], - messages: message.params.messages.map(({role, content}) => ({ + messages: message.params.messages.map(({role, content}) => ({ role, content: [contentFromMcp(content)] })), @@ -211,41 +244,16 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo ...(message.params.metadata ?? {}), }); - // Claude can return multiple content blocks (e.g., text + tool_use) - // MCP currently supports single content block per message - // Priority: tool_use > text > other - let responseContent: CreateMessageResult['content']; - const toolUseBlock = msg.content.find(block => block.type === 'tool_use'); - if (toolUseBlock) { - responseContent = contentToMcp(toolUseBlock); - } else { - // Fall back to first content block (typically text) - if (msg.content.length === 0) { - throw new Error('Claude API returned no content blocks'); - } - responseContent = contentToMcp(msg.content[0]); - } - - // Map stop reasons from Claude to MCP format - let stopReason: CreateMessageResult['stopReason'] = msg.stop_reason as any; - if (msg.stop_reason === 'tool_use') { - stopReason = 'toolUse'; - } else if (msg.stop_reason === 'max_tokens') { - stopReason = 'maxTokens'; - } else if (msg.stop_reason === 'end_turn') { - stopReason = 'endTurn'; - } else if (msg.stop_reason === 'stop_sequence') { - stopReason = 'stopSequence'; - } + console.error(`[proxy]: Claude API response: ${JSON.stringify(msg)}`); source.transport.send({ jsonrpc: "2.0", id: message.id, result: { model: msg.model, - stopReason: stopReason, + stopReason: stopReasonToMcp(msg.stop_reason), role: 'assistant', // Always assistant in MCP responses - content: responseContent, + content: (Array.isArray(msg.content) ? msg.content : [msg.content]).map(contentToMcp), }, }); } catch (error) { @@ -261,9 +269,6 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo }, {relatedRequestId: message.id}); } return; - // } else if (isElicitRequest(message) && !clientSupportsElicitation) { - // // TODO: form - // return; } } else if (isJSONRPCNotification(message)) { if (isInitializedNotification(message) && source.name === 'server') { diff --git a/src/examples/server/toolWithSampleServer.ts b/src/examples/server/toolWithSampleServer.ts index 44e5cecbb..961e5f516 100644 --- a/src/examples/server/toolWithSampleServer.ts +++ b/src/examples/server/toolWithSampleServer.ts @@ -34,11 +34,21 @@ mcpServer.registerTool( maxTokens: 500, }); + // Extract all text content blocks from the response + const parts: string[] = []; + for (const content of Array.isArray(response.content) ? response.content : [response.content]) { + if (content.type === "text") { + parts.push(content.text); + } else { + throw new Error(`Unexpected content type: ${content.type}`); + } + } + return { content: [ { type: "text", - text: response.content.type === "text" ? response.content.text : "Unable to generate summary", + text: parts.join('\n'), }, ], }; diff --git a/src/types.ts b/src/types.ts index 6085d760e..d0e540a25 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1358,11 +1358,21 @@ export const CreateMessageResultSchema = ResultSchema.extend({ /** * Response content. May be ToolCallContent if stopReason is "toolUse". */ - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, + content: z.union([ + z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), + z.array( + z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, + ]), + ) ]), }); From f5832642cf59f2a0db13f19627cff646f64e5567 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:35:10 +0100 Subject: [PATCH 14/33] feat(examples): Add logging feedback for tool loop operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added sendLoggingMessage calls to provide real-time feedback on tool loop operations: - Log iteration number at the start of each loop - Log number and names of tools being executed - Log completion message with total iteration count Also fixed toolWithSampleServer.ts to handle CreateMessageResult.content as arrays (extract and concatenate all text blocks). This provides visibility into the tool loop's progress for debugging and monitoring purposes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/examples/server/toolLoopSampling.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 36281feb2..49b96854b 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -228,6 +228,12 @@ async function runToolLoop( while (iteration < MAX_ITERATIONS) { iteration++; + // Log iteration start + await server.sendLoggingMessage({ + level: "info", + data: `Tool loop iteration ${iteration}/${MAX_ITERATIONS}`, + }); + // Request message from LLM with available tools const response: CreateMessageResult = await server.server.createMessage({ messages, @@ -254,6 +260,13 @@ async function runToolLoop( (content): content is ToolCallContent => content.type === "tool_use" ); + // Log tool invocations + const toolNames = toolCalls.map(tc => tc.name).join(", "); + await server.sendLoggingMessage({ + level: "info", + data: `Executing ${toolCalls.length} tool(s): ${toolNames}`, + }); + console.error( `[toolLoop] LLM requested ${toolCalls.length} tool(s):`, toolCalls.map(tc => `${tc.name}`).join(", ") @@ -303,6 +316,13 @@ async function runToolLoop( if (textBlocks.length > 0) { const answer = textBlocks.map(block => block.text).join("\n\n"); + + // Log completion + await server.sendLoggingMessage({ + level: "info", + data: `Tool loop completed after ${iteration} iteration(s)`, + }); + return { answer, transcript: messages }; } From c4a4582d5e39ccba5140625796325ab0082c331f Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:40:33 +0100 Subject: [PATCH 15/33] feat(examples): Add line range support and tool-specific logging to toolLoopSampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added line range support to the read tool: - Added optional startLineInclusive and endLineInclusive parameters - Returns numbered lines when range is specified - Validates line ranges and provides helpful error messages Improved logging with tool-specific messages: - Loop iteration logs: "Loop iteration N: X tool invocation(s) requested" - Ripgrep logs: "Searching pattern 'X' under Y" - Read logs: "Reading file X" or "Reading file X (lines A-B)" Updated tool descriptions: - Added hint to read tool about requesting context lines around matches - Emphasized that ripgrep output includes line numbers This provides better visibility into tool operations and enables more efficient file reading by fetching only relevant line ranges. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/examples/server/toolLoopSampling.ts | 86 ++++++++++++++++++++----- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 49b96854b..11fcc31ee 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -38,6 +38,8 @@ const RipgrepInputSchema = z.object({ const ReadInputSchema = z.object({ path: z.string(), + startLineInclusive: z.number().int().positive().optional(), + endLineInclusive: z.number().int().positive().optional(), }); /** @@ -61,10 +63,16 @@ function ensureSafePath(inputPath: string): string { * Returns search results as a string. */ async function executeRipgrep( + server: McpServer, pattern: string, path: string ): Promise<{ output?: string; error?: string }> { try { + await server.sendLoggingMessage({ + level: "info", + data: `Searching pattern "${pattern}" under ${path}`, + }); + const safePath = ensureSafePath(path); return new Promise((resolve) => { @@ -108,15 +116,54 @@ async function executeRipgrep( } /** - * Reads a file from the filesystem. + * Reads a file from the filesystem, optionally within a line range. * Returns file contents as a string. */ async function executeRead( - path: string + server: McpServer, + path: string, + startLineInclusive?: number, + endLineInclusive?: number ): Promise<{ content?: string; error?: string }> { try { + // Log the read operation + if (startLineInclusive !== undefined || endLineInclusive !== undefined) { + await server.sendLoggingMessage({ + level: "info", + data: `Reading file ${path} (lines ${startLineInclusive ?? 1}-${endLineInclusive ?? "end"})`, + }); + } else { + await server.sendLoggingMessage({ + level: "info", + data: `Reading file ${path}`, + }); + } + const safePath = ensureSafePath(path); const content = await readFile(safePath, "utf-8"); + const lines = content.split("\n"); + + // If line range specified, extract only those lines + if (startLineInclusive !== undefined || endLineInclusive !== undefined) { + const start = (startLineInclusive ?? 1) - 1; // Convert to 0-indexed + const end = endLineInclusive ?? lines.length; // Default to end of file + + if (start < 0 || start >= lines.length) { + return { error: `Start line ${startLineInclusive} is out of bounds (file has ${lines.length} lines)` }; + } + if (end < start) { + return { error: `End line ${endLineInclusive} is before start line ${startLineInclusive}` }; + } + + const selectedLines = lines.slice(start, end); + // Add line numbers to output + const numberedContent = selectedLines + .map((line, idx) => `${start + idx + 1}: ${line}`) + .join("\n"); + + return { content: numberedContent }; + } + return { content }; } catch (error) { return { @@ -151,7 +198,9 @@ const LOCAL_TOOLS: Tool[] = [ { name: "read", description: - "Read the contents of a file. Use this to examine files found by ripgrep.", + "Read the contents of a file. Use this to examine files found by ripgrep. " + + "You can optionally specify a line range to read only specific lines. " + + "Tip: When ripgrep finds matches, note the line numbers and request a few lines before and after for context.", inputSchema: { type: "object", properties: { @@ -159,6 +208,14 @@ const LOCAL_TOOLS: Tool[] = [ type: "string", description: "The file path to read (relative to current directory)", }, + startLineInclusive: { + type: "number", + description: "Optional: First line to read (1-indexed, inclusive). Use with endLineInclusive to read a specific range.", + }, + endLineInclusive: { + type: "number", + description: "Optional: Last line to read (1-indexed, inclusive). If not specified, reads to end of file.", + }, }, required: ["path"], }, @@ -169,6 +226,7 @@ const LOCAL_TOOLS: Tool[] = [ * Executes a local tool and returns the result. */ async function executeLocalTool( + server: McpServer, toolName: string, toolInput: Record ): Promise> { @@ -176,11 +234,16 @@ async function executeLocalTool( switch (toolName) { case "ripgrep": { const validated = RipgrepInputSchema.parse(toolInput); - return await executeRipgrep(validated.pattern, validated.path); + return await executeRipgrep(server, validated.pattern, validated.path); } case "read": { const validated = ReadInputSchema.parse(toolInput); - return await executeRead(validated.path); + return await executeRead( + server, + validated.path, + validated.startLineInclusive, + validated.endLineInclusive + ); } default: return { error: `Unknown tool: ${toolName}` }; @@ -228,12 +291,6 @@ async function runToolLoop( while (iteration < MAX_ITERATIONS) { iteration++; - // Log iteration start - await server.sendLoggingMessage({ - level: "info", - data: `Tool loop iteration ${iteration}/${MAX_ITERATIONS}`, - }); - // Request message from LLM with available tools const response: CreateMessageResult = await server.server.createMessage({ messages, @@ -260,11 +317,10 @@ async function runToolLoop( (content): content is ToolCallContent => content.type === "tool_use" ); - // Log tool invocations - const toolNames = toolCalls.map(tc => tc.name).join(", "); + // Log iteration with tool invocation count await server.sendLoggingMessage({ level: "info", - data: `Executing ${toolCalls.length} tool(s): ${toolNames}`, + data: `Loop iteration ${iteration}: ${toolCalls.length} tool invocation(s) requested`, }); console.error( @@ -279,7 +335,7 @@ async function runToolLoop( JSON.stringify(toolCall.input, null, 2) ); - const result = await executeLocalTool(toolCall.name, toolCall.input); + const result = await executeLocalTool(server, toolCall.name, toolCall.input); console.error( `[toolLoop] Tool ${toolCall.name} result:`, From a924cfa81fb38e7ac70d4faa9ed2934ab2d1c2b0 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:43:56 +0100 Subject: [PATCH 16/33] rm unrelated changes --- intermediate-findings/client-sampling-api.md | 776 --------- intermediate-findings/issue-876-analysis.md | 101 -- .../protocol-spec-research.md | 586 ------- intermediate-findings/sampling-analysis.md | 750 --------- .../sampling-examples-review.md | 794 --------- .../sampling-tool-additions.md | 632 -------- intermediate-findings/sep-1577-spec.md | 1429 ----------------- intermediate-findings/test-analysis.md | 664 -------- .../test-patterns-analysis.md | 794 --------- .../toolLoopSampling-review.md | 542 ------- .../toolLoopSampling-test-review.md | 249 --- intermediate-findings/transport-analysis.md | 960 ----------- 12 files changed, 8277 deletions(-) delete mode 100644 intermediate-findings/client-sampling-api.md delete mode 100644 intermediate-findings/issue-876-analysis.md delete mode 100644 intermediate-findings/protocol-spec-research.md delete mode 100644 intermediate-findings/sampling-analysis.md delete mode 100644 intermediate-findings/sampling-examples-review.md delete mode 100644 intermediate-findings/sampling-tool-additions.md delete mode 100644 intermediate-findings/sep-1577-spec.md delete mode 100644 intermediate-findings/test-analysis.md delete mode 100644 intermediate-findings/test-patterns-analysis.md delete mode 100644 intermediate-findings/toolLoopSampling-review.md delete mode 100644 intermediate-findings/toolLoopSampling-test-review.md delete mode 100644 intermediate-findings/transport-analysis.md diff --git a/intermediate-findings/client-sampling-api.md b/intermediate-findings/client-sampling-api.md deleted file mode 100644 index 6ce239d18..000000000 --- a/intermediate-findings/client-sampling-api.md +++ /dev/null @@ -1,776 +0,0 @@ -# Client Sampling API Documentation - -## Overview - -This document describes how to use the MCP TypeScript SDK Client API to handle `sampling/createMessage` requests. The sampling capability allows MCP servers to request language model completions from clients, enabling servers to use AI capabilities without directly accessing LLM APIs. - -## Table of Contents - -1. [Setup and Configuration](#setup-and-configuration) -2. [Request Handler Registration](#request-handler-registration) -3. [Handler Signature and Parameters](#handler-signature-and-parameters) -4. [Request Structure](#request-structure) -5. [Response Construction](#response-construction) -6. [Content Types](#content-types) -7. [Tool Calling Support](#tool-calling-support) -8. [Complete Examples](#complete-examples) -9. [Best Practices and Gotchas](#best-practices-and-gotchas) - ---- - -## Setup and Configuration - -### 1. Declare Sampling Capability - -To handle sampling requests, you must declare the `sampling` capability when creating the client: - -```typescript -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; - -const client = new Client( - { - name: "my-client", - version: "1.0.0", - }, - { - capabilities: { - sampling: {}, // Required to handle sampling/createMessage requests - }, - } -); -``` - -**Important:** Without declaring the `sampling` capability, calling `setRequestHandler` with `CreateMessageRequestSchema` will throw an error: -``` -Error: Client does not support sampling capability (required for sampling/createMessage) -``` - ---- - -## Request Handler Registration - -### Method: `client.setRequestHandler()` - -The `setRequestHandler` method is used to register a handler for incoming `sampling/createMessage` requests. - -```typescript -import { CreateMessageRequestSchema, CreateMessageResult } from "@modelcontextprotocol/sdk/types.js"; - -client.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - // Handler implementation - return result; // CreateMessageResult - } -); -``` - -### Parameters - -1. **Schema**: `CreateMessageRequestSchema` - Zod schema defining the request structure -2. **Handler**: Async function with signature: - ```typescript - (request: CreateMessageRequest, extra: RequestHandlerExtra) => Promise | CreateMessageResult - ``` - ---- - -## Handler Signature and Parameters - -### Handler Function Signature - -```typescript -async function handler( - request: CreateMessageRequest, - extra: RequestHandlerExtra -): Promise -``` - -### Request Parameter (`CreateMessageRequest`) - -The request object contains: - -```typescript -interface CreateMessageRequest { - method: "sampling/createMessage"; - params: { - // Required: Array of conversation messages - messages: SamplingMessage[]; - - // Required: Maximum tokens to generate - maxTokens: number; - - // Optional: System prompt for the LLM - systemPrompt?: string; - - // Optional: Temperature parameter (0-1) - temperature?: number; - - // Optional: Stop sequences - stopSequences?: string[]; - - // Optional: Tools available to the LLM - tools?: Tool[]; - - // Optional: Tool choice configuration - tool_choice?: ToolChoice; - - // Optional: Model preferences/hints - modelPreferences?: ModelPreferences; - - // Optional: Metadata - metadata?: Record; - - // DEPRECATED: Context inclusion preference - includeContext?: "none" | "thisServer" | "allServers"; - - // Internal metadata - _meta?: Record; - }; -} -``` - -### Extra Parameter (`RequestHandlerExtra`) - -The `extra` object provides additional context and utilities: - -```typescript -interface RequestHandlerExtra { - // Abort signal for cancellation - signal: AbortSignal; - - // JSON-RPC request ID - requestId: RequestId; - - // Session ID from transport (if available) - sessionId?: string; - - // Authentication info (if available) - authInfo?: AuthInfo; - - // Request metadata - _meta?: RequestMeta; - - // Original HTTP request info (if applicable) - requestInfo?: RequestInfo; - - // Send a notification related to this request - sendNotification: (notification: SendNotificationT) => Promise; - - // Send a request related to this request - sendRequest: >( - request: SendRequestT, - resultSchema: U, - options?: RequestOptions - ) => Promise>; - - // Elicit input from user (if elicitation capability enabled) - elicitInput?: (request: { - message: string; - requestedSchema?: object; - }) => Promise; -} -``` - -**Key fields:** -- `signal`: Use to detect if the request was cancelled -- `requestId`: Useful for logging/tracking -- `sendNotification`: Send progress updates or other notifications -- `sendRequest`: Make requests back to the server (if needed) - ---- - -## Response Construction - -### CreateMessageResult Structure - -```typescript -interface CreateMessageResult { - // Required: Name of the model used - model: string; - - // Required: Response role (always "assistant") - role: "assistant"; - - // Required: Response content (discriminated union) - content: TextContent | ImageContent | AudioContent | ToolCallContent; - - // Optional: Why sampling stopped - stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | "refusal" | "other" | string; -} -``` - -### Basic Text Response Example - -```typescript -const result: CreateMessageResult = { - model: "claude-3-5-sonnet-20241022", - role: "assistant", - content: { - type: "text", - text: "This is the LLM's response" - }, - stopReason: "endTurn" -}; -``` - ---- - -## Content Types - -### 1. TextContent - -Plain text response from the LLM. - -```typescript -interface TextContent { - type: "text"; - text: string; - _meta?: Record; -} -``` - -**Example:** -```typescript -content: { - type: "text", - text: "The capital of France is Paris." -} -``` - -### 2. ImageContent - -Image data (base64 encoded). - -```typescript -interface ImageContent { - type: "image"; - data: string; // Base64 encoded image data - mimeType: string; // e.g., "image/png", "image/jpeg" - _meta?: Record; -} -``` - -**Example:** -```typescript -content: { - type: "image", - data: "iVBORw0KGgoAAAANSUhEUgAA...", - mimeType: "image/png" -} -``` - -### 3. AudioContent - -Audio data (base64 encoded). - -```typescript -interface AudioContent { - type: "audio"; - data: string; // Base64 encoded audio data - mimeType: string; // e.g., "audio/wav", "audio/mp3" - _meta?: Record; -} -``` - -### 4. ToolCallContent (Tool Use) - -Request to call a tool. Used when the LLM wants to invoke a tool. - -```typescript -interface ToolCallContent { - type: "tool_use"; - id: string; // Unique ID for this tool call - name: string; // Tool name - input: Record; // Tool arguments - _meta?: Record; -} -``` - -**Example:** -```typescript -content: { - type: "tool_use", - id: "toolu_01A09q90qw90lq917835lq9", - name: "get_weather", - input: { - location: "San Francisco, CA", - unit: "celsius" - } -} -``` - -When returning `ToolCallContent`, you should typically set `stopReason: "toolUse"`. - ---- - -## Tool Calling Support - -### Overview - -The sampling API supports tool calling, allowing the LLM to use tools provided by the server. This enables agentic behavior where the LLM can: -1. Decide to use a tool -2. Return a tool call request -3. Receive tool results -4. Continue the conversation - -### Tool Definition - -Tools are provided in the request's `params.tools` array: - -```typescript -interface Tool { - name: string; - description?: string; - inputSchema: { - type: "object"; - properties: Record; - required?: string[]; - // JSON Schema for tool inputs - }; - outputSchema?: { - // Optional JSON Schema for tool outputs - }; -} -``` - -### Tool Choice Configuration - -The `tool_choice` parameter controls how the LLM should use tools: - -```typescript -interface ToolChoice { - mode: "auto" | "required" | "tool"; - disable_parallel_tool_use?: boolean; - toolName?: string; // Required when mode is "tool" -} -``` - -**Modes:** -- `"auto"`: LLM decides whether to use tools -- `"required"`: LLM must use at least one tool -- `"tool"`: LLM must use a specific tool (specified by `toolName`) - -### Tool Use Flow - -1. **Server sends request with tools:** -```typescript -{ - messages: [...], - maxTokens: 1000, - tools: [ - { - name: "get_weather", - description: "Get current weather", - inputSchema: { - type: "object", - properties: { - location: { type: "string" } - } - } - } - ], - tool_choice: { mode: "auto" } -} -``` - -2. **Client returns tool use response:** -```typescript -{ - model: "claude-3-5-sonnet-20241022", - role: "assistant", - content: { - type: "tool_use", - id: "toolu_123", - name: "get_weather", - input: { location: "Paris" } - }, - stopReason: "toolUse" -} -``` - -3. **Server executes tool and adds result to messages:** -```typescript -{ - role: "user", - content: { - type: "tool_result", - toolUseId: "toolu_123", - content: { temperature: 20, condition: "sunny" } - } -} -``` - -4. **Server sends another request with updated messages** (tool loop continues until LLM provides final answer) - ---- - -## Complete Examples - -### Example 1: Basic Text Response Handler - -```typescript -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { CreateMessageRequestSchema, CreateMessageResult } from "@modelcontextprotocol/sdk/types.js"; - -const client = new Client( - { name: "basic-client", version: "1.0.0" }, - { capabilities: { sampling: {} } } -); - -client.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - // Check if cancelled - if (extra.signal.aborted) { - throw new Error("Request was cancelled"); - } - - console.log(`Handling sampling request with ${request.params.messages.length} messages`); - - // In a real implementation, call your LLM API here - const response = await callYourLLMAPI({ - messages: request.params.messages, - maxTokens: request.params.maxTokens, - systemPrompt: request.params.systemPrompt, - temperature: request.params.temperature, - }); - - const result: CreateMessageResult = { - model: response.model, - role: "assistant", - content: { - type: "text", - text: response.text, - }, - stopReason: response.stopReason, - }; - - return result; - } -); -``` - -### Example 2: Tool Calling Handler - -```typescript -import { Anthropic } from "@anthropic-ai/sdk"; - -const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); - -client.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - // Convert MCP messages to Anthropic format - const messages = request.params.messages.map(msg => ({ - role: msg.role, - content: convertContent(msg.content) - })); - - // Convert tools to Anthropic format - const tools = request.params.tools?.map(tool => ({ - name: tool.name, - description: tool.description || "", - input_schema: tool.inputSchema, - })); - - // Call Anthropic API - const response = await anthropic.messages.create({ - model: "claude-3-5-sonnet-20241022", - system: request.params.systemPrompt, - messages: messages, - max_tokens: request.params.maxTokens, - temperature: request.params.temperature, - tools: tools, - }); - - // Convert response to MCP format - let content: CreateMessageResult['content']; - let stopReason: CreateMessageResult['stopReason']; - - // Check if LLM wants to use a tool - const toolUseBlock = response.content.find(block => block.type === 'tool_use'); - if (toolUseBlock) { - content = { - type: "tool_use", - id: toolUseBlock.id, - name: toolUseBlock.name, - input: toolUseBlock.input, - }; - stopReason = "toolUse"; - } else { - // Regular text response - const textBlock = response.content.find(block => block.type === 'text'); - content = { - type: "text", - text: textBlock?.text || "", - }; - stopReason = response.stop_reason === "end_turn" ? "endTurn" : response.stop_reason; - } - - return { - model: response.model, - role: "assistant", - content, - stopReason, - }; - } -); -``` - -### Example 3: Handler with Cancellation Support - -```typescript -client.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - // Set up cancellation handling - const controller = new AbortController(); - extra.signal.addEventListener('abort', () => { - controller.abort(); - }); - - try { - const response = await fetch('https://your-llm-api.com/v1/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - messages: request.params.messages, - max_tokens: request.params.maxTokens, - }), - signal: controller.signal, - }); - - const data = await response.json(); - - return { - model: data.model, - role: "assistant", - content: { - type: "text", - text: data.text, - }, - stopReason: "endTurn", - }; - } catch (error) { - if (error.name === 'AbortError') { - throw new Error('Request was cancelled'); - } - throw error; - } - } -); -``` - -### Example 4: Handler with Progress Notifications - -```typescript -client.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - // Send progress notification - await extra.sendNotification({ - method: "notifications/progress", - params: { - progressToken: extra.requestId, - progress: 0.5, - total: 1.0, - } - }); - - // Perform LLM call... - const response = await callLLM(request.params); - - // Send completion notification - await extra.sendNotification({ - method: "notifications/progress", - params: { - progressToken: extra.requestId, - progress: 1.0, - total: 1.0, - } - }); - - return { - model: response.model, - role: "assistant", - content: { - type: "text", - text: response.text, - }, - stopReason: "endTurn", - }; - } -); -``` - ---- - -## Best Practices and Gotchas - -### Best Practices - -1. **Always Declare Capabilities** - - Declare the `sampling` capability in client options before calling `setRequestHandler` - - Failure to do so will throw an error at registration time - -2. **Validate Input** - - The SDK automatically validates the request structure via Zod schemas - - Additional validation of your own business logic should be added - -3. **Handle Cancellation** - - Always check `extra.signal.aborted` before expensive operations - - Forward the abort signal to your LLM API calls - - Clean up resources when cancelled - -4. **Use Appropriate Stop Reasons** - - `"endTurn"`: Natural completion - - `"stopSequence"`: Hit a stop sequence - - `"maxTokens"`: Reached token limit - - `"toolUse"`: When returning ToolCallContent - - `"refusal"`: Model refused the request - - `"other"`: Provider-specific reasons - -5. **Tool Calling Patterns** - - When returning `ToolCallContent`, set `stopReason: "toolUse"` - - Generate unique IDs for each tool call (e.g., using UUID) - - The server will handle executing tools and continuing the conversation - -6. **Error Handling** - - Throw descriptive errors that will be returned to the server - - The Protocol layer will automatically convert thrown errors to JSON-RPC error responses - - Use `McpError` for MCP-specific errors with error codes - -7. **Model Selection** - - Use `request.params.modelPreferences` to select appropriate models - - Fall back to a default model if preferences don't match available models - - Return the actual model name used in the response - -### Common Gotchas - -1. **Missing Capability Declaration** - ```typescript - // ❌ Wrong - will throw error - const client = new Client({ name: "client", version: "1.0.0" }); - client.setRequestHandler(CreateMessageRequestSchema, handler); - - // ✅ Correct - const client = new Client( - { name: "client", version: "1.0.0" }, - { capabilities: { sampling: {} } } - ); - client.setRequestHandler(CreateMessageRequestSchema, handler); - ``` - -2. **Wrong Content Type for Stop Reason** - ```typescript - // ❌ Wrong - stopReason doesn't match content type - return { - content: { type: "text", text: "..." }, - stopReason: "toolUse" // Should be "endTurn" for text - }; - - // ✅ Correct - return { - content: { type: "tool_use", id: "...", name: "...", input: {} }, - stopReason: "toolUse" - }; - ``` - -3. **Not Handling All Message Types** - - `SamplingMessage` can be either `UserMessage` or `AssistantMessage` - - `UserMessage.content` can be: `TextContent`, `ImageContent`, `AudioContent`, or `ToolResultContent` - - `AssistantMessage.content` can be: `TextContent`, `ImageContent`, `AudioContent`, or `ToolCallContent` - - Make sure your LLM API supports all content types in the messages - -4. **Forgetting Role Field** - ```typescript - // ❌ Wrong - missing role - return { - model: "claude-3-5-sonnet-20241022", - content: { type: "text", text: "..." } - }; - - // ✅ Correct - return { - model: "claude-3-5-sonnet-20241022", - role: "assistant", // Always "assistant" - content: { type: "text", text: "..." } - }; - ``` - -5. **Not Propagating Tool Definitions** - - When tools are provided in `request.params.tools`, pass them to your LLM API - - Tools must be in the format expected by your LLM provider - - Convert between MCP and provider-specific tool formats - -6. **Incorrect Tool Result Format** - - Tool results come as `ToolResultContent` in user messages - - The `content` field is an object (not an array) - - Match `toolUseId` with the `id` from `ToolCallContent` - -7. **Handler Registration Order** - - Register handlers before calling `client.connect()` - - Handlers can only be set once per method (subsequent calls replace the handler) - - Use `client.removeRequestHandler(method)` to remove a handler - -8. **Message History Management** - - The `messages` array contains the full conversation history - - Each message has a `role` ("user" or "assistant") and `content` - - Tool use creates a cycle: assistant tool_use → user tool_result → assistant response - -### Type Safety - -The SDK uses Zod schemas for runtime validation and TypeScript for compile-time type safety: - -```typescript -// Request is automatically typed -client.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - // ✅ TypeScript knows the structure - request.params.messages.forEach(msg => { - if (msg.role === "user") { - // msg.content can be text, image, audio, or tool_result - } else { - // msg.content can be text, image, audio, or tool_use - } - }); - - // ✅ Return type is validated - return { - model: "...", - role: "assistant", - content: { type: "text", text: "..." }, - stopReason: "endTurn", - }; - } -); -``` - ---- - -## Related Resources - -- **MCP Specification**: [https://modelcontextprotocol.io/docs/specification](https://modelcontextprotocol.io/docs/specification) -- **Client API Source**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` -- **Protocol Base**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.ts` -- **Type Definitions**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` -- **Example: Tool Loop Sampling**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.ts` -- **Example: Backfill Proxy**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/backfill/backfillSampling.ts` - ---- - -## Summary - -The Client Sampling API allows MCP clients to handle `sampling/createMessage` requests from servers, enabling servers to use LLM capabilities without direct API access. Key points: - -1. Declare `sampling` capability in client options -2. Register handler using `setRequestHandler(CreateMessageRequestSchema, handler)` -3. Handler receives request with messages, tools, and parameters -4. Return `CreateMessageResult` with model, role, content, and stopReason -5. Support text responses and tool calling -6. Handle cancellation via `extra.signal` -7. Match content types with stop reasons - -This API enables powerful patterns like tool loops, agent-based search, and delegated LLM access in MCP architectures. diff --git a/intermediate-findings/issue-876-analysis.md b/intermediate-findings/issue-876-analysis.md deleted file mode 100644 index 8f32121e0..000000000 --- a/intermediate-findings/issue-876-analysis.md +++ /dev/null @@ -1,101 +0,0 @@ -# GitHub Issue #876 Analysis: SSE Connection 5-Minute Timeout - -## Issue Summary - -**Problem**: SSE (Server-Sent Events) connections always close after 5 minutes, despite attempts to configure longer timeouts. - -**Root Cause**: Operating system-level network connection timeouts kill inactive connections after 5 minutes. This is an OS-level limitation, not an application-level issue. - -**User's Experience**: -- The user reported that `res.on('close')` is triggered after exactly 5 minutes -- They attempted to set a longer timeout using `callTool(xx, undefined, {timeout: 20mins})` but this did not prevent the 5-minute disconnect -- The timeout configuration did not work as expected because the OS kills the connection at the network layer - -## Technical Details from GitHub Issue - -1. **The Problem**: SSE connections terminate after 5 minutes of inactivity -2. **Failed Solution**: Setting application-level timeouts (e.g., `{timeout: 20mins}`) doesn't prevent OS-level network timeouts -3. **Provided Solution**: MCP team member (antonpk1) provided a comprehensive workaround - -## Workaround Solution Provided - -**Key Insight**: Send periodic "heartbeat" messages to keep the connection alive and prevent OS timeout. - -**Implementation Strategy**: -1. Send regular "notifications/progress" messages during long-running operations -2. Use periodic notifications (e.g., every 30 seconds) to maintain connection activity -3. Successfully demonstrated a 20-minute task with periodic progress updates - -**Sample Code Pattern** (from the GitHub issue): -- Implement periodic progress notifications to prevent connection timeout -- Send notifications every 30 seconds during long operations -- This keeps the SSE connection active and prevents the 5-minute OS timeout - -## Current SDK Implementation Analysis - -### Timeout Handling in Protocol Layer - -From `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.ts`: - -1. **Default Timeout**: `DEFAULT_REQUEST_TIMEOUT_MSEC = 60000` (60 seconds) -2. **Request Options**: Support for timeout configuration: - - `timeout?: number` - Request-specific timeout in milliseconds - - `resetTimeoutOnProgress?: boolean` - Reset timeout when progress notifications are received - - `maxTotalTimeout?: number` - Maximum total time regardless of progress - -3. **Progress Support**: The SDK has built-in support for progress notifications: - - `onprogress?: ProgressCallback` - Callback for progress notifications - - `ProgressNotification` handling in the protocol layer - - Automatic timeout reset when `resetTimeoutOnProgress` is enabled - -### SSE Implementation - -From `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/sse.ts` and `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/sse.ts`: - -1. **Client SSE Transport**: Uses EventSource API for receiving messages, HTTP POST for sending -2. **Server SSE Transport**: Sends messages via SSE stream, receives via HTTP POST handlers -3. **No Built-in Keepalive**: The current SSE implementation does not include automatic keepalive/heartbeat functionality - -### Current Client Implementation Note - -In `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` (line 436): -```typescript -console.error("Calling tool", params, options, options?.timeout); -``` -This debug log shows the client does receive and process timeout options. - -## Gap Analysis - -**What's Missing**: -1. **No automatic keepalive mechanism** in SSE transport implementations -2. **No built-in progress notification sending** for long-running operations -3. **Documentation** about the 5-minute OS timeout limitation and workarounds - -**What Exists**: -1. **Progress notification support** in the protocol layer -2. **Timeout reset on progress** functionality (`resetTimeoutOnProgress`) -3. **Flexible timeout configuration** per request - -## Recommended Implementation - -Based on the GitHub issue resolution, the SDK should: - -1. **Add automatic keepalive option** to SSE transport classes -2. **Provide helper utilities** for sending periodic progress notifications -3. **Document the 5-minute limitation** and workaround patterns -4. **Include example code** showing how to implement progress notifications for long-running operations - -## Impact Assessment - -- **Current State**: Users experience unexpected disconnects after 5 minutes -- **Workaround Exists**: Manual progress notification implementation works -- **SDK Enhancement Needed**: Built-in keepalive and better documentation would improve developer experience - -## Test Coverage - -The test suite in `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.test.ts` includes: -- Timeout error handling tests -- Progress notification preservation tests -- But no specific tests for long-duration connections or keepalive functionality - -This analysis confirms that GitHub issue #876 identifies a real OS-level limitation that affects SSE connections, and the MCP team has provided a viable workaround using progress notifications to maintain connection activity. \ No newline at end of file diff --git a/intermediate-findings/protocol-spec-research.md b/intermediate-findings/protocol-spec-research.md deleted file mode 100644 index 8c870ab28..000000000 --- a/intermediate-findings/protocol-spec-research.md +++ /dev/null @@ -1,586 +0,0 @@ -# Model Context Protocol (MCP) Specification Research - -## Research Date: 2025-10-01 - -## Executive Summary - -This document provides comprehensive research on the Model Context Protocol (MCP) specification, focusing on protocol requirements, message format specifications, JSON-RPC 2.0 compliance, validation requirements, and security considerations. The research reveals significant gaps in current validation implementation, particularly around JSON-RPC 2.0 message format validation at the transport/protocol level. - ---- - -## 1. Official Protocol Specification - -### Primary Sources - -- **Official Specification**: https://modelcontextprotocol.io/specification/2025-06-18 -- **GitHub Repository**: https://github.com/modelcontextprotocol/modelcontextprotocol -- **TypeScript SDK**: https://github.com/modelcontextprotocol/typescript-sdk -- **Security Best Practices**: https://modelcontextprotocol.io/specification/draft/basic/security_best_practices - -### Protocol Overview - -MCP is an open protocol that enables seamless integration between LLM applications and external data sources and tools. The protocol: - -- **Built on JSON-RPC 2.0**: All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification -- **Stateful Session Protocol**: Focuses on context exchange and sampling coordination between clients and servers -- **Component-Based Architecture**: Defines multiple optional components (resources, prompts, tools, sampling, roots, elicitation) -- **Transport-Agnostic**: Supports multiple transport mechanisms (stdio, SSE, Streamable HTTP, WebSocket) - ---- - -## 2. JSON-RPC 2.0 Compliance Requirements - -### Core JSON-RPC 2.0 Specification - -Source: https://www.jsonrpc.org/specification - -All MCP implementations MUST comply with the JSON-RPC 2.0 specification, which defines three fundamental message types: - -#### 2.1 Request Object Requirements - -A valid JSON-RPC 2.0 Request **MUST** contain: - -1. **`jsonrpc`**: A String specifying the version of the JSON-RPC protocol. **MUST** be exactly `"2.0"`. - -2. **`method`**: A String containing the name of the method to be invoked. - - Method names beginning with `rpc.` are reserved for JSON-RPC internal methods and extensions - - **MUST NOT** be used for application-specific methods - -3. **`params`**: A Structured value (Array or Object) holding parameter values. - - This member **MAY** be omitted. - -4. **`id`**: An identifier established by the Client. - - **MUST** contain a String, Number, or NULL value if included - - If not included, the message is assumed to be a **Notification** - - **MCP Deviation**: The MCP specification states that the ID **MUST NOT** be null - -#### 2.2 Response Object Requirements - -When an RPC call is made, the Server **MUST** reply with a Response, except for Notifications. - -A valid Response **MUST** contain: - -1. **`jsonrpc`**: **MUST** be exactly `"2.0"`. - -2. **`result`**: This member is **REQUIRED** on success. - - This member **MUST NOT** exist if there was an error. - -3. **`error`**: This member is **REQUIRED** on error. - - This member **MUST NOT** exist if there was no error. - - **MUST** contain an error object with: - - `code` (Number): Integer error code - - `message` (String): Short error description - - `data` (Any, optional): Additional error details - -4. **`id`**: This member is **REQUIRED**. - - It **MUST** be the same as the value of the `id` member in the Request Object. - - If there was an error in detecting the id in the Request object, it MUST be Null. - -#### 2.3 Notification Requirements - -A Notification is a Request object without an `id` member. - -- The receiver **MUST NOT** send a response to a Notification. -- Notifications **MUST NOT** include an `id` member. -- Used for one-way messages that do not expect a response. - -#### 2.4 Batch Request Support - -JSON-RPC 2.0 supports sending multiple Request objects in an Array: - -- MCP implementations **MAY** support sending JSON-RPC batches -- MCP implementations **MUST** support receiving JSON-RPC batches -- The Server **MAY** process batch requests concurrently -- Responses can be returned in any order -- No Response is sent for Notifications in a batch - -#### 2.5 Standard Error Codes - -The JSON-RPC 2.0 specification defines the following standard error codes: - -| Code | Message | Meaning | -|------|---------|---------| -| -32700 | Parse error | Invalid JSON was received by the server | -| -32600 | Invalid Request | The JSON sent is not a valid Request object | -| -32601 | Method not found | The method does not exist / is not available | -| -32602 | Invalid params | Invalid method parameter(s) | -| -32603 | Internal error | Internal JSON-RPC error | -| -32000 to -32099 | Server error | Reserved for implementation-defined server-errors | - ---- - -## 3. MCP Protocol Requirements - -### 3.1 Message Format Requirements - -All messages between MCP clients and servers **MUST**: - -1. Follow the JSON-RPC 2.0 specification -2. Use JSON (RFC 4627) as the data format -3. Include the `jsonrpc: "2.0"` version field -4. Follow the appropriate structure for requests, responses, or notifications - -### 3.2 Response Requirements - -- Responses **MUST** include the same ID as the request they correspond to -- Either a `result` or an `error` **MUST** be set -- A response **MUST NOT** set both `result` and `error` -- Results **MAY** follow any JSON object structure -- Errors **MUST** include an error code (integer) and message (string) at minimum - -### 3.3 Transport Requirements - -From the specification: - -> Implementers choosing to support custom transport mechanisms must ensure they preserve the JSON-RPC message format and lifecycle requirements defined by MCP. - -All implementations **MUST**: -- Support the base protocol and lifecycle management components -- Preserve JSON-RPC message format across all transports -- Support receiving JSON-RPC batches (even if sending batches is not supported) - -### 3.4 Core Component Requirements - -- **Base Protocol**: All implementations **MUST** support -- **Lifecycle Management**: All implementations **MUST** support -- **Other Components** (Resources, Prompts, Tools, etc.): **MAY** be implemented based on application needs - ---- - -## 4. Validation Requirements - -### 4.1 Protocol-Level Validation - -Based on the specification and best practices, MCP implementations should rigorously validate: - -#### Message Structure Validation - -1. **JSON-RPC Version**: Verify `jsonrpc === "2.0"` -2. **Required Fields**: Ensure all required fields are present -3. **Field Types**: Validate that fields have correct types -4. **ID Consistency**: Ensure response IDs match request IDs -5. **Mutual Exclusivity**: Verify responses don't have both `result` and `error` -6. **Notification Structure**: Ensure notifications don't have `id` field - -#### Parameter Validation - -From the specification: -> Use JSON Schema validation on both client and server sides to catch type mismatches early and provide helpful error messages. - -#### Security-Related Validation - -From security best practices: -> Servers should rigorously validate incoming MCP messages against the protocol specification (structure, field consistency, recursion depth) to prevent malformed request attacks. - -### 4.2 Error Handling Requirements - -**Standard JSON-RPC Errors**: Servers should return standard JSON-RPC errors for common failure cases: - -- **-32700 (Parse error)**: Invalid JSON received -- **-32600 (Invalid Request)**: Missing required fields or invalid structure -- **-32601 (Method not found)**: Unknown method -- **-32602 (Invalid params)**: Parameter validation failed -- **-32603 (Internal error)**: Server-side processing error - -**Error Message Strategy**: -- Parameter validation with detailed error messages -- Error messages should help the LLM understand what went wrong -- Include suggestions for corrective actions - -**Timeout Requirements**: -> Implementations should implement appropriate timeouts for all requests, to prevent hung connections and resource exhaustion. - ---- - -## 5. Security Implications of Protocol Validation - -### 5.1 Critical Security Requirements - -From the official security best practices documentation: - -#### Authentication and Authorization - -**MUST Requirements**: -- MCP servers that implement authorization **MUST** verify all inbound requests -- MCP Servers **MUST NOT** use sessions for authentication -- MCP servers **MUST NOT** accept any tokens that were not explicitly issued for the MCP server -- "Token passthrough" (accepting tokens without validation) is explicitly **FORBIDDEN** - -#### Session Security - -- MCP servers **MUST** use secure, non-deterministic session IDs -- Generated session IDs (e.g., UUIDs) **SHOULD** use secure random number generators - -#### Input/Output Validation - -From security guidelines: -> Security for agent-tool protocols must start with strong auth, scoped permissions, and input/output validation. Developers should implement allow-lists, schema validations, and content filters. - -### 5.2 Attack Vectors and Mitigation - -#### Identified Vulnerabilities - -1. **Malformed Request Attacks** - - **Risk**: Servers that don't validate message structure can be exploited - - **Mitigation**: Rigorous validation of structure, field consistency, recursion depth - -2. **Prompt Injection** - - **Risk**: AI systems accepting untrusted user input with hidden prompts - - **Mitigation**: Input validation, content filtering, careful handling of user data - - **Note**: Modern exploits center on "lethal trifecta": privileged access + untrusted input + exfiltration channel - -3. **Confused Deputy Problem** - - **Risk**: MCP server acts on behalf of wrong principal - - **Mitigation**: Strict authorization checks, proper token validation - -4. **Session Hijacking** - - **Risk**: Attacker takes over legitimate session - - **Mitigation**: Secure session ID generation, session binding to user information - -5. **OAuth Phishing (Issue #544)** - - **Risk**: Insufficient authentication mechanisms allow fake MCP servers - - **Recommendation**: Add "resource" parameter to OAuth flow, validate server addresses - - **Note**: Security flaws should be addressed "at the protocol level" rather than relying on user awareness - -### 5.3 Best Practices for Validation - -#### Development Security - -- **SAST/SCA**: Build on pipelines implementing Static Application Security Testing and Software Composition Analysis -- **Dependency Management**: Identify and fix known vulnerabilities in dependencies - -#### Logging and Monitoring - -> Every time the AI uses a tool via MCP, the system should log who/what invoked it, which tool, with what parameters, and what result came back, with logs stored securely. - -#### User Consent - -From the specification: -> Users must explicitly consent to and understand all data access and operations - -Implementations **MUST** obtain explicit user consent before: -- Exposing user data to servers -- Invoking any tools -- Performing LLM sampling - -#### Local Server Configuration Safeguards - -- Display full command details before execution -- Require explicit user consent -- Highlight potentially dangerous command patterns -- Sandbox server execution -- Restrict system/file system access -- Limit network privileges - ---- - -## 6. Current TypeScript SDK Implementation Analysis - -### 6.1 Existing Validation - -The TypeScript SDK currently implements: - -1. **Zod-based Type Validation**: Uses Zod schemas for runtime type checking -2. **Message Type Guards**: Functions like `isJSONRPCRequest()`, `isJSONRPCResponse()`, etc. -3. **Application-Level Validation**: Input schema validation for tools, resources, and prompts -4. **Error Code Support**: Defines standard JSON-RPC error codes in `ErrorCode` enum - -### 6.2 Identified Gaps - -#### Critical Issue: Invalid JSON-RPC Validation (Issue #563) - -**Problem**: Some invalid JSON-RPC requests do not generate error responses as specified in the JSON-RPC 2.0 specification. - -**Example**: A request with an incorrect method property (e.g., `"method_"` instead of `"method"`) returns nothing instead of an error. - -**Expected Behavior**: -- For malformed requests: Return error code -32600 (Invalid Request) -- For invalid params: Return error code -32602 (Invalid params) - -**Current Limitation**: -- Invalid requests do not reach application error handling -- Developers cannot implement validation logic due to lack of error responses - -#### Transport-Level Validation Issues - -1. **No Validation at Transport Boundary**: The `Transport` interface doesn't enforce JSON-RPC validation -2. **Protocol Class Assumes Valid Messages**: The `Protocol` class (in `protocol.ts`) uses type guards but doesn't respond with errors for invalid messages -3. **Missing Parse Error Handling**: No handling for -32700 (Parse error) when invalid JSON is received - -#### Message Handling in Protocol Class - -Looking at `protocol.ts` lines 314-328: - -```typescript -this._transport.onmessage = (message, extra) => { - _onmessage?.(message, extra); - if (isJSONRPCResponse(message) || isJSONRPCError(message)) { - this._onresponse(message); - } else if (isJSONRPCRequest(message)) { - this._onrequest(message, extra); - } else if (isJSONRPCNotification(message)) { - this._onnotification(message); - } else { - this._onerror( - new Error(`Unknown message type: ${JSON.stringify(message)}`), - ); - } -}; -``` - -**Issue**: When a message doesn't match any type guard, it calls `_onerror()` but doesn't send a JSON-RPC error response back to the sender. - -### 6.3 Incomplete Implementation: transport-validator.ts - -The file `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/transport-validator.ts` exists but is incomplete: - -- Contains a proposal for validation -- Has a `ProtocolValidator` class that wraps Transport -- Implements logging but not actual validation -- The `close()` method throws "Method not implemented" -- No actual protocol checkers are implemented - -This suggests validation work was started but not completed. - ---- - -## 7. Recommendations for Implementation - -### 7.1 Priority 1: JSON-RPC Message Validation - -Implement comprehensive JSON-RPC 2.0 message validation at the transport/protocol boundary: - -1. **Validate All Incoming Messages**: - - Check for valid JSON structure (catch parse errors) - - Verify `jsonrpc === "2.0"` - - Validate required fields are present - - Check field types match specification - - Ensure proper Request/Response/Notification structure - -2. **Return Proper Error Responses**: - - -32700 for parse errors (invalid JSON) - - -32600 for invalid request structure (missing/wrong fields) - - -32601 for method not found - - -32602 for invalid parameters - -3. **Implementation Location**: - - Option A: At the Transport level (validate before passing to Protocol) - - Option B: As a Transport wrapper (like proposed `ProtocolValidator`) - - Option C: In the Protocol class's `onmessage` handler - -### 7.2 Priority 2: Security-Focused Validation - -1. **Malformed Request Protection**: - - Validate message structure depth (prevent deeply nested objects) - - Implement size limits on messages - - Validate recursion depth - -2. **Token Validation**: - - Ensure proper token validation (no passthrough) - - Verify token audience and claims - - Implement proper session binding - -3. **Input Sanitization**: - - Validate all user inputs against schemas - - Implement content filtering for prompt injection - - Use allow-lists where appropriate - -### 7.3 Priority 3: Comprehensive Testing - -1. **JSON-RPC Compliance Tests**: - - Test all invalid request formats - - Verify proper error responses - - Test batch request handling - - Test notification handling (no responses) - -2. **Security Tests**: - - Test malformed request handling - - Test deeply nested structures - - Test oversized messages - - Test invalid tokens - -3. **Transport-Specific Tests**: - - Test validation across all transport types - - Ensure consistent behavior - -### 7.4 Priority 4: Documentation and Guidelines - -1. **Security Documentation**: - - Document validation requirements for implementers - - Provide security best practices - - Include threat model documentation - -2. **Error Handling Guide**: - - Document all error codes - - Provide examples of proper error responses - - Include debugging guidance - ---- - -## 8. Related Issues and Discussions - -### GitHub Issues - -1. **Issue #563** - Invalid JSON RPC requests do not respond with an error - - https://github.com/modelcontextprotocol/typescript-sdk/issues/563 - - Status: Open - - Priority: High (directly impacts JSON-RPC compliance) - -2. **Issue #544** - The MCP protocol exhibits insufficient security design - - https://github.com/modelcontextprotocol/modelcontextprotocol/issues/544 - - Concerns: OAuth phishing, protocol-level security - - Recommendation: Address at protocol level, not just implementation - -3. **Various Transport Issues** - Multiple issues related to SSE, Streamable HTTP, and validation errors - - Indicates validation is a recurring concern across transport implementations - -### Security Discussions - -Multiple security researchers have identified concerns: -- Prompt injection vulnerabilities -- OAuth security issues -- Token passthrough anti-patterns -- Confused deputy problems - -The consensus is that **security must be addressed at the protocol level**, not left to individual implementations. - ---- - -## 9. Conclusion - -### Key Findings - -1. **JSON-RPC 2.0 Compliance is Mandatory**: MCP explicitly requires full JSON-RPC 2.0 compliance, including proper error handling for invalid messages. - -2. **Current Implementation Has Gaps**: The TypeScript SDK does not properly validate JSON-RPC message format at the protocol level, leading to non-compliant behavior (Issue #563). - -3. **Security Requires Validation**: Proper protocol-level validation is critical for security, protecting against malformed requests, prompt injection, and other attack vectors. - -4. **Incomplete Implementation Exists**: The `transport-validator.ts` file suggests validation work was started but not completed. - -### Critical Requirements Summary - -**MUST Implement**: -- ✅ JSON-RPC 2.0 message format validation -- ✅ Standard error code responses (-32700, -32600, -32601, -32602, -32603) -- ✅ Proper handling of invalid requests, responses, and notifications -- ✅ Batch request support (receiving) -- ✅ Token validation (no passthrough) -- ✅ Secure session ID generation - -**SHOULD Implement**: -- ✅ JSON Schema validation for parameters -- ✅ Recursion depth limits -- ✅ Message size limits -- ✅ Comprehensive error messages -- ✅ Logging and monitoring - -**MAY Implement**: -- JSON-RPC batch sending (receiving is MUST) -- Additional validation beyond spec requirements -- Custom transport mechanisms (must preserve JSON-RPC format) - -### Next Steps - -1. **Design Decision**: Choose validation implementation approach (Transport level, wrapper, or Protocol level) -2. **Implementation**: Build comprehensive JSON-RPC validation with proper error responses -3. **Testing**: Create comprehensive test suite for JSON-RPC compliance -4. **Documentation**: Update docs with validation requirements and security guidelines -5. **Security Review**: Conduct security review of validation implementation - ---- - -## References - -### Specifications -- JSON-RPC 2.0 Specification: https://www.jsonrpc.org/specification -- MCP Specification (2025-06-18): https://modelcontextprotocol.io/specification/2025-06-18 -- MCP Security Best Practices: https://modelcontextprotocol.io/specification/draft/basic/security_best_practices - -### Repositories -- MCP Specification Repository: https://github.com/modelcontextprotocol/modelcontextprotocol -- MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk - -### Security Resources -- RedHat MCP Security: https://www.redhat.com/en/blog/model-context-protocol-mcp-understanding-security-risks-and-controls -- Cisco MCP Security: https://community.cisco.com/t5/security-blogs/ai-model-context-protocol-mcp-and-security/ba-p/5274394 -- Writer MCP Security: https://writer.com/engineering/mcp-security-considerations/ -- Pillar Security MCP Risks: https://www.pillar.security/blog/the-security-risks-of-model-context-protocol-mcp -- Simon Willison on MCP Prompt Injection: https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/ -- Microsoft MCP Security: https://techcommunity.microsoft.com/blog/microsoftdefendercloudblog/plug-play-and-prey-the-security-risks-of-the-model-context-protocol/4410829 -- Windows MCP Security Architecture: https://blogs.windows.com/windowsexperience/2025/05/19/securing-the-model-context-protocol-building-a-safer-agentic-future-on-windows/ - -### Related Issues -- TypeScript SDK Issue #563: https://github.com/modelcontextprotocol/typescript-sdk/issues/563 -- MCP Issue #544: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/544 -- Invariant GitHub MCP Vulnerability: https://invariantlabs.ai/blog/mcp-github-vulnerability - ---- - -## Appendix: Current TypeScript SDK Code Structure - -### Relevant Files -- `/src/types.ts` - JSON-RPC type definitions and Zod schemas -- `/src/shared/protocol.ts` - Protocol class implementing message handling -- `/src/shared/transport.ts` - Transport interface definition -- `/src/shared/transport-validator.ts` - Incomplete validation implementation -- Various transport implementations (stdio, sse, streamableHttp, websocket) - -### Type Definitions (from types.ts) - -```typescript -// JSON-RPC Version -export const JSONRPC_VERSION = "2.0"; - -// Error Codes -export enum ErrorCode { - // SDK error codes - ConnectionClosed = -32000, - RequestTimeout = -32001, - - // Standard JSON-RPC error codes - ParseError = -32700, - InvalidRequest = -32600, - MethodNotFound = -32601, - InvalidParams = -32602, - InternalError = -32603, -} - -// Type Guards -export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => - JSONRPCRequestSchema.safeParse(value).success; - -export const isJSONRPCNotification = (value: unknown): value is JSONRPCNotification => - JSONRPCNotificationSchema.safeParse(value).success; - -export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => - JSONRPCResponseSchema.safeParse(value).success; - -export const isJSONRPCError = (value: unknown): value is JSONRPCError => - JSONRPCErrorSchema.safeParse(value).success; -``` - -### Current Message Handling (from protocol.ts, lines 314-328) - -```typescript -this._transport.onmessage = (message, extra) => { - _onmessage?.(message, extra); - if (isJSONRPCResponse(message) || isJSONRPCError(message)) { - this._onresponse(message); - } else if (isJSONRPCRequest(message)) { - this._onrequest(message, extra); - } else if (isJSONRPCNotification(message)) { - this._onnotification(message); - } else { - this._onerror( - new Error(`Unknown message type: ${JSON.stringify(message)}`), - ); - } -}; -``` - -**Gap**: When message doesn't match any type, it calls `_onerror()` but doesn't send a proper JSON-RPC error response (-32600) back to the sender. - ---- - -*End of Research Document* diff --git a/intermediate-findings/sampling-analysis.md b/intermediate-findings/sampling-analysis.md deleted file mode 100644 index 4a8b2b491..000000000 --- a/intermediate-findings/sampling-analysis.md +++ /dev/null @@ -1,750 +0,0 @@ -# MCP Sampling Analysis: Current Implementation & Tools Support Requirements - -## Executive Summary - -This document analyzes the current sampling implementation in the MCP TypeScript SDK to understand how to add tools support. The analysis covers: - -1. Current sampling API structure -2. Message content type system -3. Existing tool infrastructure -4. Gaps that need to be filled to add tools to sampling - ---- - -## 1. Current Sampling API Structure - -### 1.1 CreateMessageRequest - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1162-1189) - -```typescript -export const CreateMessageRequestSchema = RequestSchema.extend({ - method: z.literal("sampling/createMessage"), - params: BaseRequestParamsSchema.extend({ - messages: z.array(SamplingMessageSchema), - systemPrompt: z.optional(z.string()), - includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), - temperature: z.optional(z.number()), - maxTokens: z.number().int(), - stopSequences: z.optional(z.array(z.string())), - metadata: z.optional(z.object({}).passthrough()), - modelPreferences: z.optional(ModelPreferencesSchema), - }), -}); -``` - -**Key Parameters:** -- `messages`: Array of SamplingMessage objects (user/assistant conversation history) -- `systemPrompt`: Optional system prompt string -- `includeContext`: Optional context inclusion from MCP servers -- `temperature`: Optional temperature for sampling -- `maxTokens`: Maximum tokens to generate (required) -- `stopSequences`: Optional array of stop sequences -- `metadata`: Optional provider-specific metadata -- `modelPreferences`: Optional model selection preferences - -**Note:** Currently NO support for tools parameter. - -### 1.2 CreateMessageResult - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1194-1211) - -```typescript -export const CreateMessageResultSchema = ResultSchema.extend({ - model: z.string(), - stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens"]).or(z.string()), - ), - role: z.enum(["user", "assistant"]), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema - ]), -}); -``` - -**Key Fields:** -- `model`: Name of the model that generated the message -- `stopReason`: Why sampling stopped (endTurn, stopSequence, maxTokens, or custom) -- `role`: Role of the message (user or assistant) -- `content`: Single content block (text, image, or audio) - -**Note:** Content is currently a single content block, NOT an array. Also NO support for tool_use or tool_result content types. - -### 1.3 SamplingMessage - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1152-1157) - -```typescript -export const SamplingMessageSchema = z - .object({ - role: z.enum(["user", "assistant"]), - content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), - }) - .passthrough(); -``` - -**Structure:** -- `role`: Either "user" or "assistant" -- `content`: Single content block (text, image, or audio) - -**Note:** Messages in the conversation history also only support single content blocks, not arrays. - -### 1.4 How Sampling is Invoked - -#### From Server (requesting sampling from client): - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/index.ts` (lines 332-341) - -```typescript -async createMessage( - params: CreateMessageRequest["params"], - options?: RequestOptions, -) { - return this.request( - { method: "sampling/createMessage", params }, - CreateMessageResultSchema, - options, - ); -} -``` - -#### From Client (handling sampling request): - -Client must set a request handler for sampling: - -```typescript -client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - // Client implementation to call LLM - return { - model: "test-model", - role: "assistant", - content: { - type: "text", - text: "This is a test response", - }, - }; -}); -``` - -### 1.5 Sampling Capabilities - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 279-308) - -```typescript -export const ClientCapabilitiesSchema = z - .object({ - experimental: z.optional(z.object({}).passthrough()), - sampling: z.optional(z.object({}).passthrough()), - elicitation: z.optional(z.object({}).passthrough()), - roots: z.optional( - z.object({ - listChanged: z.optional(z.boolean()), - }).passthrough(), - ), - }) - .passthrough(); -``` - -The `sampling` capability is currently just an empty object. There's no granular capability for "supports tools" or similar. - ---- - -## 2. Message Content Type System - -### 2.1 ContentBlock (Used in Prompts & Tool Results) - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 851-857) - -```typescript -export const ContentBlockSchema = z.union([ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ResourceLinkSchema, - EmbeddedResourceSchema, -]); -``` - -ContentBlock is used in: -- Prompt messages (`PromptMessageSchema`) -- Tool call results (`CallToolResultSchema`) - -### 2.2 Available Content Types - -#### TextContent - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 762-776) - -```typescript -export const TextContentSchema = z - .object({ - type: z.literal("text"), - text: z.string(), - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); -``` - -#### ImageContent - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 781-799) - -```typescript -export const ImageContentSchema = z - .object({ - type: z.literal("image"), - data: Base64Schema, - mimeType: z.string(), - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); -``` - -#### AudioContent - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 804-822) - -```typescript -export const AudioContentSchema = z - .object({ - type: z.literal("audio"), - data: Base64Schema, - mimeType: z.string(), - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); -``` - -#### EmbeddedResource - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 827-837) - -```typescript -export const EmbeddedResourceSchema = z - .object({ - type: z.literal("resource"), - resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); -``` - -#### ResourceLink - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 844-846) - -```typescript -export const ResourceLinkSchema = ResourceSchema.extend({ - type: z.literal("resource_link"), -}); -``` - -### 2.3 Content Type Differences - -**Important Distinction:** - -1. **SamplingMessage content**: Single content block (text, image, or audio only) -2. **ContentBlock**: Used in prompts & tool results (includes resource types) -3. **CallToolResult content**: Array of ContentBlock - ---- - -## 3. Tool Infrastructure - -### 3.1 Tool Definition - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 947-984) - -```typescript -export const ToolSchema = BaseMetadataSchema.extend({ - description: z.optional(z.string()), - inputSchema: z - .object({ - type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), - required: z.optional(z.array(z.string())), - }) - .passthrough(), - outputSchema: z.optional( - z.object({ - type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), - required: z.optional(z.array(z.string())), - }) - .passthrough() - ), - annotations: z.optional(ToolAnnotationsSchema), - _meta: z.optional(z.object({}).passthrough()), -}).merge(IconsSchema); -``` - -**Key Fields:** -- `name`: Tool name (from BaseMetadataSchema) -- `title`: Optional display title -- `description`: Tool description -- `inputSchema`: JSON Schema for tool input -- `outputSchema`: Optional JSON Schema for tool output -- `annotations`: Optional hints about tool behavior -- `icons`: Optional icons - -### 3.2 CallToolRequest - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1048-1054) - -```typescript -export const CallToolRequestSchema = RequestSchema.extend({ - method: z.literal("tools/call"), - params: BaseRequestParamsSchema.extend({ - name: z.string(), - arguments: z.optional(z.record(z.unknown())), - }), -}); -``` - -**Structure:** -- `name`: Tool name to call -- `arguments`: Optional record of arguments - -### 3.3 CallToolResult - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 1003-1034) - -```typescript -export const CallToolResultSchema = ResultSchema.extend({ - content: z.array(ContentBlockSchema).default([]), - structuredContent: z.object({}).passthrough().optional(), - isError: z.optional(z.boolean()), -}); -``` - -**Key Fields:** -- `content`: Array of ContentBlock (text, image, audio, resource, resource_link) -- `structuredContent`: Optional structured output (if outputSchema defined) -- `isError`: Whether the tool call resulted in an error - -### 3.4 How Tools are Used - -#### Server Side (providing tools): - -**Example from:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/mcp.ts` - -```typescript -mcpServer.registerTool( - "summarize", - { - description: "Summarize any text using an LLM", - inputSchema: { - text: z.string().describe("Text to summarize"), - }, - }, - async ({ text }) => { - // Tool implementation - return { - content: [ - { - type: "text", - text: "Summary result", - }, - ], - }; - } -); -``` - -#### Client Side (calling tools): - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` (lines 429-479) - -```typescript -async callTool( - params: CallToolRequest["params"], - resultSchema: - | typeof CallToolResultSchema - | typeof CompatibilityCallToolResultSchema = CallToolResultSchema, - options?: RequestOptions, -) { - const result = await this.request( - { method: "tools/call", params }, - resultSchema, - options, - ); - - // Validate structuredContent against outputSchema if present - const validator = this.getToolOutputValidator(params.name); - if (validator) { - if (!result.structuredContent && !result.isError) { - throw new McpError( - ErrorCode.InvalidRequest, - `Tool ${params.name} has an output schema but did not return structured content` - ); - } - - if (result.structuredContent) { - const isValid = validator(result.structuredContent); - if (!isValid) { - throw new McpError( - ErrorCode.InvalidParams, - `Structured content does not match the tool's output schema` - ); - } - } - } - - return result; -} -``` - -The client caches tool output schemas from `listTools()` and validates results. - -### 3.5 Tool Capabilities - -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` (lines 377-388) - -```typescript -tools: z.optional( - z - .object({ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), -), -``` - -Server advertises `tools` capability to indicate it provides tools. - ---- - -## 4. Gaps to Fill for Tools in Sampling - -### 4.1 Missing Content Types - -The current sampling system lacks content types for tool usage: - -**Need to add:** - -1. **ToolUseContent** - Represents a tool call from the LLM - - Should include: tool name, tool call ID, arguments - -2. **ToolResultContent** - Represents the result of a tool call - - Should include: tool call ID, result content, error status - -**Example structure (based on Anthropic's API):** - -```typescript -// Tool use content -{ - type: "tool_use", - id: "tool_call_123", - name: "get_weather", - input: { city: "San Francisco" } -} - -// Tool result content -{ - type: "tool_result", - tool_use_id: "tool_call_123", - content: "Weather is sunny, 72°F" -} -``` - -### 4.2 Content Array Support - -**Current Issue:** -- `SamplingMessage.content` is a single content block -- `CreateMessageResult.content` is a single content block - -**Need to change:** -- Support array of content blocks to allow multiple tool calls in one message -- Or support both single and array (discriminated union based on whether tools are used) - -**Example:** -```typescript -// Assistant message with multiple tool calls -{ - role: "assistant", - content: [ - { type: "text", text: "Let me check the weather..." }, - { type: "tool_use", id: "1", name: "get_weather", input: { city: "SF" } }, - { type: "tool_use", id: "2", name: "get_weather", input: { city: "NYC" } } - ] -} - -// User response with tool results -{ - role: "user", - content: [ - { type: "tool_result", tool_use_id: "1", content: "72°F, sunny" }, - { type: "tool_result", tool_use_id: "2", content: "65°F, cloudy" } - ] -} -``` - -### 4.3 Tools Parameter in Request - -**Current Issue:** -`CreateMessageRequestSchema` has no `tools` parameter. - -**Need to add:** -```typescript -tools: z.optional(z.array(ToolSchema)) -``` - -This allows the server to specify which tools are available to the LLM during sampling. - -### 4.4 Tool Use in Stop Reason - -**Current Issue:** -`stopReason` enum is: `["endTurn", "stopSequence", "maxTokens"]` - -**Need to add:** -`"tool_use"` as a valid stop reason to indicate the LLM wants to call tools. - -### 4.5 Tool Choice Parameter - -**Missing Feature:** -No way to control whether/how tools are used. - -**Should consider adding:** -```typescript -tool_choice: z.optional( - z.union([ - z.literal("auto"), // LLM decides - z.literal("required"), // Must use a tool - z.literal("none"), // Don't use tools - z.object({ // Force specific tool - type: z.literal("tool"), - name: z.string() - }) - ]) -) -``` - -### 4.6 Example Flow with Tools - -**1. Server requests sampling with tools:** -```typescript -await server.createMessage({ - messages: [ - { - role: "user", - content: { type: "text", text: "What's the weather in SF?" } - } - ], - tools: [ - { - name: "get_weather", - description: "Get current weather", - inputSchema: { - type: "object", - properties: { - city: { type: "string" } - }, - required: ["city"] - } - } - ], - maxTokens: 1000 -}) -``` - -**2. Client/LLM responds with tool use:** -```typescript -{ - model: "claude-3-5-sonnet", - role: "assistant", - stopReason: "tool_use", - content: [ - { - type: "text", - text: "I'll check the weather for you." - }, - { - type: "tool_use", - id: "tool_123", - name: "get_weather", - input: { city: "San Francisco" } - } - ] -} -``` - -**3. Server calls the tool and continues conversation:** -```typescript -// Server calls its own tool -const toolResult = await callTool({ - name: "get_weather", - arguments: { city: "San Francisco" } -}); - -// Continue the conversation with tool result -await server.createMessage({ - messages: [ - { - role: "user", - content: { type: "text", text: "What's the weather in SF?" } - }, - { - role: "assistant", - content: [ - { type: "text", text: "I'll check the weather for you." }, - { type: "tool_use", id: "tool_123", name: "get_weather", input: { city: "San Francisco" } } - ] - }, - { - role: "user", - content: [ - { type: "tool_result", tool_use_id: "tool_123", content: "72°F, sunny" } - ] - } - ], - tools: [...], - maxTokens: 1000 -}) -``` - -**4. Final LLM response:** -```typescript -{ - model: "claude-3-5-sonnet", - role: "assistant", - stopReason: "endTurn", - content: { - type: "text", - text: "The weather in San Francisco is currently 72°F and sunny!" - } -} -``` - ---- - -## 5. Implementation Considerations - -### 5.1 Backward Compatibility - -The changes need to maintain backward compatibility with existing implementations that don't use tools. - -**Approach:** -1. Make `tools` parameter optional -2. Support both single content and array content (discriminated union or always array) -3. Add new content types without breaking existing ones -4. Ensure existing code without tools continues to work - -### 5.2 Content Structure Decision - -**Option A: Always use array** -```typescript -content: z.array( - z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolUseContentSchema, - ToolResultContentSchema - ]) -) -``` - -**Option B: Union of single or array** -```typescript -content: z.union([ - // Single content (backward compatible) - z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ]), - // Array content (for tools) - z.array( - z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolUseContentSchema, - ToolResultContentSchema - ]) - ) -]) -``` - -**Recommendation:** Option A (always array) is cleaner but requires migration. Option B maintains perfect backward compatibility. - -### 5.3 Validation - -The client will need to validate: -1. Tool definitions match expected schema -2. Tool use IDs are unique -3. Tool result IDs match previous tool uses -4. Tool names in tool_use match provided tools - -### 5.4 Error Handling - -Need to define behavior for: -1. Tool not found -2. Invalid tool arguments -3. Tool execution errors -4. Missing tool results -5. Mismatched tool_use_id references - ---- - -## 6. Related Code Paths - -### 6.1 Test Coverage - -**Key test files:** -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/index.test.ts` - Server sampling tests (lines 208-270) -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.test.ts` - Client tool validation tests (lines 834-1303) - -The client already has extensive tests for tool output schema validation. Similar tests will be needed for tool usage in sampling. - -### 6.2 Example Usage - -**Current example:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolWithSampleServer.ts` - -This shows a tool that uses sampling internally. With tools support in sampling, this pattern becomes more powerful - a tool can call the LLM which can in turn call other tools. - ---- - -## 7. Summary - -### What Currently Works: -- Basic sampling (text, image, audio) -- Tool definitions and tool calling (separate from sampling) -- Tool output schema validation -- Message history with roles - -### What Needs to be Added: -1. **New content types:** ToolUseContent, ToolResultContent -2. **Array content support:** Messages need to support multiple content blocks -3. **Tools parameter:** CreateMessageRequest needs tools array -4. **Tool choice parameter:** Optional control over tool usage -5. **Stop reason:** Add "tool_use" to valid stop reasons -6. **Validation logic:** Ensure tool use/result consistency -7. **Documentation:** Update examples and guides - -### Critical Design Decisions: -1. Content array vs union approach for backward compatibility -2. Tool_use_id generation: client or server responsibility? -3. Error handling strategy for tool-related errors -4. Capability negotiation: extend sampling capability or add new sub-capabilities? - ---- - -## 8. Next Steps - -1. Review MCP specification for tools in sampling (if exists) -2. Decide on content structure approach (array vs union) -3. Define new Zod schemas for tool content types -4. Update CreateMessageRequest and CreateMessageResult schemas -5. Implement validation logic -6. Write comprehensive tests -7. Update documentation and examples -8. Consider migration guide for existing users - ---- - -**Document created:** 2025-10-01 -**SDK Version:** Based on commit 856d9ec (post v1.18.2) -**Analysis completed by:** Claude (AI Assistant) diff --git a/intermediate-findings/sampling-examples-review.md b/intermediate-findings/sampling-examples-review.md deleted file mode 100644 index cd0aa8391..000000000 --- a/intermediate-findings/sampling-examples-review.md +++ /dev/null @@ -1,794 +0,0 @@ -# MCP TypeScript SDK - Sampling Examples Review - -## Overview - -This document provides a comprehensive review of sampling examples in the MCP TypeScript SDK, covering their structure, patterns, dependencies, and best practices for implementation. - ---- - -## Key Sampling Examples - -### 1. **backfillSampling.ts** (Proxy Pattern) -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/backfill/backfillSampling.ts` - -**Purpose:** Implements an MCP proxy that backfills sampling requests using the Claude API when a client doesn't support native sampling. - -**Key Features:** -- Acts as a middleware proxy between client and server -- Detects client sampling capabilities during initialization -- Intercepts `sampling/createMessage` requests -- Translates MCP requests to Claude API format -- Handles tool calling support -- Converts responses back to MCP format - -**Dependencies:** -```typescript -import { Anthropic } from "@anthropic-ai/sdk"; -import { StdioServerTransport } from '../../server/stdio.js'; -import { StdioClientTransport } from '../../client/stdio.js'; -``` - -**Usage Pattern:** -```bash -npx -y @modelcontextprotocol/inspector \ - npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ - npx -y --silent @modelcontextprotocol/server-everything -``` - ---- - -### 2. **toolWithSampleServer.ts** (Server-Side Sampling) -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolWithSampleServer.ts` - -**Purpose:** Demonstrates how a server can use LLM sampling to implement intelligent tools. - -**Key Features:** -- Registers tools that internally use sampling -- Simple `summarize` tool that uses `mcpServer.server.createMessage()` -- Shows how to call LLM through MCP sampling API -- Demonstrates proper response handling - -**Core Pattern:** -```typescript -mcpServer.registerTool( - "summarize", - { - description: "Summarize any text using an LLM", - inputSchema: { - text: z.string().describe("Text to summarize"), - }, - }, - async ({ text }) => { - // Call the LLM through MCP sampling - const response = await mcpServer.server.createMessage({ - messages: [ - { - role: "user", - content: { - type: "text", - text: `Please summarize the following text concisely:\n\n${text}`, - }, - }, - ], - maxTokens: 500, - }); - - return { - content: [ - { - type: "text", - text: response.content.type === "text" ? response.content.text : "Unable to generate summary", - }, - ], - }; - } -); -``` - -**Transport Setup:** -```typescript -const transport = new StdioServerTransport(); -await mcpServer.connect(transport); -``` - ---- - -## Common Server Example Patterns - -### 3. **simpleStreamableHttp.ts** (Full-Featured Server) -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/simpleStreamableHttp.ts` - -**Key Patterns:** -- Express-based HTTP server setup -- Session management with in-memory event store -- Tool registration with `registerTool()` or `tool()` -- Prompt registration with `registerPrompt()` -- Resource registration with `registerResource()` -- Notification handling via `sendLoggingMessage()` -- OAuth support (optional) - -**Server Initialization:** -```typescript -const getServer = () => { - const server = new McpServer({ - name: 'simple-streamable-http-server', - version: '1.0.0', - icons: [{src: './mcp.svg', sizes: ['512x512'], mimeType: 'image/svg+xml'}], - websiteUrl: 'https://github.com/modelcontextprotocol/typescript-sdk', - }, { capabilities: { logging: {} } }); - - // Register tools, prompts, resources... - return server; -}; -``` - -**Transport Management:** -```typescript -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; - -// For new sessions -const eventStore = new InMemoryEventStore(); -transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: (sessionId) => { - console.log(`Session initialized with ID: ${sessionId}`); - transports[sessionId] = transport; - } -}); - -// Connect and handle requests -const server = getServer(); -await server.connect(transport); -await transport.handleRequest(req, res, req.body); -``` - ---- - -### 4. **simpleSseServer.ts** (SSE Transport Pattern) -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/simpleSseServer.ts` - -**Key Patterns:** -- Deprecated HTTP+SSE transport (protocol version 2024-11-05) -- Separate endpoints for SSE stream and messages -- Session tracking by transport - -**Transport Setup:** -```typescript -const transports: Record = {}; - -app.get('/mcp', async (req: Request, res: Response) => { - const transport = new SSEServerTransport('/messages', res); - const sessionId = transport.sessionId; - transports[sessionId] = transport; - - transport.onclose = () => { - delete transports[sessionId]; - }; - - const server = getServer(); - await server.connect(transport); -}); - -app.post('/messages', async (req: Request, res: Response) => { - const sessionId = req.query.sessionId as string; - const transport = transports[sessionId]; - await transport.handlePostMessage(req, res, req.body); -}); -``` - ---- - -## Client Example Patterns - -### 5. **parallelToolCallsClient.ts** -**Location:** `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/client/parallelToolCallsClient.ts` - -**Key Patterns:** -- Client initialization with capabilities -- Transport connection -- Notification handlers -- Parallel tool execution -- Request handling with schemas - -**Client Setup:** -```typescript -const client = new Client({ - name: 'parallel-tool-calls-client', - version: '1.0.0' -}); - -client.onerror = (error) => { - console.error('Client error:', error); -}; - -const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); -await client.connect(transport); - -// Set up notification handlers -client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { - console.log(`Notification: ${notification.params.data}`); -}); -``` - -**Tool Calling:** -```typescript -const result = await client.request({ - method: 'tools/call', - params: { - name: 'tool-name', - arguments: { /* args */ } - } -}, CallToolResultSchema); -``` - ---- - -## Type System Structure - -### Sampling Types (from types.ts) - -**CreateMessageRequest:** -```typescript -export const CreateMessageRequestSchema = RequestSchema.extend({ - method: z.literal("sampling/createMessage"), - params: BaseRequestParamsSchema.extend({ - messages: z.array(SamplingMessageSchema), - systemPrompt: z.optional(z.string()), - includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), - temperature: z.optional(z.number()), - maxTokens: z.number().int(), - stopSequences: z.optional(z.array(z.string())), - metadata: z.optional(z.object({}).passthrough()), - modelPreferences: z.optional(ModelPreferencesSchema), - tools: z.optional(z.array(ToolSchema)), // Tool definitions - tool_choice: z.optional(ToolChoiceSchema), // Tool usage control - }), -}); -``` - -**CreateMessageResult:** -```typescript -export const CreateMessageResultSchema = ResultSchema.extend({ - model: z.string(), - stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]).or(z.string()), - ), - role: z.literal("assistant"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), -}); -``` - -**Message Types:** -```typescript -// User message (from server to LLM) -export const UserMessageSchema = z.object({ - role: z.literal("user"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolResultContentSchema, - ]), - _meta: z.optional(z.object({}).passthrough()), -}); - -// Assistant message (from LLM to server) -export const AssistantMessageSchema = z.object({ - role: z.literal("assistant"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), - _meta: z.optional(z.object({}).passthrough()), -}); -``` - ---- - -## Server Class Methods (from server/index.ts) - -### Sampling Methods - -**createMessage:** -```typescript -async createMessage( - params: CreateMessageRequest["params"], - options?: RequestOptions, -) { - return this.request( - { method: "sampling/createMessage", params }, - CreateMessageResultSchema, - options, - ); -} -``` - -**elicitInput:** -```typescript -async elicitInput( - params: ElicitRequest["params"], - options?: RequestOptions, -): Promise { - const result = await this.request( - { method: "elicitation/create", params }, - ElicitResultSchema, - options, - ); - // Validates response content against requested schema - return result; -} -``` - -### Capability Assertions - -The Server class validates capabilities before allowing methods: - -```typescript -protected assertCapabilityForMethod(method: RequestT["method"]): void { - switch (method as ServerRequest["method"]) { - case "sampling/createMessage": - if (!this._clientCapabilities?.sampling) { - throw new Error( - `Client does not support sampling (required for ${method})`, - ); - } - break; - - case "elicitation/create": - if (!this._clientCapabilities?.elicitation) { - throw new Error( - `Client does not support elicitation (required for ${method})`, - ); - } - break; - } -} -``` - ---- - -## Dependencies - -### Core Dependencies -- **zod**: Schema validation (v3.23.8) -- **express**: HTTP server framework (v5.0.1) -- **cors**: CORS middleware (v2.8.5) - -### Sampling-Specific Dependencies -- **@anthropic-ai/sdk**: Claude API client (v0.65.0) - devDependency - - Used in backfillSampling.ts example - - Provides types and API client for Claude integration - -### Transport Dependencies -- **eventsource**: SSE client support (v3.0.2) -- **eventsource-parser**: SSE parsing (v3.0.0) -- **cross-spawn**: Process spawning for stdio (v7.0.5) - -### Other Utilities -- **ajv**: JSON Schema validation (v6.12.6) -- **zod-to-json-schema**: Convert Zod to JSON Schema (v3.24.1) - ---- - -## Tool Registration Patterns - -### Pattern 1: registerTool (with metadata) -```typescript -mcpServer.registerTool( - 'tool-name', - { - title: 'Tool Display Name', - description: 'Tool description', - inputSchema: { - param1: z.string().describe('Parameter description'), - param2: z.number().optional().describe('Optional parameter'), - }, - }, - async (args): Promise => { - // Tool implementation - return { - content: [ - { - type: 'text', - text: 'Result text', - }, - ], - }; - } -); -``` - -### Pattern 2: tool (shorthand) -```typescript -mcpServer.tool( - 'tool-name', - 'Tool description', - { - param1: z.string().describe('Parameter description'), - }, - { - title: 'Tool Display Name', - readOnlyHint: true, - openWorldHint: false - }, - async (args, extra): Promise => { - // Access session ID via extra.sessionId - return { - content: [{ type: 'text', text: 'Result' }], - }; - } -); -``` - ---- - -## Notification Patterns - -### Sending Notifications from Server -```typescript -// In tool handler -async ({ name }, extra): Promise => { - // Send logging notification - await server.sendLoggingMessage({ - level: "info", - data: `Processing request for ${name}` - }, extra.sessionId); - - // Process... - - return { - content: [{ type: 'text', text: 'Done' }], - }; -} -``` - -### Receiving Notifications in Client -```typescript -client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { - console.log(`[${notification.params.level}] ${notification.params.data}`); -}); -``` - ---- - -## Error Handling Patterns - -### Server-Side Error Handling -```typescript -try { - const result = await someOperation(); - return { - content: [{ type: 'text', text: result }], - }; -} catch (error) { - return { - content: [ - { - type: 'text', - text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, - }, - ], - }; -} -``` - -### Client-Side Error Handling -```typescript -client.onerror = (error) => { - console.error('Client error:', error); -}; - -try { - const result = await client.request(request, schema); -} catch (error) { - console.error('Request failed:', error); -} -``` - ---- - -## Best Practices - -### 1. **Server Setup** -- Use `getServer()` pattern to create fresh server instances -- Register capabilities at initialization: `{ capabilities: { logging: {}, sampling: {} } }` -- Set up proper session management with unique IDs -- Implement proper cleanup in `onclose` handlers - -### 2. **Tool Implementation** -- Use Zod schemas for input validation -- Provide clear descriptions for all parameters -- Return proper `CallToolResult` format -- Handle errors gracefully and return user-friendly messages -- Use `extra.sessionId` when sending notifications - -### 3. **Sampling Integration** -- Check client capabilities before calling `createMessage()` -- Provide clear system prompts -- Set appropriate `maxTokens` limits -- Handle all possible `stopReason` values -- Check response `content.type` before accessing type-specific fields - -### 4. **Transport Management** -- Store transports by session ID in a map -- Clean up closed transports -- Support resumability with EventStore -- Handle reconnection scenarios - -### 5. **Type Safety** -- Use Zod schemas for request/response validation -- Use type guards for message discrimination -- Validate schemas with `.safeParse()` when needed -- Export and reuse schema definitions - -### 6. **Error Handling** -- Set up `onerror` handlers -- Validate capabilities before making requests -- Handle transport errors gracefully -- Provide meaningful error messages - ---- - -## File Structure Convention - -``` -src/examples/ -├── client/ # Client implementations -│ ├── simpleStreamableHttp.ts -│ ├── parallelToolCallsClient.ts -│ └── ... -├── server/ # Server implementations -│ ├── simpleStreamableHttp.ts -│ ├── simpleSseServer.ts -│ ├── toolWithSampleServer.ts -│ └── ... -├── backfill/ # Proxy/middleware implementations -│ └── backfillSampling.ts -└── shared/ # Shared utilities - └── inMemoryEventStore.ts -``` - ---- - -## Testing Patterns - -### Running Examples - -**Server:** -```bash -npx tsx src/examples/server/simpleStreamableHttp.ts -npx tsx src/examples/server/toolWithSampleServer.ts -``` - -**Client:** -```bash -npx tsx src/examples/client/simpleStreamableHttp.ts -``` - -**Proxy:** -```bash -npx -y @modelcontextprotocol/inspector \ - npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ - npx -y --silent @modelcontextprotocol/server-everything -``` - -### Test Suite Pattern -- Co-locate tests with source: `*.test.ts` -- Use descriptive test names -- Test both success and error cases -- Validate schemas with Zod -- Mock transports for unit tests - ---- - -## Code Style - -- **TypeScript**: Strict mode, explicit return types -- **Naming**: PascalCase for classes/types, camelCase for functions/variables -- **Files**: Lowercase with hyphens, test files with `.test.ts` suffix -- **Imports**: ES module style, include `.js` extension -- **Formatting**: 2-space indentation, semicolons required, single quotes preferred -- **Comments**: JSDoc for public APIs - ---- - -## Adaptable Code Snippets - -### Basic Server Setup -```typescript -import { McpServer } from "../../server/mcp.js"; -import { StdioServerTransport } from "../../server/stdio.js"; -import { z } from "zod"; - -const mcpServer = new McpServer({ - name: "my-server", - version: "1.0.0", -}, { capabilities: { sampling: {} } }); - -mcpServer.registerTool( - "my-tool", - { - description: "My tool description", - inputSchema: { - input: z.string().describe("Input parameter"), - }, - }, - async ({ input }) => { - // Tool logic here - return { - content: [ - { - type: "text", - text: `Processed: ${input}`, - }, - ], - }; - } -); - -async function main() { - const transport = new StdioServerTransport(); - await mcpServer.connect(transport); - console.log("Server running..."); -} - -main().catch((error) => { - console.error("Server error:", error); - process.exit(1); -}); -``` - -### Sampling Tool Pattern -```typescript -mcpServer.registerTool( - "llm-powered-tool", - { - description: "Tool that uses LLM sampling", - inputSchema: { - query: z.string().describe("Query to process"), - }, - }, - async ({ query }) => { - try { - const response = await mcpServer.server.createMessage({ - messages: [ - { - role: "user", - content: { - type: "text", - text: query, - }, - }, - ], - maxTokens: 1000, - }); - - return { - content: [ - { - type: "text", - text: response.content.type === "text" - ? response.content.text - : "Unable to generate response", - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, - }, - ], - }; - } - } -); -``` - -### HTTP Server with Tools -```typescript -import express from 'express'; -import { McpServer } from '../../server/mcp.js'; -import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; -import { randomUUID } from 'node:crypto'; -import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; - -const app = express(); -app.use(express.json()); - -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; - -const getServer = () => { - const server = new McpServer({ - name: 'my-http-server', - version: '1.0.0', - }, { capabilities: { logging: {} } }); - - // Register tools... - - return server; -}; - -app.post('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - - let transport: StreamableHTTPServerTransport; - if (sessionId && transports[sessionId]) { - transport = transports[sessionId]; - } else if (!sessionId && isInitializeRequest(req.body)) { - const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, - onsessioninitialized: (sid) => { - transports[sid] = transport; - } - }); - - transport.onclose = () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - delete transports[sid]; - } - }; - - const server = getServer(); - await server.connect(transport); - } else { - res.status(400).json({ - jsonrpc: '2.0', - error: { code: -32000, message: 'Bad Request' }, - id: null, - }); - return; - } - - await transport.handleRequest(req, res, req.body); -}); - -const PORT = 3000; -app.listen(PORT, () => { - console.log(`Server listening on port ${PORT}`); -}); -``` - ---- - -## Summary - -The MCP TypeScript SDK provides a robust framework for implementing sampling-enabled MCP servers. Key takeaways: - -1. **Two Main Sampling Patterns:** - - **Proxy/Backfill**: Intercept and handle sampling for non-supporting clients - - **Server-Side Tools**: Implement tools that use sampling internally - -2. **Core Components:** - - `McpServer` for server implementation - - `Server.createMessage()` for sampling requests - - Transport abstractions (Stdio, HTTP, SSE) - - Zod-based schema validation - -3. **Best Practices:** - - Check capabilities before making sampling requests - - Provide clear tool descriptions and schemas - - Handle errors gracefully - - Clean up resources properly - - Use TypeScript strict mode - -4. **Examples Structure:** - - Simple examples for learning (toolWithSampleServer.ts) - - Complex examples for reference (backfillSampling.ts) - - Full-featured servers (simpleStreamableHttp.ts) - - Client implementations for testing - -This foundation enables building sophisticated MCP servers that leverage LLM capabilities while maintaining proper protocol compliance and type safety. diff --git a/intermediate-findings/sampling-tool-additions.md b/intermediate-findings/sampling-tool-additions.md deleted file mode 100644 index 1f1725277..000000000 --- a/intermediate-findings/sampling-tool-additions.md +++ /dev/null @@ -1,632 +0,0 @@ -# Sampling Tool Call Additions Analysis (SEP-1577) - -## Summary of Changes - -The branch `ochafik/sep1577` implements comprehensive tool calling support for MCP sampling (SEP-1577). This enables agentic workflows where LLMs can request tool execution during sampling operations. The changes include: - -1. **New content types** for tool calls and results in messages -2. **Role-specific message types** (UserMessage, AssistantMessage) with appropriate content types -3. **Tool choice controls** to specify when/how tools should be used -4. **Extended sampling requests** with tools and tool_choice parameters -5. **Extended sampling responses** with new stop reasons including "toolUse" -6. **Client capabilities** signaling for tool support -7. **Complete example implementation** in the backfill sampling proxy - -## Key Types and Interfaces Added - -### 1. ToolCallContent (Assistant → User) - -Represents the LLM's request to use a tool. This appears in assistant messages. - -```typescript -export const ToolCallContentSchema = 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(), - /** - * Optional metadata - */ - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -export type ToolCallContent = z.infer; -``` - -**Example:** -```typescript -{ - type: "tool_use", - id: "call_123", - name: "get_weather", - input: { city: "San Francisco", units: "celsius" } -} -``` - -### 2. ToolResultContent (User → Assistant) - -Represents the result of executing a tool. This appears in user messages to provide tool execution results back to the LLM. - -```typescript -export const ToolResultContentSchema = z.object({ - type: z.literal("tool_result"), - /** - * The ID of the tool call this result corresponds to. - * Must match a ToolCallContent.id from a previous assistant message. - */ - toolUseId: z.string(), - /** - * The result of the tool execution. - * Can be any JSON-serializable object. - * Error information should be included in the content itself. - */ - content: z.object({}).passthrough(), - /** - * Optional metadata - */ - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -export type ToolResultContent = z.infer; -``` - -**Example (success):** -```typescript -{ - type: "tool_result", - toolUseId: "call_123", - content: { temperature: 72, condition: "sunny", units: "fahrenheit" } -} -``` - -**Example (error in content):** -```typescript -{ - type: "tool_result", - toolUseId: "call_123", - content: { error: "API_ERROR", message: "Service unavailable" } -} -``` - -**Important:** Errors are represented directly in the `content` object, not via a separate `isError` field. This aligns with Claude and OpenAI APIs. - -### 3. ToolChoice - -Controls when and how tools are used during sampling. - -```typescript -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"])), - /** - * 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 type ToolChoice = z.infer; -``` - -**Examples:** -```typescript -// Let model decide -{ mode: "auto" } - -// Force tool use -{ mode: "required" } - -// Sequential tool calls only -{ mode: "auto", disable_parallel_tool_use: true } -``` - -### 4. Role-Specific Message Types - -Messages are now split by role, with each role allowing specific content types: - -#### UserMessage -```typescript -export const UserMessageSchema = z.object({ - role: z.literal("user"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolResultContentSchema, // NEW: Users provide tool results - ]), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -export type UserMessage = z.infer; -``` - -#### AssistantMessage -```typescript -export const AssistantMessageSchema = z.object({ - role: z.literal("assistant"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, // NEW: Assistants request tool use - ]), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -export type AssistantMessage = z.infer; -``` - -#### SamplingMessage -```typescript -export const SamplingMessageSchema = z.discriminatedUnion("role", [ - UserMessageSchema, - AssistantMessageSchema, -]); - -export type SamplingMessage = z.infer; -``` - -### 5. CreateMessageRequest (Extended) - -The sampling request now supports tools: - -```typescript -export const CreateMessageRequestSchema = RequestSchema.extend({ - method: z.literal("sampling/createMessage"), - params: BaseRequestParamsSchema.extend({ - messages: z.array(SamplingMessageSchema), - systemPrompt: z.optional(z.string()), - temperature: z.optional(z.number()), - maxTokens: z.number().int(), - stopSequences: z.optional(z.array(z.string())), - metadata: z.optional(z.object({}).passthrough()), - modelPreferences: z.optional(ModelPreferencesSchema), - - // NEW: Tool support - /** - * 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. - */ - tool_choice: z.optional(ToolChoiceSchema), - - // SOFT-DEPRECATED: Use tools parameter instead - includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), - }), -}); - -export type CreateMessageRequest = z.infer; -``` - -**Example request:** -```typescript -{ - method: "sampling/createMessage", - params: { - messages: [ - { - role: "user", - content: { type: "text", text: "What's the weather in San Francisco?" } - } - ], - maxTokens: 1000, - tools: [ - { - name: "get_weather", - description: "Get current weather for a location", - inputSchema: { - type: "object", - properties: { - city: { type: "string" }, - units: { type: "string", enum: ["celsius", "fahrenheit"] } - }, - required: ["city"] - } - } - ], - tool_choice: { mode: "auto" } - } -} -``` - -### 6. CreateMessageResult (Extended) - -The sampling response now supports tool use stop reasons and tool call content: - -```typescript -export const CreateMessageResultSchema = ResultSchema.extend({ - /** - * The name of the model that generated the message. - */ - model: z.string(), - - /** - * The reason why sampling stopped. - * - "endTurn": Model completed naturally - * - "stopSequence": Hit a stop sequence - * - "maxTokens": Reached token limit - * - "toolUse": Model wants to use a tool // NEW - * - "refusal": Model refused the request // NEW - * - "other": Other provider-specific reason // NEW - */ - stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]) - .or(z.string()) - ), - - /** - * Always "assistant" for sampling responses - */ - role: z.literal("assistant"), - - /** - * Response content. May be ToolCallContent if stopReason is "toolUse". - */ - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, // NEW - ]), -}); - -export type CreateMessageResult = z.infer; -``` - -**Example response with tool call:** -```typescript -{ - model: "claude-3-5-sonnet-20241022", - role: "assistant", - content: { - type: "tool_use", - id: "call_abc123", - name: "get_weather", - input: { city: "San Francisco", units: "celsius" } - }, - stopReason: "toolUse" -} -``` - -### 7. Client Capabilities - -Signal tool support in capabilities: - -```typescript -export const ClientCapabilitiesSchema = z.object({ - sampling: z.optional( - z.object({ - /** - * Present if the client supports non-'none' values for includeContext. - * SOFT-DEPRECATED: New implementations should use tools parameter instead. - */ - context: z.optional(z.object({}).passthrough()), - - /** - * Present if the client supports tools and tool_choice parameters. - * Presence indicates full tool calling support. - */ - tools: z.optional(z.object({}).passthrough()), // NEW - }).passthrough() - ), - // ... other capabilities -}).passthrough(); -``` - -**Example:** -```typescript -{ - sampling: { - tools: {} // Indicates client supports tool calling - } -} -``` - -## How the Tool Call Loop Works - -The tool calling flow follows this pattern: - -### 1. Initial Request with Tools - -Server sends a sampling request with available tools: - -```typescript -{ - method: "sampling/createMessage", - params: { - messages: [ - { - role: "user", - content: { type: "text", text: "What's the weather in SF?" } - } - ], - maxTokens: 1000, - tools: [ - { - name: "get_weather", - description: "Get weather for a location", - inputSchema: { - type: "object", - properties: { - city: { type: "string" }, - units: { type: "string", enum: ["celsius", "fahrenheit"] } - }, - required: ["city"] - } - } - ], - tool_choice: { mode: "auto" } - } -} -``` - -### 2. LLM Responds with Tool Call - -Client/LLM decides to use a tool and responds: - -```typescript -{ - model: "claude-3-5-sonnet-20241022", - role: "assistant", - content: { - type: "tool_use", - id: "toolu_01A2B3C4D5", - name: "get_weather", - input: { city: "San Francisco", units: "celsius" } - }, - stopReason: "toolUse" -} -``` - -### 3. Server Executes Tool - -Server receives tool call, executes the tool (e.g., calls weather API), and sends another request with the result: - -```typescript -{ - method: "sampling/createMessage", - params: { - messages: [ - // Original user message - { - role: "user", - content: { type: "text", text: "What's the weather in SF?" } - }, - // Assistant's tool call - { - role: "assistant", - content: { - type: "tool_use", - id: "toolu_01A2B3C4D5", - name: "get_weather", - input: { city: "San Francisco", units: "celsius" } - } - }, - // Tool result from server - { - role: "user", - content: { - type: "tool_result", - toolUseId: "toolu_01A2B3C4D5", - content: { - temperature: 18, - condition: "partly cloudy", - humidity: 65 - } - } - } - ], - maxTokens: 1000, - tools: [...], // Same tools as before - tool_choice: { mode: "auto" } - } -} -``` - -### 4. LLM Provides Final Answer - -Client/LLM uses the tool result to provide a final answer: - -```typescript -{ - model: "claude-3-5-sonnet-20241022", - role: "assistant", - content: { - type: "text", - text: "The weather in San Francisco is currently 18°C and partly cloudy with 65% humidity." - }, - stopReason: "endTurn" -} -``` - -## Implementation Example - -The `backfillSampling.ts` example demonstrates a complete implementation. Key conversion functions: - -### Tool Definition Conversion -```typescript -function toolToClaudeFormat(tool: Tool): ClaudeTool { - return { - name: tool.name, - description: tool.description || "", - input_schema: tool.inputSchema, - }; -} -``` - -### Tool Choice Conversion -```typescript -function toolChoiceToClaudeFormat( - toolChoice: CreateMessageRequest['params']['tool_choice'] -): ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool | undefined { - if (!toolChoice) return undefined; - - if (toolChoice.mode === "required") { - return { - type: "any", - disable_parallel_tool_use: toolChoice.disable_parallel_tool_use - }; - } - - return { - type: "auto", - disable_parallel_tool_use: toolChoice.disable_parallel_tool_use - }; -} -``` - -### Content Conversion (Claude → MCP) -```typescript -function contentToMcp(content: ContentBlock): CreateMessageResult['content'] { - switch (content.type) { - case 'text': - return { type: 'text', text: content.text }; - case 'tool_use': - return { - type: 'tool_use', - id: content.id, - name: content.name, - input: content.input, - } as ToolCallContent; - default: - throw new Error(`Unsupported content type: ${(content as any).type}`); - } -} -``` - -### Content Conversion (MCP → Claude) -```typescript -function contentFromMcp( - content: UserMessage['content'] | AssistantMessage['content'] -): ContentBlockParam { - switch (content.type) { - case 'text': - return { type: 'text', text: content.text }; - case 'image': - return { - type: 'image', - source: { - data: content.data, - media_type: content.mimeType as Base64ImageSource['media_type'], - type: 'base64', - }, - }; - case 'tool_result': - return { - type: 'tool_result', - tool_use_id: content.toolUseId, - content: JSON.stringify(content.content), - }; - default: - throw new Error(`Unsupported content type: ${(content as any).type}`); - } -} -``` - -### Stop Reason Mapping -```typescript -let stopReason: CreateMessageResult['stopReason'] = msg.stop_reason as any; -if (msg.stop_reason === 'tool_use') { - stopReason = 'toolUse'; -} else if (msg.stop_reason === 'max_tokens') { - stopReason = 'maxTokens'; -} else if (msg.stop_reason === 'end_turn') { - stopReason = 'endTurn'; -} else if (msg.stop_reason === 'stop_sequence') { - stopReason = 'stopSequence'; -} -``` - -## Testing - -The implementation includes comprehensive tests in `src/types.test.ts`: - -- ToolCallContent validation (with/without _meta, error cases) -- ToolResultContent validation (success, errors in content, missing fields) -- ToolChoice validation (auto, required, parallel control) -- UserMessage/AssistantMessage with tool content types -- CreateMessageRequest with tools and tool_choice -- CreateMessageResult with tool calls and new stop reasons -- All new stop reasons: endTurn, stopSequence, maxTokens, toolUse, refusal, other -- Custom stop reason strings - -Total: 27 new test cases added, all passing (47/47 in types.test.ts, 683/683 overall). - -## Key Design Decisions - -1. **No `isError` field**: Errors are represented in the `content` object itself, matching Claude/OpenAI APIs -2. **Role-specific content types**: UserMessage can have tool_result, AssistantMessage can have tool_use -3. **Discriminated unions**: Both messages and content use discriminated unions for type safety -4. **Soft deprecation**: `includeContext` is soft-deprecated in favor of explicit `tools` parameter -5. **Extensible stop reasons**: Stop reasons are an enum but also allow arbitrary strings for provider-specific reasons -6. **Tool correlation**: Tool calls and results are linked via unique IDs (id/toolUseId) - -## Client Capabilities Check - -Before using tools in sampling requests, verify the client supports them: - -```typescript -// In server code -if (client.getServerCapabilities()?.sampling?.tools) { - // Client supports tool calling - // Can send CreateMessageRequest with tools parameter -} -``` - -## Migration Notes - -For existing code using sampling without tools: -- No breaking changes - tools are optional -- `includeContext` still works but is soft-deprecated -- All existing sampling requests continue to work unchanged -- To add tool support: - 1. Add `sampling.tools = {}` to client capabilities - 2. Include `tools` array in CreateMessageRequest.params - 3. Optionally include `tool_choice` to control tool usage - 4. Handle ToolCallContent in responses - 5. Send ToolResultContent in follow-up requests - -## Related Files - -- **Type definitions**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.ts` -- **Client implementation**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.ts` -- **Protocol base**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.ts` -- **Example implementation**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/backfill/backfillSampling.ts` -- **Tests**: `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/types.test.ts` - -## Summary - -SEP-1577 adds comprehensive, type-safe tool calling support to MCP sampling. The implementation: -- ✅ Introduces new content types (ToolCallContent, ToolResultContent) -- ✅ Splits messages by role with appropriate content types -- ✅ Adds tool choice controls -- ✅ Extends sampling request/response schemas -- ✅ Includes client capability signaling -- ✅ Provides complete example implementation -- ✅ Has comprehensive test coverage -- ✅ Maintains backward compatibility -- ✅ Aligns with Claude and OpenAI API conventions - -The tool loop enables agentic workflows where servers can provide tools to LLMs, have the LLM request tool execution, execute those tools, and provide results back to the LLM for final answers. diff --git a/intermediate-findings/sep-1577-spec.md b/intermediate-findings/sep-1577-spec.md deleted file mode 100644 index 10fafd313..000000000 --- a/intermediate-findings/sep-1577-spec.md +++ /dev/null @@ -1,1429 +0,0 @@ -# SEP-1577: Sampling With Tools - Complete Technical Specification - -**Research Date:** 2025-10-01 -**Status:** Draft SEP -**Source:** https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577 -**Author:** Olivier Chafik (@ochafik) -**Sponsor:** @bhosmer-ant -**Target Spec Version:** MCP 2025-06-18 - ---- - -## Executive Summary - -SEP-1577 introduces tool calling support to MCP's `sampling/createMessage` request, enabling MCP servers to run agentic loops using client LLM tokens. This enhancement addresses three key issues: -1. Lack of tool calling support in current sampling implementation -2. Ambiguous definition of context inclusion parameters -3. Low adoption of sampling features by MCP clients - -The specification soft-deprecates the `includeContext` parameter in favor of explicit tool definitions and introduces new capability negotiation requirements. - ---- - -## 1. Motivation and Background - -### Current Problems - -1. **No Tool Support**: Current `sampling/createMessage` lacks tool calling capability, limiting agentic workflows -2. **Ambiguous Context Inclusion**: The `includeContext` parameter's behavior is poorly defined and inconsistently implemented -3. **Low Client Adoption**: Complex and ambiguous requirements have led to minimal client support - -### Goals - -- Enable servers to orchestrate multi-step tool-based workflows using client LLM access -- Standardize tool calling across different AI model providers -- Simplify client implementation requirements -- Maintain backwards compatibility with existing implementations - -### Related Discussions - -- Discussion #124: "Improve sampling in the protocol" -- Issue #503: "Reframe sampling as a basis for bidirectional agent-to-agent communication" -- Discussion #314: "Task semantics and multi-turn interactions with tools" - ---- - -## 2. Type Definitions - -### 2.1 Client Capabilities - -**Updated Schema:** - -```typescript -interface ClientCapabilities { - sampling?: { - /** - * If present, client supports non-'none' values for includeContext parameter. - * Soft-deprecated - new implementations should use tools parameter instead. - */ - context?: object; - - /** - * If present, client supports tools and tool_choice parameters. - * Presence of this capability indicates full tool calling support. - */ - tools?: object; - }; - // ... other capabilities -} -``` - -**Capability Negotiation Rules:** - -1. If `sampling.tools` is NOT present: - - Server MUST NOT include `tools` or `tool_choice` in `CreateMessageRequest` - - Server MUST throw error if it requires tool support - -2. If `sampling.context` is NOT present: - - Server MUST NOT use `includeContext` with values `"thisServer"` or `"allServers"` - - Server MAY use `includeContext: "none"` (default behavior) - -3. Servers SHOULD prefer `tools` over `includeContext` when both are available - ---- - -### 2.2 Tool-Related Types - -#### ToolChoice - -```typescript -interface ToolChoice { - /** - * Controls when tools are used: - * - "auto": Model decides whether to use tools (default) - * - "required": Model MUST use at least one tool before completing - */ - mode?: "auto" | "required"; - - /** - * If true, model should not use multiple tools in parallel. - * Some models may ignore this hint. - * Default: false - */ - disable_parallel_tool_use?: boolean; -} -``` - -**Notes:** -- `mode` defaults to `"auto"` if not specified -- `disable_parallel_tool_use` is a hint, not a guarantee -- Future extensions may add tool-specific selection (e.g., `{"type": "tool", "name": "search"}`) - -#### Tool (Reference) - -The existing `Tool` type from `tools/list` is reused: - -```typescript -interface Tool { - name: string; - title?: string; - description?: string; - inputSchema: { - type: "object"; - properties?: Record; - required?: string[]; - }; - outputSchema?: { - type: "object"; - properties?: Record; - required?: string[]; - }; - annotations?: ToolAnnotations; - _meta?: Record; - icons?: Icon[]; -} -``` - -**Important:** Tools passed in sampling requests use the same schema as `tools/list` responses. - ---- - -### 2.3 New Content Types - -#### ToolCallContent - -Represents a tool invocation request from the assistant. - -```typescript -interface ToolCallContent { - /** - * Discriminator for content type - */ - type: "tool_use"; - - /** - * The name of the tool to invoke. - * Must match a tool name from the request's tools array. - */ - name: string; - - /** - * Unique identifier for this tool call. - * Used to correlate with ToolResultContent in subsequent messages. - */ - id: string; - - /** - * Arguments to pass to the tool. - * Must conform to the tool's inputSchema. - */ - input: object; - - /** - * Optional metadata - */ - _meta?: Record; -} -``` - -**Validation Rules:** -- `name` MUST reference a tool from the request's `tools` array -- `id` MUST be unique within the conversation -- `input` MUST validate against the tool's `inputSchema` -- `id` format is provider-specific (commonly UUIDs or sequential IDs) - -**Zod Schema:** - -```typescript -const ToolCallContentSchema = z.object({ - type: z.literal("tool_use"), - name: z.string(), - id: z.string(), - input: z.object({}).passthrough(), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); -``` - -#### ToolResultContent - -Represents the result of a tool execution, sent by the user (server). - -```typescript -interface ToolResultContent { - /** - * Discriminator for content type - */ - type: "tool_result"; - - /** - * The ID of the tool call this result corresponds to. - * Must match a ToolCallContent.id from a previous assistant message. - */ - toolUseId: string; - - /** - * The result of the tool execution. - * Can be any JSON-serializable object. - * May include error information if the tool failed. - */ - content: object; - - /** - * If true, indicates the tool execution failed. - * The content should contain error details. - * Default: false - */ - isError?: boolean; - - /** - * Optional metadata - */ - _meta?: Record; -} -``` - -**Validation Rules:** -- `toolUseId` MUST reference a previous `ToolCallContent.id` in the conversation -- All `ToolCallContent` instances MUST have corresponding `ToolResultContent` responses -- `content` SHOULD validate against the tool's `outputSchema` if defined -- If `isError` is true, `content` SHOULD contain error explanation - -**Zod Schema:** - -```typescript -const ToolResultContentSchema = z.object({ - type: z.literal("tool_result"), - toolUseId: z.string(), - content: z.object({}).passthrough(), - isError: z.optional(z.boolean()), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); -``` - ---- - -### 2.4 Message Types - -#### SamplingMessage (Updated) - -```typescript -type SamplingMessage = UserMessage | AssistantMessage; - -interface UserMessage { - role: "user"; - content: - | TextContent - | ImageContent - | AudioContent - | ToolResultContent; // NEW - _meta?: Record; -} - -interface AssistantMessage { - role: "assistant"; - content: - | TextContent - | ImageContent - | AudioContent - | ToolCallContent; // NEW - _meta?: Record; -} -``` - -**Key Changes from Current Implementation:** - -1. **Split Message Types**: `SamplingMessage` is now a discriminated union of `UserMessage` and `AssistantMessage` - - Current: Single type with both roles - - New: Separate types with role-specific content - -2. **New Content Types**: - - `UserMessage` can contain `ToolResultContent` - - `AssistantMessage` can contain `ToolCallContent` - -3. **Content Structure**: - - Current: `content` is a single union type - - New: `content` is role-specific union type - -**Backwards Compatibility:** -- Existing messages without tool content remain valid -- Parsers MUST handle both old and new content types -- Servers MUST validate role-content compatibility - -**Zod Schemas:** - -```typescript -const UserMessageSchema = z.object({ - role: z.literal("user"), - content: z.union([ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolResultContentSchema, - ]), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -const AssistantMessageSchema = z.object({ - role: z.literal("assistant"), - content: z.union([ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -const SamplingMessageSchema = z.union([ - UserMessageSchema, - AssistantMessageSchema, -]); -``` - ---- - -### 2.5 Request and Result Updates - -#### CreateMessageRequest (Updated) - -```typescript -interface CreateMessageRequest { - method: "sampling/createMessage"; - params: { - messages: SamplingMessage[]; - - /** - * System prompt for the LLM. - * Client MAY modify or omit this. - */ - systemPrompt?: string; - - /** - * SOFT-DEPRECATED: Use tools parameter instead. - * Request to include context from MCP servers. - * Requires clientCapabilities.sampling.context. - */ - includeContext?: "none" | "thisServer" | "allServers"; - - /** - * Temperature for sampling (0.0 to 1.0+) - */ - temperature?: number; - - /** - * Maximum tokens to generate - */ - maxTokens: number; - - /** - * Stop sequences - */ - stopSequences?: string[]; - - /** - * Provider-specific metadata - */ - metadata?: object; - - /** - * Model selection preferences - */ - modelPreferences?: ModelPreferences; - - /** - * NEW: Tool definitions for the LLM to use. - * Requires clientCapabilities.sampling.tools. - */ - tools?: Tool[]; - - /** - * NEW: Controls tool usage behavior. - * Requires clientCapabilities.sampling.tools. - */ - tool_choice?: ToolChoice; - - /** - * Request metadata - */ - _meta?: { - progressToken?: string | number; - }; - }; -} -``` - -**Parameter Requirements:** - -| Parameter | Required | Capability Required | Notes | -|-----------|----------|-------------------|-------| -| `messages` | Yes | None | Must be non-empty | -| `maxTokens` | Yes | None | Must be positive integer | -| `systemPrompt` | No | None | Client may override | -| `temperature` | No | None | Typically 0.0-1.0 | -| `stopSequences` | No | None | | -| `metadata` | No | None | Provider-specific | -| `modelPreferences` | No | None | | -| `includeContext` | No | `sampling.context` | Soft-deprecated | -| `tools` | No | `sampling.tools` | New in SEP-1577 | -| `tool_choice` | No | `sampling.tools` | Requires `tools` | - -#### CreateMessageResult (Updated) - -```typescript -interface CreateMessageResult { - /** - * The model that generated the response - */ - model: string; - - /** - * Why sampling stopped. - * NEW VALUES: "toolUse", "refusal", "other" - */ - stopReason?: - | "endTurn" // Model completed naturally - | "stopSequence" // Hit a stop sequence - | "maxTokens" // Reached token limit (RENAMED from "maxToken") - | "toolUse" // NEW: Model wants to use a tool - | "refusal" // NEW: Model refused the request - | "other" // NEW: Other provider-specific reason - | string; // Allow extension - - /** - * Role is always "assistant" in responses - */ - role: "assistant"; - - /** - * Response content. - * May be ToolCallContent if stopReason is "toolUse" - */ - content: - | TextContent - | ImageContent - | AudioContent - | ToolCallContent; // NEW - - /** - * Result metadata - */ - _meta?: Record; -} -``` - -**Stop Reason Semantics:** - -| Stop Reason | Meaning | Expected Content Type | Server Action | -|-------------|---------|---------------------|---------------| -| `endTurn` | Natural completion | Text/Image/Audio | Conversation complete | -| `stopSequence` | Hit stop sequence | Text | Conversation may continue | -| `maxTokens` | Token limit reached | Text/Image/Audio | May be incomplete | -| `toolUse` | Tool call requested | ToolCallContent | Server MUST execute tool | -| `refusal` | Request refused | Text (explanation) | Handle refusal | -| `other` | Provider-specific | Any | Check provider docs | - -**Key Changes:** - -1. `stopReason` expanded with 3 new values -2. `maxToken` renamed to `maxTokens` (note the 's') -3. `content` can now be `ToolCallContent` -4. `role` is fixed as `"assistant"` (no longer enum with both) - ---- - -## 3. Protocol Requirements - -### 3.1 Server Requirements - -#### Capability Validation - -Servers MUST validate capabilities before using features: - -```typescript -// Pseudocode -function validateCreateMessageRequest( - request: CreateMessageRequest, - clientCapabilities: ClientCapabilities -): void { - // Check context capability - if (request.params.includeContext && - request.params.includeContext !== "none") { - if (!clientCapabilities.sampling?.context) { - throw new McpError( - ErrorCode.InvalidRequest, - `Client does not support includeContext parameter. ` + - `Client must advertise sampling.context capability.` - ); - } - } - - // Check tools capability - if (request.params.tools || request.params.tool_choice) { - if (!clientCapabilities.sampling?.tools) { - throw new McpError( - ErrorCode.InvalidRequest, - `Client does not support tools parameter. ` + - `Client must advertise sampling.tools capability.` - ); - } - } - - // tool_choice requires tools - if (request.params.tool_choice && !request.params.tools) { - throw new McpError( - ErrorCode.InvalidParams, - `tool_choice requires tools parameter to be set` - ); - } -} -``` - -#### Message Balancing - -Servers MUST ensure tool calls and results are balanced: - -```typescript -// Pseudocode validation -function validateMessageBalance(messages: SamplingMessage[]): void { - const toolCallIds = new Set(); - const toolResultIds = new Set(); - - for (const message of messages) { - if (message.content.type === "tool_use") { - if (toolCallIds.has(message.content.id)) { - throw new Error(`Duplicate tool call ID: ${message.content.id}`); - } - toolCallIds.add(message.content.id); - } - - if (message.content.type === "tool_result") { - toolResultIds.add(message.content.toolUseId); - } - } - - // Every tool call must have a result - for (const callId of toolCallIds) { - if (!toolResultIds.has(callId)) { - throw new Error(`Tool call ${callId} has no corresponding result`); - } - } - - // Every result must reference a valid call - for (const resultId of toolResultIds) { - if (!toolCallIds.has(resultId)) { - throw new Error(`Tool result references unknown call: ${resultId}`); - } - } -} -``` - -#### Tool Execution Loop - -Servers implementing agentic loops SHOULD: - -```typescript -async function agenticLoop( - client: McpClient, - initialMessages: SamplingMessage[], - tools: Tool[] -): Promise { - let messages = [...initialMessages]; - - while (true) { - // Request completion from LLM - const result = await client.request(CreateMessageRequestSchema, { - method: "sampling/createMessage", - params: { - messages, - tools, - maxTokens: 4096, - } - }, CreateMessageResultSchema); - - // Check if tool use is required - if (result.stopReason === "toolUse" && - result.content.type === "tool_use") { - - // Add assistant message with tool call - messages.push({ - role: "assistant", - content: result.content - }); - - // Execute tool locally - const toolResult = await executeToolLocally( - result.content.name, - result.content.input - ); - - // Add user message with tool result - messages.push({ - role: "user", - content: { - type: "tool_result", - toolUseId: result.content.id, - content: toolResult, - isError: toolResult.error ? true : undefined - } - }); - - // Continue loop - continue; - } - - // Completion - return final result - return result; - } -} -``` - -### 3.2 Client Requirements - -#### Capability Advertisement - -Clients MUST advertise capabilities accurately: - -```typescript -const clientCapabilities: ClientCapabilities = { - sampling: { - // Advertise context support if implemented - context: supportContextInclusion ? {} : undefined, - - // Advertise tools support if implemented - tools: supportToolCalling ? {} : undefined, - }, - // ... other capabilities -}; -``` - -#### Tool Execution - -Clients MUST: - -1. Validate tool definitions in requests -2. Provide tools to LLM in provider-specific format -3. Handle tool calls in LLM responses -4. Return results with correct `stopReason` - -Clients MAY: - -1. Filter or modify tool definitions for safety -2. Request user approval before tool use -3. Implement tool execution client-side -4. Convert between provider-specific tool formats - -#### Error Handling - -Clients MUST return appropriate errors: - -| Condition | Error Code | Message | -|-----------|-----------|---------| -| Unsupported capability used | `InvalidRequest` | "Client does not support [feature]" | -| Invalid tool definition | `InvalidParams` | "Invalid tool schema: [details]" | -| Tool execution failed | N/A | Return success with `isError: true` | -| Request refused by LLM | N/A | Return success with `stopReason: "refusal"` | - ---- - -## 4. Backwards Compatibility - -### 4.1 Compatibility Strategy - -**Soft Deprecation:** -- `includeContext` is marked soft-deprecated but remains functional -- Implementations SHOULD prefer `tools` over `includeContext` -- Both MAY coexist in transition period -- `includeContext` MAY be removed in future spec version - -**Version Detection:** - -```typescript -function supportsToolCalling(capabilities: ClientCapabilities): boolean { - return capabilities.sampling?.tools !== undefined; -} - -function supportsContextInclusion(capabilities: ClientCapabilities): boolean { - return capabilities.sampling?.context !== undefined; -} -``` - -### 4.2 Migration Path - -**For Server Implementations:** - -1. Check client capabilities in negotiation -2. Prefer `tools` parameter if available -3. Fall back to `includeContext` for older clients -4. Validate capabilities before sending requests - -**For Client Implementations:** - -1. Add `sampling.tools` capability when ready -2. Continue supporting `sampling.context` for existing servers -3. Implement tool calling according to provider's API -4. Update to handle new content types and stop reasons - -### 4.3 Breaking Changes - -**Type Changes:** - -| Old Type | New Type | Breaking? | Migration | -|----------|----------|-----------|-----------| -| `SamplingMessage` | Split into `UserMessage` / `AssistantMessage` | Yes | Use discriminated union | -| `stopReason: "maxToken"` | `stopReason: "maxTokens"` | Yes | Support both for transition | -| Content types | Added `ToolCallContent`, `ToolResultContent` | Additive | Extend parsers | - -**Validation Changes:** - -- Parsers MUST handle new content types -- Message role validation is now stricter (role-specific content) -- Tool call/result balancing is required when tools used - ---- - -## 5. Implementation Checklist - -### 5.1 TypeScript SDK Changes Required - -#### src/types.ts - -- [ ] Add `ToolCallContentSchema` and `ToolCallContent` type -- [ ] Add `ToolResultContentSchema` and `ToolResultContent` type -- [ ] Split `SamplingMessageSchema` into `UserMessageSchema` and `AssistantMessageSchema` -- [ ] Add `ToolChoiceSchema` and `ToolChoice` type -- [ ] Update `CreateMessageRequestSchema` to include `tools` and `tool_choice` -- [ ] Update `CreateMessageResultSchema`: - - [ ] Add new stop reason values: `"toolUse"`, `"refusal"`, `"other"` - - [ ] Rename `"maxToken"` to `"maxTokens"` (keep both for transition) - - [ ] Update content type to include `ToolCallContent` - - [ ] Fix role to be `"assistant"` only -- [ ] Update `ClientCapabilitiesSchema` to include `sampling.context` and `sampling.tools` -- [ ] Add validation helpers for message balancing -- [ ] Export all new types and schemas - -#### src/client/index.ts - -- [ ] Add capability advertisement for `sampling.tools` -- [ ] Add request validation for tool capabilities -- [ ] Add helper methods for tool calling workflow -- [ ] Update example code / documentation comments - -#### src/server/index.ts - -- [ ] Add validation for client capabilities before using tools -- [ ] Add helper for building tool-enabled sampling requests -- [ ] Add validation for message balance -- [ ] Add error handling for unsupported capabilities - -### 5.2 Test Requirements - -#### Unit Tests - -- [ ] Test `ToolCallContent` schema validation -- [ ] Test `ToolResultContent` schema validation -- [ ] Test `UserMessage` and `AssistantMessage` schemas -- [ ] Test `ToolChoice` schema validation -- [ ] Test updated `CreateMessageRequest` schema -- [ ] Test updated `CreateMessageResult` schema -- [ ] Test capability negotiation logic -- [ ] Test message balance validation -- [ ] Test error conditions: - - [ ] Using tools without capability - - [ ] Using context without capability - - [ ] Unbalanced tool calls/results - - [ ] Invalid tool choice with no tools - -#### Integration Tests - -- [ ] Test full agentic loop with tool calling -- [ ] Test client-server capability negotiation -- [ ] Test backwards compatibility with old clients -- [ ] Test error propagation -- [ ] Test tool execution with various content types -- [ ] Test multi-turn conversations with tools - -### 5.3 Documentation Requirements - -- [ ] Update API documentation for new types -- [ ] Add migration guide for existing implementations -- [ ] Add examples of agentic workflows -- [ ] Document capability negotiation -- [ ] Add troubleshooting guide for common errors -- [ ] Update changelog with breaking changes - ---- - -## 6. Test Scenarios - -### 6.1 Basic Tool Calling - -**Scenario:** Client supports tools, server uses single tool - -```typescript -// Client advertises capability -const clientCaps: ClientCapabilities = { - sampling: { tools: {} } -}; - -// Server sends request -const request: CreateMessageRequest = { - method: "sampling/createMessage", - params: { - messages: [ - { - role: "user", - content: { type: "text", text: "What's the weather in SF?" } - } - ], - tools: [ - { - name: "get_weather", - description: "Get weather for a location", - inputSchema: { - type: "object", - properties: { - location: { type: "string" } - }, - required: ["location"] - } - } - ], - maxTokens: 1000 - } -}; - -// Expected response -const response: CreateMessageResult = { - model: "claude-3-5-sonnet-20241022", - role: "assistant", - stopReason: "toolUse", - content: { - type: "tool_use", - id: "tool_1", - name: "get_weather", - input: { location: "San Francisco, CA" } - } -}; -``` - -### 6.2 Tool Result Submission - -**Scenario:** Server provides tool result in follow-up - -```typescript -// Server adds tool result -const followUp: CreateMessageRequest = { - method: "sampling/createMessage", - params: { - messages: [ - { - role: "user", - content: { type: "text", text: "What's the weather in SF?" } - }, - { - role: "assistant", - content: { - type: "tool_use", - id: "tool_1", - name: "get_weather", - input: { location: "San Francisco, CA" } - } - }, - { - role: "user", - content: { - type: "tool_result", - toolUseId: "tool_1", - content: { - temperature: 65, - condition: "Partly cloudy", - humidity: 70 - } - } - } - ], - tools: [/* same tools */], - maxTokens: 1000 - } -}; - -// Expected final response -const finalResponse: CreateMessageResult = { - model: "claude-3-5-sonnet-20241022", - role: "assistant", - stopReason: "endTurn", - content: { - type: "text", - text: "The weather in San Francisco is currently 65°F and partly cloudy with 70% humidity." - } -}; -``` - -### 6.3 Tool Error Handling - -**Scenario:** Tool execution fails - -```typescript -const errorResult: CreateMessageRequest = { - method: "sampling/createMessage", - params: { - messages: [ - /* ... previous messages ... */ - { - role: "user", - content: { - type: "tool_result", - toolUseId: "tool_1", - content: { - error: "API_ERROR", - message: "Weather service unavailable" - }, - isError: true - } - } - ], - tools: [/* same tools */], - maxTokens: 1000 - } -}; - -// LLM should handle error gracefully -const errorResponse: CreateMessageResult = { - model: "claude-3-5-sonnet-20241022", - role: "assistant", - stopReason: "endTurn", - content: { - type: "text", - text: "I apologize, but I'm unable to fetch the weather data right now due to a service issue. Please try again later." - } -}; -``` - -### 6.4 Capability Rejection - -**Scenario:** Client doesn't support tools - -```typescript -// Client without tools capability -const limitedClientCaps: ClientCapabilities = { - sampling: {} // No tools property -}; - -// Server attempts to use tools -const request: CreateMessageRequest = { - method: "sampling/createMessage", - params: { - messages: [/* ... */], - tools: [/* ... */], // ERROR: Not supported - maxTokens: 1000 - } -}; - -// Expected error response -// Client should return JSON-RPC error: -{ - jsonrpc: "2.0", - id: 1, - error: { - code: ErrorCode.InvalidRequest, - message: "Client does not support tools parameter. Client must advertise sampling.tools capability." - } -} -``` - -### 6.5 Parallel Tool Use - -**Scenario:** Model uses multiple tools in one response (if supported) - -```typescript -// Note: Not all models/providers support parallel tool use -// When supported, response might contain multiple tool calls - -const parallelRequest: CreateMessageRequest = { - method: "sampling/createMessage", - params: { - messages: [ - { - role: "user", - content: { type: "text", text: "Compare weather in SF and NYC" } - } - ], - tools: [ - { - name: "get_weather", - description: "Get weather for a location", - inputSchema: { - type: "object", - properties: { - location: { type: "string" } - }, - required: ["location"] - } - } - ], - tool_choice: { - mode: "auto", - disable_parallel_tool_use: false // Allow parallel use - }, - maxTokens: 1000 - } -}; - -// IMPLEMENTATION NOTE: -// Current spec shows single content per message. -// Parallel tool use may require: -// 1. Multiple assistant messages, OR -// 2. Array of content blocks (not in current spec), OR -// 3. Sequential tool calls in separate turns -// Clarification needed in specification. -``` - -### 6.6 Backwards Compatibility - -**Scenario:** Old client without sampling capabilities - -```typescript -// Legacy client -const legacyClientCaps: ClientCapabilities = { - // No sampling property at all -}; - -// Server checks capabilities -if (!clientCaps.sampling?.tools) { - // Fall back to alternative implementation - // or return error if tools are required - throw new Error("This server requires tool support"); -} -``` - ---- - -## 7. Open Questions and Ambiguities - -### 7.1 Specification Gaps - -1. **Parallel Tool Use Implementation:** - - How should multiple tool calls be represented in a single response? - - Should `content` be an array of content blocks? - - Or should each tool call be a separate assistant message? - -2. **Tool Result Content Schema:** - - Should `content` validate against `outputSchema`? - - How should validation errors be reported? - - What's the expected format for error content? - -3. **Message Ordering:** - - Must tool results immediately follow tool calls? - - Can multiple tool calls be batched before results? - - Can user messages be interleaved? - -4. **Context + Tools Interaction:** - - How do `includeContext` and `tools` interact when both present? - - Should tools override context, or vice versa? - - Migration strategy unclear - -5. **Stop Reason Semantics:** - - What's the difference between `"refusal"` and returning text explaining refusal? - - When should `"other"` be used vs extending the enum? - - Should `"toolUse"` be used even if `tool_choice.mode` was `"required"`? - -### 7.2 Implementation Questions - -1. **Tool Validation:** - - Should SDK validate tool schemas before sending? - - Should SDK validate tool inputs against schemas? - - Who validates outputSchema compliance? - -2. **Error Handling:** - - Should tool execution errors be MCP errors or tool results with `isError: true`? - - What error codes should be used for tool-related failures? - - How should schema validation failures be reported? - -3. **Type Safety:** - - How to enforce role-content compatibility at compile time? - - Should content be a discriminated union per role? - - How to prevent `ToolCallContent` in `UserMessage`? - -4. **Testing:** - - How to test multi-model compatibility? - - Should SDK include mock LLM for testing? - - How to validate different provider tool formats? - -### 7.3 Recommendations for Clarification - -1. **Add explicit examples** for: - - Parallel tool use (if supported) - - Error handling patterns - - Multi-turn conversations with mixed content - -2. **Clarify validation requirements:** - - Who validates what and when - - Expected error responses for each validation failure - - Schema compliance requirements - -3. **Define message sequencing rules:** - - Allowed message patterns - - Prohibited patterns - - Ordering requirements - -4. **Document provider-specific behavior:** - - How to handle provider-specific tool formats - - Dealing with capability mismatches - - Fallback strategies - ---- - -## 8. Related Resources - -### Primary Documents - -- **SEP-1577 Issue:** https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577 -- **MCP Specification:** https://spec.modelcontextprotocol.io/ -- **TypeScript SDK:** https://github.com/modelcontextprotocol/typescript-sdk - -### Related SEPs - -- **SEP-973:** Icons and metadata support (merged) -- **SEP-835:** Authorization scope management -- **SEP-1299:** Server-side authorization management -- **SEP-1502:** MCP extension specification - -### Related Discussions - -- **Discussion #124:** Improve sampling in the protocol - - https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/124 -- **Issue #503:** Reframe sampling for agent-to-agent communication - - https://github.com/modelcontextprotocol/modelcontextprotocol/issues/503 -- **Discussion #314:** Task semantics and multi-turn interactions - - https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/314 -- **Discussion #315:** Suggested response format proposal - - https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/315 - -### External References - -- **Anthropic Claude API:** Tool use documentation -- **OpenAI API:** Function calling documentation -- **JSON Schema:** https://json-schema.org/ -- **RFC 9110:** HTTP Semantics - ---- - -## 9. Implementation Timeline - -### Phase 1: Type Definitions (Week 1) -- Add new content type schemas -- Update message type schemas -- Update request/result schemas -- Add capability schemas -- Write unit tests for schemas - -### Phase 2: Validation (Week 1-2) -- Implement capability checking -- Implement message balance validation -- Implement error handling -- Write validation tests - -### Phase 3: Client/Server Integration (Week 2) -- Update client implementation -- Update server implementation -- Add helper methods -- Write integration tests - -### Phase 4: Documentation and Examples (Week 2-3) -- Update API documentation -- Write migration guide -- Create example implementations -- Write user guides - -### Phase 5: Review and Polish (Week 3) -- Code review -- Documentation review -- Performance testing -- Bug fixes - ---- - -## 10. Security Considerations - -### 10.1 Tool Definition Validation - -Servers SHOULD validate tool definitions from untrusted sources: -- Validate schemas are well-formed JSON Schema -- Limit tool definition size -- Sanitize tool names and descriptions -- Prevent schema injection attacks - -### 10.2 Tool Execution Safety - -Clients MUST implement safety measures: -- Validate tool inputs before execution -- Sandbox tool execution when possible -- Request user approval for sensitive operations -- Log all tool executions -- Implement rate limiting - -### 10.3 Content Validation - -Both sides SHOULD validate content: -- Check content size limits -- Validate base64 encoding for binary data -- Sanitize text content for display -- Validate JSON structure -- Prevent injection attacks - -### 10.4 Capability-Based Security - -Implementations MUST: -- Enforce capability checks strictly -- Reject requests using unsupported features -- Never assume capabilities without negotiation -- Log capability violations - ---- - -## Appendix A: Current vs New Type Comparison - -### A.1 SamplingMessage - -**Current (pre-SEP-1577):** -```typescript -interface SamplingMessage { - role: "user" | "assistant"; - content: TextContent | ImageContent | AudioContent; -} -``` - -**New (SEP-1577):** -```typescript -type SamplingMessage = UserMessage | AssistantMessage; - -interface UserMessage { - role: "user"; - content: TextContent | ImageContent | AudioContent | ToolResultContent; -} - -interface AssistantMessage { - role: "assistant"; - content: TextContent | ImageContent | AudioContent | ToolCallContent; -} -``` - -### A.2 CreateMessageResult - -**Current (pre-SEP-1577):** -```typescript -interface CreateMessageResult { - model: string; - stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; - role: "user" | "assistant"; - content: TextContent | ImageContent | AudioContent; -} -``` - -**New (SEP-1577):** -```typescript -interface CreateMessageResult { - model: string; - stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | "refusal" | "other" | string; - role: "assistant"; // Fixed, not union - content: TextContent | ImageContent | AudioContent | ToolCallContent; -} -``` - -### A.3 ClientCapabilities - -**Current (pre-SEP-1577):** -```typescript -interface ClientCapabilities { - sampling?: object; - // ... other capabilities -} -``` - -**New (SEP-1577):** -```typescript -interface ClientCapabilities { - sampling?: { - context?: object; - tools?: object; - }; - // ... other capabilities -} -``` - ---- - -## Appendix B: Complete Zod Schema Definitions - -```typescript -// Content types -export const ToolCallContentSchema = z.object({ - type: z.literal("tool_use"), - name: z.string(), - id: z.string(), - input: z.object({}).passthrough(), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -export const ToolResultContentSchema = z.object({ - type: z.literal("tool_result"), - toolUseId: z.string(), - content: z.object({}).passthrough(), - isError: z.optional(z.boolean()), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -// Message types -export const UserMessageSchema = z.object({ - role: z.literal("user"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolResultContentSchema, - ]), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -export const AssistantMessageSchema = z.object({ - role: z.literal("assistant"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), - _meta: z.optional(z.object({}).passthrough()), -}).passthrough(); - -export const SamplingMessageSchema = z.discriminatedUnion("role", [ - UserMessageSchema, - AssistantMessageSchema, -]); - -// Tool choice -export const ToolChoiceSchema = z.object({ - mode: z.optional(z.enum(["auto", "required"])), - disable_parallel_tool_use: z.optional(z.boolean()), -}).passthrough(); - -// Updated request -export const CreateMessageRequestSchema = RequestSchema.extend({ - method: z.literal("sampling/createMessage"), - params: BaseRequestParamsSchema.extend({ - messages: z.array(SamplingMessageSchema), - systemPrompt: z.optional(z.string()), - includeContext: z.optional(z.enum(["none", "thisServer", "allServers"])), - temperature: z.optional(z.number()), - maxTokens: z.number().int(), - stopSequences: z.optional(z.array(z.string())), - metadata: z.optional(z.object({}).passthrough()), - modelPreferences: z.optional(ModelPreferencesSchema), - tools: z.optional(z.array(ToolSchema)), - tool_choice: z.optional(ToolChoiceSchema), - }), -}); - -// Updated result -export const CreateMessageResultSchema = ResultSchema.extend({ - model: z.string(), - stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]).or(z.string()) - ), - role: z.literal("assistant"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), -}); - -// Updated capabilities -export const ClientCapabilitiesSchema = z.object({ - sampling: z.optional(z.object({ - context: z.optional(z.object({}).passthrough()), - tools: z.optional(z.object({}).passthrough()), - }).passthrough()), - // ... other capabilities -}).passthrough(); -``` - ---- - -## Appendix C: TypeScript Type Definitions - -```typescript -// Inferred types -export type ToolCallContent = z.infer; -export type ToolResultContent = z.infer; -export type UserMessage = z.infer; -export type AssistantMessage = z.infer; -export type SamplingMessage = z.infer; -export type ToolChoice = z.infer; -export type CreateMessageRequest = z.infer; -export type CreateMessageResult = z.infer; -export type ClientCapabilities = z.infer; -``` - ---- - -## Document Revision History - -| Version | Date | Author | Changes | -|---------|------|--------|---------| -| 1.0 | 2025-10-01 | Research Agent | Initial comprehensive analysis | - ---- - -**END OF DOCUMENT** diff --git a/intermediate-findings/test-analysis.md b/intermediate-findings/test-analysis.md deleted file mode 100644 index cbe7aa148..000000000 --- a/intermediate-findings/test-analysis.md +++ /dev/null @@ -1,664 +0,0 @@ -# MCP TypeScript SDK Test Suite Analysis - -## Executive Summary - -This document provides a comprehensive analysis of the existing test suite in the MCP TypeScript SDK, examining testing patterns, coverage, and identifying gaps relevant to transport validation work. - -## Test File Organization - -### Test Files Discovered (35 total) - -**Client Tests:** -- `/src/client/auth.test.ts` - OAuth client authentication -- `/src/client/cross-spawn.test.ts` - Process spawning -- `/src/client/index.test.ts` - Main client functionality -- `/src/client/middleware.test.ts` - Fetch middleware (OAuth, logging) -- `/src/client/sse.test.ts` - SSE client transport -- `/src/client/stdio.test.ts` - Stdio client transport -- `/src/client/streamableHttp.test.ts` - StreamableHTTP client transport - -**Server Tests:** -- `/src/server/index.test.ts` - Main server functionality -- `/src/server/mcp.test.ts` - MCP server -- `/src/server/sse.test.ts` - SSE server transport -- `/src/server/stdio.test.ts` - Stdio server transport -- `/src/server/streamableHttp.test.ts` - StreamableHTTP server transport -- `/src/server/completable.test.ts` - Completable behavior -- `/src/server/title.test.ts` - Title handling -- `/src/server/auth/` - Multiple auth-related tests (7 files) - -**Shared/Protocol Tests:** -- `/src/shared/protocol-transport-handling.test.ts` - Protocol transport bug fixes -- `/src/shared/protocol.test.ts` - Core protocol tests -- `/src/shared/stdio.test.ts` - Shared stdio utilities -- `/src/shared/auth-utils.test.ts` - Auth utilities -- `/src/shared/auth.test.ts` - Auth functionality -- `/src/shared/uriTemplate.test.ts` - URI templates - -**Type/Integration Tests:** -- `/src/inMemory.test.ts` - In-memory transport -- `/src/types.test.ts` - Type validation -- `/src/spec.types.test.ts` - Spec type compatibility -- `/src/integration-tests/` - Integration tests (3 files) - -## Test Patterns and Structure - -### 1. Testing Framework & Tools - -**Jest Configuration:** -- Uses Jest as the primary test runner -- `@jest/globals` for describe/test/expect/beforeEach/afterEach -- Mock implementations with `jest.fn()` and `jest.spyOn()` -- Timer mocking with `jest.useFakeTimers()` for timeout testing - -**Common Patterns:** -```typescript -describe("Component name", () => { - let variable: Type; - - beforeEach(() => { - // Setup - }); - - test("should do something specific", async () => { - // Arrange - Act - Assert - }); -}); -``` - -### 2. Mock Transport Pattern - -**Consistent Mock Transport Implementation:** -```typescript -class MockTransport implements Transport { - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: unknown) => void; - - async start(): Promise {} - async close(): Promise { this.onclose?.(); } - async send(_message: unknown): Promise {} -} -``` - -**Used extensively in:** -- `/src/shared/protocol.test.ts` - Basic mock transport -- `/src/shared/protocol-transport-handling.test.ts` - Enhanced with ID tracking -- Client/server tests - For isolating protocol logic from transport details - -### 3. In-Memory Transport for Integration - -**InMemoryTransport Pattern:** -- Used for testing full client-server interactions -- Creates linked pairs for bidirectional communication -- Examples in `/src/inMemory.test.ts` and `/src/client/index.test.ts` - -```typescript -const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); -await client.connect(clientTransport); -await server.connect(serverTransport); -``` - -### 4. HTTP Server Mocking for Network Transports - -**Pattern for SSE/HTTP tests:** -- Creates actual Node.js HTTP servers on random ports -- Uses AddressInfo to get assigned port -- Simulates real HTTP interactions - -```typescript -const server = createServer((req, res) => { - // Handle requests -}); - -server.listen(0, "127.0.0.1", () => { - const addr = server.address() as AddressInfo; - const baseUrl = new URL(`http://127.0.0.1:${addr.port}`); - // Run tests -}); -``` - -## Transport Layer Coverage - -### 1. Stdio Transport Tests - -**Client-side (`/src/client/stdio.test.ts`):** -- ✅ Basic lifecycle (start, close) -- ✅ Message reading (JSON-RPC over newline-delimited) -- ✅ Process management (child process PID tracking) -- ✅ Cross-platform testing (Windows vs Unix commands) - -**Server-side (`/src/server/stdio.test.ts`):** -- ✅ Start/close lifecycle -- ✅ Message buffering (doesn't read until started) -- ✅ Multiple message handling -- ✅ Stream handling (Readable/Writable) - -**Shared (`/src/shared/stdio.test.ts`):** -- ✅ ReadBuffer implementation -- ✅ Newline-delimited message parsing -- ✅ Buffer clearing and reuse - -**Coverage: HIGH** - Well-tested for message framing and process management - -### 2. SSE Transport Tests - -**Client-side (`/src/client/sse.test.ts` - 1450 lines):** -- ✅ Connection establishment and endpoint discovery -- ✅ Message sending/receiving (GET for events, POST for messages) -- ✅ Error handling (malformed JSON, server errors) -- ✅ HTTP status code handling (401, 403, 500) -- ✅ Custom headers and fetch implementation -- ✅ **Extensive OAuth authentication flow testing:** - - Token attachment to requests - - 401 retry with token refresh - - Authorization code flow - - Error handling (InvalidClientError, InvalidGrantError, etc.) - - Custom fetch middleware integration -- ✅ DNS rebinding protection validation - -**Server-side (`/src/server/sse.test.ts` - 717 lines):** -- ✅ Session ID management and endpoint generation -- ✅ Query parameter handling (existing params, hash fragments) -- ✅ POST message validation (content-type, JSON schema) -- ✅ Request info propagation to handlers -- ✅ **DNS rebinding protection:** - - Host header validation - - Origin header validation - - Content-Type validation - - Combined validation scenarios - -**Coverage: VERY HIGH** - Comprehensive testing including security features - -### 3. StreamableHTTP Transport Tests - -**Found in:** -- `/src/client/streamableHttp.test.ts` -- `/src/server/streamableHttp.test.ts` - -**Note:** Not read in detail during this analysis, but appears to follow similar patterns to SSE tests - -### 4. In-Memory Transport Tests - -**Location:** `/src/inMemory.test.ts` - -**Coverage:** -- ✅ Linked pair creation -- ✅ Bidirectional message sending -- ✅ Auth info propagation -- ✅ Connection lifecycle (start, close) -- ✅ Error handling (send after close) -- ✅ Message queueing (before start) - -**Coverage: GOOD** - Covers basic functionality for testing purposes - -## Protocol Layer Tests - -### 1. Core Protocol Tests (`/src/shared/protocol.test.ts` - 741 lines) - -**Message Handling:** -- ✅ Request timeouts -- ✅ Connection close handling -- ✅ Hook preservation (onclose, onerror, onmessage) - -**Progress Notifications:** -- ✅ _meta preservation when adding progressToken -- ✅ Progress notification handling with timeout reset -- ✅ maxTotalTimeout enforcement -- ✅ Multiple progress updates -- ✅ Progress with message field - -**Debounced Notifications:** -- ✅ Notification debouncing (params-based conditions) -- ✅ Non-debounced notifications (with relatedRequestId) -- ✅ Clearing pending on close -- ✅ Multiple synchronous calls -- ✅ Sequential batches - -**Capabilities Merging:** -- ✅ Client capability merging -- ✅ Server capability merging -- ✅ Value overriding -- ✅ Empty object handling - -**Coverage: HIGH** - Comprehensive protocol behavior testing - -### 2. Transport Handling Bug Tests (`/src/shared/protocol-transport-handling.test.ts`) - -**Specific Bug Scenario:** -- ✅ Multiple client connections with proper response routing -- ✅ Timing issues with rapid connections -- ✅ Transport reference management - -**Context:** This file tests a specific bug where responses were sent to wrong transports when multiple clients connected - -**Coverage: TARGETED** - Focuses on specific multi-client scenario - -## Client/Server Integration Tests - -### 1. Client Tests (`/src/client/index.test.ts` - 1304 lines) - -**Protocol Negotiation:** -- ✅ Latest protocol version acceptance -- ✅ Older supported version acceptance -- ✅ Unsupported version rejection -- ✅ Version negotiation (old client, new server) - -**Capabilities:** -- ✅ Server capability respect (resources, tools, prompts, logging) -- ✅ Client notification capability validation -- ✅ Request handler capability validation -- ✅ Strict capability enforcement - -**Request Management:** -- ✅ Request cancellation (AbortController) -- ✅ Request timeout handling -- ✅ Custom request/notification schemas (type checking) - -**Output Schema Validation:** -- ✅ Tool output schema validation -- ✅ Complex JSON schema validation -- ✅ Additional properties validation -- ✅ Missing structuredContent detection - -**Coverage: VERY HIGH** - Comprehensive client behavior - -### 2. Server Tests (`/src/server/index.test.ts` - 1016 lines) - -**Protocol Support:** -- ✅ Latest protocol version handling -- ✅ Older version support -- ✅ Unsupported version handling (auto-negotiation) - -**Capabilities:** -- ✅ Client capability respect (sampling, elicitation) -- ✅ Server notification capability validation -- ✅ Request handler capability validation - -**Elicitation Feature:** -- ✅ Schema validation for accept action -- ✅ Invalid data rejection -- ✅ Decline/cancel without validation - -**Logging:** -- ✅ Log level filtering per transport (with/without sessionId) - -**Coverage: VERY HIGH** - Comprehensive server behavior - -### 3. Middleware Tests (`/src/client/middleware.test.ts` - 1214 lines) - -**OAuth Middleware:** -- ✅ Authorization header injection -- ✅ Token retrieval and usage -- ✅ 401 retry with auth flow -- ✅ Persistent 401 handling -- ✅ Request preservation (method, body, headers) -- ✅ Non-401 error pass-through -- ✅ URL object handling - -**Logging Middleware:** -- ✅ Default logger (console) -- ✅ Custom logger support -- ✅ Request/response header inclusion -- ✅ Status level filtering -- ✅ Duration measurement -- ✅ Network error logging - -**Middleware Composition:** -- ✅ Single middleware -- ✅ Multiple middleware in order -- ✅ Error propagation through middleware -- ✅ Real-world transport patterns (SSE, StreamableHTTP) - -**CreateMiddleware Helper:** -- ✅ Cleaner syntax for middleware creation -- ✅ Conditional logic support -- ✅ Short-circuit responses -- ✅ Response transformation -- ✅ Error handling and retry - -**Coverage: VERY HIGH** - Comprehensive middleware testing - -## Type and Schema Tests - -### 1. Types Test (`/src/types.test.ts`) - -**Basic Types:** -- ✅ Protocol version constants -- ✅ ResourceLink validation -- ✅ ContentBlock types (text, image, audio, resource_link, embedded resource) - -**Message Types:** -- ✅ PromptMessage with ContentBlock -- ✅ CallToolResult with ContentBlock arrays - -**Completion:** -- ✅ CompleteRequest without context -- ✅ CompleteRequest with resolved arguments -- ✅ Multiple resolved variables - -**Coverage: GOOD** - Schema validation coverage - -### 2. Spec Types Test (`/src/spec.types.test.ts` - 725 lines) - -**Type Compatibility:** -- ✅ Static type checks for SDK vs Spec types -- ✅ Runtime verification of type coverage -- ✅ 94 type compatibility checks -- ✅ Missing SDK types tracking - -**Pattern:** -```typescript -const sdkTypeChecks = { - TypeName: (sdk: SDKType, spec: SpecType) => { - sdk = spec; // Mutual assignability - spec = sdk; - }, -}; -``` - -**Coverage: COMPREHENSIVE** - Ensures SDK types match spec - -## Edge Cases and Error Handling - -### Well-Covered Edge Cases: - -1. **Timeout Scenarios:** - - Request timeouts with immediate (0ms) expiry - - Progress notification timeout reset - - Maximum total timeout enforcement - - Timeout cancellation on abort - -2. **Multi-Client Scenarios:** - - Multiple clients connecting to same server - - Transport reference management - - Response routing to correct client - -3. **Authentication:** - - Token expiry and refresh - - Invalid client/grant errors - - Authorization redirect flows - - Persistent 401 after auth - -4. **Message Framing:** - - Newline-delimited message parsing - - Buffer management - - Malformed JSON handling - -5. **DNS Rebinding Protection:** - - Host header validation - - Origin header validation - - Content-Type validation - - Combined validation rules - -6. **Protocol Negotiation:** - - Version mismatch handling - - Old client, new server scenarios - - Unsupported version rejection - -## Testing Gaps and Opportunities - -### 1. Transport Validation (MISSING - Primary Gap) - -**No dedicated tests for:** -- Message structure validation at transport layer -- Invalid JSON-RPC format detection -- Required field validation (jsonrpc, method, id) -- Type validation (id must be string/number, method must be string) -- Extra field rejection in strict mode -- Batch message handling validation - -**Current Situation:** -- Validation happens implicitly through Zod schemas in Protocol -- No explicit transport-level validation tests -- Error handling tested but not validation edge cases - -### 2. Message Boundary Tests (LIMITED) - -**Could be improved:** -- Partial message handling in buffers -- Very large message handling -- Concurrent message sends -- Message interleaving scenarios - -### 3. Transport Error Recovery (PARTIAL) - -**Covered for:** -- Network errors -- Connection drops -- Auth failures - -**Not explicitly covered:** -- Partial write failures -- Buffer overflow scenarios -- Transport-specific error conditions - -### 4. Performance and Load Testing (ABSENT) - -**No tests for:** -- High message throughput -- Large payload handling -- Memory usage under load -- Connection pool management - -### 5. Security Testing (GOOD but could be enhanced) - -**Well covered:** -- OAuth flows -- DNS rebinding protection -- Header validation - -**Could add:** -- Message injection attacks -- Buffer overflow attempts -- Resource exhaustion scenarios - -## Test Patterns to Follow - -### 1. Mock Transport Pattern - -```typescript -class MockTransport implements Transport { - id: string; - sentMessages: JSONRPCMessage[] = []; - - constructor(id: string) { - this.id = id; - } - - async send(message: JSONRPCMessage): Promise { - this.sentMessages.push(message); - } - - // Simulate receiving a message - simulateMessage(message: unknown) { - this.onmessage?.(message); - } -} -``` - -**Use for:** Isolating protocol logic from transport implementation - -### 2. Integration Test Pattern - -```typescript -const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - -const client = new Client(clientInfo, capabilities); -const server = new Server(serverInfo, capabilities); - -await Promise.all([ - client.connect(clientTransport), - server.connect(serverTransport) -]); - -// Test full round-trip behavior -``` - -**Use for:** Testing complete protocol interactions - -### 3. HTTP Server Pattern (for network transports) - -```typescript -const server = createServer((req, res) => { - // Handle requests, simulate responses -}); - -await new Promise(resolve => { - server.listen(0, "127.0.0.1", () => { - const addr = server.address() as AddressInfo; - baseUrl = new URL(`http://127.0.0.1:${addr.port}`); - resolve(); - }); -}); - -// Run tests with real HTTP -await server.close(); -``` - -**Use for:** Testing SSE, StreamableHTTP transports - -### 4. Timer Testing Pattern - -```typescript -beforeEach(() => { - jest.useFakeTimers(); -}); - -afterEach(() => { - jest.useRealTimers(); -}); - -test("should timeout", async () => { - const promise = someTimedOperation(); - jest.advanceTimersByTime(1001); - await expect(promise).rejects.toThrow("timeout"); -}); -``` - -**Use for:** Testing timeout behavior without waiting - -### 5. Spy Pattern for Callbacks - -```typescript -const mockCallback = jest.fn(); -transport.onmessage = mockCallback; - -// Trigger message -await transport.simulateMessage(message); - -expect(mockCallback).toHaveBeenCalledWith( - expect.objectContaining({ method: "test" }) -); -``` - -**Use for:** Verifying callback invocations - -## Testing Infrastructure - -### 1. Test Utilities - -**Location:** Embedded in test files, no centralized utility module found - -**Common utilities:** -- Mock transport creation -- Message builders -- Server creation helpers -- Flush microtasks helper - -**Opportunity:** Could centralize common test utilities - -### 2. Test Data Builders - -**Current approach:** Inline object creation - -```typescript -const testMessage: JSONRPCMessage = { - jsonrpc: "2.0", - id: 1, - method: "test" -}; -``` - -**Opportunity:** Could create test data builders for common scenarios - -### 3. Assertion Helpers - -**Current approach:** Direct Jest matchers - -**Opportunity:** Could create custom matchers for: -- Valid JSON-RPC structure -- Message format validation -- Transport state assertions - -## Recommendations for Transport Validation Testing - -### 1. Create Dedicated Transport Validator Tests - -**New file:** `/src/shared/transport-validator.test.ts` - -**Test cases to add:** -- Valid message acceptance -- Invalid JSON rejection -- Missing required fields -- Invalid field types -- Extra field handling (strict vs permissive) -- Batch message validation -- Edge cases (empty arrays, null values, etc.) - -### 2. Integration with Existing Tests - -**Enhance existing transport tests with validation:** -- Add invalid message test cases to stdio tests -- Add malformed message handling to SSE tests -- Test validation errors are properly propagated - -### 3. Error Message Quality - -**Test that validation errors provide:** -- Clear error messages -- Field-specific errors -- Helpful suggestions -- Proper error codes - -### 4. Performance Testing - -**Add basic performance tests:** -- Validation overhead measurement -- Large message handling -- Batch validation performance - -## Summary - -### Strengths of Current Test Suite: - -1. **Comprehensive Protocol Testing** - Well-covered protocol behavior, capabilities, negotiation -2. **Strong Transport Implementation Tests** - Good coverage of stdio, SSE with security features -3. **Excellent Integration Tests** - Full client-server scenarios well tested -4. **Type Safety** - Comprehensive type compatibility verification -5. **Authentication** - Extensive OAuth flow testing -6. **Edge Cases** - Good coverage of timeouts, multi-client scenarios, error handling - -### Primary Gaps: - -1. **Transport Validation** - No dedicated message validation tests at transport layer -2. **Message Boundary Handling** - Limited testing of partial messages, large payloads -3. **Performance** - No load or performance testing -4. **Centralized Test Utilities** - Opportunities for DRY improvements - -### Test Quality Indicators: - -- **Total test files:** 35+ -- **Largest test files:** - - client/sse.test.ts: 1450 lines - - client/middleware.test.ts: 1214 lines - - client/index.test.ts: 1304 lines - - server/index.test.ts: 1016 lines -- **Test framework:** Jest with comprehensive mocking -- **Pattern consistency:** HIGH - Consistent use of beforeEach, mock patterns -- **Documentation:** Some tests include helpful comments -- **Maintainability:** GOOD - Clear test structure, logical grouping - -### Overall Assessment: - -The MCP TypeScript SDK has a **strong, comprehensive test suite** with excellent coverage of protocol behavior, transport implementations, and integration scenarios. The primary gap is **transport-level message validation**, which is the focus of the current work. The existing test patterns provide excellent examples to follow when implementing validation tests. diff --git a/intermediate-findings/test-patterns-analysis.md b/intermediate-findings/test-patterns-analysis.md deleted file mode 100644 index 989b66f0f..000000000 --- a/intermediate-findings/test-patterns-analysis.md +++ /dev/null @@ -1,794 +0,0 @@ -# MCP TypeScript SDK Test Patterns for Sampling - -## Overview - -This document analyzes test patterns in the MCP TypeScript SDK codebase to understand how to write tests for servers that use sampling (LLM requests). Based on analysis of existing test files and examples. - -## Key Testing Components - -### 1. Transport Types for Testing - -#### InMemoryTransport (Recommended for Unit Tests) -- **Location**: `/src/inMemory.ts` -- **Use case**: Testing client-server interactions within the same process -- **Advantages**: - - Synchronous, fast execution - - No external process spawning - - Full control over both sides of the connection - - Easy to mock and test error conditions - -```typescript -import { InMemoryTransport } from "../inMemory.js"; -import { Client } from "../client/index.js"; -import { Server } from "../server/index.js"; - -// Create linked pair - one for client, one for server -const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - -// Connect both sides -await Promise.all([ - client.connect(clientTransport), - server.connect(serverTransport), -]); -``` - -#### StdioClientTransport (For Integration Tests) -- **Location**: `/src/client/stdio.ts` -- **Use case**: Testing real server processes via stdio -- **Pattern**: Spawn actual server process and communicate via stdin/stdout - -```typescript -import { StdioClientTransport } from "./stdio.js"; - -const transport = new StdioClientTransport({ - command: "/path/to/server", - args: ["arg1", "arg2"], - env: { CUSTOM_VAR: "value" } -}); - -await transport.start(); -// Use with Client instance -``` - -### 2. Setting Up Sampling Request Handlers - -#### On the Client Side (Simulating LLM) - -The client needs to implement a handler for `sampling/createMessage` requests to simulate LLM responses: - -```typescript -import { Client } from "../client/index.js"; -import { CreateMessageRequestSchema } from "../types.js"; - -const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - sampling: {}, // MUST declare sampling capability - }, - } -); - -// Set up handler for sampling requests from server -client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - return { - model: "test-model", - role: "assistant", - content: { - type: "text", - text: "This is a mock LLM response", - }, - }; -}); -``` - -#### Pattern from `src/server/index.test.ts` (lines 237-248): - -```typescript -// Server declares it will call sampling -const server = new Server( - { - name: "test server", - version: "1.0", - }, - { - capabilities: { - prompts: {}, - resources: {}, - tools: {}, - logging: {}, - }, - enforceStrictCapabilities: true, - }, -); - -// Client provides sampling capability -const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - sampling: {}, - }, - }, -); - -// Implement request handler for sampling/createMessage -client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - // Mock implementation of createMessage - return { - model: "test-model", - role: "assistant", - content: { - type: "text", - text: "This is a test response", - }, - }; -}); - -const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - -await Promise.all([ - client.connect(clientTransport), - server.connect(serverTransport), -]); - -// Now server can call createMessage -const response = await server.createMessage({ - messages: [], - maxTokens: 10, -}); -``` - -### 3. Tool Loop Testing Pattern - -Based on `toolLoopSampling.ts` example, here's how to test a server that uses sampling with tools: - -```typescript -import { McpServer } from "../server/mcp.js"; -import { Client } from "../client/index.js"; -import { InMemoryTransport } from "../inMemory.js"; -import { CreateMessageRequestSchema, ToolCallContent } from "../types.js"; - -describe("Server with sampling tool loop", () => { - test("should handle tool loop with local tools", async () => { - const mcpServer = new McpServer({ - name: "tool-loop-server", - version: "1.0.0", - }); - - // Register a tool that uses sampling - mcpServer.registerTool( - "fileSearch", - { - description: "Search files using AI", - inputSchema: { - query: z.string(), - }, - }, - async ({ query }) => { - // Tool implementation calls createMessage - const response = await mcpServer.server.createMessage({ - messages: [ - { - role: "user", - content: { - type: "text", - text: query, - }, - }, - ], - maxTokens: 1000, - tools: [ - { - name: "ripgrep", - description: "Search files", - inputSchema: { - type: "object", - properties: { - pattern: { type: "string" }, - }, - required: ["pattern"], - }, - }, - ], - tool_choice: { mode: "auto" }, - }); - - return { - content: [ - { - type: "text", - text: response.content.type === "text" - ? response.content.text - : "Tool result", - }, - ], - }; - } - ); - - // Set up client that simulates LLM with tool calling - const client = new Client( - { - name: "test-client", - version: "1.0.0", - }, - { - capabilities: { - sampling: {}, - }, - } - ); - - let callCount = 0; - client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - callCount++; - - // First call: LLM decides to use tool - if (callCount === 1) { - return { - model: "test-model", - role: "assistant", - stopReason: "toolUse", - content: { - type: "tool_use", - id: "tool-call-1", - name: "ripgrep", - input: { pattern: "test" }, - } as ToolCallContent, - }; - } - - // Second call: LLM provides final answer after tool result - return { - model: "test-model", - role: "assistant", - stopReason: "endTurn", - content: { - type: "text", - text: "Found 5 matches in the files", - }, - }; - }); - - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - - await Promise.all([ - client.connect(clientTransport), - mcpServer.server.connect(serverTransport), - ]); - - // Test the tool - const result = await client.callTool({ - name: "fileSearch", - arguments: { query: "Find TypeScript files" }, - }); - - expect(result.content[0]).toMatchObject({ - type: "text", - text: expect.stringContaining("matches"), - }); - expect(callCount).toBe(2); // Tool loop made 2 LLM calls - }); -}); -``` - -### 4. Simulating Multi-Turn Conversations - -To test a tool loop or conversation: - -```typescript -client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - const messages = request.params.messages; - const lastMessage = messages[messages.length - 1]; - - // Check if this is a tool result - if (lastMessage.role === "user" && lastMessage.content.type === "tool_result") { - // LLM processes tool result and provides final answer - return { - model: "test-model", - role: "assistant", - stopReason: "endTurn", - content: { - type: "text", - text: "Based on the tool result, here's my answer...", - }, - }; - } - - // Initial request - ask to use a tool - return { - model: "test-model", - role: "assistant", - stopReason: "toolUse", - content: { - type: "tool_use", - id: `tool-call-${Date.now()}`, - name: "some_tool", - input: { arg: "value" }, - } as ToolCallContent, - }; -}); -``` - -### 5. Test Structure and Cleanup Patterns - -#### Basic Test Structure - -```typescript -describe("Server with sampling", () => { - let server: Server; - let client: Client; - let clientTransport: InMemoryTransport; - let serverTransport: InMemoryTransport; - - beforeEach(async () => { - server = new Server( - { name: "test-server", version: "1.0" }, - { capabilities: {} } - ); - - client = new Client( - { name: "test-client", version: "1.0" }, - { capabilities: { sampling: {} } } - ); - - // Set up sampling handler - client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - return { - model: "test-model", - role: "assistant", - content: { type: "text", text: "Mock response" }, - }; - }); - - [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - - await Promise.all([ - client.connect(clientTransport), - server.connect(serverTransport), - ]); - }); - - afterEach(async () => { - await Promise.all([ - clientTransport.close(), - serverTransport.close(), - ]); - }); - - test("should make sampling request", async () => { - const result = await server.createMessage({ - messages: [ - { - role: "user", - content: { type: "text", text: "Hello" }, - }, - ], - maxTokens: 100, - }); - - expect(result.content.type).toBe("text"); - expect(result.role).toBe("assistant"); - }); -}); -``` - -#### Cleanup Pattern - -From `process-cleanup.test.ts`: - -```typescript -test("should exit cleanly after closing transport", async () => { - const server = new Server( - { name: "test-server", version: "1.0.0" }, - { capabilities: {} } - ); - - const transport = new StdioServerTransport(); - await server.connect(transport); - - // Close the transport - await transport.close(); - - // Test passes if we reach here without hanging - expect(true).toBe(true); -}); -``` - -## 6. Testing with StdioClientTransport - -For integration tests that spawn real server processes: - -```typescript -import { StdioClientTransport } from "../client/stdio.js"; -import { Client } from "../client/index.js"; - -describe("Integration test with real server", () => { - let client: Client; - let transport: StdioClientTransport; - - beforeEach(() => { - client = new Client( - { name: "test-client", version: "1.0" }, - { capabilities: { sampling: {} } } - ); - - // Set up handler for sampling requests from server - client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - // Simulate LLM response - return { - model: "claude-3-sonnet", - role: "assistant", - content: { - type: "text", - text: "Mock LLM response for integration test", - }, - }; - }); - - transport = new StdioClientTransport({ - command: "npx", - args: ["-y", "tsx", "path/to/your/server.ts"], - }); - }); - - afterEach(async () => { - await transport.close(); - }); - - test("should communicate with real server", async () => { - await client.connect(transport); - - // Test server capabilities - const serverCapabilities = client.getServerCapabilities(); - expect(serverCapabilities).toBeDefined(); - - // Call a tool that uses sampling - const result = await client.callTool({ - name: "ai-powered-tool", - arguments: { query: "test query" }, - }); - - expect(result.content).toBeDefined(); - }); -}); -``` - -## 7. Key Patterns from Existing Tests - -### Pattern 1: Parallel Connection Setup -```typescript -// Always connect client and server in parallel -await Promise.all([ - client.connect(clientTransport), - server.connect(serverTransport), -]); -``` - -### Pattern 2: Capability Declaration -```typescript -// Client MUST declare sampling capability to handle requests -const client = new Client( - { name: "test-client", version: "1.0" }, - { capabilities: { sampling: {} } } // Required! -); - -// Server checks client capabilities before making sampling requests -expect(server.getClientCapabilities()).toEqual({ sampling: {} }); -``` - -### Pattern 3: Request Handler Registration -```typescript -// Register handler BEFORE connecting -client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - // Handler implementation -}); - -// Then connect -await client.connect(transport); -``` - -### Pattern 4: Error Handling in Tests -```typescript -test("should throw when capability missing", async () => { - const clientWithoutSampling = new Client( - { name: "no-sampling", version: "1.0" }, - { capabilities: {} } // No sampling! - ); - - await clientWithoutSampling.connect(clientTransport); - - // Server should reject sampling request - await expect( - server.createMessage({ messages: [], maxTokens: 10 }) - ).rejects.toThrow(/Client does not support/); -}); -``` - -## 8. Testing Tool Loops - Complete Example - -```typescript -describe("Tool loop with sampling", () => { - test("should execute multi-turn tool loop", async () => { - const mcpServer = new McpServer({ - name: "tool-loop-test", - version: "1.0.0", - }); - - // Track tool executions - const toolExecutions: Array<{ name: string; input: any }> = []; - - // Register local tools that the LLM can call - const localTools = [ - { - name: "search", - description: "Search for information", - inputSchema: { - type: "object" as const, - properties: { - query: { type: "string" as const }, - }, - required: ["query"], - }, - }, - { - name: "read", - description: "Read a file", - inputSchema: { - type: "object" as const, - properties: { - path: { type: "string" as const }, - }, - required: ["path"], - }, - }, - ]; - - // Register a server tool that uses the tool loop - mcpServer.registerTool( - "ai_assistant", - { - description: "AI assistant with tool access", - inputSchema: { task: z.string() }, - }, - async ({ task }) => { - const messages: SamplingMessage[] = [ - { - role: "user", - content: { type: "text", text: task }, - }, - ]; - - let iteration = 0; - const MAX_ITERATIONS = 5; - - while (iteration < MAX_ITERATIONS) { - iteration++; - - const response = await mcpServer.server.createMessage({ - messages, - maxTokens: 1000, - tools: localTools, - tool_choice: { mode: "auto" }, - }); - - messages.push({ - role: "assistant", - content: response.content, - }); - - if (response.stopReason === "toolUse") { - const toolCall = response.content as ToolCallContent; - - toolExecutions.push({ - name: toolCall.name, - input: toolCall.input, - }); - - // Simulate tool execution - const toolResult = { - result: `Mock result for ${toolCall.name}`, - }; - - messages.push({ - role: "user", - content: { - type: "tool_result", - toolUseId: toolCall.id, - content: toolResult, - }, - }); - - continue; - } - - // Final answer - if (response.content.type === "text") { - return { - content: [{ type: "text", text: response.content.text }], - }; - } - } - - throw new Error("Max iterations exceeded"); - } - ); - - // Set up client to simulate LLM - const client = new Client( - { name: "test-client", version: "1.0" }, - { capabilities: { sampling: {} } } - ); - - let samplingCallCount = 0; - client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - samplingCallCount++; - const messages = request.params.messages; - const lastMessage = messages[messages.length - 1]; - - // First call: use search tool - if (samplingCallCount === 1) { - return { - model: "test-model", - role: "assistant", - stopReason: "toolUse", - content: { - type: "tool_use", - id: "call-1", - name: "search", - input: { query: "typescript files" }, - }, - }; - } - - // Second call: use read tool - if (samplingCallCount === 2) { - return { - model: "test-model", - role: "assistant", - stopReason: "toolUse", - content: { - type: "tool_use", - id: "call-2", - name: "read", - input: { path: "file.ts" }, - }, - }; - } - - // Third call: provide final answer - return { - model: "test-model", - role: "assistant", - stopReason: "endTurn", - content: { - type: "text", - text: "Found and analyzed the files", - }, - }; - }); - - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - - await Promise.all([ - client.connect(clientTransport), - mcpServer.server.connect(serverTransport), - ]); - - // Execute the tool - const result = await client.callTool({ - name: "ai_assistant", - arguments: { task: "Find TypeScript files" }, - }); - - // Verify - expect(samplingCallCount).toBe(3); - expect(toolExecutions).toHaveLength(2); - expect(toolExecutions[0].name).toBe("search"); - expect(toolExecutions[1].name).toBe("read"); - expect(result.content[0]).toMatchObject({ - type: "text", - text: expect.stringContaining("Found"), - }); - }); -}); -``` - -## 9. Common Pitfalls and Solutions - -### Pitfall 1: Not Declaring Capabilities -```typescript -// ❌ WRONG - will throw error -const client = new Client({ name: "test", version: "1.0" }); -client.setRequestHandler(CreateMessageRequestSchema, ...); // Throws! - -// ✅ CORRECT -const client = new Client( - { name: "test", version: "1.0" }, - { capabilities: { sampling: {} } } // Declare first! -); -client.setRequestHandler(CreateMessageRequestSchema, ...); -``` - -### Pitfall 2: Registering Handler After Connect -```typescript -// ❌ WRONG - handler not available during initialization -await client.connect(transport); -client.setRequestHandler(CreateMessageRequestSchema, ...); // Too late! - -// ✅ CORRECT -client.setRequestHandler(CreateMessageRequestSchema, ...); -await client.connect(transport); -``` - -### Pitfall 3: Not Handling Tool Loop Properly -```typescript -// ❌ WRONG - doesn't handle tool results -client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - // Always returns tool use, causing infinite loop - return { - model: "test", - role: "assistant", - stopReason: "toolUse", - content: { type: "tool_use", ... }, - }; -}); - -// ✅ CORRECT - check message history -client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - const messages = request.params.messages; - const lastMessage = messages[messages.length - 1]; - - if (lastMessage.content.type === "tool_result") { - // Provide final answer after tool use - return { - model: "test", - role: "assistant", - stopReason: "endTurn", - content: { type: "text", text: "Final answer" }, - }; - } - - // Initial request - use tool - return { ... }; -}); -``` - -## 10. File Locations Reference - -Key files for understanding test patterns: - -- **Client Tests**: `/src/client/index.test.ts` (lines 583-636 for sampling handler examples) -- **Server Tests**: `/src/server/index.test.ts` (lines 208-270, 728-864 for sampling tests) -- **InMemory Transport**: `/src/inMemory.ts` -- **Stdio Transport Tests**: - - `/src/client/stdio.test.ts` - - `/src/client/cross-spawn.test.ts` -- **Tool Loop Example**: `/src/examples/server/toolLoopSampling.ts` -- **Backfill Proxy Example**: `/src/examples/backfill/backfillSampling.ts` -- **McpServer Tests**: `/src/server/mcp.test.ts` - -## Summary - -**For unit tests of servers with sampling:** -1. Use `InMemoryTransport.createLinkedPair()` -2. Create `Client` with `capabilities: { sampling: {} }` -3. Register `CreateMessageRequestSchema` handler on client before connecting -4. Connect both client and server in parallel -5. Simulate LLM responses in the handler -6. For tool loops, track message history and alternate between tool use and final answer - -**For integration tests:** -1. Use `StdioClientTransport` to spawn real server process -2. Still need to provide sampling handler on client side -3. Test actual server behavior with realistic scenarios -4. Ensure proper cleanup of spawned processes diff --git a/intermediate-findings/toolLoopSampling-review.md b/intermediate-findings/toolLoopSampling-review.md deleted file mode 100644 index 62f7a2f98..000000000 --- a/intermediate-findings/toolLoopSampling-review.md +++ /dev/null @@ -1,542 +0,0 @@ -# Tool Loop Sampling Review - -## Summary - -The `toolLoopSampling.ts` file implements a sophisticated MCP server that demonstrates a tool loop pattern using sampling. The server exposes a `fileSearch` tool that uses an LLM (via MCP sampling) with locally-defined `ripgrep` and `read` tools to intelligently search and read files in the current directory. - -### Architecture - -The implementation follows a proxy pattern where: -1. Client calls the `fileSearch` tool with a natural language query -2. The server runs a tool loop that: - - Sends the query to an LLM via `server.createMessage()` - - The LLM decides to use `ripgrep` or `read` tools (defined locally) - - The server executes these tools locally - - Results are fed back to the LLM - - Process repeats until the LLM provides a final answer -3. The final answer is returned to the client - ---- - -## What's Done Well - -### 1. **Clear Separation of Concerns** -- Path safety logic is isolated in `ensureSafePath()` -- Tool execution is separated from tool loop orchestration -- Local tool definitions are cleanly defined in `LOCAL_TOOLS` constant - -### 2. **Proper Error Handling** -- Path validation with security checks -- Graceful handling of ripgrep exit codes (0 and 1 are both success) -- Error handling in tool execution functions returns structured error objects -- Top-level try-catch in the `fileSearch` handler - -### 3. **Good Documentation** -- Comprehensive file-level comments explaining the purpose -- Clear usage instructions -- Function-level JSDoc comments - -### 4. **Security Considerations** -- Path canonicalization to prevent directory traversal attacks -- Working directory constraint enforcement - -### 5. **Tool Loop Pattern** -- Implements a proper agentic loop with iteration limits -- Correctly handles tool use responses -- Properly constructs message history - ---- - -## Issues Found - -### 1. **Critical: Incorrect Content Type Handling** ⚠️ - -**Location:** Lines 214-215 - -```typescript -if (response.stopReason === "toolUse" && response.content.type === "tool_use") { - const toolCall = response.content as ToolCallContent; -``` - -**Problem:** According to the MCP protocol types (lines 1361-1366 in `types.ts`), `CreateMessageResult.content` is a **discriminated union**, not an array. The code correctly handles this as a single content block. However, the condition checks both `stopReason` and `content.type`, which is redundant. - -**Impact:** This works but is redundant. When `stopReason === "toolUse"`, the content type is guaranteed to be `"tool_use"`. - -**Fix:** Simplify the condition: -```typescript -if (response.stopReason === "toolUse") { - const toolCall = response.content as ToolCallContent; -``` - -### 2. **Type Safety Issue: Message Content Structure** ⚠️ - -**Location:** Lines 183-191, 208-211 - -```typescript -const messages: SamplingMessage[] = [ - { - role: "user", - content: { - type: "text", - text: initialQuery, - }, - }, -]; -``` - -**Problem:** According to `SamplingMessageSchema` (lines 1285-1288 in `types.ts`), the schema uses a discriminated union. The code structure is correct, but TypeScript may not enforce this properly without explicit type annotations. - -**Impact:** Currently works, but could lead to type errors if the content structure changes. - -**Fix:** Add explicit type annotation: -```typescript -const messages: SamplingMessage[] = [ - { - role: "user", - content: { - type: "text", - text: initialQuery, - } as TextContent, - } as UserMessage, -]; -``` - -### 3. **Edge Case: Empty Content Block** ⚠️ - -**Location:** Lines 244-247 - -```typescript -// LLM provided final answer -if (response.content.type === "text") { - return response.content.text; -} -``` - -**Problem:** The code assumes that if the content type is "text", it has a valid `text` field. While this should always be true according to the protocol, there's no null/empty check. - -**Impact:** Could potentially return an empty string if the LLM returns empty text content. - -**Fix:** Add validation: -```typescript -if (response.content.type === "text") { - const text = response.content.text; - if (!text) { - throw new Error("LLM returned empty text content"); - } - return text; -} -``` - -### 4. **Potential Race Condition: System Prompt Injection** - -**Location:** Lines 283-290 - -```typescript -const systemPrompt = - "You are a helpful assistant that searches through files to answer questions. " + - "You have access to ripgrep (for searching) and read (for reading file contents). " + - "Use ripgrep to find relevant files, then read them to provide accurate answers. " + - "All paths are relative to the current working directory. " + - "Be concise and focus on providing the most relevant information."; - -const fullQuery = `${systemPrompt}\n\nUser query: ${query}`; -``` - -**Problem:** The system prompt is injected into the user message rather than using the `systemPrompt` parameter of `createMessage()`. This is not following MCP best practices. - -**Impact:** -- The LLM sees this as part of the user message, not as a system instruction -- Less effective instruction following -- Deviates from the protocol design - -**Fix:** Use the proper parameter: -```typescript -const response: CreateMessageResult = await server.server.createMessage({ - messages, - systemPrompt: "You are a helpful assistant that searches through files...", - maxTokens: 4000, - tools: LOCAL_TOOLS, - tool_choice: { mode: "auto" }, -}); -``` - -And remove the system prompt from the initial query: -```typescript -const messages: SamplingMessage[] = [ - { - role: "user", - content: { - type: "text", - text: initialQuery, // Just the query, not the system prompt - }, - }, -]; -``` - -### 5. **Missing: Tool Input Validation** - -**Location:** Lines 157-173 - -```typescript -async function executeLocalTool( - toolName: string, - toolInput: Record -): Promise> { - switch (toolName) { - case "ripgrep": { - const { pattern, path } = toolInput as { pattern: string; path: string }; - return await executeRipgrep(pattern, path); - } - case "read": { - const { path } = toolInput as { path: string }; - return await executeRead(path); - } -``` - -**Problem:** No validation that the input actually contains the required fields or that they are strings. - -**Impact:** Could crash with unhelpful errors if the LLM provides malformed input. - -**Fix:** Add validation: -```typescript -case "ripgrep": { - if (typeof toolInput.pattern !== 'string' || typeof toolInput.path !== 'string') { - return { error: 'Invalid input: pattern and path must be strings' }; - } - const { pattern, path } = toolInput as { pattern: string; path: string }; - return await executeRipgrep(pattern, path); -} -``` - -### 6. **Inconsistent Logging** - -**Location:** Lines 217-228, 281, 294 - -**Problem:** Some operations log detailed information, others don't. The logging uses `console.error` inconsistently. - -**Impact:** Makes debugging harder; inconsistent user experience. - -**Fix:** Add consistent logging at key points: -- Before and after LLM calls -- Tool execution start/end -- Error conditions - -### 7. **Tool Result Structure Mismatch** - -**Location:** Lines 230-238 - -```typescript -// Add tool result to message history -messages.push({ - role: "user", - content: { - type: "tool_result", - toolUseId: toolCall.id, - content: toolResult, - }, -}); -``` - -**Problem:** According to `ToolResultContentSchema` (lines 873-893 in `types.ts`), the `content` field should be a passthrough object. The current implementation passes `toolResult` which is `Record`, which is correct. However, the tool execution functions return objects with `{ output?: string; error?: string }` or `{ content?: string; error?: string }`, which is inconsistent. - -**Impact:** This works but creates an inconsistent structure for tool results. - -**Fix:** Standardize tool result format: -```typescript -interface ToolExecutionResult { - success: boolean; - data?: unknown; - error?: string; -} -``` - ---- - -## Suggested Improvements - -### 1. **Use Zod for Input Validation** - -The code already imports `z` from zod but only uses it for the tool registration. Consider using Zod schemas to validate tool inputs: - -```typescript -const RipgrepInputSchema = z.object({ - pattern: z.string(), - path: z.string(), -}); - -const ReadInputSchema = z.object({ - path: z.string(), -}); - -async function executeLocalTool( - toolName: string, - toolInput: Record -): Promise> { - try { - switch (toolName) { - case "ripgrep": { - const validated = RipgrepInputSchema.parse(toolInput); - return await executeRipgrep(validated.pattern, validated.path); - } - case "read": { - const validated = ReadInputSchema.parse(toolInput); - return await executeRead(validated.path); - } - default: - return { error: `Unknown tool: ${toolName}` }; - } - } catch (error) { - return { - error: error instanceof Error ? error.message : "Validation error", - }; - } -} -``` - -### 2. **Add Configurable Parameters** - -Consider making some hardcoded values configurable: - -```typescript -interface ToolLoopConfig { - maxIterations?: number; - maxTokens?: number; - workingDirectory?: string; - ripgrepMaxCount?: number; -} - -async function runToolLoop( - server: McpServer, - initialQuery: string, - config: ToolLoopConfig = {} -): Promise { - const MAX_ITERATIONS = config.maxIterations ?? 10; - const maxTokens = config.maxTokens ?? 4000; - // ... -} -``` - -### 3. **Improve Error Messages** - -Currently, tool errors are opaque. Consider adding more context: - -```typescript -return { - error: `Failed to read file '${inputPath}': ${error.message}`, - errorCode: 'FILE_READ_ERROR', - filePath: inputPath, -}; -``` - -### 4. **Add Tool Execution Timeout** - -Long-running ripgrep searches could hang. Consider adding timeouts: - -```typescript -const TOOL_EXECUTION_TIMEOUT = 30000; // 30 seconds - -async function executeRipgrep( - pattern: string, - path: string -): Promise<{ output?: string; error?: string }> { - return Promise.race([ - actualRipgrepExecution(pattern, path), - new Promise<{ error: string }>((resolve) => - setTimeout( - () => resolve({ error: 'Tool execution timeout' }), - TOOL_EXECUTION_TIMEOUT - ) - ), - ]); -} -``` - -### 5. **Better Comparison with Existing Examples** - -Compared to `toolWithSampleServer.ts`: -- ✅ **More sophisticated**: Implements a full agentic loop vs simple one-shot sampling -- ✅ **Better demonstrates tool calling**: Shows recursive tool use -- ⚠️ **More complex**: Could be harder for users to understand - -Compared to `backfillSampling.ts`: -- ✅ **Simpler scope**: Focused on server-side tool loop vs full proxy -- ✅ **Better demonstrates local tools**: Shows how to define and execute tools locally -- ✅ **More practical example**: Real-world use case (file search) - -### 6. **Consider Using `tool_choice: { mode: "required" }` Initially** - -For the first iteration, you might want to ensure the LLM uses a tool: - -```typescript -const response: CreateMessageResult = await server.server.createMessage({ - messages, - maxTokens: 4000, - tools: LOCAL_TOOLS, - tool_choice: iteration === 1 ? { mode: "required" } : { mode: "auto" }, -}); -``` - -This ensures the LLM doesn't try to answer without searching first. - -### 7. **Add Debug Mode** - -```typescript -const DEBUG = process.env.DEBUG === 'true'; - -function debug(...args: unknown[]) { - if (DEBUG) { - console.error('[toolLoop DEBUG]', ...args); - } -} -``` - ---- - -## Path Safety Analysis - -The path canonicalization logic is **mostly correct** but has a subtle issue: - -```typescript -function ensureSafePath(inputPath: string): string { - const resolved = resolve(CWD, inputPath); - const rel = relative(CWD, resolved); - - // Check if the path escapes CWD (starts with .. or is absolute outside CWD) - if (rel.startsWith("..") || resolve(CWD, rel) !== resolved) { - throw new Error(`Path "${inputPath}" is outside the current directory`); - } - - return resolved; -} -``` - -**Issue:** The second condition `resolve(CWD, rel) !== resolved` is always false if the first condition is false, making it redundant. - -**Better approach:** - -```typescript -function ensureSafePath(inputPath: string): string { - const resolved = resolve(CWD, inputPath); - const rel = relative(CWD, resolved); - - // Check if the path escapes CWD - if (rel.startsWith("..") || path.isAbsolute(rel)) { - throw new Error(`Path "${inputPath}" is outside the current directory`); - } - - return resolved; -} -``` - -**Edge cases to consider:** -- Symlinks could bypass this check (consider using `realpath`) -- Windows paths with drive letters -- UNC paths on Windows - ---- - -## Best Practices Compliance - -### TypeScript Best Practices -- ✅ Uses strict type checking -- ✅ Explicit return types on functions -- ⚠️ Could use more type guards and validation -- ✅ Good use of const and let -- ✅ Proper async/await usage - -### MCP SDK Best Practices -- ⚠️ **System prompt should use `systemPrompt` parameter, not be in user message** -- ✅ Proper message history management -- ✅ Correct tool result format -- ✅ Proper use of `createMessage()` API -- ✅ Good tool schema definitions - -### Error Handling -- ✅ Try-catch blocks in appropriate places -- ✅ Structured error objects -- ⚠️ Could benefit from custom error types -- ⚠️ Some error messages could be more descriptive - -### Code Quality -- ✅ Well-structured and readable -- ✅ Good function decomposition -- ✅ Appropriate use of constants -- ⚠️ Could benefit from more validation -- ⚠️ Some minor type safety improvements needed - ---- - -## Recommendations - -### Priority 1 (Must Fix Before Commit) - -1. **Fix system prompt handling** - Use the `systemPrompt` parameter in `createMessage()` instead of prepending to user query -2. **Add input validation** - Validate tool inputs before execution -3. **Simplify redundant conditions** - Remove redundant check on line 214 - -### Priority 2 (Should Fix) - -1. **Add empty content validation** - Check for empty text responses -2. **Improve path safety** - Consider symlink handling -3. **Standardize tool result format** - Use consistent structure for all tool results -4. **Add better logging** - Consistent, structured logging throughout - -### Priority 3 (Nice to Have) - -1. **Add configuration options** - Make hardcoded values configurable -2. **Add timeouts** - Prevent hung tool executions -3. **Add debug mode** - Better debugging experience -4. **Consider using `tool_choice: required` initially** - Ensure LLM searches before answering - ---- - -## Code Quality Assessment - -| Aspect | Rating | Notes | -|--------|--------|-------| -| Correctness | 8/10 | Works but has minor issues with system prompt handling | -| Security | 8/10 | Good path validation but could handle symlinks | -| Error Handling | 7/10 | Good structure but needs more validation | -| Type Safety | 7/10 | Good but could be stricter with type guards | -| Readability | 9/10 | Well-documented and structured | -| Best Practices | 7/10 | Good but system prompt handling needs fix | -| **Overall** | **7.5/10** | Good quality, needs minor fixes before commit | - ---- - -## Verdict - -**Status: Needs Minor Changes Before Commit** - -The code demonstrates a sophisticated understanding of the MCP sampling API and implements a useful, practical example. However, there are a few issues that should be addressed: - -1. **Must fix:** System prompt handling (use `systemPrompt` parameter) -2. **Should fix:** Add input validation -3. **Should fix:** Simplify redundant conditions - -Once these issues are addressed, this will be an excellent example of MCP tool loop patterns and should be committed. - ---- - -## Testing Recommendations - -Before committing, test the following scenarios: - -1. **Happy path**: Query that requires multiple tool calls -2. **Edge cases**: - - Empty query - - Path traversal attempts (`../`, `../../`, etc.) - - Non-existent files - - Very large search results - - Binary files -3. **Error conditions**: - - Malformed tool inputs from LLM - - ripgrep not installed - - Permission denied errors - - Max iterations exceeded -4. **Security**: - - Symlink handling - - Absolute paths - - Special characters in paths - -Consider adding a unit test file (`toolLoopSampling.test.ts`) to cover these scenarios. diff --git a/intermediate-findings/toolLoopSampling-test-review.md b/intermediate-findings/toolLoopSampling-test-review.md deleted file mode 100644 index 7e668ab38..000000000 --- a/intermediate-findings/toolLoopSampling-test-review.md +++ /dev/null @@ -1,249 +0,0 @@ -# Test Review: toolLoopSampling.test.ts - -## Executive Summary - -The test file `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.test.ts` is **ready to commit with minor improvements recommended**. The tests are well-structured, provide good coverage of the tool loop functionality, and follow project conventions. However, there are opportunities for simplification and improved maintainability. - -## Test Coverage Analysis - -### Covered Scenarios ✓ - -1. **Happy Path**: Tool loop with sequential ripgrep and read operations (lines 57-165) -2. **Error Handling**: - - Path validation errors (lines 167-251) - - Invalid tool names (lines 253-331) - - Malformed tool inputs (lines 333-413) -3. **Edge Cases**: - - Maximum iteration limit enforcement (lines 415-470) - -### Coverage Assessment - -**Score: 8/10** - -**Strengths:** -- Tests the complete tool loop flow from initial query to final answer -- Validates error propagation and handling at multiple levels -- Confirms iteration limit protection against infinite loops -- Tests input validation (both tool name and tool parameters) - -**Gaps:** -- No test for successful multi-iteration loops (e.g., ripgrep → read → ripgrep again → answer) -- Missing test for stopReason variants beyond "toolUse" and "endTurn" -- No explicit test for tool result error handling when tool execution fails but returns gracefully -- No test for empty/edge case responses from ripgrep (e.g., "No matches found") - -## Code Quality Assessment - -### Structure & Organization - -**Score: 9/10** - -- Consistent test structure across all test cases -- Good use of `beforeEach` and `afterEach` for setup/teardown -- Clear test names that describe the scenario being tested -- Proper Jest timeout configuration for integration tests - -### Best Practices Adherence - -**Comparison with project patterns:** - -| Pattern | toolLoopSampling.test.ts | server/index.test.ts | client/index.test.ts | Assessment | -|---------|--------------------------|----------------------|----------------------|------------| -| Transport management | Uses StdioClientTransport | Uses InMemoryTransport | Uses mock Transport | ✓ Appropriate for integration | -| Client/Server setup | Creates real client+server | Creates real client+server | Uses mocks | ✓ Correct pattern | -| Handler setup | Uses setRequestHandler | Uses setRequestHandler | Uses setRequestHandler | ✓ Consistent | -| Assertions | Uses expect().toBe/toContain | Uses expect().toBe/toEqual | Uses expect().toBe | ✓ Standard Jest | -| Error testing | Tests via result content | Uses rejects.toThrow | Uses rejects.toThrow | ⚠️ Could be more explicit | - -**Observations:** -- **Positive**: The test properly spawns the actual server using `npx tsx`, making it a true integration test -- **Positive**: Uses `resolve(__dirname, ...)` for reliable path resolution -- **Concern**: Heavy reliance on `console.error` output for debugging (lines 71-73, etc.) -- **Concern**: Sampling handler complexity increases with each test case - -## Duplication Analysis - -### Identified Duplication - -1. **Client/Transport Setup** (repeated in every `beforeEach`): - ```typescript - // Lines 25-51 - repeated setup code - client = new Client(...) - transport = new StdioClientTransport(...) - ``` - -2. **Sampling Handler Pattern** (repeated 5 times): - ```typescript - // Lines 62-134, 171-229, 258-312, 338-394, 419-439 - client.setRequestHandler(CreateMessageRequestSchema, async (request) => { - samplingCallCount++; - const messages = request.params.messages; - const lastMessage = messages[messages.length - 1]; - // ... different logic per test - }) - ``` - -3. **Connection and Tool Call** (repeated 5 times): - ```typescript - // Lines 136-151, 231-245, 314-327, 396-409, 442-456 - await client.connect(transport); - const result = await client.request({ method: "tools/call", ... }) - ``` - -### Duplication Impact - -**Score: 6/10** (significant duplication, but manageable) - -## Simplification Opportunities - -### High Priority - -1. **Extract Helper Function for Sampling Handler Setup** - ```typescript - // Proposed helper - function createMockSamplingHandler(responses: Array) { - let callIndex = 0; - return async (request: CreateMessageRequest): Promise => { - const response = responses[callIndex]; - callIndex++; - if (!response) { - throw new Error(`Unexpected sampling call count: ${callIndex}`); - } - return { - model: "test-model", - role: "assistant", - content: response, - stopReason: response.type === "tool_use" ? "toolUse" : "endTurn", - }; - }; - } - ``` - **Benefit**: Reduce 150+ lines of duplicated handler setup code - -2. **Extract Helper for Common Test Flow** - ```typescript - // Proposed helper - async function executeFileSearchTest( - client: Client, - transport: StdioClientTransport, - query: string, - expectedSamplingCalls: number - ) { - await client.connect(transport); - const result = await client.request( - { method: "tools/call", params: { name: "fileSearch", arguments: { query } } }, - CallToolResultSchema - ); - return { result, samplingCallCount: /* tracked value */ }; - } - ``` - **Benefit**: Reduce boilerplate in each test - -3. **Simplify Error Assertions** - ```typescript - // Current (lines 199-210): - expect(lastMessage.content.type).toBe("tool_result"); - if (lastMessage.content.type === "tool_result") { - const content = lastMessage.content.content as Record; - expect(content.error).toBeDefined(); - expect(typeof content.error === "string" && content.error.includes("...")).toBe(true); - } - - // Proposed: - expectToolResultError(lastMessage, "outside the current directory"); - ``` - **Benefit**: Improve readability and maintainability - -### Medium Priority - -4. **Remove Verbose Console Logging** - - Lines 71-73: Console.error statements should use a debug flag or be removed - - These logs are helpful during development but clutter test output - -5. **Consolidate Type Assertions** - - Lines 159-161, 200-210, 284-295: Repeated type narrowing patterns - - Create a helper: `assertTextContent(content)` or use type guards - -### Low Priority - -6. **Use Test.each for Similar Tests** - - Tests for "invalid tool names" and "malformed tool inputs" follow similar patterns - - Could be parameterized to reduce code - -## Clarity Assessment - -**Score: 8/10** - -### Strengths -- Test names clearly describe what's being tested -- Comments explain the test scenario (lines 2-6, 57, etc.) -- Logical flow is easy to follow -- Good use of descriptive variable names - -### Areas for Improvement -1. **Overly Complex Inline Handlers**: The sampling handlers contain significant logic that makes tests harder to understand at a glance -2. **Mixed Concerns**: Tests verify both the sampling call count AND the result content, which could be split -3. **Implicit Behavior**: The interaction between samplingCallCount and handler logic requires mental tracking - -## Comparison with Project Test Patterns - -### server/index.test.ts Patterns -- ✓ Uses descriptive test names -- ✓ Groups related tests with `describe` blocks -- ✓ Uses `beforeEach`/`afterEach` consistently -- ✓ Tests both success and error cases -- ⚠️ toolLoopSampling uses more complex integration setup (spawning process) - -### client/index.test.ts Patterns -- ✓ Similar assertion patterns -- ✓ Uses `expect().toBe()`, `expect().toContain()` -- ✓ Tests timeout and cancellation scenarios -- ⚠️ toolLoopSampling has more verbose test bodies - -### protocol.test.ts Patterns -- ✓ Good use of jest.fn() for mocking -- ✓ Clean setup/teardown -- ✓ Focused test cases -- ⚠️ toolLoopSampling could benefit from similar spy usage - -## Recommendations - -### Must Fix (Before Commit) -None - the tests are functional and pass. - -### Should Fix (High Value) -1. **Extract sampling handler helper** - Reduces duplication by ~40% -2. **Add helper for error content assertions** - Improves readability -3. **Remove or gate console.error debug statements** - Cleaner test output - -### Nice to Have (Future Improvements) -1. Add test for successful multi-step tool loop (ripgrep → read → ripgrep → answer) -2. Add test for edge cases in tool results (empty results, no matches) -3. Consider parameterized tests for error scenarios -4. Add JSDoc comments to explain the test strategy - -## Verdict - -**Status: READY TO COMMIT AS-IS** - -The tests are well-designed, follow project conventions, and provide solid coverage of the tool loop sampling functionality. While there are opportunities for refactoring to reduce duplication and improve clarity, the current implementation is: - -- ✓ Functional and passing -- ✓ Covers main scenarios adequately -- ✓ Follows TypeScript and Jest best practices -- ✓ Provides good error coverage -- ✓ Tests important edge cases (iteration limit, validation errors) - -**Recommended Action:** -1. Commit the tests as-is to establish baseline coverage -2. Create a follow-up task to implement the suggested refactorings -3. Consider adding the recommended test cases for multi-step loops in a future iteration - -## Files Analyzed - -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.test.ts` (main test file) -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/toolLoopSampling.ts` (implementation) -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/server/index.test.ts` (reference patterns) -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/client/index.test.ts` (reference patterns) -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/shared/protocol.test.ts` (reference patterns) -- `/Users/ochafik/code/modelcontextprotocol-typescript-sdk/src/examples/server/demoInMemoryOAuthProvider.test.ts` (reference patterns) diff --git a/intermediate-findings/transport-analysis.md b/intermediate-findings/transport-analysis.md deleted file mode 100644 index 14c47778e..000000000 --- a/intermediate-findings/transport-analysis.md +++ /dev/null @@ -1,960 +0,0 @@ -# MCP TypeScript SDK Transport Architecture Analysis - -## Executive Summary - -This document provides a comprehensive analysis of the transport layer implementation in the MCP TypeScript SDK, focusing on how messages flow through the system, validation mechanisms, error handling, and potential areas for improvement. - -## 1. Architecture Overview - -### 1.1 Core Transport Architecture - -The MCP SDK uses a **layered architecture** with clear separation of concerns: - -``` -┌─────────────────────────────────────────────────────────┐ -│ Client / Server (High-level API) │ -│ - Client class (src/client/index.ts) │ -│ - Server class (src/server/index.ts) │ -└────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ Protocol Layer │ -│ - Protocol class (src/shared/protocol.ts) │ -│ - Request/Response handling │ -│ - Progress tracking │ -│ - Capability enforcement │ -│ - Timeout management │ -└────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ Transport Interface │ -│ (src/shared/transport.ts) │ -│ - start(), send(), close() │ -│ - onmessage, onerror, onclose callbacks │ -└────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────┐ -│ Transport Implementations │ -│ - StdioClientTransport │ -│ - SSEClientTransport / SSEServerTransport │ -│ - StreamableHTTPClientTransport / Server │ -│ - InMemoryTransport (testing) │ -└─────────────────────────────────────────────────────────┘ -``` - -### 1.2 Key Design Principles - -1. **Transport Agnostic**: The Protocol layer is completely independent of the transport mechanism -2. **Callback-Based**: Transport implementations use callbacks (`onmessage`, `onerror`, `onclose`) to push data up -3. **Async Operations**: All transport operations return Promises -4. **Schema Validation**: Zod schemas validate messages at the protocol boundary - -## 2. Key Files and Their Purposes - -### 2.1 Core Transport Files - -#### `/src/shared/transport.ts` (86 lines) -**Purpose**: Defines the Transport interface contract - -**Key Types**: -- `Transport` interface: The contract all transport implementations must fulfill -- `TransportSendOptions`: Options for sending messages (relatedRequestId, resumption tokens) -- `FetchLike`: Type for custom fetch implementations - -**Core Interface**: -```typescript -interface Transport { - start(): Promise; // Initialize connection - send(message: JSONRPCMessage, options?: TransportSendOptions): Promise; - close(): Promise; - - // Callbacks - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; - - // Optional features - sessionId?: string; - setProtocolVersion?: (version: string) => void; -} -``` - -#### `/src/shared/protocol.ts` (785 lines) -**Purpose**: Implements the MCP protocol layer on top of any transport - -**Key Responsibilities**: -1. **Request/Response Correlation**: Maps request IDs to response handlers -2. **Progress Tracking**: Handles progress notifications via progress tokens -3. **Timeout Management**: Implements per-request timeouts with optional progress-based reset -4. **Capability Enforcement**: Validates that requests match advertised capabilities -5. **Request Cancellation**: Supports AbortSignal-based cancellation -6. **Notification Debouncing**: Can coalesce multiple notifications in the same tick -7. **Request Handler Management**: Routes incoming requests to registered handlers - -**Key Data Structures**: -```typescript -private _requestMessageId = 0; // Monotonic counter for request IDs -private _requestHandlers: Map -private _notificationHandlers: Map -private _responseHandlers: Map -private _progressHandlers: Map -private _timeoutInfo: Map -private _pendingDebouncedNotifications: Set -``` - -**Critical Design Pattern - Transport Capture**: -The protocol uses a **transport capture pattern** in `_onrequest()` to ensure responses go to the correct client when multiple connections exist: - -```typescript -// Capture the current transport at request time -const capturedTransport = this._transport; - -// Use capturedTransport for sending responses, not this._transport -``` - -This prevents a race condition where reconnections could send responses to the wrong client. - -### 2.2 Transport Implementations - -#### Stdio Transport (`/src/shared/stdio.ts`, `/src/client/stdio.ts`) -- **Communication**: Line-delimited JSON over stdin/stdout -- **Serialization**: `serializeMessage()` adds newline, `deserializeMessage()` uses Zod validation -- **Buffering**: `ReadBuffer` class handles partial message buffering -- **Use Case**: Local process communication, MCP servers as child processes - -#### SSE Transport (Client: `/src/client/sse.ts`, Server: `/src/server/sse.ts`) -- **Client Receives**: Via Server-Sent Events (GET request with EventSource) -- **Client Sends**: Via POST requests to endpoint provided by server -- **Server Receives**: POST requests with JSON body -- **Server Sends**: SSE stream with `event: message` format -- **Authentication**: Integrated OAuth support with UnauthorizedError handling -- **Security**: DNS rebinding protection (optional, configurable) - -#### Streamable HTTP Transport -**Client** (`/src/client/streamableHttp.ts` - 570 lines): -- Most complex transport implementation -- Bidirectional HTTP with SSE for server-to-client messages -- **Reconnection Logic**: Exponential backoff with configurable parameters -- **Resumability**: Last-Event-ID header for resuming interrupted streams -- **Session Management**: Tracks session ID across reconnections - -**Server** (`/src/server/streamableHttp.ts`): -- Stateful (session-based) and stateless modes -- Event store interface for resumability support -- Stream management for multiple concurrent requests - -#### InMemory Transport (`/src/inMemory.ts` - 64 lines) -- Testing-only transport -- Direct in-memory message passing -- Useful for unit tests and integration tests - -### 2.3 Protocol Types - -#### `/src/types.ts` (1500+ lines) -**Purpose**: Central type definitions using Zod schemas - -**Key Message Types**: -```typescript -// Base types -JSONRPCRequest // { jsonrpc: "2.0", id, method, params } -JSONRPCNotification // { jsonrpc: "2.0", method, params } -JSONRPCResponse // { jsonrpc: "2.0", id, result } -JSONRPCError // { jsonrpc: "2.0", id, error: { code, message, data? }} -``` - -**Error Codes**: -```typescript -enum ErrorCode { - // SDK-specific - ConnectionClosed = -32000, - RequestTimeout = -32001, - - // JSON-RPC standard - ParseError = -32700, - InvalidRequest = -32600, - MethodNotFound = -32601, - InvalidParams = -32602, - InternalError = -32603, -} -``` - -**MessageExtraInfo** (line 1551): -```typescript -interface MessageExtraInfo { - requestInfo?: RequestInfo; // HTTP headers, etc. - authInfo?: AuthInfo; // OAuth token info -} -``` - -## 3. Message Flow - -### 3.1 Outgoing Request Flow (Client -> Server) - -``` -┌──────────────────┐ -│ Client.request()│ -└────────┬─────────┘ - │ - ▼ -┌────────────────────────────────────────┐ -│ Protocol.request() │ -│ - Generate message ID │ -│ - Add progress token if requested │ -│ - Register response handler │ -│ - Set up timeout │ -│ - Set up cancellation handler │ -└────────┬───────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────┐ -│ Transport.send(JSONRPCRequest) │ -│ - Serialize message │ -│ - Send over wire (HTTP/SSE/stdio) │ -└────────────────────────────────────────┘ -``` - -### 3.2 Incoming Request Flow (Server <- Client) - -``` -┌────────────────────────────────────────┐ -│ Transport receives data │ -│ - Parse JSON │ -│ - Validate with Zod schema │ -└────────┬───────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────┐ -│ Transport.onmessage(message, extra) │ -│ - extra contains authInfo, headers │ -└────────┬───────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────┐ -│ Protocol._onrequest() │ -│ - Capture current transport │ -│ - Look up handler │ -│ - Create AbortController │ -│ - Build RequestHandlerExtra context │ -│ - Execute handler │ -└────────┬───────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────┐ -│ Request Handler │ -│ - Business logic │ -│ - Can send notifications │ -│ - Can make requests │ -│ - Returns Result │ -└────────┬───────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────┐ -│ Send Response │ -│ - Use capturedTransport.send() │ -│ - Send JSONRPCResponse or Error │ -└────────────────────────────────────────┘ -``` - -### 3.3 Progress Notification Flow - -``` -Client Request (with onprogress) - │ - ▼ -Request includes _meta: { progressToken: messageId } - │ - ▼ -Server Handler receives request - │ - ▼ -Server sends: notifications/progress - { params: { progressToken, progress, total, message? }} - │ - ▼ -Client Protocol._onprogress() - - Look up handler by progressToken - - Optionally reset timeout - - Call onprogress callback -``` - -## 4. Validation Mechanisms - -### 4.1 Schema Validation (Zod) - -**Where**: At the transport boundary when messages are received - -**Implementation**: -```typescript -// In shared/stdio.ts -export function deserializeMessage(line: string): JSONRPCMessage { - return JSONRPCMessageSchema.parse(JSON.parse(line)); -} - -// In SSE transports -const message = JSONRPCMessageSchema.parse(JSON.parse(messageEvent.data)); - -// In protocol request handlers -this._requestHandlers.set(method, (request, extra) => { - return Promise.resolve(handler(requestSchema.parse(request), extra)); -}); -``` - -**Validation Levels**: -1. **Message Structure**: JSONRPCMessageSchema validates basic JSON-RPC structure -2. **Request Params**: Individual request schemas validate params structure -3. **Result Schema**: Response results are validated against expected schemas -4. **Type Guards**: Helper functions like `isJSONRPCRequest()`, `isJSONRPCResponse()` - -### 4.2 Capability Validation - -**Where**: In Client and Server classes before sending requests - -**Methods**: -- `assertCapabilityForMethod()`: Checks remote side supports the request method -- `assertNotificationCapability()`: Checks local side advertised notification support -- `assertRequestHandlerCapability()`: Checks local side advertised handler support - -**Example from Client**: -```typescript -protected assertCapabilityForMethod(method: RequestT["method"]): void { - switch (method as ClientRequest["method"]) { - case "tools/call": - case "tools/list": - if (!this._serverCapabilities?.tools) { - throw new Error( - `Server does not support tools (required for ${method})` - ); - } - break; - // ... more cases - } -} -``` - -**Enforcement**: Optional via `enforceStrictCapabilities` option (defaults to false for backwards compatibility) - -### 4.3 Client-Side Tool Output Validation - -**Special Case**: Client validates tool call results against declared output schemas - -**Implementation** (`/src/client/index.ts`, lines 429-479): -```typescript -async callTool(params, resultSchema, options) { - const result = await this.request(...); - - const validator = this.getToolOutputValidator(params.name); - if (validator) { - // Tool with outputSchema MUST return structuredContent - if (!result.structuredContent && !result.isError) { - throw new McpError(...); - } - - // Validate structured content against schema - if (result.structuredContent) { - const isValid = validator(result.structuredContent); - if (!isValid) { - throw new McpError(...); - } - } - } - - return result; -} -``` - -**Why This Exists**: Ensures servers respect their own tool output schemas - -### 4.4 Transport-Specific Validation - -#### DNS Rebinding Protection -Both SSE and StreamableHTTP server transports support optional DNS rebinding protection: - -```typescript -private validateRequestHeaders(req: IncomingMessage): string | undefined { - if (!this._enableDnsRebindingProtection) return undefined; - - // Validate Host header - if (this._allowedHosts && !this._allowedHosts.includes(req.headers.host)) { - return `Invalid Host header`; - } - - // Validate Origin header - if (this._allowedOrigins && !this._allowedOrigins.includes(req.headers.origin)) { - return `Invalid Origin header`; - } - - return undefined; -} -``` - -#### Session Validation (StreamableHTTP stateful mode) -- Validates session ID in headers matches expected session -- Returns 404 if session not found -- Returns 400 if non-initialization request lacks session ID - -## 5. Error Handling - -### 5.1 Error Types and Hierarchy - -``` -Error (JavaScript base) - │ - ├─ McpError (general MCP errors with error codes) - │ - ConnectionClosed - │ - RequestTimeout - │ - ParseError - │ - InvalidRequest - │ - MethodNotFound - │ - InvalidParams - │ - InternalError - │ - ├─ TransportError (transport-specific errors) - │ ├─ SseError (SSE transport errors) - │ ├─ StreamableHTTPError (Streamable HTTP errors) - │ └─ UnauthorizedError (authentication failures) - │ - └─ OAuthError (authentication/authorization errors) - └─ [Many specific OAuth error types] -``` - -### 5.2 Error Handling Patterns - -#### Protocol Layer Error Handling - -**Request Handler Errors**: -```typescript -Promise.resolve() - .then(() => handler(request, fullExtra)) - .then( - (result) => capturedTransport?.send({ result, ... }), - (error) => capturedTransport?.send({ - error: { - code: Number.isSafeInteger(error["code"]) ? error["code"] : ErrorCode.InternalError, - message: error.message ?? "Internal error" - } - }) - ) - .catch((error) => this._onerror(new Error(`Failed to send response: ${error}`))) -``` - -**Key Behaviors**: -1. Errors are caught and converted to JSON-RPC error responses -2. If error has numeric `code`, it's preserved; otherwise defaults to InternalError -3. If sending the error response fails, it's reported via `onerror` callback -4. Aborted requests don't send responses - -#### Transport Layer Error Handling - -**Stdio Transport**: -- Zod validation errors reported via `onerror` -- Process spawn errors reject start() promise -- Stream errors reported via `onerror` - -**SSE/StreamableHTTP Transports**: -- Network errors caught and reported via `onerror` -- 401 responses trigger authentication flow (if authProvider present) -- Connection close triggers cleanup and `onclose` callback -- Reconnection with exponential backoff (StreamableHTTP only) - -### 5.3 Error Propagation - -``` -Transport Layer - └─> onerror(error) callback - │ - ▼ -Protocol Layer - └─> this.onerror(error) callback - │ - ▼ -Client/Server Layer - └─> User-defined onerror handler -``` - -**Request/Response Errors**: -- Returned as rejected Promise from `request()` method -- Either McpError (from remote) or Error (local/transport) - -**Notification Handler Errors**: -- Caught and reported via `onerror` -- Do not affect other operations - -## 6. Gaps and Potential Issues - -### 6.1 Validation Gaps - -#### 6.1.1 Incomplete Message Validation -**Issue**: While JSON-RPC structure is validated, there's limited validation of: -- Message content before sending (only validated on receive) -- Semantic correctness of method names -- Parameter structure matching method requirements - -**Evidence**: -- No validation in `Protocol.request()` that `request.method` is valid before sending -- Transport implementations assume messages are well-formed - -**Impact**: Invalid messages can be sent and only caught by the receiver - -#### 6.1.2 No Validation of TransportSendOptions -**Issue**: `TransportSendOptions` (relatedRequestId, resumptionToken) are not validated - -**Potential Problems**: -- Invalid resumption tokens could cause server errors -- Wrong relatedRequestId could break request association - -#### 6.1.3 Missing Protocol Version Validation During Send -**Issue**: After negotiation, protocol version is stored but not validated on every message - -**Current State**: -- `setProtocolVersion()` called after initialization -- No validation that subsequent messages conform to negotiated version - -### 6.2 Error Handling Gaps - -#### 6.2.1 Limited Error Context -**Issue**: Errors often lose context as they propagate - -**Example**: When `JSONRPCMessageSchema.parse()` fails: -```typescript -try { - message = JSONRPCMessageSchema.parse(JSON.parse(messageEvent.data)); -} catch (error) { - this.onerror?.(error as Error); // Original message content is lost - return; -} -``` - -**Better Approach**: Include the raw message in error context - -#### 6.2.2 Silent Failures in Some Paths -**Issue**: Some error paths don't propagate errors effectively - -**Example in Protocol**: -```typescript -this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`)); -// Message is dropped silently if no onerror handler -``` - -**Better Approach**: Throw error or have required error handling - -#### 6.2.3 No Error Recovery Mechanisms -**Issue**: Most errors are fatal to the connection - -**Missing**: -- Ability to recover from transient errors -- Retry logic for failed sends (except StreamableHTTP reconnection) -- Graceful degradation when features are unavailable - -### 6.3 Transport-Specific Issues - -#### 6.3.1 Race Conditions in Protocol Connection -**Evidence**: The `protocol-transport-handling.test.ts` shows a bug where multiple rapid connections can send responses to the wrong client - -**Fix Applied**: Transport capture pattern in `_onrequest()` - -**Remaining Risk**: Similar issues could occur with: -- Progress notifications sent to wrong client -- Notification handlers accessing wrong transport - -#### 6.3.2 No Backpressure Handling -**Issue**: None of the transports implement backpressure or flow control - -**Potential Problems**: -- Stdio: If stdin write buffer fills, could block -- HTTP: No limit on concurrent requests -- No queuing or rate limiting - -#### 6.3.3 Incomplete Resumability Implementation -**Status**: Resumability API exists but: -- Only StreamableHTTP client supports it -- Server-side EventStore is an interface with no default implementation -- No automatic resumability without custom EventStore - -### 6.4 Protocol Layer Issues - -#### 6.4.1 Timeout Reset Logic Complexity -**Issue**: The timeout reset logic (for progress notifications) is complex and error-prone - -**Code** (lines 268-285): -```typescript -private _resetTimeout(messageId: number): boolean { - const info = this._timeoutInfo.get(messageId); - if (!info) return false; - - const totalElapsed = Date.now() - info.startTime; - if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { - this._timeoutInfo.delete(messageId); - throw new McpError(...); // Throws from unexpected context - } - - clearTimeout(info.timeoutId); - info.timeoutId = setTimeout(info.onTimeout, info.timeout); - return true; -} -``` - -**Problem**: Throws exception that gets caught in progress handler, but the flow is not obvious - -#### 6.4.2 Request Handler Memory Leak Risk -**Issue**: AbortControllers are stored in `_requestHandlerAbortControllers` map - -**Risk**: If request handling never completes (handler hangs), entries never cleaned up - -**Mitigation**: Cleanup happens in `finally` block, but what if handler never returns? - -#### 6.4.3 No Rate Limiting or Request Queue Management -**Issue**: Unlimited concurrent requests are allowed - -**Problems**: -- Memory usage can grow unbounded -- No prioritization of requests -- No limit on message IDs (though it's just a counter) - -### 6.5 Testing and Observability Gaps - -#### 6.5.1 Limited Error Testing -**Observation**: Test files focus on happy paths - -**Missing Tests**: -- Malformed JSON handling -- Invalid protocol version negotiation -- Capability violations -- Concurrent request/connection scenarios - -#### 6.5.2 No Built-in Logging or Tracing -**Issue**: No standardized way to trace messages through the system - -**Current State**: Each component can report via `onerror`, but: -- No structured logging -- No message IDs in logs -- No performance metrics - -#### 6.5.3 Proposed Transport Validator Not Integrated -**Evidence**: `src/shared/transport-validator.ts` exists with a `ProtocolValidator` class - -**Status**: -- Not used anywhere in codebase -- Implements logging but not enforcement -- Could be the foundation for protocol validation - -**Potential**: -```typescript -class ProtocolValidator implements Transport { - private log: ProtocolLog = { events: [] } - - // Wraps a transport and logs all events - // Can run checkers on the log -} -``` - -This could validate: -- Message ordering (initialize must be first) -- Request/response pairing -- Capability usage -- Protocol version conformance - -## 7. Existing Validation Infrastructure - -### 7.1 Zod Schema System - -**Strengths**: -- Comprehensive type definitions -- Runtime validation -- Type inference for TypeScript -- Good error messages - -**Coverage**: -- All JSON-RPC message types -- All MCP-specific request/response types -- Capability structures -- Metadata structures - -### 7.2 Type Guards - -**Available Functions**: -```typescript -isJSONRPCRequest(value) -isJSONRPCResponse(value) -isJSONRPCError(value) -isJSONRPCNotification(value) -isInitializeRequest(value) -isInitializedNotification(value) -``` - -**Usage Pattern**: -```typescript -if (isJSONRPCResponse(message) || isJSONRPCError(message)) { - this._onresponse(message); -} else if (isJSONRPCRequest(message)) { - this._onrequest(message, extra); -} else if (isJSONRPCNotification(message)) { - this._onnotification(message); -} -``` - -### 7.3 Test Infrastructure - -**Available**: -- InMemoryTransport for testing -- MockTransport in test files -- Protocol test suite with various scenarios -- Transport-specific test suites - -**Good Coverage Of**: -- Basic message flow -- Error scenarios -- Timeout behavior -- Progress notifications -- Debounced notifications - -## 8. Recommendations for Protocol Validation Improvements - -### 8.1 High Priority - -#### 8.1.1 Integrate Transport Validator -**Action**: Complete and integrate the ProtocolValidator class - -**Benefits**: -- Centralized validation logic -- Protocol conformance checking -- Better debugging and testing - -**Implementation**: -```typescript -// Wrap any transport with validation -const validatedTransport = new ProtocolValidator( - rawTransport, - [ - checkInitializeFirst, - checkRequestResponsePairing, - checkCapabilityUsage, - ] -); -``` - -#### 8.1.2 Enhanced Error Context -**Action**: Add message context to validation errors - -**Example**: -```typescript -catch (error) { - this.onerror?.(new Error( - `Failed to parse message: ${error}\nRaw message: ${rawMessage}` - )); -} -``` - -#### 8.1.3 Pre-Send Validation -**Action**: Validate messages before sending - -**Implementation**: -```typescript -async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { - // Validate message structure - const parseResult = JSONRPCMessageSchema.safeParse(message); - if (!parseResult.success) { - throw new Error(`Invalid message: ${parseResult.error}`); - } - - // Validate options - if (options?.resumptionToken && typeof options.resumptionToken !== 'string') { - throw new Error('Invalid resumption token'); - } - - // Send validated message - return this._actualSend(message, options); -} -``` - -### 8.2 Medium Priority - -#### 8.2.1 Structured Logging -**Action**: Add optional structured logging throughout - -**Example**: -```typescript -interface TransportLogger { - logMessageSent(message: JSONRPCMessage, options?: TransportSendOptions): void; - logMessageReceived(message: JSONRPCMessage, extra?: MessageExtraInfo): void; - logError(error: Error, context?: Record): void; -} -``` - -#### 8.2.2 Backpressure Support -**Action**: Add flow control to prevent overwhelming the transport - -**API**: -```typescript -interface Transport { - // ...existing methods - canSend?(): boolean; // Check if ready to accept messages - onready?: () => void; // Called when ready to send after backpressure -} -``` - -#### 8.2.3 Request Queue Management -**Action**: Add limits and prioritization - -**Options**: -```typescript -interface ProtocolOptions { - // ...existing options - maxConcurrentRequests?: number; - requestQueueSize?: number; -} -``` - -### 8.3 Low Priority (Future Enhancements) - -#### 8.3.1 Automatic Retry Logic -**Action**: Add configurable retry for failed requests - -**Options**: -```typescript -interface RequestOptions { - // ...existing options - retry?: { - maxAttempts: number; - backoff: 'exponential' | 'linear'; - retryableErrors?: number[]; // Error codes that should be retried - }; -} -``` - -#### 8.3.2 Message Compression -**Action**: Support compressed message payloads for large transfers - -#### 8.3.3 Message Batching -**Action**: Allow batching multiple requests in one transport send - -## 9. How Protocol Validation Could Be Improved - -### 9.1 State Machine Based Validation - -**Concept**: Track protocol state and validate allowed transitions - -```typescript -enum ProtocolState { - Disconnected, - Connecting, - Initializing, - Initialized, - Closing, - Closed, -} - -class StatefulProtocolValidator { - private state: ProtocolState = ProtocolState.Disconnected; - - validateMessage(message: JSONRPCMessage): ValidationResult { - if (this.state === ProtocolState.Connecting && - !isInitializeRequest(message)) { - return { valid: false, error: 'Must send initialize first' }; - } - - // ...more state-based validation - - return { valid: true }; - } -} -``` - -### 9.2 Message Sequence Validation - -**Track**: -- Initialize must be first request -- Initialized notification must follow initialize response -- No requests before initialized (except cancel/ping) -- Request IDs must be unique per direction -- Response IDs must match request IDs - -### 9.3 Capability-Based Validation - -**Enhance**: -```typescript -class CapabilityValidator { - constructor( - private localCapabilities: Capabilities, - private remoteCapabilities: Capabilities - ) {} - - canSendRequest(method: string): ValidationResult { - // Check if remote side advertised support - } - - canHandleRequest(method: string): ValidationResult { - // Check if local side advertised support - } - - canSendNotification(method: string): ValidationResult { - // Check if local side advertised capability - } -} -``` - -### 9.4 Schema-Based Request Validation - -**Validate request params match method schema**: -```typescript -class RequestValidator { - private schemas = new Map(); - - validateRequest(request: JSONRPCRequest): ValidationResult { - const schema = this.schemas.get(request.method); - if (!schema) { - return { valid: false, error: 'Unknown method' }; - } - - const result = schema.safeParse(request.params); - if (!result.success) { - return { valid: false, error: result.error }; - } - - return { valid: true }; - } -} -``` - -## 10. Summary - -### Strengths -1. ✅ Clean separation between protocol and transport layers -2. ✅ Comprehensive Zod-based type system -3. ✅ Good test coverage for basic scenarios -4. ✅ Flexible transport interface supports multiple implementations -5. ✅ Robust timeout and cancellation support -6. ✅ Authentication integration for HTTP-based transports - -### Weaknesses -1. ❌ Limited pre-send validation -2. ❌ Error context often lost during propagation -3. ❌ No backpressure or flow control -4. ❌ ProtocolValidator class exists but not integrated -5. ❌ Limited observability (logging, tracing) -6. ❌ Some race conditions with multiple connections - -### Opportunities -1. 💡 Integrate and expand ProtocolValidator -2. 💡 Add state machine based protocol validation -3. 💡 Improve error context and recovery -4. 💡 Add structured logging/tracing -5. 💡 Implement backpressure handling -6. 💡 Add request queue management and prioritization - -### Protocol Validation Specific -The codebase has excellent **foundations** for protocol validation: -- Comprehensive schemas -- Type guards -- Capability system -- Unused ProtocolValidator class - -What's **missing**: -- Pre-send validation -- State transition validation -- Message sequence validation -- Integration of validator infrastructure - -**Recommendation**: Focus on integrating the existing ProtocolValidator and expanding it with state machine validation before building entirely new validation systems. From 0f7885e2201d958650a21493c7860238e6decf42 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:44:13 +0100 Subject: [PATCH 17/33] rm unrelated changes --- protocol-debugger.ts | 9 - src/shared/transport-validator.ts | 115 ---- tmp2/client.mjs | 43 -- tmp2/client.py | 66 -- tmp2/issue766.ts | 165 ----- tmp2/package-lock.json | 1051 ----------------------------- tmp2/package.json | 15 - 7 files changed, 1464 deletions(-) delete mode 100644 protocol-debugger.ts delete mode 100644 src/shared/transport-validator.ts delete mode 100644 tmp2/client.mjs delete mode 100755 tmp2/client.py delete mode 100644 tmp2/issue766.ts delete mode 100644 tmp2/package-lock.json delete mode 100644 tmp2/package.json diff --git a/protocol-debugger.ts b/protocol-debugger.ts deleted file mode 100644 index 38e93c992..000000000 --- a/protocol-debugger.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Transport } from './src/shared/transport.js'; - -interface SessionDebugger { - -} - -class Debugger { - constructor(private transport: Transport) {} -} \ No newline at end of file diff --git a/src/shared/transport-validator.ts b/src/shared/transport-validator.ts deleted file mode 100644 index 725e71f95..000000000 --- a/src/shared/transport-validator.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - -Proposal: -- Validate streamable http inside transport code itself. -- Validate protocol-level messages as a wrapper around Transport interface. - -*/ - -import { JSONRPCMessage, MessageExtraInfo } from "src/types.js" -import { Transport, TransportSendOptions } from "./transport.js" - -export type ProtocolLog = { - version?: string, - // startTimestamp?: number, - // endTimestamp?: number, - events: ({ - type: 'sent', - timestamp: number, - message: JSONRPCMessage, - options?: TransportSendOptions, - } | { - type: 'received', - timestamp: number, - message: JSONRPCMessage, - extra?: MessageExtraInfo, - } | { - type: 'start' | 'close', - timestamp: number, - } | { - type: 'error', - timestamp: number, - error: Error, - })[], -}; - -export type ProtocolChecker = (log: ProtocolLog) => void; - -// type StreamableHttpLog = { - -// } - - -class ProtocolValidator implements Transport { - private log: ProtocolLog = { - events: [] - } - - constructor(private transport: Transport, private checkers: ProtocolChecker[], private now = () => Date.now()) { - transport.onmessage = (message, extra) => { - this.addEvent({ - type: 'received', - timestamp: this.now(), - message, - extra, - }); - this.onmessage?.(message, extra); - }; - transport.onerror = (error) => { - this.addEvent({ - type: 'error', - timestamp: this.now(), - error, - }); - this.onerror?.(error); - }; - transport.onclose = () => { - this.addEvent({ - type: 'close', - timestamp: this.now(), - }); - this.onclose?.(); - }; - } - - private check() { - for (const checker of this.checkers) { - checker(this.log); - } - } - - private addEvent(event: ProtocolLog['events'][number]) { - this.log.events.push(event); - this.check(); - } - - start(): Promise { - this.addEvent({ - type: 'start', - timestamp: this.now(), - }); - return this.transport.start() - } - - send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { - this.addEvent({ - type: 'sent', - timestamp: this.now(), - message, - options, - }); - return this.transport.send(message, options) - } - - close(): Promise { - throw new Error("Method not implemented.") - } - - onclose?: (() => void) | undefined - onerror?: ((error: Error) => void) | undefined - onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined - - sessionId?: string | undefined - - setProtocolVersion?: ((version: string) => void) | undefined -} \ No newline at end of file diff --git a/tmp2/client.mjs b/tmp2/client.mjs deleted file mode 100644 index c3a383b12..000000000 --- a/tmp2/client.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; - -const transport = new StdioClientTransport({ - command: "uvx", - args:[ - "--quiet", - "--refresh", - "git+https://github.com/emsi/slow-mcp", - "--transport", - "stdio", -] -}); - -const client = new Client( - { - name: "example-client", - version: "1.0.0" - }, - { - capabilities: { - prompts: {}, - resources: {}, - tools: {} - } - } -); - -await client.connect(transport); - -const tools = await client.listTools(); - -console.log(tools); - -// Call a tool -const result = await client.callTool({ - name: "run_command", -}, undefined, { - timeout: 300000, -}); - - -console.log(result); diff --git a/tmp2/client.py b/tmp2/client.py deleted file mode 100755 index cecaffc8f..000000000 --- a/tmp2/client.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env uv run -# /// script -# requires-python = ">=3.11" -# dependencies = [ -# "mcp", -# ] -# /// -import datetime -from mcp import ClientSession, StdioServerParameters, types -from mcp.client.stdio import stdio_client - -# Create server parameters for stdio connection -server_params = StdioServerParameters( - command="uvx", # Executable - args=[ - "--quiet", - "--refresh", - "git+https://github.com/emsi/slow-mcp", - "--transport", - "stdio", - ], - env=None, # Optional environment variables -) - - -# Optional: create a sampling callback -async def handle_sampling_message( - message: types.CreateMessageRequestParams, -) -> types.CreateMessageResult: - return types.CreateMessageResult( - role="assistant", - content=types.TextContent( - type="text", - text="Hello, world! from model", - ), - model="gpt-3.5-turbo", - stopReason="endTurn", - ) - - -async def run(): - async with stdio_client(server_params) as (read, write): - async with ClientSession( - read, write, #sampling_callback=handle_sampling_message - read_timeout_seconds=datetime.timedelta(seconds=60), - ) as session: - # Initialize the connection - await session.initialize() - - resources = await session.list_resources() - - # List available tools - tools = await session.list_tools() - - print(f"Tools: {tools}") - - # Call a tool - result = await session.call_tool("run_command") - - print(f"Result: {result}") - - -if __name__ == "__main__": - import asyncio - - asyncio.run(run()) diff --git a/tmp2/issue766.ts b/tmp2/issue766.ts deleted file mode 100644 index b4aea6beb..000000000 --- a/tmp2/issue766.ts +++ /dev/null @@ -1,165 +0,0 @@ -import express from 'express'; -import { z } from 'zod'; -import cors from 'cors'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { isInitializeRequest, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { randomUUID } from 'node:crypto'; - -const LogLevelMap = { - emergency: 0, - alert: 1, - critical: 2, - error: 3, - warning: 4, - notice: 5, - info: 6, - debug: 7, -} -const validLogLevels = Object.keys(LogLevelMap); - -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -// Store transports by session ID to send notifications -const transports = {}; - -const getServer = () => { - // Create an MCP server with implementation details - const server = new McpServer({ - name: 'stateless-streamable-http-server', - version: '1.0.0', - }, { capabilities: { logging: {} } }); - - server.tool( - "sum", - { a: z.number(), b: z.number() }, - async ({ a, b }) => { - server.server.sendLoggingMessage({ level: "debug", data: { message: "Received input", a, b } }); - await sleep(1000); - const result = a + b; - server.server.sendLoggingMessage({ level: "info", data: { message: "Sum calculated", result } }); - return { - content: [{ type: "text", text: "Result: " + result }], - }; - } - ); - server.server.setRequestHandler( - SetLevelRequestSchema, - async (request) => { - const levelName = request.params.level; - if (validLogLevels.includes(levelName)) { - server.server.sendLoggingMessage({ level: "debug", data: { message: "Set root log level to " + levelName } }); - } else { - server.server.sendLoggingMessage({ level: "warning", data: { message: "Invalid log level " + levelName + " received" } }); - } - return {}; - } - ); - - return server; -} - -const app = express(); -app.use(express.json()); - -// Configure CORS to expose Mcp-Session-Id header for browser-based clients -app.use(cors({ - origin: '*', // Allow all origins - adjust as needed for production - exposedHeaders: ['Mcp-Session-Id'], -})); - -const server = getServer(); -app.post('/mcp', async (req, res) => { - console.log('Received MCP POST request:', req.body); - try { - // Check for existing session ID - const sessionId = req.headers['mcp-session-id']; - let transport; - - if (sessionId && transports[sessionId]) { - // Reuse existing transport - transport = transports[sessionId]; - } else if (!sessionId && isInitializeRequest(req.body)) { - // New initialization request - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (sessionId) => { - // Store the transport by session ID when session is initialized - // This avoids race conditions where requests might come in before the session is stored - console.log("Session initialized with ID: ", sessionId); - transports[sessionId] = transport; - } - }); - - // Connect the transport to the MCP server - await server.connect(transport); - - // Handle the request - the onsessioninitialized callback will store the transport - await transport.handleRequest(req, res, req.body); - return; // Already handled - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided', - }, - id: null, - }); - return; - } - - // Handle the request with existing transport - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error', - }, - id: null, - }); - } - } -}); - -// Handle GET requests for SSE streams (now using built-in support from StreamableHTTP) -app.get('/mcp', async (req, res) => { - console.log('Received MCP GET request:', req); - const sessionId = req.headers['mcp-session-id']; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - - console.log("Establishing SSE stream for session", sessionId); - const transport = transports[sessionId]; - await transport.handleRequest(req, res); -}); - -// Start the server -const PORT = 3000; -app.listen(PORT, (error) => { - if (error) { - console.error('Failed to start server:', error); - process.exit(1); - } - console.log("MCP Streamable HTTP Server listening on port", PORT); -}); - -// Handle server shutdown -process.on('SIGINT', async () => { - console.log('Shutting down server...'); - process.exit(0); -}); - -/* -Refs: - -https://www.mcpevals.io/blog/mcp-logging-tutorial -https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/server/standaloneSseWithGetStreamableHttp.ts - -*/` \ No newline at end of file diff --git a/tmp2/package-lock.json b/tmp2/package-lock.json deleted file mode 100644 index a1991ddb4..000000000 --- a/tmp2/package-lock.json +++ /dev/null @@ -1,1051 +0,0 @@ -{ - "name": "tmp", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "tmp", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.5" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", - "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.6", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/wrappy/1.0.2/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "license": "ISC" - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - } - } -} diff --git a/tmp2/package.json b/tmp2/package.json deleted file mode 100644 index 8959cf3b0..000000000 --- a/tmp2/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "tmp", - "version": "1.0.0", - "description": "", - "license": "ISC", - "author": "", - "type": "commonjs", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.5" - } -} From 01903a11f917dddad66aed40bfb33a65525d32bd Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:50:32 +0100 Subject: [PATCH 18/33] improve backfill errors --- src/examples/backfill/backfillSampling.ts | 31 +++++++++-------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 1b9621500..c0b87d901 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -36,6 +36,8 @@ import { isJSONRPCNotification, Tool, ToolCallContent, + LoggingMessageNotification, + JSONRPCNotification, } from "../../types.js"; import { Transport } from "../../shared/transport.js"; @@ -185,12 +187,9 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo } let clientSupportsSampling: boolean | undefined; - // let clientSupportsElicitation: boolean | undefined; const propagateMessage = (source: NamedTransport, target: NamedTransport) => { source.transport.onmessage = async (message, extra) => { - console.error(`[proxy]: Message from ${source.name} transport: ${JSON.stringify(message)}; extra: ${JSON.stringify(extra)}`); - if (isJSONRPCRequest(message)) { const sendInternalError = (errorMessage: string) => { @@ -205,15 +204,12 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo }, {relatedRequestId: message.id}); }; - console.error(`[proxy]: Detected JSON-RPC request: ${JSON.stringify(message)}`); if (isInitializeRequest(message)) { - console.error(`[proxy]: Detected Initialize request: ${JSON.stringify(message)}`); if (!(clientSupportsSampling = !!message.params.capabilities.sampling)) { message.params.capabilities.sampling = {} message.params._meta = {...(message.params._meta ?? {}), ...backfillMeta}; } } else if (isCreateMessageRequest(message)) {// && !clientSupportsSampling) { - console.error(`[proxy]: Detected CreateMessage request: ${JSON.stringify(message)}`); if ((message.params.includeContext ?? 'none') !== 'none') { sendInternalError("includeContext != none not supported by MCP sampling backfill"); return; @@ -244,8 +240,6 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo ...(message.params.metadata ?? {}), }); - console.error(`[proxy]: Claude API response: ${JSON.stringify(msg)}`); - source.transport.send({ jsonrpc: "2.0", id: message.id, @@ -257,16 +251,7 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo }, }); } catch (error) { - console.error(`[proxy]: Error processing message: ${(error as Error).message}`); - - source.transport.send({ - jsonrpc: "2.0", - id: message.id, - error: { - code: -32601, // Method not found - message: `Error processing message: ${(error as Error).message}`, - }, - }, {relatedRequestId: message.id}); + sendInternalError(`Error processing message: ${(error as Error).message}`); } return; } @@ -282,7 +267,15 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo const relatedRequestId = isCancelledNotification(message)? message.params.requestId : undefined; await target.transport.send(message, {relatedRequestId}); } catch (error) { - console.error(`[proxy]: Error sending message to ${target.name}:`, error); + source.transport.send({ + jsonrpc: "2.0", + method: "notifications/message", + params: { + type: "log_message", + level: "error", + message: `Error sending message to ${target.name}: ${(error as Error).message}`, + } + }); } }; }; From 48115e46c3664afc0aa004d36b664ecef0fdbbec Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 18:56:43 +0100 Subject: [PATCH 19/33] less logs in toolLoopSampling --- src/examples/server/toolLoopSampling.ts | 28 ++----------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 11fcc31ee..812fb412e 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -323,25 +323,10 @@ async function runToolLoop( data: `Loop iteration ${iteration}: ${toolCalls.length} tool invocation(s) requested`, }); - console.error( - `[toolLoop] LLM requested ${toolCalls.length} tool(s):`, - toolCalls.map(tc => `${tc.name}`).join(", ") - ); - // Execute all tools in parallel const toolResultPromises = toolCalls.map(async (toolCall) => { - console.error( - `[toolLoop] Executing tool: ${toolCall.name} with input:`, - JSON.stringify(toolCall.input, null, 2) - ); - const result = await executeLocalTool(server, toolCall.name, toolCall.input); - console.error( - `[toolLoop] Tool ${toolCall.name} result:`, - JSON.stringify(result, null, 2) - ); - return { toolCall, result }; }); @@ -415,13 +400,7 @@ mcpServer.registerTool( }, async ({ query }) => { try { - console.error(`[fileSearch] Processing query: ${query}`); - const { answer, transcript } = await runToolLoop(mcpServer, query); - - console.error(`[fileSearch] Final answer: ${answer.substring(0, 200)}...`); - console.error(`[fileSearch] Transcript length: ${transcript.length} messages`); - return { content: [ { @@ -435,15 +414,12 @@ mcpServer.registerTool( ], }; } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - console.error(`[fileSearch] Error: ${errorMessage}`); - return { content: [ { type: "text", - text: `Error performing file search: ${errorMessage}`, + text: error instanceof Error ? error.message : `${error}`, + isError: true, }, ], }; From 0ec533a81f33fda7a82fd6288b2bd2f416d23ddf Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 19:21:56 +0100 Subject: [PATCH 20/33] feat(examples): Add aggregated token usage tracking to toolLoopSampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added comprehensive token usage tracking and reporting to the tool loop sampling example: **backfillSampling.ts**: - Pass Anthropic API usage data through _meta field of CreateMessageResult - Includes all token counts from Claude API response **toolLoopSampling.ts**: - Added AggregatedUsage interface to track cumulative token counts - Aggregate usage across all API calls in the tool loop: - input_tokens (regular input) - cache_creation_input_tokens (tokens to create cache) - cache_read_input_tokens (tokens read from cache) - output_tokens (generated output) - api_calls (number of createMessage calls) - Updated runToolLoop return type to include usage - Display formatted usage summary in fileSearch tool output: - Total input tokens with breakdown by type - Total output tokens - Total tokens consumed - Number of API calls made Example output: ``` --- Token Usage Summary --- Total Input Tokens: 1234 - Regular: 800 - Cache Creation: 200 - Cache Read: 234 Total Output Tokens: 567 Total Tokens: 1801 API Calls: 3 ``` This provides complete visibility into Claude API token consumption for monitoring costs and optimizing cache usage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/examples/backfill/backfillSampling.ts | 3 ++ src/examples/server/toolLoopSampling.ts | 58 +++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index c0b87d901..4dc699208 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -248,6 +248,9 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo stopReason: stopReasonToMcp(msg.stop_reason), role: 'assistant', // Always assistant in MCP responses content: (Array.isArray(msg.content) ? msg.content : [msg.content]).map(contentToMcp), + _meta: { + usage: msg.usage, + }, }, }); } catch (error) { diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 812fb412e..1fde74063 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -28,6 +28,17 @@ import type { const CWD = process.cwd(); +/** + * Interface for tracking aggregated token usage across API calls. + */ +interface AggregatedUsage { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens: number; + cache_read_input_tokens: number; + api_calls: number; +} + /** * Zod schemas for validating tool inputs */ @@ -267,7 +278,7 @@ async function executeLocalTool( async function runToolLoop( server: McpServer, initialQuery: string -): Promise<{ answer: string; transcript: SamplingMessage[] }> { +): Promise<{ answer: string; transcript: SamplingMessage[]; usage: AggregatedUsage }> { const messages: SamplingMessage[] = [ { role: "user", @@ -278,6 +289,15 @@ async function runToolLoop( }, ]; + // Initialize usage tracking + const aggregatedUsage: AggregatedUsage = { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + api_calls: 0, + }; + const MAX_ITERATIONS = 10; let iteration = 0; @@ -300,6 +320,16 @@ async function runToolLoop( tool_choice: { mode: "auto" }, }); + // Aggregate usage statistics from the response + if (response._meta?.usage) { + const usage = response._meta.usage as any; + aggregatedUsage.input_tokens += usage.input_tokens || 0; + aggregatedUsage.output_tokens += usage.output_tokens || 0; + aggregatedUsage.cache_creation_input_tokens += usage.cache_creation_input_tokens || 0; + aggregatedUsage.cache_read_input_tokens += usage.cache_read_input_tokens || 0; + aggregatedUsage.api_calls += 1; + } + // Add assistant's response to message history // Note that SamplingMessage.content doesn't yet support arrays, so we flatten the content into multiple messages. for (const content of (Array.isArray(response.content) ? response.content : [response.content])) { @@ -364,7 +394,7 @@ async function runToolLoop( data: `Tool loop completed after ${iteration} iteration(s)`, }); - return { answer, transcript: messages }; + return { answer, transcript: messages, usage: aggregatedUsage }; } // Unexpected response type @@ -400,13 +430,35 @@ mcpServer.registerTool( }, async ({ query }) => { try { - const { answer, transcript } = await runToolLoop(mcpServer, query); + const { answer, transcript, usage } = await runToolLoop(mcpServer, query); + + // Calculate total input tokens + const totalInputTokens = + usage.input_tokens + + usage.cache_creation_input_tokens + + usage.cache_read_input_tokens; + + // Format usage summary + const usageSummary = + `--- Token Usage Summary ---\n` + + `Total Input Tokens: ${totalInputTokens}\n` + + ` - Regular: ${usage.input_tokens}\n` + + ` - Cache Creation: ${usage.cache_creation_input_tokens}\n` + + ` - Cache Read: ${usage.cache_read_input_tokens}\n` + + `Total Output Tokens: ${usage.output_tokens}\n` + + `Total Tokens: ${totalInputTokens + usage.output_tokens}\n` + + `API Calls: ${usage.api_calls}`; + return { content: [ { type: "text", text: answer, }, + { + type: "text", + text: `\n\n${usageSummary}`, + }, { type: "text", text: `\n\n--- Debug Transcript (${transcript.length} messages) ---\n${JSON.stringify(transcript, null, 2)}`, From c12620dc4d932b56e75fd7e6fb1907df8cbd1e9a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 19:36:05 +0100 Subject: [PATCH 21/33] Update toolLoopSampling.ts --- src/examples/server/toolLoopSampling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 812fb412e..5f2e9ef78 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -6,7 +6,7 @@ Usage: npx -y @modelcontextprotocol/inspector \ - npx -y --silent tsx src/examples/backfill/backfillSampling.ts \ + npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \ npx -y --silent tsx src/examples/server/toolLoopSampling.ts Then connect with an MCP client and call the "fileSearch" tool with a query like: From ed94c4edca2b6c3e90dc469a4dcc7708587c6793 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 20:29:25 +0100 Subject: [PATCH 22/33] feat(examples): Support array content in SamplingMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated backfillSampling and toolLoopSampling to support the new schema where UserMessage and AssistantMessage content can be arrays: **backfillSampling.ts**: - Split contentFromMcp into contentBlockFromMcp (single block) and contentFromMcp (handles both single and arrays) - Updated message mapping to pass content arrays directly to Claude API - Now properly handles messages with multiple content blocks **toolLoopSampling.ts**: - Removed flattening logic that created multiple messages - SamplingMessage now natively supports content arrays - Simplified message history management **toolLoopSampling.test.ts**: - Added helper to handle content as potentially an array - Updated all test assertions to work with array content - All 5 tests passing This aligns with the MCP protocol change allowing content arrays in UserMessage and AssistantMessage, enabling multi-block responses (e.g., text + tool calls in one message). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/examples/backfill/backfillSampling.ts | 12 +++++-- src/examples/server/toolLoopSampling.test.ts | 34 +++++++++++++------- src/examples/server/toolLoopSampling.ts | 12 +++---- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 4dc699208..3bbb86f8b 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -115,7 +115,7 @@ function stopReasonToMcp(reason: string | null): CreateMessageResult['stopReason } } -function contentFromMcp(content: CreateMessageRequest['params']['messages'][number]['content']): ContentBlockParam { +function contentBlockFromMcp(content: any): ContentBlockParam { switch (content.type) { case 'text': return {type: 'text', text: content.text}; @@ -143,10 +143,16 @@ function contentFromMcp(content: CreateMessageRequest['params']['messages'][numb }; case 'audio': default: - throw new Error(`[contentFromMcp] Unsupported content type: ${(content as any).type}`); + throw new Error(`[contentBlockFromMcp] Unsupported content type: ${(content as any).type}`); } } +function contentFromMcp(content: CreateMessageRequest['params']['messages'][number]['content']): ContentBlockParam[] { + // Handle both single content block and arrays + const contentArray = Array.isArray(content) ? content : [content]; + return contentArray.map(contentBlockFromMcp); +} + export type NamedTransport = { name: 'client' | 'server', transport: T, @@ -230,7 +236,7 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo ], messages: message.params.messages.map(({role, content}) => ({ role, - content: [contentFromMcp(content)] + content: contentFromMcp(content) })), max_tokens: message.params.maxTokens ?? DEFAULT_MAX_TOKENS, temperature: message.params.temperature, diff --git a/src/examples/server/toolLoopSampling.test.ts b/src/examples/server/toolLoopSampling.test.ts index 20f072011..26233c850 100644 --- a/src/examples/server/toolLoopSampling.test.ts +++ b/src/examples/server/toolLoopSampling.test.ts @@ -68,8 +68,12 @@ describe("toolLoopSampling server", () => { const messages = request.params.messages; const lastMessage = messages[messages.length - 1]; + // Helper to get content as array + const getContentArray = (content: any) => Array.isArray(content) ? content : [content]; + const lastContent = getContentArray(lastMessage.content)[0]; + console.error( - `[test] Sampling call ${samplingCallCount}, messages: ${messages.length}, last message type: ${lastMessage.content.type}` + `[test] Sampling call ${samplingCallCount}, messages: ${messages.length}, last message type: ${lastContent.type}` ); // First call: Return tool_use for ripgrep @@ -93,7 +97,7 @@ describe("toolLoopSampling server", () => { // Second call: After getting ripgrep results, return tool_use for read if (samplingCallCount === 2) { // Verify we got a tool result - expect(lastMessage.content.type).toBe("tool_result"); + expect(lastContent.type).toBe("tool_result"); return { model: "test-model", @@ -113,7 +117,7 @@ describe("toolLoopSampling server", () => { // Third call: After getting read results, return final answer if (samplingCallCount === 3) { // Verify we got another tool result - expect(lastMessage.content.type).toBe("tool_result"); + expect(lastContent.type).toBe("tool_result"); return { model: "test-model", @@ -196,10 +200,12 @@ describe("toolLoopSampling server", () => { // Second call: Should receive error in tool result if (samplingCallCount === 2) { - expect(lastMessage.content.type).toBe("tool_result"); - if (lastMessage.content.type === "tool_result") { + const getContentArray = (content: any) => Array.isArray(content) ? content : [content]; + const lastContent = getContentArray(lastMessage.content)[0]; + expect(lastContent.type).toBe("tool_result"); + if (lastContent.type === "tool_result") { // Verify error is present in tool result - const content = lastMessage.content.content as Record< + const content = lastContent.content as Record< string, unknown >; @@ -281,9 +287,11 @@ describe("toolLoopSampling server", () => { // Second call: Should receive error in tool result if (samplingCallCount === 2) { - expect(lastMessage.content.type).toBe("tool_result"); - if (lastMessage.content.type === "tool_result") { - const content = lastMessage.content.content as Record< + const getContentArray = (content: any) => Array.isArray(content) ? content : [content]; + const lastContent = getContentArray(lastMessage.content)[0]; + expect(lastContent.type).toBe("tool_result"); + if (lastContent.type === "tool_result") { + const content = lastContent.content as Record< string, unknown >; @@ -362,9 +370,11 @@ describe("toolLoopSampling server", () => { // Second call: Should receive validation error if (samplingCallCount === 2) { - expect(lastMessage.content.type).toBe("tool_result"); - if (lastMessage.content.type === "tool_result") { - const content = lastMessage.content.content as Record< + const getContentArray = (content: any) => Array.isArray(content) ? content : [content]; + const lastContent = getContentArray(lastMessage.content)[0]; + expect(lastContent.type).toBe("tool_result"); + if (lastContent.type === "tool_result") { + const content = lastContent.content as Record< string, unknown >; diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 1fde74063..ed42f93b1 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -331,13 +331,11 @@ async function runToolLoop( } // Add assistant's response to message history - // Note that SamplingMessage.content doesn't yet support arrays, so we flatten the content into multiple messages. - for (const content of (Array.isArray(response.content) ? response.content : [response.content])) { - messages.push({ - role: "assistant", - content, - }); - } + // SamplingMessage now supports arrays of content + messages.push({ + role: "assistant", + content: response.content, + }); // Check if LLM wants to use tools if (response.stopReason === "toolUse") { From a7c92b44fd6855c4382dfb146a7d68bcaf0d7363 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 20:33:43 +0100 Subject: [PATCH 23/33] allow content arrays in SamplingMessage --- src/examples/backfill/backfillSampling.ts | 5 ++- src/types.ts | 47 +++++++++-------------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 3bbb86f8b..ffad2da33 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -38,6 +38,8 @@ import { ToolCallContent, LoggingMessageNotification, JSONRPCNotification, + AssistantMessageContent, + UserMessageContent, } from "../../types.js"; import { Transport } from "../../shared/transport.js"; @@ -115,7 +117,8 @@ function stopReasonToMcp(reason: string | null): CreateMessageResult['stopReason } } -function contentBlockFromMcp(content: any): ContentBlockParam { + +function contentBlockFromMcp(content: AssistantMessageContent | UserMessageContent): ContentBlockParam { switch (content.type) { case 'text': return {type: 'text', text: content.text}; diff --git a/src/types.ts b/src/types.ts index d0e540a25..1e7b9bc37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1237,18 +1237,20 @@ export const ToolChoiceSchema = z }) .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.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolResultContentSchema, - ]), + 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. @@ -1257,18 +1259,20 @@ export const UserMessageSchema = z }) .passthrough(); +export const AssistantMessageContentSchema = z.discriminatedUnion("type", [ + TextContentSchema, + ImageContentSchema, + AudioContentSchema, + ToolCallContentSchema, +]); + /** * A message from the assistant (LLM) in a sampling conversation. */ export const AssistantMessageSchema = z .object({ role: z.literal("assistant"), - content: z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), + 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. @@ -1358,22 +1362,7 @@ export const CreateMessageResultSchema = ResultSchema.extend({ /** * Response content. May be ToolCallContent if stopReason is "toolUse". */ - content: z.union([ - z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), - z.array( - z.discriminatedUnion("type", [ - TextContentSchema, - ImageContentSchema, - AudioContentSchema, - ToolCallContentSchema, - ]), - ) - ]), + content: z.union([AssistantMessageContentSchema, z.array(AssistantMessageContentSchema)]), }); /* Elicitation */ @@ -1827,6 +1816,8 @@ export type AssistantMessage = Infer; export type SamplingMessage = Infer; export type CreateMessageRequest = Infer; export type CreateMessageResult = Infer; +export type AssistantMessageContent = Infer; +export type UserMessageContent = Infer; /* Elicitation */ export type BooleanSchema = Infer; From e6cf5f8e475002ea010f9a6cbcf0867c786e6a5c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 21:46:54 +0100 Subject: [PATCH 24/33] Add toolChoice.mode == "none" + to help gracefully handle last iteration of tool loop (no tools!) --- src/examples/backfill/backfillSampling.ts | 31 ++++---- src/examples/server/toolLoopSampling.ts | 91 +++++++++++------------ src/types.ts | 6 +- 3 files changed, 64 insertions(+), 64 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index ffad2da33..ad49158f1 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -16,6 +16,7 @@ import { ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, + ToolChoiceNone, } from "@anthropic-ai/sdk/resources/messages.js"; import { StdioServerTransport } from '../../server/stdio.js'; import { StdioClientTransport } from '../../client/stdio.js'; @@ -73,17 +74,19 @@ function toolToClaudeFormat(tool: Tool): ClaudeTool { /** * Converts MCP ToolChoice to Claude API tool_choice format */ -function toolChoiceToClaudeFormat(toolChoice: CreateMessageRequest['params']['tool_choice']): ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool | undefined { - if (!toolChoice) { - return undefined; - } - - if (toolChoice.mode === "required") { - return { type: "any", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; +function toolChoiceToClaudeFormat(toolChoice: CreateMessageRequest['params']['toolChoice']): ToolChoiceAuto | ToolChoiceAny | ToolChoiceNone | ToolChoiceTool | undefined { + switch (toolChoice?.mode) { + case "auto": + return { type: "auto", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; + case "required": + return { type: "any", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; + case "none": + return { type: "none" }; + case undefined: + return undefined; + default: + throw new Error(`Unsupported toolChoice mode: ${toolChoice}`); } - - // "auto" or undefined defaults to auto - return { type: "auto", disable_parallel_tool_use: toolChoice.disable_parallel_tool_use }; } function contentToMcp(content: ContentBlock): CreateMessageResult['content'] { @@ -225,9 +228,11 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo } try { - // Convert MCP tools to Claude API format if provided - const tools = message.params.tools?.map(toolToClaudeFormat); - const tool_choice = toolChoiceToClaudeFormat(message.params.tool_choice); + // Note that having tools + tool_choice = 'none' does not disable tools, unlike in OpenAI's API. + // We forcibly empty out the tools list in that case, which messes with the prompt caching. + const tools = message.params.toolChoice?.mode === 'none' ? undefined + : message.params.tools?.map(toolToClaudeFormat); + const tool_choice = toolChoiceToClaudeFormat(message.params.toolChoice); const msg = await api.messages.create({ model: pickModel(message.params.modelPreferences), diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index c602c05e1..26be302be 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -24,6 +24,8 @@ import type { Tool, ToolCallContent, CreateMessageResult, + CreateMessageRequest, + ToolResultContent, } from "../../types.js"; const CWD = process.cwd(); @@ -298,7 +300,7 @@ async function runToolLoop( api_calls: 0, }; - const MAX_ITERATIONS = 10; + const MAX_ITERATIONS = 20; let iteration = 0; const systemPrompt = @@ -306,18 +308,22 @@ async function runToolLoop( "You have access to ripgrep (for searching) and read (for reading file contents). " + "Use ripgrep to find relevant files, then read them to provide accurate answers. " + "All paths are relative to the current working directory. " + - "Be concise and focus on providing the most relevant information."; + "Be concise and focus on providing the most relevant information." + + "You will be allowed up to " + MAX_ITERATIONS + " iterations of tool use to find the information needed. When you have enough information or reach the last iteration, provide a final answer."; + let request: CreateMessageRequest["params"] | undefined + let response: CreateMessageResult | undefined while (iteration < MAX_ITERATIONS) { iteration++; // Request message from LLM with available tools - const response: CreateMessageResult = await server.server.createMessage({ + response = await server.server.createMessage(request = { messages, systemPrompt, maxTokens: 4000, - tools: LOCAL_TOOLS, - tool_choice: { mode: "auto" }, + tools: iteration < MAX_ITERATIONS ? LOCAL_TOOLS : undefined, + // Don't allow tool calls at the last iteration: finish with an answer no matter what! + tool_choice: { mode: iteration < MAX_ITERATIONS ? "auto" : "none" }, }); // Aggregate usage statistics from the response @@ -337,72 +343,61 @@ async function runToolLoop( content: response.content, }); - // Check if LLM wants to use tools if (response.stopReason === "toolUse") { - // Extract all tool_use content blocks const contentArray = Array.isArray(response.content) ? response.content : [response.content]; const toolCalls = contentArray.filter( (content): content is ToolCallContent => content.type === "tool_use" ); - // Log iteration with tool invocation count await server.sendLoggingMessage({ level: "info", data: `Loop iteration ${iteration}: ${toolCalls.length} tool invocation(s) requested`, }); - // Execute all tools in parallel - const toolResultPromises = toolCalls.map(async (toolCall) => { + const toolResults: ToolResultContent[] = await Promise.all(toolCalls.map(async (toolCall) => { const result = await executeLocalTool(server, toolCall.name, toolCall.input); + return { + type: "tool_result", + toolUseId: toolCall.id, + content: result, + } + })) - return { toolCall, result }; + messages.push({ + role: "user", + content: iteration < MAX_ITERATIONS ? toolResults : [ + ...toolResults, + { + type: "text", + text: "Using the information retrieved from the tools, please now provide a concise final answer to the original question (last iteration of the tool loop).", + } + ], }); - - const toolResults = await Promise.all(toolResultPromises); - - // Add all tool results to message history - for (const { toolCall, result } of toolResults) { - messages.push({ - role: "user", - content: { - type: "tool_result", - toolUseId: toolCall.id, - content: result, - }, - }); + } else if (response.stopReason === "endTurn") { + const contentArray = Array.isArray(response.content) ? response.content : [response.content]; + const unexpectedBlocks = contentArray.filter(content => content.type !== "text"); + if (unexpectedBlocks.length > 0) { + throw new Error(`Expected text content in final answer, but got: ${unexpectedBlocks.map(b => b.type).join(", ")}`); } - - // Continue the loop to get next response - continue; - } - - // LLM provided final answer (no tool use) - // Extract all text content blocks and concatenate them - const contentArray = Array.isArray(response.content) ? response.content : [response.content]; - const textBlocks = contentArray.filter( - (content): content is { type: "text"; text: string } => content.type === "text" - ); - - if (textBlocks.length > 0) { - const answer = textBlocks.map(block => block.text).join("\n\n"); - - // Log completion + await server.sendLoggingMessage({ level: "info", data: `Tool loop completed after ${iteration} iteration(s)`, }); - return { answer, transcript: messages, usage: aggregatedUsage }; + return { + answer: contentArray.map(block => block.text).join("\n\n"), + transcript: messages, + usage: aggregatedUsage + }; + } else if (response?.stopReason === "maxTokens") { + throw new Error("LLM response hit max tokens limit"); + } else { + throw new Error(`Unsupported stop reason: ${response.stopReason}`); } - - // Unexpected response type - const contentTypes = contentArray.map(c => c.type).join(", "); - throw new Error( - `Unexpected response content types: ${contentTypes}` - ); } - throw new Error(`Tool loop exceeded maximum iterations (${MAX_ITERATIONS})`); + throw new Error(`Tool loop exceeded maximum iterations (${MAX_ITERATIONS}); request: ${JSON.stringify(request)}\nresponse: ${JSON.stringify(response)}`); } // Create and configure MCP server diff --git a/src/types.ts b/src/types.ts index 1e7b9bc37..f326d1f00 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1227,7 +1227,7 @@ export const ToolChoiceSchema = z * - "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"])), + mode: z.optional(z.enum(["auto", "required", "none"])), /** * If true, model should not use multiple tools in parallel. * Some models may ignore this hint. @@ -1331,7 +1331,7 @@ export const CreateMessageRequestSchema = RequestSchema.extend({ * Controls tool usage behavior. * Requires clientCapabilities.sampling.tools and tools parameter. */ - tool_choice: z.optional(ToolChoiceSchema), + toolChoice: z.optional(ToolChoiceSchema), }), }); @@ -1353,7 +1353,7 @@ export const CreateMessageResultSchema = ResultSchema.extend({ * - "other": Other provider-specific reason */ stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse", "refusal", "other"]).or(z.string()), + z.enum(["endTurn", "stopSequence", "maxTokens", "toolUse"]).or(z.string()), ), /** * The role is always "assistant" in responses from the LLM. From 46fc761c9720679af5a876460cb3f38cb9795a4b Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 22:06:51 +0100 Subject: [PATCH 25/33] rename: fileSearch -> localResearch --- src/examples/server/toolLoopSampling.test.ts | 18 +++++++++--------- src/examples/server/toolLoopSampling.ts | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/examples/server/toolLoopSampling.test.ts b/src/examples/server/toolLoopSampling.test.ts index 26233c850..73ca96540 100644 --- a/src/examples/server/toolLoopSampling.test.ts +++ b/src/examples/server/toolLoopSampling.test.ts @@ -1,7 +1,7 @@ /** * Tests for toolLoopSampling.ts * - * These tests verify that the fileSearch tool correctly implements a tool loop + * These tests verify that the localResearch tool correctly implements a tool loop * by simulating an LLM that makes ripgrep and read tool calls. */ @@ -140,12 +140,12 @@ describe("toolLoopSampling server", () => { // Connect client to server await client.connect(transport); - // Call the fileSearch tool + // Call the localResearch tool const result = await client.request( { method: "tools/call", params: { - name: "fileSearch", + name: "localResearch", arguments: { query: "Find the McpServer class definition", }, @@ -236,12 +236,12 @@ describe("toolLoopSampling server", () => { await client.connect(transport); - // Call the fileSearch tool + // Call the localResearch tool const result = await client.request( { method: "tools/call", params: { - name: "fileSearch", + name: "localResearch", arguments: { query: "Search outside current directory", }, @@ -325,7 +325,7 @@ describe("toolLoopSampling server", () => { { method: "tools/call", params: { - name: "fileSearch", + name: "localResearch", arguments: { query: "Use unknown tool", }, @@ -409,7 +409,7 @@ describe("toolLoopSampling server", () => { { method: "tools/call", params: { - name: "fileSearch", + name: "localResearch", arguments: { query: "Test malformed input", }, @@ -451,12 +451,12 @@ describe("toolLoopSampling server", () => { await client.connect(transport); - // Call fileSearch with infinite loop scenario + // Call localResearch with infinite loop scenario const result = await client.request( { method: "tools/call", params: { - name: "fileSearch", + name: "localResearch", arguments: { query: "Infinite loop test", }, diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index 26be302be..fa0c33946 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -1,7 +1,7 @@ /* This example demonstrates a tool loop using MCP sampling with locally defined tools. - It exposes a "fileSearch" tool that uses an LLM with ripgrep and read capabilities + It exposes a "localResearch" tool that uses an LLM with ripgrep and read capabilities to intelligently search and read files in the current directory. Usage: @@ -9,7 +9,7 @@ npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \ npx -y --silent tsx src/examples/server/toolLoopSampling.ts - Then connect with an MCP client and call the "fileSearch" tool with a query like: + Then connect with an MCP client and call the "localResearch" tool with a query like: "Find all TypeScript files that export a Server class" */ @@ -406,9 +406,9 @@ const mcpServer = new McpServer({ version: "1.0.0", }); -// Register the fileSearch tool that uses sampling with a tool loop +// Register the localResearch tool that uses sampling with a tool loop mcpServer.registerTool( - "fileSearch", + "localResearch", { description: "Search for information in files using an AI assistant with ripgrep and file reading capabilities. " + From ed29d478d6c521751ad49120df09e79740420f06 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 23:00:49 +0100 Subject: [PATCH 26/33] pin zod version compatible w/ dev dep of claude api --- package-lock.json | 39 +++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 75e963d45..7374cedcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", + "zod": "^3.25.0", "zod-to-json-schema": "^3.24.1" }, "devDependencies": { @@ -487,6 +487,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", @@ -4918,6 +4928,20 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6260,6 +6284,13 @@ "node": ">=0.6" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -6661,9 +6692,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index b1eaa902d..cb4e2d2ea 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", + "zod": "^3.25.0", "zod-to-json-schema": "^3.24.1" }, "devDependencies": { From e45b5b13565101e2e84dfe57294b6051b9b13d55 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Oct 2025 23:04:34 +0100 Subject: [PATCH 27/33] Update toolLoopSampling.test.ts --- src/examples/server/toolLoopSampling.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/server/toolLoopSampling.test.ts b/src/examples/server/toolLoopSampling.test.ts index 73ca96540..85b4358fc 100644 --- a/src/examples/server/toolLoopSampling.test.ts +++ b/src/examples/server/toolLoopSampling.test.ts @@ -476,6 +476,6 @@ describe("toolLoopSampling server", () => { } // Verify we hit the iteration limit (10 iterations as defined in toolLoopSampling.ts) - expect(samplingCallCount).toBe(10); + expect(samplingCallCount).toBe(20); }); }); From a8affb374702ef5ab8ac2feddee7dbbe40018da7 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 6 Oct 2025 11:40:32 +0100 Subject: [PATCH 28/33] align ToolResultContentSchema on (subset of) CallToolResult --- src/types.ts | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/src/types.ts b/src/types.ts index f326d1f00..5ad5e3c92 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ import { z, ZodTypeAny } from "zod"; import { AuthInfo } from "./server/auth/types.js"; +import { is } from "@babel/types"; export const LATEST_PROTOCOL_VERSION = "2025-06-18"; export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"; @@ -866,32 +867,6 @@ export const ToolCallContentSchema = z }) .passthrough(); -/** - * The result of a tool execution, provided by the user (server). - * Represents the outcome of invoking a tool requested via ToolCallContent. - */ -export const ToolResultContentSchema = z - .object({ - type: z.literal("tool_result"), - /** - * The ID of the tool call this result corresponds to. - * Must match a ToolCallContent.id from a previous assistant message. - */ - toolUseId: z.string(), - /** - * The result of the tool execution. - * Can be any JSON-serializable object. - * Error information should be included in the content itself. - */ - content: 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. */ @@ -1237,6 +1212,18 @@ export const ToolChoiceSchema = z }) .passthrough(); +/** + * The result of a tool execution, provided by the user (server). + * Represents the outcome of invoking a tool requested via ToolCallContent. + */ +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(z.union([TextContentSchema, ImageContentSchema])), + structuredContent: z.object({}).passthrough().optional(), + isError: z.optional(z.boolean()), +}) + export const UserMessageContentSchema = z.discriminatedUnion("type", [ TextContentSchema, ImageContentSchema, From 904d036c70dbed52f23ec5eac102d23c063b45a4 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 6 Oct 2025 11:40:54 +0100 Subject: [PATCH 29/33] Update backfillSampling.ts --- src/examples/backfill/backfillSampling.ts | 30 +++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index ad49158f1..84ccf5afe 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -12,6 +12,8 @@ import { Base64ImageSource, ContentBlock, ContentBlockParam, + TextBlockParam, + ImageBlockParam, Tool as ClaudeTool, ToolChoiceAuto, ToolChoiceAny, @@ -41,6 +43,9 @@ import { JSONRPCNotification, AssistantMessageContent, UserMessageContent, + ElicitResult, + ElicitResultSchema, +TextContent, } from "../../types.js"; import { Transport } from "../../shared/transport.js"; @@ -57,6 +62,9 @@ const isCallToolRequest: (value: unknown) => value is CallToolRequest = const isElicitRequest: (value: unknown) => value is ElicitRequest = ((value: any) => ElicitRequestSchema.safeParse(value).success) as any; +const isElicitResult: (value: unknown) => value is ElicitResult = + ((value: any) => ElicitResultSchema.safeParse(value).success) as any; + const isCreateMessageRequest: (value: unknown) => value is CreateMessageRequest = ((value: any) => CreateMessageRequestSchema.safeParse(value).success) as any; @@ -115,8 +123,10 @@ function stopReasonToMcp(reason: string | null): CreateMessageResult['stopReason return 'toolUse'; case 'end_turn': return 'endTurn'; + case null: + return undefined; default: - return 'other'; + throw new Error(`[stopReasonToMcp] Unsupported stop reason: ${reason}`); } } @@ -138,7 +148,23 @@ function contentBlockFromMcp(content: AssistantMessageContent | UserMessageConte return { type: 'tool_result', tool_use_id: content.toolUseId, - content: JSON.stringify(content.content), // TODO + content: content.content.map(c => { + if (c.type === 'text') { + return {type: 'text', text: c.text}; + } else if (c.type === 'image') { + return { + type: 'image', + source: { + type: 'base64', + data: c.data, + media_type: c.mimeType as Base64ImageSource['media_type'], + }, + }; + } else { + throw new Error(`[contentBlockFromMcp] Unsupported content type in tool_result: ${c.type}`); + } + }), + is_error: content.isError, }; case 'tool_use': return { From daedb617396d723672609a7f2b0f5d58419104a6 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 6 Oct 2025 11:41:00 +0100 Subject: [PATCH 30/33] Update backfillSampling.ts --- src/examples/backfill/backfillSampling.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index 84ccf5afe..bf6b746b1 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -260,6 +260,8 @@ export async function setupBackfill(client: NamedTransport, server: NamedTranspo : message.params.tools?.map(toolToClaudeFormat); const tool_choice = toolChoiceToClaudeFormat(message.params.toolChoice); + // TODO: switch to streaming if maxTokens is too large + // "Streaming is required when max_tokens is greater than 21,333 tokens" const msg = await api.messages.create({ model: pickModel(message.params.modelPreferences), system: message.params.systemPrompt === undefined ? undefined : [ From 27d36a10a1849d76e2af377022626b6fb6d41fc5 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 6 Oct 2025 11:41:13 +0100 Subject: [PATCH 31/33] Update toolLoopSampling.ts --- src/examples/server/toolLoopSampling.ts | 125 ++++++++++++------------ 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/src/examples/server/toolLoopSampling.ts b/src/examples/server/toolLoopSampling.ts index fa0c33946..1ebbfbf9f 100644 --- a/src/examples/server/toolLoopSampling.ts +++ b/src/examples/server/toolLoopSampling.ts @@ -6,7 +6,7 @@ Usage: npx -y @modelcontextprotocol/inspector \ - npx -y --silent tsx src/examples/backfill/backfillSampling.ts -- \ + npx -- -y --silent tsx src/examples/backfill/backfillSampling.ts \ npx -y --silent tsx src/examples/server/toolLoopSampling.ts Then connect with an MCP client and call the "localResearch" tool with a query like: @@ -25,7 +25,8 @@ import type { ToolCallContent, CreateMessageResult, CreateMessageRequest, - ToolResultContent, + ToolResultContent, + CallToolResult, } from "../../types.js"; const CWD = process.cwd(); @@ -71,6 +72,19 @@ function ensureSafePath(inputPath: string): string { return resolved; } + +function makeErrorCallToolResult(error: any): CallToolResult { + return { + content: [ + { + type: "text", + text: error instanceof Error ? `${error.message}\n${error.stack}` : `${error}`, + }, + ], + isError: true, + } +} + /** * Executes ripgrep to search for a pattern in files. * Returns search results as a string. @@ -79,7 +93,7 @@ async function executeRipgrep( server: McpServer, pattern: string, path: string -): Promise<{ output?: string; error?: string }> { +): Promise { try { await server.sendLoggingMessage({ level: "info", @@ -88,46 +102,35 @@ async function executeRipgrep( const safePath = ensureSafePath(path); - return new Promise((resolve) => { - const rg = spawn("rg", [ - "--json", - "--max-count", "50", - "--", - pattern, - safePath, - ]); + const output = await new Promise((resolve, reject) => { + const command = ["rg", "--json", "--max-count", "50", "--", pattern, safePath]; + const rg = spawn(command[0], command.slice(1)); let stdout = ""; let stderr = ""; - - rg.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - rg.stderr.on("data", (data) => { - stderr += data.toString(); - }); - + rg.stdout.on("data", (data) => stdout += data.toString()); + rg.stderr.on("data", (data) => stderr += data.toString()); rg.on("close", (code) => { if (code === 0 || code === 1) { // code 1 means no matches, which is fine - resolve({ output: stdout || "No matches found" }); + resolve(stdout || "No matches found"); } else { - resolve({ error: stderr || `ripgrep exited with code ${code}` }); + reject(new Error(`ripgrep exited with code ${code}:\n${stderr}`)); } }); - - rg.on("error", (err) => { - resolve({ error: `Failed to execute ripgrep: ${err.message}` }); - }); + rg.on("error", err => reject(new Error(`Failed to start \`${command.map(a => a.indexOf(' ') >= 0 ? `"${a}"` : a).join(' ')}\`: ${err.message}\n${stderr}`))); }); - } catch (error) { + const structuredContent = { output }; return { - error: error instanceof Error ? error.message : "Unknown error", + content: [{ type: "text", text: JSON.stringify(structuredContent) }], + structuredContent, }; + } catch (error) { + return makeErrorCallToolResult(error); } } + /** * Reads a file from the filesystem, optionally within a line range. * Returns file contents as a string. @@ -137,7 +140,7 @@ async function executeRead( path: string, startLineInclusive?: number, endLineInclusive?: number -): Promise<{ content?: string; error?: string }> { +): Promise { try { // Log the read operation if (startLineInclusive !== undefined || endLineInclusive !== undefined) { @@ -153,35 +156,39 @@ async function executeRead( } const safePath = ensureSafePath(path); - const content = await readFile(safePath, "utf-8"); - const lines = content.split("\n"); + const fileContent = await readFile(safePath, "utf-8"); + if (typeof fileContent !== "string") { + throw new Error(`Result of reading file ${path} is not text: ${fileContent}`); + } + + let content = fileContent; // If line range specified, extract only those lines if (startLineInclusive !== undefined || endLineInclusive !== undefined) { + const lines = fileContent.split("\n"); + const start = (startLineInclusive ?? 1) - 1; // Convert to 0-indexed const end = endLineInclusive ?? lines.length; // Default to end of file if (start < 0 || start >= lines.length) { - return { error: `Start line ${startLineInclusive} is out of bounds (file has ${lines.length} lines)` }; + throw new Error(`Start line ${startLineInclusive} is out of bounds (file has ${lines.length} lines)`); } if (end < start) { - return { error: `End line ${endLineInclusive} is before start line ${startLineInclusive}` }; + throw new Error(`End line ${endLineInclusive} is before start line ${startLineInclusive}`); } - const selectedLines = lines.slice(start, end); - // Add line numbers to output - const numberedContent = selectedLines - .map((line, idx) => `${start + idx + 1}: ${line}`) - .join("\n"); - - return { content: numberedContent }; + content = lines.slice(start, end) + .map((line, idx) => `${start + idx + 1}: ${line}`) + .join("\n"); } - return { content }; - } catch (error) { + const structuredContent = { content } return { - error: error instanceof Error ? error.message : "Unknown error", + content: [{ type: "text", text: JSON.stringify(structuredContent) }], + structuredContent, }; + } catch (error) { + return makeErrorCallToolResult(error); } } @@ -242,7 +249,7 @@ async function executeLocalTool( server: McpServer, toolName: string, toolInput: Record -): Promise> { +): Promise { try { switch (toolName) { case "ripgrep": { @@ -259,17 +266,13 @@ async function executeLocalTool( ); } default: - return { error: `Unknown tool: ${toolName}` }; + return makeErrorCallToolResult(`Unknown tool: ${toolName}`); } } catch (error) { if (error instanceof z.ZodError) { - return { - error: `Invalid input for tool '${toolName}': ${error.errors.map(e => e.message).join(", ")}`, - }; + return makeErrorCallToolResult(`Invalid input for tool '${toolName}': ${error.errors.map(e => e.message).join(", ")}`); } - return { - error: error instanceof Error ? error.message : "Unknown error during tool execution", - }; + return makeErrorCallToolResult(error); } } @@ -356,10 +359,12 @@ async function runToolLoop( const toolResults: ToolResultContent[] = await Promise.all(toolCalls.map(async (toolCall) => { const result = await executeLocalTool(server, toolCall.name, toolCall.input); - return { + return { type: "tool_result", toolUseId: toolCall.id, - content: result, + content: result.content, + structuredContent: result.structuredContent, + isError: result.isError, } })) @@ -416,12 +421,14 @@ mcpServer.registerTool( inputSchema: { query: z .string() + .default("describe main classes") .describe( "A natural language query describing what to search for (e.g., 'Find all TypeScript files that export a Server class')" ), + maxIterations: z.number().int().positive().optional().default(20).describe("Maximum number of tool use iterations (default 20)"), }, }, - async ({ query }) => { + async ({ query, maxIterations }) => { try { const { answer, transcript, usage } = await runToolLoop(mcpServer, query); @@ -459,15 +466,7 @@ mcpServer.registerTool( ], }; } catch (error) { - return { - content: [ - { - type: "text", - text: error instanceof Error ? error.message : `${error}`, - isError: true, - }, - ], - }; + return makeErrorCallToolResult(error); } } ); From 193bb38dcdaf206eb55d125d245b0dacc6a4508a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 6 Oct 2025 11:45:48 +0100 Subject: [PATCH 32/33] allow AudioContent in ToolResultContent.content --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 5ad5e3c92..6cd9cd36c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1219,7 +1219,7 @@ 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."), - content: z.array(z.union([TextContentSchema, ImageContentSchema])), + content: z.array(z.union([TextContentSchema, ImageContentSchema, AudioContentSchema])), structuredContent: z.object({}).passthrough().optional(), isError: z.optional(z.boolean()), }) From 86a6c913cade58afb6d9255e51f14b087a6bf02e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Mon, 6 Oct 2025 17:12:51 +0100 Subject: [PATCH 33/33] Update backfillSampling.ts --- src/examples/backfill/backfillSampling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/backfill/backfillSampling.ts b/src/examples/backfill/backfillSampling.ts index bf6b746b1..0c8c97827 100644 --- a/src/examples/backfill/backfillSampling.ts +++ b/src/examples/backfill/backfillSampling.ts @@ -1,5 +1,5 @@ /* - This example implements an stdio MCP proxy that backfills sampling requests using the Claude API. + This example implements an stdio MCP proxy that backfills context-agnostic sampling requests using the Claude API. Usage: npx -y @modelcontextprotocol/inspector \