Skip to content

Feature: Request Hooks #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 24, 2025
Merged

Conversation

tylergannon
Copy link
Contributor

@tylergannon tylergannon commented Mar 21, 2025

MCPServer is upgraded with the capacity to notify before and/or after requests run. This will be really helpful:

  • Allows collecting statistics on various types of actions, and this allows to avoid having to add analytics code into every tool definition.
  • This opens up the possibility of knowing how often resources or prompts are used.
  • Also enables the MCP implementation to report on which agent has invoked it, and possibly behave differently in each case (Cursor vs Claude Desktop, etc).

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.

Summary by CodeRabbit

  • New Features
    • Introduced an extensible hooks system for enhanced monitoring, logging, and error reporting during request processing.
    • Improved handling of incoming messages, resulting in a more robust server response mechanism.
  • Documentation
    • Added a new “Extras” section to the user guide outlining how to configure and leverage request hooks.
  • Tests
    • Expanded test coverage to ensure reliable hook execution and to validate the improved error handling mechanisms.

Copy link
Contributor

coderabbitai bot commented Mar 21, 2025

Walkthrough

This pull request introduces a hooks system to the MCP server to facilitate customized logging, telemetry, and error handling during request processing. It adds comprehensive documentation in the README and new type definitions for MCP methods. The changes span modifications in server handling logic with structured error types, integration of hooks via a new WithHooks function, and enhanced test cases to validate the new functionality. Additionally, dependency management in go.mod is updated to reflect the direct usage of an external package.

Changes

File(s) Change Summary
README.md Added a new "Extras" section with a "Request Hooks" subsection that documents how to create and integrate a Hooks object for observability and logging.
go.mod Updated dependency handling by changing github.com/yosida95/uritemplate/v3 v3.0.2 from indirect to a direct requirement.
mcp/types.go Introduced the MCPMethod type and corresponding constants (e.g., MethodInitialize, MethodPing, etc.) to improve type safety in referencing MCP methods.
examples/everything/main.go, server/hooks.go Added a hooks system with various callback types (OnBeforeAny, OnAfterAny, OnError, etc.) and demonstrated its usage by initializing and integrating a hooks object within server creation.
server/request_handler.go, server/server.go Refactored JSON-RPC message processing by adding structured error types (e.g., UnparseableMessageError), updating handler function signatures, and integrating hooks via WithHooks.
server/server_test.go Modified tests to validate the updated error handling, adjusted type assertions (from pointer to value), and added tests (e.g., TestMCPServer_WithHooks) for the new hooks system.

Possibly related PRs

  • Add tool capabilities to serverCapabilities #36: The changes in the main PR, which introduce a hooks system for request processing, are related to the modifications in the retrieved PR that also involve changes to the server/server.go file, specifically in how capabilities are managed and processed.
  • Fix tool calls #38: The changes in the main PR, which introduce a hooks system for request handling, are related to the modifications in the retrieved PR that also involve the HandleMessage method in the server.go file, as both PRs focus on enhancing the server's request processing capabilities.

Suggested reviewers

  • ezynda3

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f06109 and 252237e.

⛔ Files ignored due to path filters (1)
  • server/internal/gen/hooks.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (4)
  • examples/everything/main.go (1 hunks)
  • server/hooks.go (1 hunks)
  • server/server.go (12 hunks)
  • server/server_test.go (17 hunks)
🧰 Additional context used
🧬 Code Definitions (4)
server/server_test.go (5)
server/request_handler.go (12) (12)
  • result (61-61)
  • result (80-80)
  • err (20-20)
  • request (60-60)
  • request (79-79)
  • request (98-98)
  • request (123-123)
  • request (148-148)
  • request (173-173)
  • request (198-198)
  • request (223-223)
  • request (248-248)
mcp/types.go (6) (6)
  • Result (173-177)
  • MCPMethod (11-11)
  • t (55-57)
  • t (59-70)
  • JSONRPCResponse (198-202)
  • JSONRPCError (205-218)
mcp/prompts.go (1) (1)
  • GetPromptResult (32-37)
server/hooks.go (1) (1)
  • Hooks (79-101)
server/server.go (3) (3)
  • NewMCPServer (324-350)
  • UnparseableMessageError (70-74)
  • ServerOption (29-29)
examples/everything/main.go (4)
server/hooks.go (1) (1)
  • Hooks (79-101)
mcp/types.go (2) (2)
  • InitializeRequest (266-275)
  • InitializeResult (279-293)
mcp/tools.go (2) (2)
  • CallToolRequest (44-59)
  • CallToolResult (34-41)
server/server.go (2) (2)
  • NewMCPServer (324-350)
  • WithHooks (283-287)
server/hooks.go (1)
server/request_handler.go (10) (10)
  • result (61-61)
  • result (80-80)
  • result (99-99)
  • result (124-124)
  • result (149-149)
  • result (174-174)
  • result (199-199)
  • result (224-224)
  • result (249-249)
  • err (20-20)
server/server.go (5)
mcp/types.go (9) (9)
  • InitializeResult (279-293)
  • EmptyResult (232-232)
  • ListResourcesRequest (400-402)
  • ListResourcesResult (406-409)
  • ListResourceTemplatesRequest (413-415)
  • ListResourceTemplatesResult (419-422)
  • ReadResourceRequest (426-435)
  • ReadResourceResult (439-442)
  • Params (104-104)
server/request_handler.go (12) (12)
  • err (20-20)
  • result (61-61)
  • result (80-80)
  • request (60-60)
  • request (79-79)
  • request (98-98)
  • request (123-123)
  • request (148-148)
  • request (173-173)
  • request (198-198)
  • request (223-223)
  • request (248-248)
server/hooks.go (1) (1)
  • Hooks (79-101)
mcp/prompts.go (3) (3)
  • ListPromptsResult (13-16)
  • GetPromptRequest (20-28)
  • GetPromptResult (32-37)
mcp/tools.go (4) (4)
  • ListToolsRequest (13-15)
  • ListToolsResult (19-22)
  • CallToolRequest (44-59)
  • CallToolResult (34-41)
