Skip to content

Expose generic SendNotification on ServerSession for custom protocol extensions #745

Description

@vaijab

Use Case

I am implementing an MCP Server for the Gemini CLI IDE Integration. The specification requires the server to send custom notifications to the client to signal specific events.

Specifically, I need to send:

  • ide/diffAccepted
  • ide/diffRejected
  • ide/contextUpdate

The Problem

Currently, the mcp.ServerSession struct only exposes specific notification helpers (like NotifyProgress and Log). There is no public API to send a generic/custom JSON-RPC notification.

I attempted to use session.Notify(ctx, method, params), but:

  1. The conn field on ServerSession is unexported.
  2. The mcp.Connection interface's Write method requires *mcp.JSONRPCMessage, which is an unexported struct.

Proposed Solution

Please expose a generic notification method on ServerSession, similar to:

// In mcp/server.go

func (s *ServerSession) SendNotification(ctx context.Context, method string, params any) error {
    return s.conn.Notify(ctx, method, params)
}

Current Workaround

I am currently forced to use unsafe and reflect to access the underlying conn field to call its Notify method, which is fragile for obvious reasons.

Activity

  1. jba commented on Jan 10, 2026

    @jba
    Contributor

    While the MCP spec does not explicitly allow custom notifications, it doesn't explicitly exclude them either.

    I confirmed by reading the linked doc that the IDE plugin must talk to the IDE over an HTTP connection using MCP, and that it does specify some custom notifications.

    So I recommend we do this.

  2. jba commented on Jan 10, 2026

    @jba
    Contributor

    Some issues that make this more complicated than the simple one-liner in the top post:

    • All other notifications go through sending middleware. These should too.
    • The sending mechanism recognizes a notification because its method starts with "notifications/". These do not.
    • A server may support many sessions. Presumbably the notification should be sent to them all. (Even if there's ever only one for the IDE; we need to think more generally.)
  3. vaijab commented on Jan 10, 2026

    @vaijab
    Author

    @jba thanks for looking into this. Shall I raise a PR with the changes or you prefer that maintainers do it?

  4. jba commented on Jan 10, 2026

    @jba
    Contributor

    A PR would be great. I don't quite see how to do it myself, frankly.

  5. jba commented on Jan 10, 2026

    @jba
    Contributor

    Let's discuss the design here first. We should start with how we're going to recognize these custom notifications. I'd rather not have a registration mechanism. Maybe we can design a convention.

  6. findleyr commented on Jan 10, 2026

    @findleyr
    Contributor

    @jba I think we can just expose a method to send an arbitrary notification. No registry needed.

  7. jba commented on Jan 11, 2026

    @jba
    Contributor

    @findleyr It should go through the middleware, which means it will eventually get to https://github.com/modelcontextprotocol/go-sdk/blob/main/mcp/shared.go#L91, which expects all methods to be known and recognizes notifications by method name.

    I don't think we should skip the middleware. It may be performing accounting, logging, PII scrubbing, and other operations that should apply to all outgoing messages.

    One simple idea: if method begins with the segment x-notifications, that segment is removed and the remainder is sent as a notification. We would skip everything else in the function until L107.

  8. added a commit that references this issue on Apr 19, 2026
    6c3941f
  9. mikeschinkel commented on Apr 27, 2026

    @mikeschinkel

    Design sketch: custom notifications, send + receive

    Per @jba's request on #898, moving the discussion here. This builds on the constraints @jba named:

    • Per-session only (no broadcast), and
    • Full integration with middleware pipeline.

    Goal

    Allow MCP server and client to send and receive custom JSON-RPC notification methods in both directions, e.g. notifications/claude/channel, ide/diffAccepted:

    1. Routed through AddSendingMiddleware and AddReceivingMiddleware
    2. Symmetric across Client and Server
    3. Per-session in scope

    Send side

    ##844 implements send side via the x-notifications/ prefix convention @jba proposed above. ServerSession.SendNotification(ctx, method, params) and ClientSession.SendNotification(ctx, method, params) wrap the payload, prepend
    x-notifications/ internally so the method-validity check in defaultSendingMethodHandler accepts it, and strip the prefix before writing to the wire.

    Thus I propose we simply adopt the changes provided in #844 for send.

    Receive side

    Using x-notifications/ will not work on receive: the wire delivers whatever method the peer sent, and there is no validity check to bypass, just an unknown-method drop. We need a registration API for handlers, analogous to AddTool().

    Primary API: typed per-method registration

    // On the server.
    mcp.AddNotificationHandler[Params any](
        s *mcp.Server,
        method string,
        handler func(ctx context.Context, ss *mcp.ServerSession, p *Params) error,
    )
    
    // Symmetric on the client.
    mcp.AddNotificationHandler[Params any](
        c *mcp.Client,
        method string,
        handler func(ctx context.Context, cs *mcp.ClientSession, p *Params) error,
    )

    Behavior:

    1. Registration happens before Server.Connect() / Client.Connect(), like AddTool().
    2. Inbound dispatch checks the spec method registry first, then falls through to the custom registry.
    3. The handler runs through AddReceivingMiddleware exactly like a spec method handler.
    4. Params are unmarshalled into *Params automatically.

    Optional secondary API: catch-all handler

    For protocol bridges or dynamic-method use cases:

    type NotificationHandler func(
    	ctx context.Context, 
    	ss *mcp.ServerSession, 
    	method string, 
    	raw json.RawMessage) error
    
    // On ServerOptions and ClientOptions.
    NotificationHandler NotificationHandler

    If both a typed per-method handler and a catch-all are registered, the typed one wins for that method. The catch-all sees only methods with no typed registration. The catch-all also runs through receiving middleware.

    I would prefer to only implement the typed primary API and add the catch-all later, if a real use case appears, but I would be happy to implement both if required.

    Internals

    1. New field on Server and Client: customNotificationHandlers map[string]methodInfo, keyed by raw method name.
    2. The methodInfo for each entry: newRequest constructs a typed *Params, dispatch calls the user handler.
    3. In the inbound dispatch path, check spec methodInfos first, then the custom registry, then the catch-all if set, then drop with the existing unknown-method behavior.
    4. All paths invoke AddReceivingMiddleware consistently, so accounting, logging, and PII scrubbing apply uniformly.

    Decisions assumed

    1. Per-session, no broadcast. Confirmed by @jba on feat(mcp): add ServerSession.Notify for custom notification methods #898.
    2. Goes through middleware in both directions. Per @jba on feat(mcp): add ServerSession.Notify for custom notification methods #898.
    3. Symmetric across Client and Server. Same as the send side in feat: implement generic SendNotification method for Issue #745 #844.

    Open questions

    1. Registration timing. Pre-Connect only (like AddTool()), or also runtime registration on a live session? Pre-Connect is simpler; runtime supports plugin-style use cases.
    2. Typed vs raw on the primary API. The above assumes typed via generics, matching AddTool's shape. Alternative is func(ctx, ss, json.RawMessage) error with user doing unmarshalling. Preference?
    3. Method-name validation on registration. Accept any string, or require vendor-prefix such as {vendor}/... or notifications/{vendor}/...? The spec does not require either.
    4. Symmetry of the x- prefix. Is the prefix purely a send-side internal artifact, or do you want it on receive too, e.g. as a registration-time convention?

    Motivating use cases

    1. Claude Code channels ([my
      use-case](https://github.com/mikeschinkel/endless/blob/0b41d35bc2112129cc4bb28a55ccd
      50439786d4e/cmd/endless-channel/main.go#L125-L128)): Server pushes notifications/claude/channel to the client. Send direction only in the current implementation.
    2. Gemini IDE companion (per Expose generic SendNotification on ServerSession for custom protocol extensions #745 origin): IDE sends ide/diffAccepted, ide/diffRejected, and ide/contextUpdate notifications that need to be received.

    Claude Channels needs send; Gemini's IDE companion needs receive; a use case for
    each direction.

    Plan

    If this design is acceptable:

    1. Open a new PR with cherry-picked changes from feat: implement generic SendNotification method for Issue #745 #844 to add the receive registration API and dispatch wiring.
    2. Add tests for typed per-method receive, catch-all receive, middleware integration in both directions, and send/receive symmetry on Client and Server.
    3. Address review feedback until acceptance and merge.

    Looking forward to input on my characterization of requirements, my assumptions, and the open questions.

  10. jba commented on Apr 27, 2026

    @jba
    Contributor

    Nice.

    1. Registration timing: Why must you add tools before you connect? In fact the spec has a specific feature, change notifications, to support that.

    2. Typed vs. raw API: I think typed is fine for now.

    3. Accept any string, but fail if it matches an existing method (including a previously registered custom one). That will mean, by the way, that the order in which registrations are checked won't matter.

    4. The x- prefix should not be visible to the user.

    The NotificationHandler must take the request object so it can access meta and possibly other fields, like ToolHandlerFor.

  11. maciej-kisiel commented on Apr 27, 2026

    @maciej-kisiel
    Contributor

    I will let Jonathan do the main review, since he already engaged in the topic, but a few thoughts from my side:

    Re registration timing: I believe currently all notification handlers are specified in the ServerOptions (or ClientOptions) which are passed to Server/Client constructors. These options are not copied, so I assume they could be swapped while the Server/Client is already running, but the access is not synchronized so it could cause some data races.

    If a generic solution works well, we could consider replacing all statically typed handlers with it in the next major version, simplifying the API.

    +1 to keeping x- prefix internal and failing on existing method names (+ also validating normal JSON RPC method name requirements, if there are any).

  12. mikeschinkel commented on Apr 28, 2026

    @mikeschinkel

    Thank you @jba and @maciej-kisiel.

    Acknowledging your requests, including mirroring ToolHandlerFor's shape and reusing the existing ServerRequest[P] / ClientRequest[P] types.

    Decisions resolved

    These are what I assume have been resolved; please let me know otherwise:

    1. Per-session, no broadcast (feat(mcp): add ServerSession.Notify for custom notification methods #898).
    2. Goes through middleware in both directions (feat(mcp): add ServerSession.Notify for custom notification methods #898).
    3. Symmetric across Client and Server (per feat: implement generic SendNotification method for Issue #745 #844 send side).
    4. Typed generic API.
    5. x- prefix internal and invisible to users.
    6. Method names: any string, fail-on-conflict, reject rpc. prefix. Single flat namespace; dispatch order is irrelevant.
    7. Handler takes a request object, mirroring ToolHandlerFor (reusing ServerRequest[P] / ClientRequest[P]).
    8. Dynamic registration with Server.mu / Client.mu protection, modeled on AddTool().

    Open question

    1. Should Server.RemoveNotificationHandler(method) error (and the client equivalent) be in scope for this PR, or scoped to a follow-up? Dynamic registration implies the need for that but you did not explicitly request it.

    Next step

    Unless you have other concerns to address, I can create a PR once the open question is answered.

  13. jba commented on Apr 28, 2026

    @jba
    Contributor

    LGTM.
    Let's add removal. Following RemoveTools, the signature should be

    func ([*Server|*Client]) RemoveNotificationHandlers(methods ...string)
    

    There is no error return.

  14. dominic1988-lgtm commented on May 10, 2026

    @dominic1988-lgtm

    Looking forward to this function avaliable

  15. added 2 commits that reference this issue on Aug 5, 2026
    99154e6
    6863d59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs investigationStatus unclear, requires more work and discussion

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions