diff --git a/go.mod b/go.mod index 9cf969bf2dc..9049c0452c0 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/go-github/v71 v71.0.0 - github.com/google/jsonschema-go v0.4.2 + github.com/google/jsonschema-go v0.4.3 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.1 @@ -76,7 +76,7 @@ require ( github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7 github.com/mazznoer/csscolorparser v0.1.3 github.com/mitchellh/mapstructure v1.5.0 - github.com/modelcontextprotocol/go-sdk v1.4.1 + github.com/modelcontextprotocol/go-sdk v1.8.0 github.com/openai/openai-go/v3 v3.17.0 github.com/orbcorp/orb-go v1.32.0 github.com/paulmach/orb v0.12.0 @@ -294,7 +294,7 @@ require ( github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -429,7 +429,7 @@ require ( golang.org/x/crypto v0.55.0 // indirect golang.org/x/mod v0.40.0 // indirect golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 // indirect - golang.org/x/time v0.14.0 // indirect + golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.49.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect diff --git a/go.sum b/go.sum index e40f10a5f51..8d42489889c 100644 --- a/go.sum +++ b/go.sum @@ -1461,8 +1461,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -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/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -1554,8 +1554,8 @@ github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -1997,8 +1997,8 @@ github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7ID github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= -github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtwfBpOTAI+Urx4= +github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -3031,8 +3031,8 @@ golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/runtime/ai/mcp.go b/runtime/ai/mcp.go index 378d35c9254..ed1e4c6cf28 100644 --- a/runtime/ai/mcp.go +++ b/runtime/ai/mcp.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/jsonrpc" @@ -115,11 +116,7 @@ func (s *Session) MCPServer(ctx context.Context) *mcp.Server { srv.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler { return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) { res, err := next(ctx, method, req) - if method != "tools/call" || err == nil { - return res, err - } - var jsonrpcErr *jsonrpc.Error - if !errors.As(err, &jsonrpcErr) || jsonrpcErr.Code != jsonrpc.CodeInvalidParams { + if method != "tools/call" || !isMCPInputValidationFailure(res, err) { return res, err } if !s.coerceMCPToolArgs(req) { @@ -151,6 +148,22 @@ func (s *Session) MCPServer(ctx context.Context) *mcp.Server { return srv } +// isMCPInputValidationFailure reports whether a tools/call outcome is the SDK rejecting the arguments against the tool's input schema. +// Older SDK versions reported this as an "invalid params" protocol error; since go-sdk v1.7 it is a tool error result whose text +// starts with the SDK's validation prefix. Genuine tool errors never carry that prefix, so they are not retried. +func isMCPInputValidationFailure(res mcp.Result, err error) bool { + if err != nil { + var jsonrpcErr *jsonrpc.Error + return errors.As(err, &jsonrpcErr) && jsonrpcErr.Code == jsonrpc.CodeInvalidParams + } + ctr, ok := res.(*mcp.CallToolResult) + if !ok || !ctr.IsError || len(ctr.Content) == 0 { + return false + } + text, ok := ctr.Content[0].(*mcp.TextContent) + return ok && strings.HasPrefix(text.Text, `validating "arguments"`) +} + // coerceMCPToolArgs rewrites a tool call's raw arguments, // JSON-decoding any string values in positions where the tool's input schema expects an object or array. // It returns whether the arguments were rewritten. diff --git a/runtime/server/mcp_test.go b/runtime/server/mcp_test.go index e19d744cd25..1c56c5b5534 100644 --- a/runtime/server/mcp_test.go +++ b/runtime/server/mcp_test.go @@ -108,9 +108,12 @@ explore: require.Contains(t, mvsText, "mv1") require.Contains(t, mvsText, "mv2") - // Test that it handles missing parameters - _, err = conn.CallTool(t.Context(), &mcp.CallToolParams{Name: ai.GetMetricsViewName}) - require.ErrorContains(t, err, "missing properties") + // Test that it handles missing parameters. + // Since go-sdk v1.7, input validation failures are reported as tool errors rather than protocol errors. + missing, err := conn.CallTool(t.Context(), &mcp.CallToolParams{Name: ai.GetMetricsViewName}) + require.NoError(t, err) + require.True(t, missing.IsError) + require.Contains(t, missing.Content[0].(*mcp.TextContent).Text, "missing properties") // Test a query with object/array-typed arguments sent as JSON-encoded strings. // Some MCP clients stringify nested arguments (see https://github.com/anthropics/claude-code/issues/25865); @@ -156,7 +159,7 @@ explore: require.Contains(t, resText, "row_count_prev") // Test that a string that is not valid JSON still fails schema validation - _, err = conn.CallTool(t.Context(), &mcp.CallToolParams{ + invalid, err := conn.CallTool(t.Context(), &mcp.CallToolParams{ Name: ai.QueryMetricsViewName, Arguments: map[string]any{ "metrics_view": "mv1", @@ -164,7 +167,9 @@ explore: "time_range": "last 7 days", }, }) - require.ErrorContains(t, err, `want "object"`) + require.NoError(t, err) + require.True(t, invalid.IsError) + require.Contains(t, invalid.Content[0].(*mcp.TextContent).Text, `want "object"`) } // TestMCPSkillTools asserts that the skill tools are advertised to callers that can use AI, and only to them.