Skip to content

Streamable HTTP door serves a different tool surface when clientInfo.name is empty: intentional? #661

Description

@pbednarcik

Summary

The daemon's Streamable HTTP MCP endpoint appears to key the exposed tool surface on clientInfo.name from the initialize request. A client that sends an empty name ("clientInfo":{"name":"","version":""}) gets the flat legacy catalogue (55 tools: find_usages, search_symbols, index_health, get_repo_outline, ...). A client that sends any non-empty name gets the curated 21-tool facade (search, relations, workspace, ...). The two sessions are otherwise identical.

Is this fork intentional, for example as a compatibility path for older or unnamed clients? If so, it would help to have it documented and made deterministic across a session's lifetime, including reconnects. If not, one surface regardless of client identity would avoid the failure mode described below.

Environment

  • gortex daemon 0.63.8 (serverInfo.version from the initialize response)
  • Transport: Streamable HTTP (/mcp)
  • Observed identically on two independent daemons at this version

Reproduction

Two initialize requests that differ only in clientInfo.name, then tools/list on each session. Replace PORT with the daemon's HTTP port.

PORT=48654
URL=http://localhost:$PORT/mcp
HDR='Accept: application/json, text/event-stream'

probe() {
  local name="$1"
  local sid
  sid=$(curl -sS -D - -o /dev/null -X POST "$URL" \
    -H "Content-Type: application/json" -H "$HDR" \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"'"$name"'","version":""}}}' \
    | grep -i '^Mcp-Session-Id:' | tr -d '\r' | awk '{print $2}')
  curl -sS -o /dev/null -X POST "$URL" \
    -H "Content-Type: application/json" -H "$HDR" -H "Mcp-Session-Id: $sid" \
    -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
  curl -sS -X POST "$URL" \
    -H "Content-Type: application/json" -H "$HDR" -H "Mcp-Session-Id: $sid" \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
    | sed 's/^data: //' | grep '^{' \
    | python -c 'import json,sys; t=[x["name"] for x in json.load(sys.stdin)["result"]["tools"]]; print(len(t), sorted(t)[:8])'
}

probe ""      # empty clientInfo.name
probe "probe" # any non-empty name

The only differing byte between the two sessions is the value of clientInfo.name in the initialize params.

Observed vs expected

Observed (verified against 0.63.8):

  • name: "" session: tools/list returns 55 tools, the flat catalogue (find_usages, get_callers, search_symbols, search_text, index_health, graph_stats, get_repo_outline, overlay tools, ...).
  • name: "probe" session: tools/list returns 21 tools, the curated facade (analyze, ask, capabilities, change, edit, explore, overlay, pr, read, recall, refactor, relations, remember, review, search, session, trace, workspace, ...).

Expected: the same server endpoint serves the same tool surface regardless of the client's self-reported identity, or the selection rule is documented so clients can rely on it.

Why it matters

  1. mark3labs/mcp-go v0.58.0, a widely used Go MCP client library, sends "clientInfo":{"name":"","version":""} by default. There is no WithClientInfo client option; identity rides on InitializeParams.ClientInfo and defaults to the zero value. So an MCP client built on mark3labs/mcp-go defaults silently lands on the legacy surface and sees a different API than a named client (or than a client like Claude Code, which sends a name). Nothing in the responses indicates that another surface exists.

  2. The compound failure is worse. When the daemon expires an idle MCP session, mcp-go's automatic reconnect re-initializes without ClientInfo. A client that started on the 21-tool facade (because its author set a name at first initialize, or an intermediary did) falls to the flat surface mid conversation. From that point every call to a facade tool permanently fails with JSON-RPC error -32602:

    tool 'search' not found: tool not found
    

    To the client this looks like the server dropped half its API at a random moment. The actual cause (surface reselection during transparent reconnect) is very hard to diagnose from the client side because the reconnect is invisible to the application layer.

Suggested direction

Either of these would resolve it; the first is simpler for clients:

  1. Serve one tool surface regardless of client identity. If the flat catalogue must remain reachable for compatibility, select it via an explicit opt-in (a query parameter, header, or capability flag) rather than the absence of a client name.
  2. If identity-keyed surfaces are intentional, document the selection rule, and pin the surface to the session lineage: a reconnect or re-initialize that resumes or replaces an expired session should keep the surface the original initialize selected, even when the new initialize carries an empty clientInfo.name.

Happy to provide more traces or test against a patched build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions