Skip to content

fix: enforce toolset/promptset boundary on tools/call and prompts/get#3036

Open
sjhddh wants to merge 1 commit intogoogleapis:mainfrom
sjhddh:fix/toolset-boundary-enforcement
Open

fix: enforce toolset/promptset boundary on tools/call and prompts/get#3036
sjhddh wants to merge 1 commit intogoogleapis:mainfrom
sjhddh:fix/toolset-boundary-enforcement

Conversation

@sjhddh
Copy link
Copy Markdown

@sjhddh sjhddh commented Apr 12, 2026

Summary

  • Fix toolset boundary bypass (IDOR): tools/call previously resolved tools via resourceMgr.GetTool() without verifying the requested tool belongs to the current toolset, allowing clients to invoke any tool by name regardless of their toolset scope.
  • Fix promptset boundary bypass: Same issue existed for prompts/get with resourceMgr.GetPrompt().
  • Add ContainsTool() / ContainsPrompt() membership checks and enforce them in all four MCP protocol versions (v20241105, v20250326, v20250618, v20251125) before delegating to the global resource manager.

Fixes #2755

Test plan

  • Added unit tests for Toolset.ContainsTool() and Promptset.ContainsPrompt() (both positive and negative cases, including empty sets)
  • All existing tests pass (go test ./internal/tools/... ./internal/prompts/... ./internal/server/mcp/...)
  • Manual verification: configure two toolsets with different tools, connect to one, and confirm tools/call rejects tools from the other toolset

🤖 Generated with Claude Code

@sjhddh sjhddh requested a review from a team as a code owner April 12, 2026 17:13
@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 12, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces ContainsPrompt and ContainsTool methods to the Promptset and Toolset structs, respectively, along with corresponding unit tests. These methods are used to verify that a requested prompt or tool belongs to the current set before global resolution in the MCP handlers. The review feedback points out that the current linear search implementation ($O(N)$) for these lookups could become a performance bottleneck as the number of items grows, suggesting a map-based approach for $O(1)$ efficiency.

Comment on lines +40 to +47
func (p Promptset) ContainsPrompt(name string) bool {
for _, n := range p.PromptNames {
if n == name {
return true
}
}
return false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The ContainsPrompt method uses a linear search ($O(N)$), which is executed on every prompts/get request. While likely acceptable for small promptsets, this could impact performance as the number of prompts grows. Consider using a map for $O(1)$ lookups, which could be initialized once in the Initialize method.

Comment on lines +39 to +46
func (t Toolset) ContainsTool(name string) bool {
for _, n := range t.ToolNames {
if n == name {
return true
}
}
return false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The ContainsTool method performs a linear search ($O(N)$) on every tools/call. For toolsets with many tools, this may become a performance bottleneck. Using a map for $O(1)$ lookups (populated during Initialize) would be more efficient and scalable.

Copy link
Copy Markdown
Contributor

@duwenxin99 duwenxin99 left a comment

Choose a reason for hiding this comment

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

Hi @sjhddh , the change LGTM overall except for the error messages. Please let me know once you fix them and I'll handle the merge process for you. Thank you for contributing!

// Verify tool belongs to the current toolset before resolving globally.
if !toolset.ContainsTool(toolName) {
err = fmt.Errorf("tool %q is not part of the current toolset", toolName)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should return the same error message as non-existent tool call (invalid tool name: tool with name %q does not exist) . Otherwise if the tool doesn't exist at all, we will still return "tool %q is not part of the current toolset" which could be misleading.

)

// Verify tool belongs to the current toolset before resolving globally.
if !toolset.ContainsTool(toolName) {
Copy link
Copy Markdown
Contributor

@duwenxin99 duwenxin99 Apr 20, 2026

Choose a reason for hiding this comment

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

Same for the other errors.

span.SetAttributes(attribute.String("gen_ai.prompt.name", promptName))

// Verify prompt belongs to the current promptset before resolving globally.
if !promptset.ContainsPrompt(promptName) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar for prompt-let's treat it the same way as a non-existent prompt.

@duwenxin99
Copy link
Copy Markdown
Contributor

You may want to squash the commits to get rid of the agent account that's missing CLA.

tools/call and prompts/get resolved tools and prompts via the global
resourceMgr without verifying membership in the current toolset or
promptset. This allowed clients connected to a scoped toolset to invoke
any tool by name, bypassing toolset access boundaries (IDOR).

Add ContainsTool/ContainsPrompt membership checks and enforce them in
all four MCP protocol versions before delegating to resourceMgr. Return
the same "does not exist" error on boundary violation as on a truly
missing tool/prompt so clients cannot distinguish the two cases.

Fixes googleapis#2755
@sjhddh sjhddh force-pushed the fix/toolset-boundary-enforcement branch from 97edc7f to edb3e0c Compare April 21, 2026 06:10
@sjhddh
Copy link
Copy Markdown
Author

sjhddh commented Apr 21, 2026

@duwenxin99 Thanks! Fixed both:

  • Boundary violations on tools/call and prompts/get now return the same error message as genuinely missing tool/prompt (so clients can't distinguish the two cases) — applied across all four protocol versions.
  • Amended and force-pushed to drop the co-author trailer so the commit is signed by just my account for CLA.

Let me know if anything else needs polishing.

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.

MCP toolset boundary bypass: tools/call can invoke tools outside toolset (IDOR)

2 participants