experimental-ext-interceptors/go/sdk/interceptors/integrations/gomiddleware/middleware.go
Ukjae Jeong 8704137635
Go SDK: Upgrade MCP SDK and remove vendored copy (#35)
<!-- Provide a brief summary of your changes -->
Update the Go interceptor SDK to use
`github.com/modelcontextprotocol/go-sdk` v1.7.0 custom method APIs and
remove the vendored Go SDK copy. This shouldn't update the behaviours,
but addding following functions.

Changed public/user-facing interfaces:
extension.Extension.Install now returns an error:

```
// before
ext.Install(mcpServer)

// after
if err := ext.Install(mcpServer); err != nil {
	return err
}
```

New helper added:

```
extension.RegisterSendingMethods(client *mcp.Client) error
```

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

Pick up MCP Go SDK 1.7.0. This provides upstream custom method support,
so the local vendored SDK copy is no longer needed.

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

- `go test -v ./...`

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

```go
if err := ext.Install(mcpServer); err != nil {
    return err
}
```

The Go SDK module now requires Go 1.25 via
github.com/modelcontextprotocol/go-sdk v1.7.0.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] 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
-->
LocalChain now registers interceptor custom sending methods internally.
Manual remote chain setup should call
`extension.RegisterSendingMethods(client)` before connecting the MCP
client session used with chain.AddMCPServer.

Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
2026-08-12 14:48:17 -07:00

176 lines
5.2 KiB
Go

// Copyright 2025 The MCP Interceptors Authors. All rights reserved.
// Use of this source code is governed by an Apache-2.0
// license that can be found in the LICENSE file.
package gomiddleware
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/modelcontextprotocol/ext-interceptors/go/sdk/interceptors"
"github.com/modelcontextprotocol/ext-interceptors/go/sdk/interceptors/chain"
)
// Option configures the middleware.
type Option func(*config)
type config struct {
logger *slog.Logger
contextProvider ContextProviderFunc
}
// ContextProviderFunc extracts an InvocationContext from an incoming MCP
// request. This is called once per intercepted request and the result is
// passed to all interceptor handlers via the chain execution params.
//
// Typical use: extract principal identity from OAuth tokens available on
// the request's session (via RequestExtra.TokenInfo).
type ContextProviderFunc func(ctx context.Context, req mcp.Request) *interceptors.InvocationContext
// WithLogger sets the logger for the middleware.
// If not set, slog.Default() is used.
func WithLogger(l *slog.Logger) Option {
return func(c *config) {
c.logger = l
}
}
// WithContextProvider sets a function that populates InvocationContext
// for every intercepted request. Without this, the context is nil.
func WithContextProvider(f ContextProviderFunc) Option {
return func(c *config) {
c.contextProvider = f
}
}
// skipMethods are methods that should not be intercepted to prevent
// recursion when the chain connects back to the same server, plus
// lifecycle methods that don't need interception.
var skipMethods = map[string]bool{
"initialize": true,
"notifications/initialized": true,
"notifications/cancelled": true,
interceptors.MethodList: true,
interceptors.MethodInvoke: true,
}
// Middleware creates an [mcp.Middleware] that intercepts incoming requests
// and outgoing responses, running the interceptor chain for each.
//
// The middleware calls chain.Execute() which invokes interceptors via
// interceptor/invoke RPCs. Install it on an [mcp.Server] via
// [mcp.Server.AddReceivingMiddleware]:
//
// chain, _ := ext.LocalChain(ctx, mcpServer)
// mcpServer.AddReceivingMiddleware(
// gomiddleware.Middleware(chain, gomiddleware.WithLogger(logger)),
// )
func Middleware(ch *chain.Chain, opts ...Option) mcp.Middleware {
cfg := &config{}
for _, opt := range opts {
opt(cfg)
}
if cfg.logger == nil {
cfg.logger = slog.Default()
}
return func(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
// Skip interceptor and lifecycle methods to prevent recursion.
if skipMethods[method] {
return next(ctx, method, req)
}
event := method
// Extract invocation context from the request using the context provider, if set.
var invCtx *interceptors.InvocationContext
if cfg.contextProvider != nil {
invCtx = cfg.contextProvider(ctx, req)
}
// --- Request phase ---
reqPayload, err := json.Marshal(req.GetParams())
if err != nil {
return nil, err
}
reqResult, err := ch.Execute(ctx, &chain.ExecutionParams{
Event: event,
Phase: interceptors.PhaseRequest,
Payload: reqPayload,
Context: invCtx,
})
if err != nil {
return nil, err
}
if len(reqResult.AbortedAt) > 0 {
logAborts(cfg.logger, "request", event, reqResult.AbortedAt)
return nil, abortToJSONRPCError(reqResult.AbortedAt)
}
// Apply mutated payload back to the request params if modified.
if reqResult.FinalPayload != nil {
if err := json.Unmarshal(reqResult.FinalPayload, req.GetParams()); err != nil {
return nil, fmt.Errorf("interceptors: failed to apply mutated request payload: %w", err)
}
}
// Call the next handler.
result, err := next(ctx, method, req)
if err != nil {
return result, err
}
// Notification methods return nil result — skip response phase.
if result == nil {
return nil, nil
}
// --- Response phase ---
respPayload, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("interceptors: failed to marshal response payload for interception: %w", err)
}
respResult, err := ch.Execute(ctx, &chain.ExecutionParams{
Event: event,
Phase: interceptors.PhaseResponse,
Payload: respPayload,
Context: invCtx,
})
if err != nil {
return nil, err
}
if len(respResult.AbortedAt) > 0 {
logAborts(cfg.logger, "response", event, respResult.AbortedAt)
return nil, abortToJSONRPCError(respResult.AbortedAt)
}
// Apply mutated response payload if it was modified.
if respResult.FinalPayload != nil {
if err := json.Unmarshal(respResult.FinalPayload, result); err != nil {
return nil, fmt.Errorf("interceptors: failed to apply mutated response payload: %w", err)
}
}
return result, nil
}
}
}
// logAborts logs each abort entry at warn level.
func logAborts(logger *slog.Logger, phase, event string, aborts []chain.AbortInfo) {
for _, a := range aborts {
logger.Warn("interceptors: "+phase+" aborted",
"event", event,
"interceptor", a.Interceptor,
"reason", a.Reason,
)
}
}