Ask
121

mcp server lists 12 tools over stdio and 0 over streamable http, same code mcp-server

Same server, same registration code. Over stdio the client lists all 12 tools and they work. Behind an HTTP endpoint the client connects without an error and shows zero tools.

What I can see server side: the initialize request arrives and I return a result. Then nothing - no tools/list ever comes in. There is an nginx in front of it and the whole thing is behind a bearer token.

No error message anywhere on either side, which is what makes this hard. Where do people usually find this one?

8 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @onebag_ozzy · 4mo ago · 2 replies

    Take the client out of it and drive the endpoint by hand. Two requests tell you almost everything:

    curl -i -X POST https://host/mcp \
      -H 'content-type: application/json' \
      -H 'accept: application/json, text/event-stream' \
      -H 'authorization: Bearer ...' \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}'
    

    then the same with "method":"tools/list" and whatever session header came back on the first response.

    The three things that catch people, in the order I would check them:

    • the session id. The HTTP transport hands you one on initialize and expects it back on every subsequent request. If it is missing from your response, or the client cannot read it, the second request has nowhere to go and the client gives up quietly.
    • the proxy. nginx buffers responses by default, which breaks anything streamed. proxy_buffering off; and a long proxy_read_timeout on that location.
    • the accept header. The transport wants both content types accepted; a server that rejects one of them fails in a way that reads as "connected but empty".

    Stdio has none of these problems, which is exactly why it works and tells you nothing.

    112
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @printbed_gremlin · 4mo ago

      It was the session header and the proxy, both. nginx was stripping the custom header on the response, so the client never had a session to continue with. Adding it to the passthrough and turning off buffering fixed it in one deploy.

      41
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @duckweed_dev · 4mo ago

    One more: if auth is wrong you may be getting a 401 on the second request only, because some clients send the token on connect and not on subsequent calls, or send it in a header your proxy drops. Log every request that reaches the app with method and status. "tools/list never arrives" and "tools/list arrives and nginx answers it" look the same from inside your handler.

    38
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @adjunct_ora · 4mo ago

    If a browser-based client is involved, add CORS to that list and specifically the expose-headers part. A custom response header is invisible to browser JavaScript unless the server names it in Access-Control-Expose-Headers, so the request succeeds, the header is physically present in the response, and the client still cannot read the session id. Devtools will show you the header, which makes this one genuinely maddening to debug.

    84
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @listening_lane · 4mo ago · 2 replies

    Check where you register the tools relative to where you connect the transport. With stdio there is one long-lived server instance so registration order rarely bites you. With HTTP, if you construct a fresh server per session, anything registered on a module-level instance outside that factory is registered on an object no request will ever see. Ours listed zero tools for exactly that reason and the code looked completely correct.

    63
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @printbed_gremlin · 4mo ago

      Mine is a per-session factory but the registration is inside it, so that one was clean. Adding a log line that prints the tool count at the end of the factory anyway, because "looked completely correct" is doing real work in your sentence.

      26
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @torque_spec · 4mo ago

    Also try it against the inspector rather than your real client. Half the clients out there swallow protocol errors and render an empty tool list, which is how you end up with "no error anywhere". The inspector will show you the raw JSON-RPC in both directions and the problem is usually obvious within thirty seconds of seeing the actual bytes.

    47
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @pivot_pilot · 4mo ago

    Works over stdio, silent over HTTP, nginx in the middle. That sentence has been the setup for the same joke for twenty years.

    18
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report