Skip to content

Commit 0ae7081

Browse files
committed
mcp: expose jsonrpc.Error type and standard error codes
Expose the jsonrpc.Error type to allow access to underlying JSON-RPC error codes. Also, expose common JSON-RPC error codes in the jsonrpc package, and MCP error codes in the mcp package. + tests Fixes #452
1 parent 21fb03d commit 0ae7081

File tree

11 files changed

+177
-40
lines changed

11 files changed

+177
-40
lines changed

jsonrpc/jsonrpc.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type (
1717
Request = jsonrpc2.Request
1818
// Response is a JSON-RPC response.
1919
Response = jsonrpc2.Response
20+
// Error is a structured error in a JSON-RPC response.
21+
Error = jsonrpc2.WireError
2022
)
2123

2224
// MakeID coerces the given Go value to an ID. The value should be the
@@ -37,3 +39,18 @@ func EncodeMessage(msg Message) ([]byte, error) {
3739
func DecodeMessage(data []byte) (Message, error) {
3840
return jsonrpc2.DecodeMessage(data)
3941
}
42+
43+
// Standard JSON-RPC 2.0 error codes.
44+
// See https://www.jsonrpc.org/specification#error_object
45+
const (
46+
// CodeParseError indicates invalid JSON was received by the server.
47+
CodeParseError = -32700
48+
// CodeInvalidRequest indicates the JSON sent is not a valid Request object.
49+
CodeInvalidRequest = -32600
50+
// CodeMethodNotFound indicates the method does not exist or is not available.
51+
CodeMethodNotFound = -32601
52+
// CodeInvalidParams indicates invalid method parameter(s).
53+
CodeInvalidParams = -32602
54+
// CodeInternalError indicates an internal JSON-RPC error.
55+
CodeInternalError = -32603
56+
)

mcp/client.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,14 @@ func (c *Client) listRoots(_ context.Context, req *ListRootsRequest) (*ListRoots
304304
func (c *Client) createMessage(ctx context.Context, req *CreateMessageRequest) (*CreateMessageResult, error) {
305305
if c.opts.CreateMessageHandler == nil {
306306
// TODO: wrap or annotate this error? Pick a standard code?
307-
return nil, jsonrpc2.NewError(codeUnsupportedMethod, "client does not support CreateMessage")
307+
return nil, &jsonrpc.Error{Code: codeUnsupportedMethod, Message: "client does not support CreateMessage"}
308308
}
309309
return c.opts.CreateMessageHandler(ctx, req)
310310
}
311311

312312
func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
313313
if c.opts.ElicitationHandler == nil {
314-
return nil, jsonrpc2.NewError(codeInvalidParams, "client does not support elicitation")
314+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: "client does not support elicitation"}
315315
}
316316

317317
// Validate the elicitation parameters based on the mode.
@@ -323,11 +323,11 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult,
323323
switch mode {
324324
case "form":
325325
if req.Params.URL != "" {
326-
return nil, jsonrpc2.NewError(codeInvalidParams, "URL must not be set for form elicitation")
326+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: "URL must not be set for form elicitation"}
327327
}
328328
schema, err := validateElicitSchema(req.Params.RequestedSchema)
329329
if err != nil {
330-
return nil, jsonrpc2.NewError(codeInvalidParams, err.Error())
330+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: err.Error()}
331331
}
332332
res, err := c.opts.ElicitationHandler(ctx, req)
333333
if err != nil {
@@ -337,28 +337,28 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult,
337337
if schema != nil && res.Content != nil {
338338
resolved, err := schema.Resolve(nil)
339339
if err != nil {
340-
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("failed to resolve requested schema: %v", err))
340+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("failed to resolve requested schema: %v", err)}
341341
}
342342
if err := resolved.Validate(res.Content); err != nil {
343-
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("elicitation result content does not match requested schema: %v", err))
343+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("elicitation result content does not match requested schema: %v", err)}
344344
}
345345
err = resolved.ApplyDefaults(&res.Content)
346346
if err != nil {
347-
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("failed to apply schema defalts to elicitation result: %v", err))
347+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("failed to apply schema defalts to elicitation result: %v", err)}
348348
}
349349
}
350350
return res, nil
351351
case "url":
352352
if req.Params.RequestedSchema != nil {
353-
return nil, jsonrpc2.NewError(codeInvalidParams, "requestedSchema must not be set for URL elicitation")
353+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: "requestedSchema must not be set for URL elicitation"}
354354
}
355355
if req.Params.URL == "" {
356-
return nil, jsonrpc2.NewError(codeInvalidParams, "URL must be set for URL elicitation")
356+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: "URL must be set for URL elicitation"}
357357
}
358358
// No schema validation for URL mode, just pass through to handler.
359359
return c.opts.ElicitationHandler(ctx, req)
360360
default:
361-
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("unsupported elicitation mode: %q", mode))
361+
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("unsupported elicitation mode: %q", mode)}
362362
}
363363
}
364364

mcp/elicitation_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/google/jsonschema-go/jsonschema"
15+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
1516
)
1617

1718
// TODO: migrate other elicitation tests here.
@@ -74,7 +75,7 @@ func TestElicitationURLMode(t *testing.T) {
7475
Message: "URL is missing",
7576
},
7677
wantErrMsg: "URL must be set for URL elicitation",
77-
wantErrCode: codeInvalidParams,
78+
wantErrCode: jsonrpc.CodeInvalidParams,
7879
},
7980
{
8081
name: "schema not allowed",
@@ -90,7 +91,7 @@ func TestElicitationURLMode(t *testing.T) {
9091
},
9192
},
9293
wantErrMsg: "requestedSchema must not be set for URL elicitation",
93-
wantErrCode: codeInvalidParams,
94+
wantErrCode: jsonrpc.CodeInvalidParams,
9495
},
9596
}
9697
for _, tc := range testCases {

mcp/error_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package mcp
6+
7+
import (
8+
"context"
9+
"errors"
10+
"testing"
11+
12+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
13+
)
14+
15+
// TestServerErrors validates that the server returns appropriate error codes
16+
// for various invalid requests.
17+
func TestServerErrors(t *testing.T) {
18+
ctx := context.Background()
19+
20+
// Set up a server with tools, prompts, and resources for testing
21+
cs, _, cleanup := basicConnection(t, func(s *Server) {
22+
// Add a tool with required parameters
23+
type RequiredParams struct {
24+
Name string `json:"name" jsonschema:"the name is required"`
25+
}
26+
handler := func(ctx context.Context, req *CallToolRequest, args RequiredParams) (*CallToolResult, any, error) {
27+
return &CallToolResult{
28+
Content: []Content{&TextContent{Text: "success"}},
29+
}, nil, nil
30+
}
31+
AddTool(s, &Tool{Name: "validate", Description: "validates params"}, handler)
32+
33+
// Add a prompt
34+
s.AddPrompt(codeReviewPrompt, codReviewPromptHandler)
35+
36+
// Add a resource that returns ResourceNotFoundError
37+
s.AddResource(
38+
&Resource{URI: "file:///test.txt", Name: "test", MIMEType: "text/plain"},
39+
func(ctx context.Context, req *ReadResourceRequest) (*ReadResourceResult, error) {
40+
return nil, ResourceNotFoundError(req.Params.URI)
41+
},
42+
)
43+
})
44+
defer cleanup()
45+
46+
testCases := []struct {
47+
name string
48+
executeCall func() error
49+
expectedCode int64
50+
}{
51+
{
52+
name: "missing required param",
53+
executeCall: func() error {
54+
_, err := cs.CallTool(ctx, &CallToolParams{
55+
Name: "validate",
56+
Arguments: map[string]any{}, // Missing required "name" field
57+
})
58+
return err
59+
},
60+
expectedCode: jsonrpc.CodeInvalidParams,
61+
},
62+
{
63+
name: "unknown tool",
64+
executeCall: func() error {
65+
_, err := cs.CallTool(ctx, &CallToolParams{
66+
Name: "nonexistent_tool",
67+
Arguments: map[string]any{},
68+
})
69+
return err
70+
},
71+
expectedCode: jsonrpc.CodeInvalidParams,
72+
},
73+
{
74+
name: "unknown prompt",
75+
executeCall: func() error {
76+
_, err := cs.GetPrompt(ctx, &GetPromptParams{
77+
Name: "nonexistent_prompt",
78+
Arguments: map[string]string{},
79+
})
80+
return err
81+
},
82+
expectedCode: jsonrpc.CodeInvalidParams,
83+
},
84+
{
85+
name: "resource not found",
86+
executeCall: func() error {
87+
_, err := cs.ReadResource(ctx, &ReadResourceParams{
88+
URI: "file:///test.txt",
89+
})
90+
return err
91+
},
92+
expectedCode: CodeResourceNotFound,
93+
},
94+
}
95+
96+
for _, tc := range testCases {
97+
t.Run(tc.name, func(t *testing.T) {
98+
err := tc.executeCall()
99+
if err == nil {
100+
t.Fatal("expected error, got nil")
101+
}
102+
103+
var rpcErr *jsonrpc.Error
104+
if !errors.As(err, &rpcErr) {
105+
t.Fatalf("expected jsonrpc.Error, got %T: %v", err, err)
106+
}
107+
108+
if rpcErr.Code != tc.expectedCode {
109+
t.Errorf("expected error code %d, got %d", tc.expectedCode, rpcErr.Code)
110+
}
111+
112+
if rpcErr.Message == "" {
113+
t.Error("expected non-empty error message")
114+
}
115+
})
116+
}
117+
}

mcp/mcp_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424

2525
"github.com/google/go-cmp/cmp"
2626
"github.com/google/jsonschema-go/jsonschema"
27-
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
27+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2828
)
2929

