Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/guide/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ ggcode handles these requests by routing them through the same `ask_user` intera

No configuration is needed — elicitation is enabled automatically when an interactive session is active.

## Subscription Streams (MCP 2026-07-28)

Protocol revision 2026-07-28 added correlated notification streams: a client may open a subscription with the `subscriptions/listen` request, and every notification the server sends on that stream carries a `_meta` field binding it to the subscription. This closes a long-standing ambiguity — when an agent talks to several MCP servers concurrently, a bare `notifications/tools/list_changed` cannot be attributed to a specific connection with certainty.

ggcode opens this stream automatically when a server connects, and self-detects protocol support:

- The listen request asks for the exact change set ggcode cares about (tool/prompt/resource list changes, resource subscriptions). The server replies with a `notifications/subscriptions/acknowledged` control message confirming the granted subset.
- If the server answers `-32601` (method not found), ggcode downgrades permanently for that connection — legacy servers never see a second `subscriptions/listen` call, and the classic uncorrelated notifications keep working exactly as before.
- Cancelling a subscription (client shutdown or transport teardown) follows the spec by sending `notifications/cancelled` with the listen request id.
- Per the protocol MUSTs, notifications with a subscription id for an unknown or unacknowledged subscription are dropped rather than forwarded, so a server bug cannot poison the client's cache invalidation logic.

**Transport support**: stdio (streaming) today; the HTTP transport only probes for the feature and treats unsupported responses as a graceful downgrade. WebSocket servers currently keep using uncorrelated notifications.

No configuration is needed — enablement is automatic and transparent, with fallback to the pre-2026-07-28 behavior on any server that predates the revision.

## Request Cancellation (Client-to-Server)

When ggcode gives up on an in-flight MCP request — the user interrupts a tool call, or a request exceeds its deadline — it sends the server a `notifications/cancelled` notification referencing the outstanding request id (MCP spec 2025-03-26). Spec-compliant servers stop processing the cancelled request and free associated resources instead of finishing orphaned work (a long-running database query, a partial file upload, etc.).
Expand Down
27 changes: 27 additions & 0 deletions internal/mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ type Client struct {
// entries for the List*/ReadResource calls (see cacheable.go). Zero value
// is usable, so struct-literal clients (tests) stay safe.
listingCache listingCache

// Subscription-stream registry (MCP 2026-07-28 subscriptions/listen,
// see subscriptions.go). subs maps the normalized listen-request ID JSON
// to its Subscription, used by routeSubscriptionNotification for ack
// correlation and MUST-gating. Guarded by subMu. modernSub holds the
// default list-change stream opened by enableModernSubscriptions;
// subListenState caches the server's protocol support so a legacy server
// never sees a second subscriptions/listen call.
subMu sync.Mutex
subs map[string]*Subscription
modernSub *Subscription
subListenState atomic.Int32
}

// negotiatedState returns the protocol version and server capabilities
Expand Down Expand Up @@ -710,6 +722,12 @@ func (c *Client) Abort() {
if c.notificationDone != nil {
close(c.notificationDone)
}

// End every open subscriptions/listen stream: the transport is gone,
// so no acknowledged notification or correlated traffic can arrive
// anymore (subscriptions.go). No notifications/cancelled is sent on
// this path — there is no connection left to deliver it on.
c.closeAllSubscriptions(fmt.Errorf("mcp[%s]: connection closed", c.name))
})
}

Expand Down Expand Up @@ -2788,6 +2806,15 @@ func (c *Client) processNotification(notif *Notification) {
c.handleElicitationComplete(notif.Params)
return
}
// MCP 2026-07-28 subscriptions/listen: consume subscription control
// traffic and gate listen-stream notifications per the protocol MUSTs
// (unknown/unacked subscription ⇒ drop). Valid correlated traffic falls
// through so cache invalidation and the user handler still fire — the
// legacy dispatch is unchanged, this only adds correlation. Mutex-only
// work, safe on the read loop.
if c.routeSubscriptionNotification(notif) {
return
}
// MCP 2026-07-28 CacheableResult (SEP-2549): drop cached listings/read
// results affected by change notifications BEFORE the handler runs, so a
// hot refresh (ListTools etc.) observes fresh data. Mutex-only work —
Expand Down
Loading
Loading