mcp: Allow registration of custom JSON-RPC methods (#956)

## Description

This PR introduces support for custom (non-standard) JSON-RPC methods in
the MCP Go SDK.
This feature enables servers to handle arbitrary JSON-RPC methods and
clients to invoke them, all while maintaining full type safety and
integrating seamlessly with the SDK's existing middleware and session
management.

- Server-Side Registration: Added AddReceivingCustomMethod to allow
servers to register handlers for custom methods. Incoming requests are
automatically unmarshaled into user-defined parameter structs.
- Client-Side Calling: Added AddSendingCustomMethod which returns a
strongly-typed caller function that clients can use to invoke custom
methods and receive strongly-typed results.
- Helper Structs: Provided ParamsBase and ResultBase for users to embed
in their custom structs. This allows them to easily satisfy the SDK's
internal Params and Result interfaces.

Fixes #954
This commit is contained in:
Guglielmo Colombo 2026-06-25 23:13:17 +02:00 • committed by GitHub
commit 65ece59643
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,6 +10,8 @@ import (
"fmt"
"iter"
"log/slog"
"maps"
"reflect"
"slices"
"strings"
"sync"
@ -32,6 +34,11 @@ type Client struct {
sessions []*ClientSession
sendingMethodHandler_ MethodHandler
receivingMethodHandler_ MethodHandler
// sendMethods is the list of methods this client may send to a
// server: it always contains the standard server methods (from
// serverMethodInfos) plus any custom methods registered via
// [AddSendingCustomMethod].
sendMethods map[string]methodInfo
}
// NewClient creates a new [Client].
@ -58,12 +65,16 @@ func NewClient(impl *Implementation, options *ClientOptions) *Client {
opts.Logger = ensureLogger(nil)
}
sendMethods := make(map[string]methodInfo, len(serverMethodInfos))
maps.Copy(sendMethods, serverMethodInfos)
c := &Client{
impl: impl,
opts: opts,
roots: newFeatureSet(func(r *Root) string { return r.URI }),
sendingMethodHandler_: defaultSendingMethodHandler,
receivingMethodHandler_: defaultReceivingMethodHandler[*ClientSession],
sendMethods: sendMethods,
}
if opts.MultiRoundTrip == nil || !opts.MultiRoundTrip.Disabled {
c.AddSendingMiddleware(clientMultiRoundTripMiddleware())
@ -504,7 +515,7 @@ func injectRequestMeta[T any, P interface {
Params
}](cs *ClientSession, params P) P {
res := cs.state.InitializeResult
if params.isNil() {
if params == nil {
params = new(T)
}
m := params.GetMeta()
@ -1153,7 +1164,9 @@ var clientMethodInfos = map[string]methodInfo{
}
func (cs *ClientSession) sendingMethodInfos() map[string]methodInfo {
return serverMethodInfos
cs.client.mu.Lock()
defer cs.client.mu.Unlock()
return cs.client.sendMethods
}
func (cs *ClientSession) receivingMethodInfos() map[string]methodInfo {
@ -1597,3 +1610,67 @@ func paginate[P listParams, R listResult[T], T any](ctx context.Context, params
}
}
}
// AddSendingCustomMethod registers a custom JSON-RPC method
// that the client may send to the server.
//
// Registration is decoupled from invocation: extensions typically call this
// during setup, while the actual call site uses [CallCustomMethod].
//
// if err := mcp.AddSendingCustomMethod[*SearchParams, *SearchResult](c, "acme/search"); err != nil {
// return err
// }
// // ... later, anywhere a *ClientSession is available:
// result, err := mcp.CallCustomMethod[*SearchParams, *SearchResult](
// ctx, cs, "acme/search", &SearchParams{Query: "hello"})
//
// AddSendingCustomMethod returns an error if method is the name of a standard
// MCP method. Registering the same method twice replaces the previous
// registration.
func AddSendingCustomMethod[P paramsPtr[PT], R Result, PT any](
c *Client,
method string,
) error {
if _, ok := serverMethodInfos[method]; ok {
return fmt.Errorf("mcp: AddSendingCustomMethod: %q shadows a standard MCP method", method)
}
mi := methodInfo{
newResult: func() Result {
return reflect.New(reflect.TypeFor[R]().Elem()).Interface().(R)
},
}
c.mu.Lock()
defer c.mu.Unlock()
c.sendMethods[method] = mi
return nil
}
// CallCustomMethod sends a custom (non-standard) JSON-RPC method to the
// server and decodes the response into R.
//
// The method must have been registered on the session's client via
// [AddSendingCustomMethod].
func CallCustomMethod[P paramsPtr[PT], R Result, PT any](
ctx context.Context,
cs *ClientSession,
method string,
params P,
) (R, error) {
c := cs.client
c.mu.Lock()
_, ok := c.sendMethods[method]
c.mu.Unlock()
if !ok {
var zero R
return zero, fmt.Errorf("mcp: CallCustomMethod: %q is not registered; call AddSendingCustomMethod first", method)
}
if cs.usesNewProtocol() {
params = injectRequestMeta(cs, params)
}
return handleSend[R](ctx, method, &ClientRequest[P]{
Session: cs,
Params: params,
})
}