fix(mcp): stop duplicate tool calls and keep full tool metadata

Federated tools/call re-initializes a session only when the backend
signals a missing session: HTTP 400 or 404, or a JSON-RPC error that
names a session. A failing tool is no longer called twice.

tools/list now:
- passes each tool object through and rewrites only its name
- follows nextCursor up to a page cap and keeps the earlier pages if a
  later page fails
- skips tools with no name
- resolves name collisions to the server that tools/call routes to

The handshake sends notifications/initialized. tools/call forwards
_meta. Multi-line SSE data is joined. A2A target URLs are joined with
url.JoinPath and keep their query string. agents[].card must start with
"/". Logged target URLs and errors are scrubbed of credentials.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
This commit is contained in:
2026-09-23 09:44:32 +01:00
parent 4c01c75276
commit 4a70daa8b8
5 changed files with 1338 additions and 110 deletions
+8 -1
View File
@@ -160,9 +160,16 @@ func TestHandleMCPFederated_DefaultFraming_ByteIdenticalToBaseline(t *testing.T)
},
},
{
// F2, review round 3, 2026-09: each tool now round-trips
// through map[string]json.RawMessage (mcpToolsListResult's
// own doc comment) rather than the earlier fixed-field
// struct, so its own keys marshal back out in SORTED order —
// encoding/json's own documented behavior for a map — not
// the struct-declared "name" then "description" order the
// prior baseline pinned. Cosmetic only: content is identical.
name: "tools/list two-server aggregate, Accept: application/json",
accept: "application/json",
wantBody: `{"jsonrpc":"2.0","result":{"tools":[{"name":"alpha_lookup","description":"alpha's tool"},{"name":"beta_search","description":"beta's tool"}]},"id":42}` + "\n",
wantBody: `{"jsonrpc":"2.0","result":{"tools":[{"description":"alpha's tool","name":"alpha_lookup"},{"description":"beta's tool","name":"beta_search"}]},"id":42}` + "\n",
buildTest: func(t *testing.T) (http.Handler, *http.Request) {
alpha := newMockJSONRPCServer(t, []mcpTool{{Name: "lookup", Description: "alpha's tool"}})
beta := newMockJSONRPCServer(t, []mcpTool{{Name: "search", Description: "beta's tool"}})