mcp: bidirectional custom methods, CustomMethod type, and extension registry#1031
mcp: bidirectional custom methods, CustomMethod type, and extension registry#1031sambhav wants to merge 1 commit into
Conversation
…egistry Builds on the custom JSON-RPC method support in modelcontextprotocol#956 in three ways: 1. **Bidirectionality** — the server can now send requests to the client, and the client can register handlers for them, mirroring the existing client→server direction: - AddServerSendingCustomMethod / ServerCallCustomMethod - AddClientReceivingCustomMethod - CustomMethod.RegisterServerSending / .ServerCall - CustomMethod.RegisterClientReceiving 2. **CustomMethod[P, R, T]** — a phantom-type wrapper that captures the method name and parameter/result types once at package level, so call sites never repeat generic arguments or method-name strings: var Method = mcp.NewCustomMethod[*Params, *Result]("acme/method") result, err := Method.Call(ctx, cs, &Params{...}) 3. **Extension registry** — a mechanism for libraries to auto-wire custom methods into every Server/Client without requiring callers to do any manual setup: - RegisterExtension (global, typically called from init) - ServerOptions.Extensions / ClientOptions.Extensions (per-instance) Global extensions are applied first; per-instance extensions after (last writer wins on name collision). The example is restructured to demonstrate the pattern: a latinext sub-package is the "extension author" (defines types, registers via init(), exports a Translate() helper); main.go is the "consumer" (just imports latinext, no generics or method-name strings visible). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
piyushbag
left a comment
There was a problem hiding this comment.
Looked through the extension registry and bidirectional custom-method wiring. The CustomMethod wrapper and latinext example make the #956 follow-up easy to follow.
Local check:
go test ./mcp/... -count=1
All green on my machine.
One non-blocking suggestion: TestCustomMethods only exercises client→server today. A small round-trip test for AddServerSendingCustomMethod + AddClientReceivingCustomMethod (or ServerCallCustomMethod) would lock in the new path.
|
@sambhav thank you for your contribution. |
gotcha, let me remove those bits. I think the rest of the syntactic sugar still helps though. |
This PR builds on the custom JSON-RPC method support added in #956, addressing the review comments I left there. It adds three related capabilities.
What's new
1. Bidirectional custom methods
#956 added client→server only. This PR adds the server→client direction:
Under the hood:
Servergains asendMethodsmap (parallel to its existingreceiveMethods);Clientgains areceiveMethodsmap (parallel to its existingsendMethods). The per-session dispatch functions (sendingMethodInfos/receivingMethodInfos) now read from these maps under the server/client mutex rather than returning the static global maps.2.
CustomMethod[P, R, T]— ergonomic type-safe wrapperExtension authors typically define a package-level var:
This captures the name and types once. Every registration and call site then uses the var rather than repeating generic type arguments or the method-name string:
The lower-level
Add*/Call*free functions remain exported for callers who don't want the wrapper.3. Extension registry
A library can now package a custom method as an extension — importers get everything wired automatically:
Two registration scopes:
RegisterExtension): process-wide, typically called frominit. Applied first.ServerOptions.Extensions/ClientOptions.Extensions): applied after global, so per-instance wins on collision. Useful for tests or programs that instantiate multiple servers with different behaviors.Example restructure
The
examples/server/custom-methodexample is rewritten to demonstrate the pattern:latinext/— the extension-author package: types,Methodvar,init()registration,Translate()helpermain.go— the consumer: just importslatinext;NewServer/NewClientneed no manual setupDesign notes
CustomMethodis a phantom-type struct —P,R,Tappear only in the type parameters, not as stored fields. This is purely a compile-time ergonomics device.Extension.Server/Extension.Clientare independently nullable — an extension can be server-only or client-only.NewServer/NewClientpanic, matching the pattern established byAddReceivingMiddlewareand similar setup calls.runExtensionsis a generic helper shared between the global-registry path and the per-instance path to avoid duplicating the nil-check + call + panic loop.cc @guglielmo-san — tagging you as the author of #956 and SDK maintainer.
Closes the review comments from #956.