Skip to content

mcp: support custom notifications - #1146

Open
delaneyj wants to merge 8 commits into
modelcontextprotocol:mainfrom
delaneyj:custom-notifications
Open

mcp: support custom notifications#1146
delaneyj wants to merge 8 commits into
modelcontextprotocol:mainfrom
delaneyj:custom-notifications

Conversation

@delaneyj

@delaneyj delaneyj commented Aug 5, 2026

Copy link
Copy Markdown

Protocol extensions need to send and receive custom JSON-RPC notifications. The SDK session APIs currently support only standard notification methods.

This change adds:

  • SendNotification to ClientSession and ServerSession.
  • Generic AddReceivingCustomNotification registration for typed client handlers.
  • SendSubscriptionNotification for custom notifications on subscriptions/listen streams.
  • Automatic io.modelcontextprotocol/subscriptionId metadata on subscription notifications.
  • Carrier cancellation for Streamable HTTP listen streams without a separate cancellation notification.
  • Exact preservation of custom parameter objects.
  • Empty object encoding for nil parameters.

Sending middleware processes custom notifications before transmission. Typed receiving handlers use parameter structs that embed ParamsBase.

The tests cover:

  • Client-to-server custom notifications.
  • Server-to-client custom notifications.
  • Typed client-side receipt.
  • Exact wire method names.
  • Arbitrary parameter objects.
  • Nil parameters.
  • Subscription metadata.
  • Streamable HTTP unsubscribe cleanup and subsequent client requests.

Validation:

go test ./mcp -run '^(TestSendNotification|TestResourceSubscriptions_Streamable|TestResourceSubscriptions_InMemory)$'
go test ./...

This PR supersedes #844.

Fixes #745

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
delaneyj force-pushed the custom-notifications branch from 99154e6 to 6863d59 Compare August 5, 2026 14:37
@delaneyj
delaneyj marked this pull request as ready for review August 5, 2026 15:16
@delaneyj delaneyj changed the title WIP: mcp: support custom notifications mcp: support custom notifications Aug 6, 2026
@delaneyj

Copy link
Copy Markdown
Author

Hello? It's been nearly a month with no response or human interaction. Are PRs not wanted here?

@guglielmo-san

Copy link
Copy Markdown
Contributor

Hi @delaneyj, thank you for the patience, we will review the PR as soon as possible

@bencroker

Copy link
Copy Markdown

Thanks @guglielmo-san, looking forward to having this functionality.

Comment thread mcp/server.go
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)),
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
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.

Expose generic SendNotification on ServerSession for custom protocol extensions

3 participants