3030
type hiParams struct {
@@ -316,7 +316,7 @@ func TestEndToEnd(t *testing.T) {
316316
} {
317317
rres, err := cs.ReadResource(ctx, &ReadResourceParams{URI: tt.uri})
318318
if err != nil {
319-
if code := errorCode(err); code == codeResourceNotFound {
319+
if code := errorCode(err); code == CodeResourceNotFound {
320320
if tt.mimeType != "" {
321321
t.Errorf("%s: not found but expected it to be", tt.uri)
322322
}
@@ -576,7 +576,7 @@ func errorCode(err error) int64 {
576576
if err == nil {
577577
return 0
578578
}
579-
var werr *jsonrpc2.WireError
579+
var werr *jsonrpc.Error
580580
if errors.As(err, &werr) {
581581
return werr.Code
582582
}
@@ -1367,8 +1367,8 @@ func TestElicitationSchemaValidation(t *testing.T) {
13671367
t.Errorf("expected error for invalid schema %q, got nil", tc.name)
13681368
return
13691369
}
1370-
if code := errorCode(err); code != codeInvalidParams {
1371-
t.Errorf("got error code %d, want %d (CodeInvalidParams)", code, codeInvalidParams)
1370+
if code := errorCode(err); code != jsonrpc.CodeInvalidParams {
1371+
t.Errorf("got error code %d, want %d (CodeInvalidParams)", code, jsonrpc.CodeInvalidParams)
13721372
}
13731373
if !strings.Contains(err.Error(), tc.expectedError) {
13741374
t.Errorf("error message %q does not contain expected text %q", err.Error(), tc.expectedError)

mcp/resource.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import (
1515
"path/filepath"
1616
"strings"
1717

18-
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
1918
"github.com/modelcontextprotocol/go-sdk/internal/util"
19+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2020
"github.com/yosida95/uritemplate/v3"
2121
)
2222

@@ -40,8 +40,8 @@ type ResourceHandler func(context.Context, *ReadResourceRequest) (*ReadResourceR
4040
// ResourceNotFoundError returns an error indicating that a resource being read could
4141
// not be found.
4242
func ResourceNotFoundError(uri string) error {
43-
return &jsonrpc2.WireError{
44-
Code: codeResourceNotFound,
43+
return &jsonrpc.Error{
44+
Code: CodeResourceNotFound,
4545
Message: "Resource not found",
4646
Data: json.RawMessage(fmt.Sprintf(`{"uri":%q}`, uri)),
4747
}

mcp/server.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,12 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan
293293
// Call typed handler.
294294
res, out, err := h(ctx, req, in)
295295
// Handle server errors appropriately:
296-
// - If the handler returns a structured error (like jsonrpc2.WireError), return it directly
296+
// - If the handler returns a structured error (like jsonrpc.Error), return it directly
297297
// - If the handler returns a regular error, wrap it in a CallToolResult with IsError=true
298298
// - This allows tools to distinguish between protocol errors and tool execution errors
299299
if err != nil {
300300
// Check if this is already a structured JSON-RPC error
301-
if wireErr, ok := err.(*jsonrpc2.WireError); ok {
301+
if wireErr, ok := err.(*jsonrpc.Error); ok {
302302
return nil, wireErr
303303
}
304304
// For regular errors, embed them in the tool result as per MCP spec
@@ -542,8 +542,8 @@ func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetProm
542542
s.mu.Unlock()
543543
if !ok {
544544
// Return a proper JSON-RPC error with the correct error code
545-
return nil, &jsonrpc2.WireError{
546-
Code: codeInvalidParams,
545+
return nil, &jsonrpc.Error{
546+
Code: jsonrpc.CodeInvalidParams,
547547
Message: fmt.Sprintf("unknown prompt %q", req.Params.Name),
548548
}
549549
}
@@ -569,8 +569,8 @@ func (s *Server) callTool(ctx context.Context, req *CallToolRequest) (*CallToolR
569569
st, ok := s.tools.get(req.Params.Name)
570570
s.mu.Unlock()
571571
if !ok {
572-
return nil, &jsonrpc2.WireError{
573-
Code: codeInvalidParams,
572+
return nil, &jsonrpc.Error{
573+
Code: jsonrpc.CodeInvalidParams,
574574
Message: fmt.Sprintf("unknown tool %q", req.Params.Name),
575575
}
576576
}

mcp/shared.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,19 @@ func clientSessionMethod[P Params, R Result](f func(*ClientSession, context.Cont
331331
}
332332
}
333333

334-
// Error codes
334+
// MCP-specific error codes.
335+
const (
336+
// CodeResourceNotFound indicates that a requested resource could not be found.
337+
CodeResourceNotFound = -32002
338+
)
339+
340+
// Internal error codes
335341
const (
336-
codeResourceNotFound = -32002
337342
// The error code if the method exists and was called properly, but the peer does not support it.
338343
//
339344
// TODO(rfindley): this code is wrong, and we should fix it to be
340345
// consistent with other SDKs.
341346
codeUnsupportedMethod = -31001
342-
// The error code for invalid parameters
343-
codeInvalidParams = -32602
344347
)
345348

346349
// notifySessions calls Notify on all the sessions.

mcp/streamable_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ func TestStreamableServerTransport(t *testing.T) {
962962
method: "POST",
963963
messages: []jsonrpc.Message{req(2, "tools/call", &CallToolParams{Name: "tool"})},
964964
wantStatusCode: http.StatusOK,
965-
wantMessages: []jsonrpc.Message{resp(2, nil, &jsonrpc2.WireError{
965+
wantMessages: []jsonrpc.Message{resp(2, nil, &jsonrpc.Error{
966966
Message: `method "tools/call" is invalid during session initialization`,
967967
})},
968968
},

0 commit comments

Comments
 (0)