Skip to content

Commit e183dd1

Browse files
authored
Feature: Request Hooks (mark3labs#62)
* Add Callbacks Feature MCPServer is upgraded with the capacity to notify before and/or after requests run. Developers can choose Before / After events, and can submit callbacks that are notified during EVERY request lifecycle, or else specific request methods. The specific request method callbacks receive typed message and/or result objects. A special OnError callback is also provided. To accomplish this with a minimal margin for error, a bit of a refactoring was done: - The `MCPServer.HandleMessage` method was moved from server.go into its own file. - The individual request handlers' prototypes were changed. rather than returning `mcp.JSONRPCMessage`, they return a tuple of their specific result type and error. - A code generation schema was introduced, to facilitate adding functionality to each clause in the switch statement within `HandleMessage()`. - Now that HandleMessage() now receives a typed result object and possible error, it is able to pass those results to any configured callbacks. I considered several options including a strategy pattern to make HandleMessage() more DRY, but opted to stick with the minimal reorganization of the code, and to keep the algorithm as flat and obvious as possible. I think the codegen mitigates the repetitiveness of that section of code. * Added documentation to MCP method descriptions * Rename Callbacks --> Hooks Reinstate test * 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. * Correct the indirection of message propagation Added tests to verify that messages are propagated to the BeforeAny and AfterAny hooks * Rename AfterAny --> OnSuccess to clarify convention
1 parent efc428d commit e183dd1

File tree

13 files changed

+1683
-248
lines changed

13 files changed

+1683
-248
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,18 @@ Prompts can include:
509509

510510
For examples, see the `examples/` directory.
511511

512+
## Extras
513+
514+
### Request Hooks
515+
516+
Hook into the request lifecycle by creating a `Hooks` object with your
517+
selection among the possible callbacks. This enables telemetry across all
518+
functionality, and observability of various facts, for example the ability
519+
to count improperly-formatted requests, or to log the agent identity during
520+
initialization.
521+
522+
Add the `Hooks` to the server at the time of creation using the
523+
`server.WithHooks` option.
512524

513525
## Contributing
514526

examples/everything/main.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,38 @@ const (
2929
)
3030

3131
func NewMCPServer() *server.MCPServer {
32+
33+
hooks := &server.Hooks{}
34+
35+
hooks.AddBeforeAny(func(id any, method mcp.MCPMethod, message any) {
36+
fmt.Printf("beforeAny: %s, %v, %v\n", method, id, message)
37+
})
38+
hooks.AddOnSuccess(func(id any, method mcp.MCPMethod, message any, result any) {
39+
fmt.Printf("onSuccess: %s, %v, %v, %v\n", method, id, message, result)
40+
})
41+
hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
42+
fmt.Printf("onError: %s, %v, %v, %v\n", method, id, message, err)
43+
})
44+
hooks.AddBeforeInitialize(func(id any, message *mcp.InitializeRequest) {
45+
fmt.Printf("beforeInitialize: %v, %v\n", id, message)
46+
})
47+
hooks.AddAfterInitialize(func(id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
48+
fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
49+
})
50+
hooks.AddAfterCallTool(func(id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
51+
fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
52+
})
53+
hooks.AddBeforeCallTool(func(id any, message *mcp.CallToolRequest) {
54+
fmt.Printf("beforeCallTool: %v, %v\n", id, message)
55+
})
56+
3257
mcpServer := server.NewMCPServer(
3358
"example-servers/everything",
3459
"1.0.0",
3560
server.WithResourceCapabilities(true, true),
3661
server.WithPromptCapabilities(true),
3762
server.WithLogging(),
63+
server.WithHooks(hooks),
3864
)
3965

4066
mcpServer.AddResource(mcp.NewResource("test://static/resource",

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ go 1.23
55
require (
66
github.com/google/uuid v1.6.0
77
github.com/stretchr/testify v1.9.0
8+
github.com/yosida95/uritemplate/v3 v3.0.2
89
)
910

1011
require (
1112
github.com/davecgh/go-spew v1.1.1 // indirect
1213
github.com/pmezard/go-difflib v1.0.0 // indirect
13-
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
1414
gopkg.in/yaml.v3 v3.0.1 // indirect
1515
)

mcp/types.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,46 @@ import (
88
"github.com/yosida95/uritemplate/v3"
99
)
1010

11+
type MCPMethod string
12+
13+
const (
14+
// Initiates connection and negotiates protocol capabilities.
15+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization
16+
MethodInitialize MCPMethod = "initialize"
17+
18+
// Verifies connection liveness between client and server.
19+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/ping/
20+
MethodPing MCPMethod = "ping"
21+
22+
// Lists all available server resources.
23+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
24+
MethodResourcesList MCPMethod = "resources/list"
25+
26+
// Provides URI templates for constructing resource URIs.
27+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
28+
MethodResourcesTemplatesList MCPMethod = "resources/templates/list"
29+
30+
// Retrieves content of a specific resource by URI.
31+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
32+
MethodResourcesRead MCPMethod = "resources/read"
33+
34+
// Lists all available prompt templates.
35+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/
36+
MethodPromptsList MCPMethod = "prompts/list"
37+
38+
// Retrieves a specific prompt template with filled parameters.
39+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/
40+
MethodPromptsGet MCPMethod = "prompts/get"
41+
42+
// Lists all available executable tools.
43+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
44+
MethodToolsList MCPMethod = "tools/list"
45+
46+
// Invokes a specific tool with provided parameters.
47+
// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
48+
MethodToolsCall MCPMethod = "tools/call"
49+
)
50+
1151
type URITemplate struct {
1252
*uritemplate.Template
1353
}

0 commit comments

Comments
 (0)