Skip to content

StdioServerTransportProvider stops reading after malformed JSON #1172

Open

Description

@VogSok

Summary

With io.modelcontextprotocol.sdk:mcp-core:2.0.1 and the Jackson 2 mapper, a malformed JSON line terminates the server transport's inbound reader. Subsequent valid requests receive no response.

This was reproduced with a minimal SDK server, independently of any application input adapter.

Environment

  • MCP Java SDK: 2.0.1
  • Jackson mapper: JacksonMcpJsonMapper
  • Java: 21.0.12.1
  • Windows 11
  • Initialized protocol version: 2025-11-25

Minimal server

Use io.modelcontextprotocol.sdk:mcp:2.0.1, io.modelcontextprotocol.sdk:mcp-json-jackson2:2.0.1, and their transitive dependencies:

import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;

public class Repro {
    public static void main(String[] args) throws Exception {
        var mapper = new JacksonMcpJsonMapper(new ObjectMapper());
        var transport = new StdioServerTransportProvider(mapper, System.in, System.out);
        var server = McpServer.sync(transport)
                .serverInfo("stdio-repro", "1.0")
                .build();
        Thread.currentThread().join();
    }
}

Keep logging on stderr. Send these newline-delimited messages to stdin in order, waiting for the initialize response before the initialized notification:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"repro","version":"1.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"ping"}
{broken
{"jsonrpc":"2.0","id":3,"method":"ping"}

Actual result

  • Initialize succeeds.
  • Ping with id 2 returns an empty result.
  • The malformed line logs Error processing inbound message to stderr.
  • No JSON-RPC parse-error response is returned.
  • Ping with id 3 receives no response during a four-second observation window.
  • The minimal process remains alive because the main thread is waiting, but its transport no longer reads requests.

The catch block in StdioServerTransportProvider at v2.0.1, lines 249–251 breaks out of the inbound loop. The same catch-and-break behavior is present on the current main branch.

Expected result

For a malformed JSON line, return a JSON-RPC parse error with id: null and code -32700, then continue reading subsequent newline-delimited requests. The later ping should still receive its response.

Any error response should use the transport's serialized output path to avoid interleaving with concurrent responses. This report concerns malformed JSON recovery; protocol/session validation should remain in the SDK.

Related report and workaround

Issue #1157 describes a related failure on the STDIO client when a response is rejected. This reproduction concerns the server receiving malformed JSON input.

Our local workaround is a bounded input adapter that rejects malformed lines before forwarding valid messages to the official SDK. Tests verify continued reading after malformed input, invalid UTF-8, and oversized lines. Recovery in the transport itself would remove the need for that application-level adapter.

Activity

  1. jarrettdustinqq commented on Oct 12, 2026

    @jarrettdustinqq

    Source-level review of current main found two implementation boundaries worth covering before changing the inner catch to continue:

    1. McpSchema.JSONRPCResponse currently rejects a null id, so JSONRPCResponse.error(null, ...) cannot represent the required parse-error response. It is also annotated with @JsonInclude(NON_ABSENT), so simply relaxing the assertion risks serializing no id member rather than the required "id": null. This appears to need either a transport-internal error envelope with explicit null-ID serialization or a narrowly scoped response-model/serialization change that preserves the normal MCP response-ID checks.
    2. The current catch (Exception) covers both JSON syntax failures and failures after a JSON value has parsed (for example, deserializeJsonRpcMessage rejecting an unrecognized/invalid envelope). A recovery patch should keep -32700 (Parse error) separate from -32600 (Invalid Request), using id: null when no valid request ID can be established, rather than mapping every exception to a parse error.

    I would also keep MaxSizeExceededException and I/O failures connection-fatal unless the oversized-line path deliberately drains through its terminator: readLine currently throws while still positioned inside that line, so continuing immediately would reinterpret its remainder as a new message.

    A focused regression matrix could assert the exact wire object (including "id":null), then cover malformed JSON → valid ping, parsed-but-invalid envelope → valid ping, consecutive bad lines, and both Jackson 2/3 profiles. It should also assert that replies still use the single outbound queue, remain ordered, and leave the session open after recoverable complete-line errors.

    This is an AI-assisted source review submitted for human verification. I did not run the Java test suite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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