From ba9e6c9f3f3bd09da0f6343876e501a7bafb5f91 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Tue, 24 Jun 2025 22:25:57 +1000 Subject: [PATCH] Add debug logging and Accept headers for MCP transport debugging - Added extensive debug logging to transport creation and connection flows - Added proper Accept header (application/json, text/event-stream) to transport options - Fixed scroll jittering during streaming with instant scroll behavior - Investigating HTTP transport 'Not connected' errors and SSE connection hanging --- .../src/components/ConversationThread.tsx | 2 +- examples/chat-ui/src/hooks/useAutoscroll.ts | 6 +-- .../chat-ui/src/hooks/useStreamResponse.ts | 6 +-- examples/example-server/vengabus/src/index.ts | 19 +++++++- package.json | 4 +- pnpm-lock.yaml | 46 ++++++++++++++++--- src/react/useMcp.ts | 36 ++++++++++++++- 7 files changed, 99 insertions(+), 20 deletions(-) diff --git a/examples/chat-ui/src/components/ConversationThread.tsx b/examples/chat-ui/src/components/ConversationThread.tsx index 49147a1..0e0d32f 100644 --- a/examples/chat-ui/src/components/ConversationThread.tsx +++ b/examples/chat-ui/src/components/ConversationThread.tsx @@ -101,7 +101,7 @@ const ConversationThread: React.FC = ({ //when new message chunks are streamed in, scroll to bottom useEffect(() => { - scrollToBottom() + scrollToBottom(isLoading && streamStarted) }, [aiResponseRef.current]) //when conversation changes, scroll to bottom diff --git a/examples/chat-ui/src/hooks/useAutoscroll.ts b/examples/chat-ui/src/hooks/useAutoscroll.ts index 46688f4..dac765a 100644 --- a/examples/chat-ui/src/hooks/useAutoscroll.ts +++ b/examples/chat-ui/src/hooks/useAutoscroll.ts @@ -22,9 +22,9 @@ export const useAutoscroll = () => { }; }, []); - const scrollToBottom = () => { - if (shouldAutoScrollRef.current) { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + const scrollToBottom = (force?: boolean) => { + if (shouldAutoScrollRef.current || force) { + messagesEndRef.current?.scrollIntoView({ behavior: force ? "instant" : "smooth" }); } }; diff --git a/examples/chat-ui/src/hooks/useStreamResponse.ts b/examples/chat-ui/src/hooks/useStreamResponse.ts index 669922c..ce50559 100644 --- a/examples/chat-ui/src/hooks/useStreamResponse.ts +++ b/examples/chat-ui/src/hooks/useStreamResponse.ts @@ -22,7 +22,7 @@ const hasContent = (message: Message): message is UserMessage | AssistantMessage interface UseStreamResponseProps { conversationId?: number setConversations: React.Dispatch> - scrollToBottom: () => void + scrollToBottom: (force?: boolean) => void selectedModel: Model onApiKeyRequired: (model: Model) => Promise mcpTools: Tool[] @@ -284,7 +284,7 @@ export const useStreamResponse = ({ } return updated }) - scrollToBottom() + scrollToBottom(true) } else { console.warn('[useStreamResponse] Tool call event missing toolName:', event) } @@ -382,7 +382,7 @@ export const useStreamResponse = ({ return updated }) - scrollToBottom() + scrollToBottom(true) } } catch (e) { console.error('[useStreamResponse] Error in full stream processing:', e) diff --git a/examples/example-server/vengabus/src/index.ts b/examples/example-server/vengabus/src/index.ts index d7fd902..af12712 100644 --- a/examples/example-server/vengabus/src/index.ts +++ b/examples/example-server/vengabus/src/index.ts @@ -6,6 +6,12 @@ import { cors } from 'hono/cors' const app = new Hono() +app.use('/*', cors({ + origin: '*', + allowHeaders: ['content-type','mcp-session-id','mcp-protocol-version'], + exposeHeaders: ['mcp-session-id'], +})) + // Your MCP server implementation const mcpServer = new McpServer({ name: 'my-mcp-server', @@ -23,7 +29,18 @@ mcpServer.registerTool("add", }) ); -app.all('/mcp', cors(), async (c) => { +mcpServer.registerTool("get_vengabus_times", + { + title: "Vengabus Schedule Checker", + description: "This checks to see when the next Vengabus is, or whether there is one for the user's current location. Please display all the information to the user in a tabulated form.", + inputSchema: { } + }, + async () => ({ + content: [{ type: "text", text: `Next Vengabus: imminent. Current user's route: New York to San Francisco. Route name: "Intercity Disco".` }] + }) +); + +app.post('/mcp', async (c) => { const transport = new StreamableHTTPTransport() await mcpServer.connect(transport) return transport.handleRequest(c) diff --git a/package.json b/package.json index 46a585f..1243f9f 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,9 @@ "build:site": "npx @axodotdev/oranda build", "deploy:site": "npx wrangler deploy" }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.0" - }, "devDependencies": { "@axodotdev/oranda": "^0.6.5", + "@modelcontextprotocol/sdk": "^1.13.1", "@types/react": "^19.0.12", "prettier": "^3.5.3", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f6f618..357860c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,14 +7,13 @@ settings: importers: .: - dependencies: - '@modelcontextprotocol/sdk': - specifier: ^1.11.0 - version: 1.11.0 devDependencies: '@axodotdev/oranda': specifier: ^0.6.5 version: 0.6.5 + '@modelcontextprotocol/sdk': + specifier: ^1.13.1 + version: 1.13.1 '@types/react': specifier: ^19.0.12 version: 19.0.12 @@ -532,8 +531,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@modelcontextprotocol/sdk@1.11.0': - resolution: {integrity: sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==} + '@modelcontextprotocol/sdk@1.13.1': + resolution: {integrity: sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==} engines: {node: '>=18'} '@pkgjs/parseargs@0.11.0': @@ -654,6 +653,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -913,6 +915,12 @@ packages: exsolve@1.0.5: resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -1038,6 +1046,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1453,6 +1464,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -1840,8 +1854,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@modelcontextprotocol/sdk@1.11.0': + '@modelcontextprotocol/sdk@1.13.1': dependencies: + ajv: 6.12.6 content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 @@ -1930,6 +1945,13 @@ snapshots: acorn@8.14.0: {} + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -2232,6 +2254,10 @@ snapshots: exsolve@1.0.5: {} + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -2359,6 +2385,8 @@ snapshots: joycon@3.1.1: {} + json-schema-traverse@0.4.1: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -2794,6 +2822,10 @@ snapshots: unpipe@1.0.0: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + utils-merge@1.0.1: {} vary@1.1.2: {} diff --git a/src/react/useMcp.ts b/src/react/useMcp.ts index e755bb2..6da6e1b 100644 --- a/src/react/useMcp.ts +++ b/src/react/useMcp.ts @@ -187,18 +187,34 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { const commonOptions: SSEClientTransportOptions = { authProvider: authProviderRef.current, - requestInit: { headers: customHeaders }, + requestInit: { + headers: { + 'Accept': 'application/json, text/event-stream', + ...customHeaders + } + }, } const targetUrl = new URL(url) + addLog('debug', `Creating ${transportType.toUpperCase()} transport for URL: ${targetUrl.toString()}`) + addLog('debug', `Transport options:`, { + authProvider: !!authProviderRef.current, + headers: customHeaders, + url: targetUrl.toString() + }) + if (transportType === 'http') { + addLog('debug', 'Creating StreamableHTTPClientTransport...') transportInstance = new StreamableHTTPClientTransport(targetUrl, commonOptions) + addLog('debug', 'StreamableHTTPClientTransport created successfully') } else { // sse + addLog('debug', 'Creating SSEClientTransport...') transportInstance = new SSEClientTransport(targetUrl, commonOptions) + addLog('debug', 'SSEClientTransport created successfully') } transportRef.current = transportInstance // Assign to ref immediately - addLog('debug', `${transportType.toUpperCase()} transport created.`) + addLog('debug', `${transportType.toUpperCase()} transport created and assigned to ref.`) } catch (err) { // Use stable failConnection failConnection( @@ -217,6 +233,13 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { } transportInstance.onerror = (err: Error) => { // Transport errors usually mean connection is lost/failed definitively for this transport + addLog('warn', `Transport error event (${transportType.toUpperCase()}):`, err) + addLog('debug', `Error details:`, { + message: err.message, + stack: err.stack, + name: err.name, + cause: err.cause + }) // Use stable failConnection failConnection(`Transport error (${transportType.toUpperCase()}): ${err.message}`, err) // Should we return 'failed' here? failConnection sets state, maybe that's enough. @@ -247,6 +270,9 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { // 3. Attempt client.connect() try { addLog('info', `Connecting client via ${transportType.toUpperCase()}...`) + addLog('debug', `About to call client.connect() with transport instance`) + addLog('debug', `Transport instance type: ${transportInstance.constructor.name}`) + await clientRef.current!.connect(transportInstance) // --- Success Path --- @@ -270,6 +296,12 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { } catch (connectErr) { // --- Error Handling Path --- addLog('debug', `Client connect error via ${transportType.toUpperCase()}:`, connectErr) + addLog('debug', `Connect error details:`, { + message: connectErr instanceof Error ? connectErr.message : String(connectErr), + stack: connectErr instanceof Error ? connectErr.stack : 'N/A', + name: connectErr instanceof Error ? connectErr.name : 'Unknown', + cause: connectErr instanceof Error ? connectErr.cause : undefined + }) const errorInstance = connectErr instanceof Error ? connectErr : new Error(String(connectErr)) // Check for 404/405 specifically for HTTP transport