Skip to content

post_writer Client error 400 Bad Request when initializing session between Python MCP Client and TypeScript MCP Server #492

Closed
@mhuang448

Description

@mhuang448

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):

  1. Start the Node.js/TypeScript MCP server (listening on 0.0.0.0:8080).
  2. Start the Python FastAPI backend (listening on 127.0.0.1:8000), configured with MCP_PERPLEXITY_SSE_URL=http://127.0.0.1:8080/sse.
  3. Send a POST request to a specific API endpoint on the Python backend (/api/query/async).
  4. This triggers the Python backend to create an mcp.ClientSession and call await session.initialize().
  5. Observe the Python client logs showing the Error in post_writer: Client error '400 Bad Request' error.
  6. 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:

  1. The Python client (mcp v1.6.0, using sse.py and session.py) sends an initialize request via POST /messages?sessionId=....
  2. The Node.js server (@modelcontextprotocol/sdk v1.9.0, using sse.js) receives the request.
  3. The server successfully parses the JSON body and validates it against the JSONRPCMessageSchema.
  4. The server calls the SDK's transport.handlePostMessage.
  5. The server logs indicate that handlePostMessage completes execution without throwing an error outwards that our try/catch block in index.ts can see.
  6. However, the Python client's post_writer (from sse.py, using httpx) receives an HTTP 400 Bad Request response to its POST request, causing it to log the error.
  7. The Python client's await session.initialize() call hangs or fails without raising a standard exception catchable by except Exception.

Our leading hypothesis is that:

  • The Node.js Server logic (triggered via onmessage inside handlePostMessage) detects an issue processing the valid initialize 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 the catch block shown in sse.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's raise_for_status() but logs it in a way that doesn't cause the await 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.


Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions