Skip to content

feat(mcp): session-scoped notification delivery#273

Merged
zzet merged 3 commits into
zzet:mainfrom
vitaliyslion:feat/mcp-session-notifications
Jul 19, 2026
Merged

feat(mcp): session-scoped notification delivery#273
zzet merged 3 commits into
zzet:mainfrom
vitaliyslion:feat/mcp-session-notifications

Conversation

@vitaliyslion

Copy link
Copy Markdown
Contributor

Summary

Daemon-socket MCP sessions (the transport used by gortex mcp proxying and bridge-style clients like Pi) were never registered with mcp-go's internal client-session tracking, so any server-initiated notification — tools/list_changed after a tools_search promotion, resources/updated, etc. — was silently dropped for them. This PR wires those sessions in so pushes actually reach the client, and adds a capability-based mechanism for clients to self-declare their compact wire-format support instead of relying solely on a server-side client-name allowlist.

Changes

  • Added SessionStartedHook (internal/daemon/server.go): an optional hook MCPDispatcher implementations can satisfy to receive a write func([]byte) error callback per connection, invoked after the handshake ack and before the first frame is dispatched. Server-initiated frames now share the connection under one write lock so they never interleave with reply frames.
  • Added ConnectSession / DisconnectSession / WithClientSession (internal/mcp/conn_session.go, new): registers a daemon-socket connection as a live mcp-go client session so SendNotificationToAllClients (which mcp-go gates on session-initialized state) can actually deliver to it.
  • cmd/gortex/daemon_mcp.go: mcpDispatcher now implements SessionStarted/SessionEnded to connect/disconnect the session, and attaches the client session to the request context via WithClientSession before dispatch.
  • Added a gortex/wire MCP capability: a client can declare its supported compact formats during initialize (capabilities["gortex/wire"]: ["gcx", "toon"], in preference order). maybeSnoopInitialize parses it and calls the new Server.NoteSessionWireFormats; resolveSessionFormat prefers this declaration over the hardcoded defaultFormatForClient name allowlist, so new clients get compact output without a server-side code change.
  • internal/mcp/server.go: added sessionState.wireFormats + recordWireFormats/snapshotWireFormats, NoteSessionWireFormats, and formatFromWireCapability (maps the declared list to the first format this server can emit, skipping unknown/future format names).

Testing

  • All tests pass (go test -race ./internal/mcp/... ./internal/daemon/... ./cmd/gortex/...)
  • New tests added for new functionality (internal/mcp/conn_session_test.go, additions to internal/mcp/format_negotiation_test.go and internal/daemon/mcp_roundtrip_test.go)
  • Benchmarks run if performance-relevant

Checklist

  • Code follows existing patterns in the codebase
  • No unnecessary abstractions added
  • Language extractor includes Meta["methods"] for interfaces (if applicable)
  • Methods have EdgeMemberOf edges to their containing type (if applicable)

@vitaliyslion

Copy link
Copy Markdown
Contributor Author

On the gortex/wire capability: this PR adds it purely additively — defaultFormatForClient's hardcoded allowlist (claude-code, cursor, vscode, zed, aider, kilocode, opencode, openclaw, codex, omp-coding-agent) still stands as the fallback, and resolveSessionFormat only prefers a client's self-declared capabilities["gortex/wire"] over it when present.

Once this architecture is approved, the follow-up is to flip the model around: have each of the currently-allowlisted clients declare gortex/wire: ["gcx", ...] themselves during initialize (their bridge/plugin already knows what it can decode), and then delete knownAgentClients / defaultFormatForClient from the server entirely. That removes the need for a server-side release every time a new client wants compact output — the allowlist becomes a distributed fact each client owns, not a table Gortex maintains on their behalf.

Pi's own bridge (next PR) will be the first client built against this from day one — self-declaring instead of getting a server-side entry — so it'll double as the reference implementation for migrating the rest.

@zzet

zzet commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Hey @vitaliyslion,

Once this architecture is approved

The capability placement is hard to change later, and it's a one-door decision. According to the change, the client declares capabilities["gortex/wire"] as a top-level key under capabilities. That's non-standard MCP: custom capabilities belong under capabilities.experimental, and typed SDKs can't even emit a custom top-level key — mcp-go's own ClientCapabilities struct only has experimental/roots/sampling. So the exact clients this feature targets ("new clients without a server-side change") can't use it unless they hand-roll their initialize JSON.
I'd accept capabilities.experimental["gortex/wire"] as well (or instead) before this ships and third parties start depending on the current shape.

Also, such a change requires a documentation actualisation for a public protocol extension. docs/mcp.md:70 documents the client-name allowlist, but nothing anywhere mentions gortex/wire — and the entire point of the feature is that external client authors adopt it. At minimum, a paragraph in docs/mcp.md next to the allowlist description.

@vitaliyslion

Copy link
Copy Markdown
Contributor Author

Hey @zzet,

You're right on both — and I should've understood MCP capability negotiation better before opening this; that's on me. experimental being the only open-ended bucket for custom keys is obvious in hindsight.

Rather than fix the placement, I pulled gortex/wire out entirely (1ad62f06). Committing to a public capability shape for one prospective client (Pi) was premature — exactly the one-door risk you flagged. Pi gets gcx via the existing knownAgentClients allowlist instead. If self-declaration is worth it later, it deserves its own PR once there's more than one client asking for it.

Also rebased onto current main and resolved the conflicts in daemon_mcp.go / server.go — the actual point of this PR (session-scoped notification delivery) is untouched and still covered by the existing tests.

CI's flagging TestGatherExploreSourceLiteralRecallMapsParsedCSharpConstructor as failed, but it passed locally every time I tried (30 runs, including with -race) and isn't touched by this diff — looks flaky, not related to these changes.

@zzet
zzet merged commit 68a953d into zzet:main Jul 19, 2026
20 of 22 checks passed
@zzet

zzet commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Thank you @vitaliyslion for the fix.

Regarding the GCX - it's still on the table and makes sense to discuss it. We can move it to Discussions in the repository or on the Discord server

@zzet zzet changed the title feat(mcp): session-scoped notification delivery and wire-format negotiation feat(mcp): session-scoped notification delivery Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants