Skip to content

Commit c59df07

Browse files
shivasuryaclaude
andcommitted
feat(mcp): add robust error handling per JSON-RPC 2.0 spec
Implement comprehensive error handling: - errors.go: Standard and custom error codes - errors_test.go: Full test coverage Error codes: - -32700: Parse error - -32600: Invalid Request - -32601: Method not found - -32602: Invalid params - -32603: Internal error - -32001: Symbol not found (with suggestions) - -32002: Index not ready - -32003: Query timeout Features: - Parameter validation helpers (ValidateStringParam, ValidateIntParam) - Structured error responses with data field - Tool error format with code and details - Request validation (JSONRPC version, method required) Coverage: errors.go 100%, total 91.6% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 34eafd1 commit c59df07

File tree

4 files changed

+573
-3
lines changed

4 files changed

+573
-3
lines changed

sast-engine/mcp/errors.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package mcp
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
// Standard JSON-RPC 2.0 error codes.
9+
const (
10+
ErrCodeParseError = -32700
11+
ErrCodeInvalidRequest = -32600
12+
ErrCodeMethodNotFound = -32601
13+
ErrCodeInvalidParams = -32602
14+
ErrCodeInternalError = -32603
15+
16+
// Custom server error codes (-32000 to -32099).
17+
ErrCodeSymbolNotFound = -32001
18+
ErrCodeIndexNotReady = -32002
19+
ErrCodeQueryTimeout = -32003
20+
ErrCodeResultsTruncated = -32004
21+
)
22+
23+
// errorMessages maps error codes to default messages.
24+
var errorMessages = map[int]string{
25+
ErrCodeParseError: "Parse error",
26+
ErrCodeInvalidRequest: "Invalid Request",
27+
ErrCodeMethodNotFound: "Method not found",
28+
ErrCodeInvalidParams: "Invalid params",
29+
ErrCodeInternalError: "Internal error",
30+
ErrCodeSymbolNotFound: "Symbol not found",
31+
ErrCodeIndexNotReady: "Index not ready",
32+
ErrCodeQueryTimeout: "Query timeout",
33+
ErrCodeResultsTruncated: "Results truncated",
34+
}
35+
36+
// Error implements the error interface for RPCError.
37+
func (e *RPCError) Error() string {
38+
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
39+
}
40+
41+
// NewRPCError creates a new RPC error with optional data.
42+
func NewRPCError(code int, data interface{}) *RPCError {
43+
message := errorMessages[code]
44+
if message == "" {
45+
message = "Unknown error"
46+
}
47+
return &RPCError{
48+
Code: code,
49+
Message: message,
50+
Data: data,
51+
}
52+
}
53+
54+
// NewRPCErrorWithMessage creates an RPC error with custom message.
55+
func NewRPCErrorWithMessage(code int, message string, data interface{}) *RPCError {
56+
return &RPCError{
57+
Code: code,
58+
Message: message,
59+
Data: data,
60+
}
61+
}
62+
63+
// ParseError creates a parse error response.
64+
func ParseError(detail string) *RPCError {
65+
return NewRPCErrorWithMessage(ErrCodeParseError, "Parse error: "+detail, nil)
66+
}
67+
68+
// InvalidRequestError creates an invalid request error.
69+
func InvalidRequestError(detail string) *RPCError {
70+
return NewRPCErrorWithMessage(ErrCodeInvalidRequest, "Invalid request: "+detail, nil)
71+
}
72+
73+
// MethodNotFoundError creates a method not found error.
74+
func MethodNotFoundError(method string) *RPCError {
75+
return NewRPCErrorWithMessage(ErrCodeMethodNotFound,
76+
fmt.Sprintf("Method not found: %s", method),
77+
map[string]string{"method": method})
78+
}
79+
80+
// InvalidParamsError creates an invalid params error.
81+
func InvalidParamsError(detail string) *RPCError {
82+
return NewRPCErrorWithMessage(ErrCodeInvalidParams, "Invalid params: "+detail, nil)
83+
}
84+
85+
// InternalError creates an internal error.
86+
func InternalError(detail string) *RPCError {
87+
return NewRPCErrorWithMessage(ErrCodeInternalError, "Internal error: "+detail, nil)
88+
}
89+
90+
// SymbolNotFoundError creates a symbol not found error with suggestions.
91+
func SymbolNotFoundError(symbol string, suggestions []string) *RPCError {
92+
data := map[string]interface{}{
93+
"symbol": symbol,
94+
}
95+
if len(suggestions) > 0 {
96+
data["suggestions"] = suggestions
97+
}
98+
return NewRPCErrorWithMessage(ErrCodeSymbolNotFound,
99+
fmt.Sprintf("Symbol not found: %s", symbol), data)
100+
}
101+
102+
// IndexNotReadyError creates an index not ready error.
103+
func IndexNotReadyError() *RPCError {
104+
return NewRPCError(ErrCodeIndexNotReady, nil)
105+
}
106+
107+
// QueryTimeoutError creates a query timeout error.
108+
func QueryTimeoutError(timeout string) *RPCError {
109+
return NewRPCErrorWithMessage(ErrCodeQueryTimeout,
110+
fmt.Sprintf("Query timed out after %s", timeout),
111+
map[string]string{"timeout": timeout})
112+
}
113+
114+
// MakeErrorResponse creates a JSON-RPC error response from an RPCError.
115+
func MakeErrorResponse(id interface{}, err *RPCError) *JSONRPCResponse {
116+
return &JSONRPCResponse{
117+
JSONRPC: "2.0",
118+
ID: id,
119+
Error: err,
120+
}
121+
}
122+
123+
// ToolError represents a structured tool error response.
124+
type ToolError struct {
125+
Error string `json:"error"`
126+
Code int `json:"code,omitempty"`
127+
Details interface{} `json:"details,omitempty"`
128+
}
129+
130+
// NewToolError creates a JSON-formatted tool error response.
131+
func NewToolError(message string, code int, details interface{}) string {
132+
te := ToolError{
133+
Error: message,
134+
Code: code,
135+
Details: details,
136+
}
137+
bytes, _ := json.Marshal(te)
138+
return string(bytes)
139+
}
140+
141+
// ValidateRequiredParams checks for required parameters.
142+
func ValidateRequiredParams(args map[string]interface{}, required []string) *RPCError {
143+
missing := []string{}
144+
for _, param := range required {
145+
if _, ok := args[param]; !ok {
146+
missing = append(missing, param)
147+
}
148+
}
149+
if len(missing) > 0 {
150+
return InvalidParamsError(fmt.Sprintf("missing required parameters: %v", missing))
151+
}
152+
return nil
153+
}
154+
155+
// ValidateStringParam validates a string parameter.
156+
func ValidateStringParam(args map[string]interface{}, name string) (string, *RPCError) {
157+
val, ok := args[name]
158+
if !ok {
159+
return "", InvalidParamsError(fmt.Sprintf("missing required parameter: %s", name))
160+
}
161+
str, ok := val.(string)
162+
if !ok {
163+
return "", InvalidParamsError(fmt.Sprintf("parameter %s must be a string", name))
164+
}
165+
if str == "" {
166+
return "", InvalidParamsError(fmt.Sprintf("parameter %s cannot be empty", name))
167+
}
168+
return str, nil
169+
}
170+
171+
// ValidateIntParam validates an integer parameter with optional default.
172+
func ValidateIntParam(args map[string]interface{}, name string, defaultVal int) (int, *RPCError) {
173+
val, ok := args[name]
174+
if !ok {
175+
return defaultVal, nil
176+
}
177+
178+
// JSON numbers come as float64.
179+
switch v := val.(type) {
180+
case float64:
181+
return int(v), nil
182+
case int:
183+
return v, nil
184+
default:
185+
return 0, InvalidParamsError(fmt.Sprintf("parameter %s must be a number", name))
186+
}
187+
}

0 commit comments

Comments
 (0)