🔇 Additional comments (11)
server/hooks.go (1)

1-443: Well-structured hooks system implementation.

This file introduces a complete hooks system for the MCP server with consistent patterns for all request types. The approach of having both general hooks (BeforeAny, OnSuccess, OnError) and method-specific hooks provides excellent flexibility. The error handling documentation is particularly thorough with practical examples.

The hooks implementation pattern is consistent throughout the file, making it maintainable and extensible. Each hook follows the same null-checking pattern and appropriate type signatures.

server/server_test.go (3)

1151-1307: Thorough test coverage for the hooks system.

The test function provides comprehensive verification of the hooks functionality by testing:

  1. Multiple hook types (before/after, general and method-specific)
  2. Hook execution counts
  3. Parameter passing between hooks
  4. Error handling through hooks

Consider adding a comment at the beginning of this function explaining its purpose and test strategy for better readability.


667-730: Properly modified test cases for type assertion changes.

The test cases have been updated to use value type assertions (mcp.GetPromptResult) instead of pointer type assertions (*mcp.GetPromptResult). This is consistent with the changes in the handler function return types.


668-702: Enhanced error validation in test cases.

Test cases now include validation of the specific error types using errors.Is and errors.As, which is a more robust approach to error handling than simple string matching.

examples/everything/main.go (2)

33-55: Great practical example of the hooks system.

This code effectively demonstrates how to use the hooks system for logging request lifecycle events. Both general-purpose hooks and method-specific hooks are showcased.

Consider adding brief comments above each hook registration to explain the specific purpose or benefit of that hook type for better documentation.


63-63: Proper integration of hooks with server creation.

The hooks are correctly passed to the server via the WithHooks option, showing the intended usage pattern.

server/server.go (5)

68-121: Improved error handling with structured error types.

The new error types UnparseableMessageError and enhanced requestError provide better error handling capabilities:

  1. Both implement proper error interfaces including Unwrap() for error chain inspection
  2. UnparseableMessageError includes the raw message and method for better debugging
  3. The ToJSONRPCError method provides clean conversion to the protocol's error format

This structured approach allows for more precise error handling using Go's errors.Is and errors.As patterns.


123-128: Well-defined standard error values.

The standard error values (ErrUnsupported, ErrResourceNotFound, etc.) enable consistent error checking with errors.Is() across the codebase. This is a good practice that improves error handling consistency.


149-149: Added hooks support to MCPServer.

The addition of the hooks field to the MCPServer struct enables the integration of the hooks system, allowing for extending server functionality at various request lifecycle points.


280-287: Clean implementation of WithHooks option.

The WithHooks function follows the established pattern for server options, making the hooks system integration consistent with other server configuration options.


451-503: Improved handler function signature.

The handler functions now return specific result types and error objects instead of generic JSONRPCMessage objects. This provides better type safety and allows for more precise error handling.

This pattern is consistently applied across all handler functions, which improves code maintainability.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (9)
README.md (1)

519-519: Spelling/grammar nitpick: remove the hyphen in 'improperly-formatted' for standard usage.

“improperly” is an adverb, so the hyphen isn't needed. This is a purely stylistic point.

-... ability to count improperly-formatted requests ...
+... ability to count improperly formatted requests ...
🧰 Tools
🪛 LanguageTool

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

server/request_handler.go (1)

11-278: Consider modularizing repeated callback pre- and post- calls.

Each case block pattern (unmarshal → callback.before* → handle* → error check → callback.after*) is repeated. A small helper could reduce repetition and unify logic, though code generation partially addresses DRY concerns.

server/server_test.go (3)

1066-1080: Check for potential concurrency testing or parallel calls.
While this block correctly sets up callback counters and registers them, consider adding additional tests to ensure callback ordering and counts remain correct when multiple requests are processed in parallel. This will provide stronger guarantees of thread-safety and correctness in concurrent scenarios.


1081-1093: Log message structure refinement.
The callbacks use simple fmt.Printf statements, which may not be ideal in production-grade logging. Consider integrating a structured logger (e.g., logrus, zap) to capture callback context and severity levels consistently, facilitating easier analysis and troubleshooting.


1094-1110: Validate method-specific callback coverage.
The callbacks here cover only a few method-specific cases (Ping, ListTools). If you anticipate more methods in the future, consider adding further method-specific callbacks for thorough coverage or a generic fallback for unhandled methods.

examples/everything/main.go (1)

31-55: Guard against sensitive data leaks in log messages.
The callback functions utilize %v in fmt.Printf, which may unintentionally log sensitive data included within request parameters (e.g., user PII). In a production environment, consider sanitizing or filtering sensitive details before printing to logs.

Also applies to: 63-63

server/server.go (1)

85-85: Handle nil callbacks gracefully.
Although this pointer can remain nil if no callbacks are set, consider adding a quick safety check (e.g., if s.callbacks != nil) before calling any callback methods to prevent potential nil-pointer dereferences when referencing s.callbacks.

server/callbacks.go (2)

7-9: Clarify the doc comment for OnBeforeAnyCallbackFunc.
The current wording suggests this callback is invoked "after the request is parsed but before the method is called," which is correct. However, consider prefacing the comment with the function name (e.g., "OnBeforeAnyCallbackFunc is...") to align with standard Go doc conventions, ensuring clarity when generated documentation is viewed.


116-350: Repeated boilerplate for typed request callbacks.
Many methods (e.g., beforeInitialize, afterInitialize, beforePing, afterPing, etc.) follow a similar pattern. Although this is functional, it can become cumbersome to maintain. Consider a code generation approach or more generic abstraction to reduce duplication, improve readability, and centralize updates for typed callbacks.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 771de1d and 57d4c45.

