Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 33 additions & 15 deletions mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,21 +515,26 @@ func (cs *ClientSession) usesNewProtocol() bool {
return res != nil && res.ProtocolVersion >= protocolVersion20260728
}

// injectRequestMeta populates the SEP-2575 per-request `_meta` fields
// (protocolVersion, optional clientInfo, clientCapabilities) on the given
// outgoing request params. Keys already present in params.Meta are not
// overwritten. Per PR modelcontextprotocol/modelcontextprotocol#3002
// clientInfo is SHOULD (not MUST), and is omitted when the client has no
// [Implementation] configured.
func injectRequestMeta[T any, P interface {
*T
Params
}](cs *ClientSession, params P) P {
res := cs.state.InitializeResult
func clientMethodUsesLegacyCompatPath(method string) bool {
switch method {
case methodPing,
methodSetLevel,
notificationRootsListChanged,
methodSubscribe,
methodUnsubscribe:
return true
default:
return false
}
}

func injectRequestMetaParams(cs *ClientSession, params Params) Params {
if params == nil {
params = new(T)
return nil
}
res := cs.state.InitializeResult
m := params.GetMeta()
m = maps.Clone(m)
if m == nil {
m = map[string]any{}
}
Expand All @@ -546,6 +551,22 @@ func injectRequestMeta[T any, P interface {
return params
}

// injectRequestMeta populates the SEP-2575 per-request `_meta` fields
// (protocolVersion, optional clientInfo, clientCapabilities) on the given
// outgoing request params. Keys already present in params.Meta are not
// overwritten. Per PR modelcontextprotocol/modelcontextprotocol#3002
// clientInfo is SHOULD (not MUST), and is omitted when the client has no
// [Implementation] configured.
func injectRequestMeta[T any, P interface {
*T
Params
}](cs *ClientSession, params P) P {
if params == nil {
params = new(T)
}
return injectRequestMetaParams(cs, params).(P)
}

func (cs *ClientSession) ID() string {
if c, ok := cs.mcpConn.(hasSessionID); ok {
return c.SessionID()
Expand Down Expand Up @@ -1674,9 +1695,6 @@ func CallCustomMethod[P paramsPtr[PT], R Result, PT any](
var zero R
return zero, fmt.Errorf("mcp: CallCustomMethod: %q is not registered; call AddSendingCustomMethod first", method)
}
if cs.usesNewProtocol() {
params = injectRequestMeta(cs, params)
}
Comment on lines -1677 to -1679

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not removing this call from all the other client send methods

return handleSend[R](ctx, method, &ClientRequest[P]{
Session: cs,
Params: params,
Expand Down
110 changes: 110 additions & 0 deletions mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"fmt"
"io"
"log/slog"
"maps"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -2166,6 +2167,57 @@ func TestNoDistributedDeadlock(t *testing.T) {
})
}

func TestClientNotifyProgressInjectsMetaOnNewProtocol(t *testing.T) {
ctx := context.Background()
metaCh := make(chan Meta, 1)

server := NewServer(testImpl, &ServerOptions{
ProgressNotificationHandler: func(_ context.Context, req *ProgressNotificationServerRequest) {
metaCh <- maps.Clone(req.Params.Meta)
},
})
ct, st := NewInMemoryTransports()
ss, err := server.Connect(ctx, st, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()

client := NewClient(testImpl, nil)
cs, err := client.Connect(ctx, ct, &ClientSessionOptions{ProtocolVersion: protocolVersion20260728})
if err != nil {
t.Fatal(err)
}
defer cs.Close()

if err := cs.NotifyProgress(ctx, &ProgressNotificationParams{
ProgressToken: "tok-1",
Progress: 1,
Message: "working",
}); err != nil {
t.Fatalf("NotifyProgress: %v", err)
}

select {
case meta := <-metaCh:
if got, want := meta[MetaKeyProtocolVersion], any(protocolVersion20260728); got != want {
t.Fatalf("_meta[%s] = %v, want %v", MetaKeyProtocolVersion, got, want)
}
if _, ok := meta[MetaKeyClientCapabilities].(map[string]any); !ok {
t.Fatalf("_meta[%s] = %T, want map[string]any", MetaKeyClientCapabilities, meta[MetaKeyClientCapabilities])
}
info, ok := meta[MetaKeyClientInfo].(map[string]any)
if !ok {
t.Fatalf("_meta[%s] = %T, want map[string]any", MetaKeyClientInfo, meta[MetaKeyClientInfo])
}
if got, want := info["name"], any(testImpl.Name); got != want {
t.Fatalf("clientInfo.name = %v, want %v", got, want)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for progress notification")
}
}

var testImpl = &Implementation{Name: "test", Version: "v1.0.0"}

// This test checks that when we use pointer types for tools, we get the same
Expand Down Expand Up @@ -3401,6 +3453,64 @@ func TestAddCustomMethodRejectsStandardMethods(t *testing.T) {
})
}

func TestCallCustomMethodInjectsMetaOnNewProtocol(t *testing.T) {
type pingParams struct{ ParamsBase }
type pingResult struct{ ResultBase }

ctx := context.Background()
metaCh := make(chan Meta, 1)
s := NewServer(testImpl, nil)
if err := AddReceivingCustomMethod(s, "acme/ping",
func(ctx context.Context, ss *ServerSession, p *pingParams) (*pingResult, error) {
metaCh <- maps.Clone(p.Meta)
return &pingResult{}, nil
}); err != nil {
t.Fatal(err)
}
ct, st := NewInMemoryTransports()
ss, err := s.Connect(ctx, st, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = ss.Close() })

c := NewClient(testImpl, nil)
if err := AddSendingCustomMethod[*pingParams, *pingResult](c, "acme/ping"); err != nil {
t.Fatal(err)
}
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{ProtocolVersion: protocolVersion20260728})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = cs.Close() })

params := &pingParams{ParamsBase: ParamsBase{Meta: Meta{"keep": "original"}}}
if _, err := CallCustomMethod[*pingParams, *pingResult](ctx, cs, "acme/ping", params); err != nil {
t.Fatalf("CallCustomMethod: %v", err)
}
if _, ok := params.Meta[MetaKeyProtocolVersion]; ok {
t.Fatalf("CallCustomMethod mutated caller params Meta: %v", params.Meta)
}

select {
case meta := <-metaCh:
if got, want := meta["keep"], any("original"); got != want {
t.Fatalf("_meta[keep] = %v, want %v", got, want)
}
if got, want := meta[MetaKeyProtocolVersion], any(protocolVersion20260728); got != want {
t.Fatalf("_meta[%s] = %v, want %v", MetaKeyProtocolVersion, got, want)
}
if _, ok := meta[MetaKeyClientCapabilities].(map[string]any); !ok {
t.Fatalf("_meta[%s] = %T, want map[string]any", MetaKeyClientCapabilities, meta[MetaKeyClientCapabilities])
}
if _, ok := meta[MetaKeyClientInfo].(map[string]any); !ok {
t.Fatalf("_meta[%s] = %T, want map[string]any", MetaKeyClientInfo, meta[MetaKeyClientInfo])
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for custom method")
}
}

// TestCallCustomMethodTypedNilParams exercises the typed-nil params path.
// User param types embed ParamsBase, so the inherited isNil forwarder would
// dereference a typed-nil outer if injectRequestMeta were called with it.
Expand Down
22 changes: 22 additions & 0 deletions mcp/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ func defaultSendingMethodHandler(ctx context.Context, method string, req Request
return nil, jsonrpc2.ErrNotHandled
}
params := req.GetParams()
if cs, ok := req.GetSession().(*ClientSession); ok && cs.usesNewProtocol() {
if params != nil && !clientMethodUsesLegacyCompatPath(method) {
params = cloneParams(params)
params = injectRequestMetaParams(cs, params)
}
}
if initParams, ok := params.(*InitializeParams); ok {
// Fix the marshaling of initialize params, to work around #607.
//
Expand Down Expand Up @@ -156,6 +162,22 @@ func orZero[T any, P *U, U any](p P) T {
return any(p).(T)
}

func cloneParams(params Params) Params {
if params == nil {
return nil
}
value := reflect.ValueOf(params)
if value.Kind() != reflect.Pointer {
return params
}
if value.IsNil() {
return reflect.New(value.Type().Elem()).Interface().(Params)
}
clone := reflect.New(value.Elem().Type())
clone.Elem().Set(value.Elem())
return clone.Interface().(Params)
}

func handleNotify(ctx context.Context, method string, req Request) error {
mh := req.GetSession().sendingMethodHandler()
_, err := mh(ctx, method, req)
Expand Down