Skip to content

Commit 5203a3d

Browse files
committed
Improved error propagation via hooks
This commit enhances the MCP server's error handling capabilities by: 1. Improving error propagation through the OnError hook system 2. Adding comprehensive documentation with usage examples 3. Ensuring error types can be properly inspected with Go's standard error handling patterns We've added better support for typed errors that propagate through the request handling chain: - `ErrUnsupported`: Used when a capability is not enabled on the server - `UnparseableMessageError`: Used when parsing a message fails - `ErrResourceNotFound`: Used when a requested resource doesn't exist - `ErrPromptNotFound`: Used when a requested prompt doesn't exist - `ErrToolNotFound`: Used when a requested tool doesn't exist These errors can be interrogated using `errors.Is` and `errors.As` to provide more targeted error handling. The documentation now includes examples like: ```go hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) { // Check for specific error types using errors.Is if errors.Is(err, ErrUnsupported) { // Handle capability not supported errors log.Printf("Capability not supported: %v", err) } // Use errors.As to get specific error types var parseErr = &UnparseableMessageError{} if errors.As(err, &parseErr) { // Access specific methods/fields of the error type log.Printf("Failed to parse message for method %s: %v", parseErr.GetMethod(), parseErr.Unwrap()) // Access the raw message that failed to parse rawMsg := parseErr.GetMessage() } // Check for specific resource/prompt/tool errors switch { case errors.Is(err, ErrResourceNotFound): log.Printf("Resource not found: %v", err) case errors.Is(err, ErrPromptNotFound): log.Printf("Prompt not found: %v", err) case errors.Is(err, ErrToolNotFound): log.Printf("Tool not found: %v", err) } }) ``` We've also added examples for testing scenarios: ```go // Create a channel to receive errors for testing errChan := make(chan error, 1) // Register hook to capture and inspect errors hooks := &Hooks{} hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) { // For capability-related errors if errors.Is(err, ErrUnsupported) { // Handle capability not supported errChan <- err return } // For parsing errors var parseErr = &UnparseableMessageError{} if errors.As(err, &parseErr) { // Handle unparseable message errors fmt.Printf("Failed to parse %s request: %v\n", parseErr.GetMethod(), parseErr.Unwrap()) errChan <- parseErr return } // For resource/prompt/tool not found errors if errors.Is(err, ErrResourceNotFound) || errors.Is(err, ErrPromptNotFound) || errors.Is(err, ErrToolNotFound) { // Handle not found errors errChan <- err return } // For other errors errChan <- err }) ``` These improvements make the MCP server more robust and user-friendly by providing clear error patterns and detailed documentation.
1 parent 38dfe31 commit 5203a3d

File tree

8 files changed

+373
-75
lines changed

8 files changed

+373
-75
lines changed

server/callbacks.go renamed to server/hooks.go

Lines changed: 90 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/internal/gen/data.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ var MCPRequestTypes = []MCPRequestType{
1818
ParamType: "InitializeRequest",
1919
ResultType: "InitializeResult",
2020
HookName: "Initialize",
21-
UnmarshalError: "Invalid initialize request",
21+
UnmarshalError: "invalid initialize request",
2222
HandlerFunc: "handleInitialize",
2323
}, {
2424
MethodName: "MethodPing",
2525
ParamType: "PingRequest",
2626
ResultType: "EmptyResult",
2727
HookName: "Ping",
28-
UnmarshalError: "Invalid ping request",
28+
UnmarshalError: "invalid ping request",
2929
HandlerFunc: "handlePing",
3030
}, {
3131
MethodName: "MethodResourcesList",
@@ -35,7 +35,7 @@ var MCPRequestTypes = []MCPRequestType{
3535
GroupName: "Resources",
3636
GroupHookName: "Resource",
3737
HookName: "ListResources",
38-
UnmarshalError: "Invalid list resources request",
38+
UnmarshalError: "invalid list resources request",
3939
HandlerFunc: "handleListResources",
4040
}, {
4141
MethodName: "MethodResourcesTemplatesList",
@@ -45,7 +45,7 @@ var MCPRequestTypes = []MCPRequestType{
4545
GroupName: "Resources",
4646
GroupHookName: "Resource",
4747
HookName: "ListResourceTemplates",
48-
UnmarshalError: "Invalid list resource templates request",
48+
UnmarshalError: "invalid list resource templates request",
4949
HandlerFunc: "handleListResourceTemplates",
5050
}, {
5151
MethodName: "MethodResourcesRead",
@@ -55,7 +55,7 @@ var MCPRequestTypes = []MCPRequestType{
5555
GroupName: "Resources",
5656
GroupHookName: "Resource",
5757
HookName: "ReadResource",
58-
UnmarshalError: "Invalid read resource request",
58+
UnmarshalError: "invalid read resource request",
5959
HandlerFunc: "handleReadResource",
6060
}, {
6161
MethodName: "MethodPromptsList",
@@ -65,7 +65,7 @@ var MCPRequestTypes = []MCPRequestType{
6565
GroupName: "Prompts",
6666
GroupHookName: "Prompt",
6767
HookName: "ListPrompts",
68-
UnmarshalError: "Invalid list prompts request",
68+
UnmarshalError: "invalid list prompts request",
6969
HandlerFunc: "handleListPrompts",
7070
}, {
7171
MethodName: "MethodPromptsGet",
@@ -75,7 +75,7 @@ var MCPRequestTypes = []MCPRequestType{
7575
GroupName: "Prompts",
7676
GroupHookName: "Prompt",
7777
HookName: "GetPrompt",
78-
UnmarshalError: "Invalid get prompt request",
78+
UnmarshalError: "invalid get prompt request",
7979
HandlerFunc: "handleGetPrompt",
8080
}, {
8181
MethodName: "MethodToolsList",
@@ -85,7 +85,7 @@ var MCPRequestTypes = []MCPRequestType{
8585
GroupName: "Tools",
8686
GroupHookName: "Tool",
8787
HookName: "ListTools",
88-
UnmarshalError: "Invalid list tools request",
88+
UnmarshalError: "invalid list tools request",
8989
HandlerFunc: "handleListTools",
9090
}, {
9191
MethodName: "MethodToolsCall",
@@ -95,7 +95,7 @@ var MCPRequestTypes = []MCPRequestType{
9595
GroupName: "Tools",
9696
GroupHookName: "Tool",
9797
HookName: "CallTool",
98-
UnmarshalError: "Invalid call tool request",
98+
UnmarshalError: "invalid call tool request",
9999
HandlerFunc: "handleToolCall",
100100
},
101101
}

server/internal/gen/hooks.go.tmpl

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package server
55
import (
66
"context"
77
"encoding/json"
8+
"errors"
89
"fmt"
910

1011
"github.com/mark3labs/mcp-go/mcp"
@@ -20,6 +21,36 @@ type OnAfterAnyHookFunc func(id any, method mcp.MCPMethod, message any, result a
2021

2122
// OnErrorHookFunc is a hook that will be called when an error occurs,
2223
// either during the request parsing or the method execution.
24+
//
25+
// Example usage:
26+
// ```
27+
// hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
28+
// // Check for specific error types using errors.Is
29+
// if errors.Is(err, ErrUnsupported) {
30+
// // Handle capability not supported errors
31+
// log.Printf("Capability not supported: %v", err)
32+
// }
33+
//
34+
// // Use errors.As to get specific error types
35+
// var parseErr = &UnparseableMessageError{}
36+
// if errors.As(err, &parseErr) {
37+
// // Access specific methods/fields of the error type
38+
// log.Printf("Failed to parse message for method %s: %v",
39+
// parseErr.GetMethod(), parseErr.Unwrap())
40+
// // Access the raw message that failed to parse
41+
// rawMsg := parseErr.GetMessage()
42+
// }
43+
//
44+
// // Check for specific resource/prompt/tool errors
45+
// switch {
46+
// case errors.Is(err, ErrResourceNotFound):
47+
// log.Printf("Resource not found: %v", err)
48+
// case errors.Is(err, ErrPromptNotFound):
49+
// log.Printf("Prompt not found: %v", err)
50+
// case errors.Is(err, ErrToolNotFound):
51+
// log.Printf("Tool not found: %v", err)
52+
// }
53+
// })
2354
type OnErrorHookFunc func(id any, method mcp.MCPMethod, message any, err error)
2455

2556
{{range .}}
@@ -45,6 +76,50 @@ func (c *Hooks) AddAfterAny(hook OnAfterAnyHookFunc) {
4576
c.OnAfterAny = append(c.OnAfterAny, hook)
4677
}
4778

79+
// AddOnError registers a hook function that will be called when an error occurs.
80+
// The error parameter contains the actual error object, which can be interrogated
81+
// using Go's error handling patterns like errors.Is and errors.As.
82+
//
83+
// Example:
84+
// ```
85+
// // Create a channel to receive errors for testing
86+
// errChan := make(chan error, 1)
87+
//
88+
// // Register hook to capture and inspect errors
89+
// hooks := &Hooks{}
90+
// hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
91+
// // For capability-related errors
92+
// if errors.Is(err, ErrUnsupported) {
93+
// // Handle capability not supported
94+
// errChan <- err
95+
// return
96+
// }
97+
//
98+
// // For parsing errors
99+
// var parseErr = &UnparseableMessageError{}
100+
// if errors.As(err, &parseErr) {
101+
// // Handle unparseable message errors
102+
// fmt.Printf("Failed to parse %s request: %v\n",
103+
// parseErr.GetMethod(), parseErr.Unwrap())
104+
// errChan <- parseErr
105+
// return
106+
// }
107+
//
108+
// // For resource/prompt/tool not found errors
109+
// if errors.Is(err, ErrResourceNotFound) ||
110+
// errors.Is(err, ErrPromptNotFound) ||
111+
// errors.Is(err, ErrToolNotFound) {
112+
// // Handle not found errors
113+
// errChan <- err
114+
// return
115+
// }
116+
//
117+
// // For other errors
118+
// errChan <- err
119+
// })
120+
//
121+
// server := NewMCPServer("test-server", "1.0.0", WithHooks(hooks))
122+
// ```
48123
func (c *Hooks) AddOnError(hook OnErrorHookFunc) {
49124
c.OnError = append(c.OnError, hook)
50125
}
@@ -67,6 +142,20 @@ func (c *Hooks) afterAny(id any, method mcp.MCPMethod, message any, result any)
67142
}
68143
}
69144

145+
// onError calls all registered error hooks with the error object.
146+
// The err parameter contains the actual error that occurred, which implements
147+
// the standard error interface and may be a wrapped error or custom error type.
148+
//
149+
// This allows consumer code to use Go's error handling patterns:
150+
// - errors.Is(err, ErrUnsupported) to check for specific sentinel errors
151+
// - errors.As(err, &customErr) to extract custom error types
152+
//
153+
// Common error types include:
154+
// - ErrUnsupported: When a capability is not enabled
155+
// - UnparseableMessageError: When request parsing fails
156+
// - ErrResourceNotFound: When a resource is not found
157+
// - ErrPromptNotFound: When a prompt is not found
158+
// - ErrToolNotFound: When a tool is not found
70159
func (c *Hooks) onError(id any, method mcp.MCPMethod, message any, err error) {
71160
if c == nil {
72161
return

server/internal/gen/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"os/exec"
1010
"path/filepath"
11+
"strings"
1112
"text/template"
1213
)
1314

@@ -29,7 +30,9 @@ func RenderTemplateToFile(templateContent, destPath, fileName string, data any)
2930
defer os.Remove(tempFilePath) // Clean up temp file when done
3031

3132
// Parse and execute template to temp file
32-
tmpl, err := template.New(fileName).Parse(templateContent)
33+
tmpl, err := template.New(fileName).Funcs(template.FuncMap{
34+
"toLower": strings.ToLower,
35+
}).Parse(templateContent)
3336
if err != nil {
3437
tempFile.Close()
3538
return err

server/internal/gen/request_handler.go.tmpl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package server
55
import (
66
"context"
77
"encoding/json"
8+
"errors"
89
"fmt"
910

1011
"github.com/mark3labs/mcp-go/mcp"
@@ -64,13 +65,13 @@ func (s *MCPServer) HandleMessage(
6465
err = &requestError{
6566
id: baseMessage.ID,
6667
code: mcp.METHOD_NOT_FOUND,
67-
err: "{{.GroupName}} not supported",
68+
err: fmt.Errorf("{{toLower .GroupName}} %w", ErrUnsupported),
6869
}
69-
} else{{ end }} if json.Unmarshal(message, &request) != nil {
70+
} else{{ end }} if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
7071
err = &requestError{
7172
id: baseMessage.ID,
7273
code: mcp.INVALID_REQUEST,
73-
err: "{{.UnmarshalError}}",
74+
err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
7475
}
7576
} else {
7677
s.hooks.before{{.HookName}}(baseMessage.ID, &request)

0 commit comments

Comments
 (0)