Route go interceptor methods through receiving middleware (#10)

<!-- Provide a brief summary of your changes -->

Route `interceptors/list` and `interceptor/invoke` through the receiving
middleware chain so that middleware installed (audit, etc.) fires for
interceptor protocol methods the same way it does for standard MCP
methods.

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

Before this change, custom JSON-RPC methods were dispatched via a
separate code path that bypassed receiving middleware entirely. This
meant any middleware an operator installed would silently not apply to
the interceptor protocol endpoints.

The fix adds `AddReceivingCustomMethod[P, R, T]` as a package-level
generic function to the vendored go-sdk/mcp package. Custom methods
registered through it are stored in a per-server customMethods map and
merged into `receivingMethodInfos()`, so `defaultReceivingMethodHandler`
dispatches them exactly like built-in methods. External types satisfy
the sealed `Params`/`Result` interfaces by embedding the
`ParamsBase`/`ResultBase` types exported from the mcp package.

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

Existing test suites and new ones for verifying middleware paths.

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

no.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

The changes to `vendor/go-sdk/mcp/server.go` are intentionally minimal
and carry a `NOTE` comment acknowledging they are additions to the
upstream SDK needed for this reference implementation.

---------

Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
This commit is contained in:
Ukjae Jeong 2026-04-30 17:14:55 +01:00 • committed by GitHub
commit 5bd7ab4d2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -54,27 +54,46 @@ type Server struct {
receivingMethodHandler_ MethodHandler
resourceSubscriptions map[string]map[*ServerSession]bool // uri -> session -> bool
pendingNotifications map[string]*time.Timer // notification name -> timer for pending notification send
customMethods map[string]CustomMethodHandler
customMethods map[string]methodInfo
}
// CustomMethodHandler handles a custom JSON-RPC method registered via
// [Server.AddCustomMethod]. It receives the raw JSON params and returns
// a result that will be marshaled to JSON.
type CustomMethodHandler func(ctx context.Context, ss *ServerSession, params json.RawMessage) (any, error)
// NOTE: ParamsBase, ResultBase, and AddReceivingCustomMethod are additions to
// the upstream go-sdk to route custom JSON-RPC methods through receiving
// handler calls like any built-in MCP method, as needed by the MCP interceptors
// extension (SEP). This is a reference implementation, so there's some
// inefficiency; receivingMethodInfos() merges maps on every request instead of
// caching.
// AddCustomMethod registers a custom JSON-RPC method on the server.
// Custom methods bypass the typed Params/Result pipeline and operate
// on raw JSON. They are dispatched before the standard method handlers.
//
// This is useful for extensions that define their own JSON-RPC methods
// (e.g. interceptors/list, interceptor/executeChain).
func (s *Server) AddCustomMethod(method string, handler CustomMethodHandler) {
// ParamsBase can be embedded by types outside this package to satisfy the
// mcp.Params interface, including its unexported marker method.
type ParamsBase struct {
Meta `json:"_meta,omitempty"`
}
func (x *ParamsBase) isParams() {}
func (x *ParamsBase) GetProgressToken() any { return getProgressToken(x) }
func (x *ParamsBase) SetProgressToken(t any) { setProgressToken(x, t) }
// ResultBase can be embedded by types outside this package to satisfy the
// mcp.Result interface, including its unexported marker method.
type ResultBase struct {
Meta `json:"_meta,omitempty"`
}
func (x *ResultBase) isResult() {}
// AddReceivingCustomMethod registers a typed custom JSON-RPC method that flows
// through receiving handler calls like any built-in MCP method. P must embed
// ParamsBase and R must embed ResultBase to satisfy the interface constraints.
// Go does not support generic methods, so this is a package-level function.
func AddReceivingCustomMethod[P paramsPtr[T], R Result, T any](
s *Server, method string,
handler typedServerMethodHandler[P, R],
) {
s.mu.Lock()
defer s.mu.Unlock()
if s.customMethods == nil {
s.customMethods = make(map[string]CustomMethodHandler)
}
s.customMethods[method] = handler
// missingParamsOK: custom methods may legally omit params (e.g. interceptors/list).
s.customMethods[method] = newServerMethodInfo(handler, missingParamsOK)
}
// hasCustomMethod reports whether the given method is registered as a custom method.
@ -85,17 +104,6 @@ func (s *Server) hasCustomMethod(method string) bool {
return ok
}
// callCustomMethod calls the custom method handler for the given method.
func (s *Server) callCustomMethod(ctx context.Context, ss *ServerSession, method string, params json.RawMessage) (any, error) {
s.mu.Lock()
h := s.customMethods[method]
s.mu.Unlock()
if h == nil {
return nil, fmt.Errorf("unknown custom method %q", method)
}
return h(ctx, ss, params)
}
// ServerOptions is used to configure behavior of the server.
type ServerOptions struct {
// Optional instructions for connected clients.
@ -235,6 +243,7 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
receivingMethodHandler_: defaultReceivingMethodHandler[*ServerSession],
resourceSubscriptions: make(map[string]map[*ServerSession]bool),
pendingNotifications: make(map[string]*time.Timer),
customMethods: make(map[string]methodInfo),
}
}
@ -1426,7 +1435,12 @@ func initializeMethodInfo() methodInfo {
func (ss *ServerSession) sendingMethodInfos() map[string]methodInfo { return clientMethodInfos }
func (ss *ServerSession) receivingMethodInfos() map[string]methodInfo { return serverMethodInfos }
func (ss *ServerSession) receivingMethodInfos() map[string]methodInfo {
infos := make(map[string]methodInfo)
maps.Copy(infos, serverMethodInfos)
maps.Copy(infos, ss.server.customMethods)
return infos
}
func (ss *ServerSession) sendingMethodHandler() MethodHandler {
s := ss.server
@ -1474,12 +1488,6 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
// server->client calls and notifications to the incoming request from which
// they originated. See [idContextKey] for details.
ctx = context.WithValue(ctx, idContextKey{}, req.ID)
// Dispatch custom methods before the standard typed pipeline.
if ss.server.hasCustomMethod(req.Method) {
return ss.server.callCustomMethod(ctx, ss, req.Method, req.Params)
}
return handleReceive(ctx, ss, req)
}