Description
Environment:
- Python SDK:
mcp
v1.6.0 (installed via pip) - Node.js Server SDK:
@modelcontextprotocol/sdk
v1.9.0 (installed via npm) - Python Version: [Specify your Python version, e.g., 3.10]
- Node.js Version: [Specify your Node version, e.g., v18.18.0]
- Operating System: [Specify your OS, e.g., macOS Sonoma, Ubuntu 22.04]
Problem Description:
We are attempting to establish communication between a Python MCP client (running within a FastAPI backend) and a Node.js/TypeScript MCP server using the HTTP+SSE transport mechanism as defined for protocol version 2024-11-05
.
The Node.js server is adapted from Perplexity's official perplexity-ask
MCP server example but modified to use SSEServerTransport
from @modelcontextprotocol/sdk/server/sse.js
and wrapped within an Express application to expose /sse
(GET) and /messages
(POST) endpoints.
When the Python client attempts to initialize the session using mcp.ClientSession.initialize()
, the client-side code consistently fails, logging Error in post_writer: Client error '400 Bad Request'
for the /messages
POST request.
This is confusing because the Node.js server logs indicate that it successfully receives the POST request, parses the initialize
message body, validates it against JSONRPCMessageSchema
, and completes the transport.handlePostMessage
call without any logged errors on the server side.
Steps to Reproduce (Conceptual):
- Start the Node.js/TypeScript MCP server (listening on
0.0.0.0:8080
). - Start the Python FastAPI backend (listening on
127.0.0.1:8000
), configured withMCP_PERPLEXITY_SSE_URL=http://127.0.0.1:8080/sse
. - Send a POST request to a specific API endpoint on the Python backend (
/api/query/async
). - This triggers the Python backend to create an
mcp.ClientSession
and callawait session.initialize()
. - Observe the Python client logs showing the
Error in post_writer: Client error '400 Bad Request'
error. - Observe the Node.js server logs showing successful receipt and apparent handling of the POST request containing the
initialize
message.
Logs:
Node.js MCP Server Output:
Perplexity MCP Server (HTTP+SSE) listening on port 8080
-> SSE connections on GET /sse
-> Client messages on POST /messages?sessionId=<sessionId>
-> Health check on GET /health
SSE connection requested from 127.0.0.1
[c600f666-a379-444c-8cbd-bf44c17d5391] SSE transport created with sessionId: c600f666-a379-444c-8cbd-bf44c17d5391
[c600f666-a379-444c-8cbd-bf44c17d5391] Server connected to transport.
[c600f666-a379-444c-8cbd-bf44c17d5391] POST request received for /messages
[c600f666-a379-444c-8cbd-bf44c17d5391] --> Received body: {
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"sampling": {},
"roots": {
"listChanged": true
}
},
"clientInfo": {
"name": "mcp",
"version": "0.1.0"
}
},
"jsonrpc": "2.0",
"id": 0
}
[c600f666-a379-444c-8cbd-bf44c17d5391] DEBUG: Attempting to parse body with JSONRPCMessageSchema...
[c600f666-a379-444c-8cbd-bf44c17d5391] DEBUG: Schema validation successful.
[c600f666-a379-444c-8cbd-bf44c17d5391] DEBUG: Attempting to call transport.handlePostMessage...
[c600f666-a379-444c-8cbd-bf44c17d5391] DEBUG: transport.handlePostMessage completed.
Python Backend Output (Client):
Connecting to MCP server via SSE at 'http://127.0.0.1:8080/sse' (LLM Selection: True)...
SSE client connection established, creating session object...
DEBUG: Attempting session.initialize()...
Error in post_writer: Client error '400 Bad Request' for url 'http://127.0.0.1:8080/messages?sessionId=c600f666-a379-444c-8cbd-bf44c17d5391'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400
Relevant Code Snippets:
1. Python Client Logic (backend/app/pipeline_logic.py
)
# Relevant Imports
import asyncio
import time
from .utils import CONFIG, ANTHROPIC_CLIENT
from mcp import ClientSession # Using mcp v1.6.0
from mcp.client.sse import sse_client
async def _call_mcp(
intermediate_prompt: str,
mcp_server_name: str = "perplexity-ask", # Not directly used with SSE URL
use_llm_selection: bool = True
) -> str:
"""Connects to the configured MCP server URL via SSE/HTTP, establishes a session,
selects and calls a tool, and returns the text result.
"""
mcp_sse_url = CONFIG.get("mcp_perplexity_sse_url") # Reads e.g., 'http://127.0.0.1:8080/sse'
if not mcp_sse_url:
print("ERROR: MCP_PERPLEXITY_SSE_URL environment variable is not set.")
return "[Error: MCP Server URL not configured]"
print(f"Connecting to MCP server via SSE at '{mcp_sse_url}' (LLM Selection: {use_llm_selection})...")
mcp_result_text = "[MCP call failed]"
try:
# Use the sse_client context manager (assuming it's based on mcp.client.sse.sse_client)
async with sse_client(mcp_sse_url) as streams:
read_stream, write_stream = streams
print(" SSE client connection established, creating session object...")
# Create a ClientSession using the streams from sse_client
# NOTE: Using default clientInfo and capabilities as defined in mcp/client/session.py
async with ClientSession(*streams) as session:
try:
print(" DEBUG: Attempting session.initialize()...")
# ===== THIS IS WHERE THE ERROR OCCURS =====
# The 'Error in post_writer' log appears after this line is executed,
# but the exception block below is NOT triggered.
await session.initialize()
# ==========================================
print(" DEBUG: session.initialize() call completed without raising Exception.") # This line is never reached
except Exception as init_error:
# This block is currently NOT being hit
print(f" ERROR: Exception caught directly during session.initialize(): {type(init_error).__name__} - {init_error}")
raise
print(" MCP session initialized successfully (apparently).") # This line is never reached
# ... subsequent logic using the session (list_tools, call_tool) ...
# (Code omitted for brevity as initialization fails)
except httpx.ConnectError as e:
# ... error handling ...
except Exception as e:
# ... error handling ...
finally:
print(f"MCP SSE/HTTP interaction complete for server at '{mcp_sse_url}'.")
return mcp_result_text
# This _call_mcp function is called by background tasks triggered by FastAPI endpoints
2. Node.js Server Setup (backend/perplexity-mcp-server/index.ts
)
#!/usr/bin/env node
import express, { type Request, type Response } from "express";
// Using @modelcontextprotocol/sdk v1.9.0
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import {
type CallToolResult,
type ListToolsResult,
McpError,
ErrorCode,
type Tool,
ListToolsRequestSchema,
CallToolRequestSchema,
JSONRPCMessageSchema // Used for debug logging
} from "@modelcontextprotocol/sdk/types.js";
import dotenv from "dotenv";
dotenv.config();
// --- Tool Definitions (PERPLEXITY_ASK_TOOL, etc.) ---
// (Omitted for brevity - Standard tool definitions)
// --- performChatCompletion function ---
// (Omitted for brevity - Calls Perplexity API)
// --- Server Initialization ---
const server = new Server(
{
name: "perplexity-mcp-server",
version: "0.1.0",
},
{
// Explicitly declaring capabilities to match Python SDK's defaults
capabilities: {
tools: { listChanged: false },
sampling: {},
roots: { listChanged: true },
},
}
);
// --- Request Handlers (server.setRequestHandler) ---
server.setRequestHandler(
ListToolsRequestSchema,
async (): Promise<ListToolsResult> => { /* ... */ }
);
server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => { /* ... */ }
);
// --- Express App Setup ---
const app = express();
const port = parseInt(process.env.PORT || "8080", 10);
const transports: { [sessionId: string]: SSEServerTransport } = {};
// --- SSE Connection Route ---
app.get("/sse", async (req: Request, res: Response) => {
console.error(`SSE connection requested from ${req.ip}`);
try {
const transport = new SSEServerTransport("/messages", res); // Using SDK transport
const logPrefix = `[${transport.sessionId}]`;
transports[transport.sessionId] = transport;
console.error(`${logPrefix} SSE transport created with sessionId: ${transport.sessionId}`);
// Added for debugging - does not seem to catch the relevant error
transport.onerror = (error: Error) => {
console.error(`${logPrefix} SSEServerTransport received error:`, error);
};
res.on("close", () => { /* ... cleanup ... */ });
// Connects Server logic to Transport event handlers (onmessage, onclose)
await server.connect(transport);
console.error(`${logPrefix} Server connected to transport.`);
} catch (error) { /* ... error handling ... */ }
});
// --- Message Posting Route ---
app.post(
"/messages",
express.json({ limit: "5mb" }), // Parse JSON body
async (req: Request, res: Response) => {
const sessionId = req.query.sessionId as string;
// ... sessionId check ...
const logPrefix = `[${sessionId}]`;
console.error(`${logPrefix} POST request received for /messages`);
const transport = transports[sessionId];
console.error(`${logPrefix} --> Received body:`, JSON.stringify(req.body, null, 2));
if (transport) {
try {
// Debugging: Explicitly validate schema before SDK handles it
console.log(`${logPrefix} DEBUG: Attempting to parse body with JSONRPCMessageSchema...`);
const parsedMessage = JSONRPCMessageSchema.parse(req.body);
console.log(`${logPrefix} DEBUG: Schema validation successful.`); // Passes
// Call the SDK's handler
console.log(`${logPrefix} DEBUG: Attempting to call transport.handlePostMessage...`);
// This internally calls handleMessage -> onmessage -> Server logic
await transport.handlePostMessage(req, res);
// This line IS reached in the logs
console.log(`${logPrefix} DEBUG: transport.handlePostMessage completed.`);
} catch (error) {
// This block is NOT reached in the logs for the initialize error
console.error(`${logPrefix} ERROR caught during schema parsing or handlePostMessage:`, error);
if (!res.headersSent) { /* ... send 500 ... */ }
}
} else { /* ... handle no transport ... */ }
}
);
// --- Health Check Route ---
app.get("/health", (req: Request, res: Response) => { /* ... */ });
// --- Start Server ---
app.listen(port, "0.0.0.0", () => { /* ... logging ... */ });
// Server started via: npm run build && node dist/index.js
Analysis and Hypothesis:
Based on the logs and code:
- The Python client (
mcp
v1.6.0, usingsse.py
andsession.py
) sends aninitialize
request viaPOST /messages?sessionId=...
. - The Node.js server (
@modelcontextprotocol/sdk
v1.9.0, usingsse.js
) receives the request. - The server successfully parses the JSON body and validates it against the
JSONRPCMessageSchema
. - The server calls the SDK's
transport.handlePostMessage
. - The server logs indicate that
handlePostMessage
completes execution without throwing an error outwards that ourtry/catch
block inindex.ts
can see. - However, the Python client's
post_writer
(fromsse.py
, usinghttpx
) receives an HTTP 400 Bad Request response to its POST request, causing it to log the error. - The Python client's
await session.initialize()
call hangs or fails without raising a standard exception catchable byexcept Exception
.
Our leading hypothesis is that:
- The Node.js
Server
logic (triggered viaonmessage
insidehandlePostMessage
) detects an issue processing the validinitialize
payload (potentially still capability or protocol related, or another internal check). - Instead of sending a JSON-RPC Error over the SSE stream, the
Server
logic throws an internal JavaScript error. - This error is caught silently within the Node.js SDK's
SSEServerTransport.handlePostMessage
method (specifically in thecatch
block shown insse.js
source), which incorrectly sends an HTTP 400 response back to the originating POST request. - The Python client correctly identifies the HTTP 400 as an error via
httpx
'sraise_for_status()
but logs it in a way that doesn't cause theawait session.initialize()
call to raise a catchable exception in our application code.
Request:
Could you please investigate the error handling within the Node.js @modelcontextprotocol/sdk
v1.9.0, specifically how errors thrown by the Server
logic during the processing of an initialize
request (after schema validation) are handled by the SSEServerTransport
?
It appears that such errors might be causing the transport to send an HTTP 400 response to the client's POST request, rather than sending a JSON-RPC Error response over the SSE stream as expected by the protocol flow. This prevents the Python client from successfully completing the initialization handshake.
Any insight into why the server might be rejecting the initialize
message internally after schema validation, or how to ensure errors are correctly propagated via SSE, would be greatly appreciated.