Skip to content

fix(mcp): reset registeredTools between reloads to prevent false tool name collisions#2625

Merged
asoorm merged 2 commits intomainfrom
ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads
Mar 12, 2026
Merged

fix(mcp): reset registeredTools between reloads to prevent false tool name collisions#2625
asoorm merged 2 commits intomainfrom
ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads

Conversation

@asoorm
Copy link
Copy Markdown
Contributor

@asoorm asoorm commented Mar 10, 2026

Summary

  • Reset s.registeredTools = nil after DeleteTools to prevent unbounded slice growth across config reloads
  • When omit_tool_name_prefix: true, every operation was falsely colliding with its own name from the previous reload cycle
  • Changed collision behavior to skip duplicate tools with a warning instead of silently falling back to a prefixed name

Fixes ENG-9158
Closes #2623

Test plan

  • Configure router with omit_tool_name_prefix: true and execution_config.file.watch: true
  • Trigger a config reload by modifying the execution config file
  • Verify no false collision warnings are logged after reload
  • Verify tools retain their unprefixed names after reload
  • Verify actual collisions (e.g. operation named GetSchemaget_schema) are still detected and skipped with a warning

Summary by CodeRabbit

  • Bug Fixes

    • Reload now fully clears previously registered tools before re-registering to avoid stale entries.
    • Tool registration now skips operations whose names collide with built-in tools and logs the skip instead of attempting a fallback rename.
  • Tests

    • Updated test and test name to reflect that colliding tools are skipped (assertions adjusted accordingly).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Resets the server's registeredTools slice during config reloads and changes MCP tool registration collision handling: when an operation name collides with a built-in tool while OmitToolNamePrefix is enabled, the colliding operation is skipped (no prefixed fallback). Test updated accordingly.

Changes

Cohort / File(s) Summary
MCP server registration
router/pkg/mcpserver/server.go
After removing tools on reload, sets s.registeredTools = nil. On collision with a built-in MCP tool when OmitToolNamePrefix is enabled, logs the collision and skips registering that operation instead of registering a prefixed name.
Tests
router-tests/mcp_test.go
Renamed test case and updated assertion to expect the colliding operation is not registered (asserts absence of the previously used prefixed name).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: resetting registeredTools between reloads to prevent false tool name collisions, which directly addresses the primary bug fix in the PR.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #2623: resets s.registeredTools to nil in Reload, removes the prefixed name fallback in collision handling, and skips conflicting operations with a warning instead of renaming them.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the registeredTools growth issue and collision handling logic, with corresponding test updates that validate the new behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 10, 2026

Router image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-9faf803b90ba1094b7628c3582e94df3fc897d57

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
router/pkg/mcpserver/server.go (1)

541-549: Optional improvement: Handle operation-to-operation collisions consistently regardless of prefix setting.

When omitToolNamePrefix is false (prefixed mode), two operations with names that normalize to the same snake_case (e.g., GetUser and Get_Userget_user) would both attempt to register as execute_operation_get_user. The collision check at line 544 only runs when omitToolNamePrefix is true.

Consider extending the collision check to cover all cases:

♻️ Suggested improvement
 		toolName := operationToolName
 		if !s.omitToolNamePrefix {
 			toolName = fmt.Sprintf("execute_operation_%s", operationToolName)
-		} else if slices.Contains(s.registeredTools, operationToolName) {
+		}
+		if slices.Contains(s.registeredTools, toolName) {
 			s.logger.Warn("Skipping operation due to tool name collision",
 				zap.String("operation", op.Name),
-				zap.String("conflicting_tool", operationToolName),
+				zap.String("conflicting_tool", toolName),
 			)
 			continue
 		}

This makes collision detection work consistently regardless of the prefix setting. However, this is a pre-existing edge case and not a regression from this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router/pkg/mcpserver/server.go` around lines 541 - 549, The collision check
is only run when s.omitToolNamePrefix is true, so when you compute toolName
(using operationToolName or fmt.Sprintf("execute_operation_%s",
operationToolName)) perform a membership check against s.registeredTools for the
computed toolName and handle collisions consistently: after setting toolName
(the variable in this block) check slices.Contains(s.registeredTools, toolName)
and if true call s.logger.Warn with the same fields (use op.Name and
operationToolName/conflicting_tool) and continue, ensuring the collision branch
is reached regardless of s.omitToolNamePrefix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@router/pkg/mcpserver/server.go`:
- Around line 541-549: The collision check is only run when s.omitToolNamePrefix
is true, so when you compute toolName (using operationToolName or
fmt.Sprintf("execute_operation_%s", operationToolName)) perform a membership
check against s.registeredTools for the computed toolName and handle collisions
consistently: after setting toolName (the variable in this block) check
slices.Contains(s.registeredTools, toolName) and if true call s.logger.Warn with
the same fields (use op.Name and operationToolName/conflicting_tool) and
continue, ensuring the collision branch is reached regardless of
s.omitToolNamePrefix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dc43ad8-d7cc-4fee-94c8-4304f9caa4e0

📥 Commits

Reviewing files that changed from the base of the PR and between 1700918 and cc200f8.

📒 Files selected for processing (1)
  • router/pkg/mcpserver/server.go

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
router-tests/mcp_test.go (1)

237-238: Assert the collision is skipped, not just that the fallback name disappeared.

get_schema is already present here because ExposeSchema is enabled, so assert.Contains(t, toolNames, "get_schema") still passes if the colliding operation is incorrectly registered under the same unprefixed name. This should verify there is exactly one get_schema entry, or assert the full tool-name set, to prove the operation was actually skipped.

Suggested tightening
-					assert.Contains(t, toolNames, "get_schema")                      // built-in tool (ExposeSchema=true)
-					assert.NotContains(t, toolNames, "execute_operation_get_schema") // colliding operation is skipped, not prefixed
+					assert.NotContains(t, toolNames, "execute_operation_get_schema") // colliding operation is skipped, not prefixed
+
+					getSchemaCount := 0
+					for _, name := range toolNames {
+						if name == "get_schema" {
+							getSchemaCount++
+						}
+					}
+					assert.Equal(t, 1, getSchemaCount, "expected only the built-in get_schema tool to be registered")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router-tests/mcp_test.go` around lines 237 - 238, The test currently only
checks that "get_schema" is present and "execute_operation_get_schema" is
absent, which doesn't prove the collision was skipped; change the assertions to
verify there is exactly one "get_schema" entry (e.g., count occurrences of
"get_schema" in toolNames and assert it equals 1) and still assert NotContains
for "execute_operation_get_schema", or alternatively assert the entire toolNames
equals the expected set (using assert.ElementsMatch) so you prove the colliding
operation was not registered under the same unprefixed name; update the
assertions referencing toolNames, "get_schema", and
"execute_operation_get_schema" accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@router-tests/mcp_test.go`:
- Around line 237-238: The test currently only checks that "get_schema" is
present and "execute_operation_get_schema" is absent, which doesn't prove the
collision was skipped; change the assertions to verify there is exactly one
"get_schema" entry (e.g., count occurrences of "get_schema" in toolNames and
assert it equals 1) and still assert NotContains for
"execute_operation_get_schema", or alternatively assert the entire toolNames
equals the expected set (using assert.ElementsMatch) so you prove the colliding
operation was not registered under the same unprefixed name; update the
assertions referencing toolNames, "get_schema", and
"execute_operation_get_schema" accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0910158-8a5e-4386-822e-a4af3616fa17

📥 Commits

Reviewing files that changed from the base of the PR and between cc200f8 and 5a18771.

📒 Files selected for processing (1)
  • router-tests/mcp_test.go

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.90%. Comparing base (670f75d) to head (ca84c26).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2625      +/-   ##
==========================================
+ Coverage   62.41%   62.90%   +0.48%     
==========================================
  Files         244      244              
  Lines       25826    25826              
