Skip to content

[Bug]: /mcp answers unknown methods (including the 2026-07-28 server/discover probe) with -32602, so modern MCP clients re-probe 17× before falling back #5841

Open

Description

@XHR666

Issue Origin

Observed or reproduced in a real environment

Bug Description

POST /mcp answers the 2026-07-28-era probe server/discover — and any unknown method —
with JSON-RPC -32602 Invalid request parameters instead of -32601 Method not found.
This is an upstream defect in the pinned MCP Python SDK, not an OpenViking logic bug:

Root cause: mcp/shared/session.py maps any request-union validation failure to
INVALID_PARAMS, so "method does not exist" never reaches the METHOD_NOT_FOUND branch.
OpenViking pins mcp<2 (open PR #4651), so 1.29.0 is affected.

Client-visible impact with an MCP 2.x client: because a probe has no parameters to fix, the
client does not treat -32602 as the definitive legacy-fallback signal and re-sends the
probe 17 times per connection before falling back to the 2025-era initialize
handshake. The connection then succeeds (era legacy, 15 tools), so this is a
compatibility/noise defect rather than a hard failure — but every connect costs 17 extra
round trips and 17 WARNING Failed to validate request lines.

Steps to Reproduce

# 1) modern probe
curl -s -i -X POST http://127.0.0.1:1933/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}'
# HTTP/1.1 200 ... text/event-stream
# data: {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}

# 2) any unknown method — same answer, proving it is one code path
curl -s -X POST http://127.0.0.1:1933/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"totally/unknown","params":{}}'

# 3) control: the legacy handshake down-negotiates correctly
curl -s -X POST http://127.0.0.1:1933/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":3,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
# data: {"jsonrpc":"2.0","id":3,"result":{"protocolVersion":"2025-11-25",...,"serverInfo":{"name":"openviking","version":"1.29.0"}}}

Expected Behavior

-32601 Method not found for methods outside the handler registry, keeping -32602 for
genuine parameter errors, so modern clients take the legacy path on the first response.

Actual Behavior

-32602 for every unknown method; the client retries the probe until it gives up and falls
back. Measured A/B with the same client and parameters: 17 probes on the wire with the
current server, 0 when the same method is answered -32601; both connect successfully
and list the same 15 tools.

Minimal Reproducible Example

### Minimal Reproducible Example

**A. Server-side: the defect itself (no dependencies, ~10 s)**


# 1) the modern-era probe → HTTP 200 + JSON-RPC -32602 (should be -32601)
curl -s -i -X POST http://127.0.0.1:1933/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}'
# HTTP/1.1 200 OK, content-type: text/event-stream
# data: {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}

# 2) any unknown method → the SAME code, i.e. this is one generic path, not a server/discover special case
curl -s -X POST http://127.0.0.1:1933/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"totally/unknown","params":{}}'
# data: {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}

# 3) control: the legacy handshake itself is fine and down-negotiates correctly
curl -s -X POST http://127.0.0.1:1933/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":3,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
# data: {"jsonrpc":"2.0","id":3,"result":{"protocolVersion":"2025-11-25",...,"serverInfo":{"name":"openviking","version":"1.29.0"}}}


One `WARNING Failed to validate request` line is appended to the server log per attempt
(`PingRequest.method Input should be 'ping'`, `input_value='server/discover'`).

**B. Client-side: the impact, on the stdio transport (this is the topology DSH uses)**

Requires an MCP client SDK from the 2026-07-28 era; we used
`@modelcontextprotocol/client@2.0.0`. Any stdio↔HTTP bridge in front of `/mcp` reproduces
it; we used the OpenViking DSH plugin's own proxy
(`node_modules/@openviking/dsh-memory-plugin/servers/mcp-proxy.mjs`).


// repro.mjs —  node repro.mjs
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';

const PROXY = process.argv[2]; // path to @openviking/dsh-memory-plugin/servers/mcp-proxy.mjs
const client = new Client(
  { name: 'dsh-mcp-client', version: '0.0.1' },
  { capabilities: {}, versionNegotiation: { mode: 'auto' } },  // the mode DSH uses
);
await client.connect(new StdioClientTransport({
  command: 'node', args: [PROXY], env: { ...process.env, HOME: process.env.HOME },
}));
console.log('era =', client.getProtocolEra?.(), 'tools =', (await client.listTools()).tools.length);
await client.close();


Measure the probe storm by counting the server-side log around one connect:


before=$(grep -c 'server/discover' /tmp/openviking-server.log)
node repro.mjs /path/to/@openviking/dsh-memory-plugin/servers/mcp-proxy.mjs
sleep 4   # let the server flush
after=$(grep -c 'server/discover' /tmp/openviking-server.log)
echo "sent $(($after - $before)) server/discover probes for ONE connect"


Observed on OpenViking 0.4.15 / mcp 1.29.0:


era = legacy tools = 15
sent 17 server/discover probes for ONE connect


The connection still succeeds (era `legacy`, all 15 tools), so this is a compatibility/noise
defect: 17 wasted round trips and 17 misleading `WARNING` lines per connect, with no
diagnostic in `error.data`.

**C. Control: the error code is the cause**

Insert a shim between the client and `/mcp` that answers `server/discover` locally with
`{"jsonrpc":"2.0","id":<same id>,"error":{"code":-32601,"message":"Method not found"}}` and
forwards everything else unchanged. Same client, same options:


era = legacy tools = 15
sent 0 server/discover probes for ONE connect


Same successful connection, zero probe retries — i.e. `-32602` (not the transport, not the
server's capabilities) is what makes the client keep re-probing.

**Scope note (verified):** the probe storm is transport-dependent. With the *same* client and
the *same* `versionNegotiation: {mode: 'auto'}` over `StreamableHTTPClientTransport` pointed
directly at `http://127.0.0.1:1933/mcp`, we measured **0** probes and a successful connect.
The stdio path is the affected one (and it is the path DSH uses); #4830 shows an HTTP-side
client (ChatGPT Secure MCP Tunnel) sending `server/discover` as well.

Error Logs

### Error Logs


WARNING  Failed to validate request
  PingRequest.method  Input should be 'ping'
    input_value='server/discover'
  session.py:383


One line per probe attempt; on one local install the `server/discover` occurrences grew
102 → 136 → 170 across a handful of client connects (~17 per connect).

OpenViking Version

0.4.15 (reports serverInfo.version = "1.29.0", i.e. MCP Python SDK 1.29.0)

Python Version

3.10

Operating System

Linux

Model Backend

Other

Additional Context

Severity: connections still succeed, so this is a compatibility/noise defect. It becomes
user-visible for clients that pin the modern era, use tighter timeouts, or surface the last
negotiation error — and the log line actively misleads ("Failed to validate request" reads
like a malformed client request, not "unknown method").

Options

  1. Local workaround (no upstream dependency): resolve the method against the handler
    registry before request validation and answer -32601 for anything unknown. Suggested
    regression tests: server/discover and an arbitrary unknown method both return -32601;
    a genuine parameter error still returns -32602; initialize with 2026-07-28 keeps
    down-negotiating to 2025-11-25 and tools/list still returns all 15 tools.
  2. Upstream: follow refactor(pathlock):using rust implement instead python #3557; the fix already exists as PR fix(server): default OAuth client scope for scope-less DCR (ChatGPT invalid_scope) #3210 (closed for a missing issue
    assignment). Alternatively relax the dependency bound to mcp>=1.27,<3 (PR chore(deps): update mcp requirement from <2,>=1.27 to >=1.27,<3 #4651) — per
    the SDK v2 notes, one MCPServer then serves the 2026-07-28 revision and every 2025-era
    client from the same server.

Related

Activity

  1. XHR666 commented on Oct 11, 2026

    @XHR666
    Author

    1. Reporter environment (measured, not from memory)

    Item Value
    OS Linux (aarch64)
    Node / Python v24.19.0 / 3.10.12
    DSH 0.2.0-rc.2
    @openviking/dsh-memory-plugin 0.5.20 (installed under ~/.dsh/profiles/web/node_modules/@openviking/dsh-memory-plugin)
    OpenViking server 0.4.15 (pip), listening on 127.0.0.1:1933
    MCP Python SDK mcp 1.29.0 (matches serverInfo.version = "1.29.0" seen in the handshake)
    MCP transport / client path stdio, via the plugin's servers/mcp-proxy.mjs — the path DSH actually uses
    Client SDK / negotiation mode MCP client SDK of the 2026-07-28 era, versionNegotiation: { mode: 'auto' }

    Re-checked live on exactly this stack while writing this comment (still reproducible):

    curl -s -m 3 -X POST http://127.0.0.1:1933/mcp \
      -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}'
    # => event: message
    #    data: {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}
    

    2. Impact on DSH (why this is not ignorable for us)

    DSH reaches this endpoint over a stdio MCP proxy, i.e. the transport where the probe storm reproduces
    (per the scope note above: StreamableHTTP straight to /mcp measured 0 probes, stdio measured 17). Concretely:

    1. Every connect pays 17 extra round trips (server/discover re-probes) before the client falls back to the
      2025-era initialize handshake — a fixed latency tax on every session start / reconnect, multiplied by the
      number of memory-plugin connections a session opens.
    2. Every connect emits 17 misleading WARNING Failed to validate request lines
      (PingRequest.method Input should be 'ping', input_value='server/discover'). They read like malformed client
      requests
      , so they pollute our logs and make real client-side bugs harder to triage — and error.data is empty,
      so nothing distinguishes "unknown method" from "bad parameters".
    3. It is a latent hard failure, not only noise, on stricter clients: a client that pins the modern era, uses
      tighter timeouts, or surfaces the last negotiation error to the user turns this into a visible connect
      failure, even though the connection would have succeeded after the fallback. We currently rely on
      versionNegotiation: { mode: 'auto' }, so we sit on the "noise + latency" side of that line — but the margin is
      set by the upstream SDK, not by us.

    3. User-visible blocker observed on DSH (severity is higher than "noise")

    Beyond the 17 probes, we have hit a state where the DSH chat itself becomes unusable while the OpenViking
    plugin is active
    :

    • A message sent from the DSH UI never gets processed — it goes into a queued state and stays there;
      no reply appears, and the request never reaches the model (no "deep thinking" phase starts).
    • Reloading the page does not recover it: the message is re-queued, and the send button becomes
      permanently disabled
      — the user can no longer send anything until the plugin is detached / the session is reset.
    • Reproducibility: observed repeatedly with the plugin enabled on the environment above; disabling the
      plugin restores normal sending.

    Honest caveat (please treat as unconfirmed): we have not yet isolated whether this blocker is caused by
    this negotiation defect specifically, by the probe storm exhausting a timeout/retry budget in the client, or by a
    separate plugin-side issue. What we can state is the observed behavior above, on this exact stack, with the
    client's versionNegotiation mode 'auto' going down the stdio path. If the client blocks session readiness on
    negotiation completion, the extra 17 re-probes are a plausible amplifier — but that is a hypothesis, not a
    confirmed causal chain, and we would rather report it plainly than overstate it.

    Suggested next step for whoever picks this up: while the fix from Option 1 (answer -32601 for unknown methods)
    lands, it would help to know whether the client treats a repeated -32602 during negotiation as "keep waiting"
    rather than "fall back now"
    — that is the part that would explain a queue that never drains.

  2. r266-tech commented on Oct 11, 2026

    @r266-tech
    Contributor

    I checked the client fallback question independently with @modelcontextprotocol/client@2.0.0, Node 26.8.2, and the production shared proxy core at 4c9a755b (unchanged from the fetched checkout). The HTTP endpoint was a synthetic legacy server returning the exact error shape shown here, followed by a valid legacy initialize/tools response.

    For both -32602 and -32601, with probe.maxRetries set to either 0 or 16, the upstream trace was one server/discover → initialize → notifications/initialized → tools/list. All four controls resolved connect() with era legacy and returned the fixture tool. In this published client, classifyRpcError() falls back for either code; negotiateEra() repeats on probe timeouts, not a received -32602.

    This does not reproduce your DSH UI or live OpenViking environment, and does not excuse the server's wrong JSON-RPC code. It does mean the 17 attempts / readiness blockage need the exact loaded client build and connection trace before attributing them to error-code retries. Could you provide the installed client package version/resolved path and whether those 17 requests come from one child PID/connection or repeated connections? A timeout or host-level retry could produce a different path.

    AI-assisted investigation; no live DSH queue reproduction or server fix claimed.

  3. XHR666 commented on Oct 11, 2026

    @XHR666
    Author

    Thanks — your two readings check out on our side, and I'm withdrawing my earlier attribution.

    Loaded build (exact): DSH 0.2.0-rc.2 on Node v24.19.0 (note: not 26.8.2) → @deepseek-ai/dsh-mcp-client@0.2.0-rc.2 (lib/index.js sha256 758d8fc473b700b5…) → @modelcontextprotocol/client@2.0.0, resolved to /opt/node/lib/node_modules/@deepseek-ai/dsh/node_modules/@modelcontextprotocol/client (nested under dsh, not a hoisted copy). dist/index.cjs sha256 60dc3a0f0f1cdafc… (246,150 B), dist/index.mjs af3ec9fcf3a70fb4…. Your probe.maxRetries-is-timeouts-only reading matches our installed dist/index.d.ts:1655-1657 and index.cjs:2693-2702.

    Where the retries come from: DSH's own reconnect supervisor, not the SDK probe — our harness log shows mcp-client(openviking): connection failed; retrying in 500ms (attempt 1/10) → 1000ms (2/10) → 2000ms (3/10), with maxAttempts defaulting to 10 and exhaustion unregistering the tool (dsh-mcp-client/lib/types/connection.d.ts:24-29; one attempt = "fresh transport + client", lib/index.js:586).

    One PID vs repeated connections: the transport is StreamableHTTP to
    http://127.0.0.1:1933/mcp
    — no command:/stdio — so there is no per-attempt child PID; each attempt is a new connection to the same URL. Your hypothesis about a timeout/host-level retry path is live on our side too: the server log is dominated by JSONDecodeError (6,136) and ExceptionGroup (3,068), so transport-level failures are plausible confounders.

    On the "17": I can't corroborate it from our logs — our startup log shows 3 attempts and no "giving up", and my attempt to derive 17 from 34 server/discover hits failed (Failed to validate request occurs only twice; the rest are traceback echoes). To pin it down I need the host log from the run where the blockage happened, plus the effective reconnect/probe config.

    Server-side trace we do have: the probe arrives as server/discover with only a _meta envelope, and pydantic's ClientRequest union rejects it (Input should be 'ping' / 'initialize') — so the wrong method/code on the server is confirmed; that part is not in dispute.

  4. akushonkamen commented on Oct 11, 2026

    @akushonkamen
    Contributor

    Confirmed the root cause — it's in the pinned MCP Python SDK, not OpenViking logic:

    • mcp/shared/session.py (v1.29.0, src/mcp/shared/session.py:360-395): _receive_request_type.model_validate(...) (:362) fails for any method outside the ClientRequest union; the generic except (:380) then always answers INVALID_PARAMS (:385-393), so the METHOD_NOT_FOUND branch is unreachable. The streamable-HTTP transport (streamable_http.py:500-509) only validates the JSON-RPC envelope, so unknown methods do reach the session layer.
    • OpenViking pins mcp>=1.27,<2 (pyproject.toml:84), so 1.29.0 is affected; /mcp is mounted at openviking/server/app.py (_ScopedRoute("/mcp", ...) → create_mcp_app() in openviking/server/mcp_endpoint.py), with no method pre-check before the SDK.

    I'm working on a repo-side compatibility layer: a small pure-ASGI middleware in front of the /mcp mount that resolves the request method against the known set (derived dynamically from the pinned SDK's request types, so it tracks SDK changes) and answers -32601 Method not found before the request hits the SDK union. Regression targets: server/discover / unknown method → -32601; genuine parameter errors → still -32602; initialize with 2026-07-28 keeps down-negotiating to 2025-11-25; tools/list unchanged.

    To be explicit on scope: this is a mitigation only — the proper fix is upstream in modelcontextprotocol/python-sdk#3557, and I won't touch the mcp<2 pin (that's #4651 territory). And agreeing with @r266-tech's client-side analysis above, the value here is spec-compliant error codes and removing probe noise/log spam, not a hard-failure rescue.

    AI-assisted contribution: investigation and patch prepared with AI assistance under human review; will be disclosed in the PR per the template.

  5. moved this from Backlog to In progress in OpenViking projecton Oct 12, 2026
  6. akushonkamen commented on Oct 12, 2026

    @akushonkamen
    Contributor

    Opened PR #5923 with a repo-side compatibility shim: POST /mcp now answers -32601 Method not found for methods outside the installed SDK's routable set (derived from the ClientRequest union at init, fail-open on introspection failure), while known methods, notifications, batch/unparseable bodies and non-JSON Accept headers keep the SDK's exact behavior. The proper fix remains upstream (python-sdk#3557), as noted in my claim comment. Live green run: both unknown methods → -32601, initialize → 2025-11-25 unchanged; tools/call bad params → SDK -32602; 12 new unit tests green.

  7. added
    kernelCore server, runtime, storage, retrieval, SDK, and CLI.
    agent-pluginsAgent harness and plugin integrations
    on Oct 12, 2026
  8. r266-tech commented on Oct 12, 2026

    @r266-tech
    Contributor

    Agreed with the correction. I also checked the revised direct-HTTP path with the real @modelcontextprotocol/client@2.0.0 against a synthetic legacy HTTP server. Both error codes, with probe retries set to 0 or 16, produced one server/discover, then initialize, notifications/initialized, and tools/list; all four controls connected and returned the fixture tool. This used Node 26.8.2, so it does not reproduce your Node 24 / DSH UI environment.

    The published @deepseek-ai/dsh-mcp-client@0.2.0-rc.2 lib/index.js has SHA-256 758d8fc473b700b5d351158d06ce9f9e92c16e8b25497b08bb7bd2122a87f5dd, matching your prefix. Its connection attempt includes both generation.connect(transport) and the initial tool sync. Thus a successful initialize followed by a failed tools/list can also enter the supervisor retry path. It logs the caught error separately as connection attempt failed: … before connection failed; retrying ….

    For the blocked run, the most useful next evidence is that first connection attempt failed line with its stack, the matching server traceback, whether initialize completed before it, and the effective failOnStartupError / reconnect settings. Please redact credentials and session identifiers. The counts of JSONDecodeError / ExceptionGroup alone cannot identify the failed request or establish why the UI queue stayed blocked. #5923 addresses the server error-code issue separately.

    AI-assisted investigation; no live DSH queue reproduction claimed.

  9. XHR666 commented on Oct 12, 2026

    @XHR666
    Author

    Correction to my own report. I ran the chain end-to-end and the causal claim in the title does not hold, so please treat this comment as superseding my earlier attribution.

    Chain (corrected)

    • DSH's MCP transport for OpenViking is stdio to a spawned proxy: @openviking/dsh-memory-plugin@0.5.20 builds { transport: "stdio", serverName: "openviking", command: process.execPath, args: [servers/mcp-proxy.mjs], env }.
    • That proxy is the "stdio → streamable-HTTP" bridge that posts to http://127.0.0.1:1933/mcp, so the probe reaches /mcp through the proxy.
    • Each connection attempt is therefore a fresh child spawn plus a fresh HTTP session — "one long-lived child PID" is not the shape here.
    • No overrides are passed: no reconnect, no probe, no failOnStartupError. Attempts run on dsh-mcp-client defaults (initialDelayMs 500, maxDelayMs 30000, maxAttempts 10), which is exactly what our attempt N/10 lines show.
    • The ProtocolError: OpenViking MCP request failed. Check the configured URL … line our harness logged originates in that proxy, not in the client SDK.

    Measured

    Method: the SDK DSH actually loads (@modelcontextprotocol/client@2.0.0, versionNegotiation: { mode: "auto" }, same Client construction as dsh-mcp-client) driving the real proxy; wire order observed at the endpoint was server/discover → initialize → notifications/initialized → tools/list in all three runs.

    • Synthetic endpoint answering -32602 (mimics current behavior): 1 attempt, connect() succeeded, fixture tool returned.
    • Synthetic endpoint answering -32601: 1 attempt, connect() succeeded, fixture tool returned.
    • Live OpenViking, which still answers -32602 for server/discover: 1 attempt, connect() succeeded, and tools/list returned 15 real tools.

    Conclusion: -32602 for server/discover does not by itself break the client or cause repeated probing. The fallback to the legacy initialize handshake runs either way, and this server answers that handshake correctly (protocolVersion: 2025-11-25, serverInfo: openviking/1.29.0).

    What did produce our retries: a startup race

    Our DSH host log, verbatim:
    17:29:14.522 mcp-client(openviking): connection attempt failed: ProtocolError: OpenViking MCP request failed. … 17:29:14.539 mcp-client(openviking): connection failed; retrying in 500ms (attempt 1/10) 17:29:15.358 mcp-client(openviking): connection attempt failed: ProtocolError: … 17:29:15.370 mcp-client(openviking): connection failed; retrying in 1000ms (attempt 2/10) 17:29:16.679 mcp-client(openviking): connection attempt failed: ProtocolError: … 17:29:16.691 mcp-client(openviking): connection failed; retrying in 2000ms (attempt 3/10)

    • Three attempts, backoff 500 / 1000 / 2000 ms, and no giving up line — so attempt 4 (≈ 17:29:18.7) appears to have succeeded.
    • The OpenViking server's own log begins at 17:29:16 and its streamable session manager reports started at 17:29:17; the first server/discover reaches it right after.
    • That places DSH's first attempts roughly 2.5 s before the endpoint was serving — a plain startup-ordering race, not an error-code retry.
    • I also cannot corroborate "17×" from our logs (three attempts, then success). I am editing the title to drop both that figure and the causal claim.

    On the two evidence requests

    • Our harness persists structured log records with the message only, so there is no error.stack for me to post.
    • It is capturable with OPENVIKING_DEBUG=1 plus OPENVIKING_DEBUG_LOG=<path> (the proxy also honours OV_DEBUG_LOG). That is what I enabled for the runs above; I can post paired proxy/server traces if that is useful.
    • Whether initialize had completed before those earlier failures remains unanswered for that specific run, but it is no longer load-bearing given the race explanation.

    Server-side code

    The error code is still wrong and still worth fixing — tracked in #5923. Verified locally on the installed mcp 1.29.0: ClientRequestType exposes 17 routable methods and server/discover is not among them.

    AI-assisted investigation; measurements above are from this reporter's host, no server-side fix claimed.

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

    agent-pluginsAgent harness and plugin integrationsbugSomething isn't workingkernelCore server, runtime, storage, retrieval, SDK, and CLI.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions