From 365e3bca95aca100eb103a39c615a1a71b816ca8 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 23 Jul 2025 10:26:40 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=81=20feat:=20Allow=20"http"=20as=20Al?= =?UTF-8?q?ias=20for=20"streamable-http"=20in=20MCP=20Options=20(#8624)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated StreamableHTTPOptionsSchema to accept "http" alongside "streamable-http". - Enhanced isStreamableHTTPOptions function to handle both types and validate URLs accordingly. - Added tests to ensure correct processing of "http" type options and rejection of websocket URLs. --- packages/api/src/mcp/connection.ts | 10 ++++-- packages/api/src/mcp/mcp.spec.ts | 57 ++++++++++++++++++++++++++++++ packages/data-provider/src/mcp.ts | 2 +- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index 99e59b5467..baa69e93aa 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -45,9 +45,12 @@ function isSSEOptions(options: t.MCPOptions): options is t.SSEOptions { * @returns True if options are for a streamable HTTP transport */ function isStreamableHTTPOptions(options: t.MCPOptions): options is t.StreamableHTTPOptions { - if ('url' in options && options.type === 'streamable-http') { - const protocol = new URL(options.url).protocol; - return protocol !== 'ws:' && protocol !== 'wss:'; + if ('url' in options && 'type' in options) { + const optionType = options.type as string; + if (optionType === 'streamable-http' || optionType === 'http') { + const protocol = new URL(options.url).protocol; + return protocol !== 'ws:' && protocol !== 'wss:'; + } } return false; } @@ -142,6 +145,7 @@ export class MCPConnection extends EventEmitter { } else if (isWebSocketOptions(options)) { type = 'websocket'; } else if (isStreamableHTTPOptions(options)) { + // Could be either 'streamable-http' or 'http', normalize to 'streamable-http' type = 'streamable-http'; } else if (isSSEOptions(options)) { type = 'sse'; diff --git a/packages/api/src/mcp/mcp.spec.ts b/packages/api/src/mcp/mcp.spec.ts index f97d4b6bdd..f27e4ed9ab 100644 --- a/packages/api/src/mcp/mcp.spec.ts +++ b/packages/api/src/mcp/mcp.spec.ts @@ -140,6 +140,31 @@ describe('Environment Variable Extraction (MCP)', () => { expect(result.headers).toEqual(options.headers); }); + + it('should accept "http" as an alias for "streamable-http"', () => { + const options = { + type: 'http', + url: 'https://example.com/api', + headers: { + Authorization: 'Bearer token', + }, + }; + + const result = StreamableHTTPOptionsSchema.parse(options); + + expect(result.type).toBe('http'); + expect(result.url).toBe('https://example.com/api'); + expect(result.headers).toEqual(options.headers); + }); + + it('should reject websocket URLs with "http" type', () => { + const options = { + type: 'http', + url: 'ws://example.com/socket', + }; + + expect(() => StreamableHTTPOptionsSchema.parse(options)).toThrow(); + }); }); describe('processMCPEnv', () => { @@ -298,6 +323,38 @@ describe('Environment Variable Extraction (MCP)', () => { expect(result.type).toBe('streamable-http'); }); + it('should maintain http type in processed options', () => { + const obj = { + type: 'http' as const, + url: 'https://example.com/api', + }; + + const result = processMCPEnv(obj as unknown as MCPOptions); + + expect(result.type).toBe('http'); + }); + + it('should process headers in http options', () => { + const user = createTestUser({ id: 'test-user-123' }); + const obj = { + type: 'http' as const, + url: 'https://example.com', + headers: { + Authorization: '${TEST_API_KEY}', + 'User-Id': '{{LIBRECHAT_USER_ID}}', + 'Content-Type': 'application/json', + }, + }; + + const result = processMCPEnv(obj as unknown as MCPOptions, user); + + expect('headers' in result && result.headers).toEqual({ + Authorization: 'test-api-key-value', + 'User-Id': 'test-user-123', + 'Content-Type': 'application/json', + }); + }); + it('should process dynamic user fields in headers', () => { const user = createTestUser({ id: 'user-123', diff --git a/packages/data-provider/src/mcp.ts b/packages/data-provider/src/mcp.ts index 696777131d..2b85449a06 100644 --- a/packages/data-provider/src/mcp.ts +++ b/packages/data-provider/src/mcp.ts @@ -125,7 +125,7 @@ export const SSEOptionsSchema = BaseOptionsSchema.extend({ }); export const StreamableHTTPOptionsSchema = BaseOptionsSchema.extend({ - type: z.literal('streamable-http'), + type: z.union([z.literal('streamable-http'), z.literal('http')]), headers: z.record(z.string(), z.string()).optional(), url: z .string()