diff --git a/examples/transport-modes/streamable-http/stateful-json-getsse/client/main.go b/examples/transport-modes/streamable-http/stateful-json-getsse/client/main.go index cded46a..a3bb5eb 100644 --- a/examples/transport-modes/streamable-http/stateful-json-getsse/client/main.go +++ b/examples/transport-modes/streamable-http/stateful-json-getsse/client/main.go @@ -102,9 +102,7 @@ func main() { Arguments: map[string]interface{}{ "increment": 1, }, - Meta: &struct { - ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"` - }{ + Meta: &mcp.Meta{ ProgressToken: 123, }, }, diff --git a/mcp_messages.go b/mcp_messages.go index a9d7175..9467a5b 100644 --- a/mcp_messages.go +++ b/mcp_messages.go @@ -204,10 +204,12 @@ const ( const ( ProtocolVersion_2024_11_05 = "2024-11-05" ProtocolVersion_2025_03_26 = "2025-03-26" + ProtocolVersion_2025_06_18 = "2025-06-18" ) // List of supported protocol versions, ordered by priority var SupportedProtocolVersions = []string{ + ProtocolVersion_2025_06_18, // Latest: Added _meta AdditionalFields support ProtocolVersion_2025_03_26, ProtocolVersion_2024_11_05, } diff --git a/mcp_tools.go b/mcp_tools.go index c8ea6a2..38b20ae 100644 --- a/mcp_tools.go +++ b/mcp_tools.go @@ -74,9 +74,7 @@ type CallToolRequest struct { type CallToolParams struct { Name string `json:"name"` Arguments map[string]interface{} `json:"arguments,omitempty"` - Meta *struct { - ProgressToken ProgressToken `json:"progressToken,omitempty"` - } `json:"_meta,omitempty"` + Meta *Meta `json:"_meta,omitempty"` } // RequestMeta represents request metadata diff --git a/mcp_types.go b/mcp_types.go index 37f14ab..3fabc0d 100644 --- a/mcp_types.go +++ b/mcp_types.go @@ -25,13 +25,89 @@ const ( // MCP protcol Layer +// Meta represents metadata attached to a request's parameters. +// This can include fields formally defined by the protocol (like ProgressToken) +// or other arbitrary data for custom use cases. +// Based on mcp-go implementation for MCP 2025-06-18 protocol support. +type Meta struct { + // ProgressToken is used to request out-of-band progress notifications. + // If specified, the caller is requesting progress notifications for this + // request (as represented by notifications/progress). The value is an + // opaque token that will be attached to any subsequent notifications. + // The receiver is not obligated to provide these notifications. + ProgressToken ProgressToken `json:"-"` + + // AdditionalFields are any fields present in the Meta that are not + // otherwise defined in the protocol. This allows for custom metadata + // to be passed between clients and servers. + AdditionalFields map[string]interface{} `json:"-"` +} + +// MarshalJSON implements custom JSON marshaling for Meta. +// It flattens ProgressToken and AdditionalFields into a single JSON object. +func (m *Meta) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + + raw := make(map[string]interface{}) + + // Add progressToken if present + if m.ProgressToken != nil { + raw["progressToken"] = m.ProgressToken + } + + // Add all additional fields + for k, v := range m.AdditionalFields { + raw[k] = v + } + + return json.Marshal(raw) +} + +// UnmarshalJSON implements custom JSON unmarshaling for Meta. +// It extracts progressToken and puts all other fields into AdditionalFields. +func (m *Meta) UnmarshalJSON(data []byte) error { + raw := make(map[string]interface{}) + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Extract progressToken + if pt, ok := raw["progressToken"]; ok { + m.ProgressToken = pt + delete(raw, "progressToken") + } + + // Store remaining fields as additional fields + m.AdditionalFields = raw + + return nil +} + +// Get retrieves a value from AdditionalFields by key. +// Returns nil if the key doesn't exist or AdditionalFields is nil. +func (m *Meta) Get(key string) interface{} { + if m == nil || m.AdditionalFields == nil { + return nil + } + return m.AdditionalFields[key] +} + +// Set sets a value in AdditionalFields. +// Initializes AdditionalFields if it's nil. +func (m *Meta) Set(key string, value interface{}) { + if m.AdditionalFields == nil { + m.AdditionalFields = make(map[string]interface{}) + } + m.AdditionalFields[key] = value +} + // Request is the base request struct for all MCP requests. type Request struct { Method string `json:"method"` Params struct { - Meta *struct { - ProgressToken ProgressToken `json:"progressToken,omitempty"` - } `json:"_meta,omitempty"` + Meta *Meta `json:"_meta,omitempty"` } `json:"params,omitempty"` } @@ -47,10 +123,6 @@ type NotificationParams struct { AdditionalFields map[string]interface{} `json:"-"` // Additional fields that are not part of the MCP protocol. } -// Meta represents the _meta field in MCP objects. -// Using map[string]interface{} for flexibility as in mcp-go. -type Meta map[string]interface{} - // MarshalJSON implements custom JSON marshaling for NotificationParams. // It flattens the AdditionalFields into the main JSON object. func (p NotificationParams) MarshalJSON() ([]byte, error) { @@ -91,7 +163,7 @@ func (p *NotificationParams) UnmarshalJSON(data []byte) error { if sData == "null" || sData == "{}" { // If params is null or an empty object, initialize and return p.AdditionalFields = make(map[string]interface{}) - p.Meta = make(Meta) // Initialize Meta as well + p.Meta = make(map[string]interface{}) // Initialize Meta as well return nil } @@ -103,18 +175,13 @@ func (p *NotificationParams) UnmarshalJSON(data []byte) error { if p.AdditionalFields == nil { p.AdditionalFields = make(map[string]interface{}) } - // Ensure Meta is initialized if it's going to be populated or checked - // p.Meta might be nil initially. - // if p.Meta == nil { // Not strictly needed here as we assign directly or check m["_meta"] - // p.Meta = make(Meta) - // } for k, v := range m { if k == "_meta" { if metaMap, ok := v.(map[string]interface{}); ok { // Initialize p.Meta only if it's nil and metaMap is not nil and not empty if p.Meta == nil && metaMap != nil && len(metaMap) > 0 { - p.Meta = make(Meta) + p.Meta = make(map[string]interface{}) } // Populate p.Meta. This handles case where p.Meta was nil or already existed. if p.Meta != nil { // ensure p.Meta is not nil before assigning to it @@ -123,8 +190,6 @@ func (p *NotificationParams) UnmarshalJSON(data []byte) error { } } } - // else: you might want to handle cases where _meta is not a map[string]interface{} - // or log a warning, depending on strictness. } else { p.AdditionalFields[k] = v } diff --git a/mcp_types_meta_test.go b/mcp_types_meta_test.go new file mode 100644 index 0000000..5a61ac3 --- /dev/null +++ b/mcp_types_meta_test.go @@ -0,0 +1,359 @@ +// Tencent is pleased to support the open source community by making trpc-mcp-go available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// trpc-mcp-go is licensed under the Apache License Version 2.0. + +package mcp + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMetaMarshalling(t *testing.T) { + tests := []struct { + name string + json string + meta *Meta + expMeta *Meta + }{ + { + name: "nil meta", + json: "null", + meta: nil, + }, + { + name: "empty meta", + json: "{}", + meta: &Meta{}, + }, + { + name: "only progressToken", + json: `{"progressToken":123}`, + meta: &Meta{ + ProgressToken: 123, + }, + expMeta: &Meta{ + ProgressToken: 123, + AdditionalFields: map[string]interface{}{}, + }, + }, + { + name: "progressToken string", + json: `{"progressToken":"abc-123"}`, + meta: &Meta{ + ProgressToken: "abc-123", + }, + expMeta: &Meta{ + ProgressToken: "abc-123", + AdditionalFields: map[string]interface{}{}, + }, + }, + { + name: "only additional fields", + json: `{"customKey":"customValue","nested":{"field":"value"}}`, + meta: &Meta{ + AdditionalFields: map[string]interface{}{ + "customKey": "customValue", + "nested": map[string]interface{}{ + "field": "value", + }, + }, + }, + }, + { + name: "progressToken and additional fields", + json: `{"progressToken":456,"platform.auth/token":"eyJhbGci...","platform.auth/tenant":"tenant-abc"}`, + meta: &Meta{ + ProgressToken: 456, + AdditionalFields: map[string]interface{}{ + "platform.auth/token": "eyJhbGci...", + "platform.auth/tenant": "tenant-abc", + }, + }, + }, + { + name: "complex additional fields", + json: `{"progressToken":789,"custom.domain/array":["item1","item2"],"custom.domain/number":42.5}`, + meta: &Meta{ + ProgressToken: 789, + AdditionalFields: map[string]interface{}{ + "custom.domain/array": []interface{}{"item1", "item2"}, + "custom.domain/number": 42.5, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name+" marshal", func(t *testing.T) { + data, err := json.Marshal(tt.meta) + require.NoError(t, err) + + // Verify JSON structure matches expected + var got, expected map[string]interface{} + if tt.json != "null" { + require.NoError(t, json.Unmarshal([]byte(tt.json), &expected)) + require.NoError(t, json.Unmarshal(data, &got)) + assert.Equal(t, expected, got, "marshalled JSON should match expected") + } + }) + + t.Run(tt.name+" unmarshal", func(t *testing.T) { + var meta Meta + err := json.Unmarshal([]byte(tt.json), &meta) + require.NoError(t, err) + + // Use expMeta if provided, otherwise use original meta + expected := tt.meta + if tt.expMeta != nil { + expected = tt.expMeta + } + + if expected != nil { + assert.Equal(t, expected.ProgressToken, meta.ProgressToken, "progressToken should match") + assert.Equal(t, expected.AdditionalFields, meta.AdditionalFields, "additionalFields should match") + } + }) + + t.Run(tt.name+" roundtrip", func(t *testing.T) { + if tt.meta == nil { + t.Skip("skipping roundtrip for nil meta") + } + + // Marshal + data, err := json.Marshal(tt.meta) + require.NoError(t, err) + + // Unmarshal + var meta Meta + err = json.Unmarshal(data, &meta) + require.NoError(t, err) + + // Verify roundtrip + assert.Equal(t, tt.meta.ProgressToken, meta.ProgressToken, "progressToken should survive roundtrip") + assert.Equal(t, tt.meta.AdditionalFields, meta.AdditionalFields, "additionalFields should survive roundtrip") + }) + } +} + +func TestMetaGetSet(t *testing.T) { + meta := &Meta{} + + // Test Get on empty meta + assert.Nil(t, meta.Get("nonexistent")) + + // Test Set + meta.Set("key1", "value1") + assert.Equal(t, "value1", meta.Get("key1")) + + meta.Set("key2", 123) + assert.Equal(t, 123, meta.Get("key2")) + + meta.Set("key3", map[string]interface{}{"nested": "value"}) + assert.Equal(t, map[string]interface{}{"nested": "value"}, meta.Get("key3")) + + // Test overwriting + meta.Set("key1", "value2") + assert.Equal(t, "value2", meta.Get("key1")) + + // Test Get on nil meta + var nilMeta *Meta + assert.Nil(t, nilMeta.Get("key")) +} + +func TestRequestWithMeta(t *testing.T) { + tests := []struct { + name string + json string + wantErr bool + }{ + { + name: "request with progressToken only", + json: `{ + "method": "tools/call", + "params": { + "_meta": { + "progressToken": 123 + } + } + }`, + }, + { + name: "request with custom metadata", + json: `{ + "method": "tools/call", + "params": { + "_meta": { + "progressToken": 456, + "platform.auth/token": "eyJhbGci...", + "platform.auth/tenant": "tenant-abc" + } + } + }`, + }, + { + name: "request without meta", + json: `{ + "method": "tools/call", + "params": {} + }`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var req Request + err := json.Unmarshal([]byte(tt.json), &req) + + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + + // Marshal back + data, err := json.Marshal(req) + require.NoError(t, err) + + // Unmarshal again to verify roundtrip + var req2 Request + err = json.Unmarshal(data, &req2) + require.NoError(t, err) + + // Verify meta preserved + if req.Params.Meta != nil { + require.NotNil(t, req2.Params.Meta) + assert.Equal(t, req.Params.Meta.ProgressToken, req2.Params.Meta.ProgressToken) + assert.Equal(t, req.Params.Meta.AdditionalFields, req2.Params.Meta.AdditionalFields) + } + }) + } +} + +func TestCallToolParamsWithMeta(t *testing.T) { + tests := []struct { + name string + json string + wantErr bool + }{ + { + name: "tool call with progressToken", + json: `{ + "name": "getUserData", + "arguments": { + "userId": "12345" + }, + "_meta": { + "progressToken": 123 + } + }`, + }, + { + name: "tool call with custom metadata", + json: `{ + "name": "getUserData", + "arguments": { + "userId": "12345" + }, + "_meta": { + "progressToken": 456, + "platform.auth/token": "eyJhbGci...", + "platform.auth/tenant": "tenant-abc", + "platform.auth/permissions": ["read", "write"] + } + }`, + }, + { + name: "tool call without meta", + json: `{ + "name": "getUserData", + "arguments": { + "userId": "12345" + } + }`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var params CallToolParams + err := json.Unmarshal([]byte(tt.json), ¶ms) + + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + + // Marshal back + data, err := json.Marshal(params) + require.NoError(t, err) + + // Unmarshal again to verify roundtrip + var params2 CallToolParams + err = json.Unmarshal(data, ¶ms2) + require.NoError(t, err) + + // Verify basic fields + assert.Equal(t, params.Name, params2.Name) + assert.Equal(t, params.Arguments, params2.Arguments) + + // Verify meta preserved + if params.Meta != nil { + require.NotNil(t, params2.Meta) + assert.Equal(t, params.Meta.ProgressToken, params2.Meta.ProgressToken) + assert.Equal(t, params.Meta.AdditionalFields, params2.Meta.AdditionalFields) + } + }) + } +} + +func TestMetaUseCaseFromFaustli(t *testing.T) { + // This test demonstrates the use case from faustli's requirement: + // Passing auth metadata from A2A protocol through MCP to business MCP server + + // Client sends request with auth metadata + reqJSON := `{ + "method": "tools/call", + "params": { + "name": "getUserData", + "arguments": { + "userId": "12345" + }, + "_meta": { + "progressToken": "token-123", + "platform.auth/token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "platform.auth/tenant": "tenant-abc", + "platform.auth/permissions": ["read", "write"] + } + } + }` + + var req CallToolRequest + err := json.Unmarshal([]byte(reqJSON), &req) + require.NoError(t, err) + + // Server extracts auth metadata + assert.NotNil(t, req.Params.Meta) + assert.Equal(t, "token-123", req.Params.Meta.ProgressToken) + + authToken := req.Params.Meta.Get("platform.auth/token") + assert.Equal(t, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", authToken) + + tenant := req.Params.Meta.Get("platform.auth/tenant") + assert.Equal(t, "tenant-abc", tenant) + + permissions := req.Params.Meta.Get("platform.auth/permissions") + assert.Equal(t, []interface{}{"read", "write"}, permissions) + + // Verify metadata is not exposed to LLM (not in arguments) + _, hasAuthInArgs := req.Params.Arguments["platform.auth/token"] + assert.False(t, hasAuthInArgs, "auth metadata should not be in arguments") +}