⛔ Files ignored due to path filters (5)
  • server/internal/gen/README.md is excluded by !**/gen/**
  • server/internal/gen/callbacks.go.tmpl is excluded by !**/gen/**
  • server/internal/gen/data.go is excluded by !**/gen/**
  • server/internal/gen/main.go is excluded by !**/gen/**
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (8)
  • README.md (1 hunks)
  • examples/everything/main.go (1 hunks)
  • go.mod (1 hunks)
  • mcp/types.go (1 hunks)
  • server/callbacks.go (1 hunks)
  • server/request_handler.go (1 hunks)
  • server/server.go (10 hunks)
  • server/server_test.go (3 hunks)
🧰 Additional context used
🧬 Code Definitions (4)
server/request_handler.go (2)
server/server.go (20) (20)
  • s (100-105)
  • s (108-116)
  • s (119-123)
  • s (126-150)
  • s (153-179)
  • s (289-302)
  • s (305-318)
  • s (321-329)
  • s (332-334)
  • s (337-352)
  • s (355-360)
  • s (363-375)
  • s (378-385)
  • s (387-439)
  • s (441-447)
  • s (449-468)
  • MCPServer (71-86)
  • requestError (639-643)
  • createErrorResponse (715-732)
  • createResponse (707-713)
mcp/types.go (13) (13)
  • JSONRPCMessage (49-49)
  • MCPMethod (11-11)
  • JSONRPC_VERSION (55-55)
  • INVALID_REQUEST (197-197)
  • MethodInitialize (14-14)
  • InitializeRequest (240-249)
  • InitializeResult (253-267)
  • PingRequest (329-331)
  • EmptyResult (206-206)
  • ListResourcesRequest (374-376)
  • ListResourcesResult (380-383)
  • METHOD_NOT_FOUND (198-198)
  • MethodResourcesRead (18-18)
server/server_test.go (6)
server/request_handler.go (12) (12)
  • result (59-59)
  • result (78-78)
  • err (18-18)
  • request (58-58)
  • request (77-77)
  • request (96-96)
  • request (121-121)
  • request (146-146)
  • request (171-171)
  • request (196-196)
  • request (221-221)
  • request (246-246)
mcp/types.go (4) (4)
  • Result (147-151)
  • t (29-31)
  • t (33-44)
  • JSONRPCResponse (172-176)
mcp/prompts.go (1) (1)
  • GetPromptResult (32-37)
mcp/tools.go (3) (3)
  • t (82-104)
  • ListToolsResult (19-22)
  • CallToolResult (34-41)
server/callbacks.go (1) (1)
  • Callbacks (46-68)
server/server.go (1) (1)
  • NewMCPServer (260-286)
server/server.go (7)
server/callbacks.go (1) (1)
  • Callbacks (46-68)
mcp/types.go (9) (9)
  • InitializeResult (253-267)
  • EmptyResult (206-206)
  • ListResourcesRequest (374-376)
  • ListResourcesResult (380-383)
  • ListResourceTemplatesRequest (387-389)
  • ListResourceTemplatesResult (393-396)
  • ReadResourceRequest (400-409)
  • ReadResourceResult (413-416)
  • Params (78-78)
client/stdio.go (6) (6)
  • result (251-251)
  • result (301-301)
  • result (323-323)
  • result (369-369)
  • result (398-398)
  • result (435-435)
mcp/utils.go (2) (2)
  • result (468-468)
  • result (546-546)
server/request_handler.go (12) (12)
  • result (59-59)
  • result (78-78)
  • request (58-58)
  • request (77-77)
  • request (96-96)
  • request (121-121)
  • request (146-146)
  • request (171-171)
  • request (196-196)
  • request (221-221)
  • request (246-246)
  • err (18-18)
mcp/prompts.go (2) (2)
  • ListPromptsResult (13-16)
  • GetPromptRequest (20-28)
mcp/tools.go (4) (4)
  • ListToolsRequest (13-15)
  • ListToolsResult (19-22)
  • CallToolRequest (44-59)
  • CallToolResult (34-41)
server/callbacks.go (1)
mcp/types.go (20) (20)
  • MCPMethod (11-11)
  • InitializeRequest (240-249)
  • InitializeResult (253-267)
  • PingRequest (329-331)
  • EmptyResult (206-206)
  • ListResourcesRequest (374-376)
  • ListResourcesResult (380-383)
  • ListResourceTemplatesRequest (387-389)
  • ListResourceTemplatesResult (393-396)
  • ReadResourceRequest (400-409)
  • ReadResourceResult (413-416)
  • MethodInitialize (14-14)
  • MethodPing (15-15)
  • MethodResourcesList (16-16)
  • MethodResourcesTemplatesList (17-17)
  • MethodResourcesRead (18-18)
  • MethodPromptsList (19-19)
  • MethodPromptsGet (20-20)
  • MethodToolsList (21-21)
  • MethodToolsCall (22-22)
🪛 LanguageTool
README.md

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

🔇 Additional comments (8)
go.mod (1)

8-8:

✅ Verification successful

Pinning the dependency version looks good, but verify security and compatibility.

This new direct dependency on github.com/yosida95/uritemplate/v3 is appropriate for the callback feature. However, it's best practice to confirm the chosen version for security advisories and ensure it meets all use cases.

Below is a script to check for any known vulnerabilities or advisories regarding this module:


🏁 Script executed:

#!/bin/bash
# Description: Check for security advisories for yosida95/uritemplate and verify the latest available version on pkg.go.dev.

# Retrieve the module's information from pkg.go.dev
curl -s "https://pkg.go.dev/github.com/yosida95/uritemplate/v3?tab=versions" | grep "Version"
echo "Check if there are any GH security advisories for uritemplate..."
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: GO, package: "github.com/yosida95/uritemplate/v3") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

Length of output: 8681


Dependency Version Verified: Security & Compatibility Confirmed

The pinned dependency github.com/yosida95/uritemplate/v3 v3.0.2 has been verified via the security advisory check and the package information available on pkg.go.dev. No vulnerabilities were reported by the GH API query, and the module appears to fully support the use case for the callback feature.

README.md (1)

512-523: Great addition: “Extras” and “Request Callbacks” documentation.

This new section is concise and well-defined, showing how to integrate callbacks into the server. It clearly describes the purpose of callbacks (telemetry, logging, error handling) and how to attach them using server.WithCallbacks, helping developers adopt the feature without confusion.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

mcp/types.go (1)

11-23: String-based method constants enhance clarity.

Defining MCPMethod as a string type and enumerating all recognized method constants improves type safety and readability. This approach avoids “magic strings” and facilitates future maintenance.

server/request_handler.go (3)

11-54: Notification handling is correctly segregated from request/response flow.

The check for baseMessage.ID == nil and subsequent unmarshal into a notification struct is a clean, JSON-RPC-compliant approach. Returning nil for notifications avoids sending unnecessary responses.


56-70: Initialize request handling logic looks consistent.

Unmarshaling the request and invoking handleInitialize under typed callbacks fits well with the new callback design. The structured error handling with requestError ensures clear feedback if the request is invalid.


96-119: Graceful fallback when resources are unavailable.

Returning METHOD_NOT_FOUND if resources capabilities are absent is sensible. This switch-case extension ensures the contract is respected while providing explicit feedback to clients about unsupported features.

server/server_test.go (1)

1134-1183: Ensure error callback logic aligns with the success callback flow.
In lines 1177–1179, the afterAny callback is not called for failed methods, while the onError callback is invoked exactly once for the invalid tool. This approach is valid, but confirm whether you need the afterAny callback to run even for failed requests (e.g., for cleanup or final logging).

server/server.go (1)

216-224: Approve addition of WithCallbacks configuration.
This new option neatly integrates callback functionality into the server’s initialization flow, providing a clean, composable design for hooking into request lifecycle events. No issues noted.

@tylergannon tylergannon force-pushed the server-callbacks branch 5 times, most recently from 2e9e3d7 to 35b3345 Compare March 21, 2025 06:06
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
README.md (1)

517-520: Remove the hyphen to follow standard usage for adverb + adjective combinations.
Since "improperly" is an adverb ending in -ly, the hyphen before "formatted" is unnecessary:

-  to count improperly-formatted requests, or to log the agent identity
+  to count improperly formatted requests, or to log the agent identity
🧰 Tools
🪛 LanguageTool

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

server/server.go (1)

585-586: Remove debug print statement.

This fmt.Println statement appears to be debugging code that should be removed or replaced with proper logging before merging to production.

-fmt.Println("request.Params.Name", request.Params.Name)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57d4c45 and 35b3345.

⛔ Files ignored due to path filters (5)
  • server/internal/gen/README.md is excluded by !**/gen/**
  • server/internal/gen/callbacks.go.tmpl is excluded by !**/gen/**
  • server/internal/gen/data.go is excluded by !**/gen/**
  • server/internal/gen/main.go is excluded by !**/gen/**
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (8)
  • README.md (1 hunks)
  • examples/everything/main.go (1 hunks)
  • go.mod (1 hunks)
  • mcp/types.go (1 hunks)
  • server/callbacks.go (1 hunks)
  • server/request_handler.go (1 hunks)
  • server/server.go (10 hunks)
  • server/server_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • go.mod
  • mcp/types.go
  • server/server_test.go
  • server/request_handler.go
🧰 Additional context used
🧬 Code Definitions (1)
server/server.go (3)
server/callbacks.go (1) (1)
  • Callbacks (48-70)
mcp/types.go (12) (12)
  • InitializeResult (253-267)
  • PingRequest (329-331)
  • EmptyResult (206-206)
  • ListResourcesRequest (374-376)
  • ListResourcesResult (380-383)
  • ListResourceTemplatesRequest (387-389)
  • ListResourceTemplatesResult (393-396)
  • ReadResourceRequest (400-409)
  • ReadResourceResult (413-416)
  • Params (78-78)
  • JSONRPCError (179-192)
  • JSONRPC_VERSION (55-55)
server/request_handler.go (12) (12)
  • result (61-61)
  • result (80-80)
  • request (60-60)
  • request (79-79)
  • request (98-98)
  • request (123-123)
  • request (148-148)
  • request (173-173)
  • request (198-198)
  • request (223-223)
  • request (248-248)
  • err (20-20)
🪛 LanguageTool
README.md

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

🔇 Additional comments (7)
server/callbacks.go (2)

48-70: Consider concurrency safety when managing callback slices.
This mirrors a previous comment stating that if callbacks can be registered or modified at runtime, you may face data races when reading them concurrently. Apply a concurrency-safe approach such as using read-write locks or another synchronization mechanism if runtime modifications are anticipated.


88-99: Avoid passing &message to callbacks expecting a direct value.
This code still passes &message instead of message, repeating the existing pointer-mismatch concern. The callback signatures expect message any, not *any.

examples/everything/main.go (1)

35-55:

❓ Verification inconclusive

Verify safe handling of potentially sensitive request data in logs.
You're logging all messages and results through fmt.Printf. These may contain user data, raising privacy and security concerns. Please confirm whether this logging is appropriate in production environments.

Below is a script to locate all fmt.Printf calls throughout the repository so you can review them for sensitive data leakage:


🏁 Script executed:

#!/bin/bash
rg "fmt\.Printf" -A 2

Length of output: 2767


Attention: Potential Sensitive Data Exposure in Logging

The code registers several callbacks that use fmt.Printf to log dynamic data (e.g., message, id, result). Our repository-wide search confirms similar patterns across multiple files (notably in examples/everything/main.go and other modules). These log entries may inadvertently expose sensitive user data if they remain active in production.

  • Location to Review:
    • examples/everything/main.go (lines 35–55)
    • Other occurrences in files like client/stdio.go and client/sse.go

Please verify whether this logging behavior is intended for production use. If these logs are only meant for debugging purposes, consider conditioning them on an environment flag or migrating to a structured logging framework that supports log sanitization and redaction.

server/server.go (4)

85-85: LGTM! The addition of callbacks field to MCPServer struct.

The new callbacks field allows the server to support the callbacks feature described in the PR objectives. This design enables notifying users before/after requests are executed, facilitating statistics collection and agent identification.


216-223: Well-designed callback configuration function.

The WithCallbacks function follows the established pattern of other server configuration options and has clear documentation. It integrates seamlessly with the existing server option architecture.


387-439: Excellent refactoring of handler return types.

The handler functions now return specific result types and a requestError instead of the generic mcp.JSONRPCMessage. This refactoring:

  1. Improves type safety by using concrete types
  2. Makes the code more maintainable
  3. Enables the callbacks feature by allowing typed results to be passed to callbacks
  4. Creates a consistent error handling pattern across all handlers

This change directly supports the PR objective of modifying request handler prototypes to return their specific result type and error.

Also applies to: 441-447, 449-468, 470-489, 491-548, 555-574, 576-605, 607-637, 664-691


639-662: Well-designed error handling type.

The requestError type effectively encapsulates the error information needed for JSON-RPC responses. The implementation includes:

  1. Fields for ID, error code, and error message
  2. Implementation of the standard Go error interface
  3. A method to convert to the JSON-RPC error format

This structured approach to error handling is cleaner than the previous direct creation of error responses.

@ezynda3
Copy link
Contributor

ezynda3 commented Mar 21, 2025

Hi @tylergannon I really like this idea and think it's a killer feature to have. With the risk of being nitpicky though, could we rename this to Hooks instead of Callbacks? Just a preference and think it matches with the naming I've seen in other projects as well.

@tylergannon
Copy link
Contributor Author

@ezynda3 Done 👍🏽 Also added additional documentation comments and a more structured error messaging to allow the OnError callback better responses:

Add error type handling and documentation for MCP server

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

Error Types and Usage

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.

Code Examples

The documentation now includes examples like:

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:

// 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
server/hooks.go (3)

9-11: Consider using consistent naming for hook function parameters.
Currently, these types use a generic id any, method mcp.MCPMethod, message any signature for the different hooks. Although flexible, it may become harder to maintain if types vary for different hooks. Consider introducing stronger types or documented interfaces for clarity and safer refactoring.


178-199: Ensure adequate error context is provided.
The onError method nicely forwards the error to all hooks. However, consider including additional context (e.g., server name, timestamp, user info) for more comprehensive diagnostics if the error is to be stored or escalated.


200-442: Recognize repetitive pattern across before and after methods.
Most of these hooks follow a repetitive pattern of calling beforeAny/afterAny plus a local slice iteration. Consider abstracting common functionality into a helper to reduce boilerplate and simplify future maintenance.

README.md (1)

516-520: Remove the hyphen from “improperly-formatted requests.”
According to style guides, an adverb ending in “-ly” (e.g. “improperly”) does not typically require a hyphen before an adjective.

- to count improperly-formatted requests
+ to count improperly formatted requests
🧰 Tools
🪛 LanguageTool

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

examples/everything/main.go (2)

33-43: Validate log output vs. production readiness.
These hooks use fmt.Printf to print diagnostic messages. Consider using structured logging or a centralized logger for consistent formatting, severity levels, or log forwarding in production environments.


63-63: Suggest forming dedicated logging hooks in a separate function.
All hooks are defined inline in NewMCPServer. Extracting them into a dedicated helper (e.g., func newLoggingHooks() *server.Hooks) may improve readability and testability.

server/server.go (2)

649-650: Remove debug print statement.

The fmt.Println statement should be removed as it appears to be debugging code that was accidentally left in.

-	fmt.Println("request.Params.Name", request.Params.Name)
-

731-770: Consider updating notification handler for consistency.

While most handler functions have been updated to return typed results, the handleNotification function still returns a general mcp.JSONRPCMessage. For consistency, consider updating this handler to follow the same pattern as the other handlers.

- func (s *MCPServer) handleNotification(
-	ctx context.Context,
-	notification mcp.JSONRPCNotification,
- ) mcp.JSONRPCMessage {
+ func (s *MCPServer) handleNotification(
+	ctx context.Context,
+	notification mcp.JSONRPCNotification,
+ ) (*mcp.EmptyResult, *requestError) {
	s.mu.RLock()
	handler, ok := s.notificationHandlers[notification.Method]
	s.mu.RUnlock()

	if ok {
		handler(ctx, notification)
	}
-	return nil
+	return &mcp.EmptyResult{}, nil
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35b3345 and 5203a3d.

⛔ Files ignored due to path filters (5)
  • server/internal/gen/README.md is excluded by !**/gen/**
  • server/internal/gen/data.go is excluded by !**/gen/**
  • server/internal/gen/hooks.go.tmpl is excluded by !**/gen/**
  • server/internal/gen/main.go is excluded by !**/gen/**
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (7)
  • README.md (1 hunks)
  • examples/everything/main.go (1 hunks)
  • mcp/types.go (1 hunks)
  • server/hooks.go (1 hunks)
  • server/request_handler.go (1 hunks)
  • server/server.go (12 hunks)
  • server/server_test.go (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/request_handler.go
  • server/server_test.go
  • mcp/types.go
🧰 Additional context used
🧬 Code Definitions (2)
server/server.go (4)
mcp/types.go (2) (2)
  • JSONRPCError (205-218)
  • Params (104-104)
server/hooks.go (1) (1)
  • Hooks (79-101)
mcp/prompts.go (1) (1)
  • GetPromptRequest (20-28)
mcp/tools.go (1) (1)
  • CallToolRequest (44-59)
server/hooks.go (2)
server/request_handler.go (10) (10)
  • result (61-61)
  • result (80-80)
  • result (99-99)
  • result (124-124)
  • result (149-149)
  • result (174-174)
  • result (199-199)
  • result (224-224)
  • result (249-249)
  • err (20-20)
server/server_test.go (2) (2)
  • err (919-919)
  • _ (1141-1141)
🪛 LanguageTool
README.md

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

🔇 Additional comments (18)
server/hooks.go (1)

79-101: Validate potential nil usage of Hooks.
The Hooks struct organizes all hooks in slices, but some methods and internal calls rely on c == nil checks. Ensure you consistently handle or document how an uninitialized Hooks pointer is used to avoid unexpected nil pointer dereferences.

examples/everything/main.go (2)

44-55: Confirm initialization ordering.
The beforeInitialize and afterInitialize hooks are defined but ensure that hooks requiring data from initialization run strictly after the server instance is fully set up. If additional dependencies need initialization, consider clarifying the order in documentation.


50-52: Proactively verify potential concurrency.
Hooks like AddAfterCallTool and AddBeforeCallTool might be invoked concurrently if multiple requests arrive. Confirm that the code within these hook callbacks is safe for concurrent execution, or add synchronization where needed.

Also applies to: 53-55

server/server.go (15)

68-90: Good addition of structured error handling for unparseable messages.

The UnparseableMessageError type provides a robust way to handle JSON unmarshalling failures while preserving the original message and method information. This is a good improvement that makes error handling more explicit and structured.


92-121: Improved error handling with proper error wrapping.

Updating requestError to include an error field instead of a string is a good practice. The implementation of Error(), ToJSONRPCError(), and Unwrap() methods follows Go's error handling conventions well and enables error inspection via errors.Is and errors.As.


123-128: Well-defined error constants.

Defining common error sentinels like ErrResourceNotFound makes error handling more consistent throughout the codebase and enables callers to check for specific error conditions using errors.Is.


149-149: Good addition of hooks field to MCPServer struct.

The hooks field enables the callback functionality described in the PR objectives, which will allow for telemetry, logging, and other cross-cutting concerns.


280-287: Well-implemented hooks configuration function.

The WithHooks function follows the established pattern for server options in this codebase, making it consistent with other configuration functions. The comment provides clear documentation on its purpose.


451-503: Handler signature refactored to support hooks.

The handleInitialize method now returns a typed result (*mcp.InitializeResult) and error (*requestError) instead of a generic mcp.JSONRPCMessage. This change supports the hooks system by providing typed objects that can be passed to callbacks.


505-511: Simplified ping handler with proper return type.

The ping handler now returns a properly typed empty result, consistent with the other handler changes.


513-532: List resources handler properly returns typed result.

The handler correctly implements the new return signature pattern, returning a typed result that can be used by hooks.


534-553: List resource templates handler follows new pattern.

This handler has been updated consistently with the other handlers to return a typed result.


555-612: Read resource handler with improved error handling.

The handler now returns specific error information using the requestError struct, which provides better context about the nature of failures. The error messages include appropriate wrapping of sentinel errors.


619-638: List prompts handler consistently updated.

This handler follows the same pattern as the other handlers, returning a typed result.


640-669: Get prompt handler with better error handling.

Aside from the debug print statement, this handler has been properly updated to the new pattern with improved error handling.


671-701: List tools handler follows consistent pattern.

This handler has been updated to return a typed result in line with the other handlers.


702-729: Tool call handler with proper error propagation.

The handler now returns appropriate error information when a tool is not found or when the tool handler encounters an error.


4-14: Added error package import.

The addition of the errors import is necessary for the new error handling features like error wrapping and sentinel errors.

@tylergannon tylergannon changed the title Add Callbacks Feature Feature: Request Hooks Mar 21, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
server/hooks.go (2)

9-11: Consider strongly typed parameters
OnBeforeAnyHookFunc uses any for the message; while flexible, typed parameters can improve clarity and reduce casting overhead in hook implementations.


79-101: Large struct might benefit from grouping
While auto-generated, the Hooks struct is sizable, with many slices. Consider grouping method-specific hooks in related sub-structures (e.g., resourceHooks, promptHooks) for improved readability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5203a3d and 6c6275c.

⛔ Files ignored due to path filters (1)
  • server/internal/gen/hooks.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (2)
  • server/hooks.go (1 hunks)
  • server/server_test.go (12 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
server/server_test.go (6)
mcp/utils.go (16) (16)
  • result (468-468)
  • result (546-546)
  • _ (9-9)
  • _ (10-10)
  • _ (11-11)
  • _ (12-12)
  • _ (13-13)
  • _ (14-14)
  • _ (15-15)
  • _ (16-16)
  • _ (17-17)
  • _ (18-18)
  • _ (19-19)
  • _ (20-20)
  • _ (23-23)
  • _ (24-24)
mcp/types.go (5) (5)
  • Result (173-177)
  • MCPMethod (11-11)
  • t (55-57)
  • t (59-70)
  • METHOD_NOT_FOUND (224-224)
mcp/prompts.go (1) (1)
  • GetPromptResult (32-37)
server/hooks.go (1) (1)
  • Hooks (79-101)
server/server.go (4) (4)
  • NewMCPServer (324-350)
  • WithHooks (283-287)
  • ServerOption (29-29)
  • ErrUnsupported (124-124)
mcp/tools.go (4) (4)
  • t (82-104)
  • ListToolsResult (19-22)
  • CallToolRequest (44-59)
  • CallToolResult (34-41)
🔇 Additional comments (16)
server/hooks.go (7)

1-2: Generated disclaimers look good
This is the standard go:generate disclaimer plus reference to the source template.


13-15: Symmetry with OnBeforeAnyHookFunc is consistent
OnAfterAnyHookFunc follows the same signature pattern, preserving clarity for a post-execution hook. No issues spotted.


17-50: Descriptive documentation for OnErrorHookFunc
The docstring comprehensively demonstrates how to handle various errors and is a strong reference for implementers. Good job ensuring standard usage of errors.Is and errors.As.


160-167: beforeAny invocation logic is straightforward
Executes each registered “beforeAny” hook in sequence. No obvious concurrency or error-handling issues.


169-176: afterAny invocation logic is consistent
Executes “afterAny” hooks similarly. Looks good and mirrors the beforeAny approach.


201-443: Method-specific hook implementations align with code generation
All typed hooks (Initialize, Ping, ReadResource, etc.) cleanly wrap around the generic beforeAny/afterAny calls. The repetitive pattern is acceptable given code generation.


178-200:

✅ Verification successful

Double-check usage of afterAny call for errors
Calling c.afterAny(id, method, message, err) on error paths is a valid design choice but slightly unconventional (passing an error object as "result"). Verify that any “afterAny” hooks correctly handle this scenario.


🏁 Script executed:

#!/bin/bash
# Script to search afterAny references and check if they handle error types gracefully.
rg -A 5 "afterAny" 

Length of output: 6286


Confirmed handling of error paths in afterAny hooks

  • The tests in server/server_test.go confirm that the afterAny invocation with an error (i.e., passing err as the result) for error cases works as intended.
  • All registered hooks are correctly called, and type handling aligns with the design expectations.
  • Consider adding a clarifying comment or documentation note to highlight this unconventional, yet validated, design decision.
server/server_test.go (9)

6-6: Minor import change
Importing "errors" is fine. Appears consistent with usage in the rest of the test file.


616-616: Type assertion from interface
Casting resp.Result to mcp.GetPromptResult is safe given the prior validation. No issues spotted.


646-646: Matching pattern for the missing-argument path
Again casting to mcp.GetPromptResult is consistent with the “get prompts” flow. Looks correct.


668-675: Introducing error hooks in invalid messages test
Storing all errors in errs via AddOnError is a good approach to verify multiple error scenarios. Code is clear and well-scoped for test usage.


680-680: Flexible error validation function
Adding a validateErr callback in the test table fosters targeted checks for custom error types. Nice design for extensibility.


696-702: Proper usage of errors.As to detect UnparseableMessageError
The test accurately checks for the parsed method and raw message. Great demonstration of advanced error handling.


713-713: Resetting errs before each test
Ensures no leakage of errors between sub-tests. Good practice for clean test state.


734-812: Handling capabilities absence with hooks
This block tests methods missing their capabilities. It correctly checks for METHOD_NOT_FOUND and ensures ErrUnsupported surfaces, verifying hook-based error capturing. Nicely done.


1025-1182: Comprehensive “WithHooks” test
Validates that all hooks (BeforeAny, AfterAny, OnError, and method-specific) trigger as intended. This thorough test raises confidence in the new callback system.

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.
@tylergannon tylergannon marked this pull request as draft March 21, 2025 21:02
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.
Added tests to verify that messages are propagated to the BeforeAny
and AfterAny hooks
@tylergannon tylergannon marked this pull request as ready for review March 22, 2025 00:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
README.md (1)

519-519: Remove the hyphen in “improperly-formatted.”

According to style conventions, adverbs ending in “-ly” typically don’t require a hyphen before adjectives.

Here’s a proposed change:

- to count improperly-formatted requests
+ to count improperly formatted requests
🧰 Tools
🪛 LanguageTool

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c6275c and 0f06109.

⛔ Files ignored due to path filters (5)
  • server/internal/gen/README.md is excluded by !**/gen/**
  • server/internal/gen/data.go is excluded by !**/gen/**
  • server/internal/gen/hooks.go.tmpl is excluded by !**/gen/**
  • server/internal/gen/main.go is excluded by !**/gen/**
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (8)
  • README.md (1 hunks)
  • examples/everything/main.go (1 hunks)
  • go.mod (1 hunks)
  • mcp/types.go (1 hunks)
  • server/hooks.go (1 hunks)
  • server/request_handler.go (1 hunks)
  • server/server.go (12 hunks)
  • server/server_test.go (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • go.mod
  • mcp/types.go
🧰 Additional context used
🧬 Code Definitions (4)
server/request_handler.go (2)
server/server.go (18) (18)
  • s (164-169)
  • s (172-180)
  • s (183-187)
  • s (190-214)
  • s (217-243)
  • s (353-366)
  • s (369-382)
  • s (385-393)
  • s (396-398)
  • s (401-416)
  • s (419-424)
  • s (427-439)
  • s (442-449)
  • s (451-503)
  • s (505-511)
  • s (513-532)
  • MCPServer (135-150)
  • requestError (94-98)
mcp/types.go (14) (14)
  • MCPMethod (11-11)
  • PARSE_ERROR (222-222)
  • JSONRPC_VERSION (81-81)
  • INVALID_REQUEST (223-223)
  • JSONRPCNotification (192-195)
  • InitializeRequest (266-275)
  • InitializeResult (279-293)
  • PingRequest (355-357)
  • EmptyResult (232-232)
  • ListResourcesRequest (400-402)
  • ListResourcesResult (406-409)
  • METHOD_NOT_FOUND (224-224)
  • ReadResourceRequest (426-435)
  • ReadResourceResult (439-442)
server/server_test.go (4)
mcp/types.go (4) (4)
  • Result (173-177)
  • MCPMethod (11-11)
  • t (55-57)
  • t (59-70)
mcp/prompts.go (1) (1)
  • GetPromptResult (32-37)
server/hooks.go (1) (1)
  • Hooks (79-101)
server/server.go (8) (8)
  • NewMCPServer (324-350)
  • WithHooks (283-287)
  • UnparseableMessageError (70-74)
  • ErrToolNotFound (127-127)
  • ErrPromptNotFound (126-126)
  • ErrResourceNotFound (125-125)
  • ServerOption (29-29)
  • ErrUnsupported (124-124)
server/hooks.go (1)
server/request_handler.go (9) (9)
  • result (61-61)
  • result (80-80)
  • result (99-99)
  • result (124-124)
  • result (149-149)
  • result (174-174)
  • result (199-199)
  • result (224-224)
  • result (249-249)
server/server.go (6)
mcp/types.go (6) (6)
  • JSONRPCError (205-218)
  • InitializeResult (279-293)
  • EmptyResult (232-232)
  • ListResourcesRequest (400-402)
  • ListResourceTemplatesRequest (413-415)
  • ReadResourceRequest (426-435)
server/request_handler.go (12) (12)
  • err (20-20)
  • result (61-61)
  • result (80-80)
  • request (60-60)
  • request (79-79)
  • request (98-98)
  • request (123-123)
  • request (148-148)
  • request (173-173)
  • request (198-198)
  • request (223-223)
  • request (248-248)
server/hooks.go (1) (1)
  • Hooks (79-101)
mcp/utils.go (2) (2)
  • result (468-468)
  • result (546-546)
mcp/prompts.go (2) (2)
  • ListPromptsResult (13-16)
  • GetPromptRequest (20-28)
mcp/tools.go (4) (4)
  • ListToolsRequest (13-15)
  • ListToolsResult (19-22)
  • CallToolRequest (44-59)
  • CallToolResult (34-41)
🪛 LanguageTool
README.md

[uncategorized] ~519-~519: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...facts, for example the ability to count improperly-formatted requests, or to log the agent identity ...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

🔇 Additional comments (21)
server/hooks.go (1)

79-101: Consider concurrency safety for appending hooks.

Currently, each “Add” method (e.g., AddBeforeAny) appends to a slice in the Hooks struct without synchronization. If these methods are called while the server is actively processing requests, data races could occur. If the intended usage is strictly single-threaded (i.e., the hooks are only added before the server starts handling requests), then this is acceptable. Otherwise, it might be prudent to document or ensure concurrency safety.

Would you like me to generate a script to check for concurrent modifications of these hooks within the repository?

server/request_handler.go (2)

28-34: Robust handling of JSON parsing errors.

Nicely done returning a JSON-RPC parse error response when json.Unmarshal fails, ensuring the client is informed of malformed messages.


58-278: Commendable typed branching approach.

Using a switch statement to parse and route each specific request type ensures strong type safety and clear separation of logic for each method. This approach, coupled with hooks, improves maintainability and extensibility.

server/server_test.go (6)

619-619: Type assertion changed from pointer to value type.

The type assertion for the result field has been changed from a pointer type *mcp.GetPromptResult to a value type mcp.GetPromptResult. This change aligns with the modified function signatures in server/request_handler.go which now return value types instead of pointer types.


646-646: Type assertion changed from pointer to value type.

Similar to the previous comment, this is a consistent change to use value types instead of pointer types for the result. This refactoring aligns with the changes made to function signatures throughout the codebase.


667-729: Improved error handling with structured error types.

The test now validates that errors are properly propagated through the hooks system and that they match the expected error types. This change is part of the broader efforts to improve error handling by using Go's standard error handling patterns (errors.Is and errors.As).


733-867: Enhanced test coverage for error types and hooks.

The changes to this test include additional validation of callbacks and error types. This provides better test coverage for the new error handling system, ensuring that specific error types (like ErrToolNotFound, ErrPromptNotFound, and ErrResourceNotFound) are properly propagated through the system.


871-950: Added error string validation for unsupported capabilities.

The test now includes an errString field and validation to ensure that the correct error messages are returned when handlers are called without the necessary capabilities. This enhances the test coverage for the error handling system.


1164-1320: Added comprehensive test for hooks system.

This new test function validates the hook functionality by:

  1. Creating counters to verify each type of hook is called the expected number of times
  2. Setting up various hooks including generic ones (BeforeAny, AfterAny, OnError) and method-specific ones
  3. Making requests that should trigger different hooks
  4. Verifying type matching between generic and specific hooks

This comprehensive test ensures the hooks system works as expected and provides good coverage for the new feature.

examples/everything/main.go (2)

33-56: Well-implemented hooks for logging and debugging.

The implementation demonstrates how to use the new hooks system for logging server operations. Each hook type (BeforeAny, AfterAny, OnError, etc.) is registered with appropriate logging functions that print relevant information about the request lifecycle.

This provides an excellent real-world example of how the hooks can be used for monitoring, debugging, and telemetry purposes. The logging format is consistent and includes all relevant information (method, ID, message, result, error) for each hook type.


63-63: Hooks configuration added to server options.

The server.WithHooks(hooks) option is correctly added to the server initialization, demonstrating the proper way to configure hooks when creating a new MCP server.

server/server.go (10)

68-90: Well-designed error type for unparseable messages.

The UnparseableMessageError type provides a structured way to handle JSON parsing errors with access to the raw message, method, and underlying error. The implementation includes:

  1. A clear error message format
  2. Support for error unwrapping with Unwrap()
  3. Accessor methods for the raw message and method

This structure allows for detailed error reporting and inspection, which will be valuable for debugging and error handling.


92-121: Improved request error handling with error unwrapping.

The redesigned requestError type now properly implements the error interface with:

  1. Support for error unwrapping with Unwrap()
  2. Conversion to a JSON-RPC error response
  3. A clear error message format

This change enables more robust error handling and allows errors to be inspected with errors.Is and errors.As, aligning with Go's standard error handling patterns.


123-128: Standardized error variables for common error conditions.

These standardized error variables provide consistent error types that can be checked using errors.Is(). This approach is a best practice in Go error handling and improves error handling throughout the codebase.


149-149: Added hooks field to MCPServer struct.

This field will store the hooks configuration, enabling the hook functionality throughout the server's operation. This is a clean way to add the hooks system to the existing server structure.


280-287: Well-documented WithHooks option function.

The WithHooks function is well-documented and follows the established pattern for server options. The function clearly explains its purpose and how it integrates with the request lifecycle.


451-503: Function signature updated to return typed result and error.

The handler now returns a specific result type (*mcp.InitializeResult) and an error (*requestError) instead of a generic mcp.JSONRPCMessage. This change improves type safety and error handling.


505-511: Simplified ping handler with improved return types.

The ping handler now returns a more specific type (*mcp.EmptyResult) and error (*requestError), following the same pattern as other handlers for consistency.


607-612: Improved error return with error wrapping.

The error now includes the underlying ErrResourceNotFound error using fmt.Errorf with the %w verb. This allows for error inspection using errors.Is() and provides a more detailed error message.


652-657: Improved error handling for prompt not found.

The error now wraps the ErrPromptNotFound error, allowing for error inspection using errors.Is(). This is consistent with the error handling approach in other handlers.


711-717: Consistent error handling for tool not found.

The error handling follows the same pattern as other handlers, wrapping the ErrToolNotFound error to allow for error inspection using errors.Is().

@ezynda3 ezynda3 merged commit e183dd1 into mark3labs:main Mar 24, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants