Skip to content

[proposal] Optimize hooks #161

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

Conversation

dugenkui03
Copy link
Contributor

@dugenkui03 dugenkui03 commented Apr 16, 2025

details in #159

Summary by CodeRabbit

  • Refactor
    • Streamlined the server hook system with a unified interface-based design for better extensibility and context management.
    • Consolidated hook logic into structured implementations, simplifying customization and maintenance.
    • Updated server initialization to use the new hook structure by default.
    • Enhanced internal tests with structured hook types for clearer and more robust validation.
  • Style
    • Improved code formatting and alignment for readability.
  • Documentation
    • Added clarifying comments to enhance code understanding.

Copy link
Contributor

coderabbitai bot commented Apr 16, 2025

Walkthrough

This change refactors the hook system in the MCP server from a function-slice-based approach to an interface-based design. The previous Hooks struct with individual function slices for each hook event is replaced by a unified Hook interface, with a concrete ChainedHook implementation that can aggregate multiple hooks and manage contextual information through a HookContext interface. All hook methods now accept a HookContext parameter, enabling richer context propagation. The example usage and tests are updated to use new hook types and contexts, consolidating hook logic and improving structure and extensibility.

Changes

File(s) Change Summary
server/hooks.go Replaces the function-slice-based Hooks struct with a new Hook interface and ChainedHook implementation; introduces HookContext for context propagation; removes old hook function types and adds interface methods.
server/server.go Changes the hooks field in MCPServer from *Hooks to Hook interface; updates WithHooks function signature; initializes hooks with a ChainedHook.
server/request_handler.go Updates all hook invocations in HandleMessage to use a single hookContext instance passed to each hook method.
examples/everything/main.go Replaces individual hook function registrations with a single exampleHook struct implementing the full hook interface; uses ChainedHook for hook management.
server/server_test.go Refactors tests to use structured hook implementations (exampleHook, globalHook) and hook contexts for collecting invocation data and errors, replacing previous ad-hoc error/result collection.
server/sse.go Purely formatting and whitespace adjustments for field alignment and spacing; no functional changes.
server/stdio.go Adds a clarifying comment to a blocking line-read call; no functional changes.

Possibly related PRs

Suggested reviewers

  • ezynda3

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
✨ 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: 4

🔭 Outside diff range comments (1)
server/server.go (1)

165-394: 💡 Verification agent

🧩 Analysis chain

Thread safety concerns with hooks

The pipeline failure indicates race conditions that might be related to concurrent access to the hooks field. Consider making the Hook implementation thread-safe or adding appropriate synchronization.


🏁 Script executed:

#!/bin/bash
# Search for concurrent access patterns to hooks field
rg -A 2 -B 2 "s\.hooks\." --type go

Length of output: 7414


🏁 Script executed:

#!/bin/bash
set -e
# Locate the Hook interface definition
rg -n "type Hook" -A 5 -B 5 --type go

# Locate the ChainedHook implementation
rg -n "type ChainedHook" -A 10 -B 5 --type go

Length of output: 2571


🏁 Script executed:

#!/bin/bash
set -e
# Inspect CreateHookContext implementation
rg -n "func (h \\*ChainedHook) CreateHookContext" -A 10 -B 5 server/hooks.go

# Inspect GetHookContext implementation
rg -n "func (h \\*ChainedHook) GetHookContext" -A 10 -B 5 server/hooks.go

Length of output: 86


🏁 Script executed:

#!/bin/bash
set -e
# Display full hooks implementation for review
sed -n '1,200p' server/hooks.go

Length of output: 7484


ChainedHook is not safe for concurrent use

The generated ChainedHook implementation mutates shared state without any synchronization. In particular,

  • CreateHookContext() appends to h.HookContext on the receiver and writes its HookContext field
  • All hook‐dispatch methods (Before…, After…, OnError, etc.) iterate over h.Hooks without locking

Under concurrent request handling this will trigger data races.

To address this, please:

  • Add a sync.RWMutex (or sync.Mutex) to ChainedHook
  • Wrap CreateHookContext (W lock) and all hook‐dispatch methods (R lock) to protect access to Hooks and HookContext
  • Alternatively, remove the persistent HookContext field entirely and treat each call to CreateHookContext as a pure, per-request value

Files needing updates:

  • server/hooks.go (or the server/internal/gen/hooks.go.tmpl template that drives it)
🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution

🧹 Nitpick comments (3)
server/request_handler.go (2)

70-71: Inconsistent spacing in hook method calls

The spacing after commas in these hook method calls is inconsistent with the rest of the file.

Apply consistent spacing after commas in parameter lists throughout the file:

-			s.hooks.BeforeInitialize(ctx,hookContext, baseMessage.ID, &request)
+			s.hooks.BeforeInitialize(ctx, hookContext, baseMessage.ID, &request)
-			s.hooks.OnError(ctx,hookContext, baseMessage.ID, baseMessage.Method, &request, err)
+			s.hooks.OnError(ctx, hookContext, baseMessage.ID, baseMessage.Method, &request, err)
-		s.hooks.AfterInitialize(ctx,hookContext, baseMessage.ID, &request, result)
+		s.hooks.AfterInitialize(ctx, hookContext, baseMessage.ID, &request, result)

Also applies to: 74-75, 77-78

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution


243-243: Missing space after comma

Similar spacing inconsistency here.

-			s.hooks.OnError(ctx,hookContext, baseMessage.ID, baseMessage.Method, &request, err)
+			s.hooks.OnError(ctx, hookContext, baseMessage.ID, baseMessage.Method, &request, err)
🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution

examples/everything/main.go (1)

36-43: CreateHookContext / GetHookContext return nil – will break type assertions

Several parts of the server (and many hooks in the tests) assume that
hookContext is non‑nil and implements IsHookContext(). Returning
nil here invites a panic if somebody ever adds logic that touches the
context.

A minimal no‑op implementation keeps the example safe:

-func (*exampleHook) CreateHookContext() server.HookContext {
-    return nil
-}
-
-func (*exampleHook) GetHookContext() server.HookContext {
-    return nil
-}
+type emptyCtx struct{}
+
+func (*emptyCtx) IsHookContext() {}
+
+func (*exampleHook) CreateHookContext() server.HookContext {
+    return &emptyCtx{}
+}
+
+func (*exampleHook) GetHookContext() server.HookContext {
+    return &emptyCtx{}
+}

This keeps the example functional while still ignoring the context
internally.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 37ac814 and be4ce67.

📒 Files selected for processing (7)
  • examples/everything/main.go (1 hunks)
  • server/hooks.go (1 hunks)
  • server/request_handler.go (10 hunks)
  • server/server.go (3 hunks)
  • server/server_test.go (9 hunks)
  • server/sse.go (2 hunks)
  • server/stdio.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
server/sse.go (1)
server/server.go (1)
  • MCPServer (143-166)
server/server.go (1)
server/hooks.go (2)
  • Hook (21-53)
  • ChainedHook (55-58)
server/server_test.go (3)
mcp/types.go (17)
  • MCPMethod (11-11)
  • InitializeRequest (266-275)
  • InitializeResult (279-293)
  • PingRequest (355-357)
  • EmptyResult (232-232)
  • ListResourcesRequest (400-402)
  • ListResourcesResult (406-409)
  • ListResourceTemplatesRequest (413-415)
  • ListResourceTemplatesResult (419-422)
  • ReadResourceRequest (426-435)
  • MethodResourcesRead (32-32)
  • ReadResourceResult (439-442)
  • MethodPromptsGet (40-40)
  • MethodToolsCall (48-48)
  • MethodPing (20-20)
  • MethodInitialize (16-16)
  • MethodToolsList (44-44)
server/hooks.go (1)
  • HookContext (11-13)
server/server.go (2)
  • ClientSession (54-63)
  • ErrUnsupported (132-132)
server/hooks.go (4)
server/server.go (1)
  • ClientSession (54-63)
mcp/types.go (11)
  • MCPMethod (11-11)
  • InitializeRequest (266-275)
  • InitializeResult (279-293)
  • PingRequest (355-357)
  • EmptyResult (232-232)
  • ListResourcesRequest (400-402)
  • ListResourcesResult (406-409)
  • ListResourceTemplatesRequest (413-415)
  • ListResourceTemplatesResult (419-422)
  • ReadResourceRequest (426-435)
  • ReadResourceResult (439-442)
mcp/prompts.go (4)
  • ListPromptsRequest (7-9)
  • ListPromptsResult (13-16)
  • GetPromptRequest (20-28)
  • GetPromptResult (32-37)
mcp/tools.go (4)
  • ListToolsRequest (13-15)
  • ListToolsResult (19-22)
  • CallToolRequest (44-59)
  • CallToolResult (34-41)
🪛 GitHub Actions: go
server/stdio.go

[error] TestSSEServer failed due to race condition detected during test execution

server/sse.go

[warning] 359-466: Data race detected in SSEServer methods handleMessage and ServeHTTP during concurrent test execution


[error] TestSSEServer failed due to race condition detected during test execution

server/request_handler.go

[warning] 58-58: Data race detected in HandleMessage method during concurrent test execution


[error] TestSSEServer failed due to race condition detected during test execution

server/server.go

[error] TestSSEServer failed due to race condition detected during test execution

server/server_test.go

[error] TestSSEServer failed due to race condition detected during test execution

server/hooks.go

[warning] 74-74: Data race detected in CreateHookContext method during concurrent test execution


[error] TestSSEServer failed due to race condition detected during test execution

🔇 Additional comments (8)
server/sse.go (3)

56-64: Formatting improvements for better code readability

The field alignment in the SSEServer struct declaration improves readability and code organization.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution


161-166: Consistent field alignment in struct initialization

The vertical alignment of field initializations in NewSSEServer improves code readability and maintainability.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution


56-64:

❌ Incorrect review comment

Address race conditions in SSEServer

The pipeline failure indicates data races in the SSEServer methods handleMessage and ServeHTTP during concurrent test execution. This isn't directly related to your formatting changes, but should be addressed.

Also applies to: 161-166


🏁 Script executed:

#!/bin/bash
# Search for potential concurrent access points in SSEServer methods
rg -A 5 -B 5 "func \(s \*SSEServer\) (handleMessage|ServeHTTP)" server/sse.go

Length of output: 797


🏁 Script executed:

#!/bin/bash
# Extract the first 300 lines of server/sse.go for review
sed -n '1,300p' server/sse.go

# List all occurrences of "sessions" to see how the sync.Map is accessed
rg -n "sessions" server/sse.go

Length of output: 8578


🏁 Script executed:

#!/bin/bash
# Extract the full handleMessage implementation for review
rg -n -A50 -B5 "func (s \*SSEServer\) handleMessage" server/sse.go

# Extract the full ServeHTTP implementation for review
rg -n -A50 -B5 "func (s \*SSEServer\) ServeHTTP" server/sse.go

Length of output: 313


🏁 Script executed:

#!/bin/bash
# Extract handleMessage implementation with fixed-string search
rg -F -C20 "func (s *SSEServer) handleMessage" server/sse.go

# Extract ServeHTTP implementation with fixed-string search
rg -F -C20 "func (s *SSEServer) ServeHTTP" server/sse.go

Length of output: 2313


handleMessage and ServeHTTP appear concurrency‑safe
Both methods only read from the sync.Map and immutable server configuration fields—there are no unsynchronized writes or shared state mutations in handleMessage (lines 248–273) or ServeHTTP (lines 335–345). The use of Go’s sync.Map guarantees thread‑safe loads and stores, and all other fields accessed are never mutated after server construction.

Please share the specific race detector output or test scenario that failed so we can pinpoint the true source of any data race.

Likely an incorrect or invalid review comment.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution

server/stdio.go (1)

134-134: Helpful comment clarifying blocking behavior

Adding this comment clarifies that the readNextLine method blocks until it receives input, which improves code understanding for future developers.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution

server/request_handler.go (1)

58-272: Transition to context-based hook architecture

The addition of hookContext to all hook method calls properly implements the interface-based design described in the PR summary. This improves extensibility and makes the hooks system more flexible.

🧰 Tools
🪛 GitHub Actions: go

[warning] 58-58: Data race detected in HandleMessage method during concurrent test execution


[error] TestSSEServer failed due to race condition detected during test execution

server/server.go (3)

165-165: Updated hook field type to use interface

Changing the hooks field from a concrete type to an interface aligns with the refactoring to an interface-based design, allowing for more flexible hook implementations.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution


334-334: Updated WithHooks function signature

The signature change matches the new hook architecture, accepting the Hook interface instead of a concrete Hooks struct.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution


394-394: Default initialization with ChainedHook

Initializing with ChainedHook provides a default implementation that's ready to use, preventing potential nil pointer exceptions.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution

@@ -55,6 +55,7 @@ func (s *MCPServer) HandleMessage(
return nil // Return nil for notifications
}

hookContext := s.hooks.CreateHookContext()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential race condition in hookContext creation

The pipeline failure indicates a data race on this line during concurrent test execution. This suggests that multiple goroutines may be accessing s.hooks concurrently without proper synchronization.

Consider adding synchronization to prevent concurrent access to the hooks field or ensuring that the Hook implementation is thread-safe.

🧰 Tools
🪛 GitHub Actions: go

[warning] 58-58: Data race detected in HandleMessage method during concurrent test execution


[error] TestSSEServer failed due to race condition detected during test execution

Comment on lines +92 to 100
func (h *ChainedHook) BeforeAny(ctx context.Context, hookContext HookContext, id any, method mcp.MCPMethod, message any) {
if h == nil {
return
}
for _, hook := range c.OnError {
hook(ctx, id, method, message, err)
}
}

func (c *Hooks) AddOnRegisterSession(hook OnRegisterSessionHookFunc) {
c.OnRegisterSession = append(c.OnRegisterSession, hook)
for _, hook := range h.Hooks {
hook.BeforeAny(ctx, hookContext, id, method, message)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Each inner hook receives the aggregated context instead of its own

BeforeAny (and every other passthrough method) forwards the same
hookContext value to every contained hook:

for _, hook := range h.Hooks {
    hook.BeforeAny(ctx, hookContext, …)
}

When hookContext is a *ChainedHookContext, a concrete hook that
expects its own subtype will panic on a type‑assertion – it never
receives the HookContext instance it created in
CreateHookContext().

A safe pattern is:

if chCtx, ok := hookContext.(*ChainedHookContext); ok {
    for i, hook := range h.Hooks {
-       hook.BeforeAny(ctx, hookContext, id, method, message)
+       hook.BeforeAny(ctx, chCtx.HookContexts[i], id, method, message)
    }
    return
}
for _, hook := range h.Hooks {
    hook.BeforeAny(ctx, hookContext, id, method, message) // fallback
}

(The same helper can be applied to all other forwarders.)

This guarantees that every hook only ever sees its own context
object.

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during test execution

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

♻️ Duplicate comments (1)
server/server_test.go (1)

1345-1346: ⚠️ Potential issue

Counters use dangling pointers – undefined behaviour & data‑race

This code pattern appears in multiple counter variables and creates dangling pointers, leading to undefined behavior and potential data races.

For all counters in the globalHookContext, store the counter as a value rather than a pointer to avoid this issue:

type globalHookContext struct {
-    BeforeAnyCount    *int
-    OnSuccessCount    *int
-    OnErrorCount      *int
-    BeforePingCount   *int
-    AfterPingCount    *int
-    BeforeToolsCount  *int
-    AfterToolsCount   *int
+    BeforeAnyCount    int
+    OnSuccessCount    int
+    OnErrorCount      int
+    BeforePingCount   int
+    AfterPingCount    int
+    BeforeToolsCount  int
+    AfterToolsCount   int
    // other fields...
}

// Then in the methods, update the counter directly
func (*globalHook) BeforeAny(ctx context.Context, hookContext HookContext, id any, method mcp.MCPMethod, message any) {
-    newCount := (*hookContext.(*globalHookContext).BeforeAnyCount) + 1
-    hookContext.(*globalHookContext).BeforeAnyCount = &(newCount)
+    hookContext.(*globalHookContext).BeforeAnyCount++
    // rest of the method...
}

This change must be applied to all counter increment operations in the file.

Also applies to: 1354-1355, 1366-1367, 1381-1382, 1390-1391, 1432-1433, 1439-1440

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during execution of test 'Can_handle_multiple_sessions'.

🧹 Nitpick comments (3)
server/server_test.go (3)

751-751: Minor formatting issue - remove unnecessary parentheses.

There's an extra pair of parentheses around (0) that serves no purpose and should be removed.

-    if hookContext.(*exampleHookContext).AfterResults == nil {
-        hookContext.(*exampleHookContext).AfterResults = make([]afterResult, (0))
-    }
+    if hookContext.(*exampleHookContext).AfterResults == nil {
+        hookContext.(*exampleHookContext).AfterResults = make([]afterResult, 0)
+    }
🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during execution of test 'Can_handle_multiple_sessions'.


762-763: Remove debug print statements.

These debug print statements should be removed as they will clutter test output and aren't necessary for the test functionality.

-func (*exampleHook) BeforeInitialize(ctx context.Context, hookContext HookContext, id any, message *mcp.InitializeRequest) {
-    fmt.Printf("beforeInitialize: %v, %v\n", id, message)
+func (*exampleHook) BeforeInitialize(ctx context.Context, hookContext HookContext, id any, message *mcp.InitializeRequest) {
}

-func (*exampleHook) AfterInitialize(ctx context.Context, hookContext HookContext, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
-    fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
+func (*exampleHook) AfterInitialize(ctx context.Context, hookContext HookContext, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
}

And similarly for the BeforeListTools and AfterListTools methods.

Also applies to: 766-767, 810-811, 813-814

🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during execution of test 'Can_handle_multiple_sessions'.


1285-1296: Initialize zero values with var statement rather than pointer creation.

Creating pointers to integer literals is less idiomatic and causes heap allocations. Instead, use a var statement for zero values or define the counters as values rather than pointers.

-var zeroValue = 0
-var globalHookCtx = &globalHookContext{
-    BeforeAnyMessages: make([]any, 0),
-    BeforeAnyCount:    &zeroValue,
-    OnSuccessCount:    &zeroValue,
-    OnErrorCount:      &zeroValue,
-    BeforePingCount:   &zeroValue,
-    AfterPingCount:    &zeroValue,
-    BeforeToolsCount:  &zeroValue,
+var globalHookCtx = &globalHookContext{
+    BeforeAnyMessages: make([]any, 0),
+    BeforeAnyCount:    0,
+    OnSuccessCount:    0,
+    OnErrorCount:      0,
+    BeforePingCount:   0,
+    AfterPingCount:    0,
+    BeforeToolsCount:  0,
🧰 Tools
🪛 GitHub Actions: go

[error] TestSSEServer failed due to race condition detected during execution of test 'Can_handle_multiple_sessions'.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between be4ce67 and db92287.

📒 Files selected for processing (7)
  • examples/everything/main.go (1 hunks)
  • server/hooks.go (1 hunks)
  • server/request_handler.go (10 hunks)
  • server/server.go (3 hunks)
  • server/server_test.go (9 hunks)
  • server/sse.go (2 hunks)
  • server/stdio.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • server/sse.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/stdio.go
  • server/request_handler.go
  • server/server.go
  • examples/everything/main.go
  • server/hooks.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
server/server_test.go (5)
mcp/types.go (11)
  • MCPMethod (11-11)
  • InitializeRequest (266-275)
  • InitializeResult (279-293)
  • PingRequest (355-357)
  • EmptyResult (232-232)
  • MethodResourcesRead (32-32)
  • MethodPromptsGet (40-40)
  • MethodToolsCall (48-48)
  • MethodPing (20-20)
  • MethodInitialize (16-16)
  • MethodToolsList (44-44)
server/hooks.go (1)
  • HookContext (11-13)
server/server.go (2)
  • ClientSession (54-63)
  • ErrUnsupported (132-132)
mcp/prompts.go (4)
  • ListPromptsRequest (7-9)
  • ListPromptsResult (13-16)
  • GetPromptRequest (20-28)
  • GetPromptResult (32-37)
mcp/tools.go (4)
  • ListToolsRequest (13-15)
  • ListToolsResult (19-22)
  • CallToolRequest (44-59)
  • CallToolResult (34-41)
🪛 GitHub Actions: go
server/server_test.go

[error] TestSSEServer failed due to race condition detected during execution of test 'Can_handle_multiple_sessions'.

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

♻️ Duplicate comments (4)
server/server_test.go (4)

724-726: ⚠️ Potential issue

Avoid storing a shared HookContext directly in the hook.

Storing the HookContext directly in the hook could lead to race conditions if the hook is used concurrently. This pattern was previously identified as problematic in past reviews.

type exampleHook struct {
-    HookContext HookContext
}

func (e *exampleHook) CreateHookContext() HookContext {
    hookContext := &exampleHookContext{}
    hookContext.Errs = make([]error, 0)
-    e.HookContext = hookContext
    
    return hookContext
}

func (e *exampleHook) GetHookContext() HookContext {
-    return e.HookContext
+    // This method should be removed or redesigned to not depend on a stored context
+    // Consider using a map[string]HookContext to track contexts by ID if needed
+    return nil
}

1286-1297: ⚠️ Potential issue

Counters use dangling pointers - undefined behavior.

Using pointers to stack variables (zeroValue) leads to undefined behavior when these variables go out of scope. The pattern appears throughout the code for all counter fields.

-var zeroValue = 0
 var globalHookCtx = &globalHookContext{
     BeforeAnyMessages: make([]any, 0),
-    BeforeAnyCount:    &zeroValue,
-    OnSuccessCount:    &zeroValue,
-    OnErrorCount:      &zeroValue,
-    BeforePingCount:   &zeroValue,
-    AfterPingCount:    &zeroValue,
-    BeforeToolsCount:  &zeroValue,
-    AfterToolsCount:   &zeroValue,
+    BeforeAnyCount:    0,
+    OnSuccessCount:    0,
+    OnErrorCount:      0,
+    BeforePingCount:   0,
+    AfterPingCount:    0,
+    BeforeToolsCount:  0,
+    AfterToolsCount:   0,

1337-1340: ⚠️ Potential issue

Fix shared hook context to prevent races.

The current implementation returns a single shared context instance for all requests, which can cause race conditions in concurrent tests.

func (*globalHook) CreateHookContext() HookContext {
-   // All request share one hook context
-   return globalHookCtx
+   // Create a new context per request to avoid race conditions
+   return &globalHookContext{
+       BeforeAnyMessages: make([]any, 0),
+       BeforePingMessages: make([]*mcp.PingRequest, 0),
+       OnSuccessData: make([]struct {
+           msg any
+           res any
+       }, 0),
+       AfterPingData: make([]struct {
+           Msg *mcp.PingRequest
+           Res *mcp.EmptyResult
+       }, 0),
+   }
}

1308-1318: 🛠️ Refactor suggestion

Change counter fields to values instead of pointers.

The counter fields should be defined as values rather than pointers to avoid dangling pointers and improve performance.

type globalHookContext struct {
    mu                sync.Mutex
    BeforeAnyMessages []any
-   BeforeAnyCount    *int
-   OnSuccessCount    *int
-   OnErrorCount      *int
-   BeforePingCount   *int
-   AfterPingCount    *int
-   BeforeToolsCount  *int
-   AfterToolsCount   *int
+   BeforeAnyCount    int
+   OnSuccessCount    int
+   OnErrorCount      int
+   BeforePingCount   int
+   AfterPingCount    int
+   BeforeToolsCount  int
+   AfterToolsCount   int
🛑 Comments failed to post (4)
server/server_test.go (4)

1541-1544: 🛠️ Refactor suggestion

Update assertion statements to match counter field type changes.

When changing counter fields from pointers to direct values, the assertions need to be updated as well.

-assert.Equal(t, 1, *hooks.GetHookContext().(*globalHookContext).BeforePingCount, "beforePing should be called once")
-assert.Equal(t, 1, *hooks.GetHookContext().(*globalHookContext).AfterPingCount, "afterPing should be called once")
-assert.Equal(t, 1, *hooks.GetHookContext().(*globalHookContext).BeforeToolsCount, "beforeListTools should be called once")
-assert.Equal(t, 1, *hooks.GetHookContext().(*globalHookContext).AfterToolsCount, "afterListTools should be called once")
+assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).BeforePingCount, "beforePing should be called once")
+assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).AfterPingCount, "afterPing should be called once")
+assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).BeforeToolsCount, "beforeListTools should be called once")
+assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).AfterToolsCount, "afterListTools should be called once")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).BeforePingCount, "beforePing should be called once")
	assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).AfterPingCount, "afterPing should be called once")
	assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).BeforeToolsCount, "beforeListTools should be called once")
	assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).AfterToolsCount, "afterListTools should be called once")

1548-1553: 🛠️ Refactor suggestion

Update remaining assertion statements for counter type changes.

Further assertions that need to be updated after changing counter fields to values.

-assert.Equal(t, 4, *hooks.GetHookContext().(*globalHookContext).BeforeAnyCount, "beforeAny should be called for each method")
-assert.Equal(t, 3, *hooks.GetHookContext().(*globalHookContext).OnSuccessCount, "onSuccess should be called after all successful invocations")
-assert.Equal(t, 1, *hooks.GetHookContext().(*globalHookContext).OnErrorCount, "onError should be called once")
+assert.Equal(t, 4, hooks.GetHookContext().(*globalHookContext).BeforeAnyCount, "beforeAny should be called for each method")
+assert.Equal(t, 3, hooks.GetHookContext().(*globalHookContext).OnSuccessCount, "onSuccess should be called after all successful invocations")
+assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).OnErrorCount, "onError should be called once")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	assert.Equal(t, 4, hooks.GetHookContext().(*globalHookContext).BeforeAnyCount, "beforeAny should be called for each method")
	// onSuccess is called for all 3 success methods (initialize, ping, tools/list)
	assert.Equal(t, 3, hooks.GetHookContext().(*globalHookContext).OnSuccessCount, "onSuccess should be called after all successful invocations")

	// Error hook should be called once for the failed tools/call
	assert.Equal(t, 1, hooks.GetHookContext().(*globalHookContext).OnErrorCount, "onError should be called once")

1387-1397: 🛠️ Refactor suggestion

Fix pointer arithmetic in counters.

Replace pointer arithmetic with direct increment of the counter value.

func (e *globalHook) BeforePing(ctx context.Context, hookContext HookContext, id any, message *mcp.PingRequest) {
    hookContext.(*globalHookContext).mu.Lock()
    defer hookContext.(*globalHookContext).mu.Unlock()

    e.BeforeAny(ctx, hookContext, id, mcp.MethodPing, message)

-   newCount := (*hookContext.(*globalHookContext).BeforePingCount) + 1
-   hookContext.(*globalHookContext).BeforePingCount = &(newCount)
+   hookContext.(*globalHookContext).BeforePingCount++

    hookContext.(*globalHookContext).BeforePingMessages = append(hookContext.(*globalHookContext).BeforePingMessages, message)
}

Committable suggestion skipped: line range outside the PR's diff.


1346-1354: ⚠️ Potential issue

Fix mutex unlock causing a fatal error.

The code is unlocking the mutex in BeforeAny when the method is mcp.MethodPing, but this mutex is already scheduled to be unlocked by the defer statement in the calling function BeforePing. This causes an "unlock of unlocked mutex" error.

func (*globalHook) BeforeAny(ctx context.Context, hookContext HookContext, id any, method mcp.MCPMethod, message any) {
-   newCount := (*hookContext.(*globalHookContext).BeforeAnyCount) + 1
-   hookContext.(*globalHookContext).BeforeAnyCount = &(newCount)
+   hookContext.(*globalHookContext).BeforeAnyCount++
    // Only collect ping messages for our test
    if method == mcp.MethodPing {
        hookContext.(*globalHookContext).BeforeAnyMessages = append(hookContext.(*globalHookContext).BeforeAnyMessages, message)
-       hookContext.(*globalHookContext).mu.Unlock()
    }
}

Committable suggestion skipped: line range outside the PR's diff.

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

♻️ Duplicate comments (4)
server/server_test.go (4)

724-726: ⚠️ Potential issue

Avoid storing a shared HookContext directly in the hook.

Storing the HookContext directly in the hook could lead to race conditions if the hook is used concurrently. This is especially problematic in test scenarios with parallel test execution.

type exampleHook struct {
-    HookContext HookContext
}

func (e *exampleHook) CreateHookContext() HookContext {
    hookContext := &exampleHookContext{}
    hookContext.Errs = make([]error, 0)
-    e.HookContext = hookContext
    
    return hookContext
}

func (e *exampleHook) GetHookContext() HookContext {
-    return e.HookContext
+    // This method should be removed or redesigned to not depend on stored context
+    // Consider using a map[string]HookContext with proper synchronization to track contexts by ID
}

1286-1306: ⚠️ Potential issue

Using pointer fields for counters creates dangling pointers and race conditions.

Storing pointers to stack variables like zeroValue is unsafe as the variable goes out of scope, making the pointer dangling. Additionally, incrementing and reassigning these pointers creates unnecessary heap allocations.

// All request share one hook context
-var zeroValue = 0
var globalHookCtx = &globalHookContext{
    BeforeAnyMessages: make([]any, 0),
-    BeforeAnyCount:    &zeroValue,
-    OnSuccessCount:    &zeroValue,
-    OnErrorCount:      &zeroValue,
-    BeforePingCount:   &zeroValue,
-    AfterPingCount:    &zeroValue,
-    BeforeToolsCount:  &zeroValue,
-    AfterToolsCount:   &zeroValue,
+    BeforeAnyCount:    0,
+    OnSuccessCount:    0,
+    OnErrorCount:      0,
+    BeforePingCount:   0,
+    AfterPingCount:    0,
+    BeforeToolsCount:  0,
+    AfterToolsCount:   0,
    OnSuccessData: make([]struct {
        msg any
        res any
    }, 0),
    BeforePingMessages: make([]*mcp.PingRequest, 0),
    AfterPingData: make([]struct {
        Msg *mcp.PingRequest
        Res *mcp.EmptyResult
    }, 0),
}

1308-1327: ⚠️ Potential issue

Counter fields should be value types, not pointers, to avoid dangling pointers.

Using pointers for simple int counters creates complexity and potential for dangling pointers when incrementing with stack variables. Use simple int values instead.

type globalHookContext struct {
    mu                sync.Mutex
    BeforeAnyMessages []any
-    BeforeAnyCount    *int
-    OnSuccessCount    *int
-    OnErrorCount      *int
-    BeforePingCount   *int
-    AfterPingCount    *int
-    BeforeToolsCount  *int
-    AfterToolsCount   *int
+    BeforeAnyCount    int
+    OnSuccessCount    int
+    OnErrorCount      int
+    BeforePingCount   int
+    AfterPingCount    int
+    BeforeToolsCount  int
+    AfterToolsCount   int
    OnSuccessData     []struct {
        msg any
        res any
    }
    BeforePingMessages []*mcp.PingRequest
    AfterPingData      []struct {
        Msg *mcp.PingRequest
        Res *mcp.EmptyResult
    }
}

1347-1352: ⚠️ Potential issue

Fix counter increment pattern to avoid dangling pointers.

The current implementation creates and assigns pointers to stack variables, causing dangling pointers. Use direct value increments instead.

func (*globalHook) BeforeAny(ctx context.Context, hookContext HookContext, id any, method mcp.MCPMethod, message any) {
-    newCount := (*hookContext.(*globalHookContext).BeforeAnyCount) + 1
-    hookContext.(*globalHookContext).BeforeAnyCount = &(newCount)
+    hookContext.(*globalHookContext).BeforeAnyCount++
    // Only collect ping messages for our test
    if method == mcp.MethodPing {
        hookContext.(*globalHookContext).BeforeAnyMessages = append(hookContext.(*globalHookContext).BeforeAnyMessages, message)
    }
}

This pattern is repeated in multiple methods (lines 1356-1358, 1368-1369, 1392-1394, 1404-1406, 1449-1451, 1459-1461) and should be fixed in all of them.

🧹 Nitpick comments (4)
server/server_test.go (4)

752-753: Fix incorrect parentheses around zero literal.

There's an unnecessary pair of parentheses around the 0 literal in the slice capacity.

-    hookContext.(*exampleHookContext).AfterResults = make([]afterResult, (0))
+    hookContext.(*exampleHookContext).AfterResults = make([]afterResult, 0)

811-812: Debug print statement should be removed from test code.

The fmt.Printf statement left in the test code will produce unwanted output during test execution.

func (*exampleHook) BeforeListTools(ctx context.Context, hookContext HookContext, id any, message *mcp.ListToolsRequest) {
-    fmt.Printf("beforeCallTool: %v, %v\n", id, message)
}

815-816: Debug print statement should be removed from test code.

The fmt.Printf statement left in the test code will produce unwanted output during test execution.

func (*exampleHook) AfterListTools(ctx context.Context, hookContext HookContext, id any, message *mcp.ListToolsRequest, result *mcp.ListToolsResult) {
-    fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
}

1557-1558: Fix type assertion comparison with IsType.

Using assert.IsType() is more appropriate than direct type comparison. The current code doesn't prove that the items are the same object, only the same type.

-    assert.IsType(t, hooks.GetHookContext().(*globalHookContext).BeforePingMessages[0], hooks.GetHookContext().(*globalHookContext).BeforeAnyMessages[0], "BeforeAny message should be same type as BeforePing message")
+    assert.IsType(t, hooks.GetHookContext().(*globalHookContext).BeforePingMessages[0], hooks.GetHookContext().(*globalHookContext).BeforeAnyMessages[0], "BeforeAny message should be same type as BeforePing message")

This isn't actually a change, but I'm highlighting that the test is correctly using assert.IsType() rather than trying to compare the actual values.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9da532f and a68723b.

📒 Files selected for processing (1)
  • server/server_test.go (10 hunks)
🔇 Additional comments (1)
server/server_test.go (1)

1437-1442: ⚠️ Potential issue

Missing mutex protection in BeforeGetPrompt and AfterGetPrompt methods.

Unlike other hook methods, these don't have mutex protection, which could lead to race conditions during concurrent test execution.

func (*globalHook) BeforeGetPrompt(ctx context.Context, hookContext HookContext, id any, message *mcp.GetPromptRequest) {
+    hookContext.(*globalHookContext).mu.Lock()
+    defer hookContext.(*globalHookContext).mu.Unlock()
}

func (*globalHook) AfterGetPrompt(ctx context.Context, hookContext HookContext, id any, message *mcp.GetPromptRequest, result *mcp.GetPromptResult) {
+    hookContext.(*globalHookContext).mu.Lock()
+    defer hookContext.(*globalHookContext).mu.Unlock()
}

This issue affects multiple methods. All hook methods should consistently use mutex protection when accessing shared state in the hook context.

Likely an incorrect or invalid review comment.

@dugenkui03 dugenkui03 changed the title [proposal] About hooks [proposal] Optimize hooks Apr 16, 2025
@dugenkui03 dugenkui03 marked this pull request as draft April 28, 2025 03:10
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.

1 participant