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
-
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.
-
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:
- 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.
- 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.
Summary
The daemon's Streamable HTTP MCP endpoint appears to key the exposed tool surface on
clientInfo.namefrom theinitializerequest. 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
serverInfo.versionfrom theinitializeresponse)/mcp)Reproduction
Two
initializerequests that differ only inclientInfo.name, thentools/liston each session. ReplacePORTwith the daemon's HTTP port.The only differing byte between the two sessions is the value of
clientInfo.namein theinitializeparams.Observed vs expected
Observed (verified against 0.63.8):
name: ""session:tools/listreturns 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/listreturns 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
mark3labs/mcp-go v0.58.0, a widely used Go MCP client library, sends
"clientInfo":{"name":"","version":""}by default. There is noWithClientInfoclient option; identity rides onInitializeParams.ClientInfoand 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.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: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:
clientInfo.name.Happy to provide more traces or test against a patched build.