Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- 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
Expand Down
86 changes: 82 additions & 4 deletions lib/mcp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,93 @@
* - 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"
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.
*
* @internal This is an implementation detail of McpClient and should not be used directly.
* It is exported for testing purposes only.
*/
export class BrowserCompatibleTransport implements Transport {
private innerTransport: StreamableHTTPClientTransport

constructor(innerTransport: StreamableHTTPClientTransport) {
this.innerTransport = innerTransport
}

async start(): Promise<void> {
return this.innerTransport.start()
}

async send(message: JSONRPCMessage | JSONRPCMessage[]): Promise<void> {
return this.innerTransport.send(message)
}

async close(): Promise<void> {
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.
}

/**
* MCP Client wrapper for browser use
*/
export class McpClient {
private client: Client | null = null
private transport: StreamableHTTPClientTransport | null = null
private transport: BrowserCompatibleTransport | null = null
private config: McpServerConfig
private statusCallback?: (status: McpConnectionStatus, error?: string) => void
private sessionId: string | undefined
Expand Down Expand Up @@ -57,7 +131,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,
},
Expand All @@ -66,6 +140,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.transport = new BrowserCompatibleTransport(innerTransport)

// Create MCP client
this.client = new Client(
{
Expand All @@ -79,11 +157,11 @@ export class McpClient {
}
)

// Connect to the server
// Connect to the server using the wrapped transport
try {
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
Expand Down