From 8630a9601bb40d41f1e554eb89ffa1f2a3cb8403 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 8 Nov 2025 08:02:46 +0000 Subject: [PATCH] feat: auto-infer streamable-http type for MCP servers with URL configuration - Modified validateServerConfig to auto-infer streamable-http when URL is present without explicit type - Updated ServerConfigSchema to prioritize streamable-http over SSE for URL configs - SSE now requires explicit type specification - Added comprehensive test coverage for URL auto-inference - Maintains full backward compatibility with existing configs Fixes #9118 --- src/services/mcp/McpHub.ts | 37 ++++------ src/services/mcp/__tests__/McpHub.spec.ts | 90 +++++++++++++++++++++++ 2 files changed, 106 insertions(+), 21 deletions(-) diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index f5ddf3e57b..de81fa7aac 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -77,7 +77,7 @@ const streamableHttpFieldsErrorMessage = const mixedFieldsErrorMessage = "Cannot mix 'stdio' and ('sse' or 'streamable-http') fields. For 'stdio' use 'command', 'args', and 'env'. For 'sse'/'streamable-http' use 'url' and 'headers'" const missingFieldsErrorMessage = - "Server configuration must include either 'command' (for stdio) or 'url' (for sse/streamable-http) and a corresponding 'type' if 'url' is used." + "Server configuration must include either 'command' (for stdio) or 'url' (for sse/streamable-http). When 'url' is present without 'type', 'streamable-http' will be inferred." // Helper function to create a refined schema with better error messages const createServerTypeSchema = () => { @@ -98,22 +98,7 @@ const createServerTypeSchema = () => { type: "stdio" as const, })) .refine((data) => data.type === undefined || data.type === "stdio", { message: typeErrorMessage }), - // SSE config (has url field) - BaseConfigSchema.extend({ - type: z.enum(["sse"]).optional(), - url: z.string().url("URL must be a valid URL format"), - headers: z.record(z.string()).optional(), - // Ensure no stdio fields are present - command: z.undefined().optional(), - args: z.undefined().optional(), - env: z.undefined().optional(), - }) - .transform((data) => ({ - ...data, - type: "sse" as const, - })) - .refine((data) => data.type === undefined || data.type === "sse", { message: typeErrorMessage }), - // StreamableHTTP config (has url field) + // StreamableHTTP config (has url field, default for URL-based configs) BaseConfigSchema.extend({ type: z.enum(["streamable-http"]).optional(), url: z.string().url("URL must be a valid URL format"), @@ -130,6 +115,16 @@ const createServerTypeSchema = () => { .refine((data) => data.type === undefined || data.type === "streamable-http", { message: typeErrorMessage, }), + // SSE config (requires explicit type="sse") + BaseConfigSchema.extend({ + type: z.enum(["sse"]), + url: z.string().url("URL must be a valid URL format"), + headers: z.record(z.string()).optional(), + // Ensure no stdio fields are present + command: z.undefined().optional(), + args: z.undefined().optional(), + env: z.undefined().optional(), + }).refine((data) => data.type === "sse", { message: typeErrorMessage }), ]) } @@ -209,9 +204,9 @@ export class McpHub { config.type = "stdio" } - // For url-based configs, type must be provided by the user + // For url-based configs, auto-infer type as 'streamable-http' if not provided if (hasUrlFields && !config.type) { - throw new Error("Configuration with 'url' must explicitly specify 'type' as 'sse' or 'streamable-http'.") + config.type = "streamable-http" } // Validate type if provided @@ -1378,8 +1373,8 @@ export class McpHub { await this.deleteConnection(serverName, serverSource) // Re-add as a disabled connection // Re-read config from file to get updated disabled state - const updatedConfig = await this.readServerConfigFromFile(serverName, serverSource) - await this.connectToServer(serverName, updatedConfig, serverSource) + const updatedConfig = await this.readServerConfigFromFile(serverName, serverSource) + await this.connectToServer(serverName, updatedConfig, serverSource) } else if (!disabled && connection.server.status === "disconnected") { // If enabling a disabled server, connect it // Re-read config from file to get updated disabled state diff --git a/src/services/mcp/__tests__/McpHub.spec.ts b/src/services/mcp/__tests__/McpHub.spec.ts index 1db924ed6c..f8afeb3bb4 100644 --- a/src/services/mcp/__tests__/McpHub.spec.ts +++ b/src/services/mcp/__tests__/McpHub.spec.ts @@ -2147,4 +2147,94 @@ describe("McpHub", () => { ) }) }) + + describe("URL type auto-inference", () => { + it("should auto-infer 'streamable-http' type when URL is present without explicit type", () => { + const config = { + url: "https://api.example.com/mcp", + headers: { + Authorization: "Bearer token", + }, + } + + const result = ServerConfigSchema.parse(config) + expect(result.type).toBe("streamable-http") + }) + + it("should preserve explicit 'sse' type when specified with URL", () => { + const config = { + type: "sse", + url: "https://api.example.com/mcp", + headers: { + Authorization: "Bearer token", + }, + } + + const result = ServerConfigSchema.parse(config) + expect(result.type).toBe("sse") + }) + + it("should preserve explicit 'streamable-http' type when specified", () => { + const config = { + type: "streamable-http", + url: "https://api.example.com/mcp", + } + + const result = ServerConfigSchema.parse(config) + expect(result.type).toBe("streamable-http") + }) + + it("should maintain backward compatibility for stdio type", () => { + const config = { + command: "node", + args: ["server.js"], + } + + const result = ServerConfigSchema.parse(config) + expect(result.type).toBe("stdio") + }) + + it("should work with minimal URL config", () => { + const config = { + url: "https://docs.langchain.com/mcp", + } + + const result = ServerConfigSchema.parse(config) + expect(result.type).toBe("streamable-http") + }) + + it("should auto-infer for GitHub Copilot-like config", () => { + const config = { + url: "https://api.githubcopilot.com/mcp", + headers: { + Authorization: "Bearer GITHUB_PAT", + }, + alwaysAllow: ["SearchDocsByLangChain"], + } + + const result = ServerConfigSchema.parse(config) + expect(result.type).toBe("streamable-http") + }) + + it("should reject invalid URL", () => { + const config = { + url: "not-a-url", + } + + expect(() => ServerConfigSchema.parse(config)).toThrow("URL must be a valid URL format") + }) + + it("should reject mixed stdio and URL fields", () => { + const config = { + command: "node", + url: "https://api.example.com/mcp", + } + + // This should be caught by the validateServerConfig function + const mcpHub = new McpHub(mockProvider as ClineProvider) + expect(() => mcpHub["validateServerConfig"](config)).toThrow( + "Cannot mix 'stdio' and ('sse' or 'streamable-http') fields", + ) + }) + }) })