diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 87636b9..677768c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: - go-version: "^1.25" + go-version: "^1.24" - name: Check formatting run: | cd go/sdk @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: ["1.23", "1.24", "1.25"] + go: ["1.24", "1.25"] steps: - name: Check out code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 diff --git a/go/sdk/go.mod b/go/sdk/go.mod index e4a7abe..6a7718f 100644 --- a/go/sdk/go.mod +++ b/go/sdk/go.mod @@ -1,19 +1,22 @@ module github.com/modelcontextprotocol/experimental-ext-variants/go/sdk -go 1.23.0 +go 1.24.0 toolchain go1.24.3 require ( - github.com/modelcontextprotocol/go-sdk v1.2.0 + github.com/modelcontextprotocol/go-sdk v1.4.0 github.com/stretchr/testify v1.11.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/jsonschema-go v0.3.0 // indirect + github.com/google/jsonschema-go v0.4.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go/sdk/go.sum b/go/sdk/go.sum index 2c0fd3d..b51fb72 100644 --- a/go/sdk/go.sum +++ b/go/sdk/go.sum @@ -1,23 +1,29 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= -github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s= -github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8= +github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go/sdk/variants/server.go b/go/sdk/variants/server.go index 7405a97..66a1f46 100644 --- a/go/sdk/variants/server.go +++ b/go/sdk/variants/server.go @@ -378,8 +378,8 @@ func (s *Server) enrichInitResult(ctx context.Context, result mcp.Result, req mc // one inner server supports it. // - Completions, Logging: marker capabilities (empty structs). Advertised // if any variant advertises them; the first non-nil value is kept. -// - Experimental: keys are merged into a single map. The first variant to -// register a given key wins; later duplicates are ignored. +// - Experimental, Extensions: keys are merged into a single map. The first +// variant to register a given key wins; later duplicates are ignored. func unionCapabilities(allCaps []*mcp.ServerCapabilities) *mcp.ServerCapabilities { union := &mcp.ServerCapabilities{} @@ -427,6 +427,17 @@ func unionCapabilities(allCaps []*mcp.ServerCapabilities) *mcp.ServerCapabilitie } } } + + if caps.Extensions != nil { + if union.Extensions == nil { + union.Extensions = make(map[string]any) + } + for k, v := range caps.Extensions { + if _, exists := union.Extensions[k]; !exists { + union.Extensions[k] = v + } + } + } } return union diff --git a/go/sdk/variants/server_test.go b/go/sdk/variants/server_test.go index 3110bc6..5f23a08 100644 --- a/go/sdk/variants/server_test.go +++ b/go/sdk/variants/server_test.go @@ -387,6 +387,71 @@ func TestIntegration_HTTP_Stateless(t *testing.T) { assert.NotEmpty(t, result.Content) } +// TestUnionCapabilities_MergesExtensions verifies that unionCapabilities +// merges Extensions from multiple inner servers using first-wins semantics. +func TestUnionCapabilities_MergesExtensions(t *testing.T) { + caps := []*mcp.ServerCapabilities{ + { + Extensions: map[string]any{ + "io.modelcontextprotocol/skills": map[string]any{}, + }, + }, + { + Extensions: map[string]any{ + "io.modelcontextprotocol/skills": map[string]any{"overridden": true}, + "io.modelcontextprotocol/other": map[string]any{"key": "val"}, + }, + }, + {}, + } + + union := unionCapabilities(caps) + + require.NotNil(t, union.Extensions) + assert.Contains(t, union.Extensions, "io.modelcontextprotocol/skills") + assert.Contains(t, union.Extensions, "io.modelcontextprotocol/other") + + // First-wins: the first variant's value should be kept. + skillsExt, ok := union.Extensions["io.modelcontextprotocol/skills"].(map[string]any) + require.True(t, ok) + assert.Empty(t, skillsExt, "first variant's empty map should win over second variant's") +} + +// TestIntegration_ExtensionsAdvertised verifies that extensions registered on +// inner variant servers are advertised to clients in the InitializeResult. +func TestIntegration_ExtensionsAdvertised(t *testing.T) { + codingServer := mcp.NewServer( + &mcp.Implementation{Name: "coding", Version: "v1"}, + &mcp.ServerOptions{ + Capabilities: func() *mcp.ServerCapabilities { + caps := &mcp.ServerCapabilities{} + caps.AddExtension("io.modelcontextprotocol/skills", nil) + return caps + }(), + }, + ) + mcp.AddTool(codingServer, &mcp.Tool{Name: "analyze_code", Description: "Static analysis"}, analyzeCode) + + compactServer := mcp.NewServer( + &mcp.Implementation{Name: "compact", Version: "v1"}, + nil, + ) + mcp.AddTool(compactServer, &mcp.Tool{Name: "summarize", Description: "Summarize text"}, summarize) + + vs := NewServer(&mcp.Implementation{Name: "test", Version: "1.0.0"}). + WithVariant(ServerVariant{ID: "coding", Description: "Coding", Status: Stable}, codingServer, 0). + WithVariant(ServerVariant{ID: "compact", Description: "Compact", Status: Stable}, compactServer, 1) + + session := connectTestClient(t, vs, nil) + + initResult := session.InitializeResult() + require.NotNil(t, initResult) + require.NotNil(t, initResult.Capabilities) + require.NotNil(t, initResult.Capabilities.Extensions, + "extensions from inner servers should be advertised in InitializeResult") + assert.Contains(t, initResult.Capabilities.Extensions, "io.modelcontextprotocol/skills") +} + // --------------------------------------------------------------------------- // Test tool handlers // ---------------------------------------------------------------------------