From 0343816e4a5a8b5e32a8c32aff386a863c600367 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:02:32 +0000 Subject: [PATCH 1/6] Initial plan From 551b9d815ff2f175689344a5272dbf14a285c474 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:07:39 +0000 Subject: [PATCH 2/6] Add BrowserCompatibleTransport to prevent CORS preflight issues Co-authored-by: slhmy <31381093+slhmy@users.noreply.github.com> --- lib/mcp/client.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/lib/mcp/client.ts b/lib/mcp/client.ts index cab6861..feec571 100644 --- a/lib/mcp/client.ts +++ b/lib/mcp/client.ts @@ -16,6 +16,74 @@ import { Client } from "@modelcontextprotocol/sdk/client" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { McpServerConfig, McpConnectionStatus, McpTool } from "./types" import { Tool } from "../tools" +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js" +import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js" + +/** + * Transport wrapper that prevents the mcp-protocol-version header from being sent. + * + * This is necessary to avoid CORS preflight issues with some MCP servers that don't + * properly configure CORS for custom headers. By preventing the protocol version + * header from being sent, we avoid triggering CORS preflight requests. + * + * Note: The protocol version is still negotiated during the initialize handshake + * (via the request/response body), so this doesn't affect protocol compatibility. + */ +class BrowserCompatibleTransport implements Transport { + private innerTransport: StreamableHTTPClientTransport + + constructor(innerTransport: StreamableHTTPClientTransport) { + this.innerTransport = innerTransport + } + + async start(): Promise { + return this.innerTransport.start() + } + + async send(message: JSONRPCMessage | JSONRPCMessage[]): Promise { + return this.innerTransport.send(message) + } + + async close(): Promise { + return this.innerTransport.close() + } + + set onclose(handler: (() => void) | undefined) { + this.innerTransport.onclose = handler + } + + get onclose(): (() => void) | undefined { + return this.innerTransport.onclose + } + + set onerror(handler: ((error: Error) => void) | undefined) { + this.innerTransport.onerror = handler + } + + get onerror(): ((error: Error) => void) | undefined { + return this.innerTransport.onerror + } + + set onmessage(handler: ((message: JSONRPCMessage) => void) | undefined) { + this.innerTransport.onmessage = handler + } + + get onmessage(): ((message: JSONRPCMessage) => void) | undefined { + return this.innerTransport.onmessage + } + + get sessionId(): string | undefined { + return this.innerTransport.sessionId + } + + // Intentionally NOT implementing setProtocolVersion to prevent the + // mcp-protocol-version header from being added to requests. + // This avoids CORS preflight issues with servers that don't properly + // configure CORS for custom headers. + // setProtocolVersion(version: string): void { + // // No-op: Don't set protocol version to avoid CORS issues + // } +} /** * MCP Client wrapper for browser use @@ -23,6 +91,7 @@ import { Tool } from "../tools" export class McpClient { private client: Client | null = null private transport: StreamableHTTPClientTransport | null = null + private wrappedTransport: BrowserCompatibleTransport | null = null private config: McpServerConfig private statusCallback?: (status: McpConnectionStatus, error?: string) => void private sessionId: string | undefined @@ -66,6 +135,10 @@ export class McpClient { // Note: Session resumption across page loads is not supported by most MCP servers }) + // Wrap transport to prevent mcp-protocol-version header from being sent + // This avoids CORS preflight issues with servers that don't properly configure CORS + this.wrappedTransport = new BrowserCompatibleTransport(this.transport) + // Create MCP client this.client = new Client( { @@ -79,9 +152,9 @@ export class McpClient { } ) - // Connect to the server + // Connect to the server using the wrapped transport try { - await this.client.connect(this.transport) + await this.client.connect(this.wrappedTransport) // Read session ID from response header (set by the transport after initial request) // The transport receives the mcp-session-id header from the server and stores it const receivedSessionId = this.transport.sessionId @@ -124,8 +197,12 @@ export class McpClient { await this.client.close() this.client = null } + if (this.wrappedTransport) { + await this.wrappedTransport.close() + this.wrappedTransport = null + } if (this.transport) { - await this.transport.close() + // Transport is already closed via wrappedTransport, just clear the reference this.transport = null } this.updateStatus("disconnected") From bc5347ea5a4091bde3d7406cf6334542c2d8089c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:09:18 +0000 Subject: [PATCH 3/6] Add documentation for CORS compatibility fix Co-authored-by: slhmy <31381093+slhmy@users.noreply.github.com> --- README.md | 1 + lib/mcp/client.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 7512ae7..3375c98 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ This enables seamless session resumption without manual intervention. - Ensure your MCP server has CORS properly configured - Check that the server URL is correct and publicly accessible - Verify that the server supports the Streamable HTTP transport +- **Note**: Air Agent automatically handles CORS preflight issues by not sending the `mcp-protocol-version` header in subsequent requests. This prevents CORS preflight failures with servers that don't properly configure CORS for custom headers, while maintaining full MCP protocol compatibility. **Tools Not Appearing** - Make sure the MCP server is enabled in the configuration diff --git a/lib/mcp/client.ts b/lib/mcp/client.ts index feec571..dd0f4e7 100644 --- a/lib/mcp/client.ts +++ b/lib/mcp/client.ts @@ -10,6 +10,12 @@ * - Session IDs are stored in memory for the duration of the browser session (while page is loaded) * - The underlying transport automatically includes the session ID in request headers * - Note: Session persistence across page reloads is not supported + * + * CORS Compatibility: + * - Uses BrowserCompatibleTransport wrapper to prevent mcp-protocol-version header from being sent + * - This avoids CORS preflight requests that many MCP servers don't handle properly + * - Protocol version is still negotiated via the initialize request/response body + * - Maintains full MCP protocol compatibility while improving browser compatibility */ import { Client } from "@modelcontextprotocol/sdk/client" From 5a118c18fcea15983725e75ac968927104933d04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:11:51 +0000 Subject: [PATCH 4/6] Address code review feedback - remove redundant code and improve comments Co-authored-by: slhmy <31381093+slhmy@users.noreply.github.com> --- lib/mcp/client.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/mcp/client.ts b/lib/mcp/client.ts index dd0f4e7..966628c 100644 --- a/lib/mcp/client.ts +++ b/lib/mcp/client.ts @@ -86,9 +86,6 @@ class BrowserCompatibleTransport implements Transport { // mcp-protocol-version header from being added to requests. // This avoids CORS preflight issues with servers that don't properly // configure CORS for custom headers. - // setProtocolVersion(version: string): void { - // // No-op: Don't set protocol version to avoid CORS issues - // } } /** @@ -204,13 +201,12 @@ export class McpClient { this.client = null } if (this.wrappedTransport) { + // Close the wrapper, which delegates to the inner transport await this.wrappedTransport.close() this.wrappedTransport = null } - if (this.transport) { - // Transport is already closed via wrappedTransport, just clear the reference - this.transport = null - } + // Clear the inner transport reference (already closed via wrapper) + this.transport = null this.updateStatus("disconnected") } catch (error) { console.error("Error disconnecting from MCP server:", error) From cfe51b96ef69df5efd356452776d5f7ecb1e6d12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:13:46 +0000 Subject: [PATCH 5/6] Refactor to eliminate redundant transport field Co-authored-by: slhmy <31381093+slhmy@users.noreply.github.com> --- lib/mcp/client.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/mcp/client.ts b/lib/mcp/client.ts index 966628c..9a200ed 100644 --- a/lib/mcp/client.ts +++ b/lib/mcp/client.ts @@ -93,8 +93,7 @@ class BrowserCompatibleTransport implements Transport { */ export class McpClient { private client: Client | null = null - private transport: StreamableHTTPClientTransport | null = null - private wrappedTransport: BrowserCompatibleTransport | null = null + private transport: BrowserCompatibleTransport | null = null private config: McpServerConfig private statusCallback?: (status: McpConnectionStatus, error?: string) => void private sessionId: string | undefined @@ -129,7 +128,7 @@ export class McpClient { // MCD's MCP doc uses the origin URL directly (e.g. https://mcp.mcd.cn). // Some deployments may respond 405 for GET /; we treat that as ignorable. const effectiveUrl = normalizeMcpEndpointUrl(this.config.url) - this.transport = new StreamableHTTPClientTransport(new URL(effectiveUrl), { + const innerTransport = new StreamableHTTPClientTransport(new URL(effectiveUrl), { requestInit: { headers, }, @@ -140,7 +139,7 @@ export class McpClient { // Wrap transport to prevent mcp-protocol-version header from being sent // This avoids CORS preflight issues with servers that don't properly configure CORS - this.wrappedTransport = new BrowserCompatibleTransport(this.transport) + this.transport = new BrowserCompatibleTransport(innerTransport) // Create MCP client this.client = new Client( @@ -157,9 +156,9 @@ export class McpClient { // Connect to the server using the wrapped transport try { - await this.client.connect(this.wrappedTransport) + await this.client.connect(this.transport) // Read session ID from response header (set by the transport after initial request) - // The transport receives the mcp-session-id header from the server and stores it + // The transport wrapper delegates sessionId to the inner transport const receivedSessionId = this.transport.sessionId if (receivedSessionId) { this.sessionId = receivedSessionId @@ -200,13 +199,10 @@ export class McpClient { await this.client.close() this.client = null } - if (this.wrappedTransport) { - // Close the wrapper, which delegates to the inner transport - await this.wrappedTransport.close() - this.wrappedTransport = null + if (this.transport) { + await this.transport.close() + this.transport = null } - // Clear the inner transport reference (already closed via wrapper) - this.transport = null this.updateStatus("disconnected") } catch (error) { console.error("Error disconnecting from MCP server:", error) From b72d6c10b4c698271032faa479b36b2bf2064327 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:15:14 +0000 Subject: [PATCH 6/6] Final improvements: export class for testability and improve README clarity Co-authored-by: slhmy <31381093+slhmy@users.noreply.github.com> --- README.md | 2 +- lib/mcp/client.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3375c98..44e14ea 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ This enables seamless session resumption without manual intervention. - Ensure your MCP server has CORS properly configured - Check that the server URL is correct and publicly accessible - Verify that the server supports the Streamable HTTP transport -- **Note**: Air Agent automatically handles CORS preflight issues by not sending the `mcp-protocol-version` header in subsequent requests. This prevents CORS preflight failures with servers that don't properly configure CORS for custom headers, while maintaining full MCP protocol compatibility. +- Air Agent includes built-in compatibility for MCP servers with limited CORS support, automatically working around common CORS preflight issues **Tools Not Appearing** - Make sure the MCP server is enabled in the configuration diff --git a/lib/mcp/client.ts b/lib/mcp/client.ts index 9a200ed..e2ecf36 100644 --- a/lib/mcp/client.ts +++ b/lib/mcp/client.ts @@ -34,8 +34,11 @@ import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js" * * Note: The protocol version is still negotiated during the initialize handshake * (via the request/response body), so this doesn't affect protocol compatibility. + * + * @internal This is an implementation detail of McpClient and should not be used directly. + * It is exported for testing purposes only. */ -class BrowserCompatibleTransport implements Transport { +export class BrowserCompatibleTransport implements Transport { private innerTransport: StreamableHTTPClientTransport constructor(innerTransport: StreamableHTTPClientTransport) {