==========================================
+ Hits        16119    16245     +126     
+ Misses       8326     8209     -117     
+ Partials     1381     1372       -9     
Files with missing lines Coverage Δ
router/pkg/mcpserver/server.go 79.06% <100.00%> (ø)

... and 17 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@asoorm asoorm force-pushed the ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads branch 4 times, most recently from 49755be to b73dddc Compare March 11, 2026 11:19
Copy link
Copy Markdown
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
router/pkg/mcpserver/server.go (1)

541-549: ⚠️ Potential issue | 🟠 Major

get_operation_info can still collide with an operation name.

This check only covers names already present in s.registeredTools. Because get_operation_info is registered later in the function, an operation whose snake_case name is get_operation_info will still be added when omit_tool_name_prefix is enabled, leaving one real collision path unfixed.

💡 Minimal fix
-		} else if slices.Contains(s.registeredTools, operationToolName) {
+		} else if operationToolName == "get_operation_info" || slices.Contains(s.registeredTools, operationToolName) {
 			s.logger.Error("Skipping operation due to tool name collision",
 				zap.String("operation", op.Name),
 				zap.String("conflicting_tool", operationToolName),
 			)
 			continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router/pkg/mcpserver/server.go` around lines 541 - 549, When computing
toolName in the block using operationToolName and s.omitToolNamePrefix, also
detect collisions with the reserved handler name "get_operation_info" (or any
other reserved/future-registered tool names) rather than only checking
s.registeredTools; update the condition that currently does
slices.Contains(s.registeredTools, operationToolName) to check against a
combined set (e.g., registeredTools plus a small reservedNames list containing
"get_operation_info") and, if found, log the same error and continue so an
operation whose snake_case is "get_operation_info" is skipped when
omitToolNamePrefix is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@router/pkg/mcpserver/server.go`:
- Around line 545-548: The current call to s.logger.Error("Skipping operation
due to tool name collision", ...) should be downgraded to a warning since the
collision is handled by skipping registration; update the call in server.go
where the collision is logged (the site using s.logger.Error with the message
"Skipping operation due to tool name collision" and fields
zap.String("operation", op.Name) and zap.String("conflicting_tool",
operationToolName)) to s.logger.Warn with the same message and fields so it logs
as a warning rather than an error.

---

Outside diff comments:
In `@router/pkg/mcpserver/server.go`:
- Around line 541-549: When computing toolName in the block using
operationToolName and s.omitToolNamePrefix, also detect collisions with the
reserved handler name "get_operation_info" (or any other
reserved/future-registered tool names) rather than only checking
s.registeredTools; update the condition that currently does
slices.Contains(s.registeredTools, operationToolName) to check against a
combined set (e.g., registeredTools plus a small reservedNames list containing
"get_operation_info") and, if found, log the same error and continue so an
operation whose snake_case is "get_operation_info" is skipped when
omitToolNamePrefix is true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0ae5a65-532e-4a29-9708-ca9be7fe89b0

📥 Commits

Reviewing files that changed from the base of the PR and between b8ac108 and 49755be.

📒 Files selected for processing (2)
  • router-tests/mcp_test.go
  • router/pkg/mcpserver/server.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router-tests/mcp_test.go

Comment thread router/pkg/mcpserver/server.go
Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
router/pkg/mcpserver/server.go (1)

572-589: Consider potential edge case with get_operation_info registration order.

The get_operation_info tool is registered after the operation loop, so an operation named "GetOperationInfo" with omitToolNamePrefix: true would bypass collision detection and potentially overwrite the built-in handler (or vice versa depending on AddTool behavior).

Pre-existing issue not introduced by this PR, but could be addressed by moving line 589's append to before the operation loop, or by adding "get_operation_info" to registeredTools earlier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router/pkg/mcpserver/server.go` around lines 572 - 589, The built-in tool
"get_operation_info" is appended to s.registeredTools after the operation
registration loop, which can allow a user operation named "GetOperationInfo"
with omitToolNamePrefix:true to collide; update the registration order so the
built-in name is reserved before user operations by adding "get_operation_info"
into s.registeredTools prior to the operation loop (or moving the
s.registeredTools = append(s.registeredTools, "get_operation_info") call to
execute before the loop that registers user operations), ensuring the built-in
handler (handleGraphQLOperationInfo / mcp.NewTool("get_operation_info")) cannot
be shadowed by user tools registered via AddTool.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@router/pkg/mcpserver/server.go`:
- Around line 572-589: The built-in tool "get_operation_info" is appended to
s.registeredTools after the operation registration loop, which can allow a user
operation named "GetOperationInfo" with omitToolNamePrefix:true to collide;
update the registration order so the built-in name is reserved before user
operations by adding "get_operation_info" into s.registeredTools prior to the
operation loop (or moving the s.registeredTools = append(s.registeredTools,
"get_operation_info") call to execute before the loop that registers user
operations), ensuring the built-in handler (handleGraphQLOperationInfo /
mcp.NewTool("get_operation_info")) cannot be shadowed by user tools registered
via AddTool.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b57768dd-8d66-4bab-8e8e-9247014d4f5d

📥 Commits

Reviewing files that changed from the base of the PR and between 49755be and b73dddc.

📒 Files selected for processing (2)
  • router-tests/mcp_test.go
  • router/pkg/mcpserver/server.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router-tests/mcp_test.go

@asoorm asoorm force-pushed the ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads branch from b73dddc to 1f44a5f Compare March 11, 2026 12:30
Copy link
Copy Markdown
Contributor

@shamashel shamashel left a comment

Choose a reason for hiding this comment

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

Didn't realize you wanted me to submit an actual review here. LGTM from our side!

@asoorm asoorm force-pushed the ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads branch from 1f44a5f to a4213a4 Compare March 11, 2026 13:48
Copy link
Copy Markdown
Member

@endigma endigma left a comment

Choose a reason for hiding this comment

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

small things

Comment thread router/pkg/mcpserver/server_test.go Outdated
Comment thread router/pkg/mcpserver/server_test.go Outdated
Comment thread router/pkg/mcpserver/server_test.go Outdated
@asoorm asoorm force-pushed the ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads branch from a4213a4 to cfc508a Compare March 11, 2026 17:29
@asoorm asoorm requested a review from endigma March 11, 2026 17:30
Copy link
Copy Markdown
Member

@endigma endigma left a comment

Choose a reason for hiding this comment

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

LGTM

@asoorm asoorm force-pushed the ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads branch from cfc508a to 5622a49 Compare March 11, 2026 21:28
asoorm added 2 commits March 12, 2026 09:49
… name collisions

registeredTools was never cleared between config reloads, causing it to
grow unbounded. When omit_tool_name_prefix was enabled, every operation
would falsely collide with its own name from the previous reload cycle.

Also changes collision behavior to skip duplicate tools with a warning
instead of silently falling back to a prefixed name.

Fixes: ENG-9158

fix(mcp): update collision test to expect skip behavior instead of prefix fallback
- Assert full registeredTools list with ElementsMatch instead of individual Contains/counting
- Rename test to accurately reflect what it verifies
- Add reserved name collision scenario to prefix mode test
@asoorm asoorm force-pushed the ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads branch from 5622a49 to ca84c26 Compare March 12, 2026 09:49
@asoorm asoorm merged commit fbbd11f into main Mar 12, 2026
31 checks passed
@asoorm asoorm deleted the ahmet/eng-9158-mcp-registeredtools-grows-unbounded-across-config-reloads branch March 12, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: registeredTools slice grows unbounded across config reloads causing false tool name collisions

3 participants