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

@ -57,6 +57,11 @@ type Server struct {
resourceChangeSubscriptions map[*ServerSession]jsonrpc.ID // session -> requestID for "resources/changed"
resourceSubscriptions map[string]map[*ServerSession]jsonrpc.ID // uri -> session -> requestID
pendingNotifications map[string]*time.Timer // notification name -> timer for pending notification send
// receiveMethods is the merged map of methods this server may receive
// from a client: it always contains the standard server methods (from
// serverMethodInfos) plus any custom methods registered via
// [AddReceivingCustomMethod].
receiveMethods map[string]methodInfo
}
// ServerOptions is used to configure behavior of the server.
@ -203,6 +208,9 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
opts.Logger = ensureLogger(nil)
}
receiveMethods := make(map[string]methodInfo, len(serverMethodInfos))
maps.Copy(receiveMethods, serverMethodInfos)
s := &Server{
impl: impl,
opts: opts,
@ -217,6 +225,7 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
resourceChangeSubscriptions: make(map[*ServerSession]jsonrpc.ID),
resourceSubscriptions: make(map[string]map[*ServerSession]jsonrpc.ID),
pendingNotifications: make(map[string]*time.Timer),
receiveMethods: receiveMethods,
}
s.AddReceivingMiddleware(serverMultiRoundTripMiddleware())
return s
@ -1761,7 +1770,15 @@ func initializeMethodInfo() methodInfo {
func (ss *ServerSession) sendingMethodInfos() map[string]methodInfo { return clientMethodInfos }
func (ss *ServerSession) receivingMethodInfos() map[string]methodInfo { return serverMethodInfos }
func (s *Server) receivingMethodInfos() map[string]methodInfo {
s.mu.Lock()
defer s.mu.Unlock()
return s.receiveMethods
}
func (ss *ServerSession) receivingMethodInfos() map[string]methodInfo {
return ss.server.receivingMethodInfos()
}
func (ss *ServerSession) sendingMethodHandler() MethodHandler {
s := ss.server
@ -2016,3 +2033,55 @@ func paginateList[P listParams, R listResult[T], T any](fs *featureSet[T], pageS
*res.nextCursorPtr() = nextCursor
return res, nil
}
// AddReceivingCustomMethod registers a handler for a custom (non-standard)
// JSON-RPC method on the server.
//
// When a client sends a request with the given method name, the params will be
// unmarshaled into P, the handler will be called, and the returned R will be
// marshaled as the JSON-RPC result.
//
// Custom methods go through the server's middleware chain just like standard
// MCP methods (tools/call, prompts/list, etc.).
//
// P and R must implement [Params] and [Result] respectively, which is most
// easily done by embedding [ParamsBase] and [ResultBase]:
//
// type SearchParams struct {
// mcp.ParamsBase
// Query string `json:"query"`
// }
//
// type SearchResult struct {
// mcp.ResultBase
// Hits []string `json:"hits"`
// }
//
// if err := mcp.AddReceivingCustomMethod(server, "acme/search",
// func(ctx context.Context, ss *mcp.ServerSession, params *SearchParams) (*SearchResult, error) {
// return &SearchResult{Hits: []string{"result"}}, nil
// }); err != nil {
// return err
// }
//
// AddReceivingCustomMethod returns an error if method is the name of a
// standard MCP method. Registering the same custom method twice replaces the
// previous handler.
func AddReceivingCustomMethod[P paramsPtr[T], R Result, T any](
s *Server,
method string,
handler func(ctx context.Context, ss *ServerSession, params P) (R, error),
) error {
if _, ok := serverMethodInfos[method]; ok {
return fmt.Errorf("mcp: AddReceivingCustomMethod: %q shadows a standard MCP method", method)
}
typed := typedServerMethodHandler[P, R](func(ctx context.Context, req *ServerRequest[P]) (R, error) {
return handler(ctx, req.Session, req.Params)
})
s.mu.Lock()
defer s.mu.Unlock()
s.receiveMethods[method] = newServerMethodInfo(typed, missingParamsOK)
return nil
}