mcp: support custom notifications - #1146
Open
delaneyj wants to merge 8 commits into
Open
Conversation
Protocol extensions can define custom JSON-RPC notifications, but the SDK only exposes helpers for standard notifications. Add SendNotification to client and server sessions. Route custom notifications through sending middleware and preserve arbitrary parameters. Fixes modelcontextprotocol#745.
delaneyj
force-pushed
the
custom-notifications
branch
from
August 5, 2026 14:37
99154e6 to
6863d59
Compare
delaneyj
marked this pull request as ready for review
August 5, 2026 15:16
Author
|
Hello? It's been nearly a month with no response or human interaction. Are PRs not wanted here? |
Contributor
|
Hi @delaneyj, thank you for the patience, we will review the PR as soon as possible |
|
Thanks @guglielmo-san, looking forward to having this functionality. |
Comment on lines
+1479
to
+1492
| func (ss *ServerSession) SendSubscriptionNotification(ctx context.Context, method string, params any) error { | ||
| requestID, ok := ctx.Value(idContextKey{}).(jsonrpc.ID) | ||
| if !ok || !requestID.IsValid() { | ||
| return fmt.Errorf("mcp: SendSubscriptionNotification: context has no subscription ID") | ||
| } | ||
| customParams := &customNotificationParams{payload: params} | ||
| injectMetaSubscriptionID(customParams, requestID) | ||
| return handleNotify( | ||
| ctx, | ||
| "x-notifications/"+method, | ||
| newServerRequest(ss, Params(customParams)), | ||
| ) | ||
| } | ||
|
|
Contributor
There was a problem hiding this comment.
The listen stream is a closed opt-in set
From schema/2026-07-28/schema.ts (https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.ts), SubscriptionFilter (L1270):
Each notification type is opt-in; the server MUST NOT send notification types the client has not explicitly requested here.
Also it is not clear how this would send a notification using the subscriptions/listen stream
thegrumpylion
added a commit
to thegrumpylion/go-sdk
that referenced
this pull request
Sep 1, 2026
Fork-only patch (rebased onto upstream main until upstream lands the feature; consumed via go.mod replace). Motivating consumer: Claude Code channels (notifications/claude/channel push, permission relay receive). Send side matches upstream PR modelcontextprotocol#1146 near-verbatim: SendNotification on ServerSession and ClientSession routes through sending middleware under an internal x-notifications/ method prefix, stripped before the wire write; params marshal verbatim (object content preserved, canonical key order), nil encodes as {}. Beyond the PR: method validation (empty, reserved prefix, standard-method shadowing, custom-method collision), a _meta merge where payload keys win, and the routing branch placed after the registered-method lookup so a custom method named with the prefix keeps call semantics. Receive side adds AddServerReceivingCustomNotification, mirroring the PR's client-side signature; the unqualified name is left free for upstream's client function. Spec-first: canonical=modelcontextprotocol#745 converged design + Claude Code channels reference; contract=middleware-traversing sends, caller's method verbatim on wire, params object verbatim; mechanism=x-notifications/ internal prefix + verbatim params wrapper; collapse-check=faithful. Invariant: kind=clause-explicit; property=params serialize exactly as given ({content, meta} literal keys); from=channels wire contract; violation=wrapped or reshaped params make Claude Code drop the event. Invariant: kind=clause-explicit; property=internal prefix never on wire; from=modelcontextprotocol#745 design; violation=peer receives unknown x-notifications/* method and drops it. Invariant: kind=entailed; property=custom notifications traverse send/receive middleware; from=entailed: accounting/scrubbing middleware must see all traffic; violation=middleware silently misses custom notifications. Diagnosis: fault=params-less custom notification panics handler; proximate=wrappers passed req.Params through unconditionally; root=missingParamsOK admits absent params but custom dispatch materialized no zero value; fixing at=registration wrappers; wider scope justified by=same mechanism demonstrated at HEAD in AddReceivingCustomMethod, left failing by the narrower fix. Diagnosis: fault=concurrent map read/write between live registration and dispatch (pre-existing in AddReceivingCustomMethod / AddSendingCustomMethod); proximate=in-place write to a map value handed out for unlocked reads; root=published map doubled as mutable state; fixing at=clone-on-write in all three registration functions; wider scope justified by=n/a - narrowest sufficient. Structural-check: invariant=readers never observe a mutating map; illegal-state=in-place write to published map; representable-because= redundant (live map doubled as snapshot); decision=collapse to immutable snapshots via clone-on-write. Consolidation: scanned=custom registration, send dispatch, params marshalling; candidates=unified kind-parameterized registration helper (surfaced, user decision), params-wrapper dedup (surfaced, user decision), map-snapshot collapse (folded here). Adversarial loop: 4 rounds, converged (zero new findings, all dispositioned). 17 hand mutations killed via gomutant ephemeral; one attested equivalent: reverting clone-on-write is indistinguishable under a single-threaded oracle and race-enabled tests are barred by project policy - correctness rests on the published-snapshot mechanism. Disputed and accepted by reviewer: replace-on-reregister semantics (mirrors modelcontextprotocol#956/modelcontextprotocol#1146); non-object payload _meta passes through verbatim off the merge path; no docs/*.md (no precedent for modelcontextprotocol#956 either). Spec amendments (standing authority): middleware-set _meta merges with payload keys winning; middleware observes the prefixed method, never the wire; wire key order is canonical. Upstream-PR candidates carried by this fork: nil-params zero-materialization inside modelcontextprotocol#956's AddReceivingCustomMethod; send-side method validation beyond modelcontextprotocol#1146.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Protocol extensions need to send and receive custom JSON-RPC notifications. The SDK session APIs currently support only standard notification methods.
This change adds:
SendNotificationtoClientSessionandServerSession.AddReceivingCustomNotificationregistration for typed client handlers.SendSubscriptionNotificationfor custom notifications onsubscriptions/listenstreams.io.modelcontextprotocol/subscriptionIdmetadata on subscription notifications.Sending middleware processes custom notifications before transmission. Typed receiving handlers use parameter structs that embed
ParamsBase.The tests cover:
Validation:
This PR supersedes #844.
Fixes #745