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:
parent
22d4988a16
commit
65ece59643
1 changed files with 442 additions and 4 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue