Skip to content

Add RegisterTools API to McpClient for pre-populating tool cache#1590

Open
tarekgh wants to merge 3 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
Open

Add RegisterTools API to McpClient for pre-populating tool cache#1590
tarekgh wants to merge 3 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api

Conversation

@tarekgh
Copy link
Copy Markdown
Contributor

@tarekgh tarekgh commented May 20, 2026

Summary

This PR adds a RegisterTools API to McpClient that allows pre-populating the internal tool cache with tool definitions. This enables MCP clients to send \Mcp-Param-*\ HTTP headers (based on \x-mcp-header\ schema annotations) without requiring a prior \ListToolsAsync\ call.

Fixes #1577

Changes

API Addition

  • *\McpClient.RegisterTools(IEnumerable)* — registers tool definitions in the client's tool cache, enabling the transport to send \Mcp-Param-*\ headers for those tools

Implementation Details

  • Registered tools are added to the same _toolCache\ used by \ListToolsAsync\
  • A thread-safe \ConcurrentDictionary<string, byte>\ tracks registered tool names (used as a concurrent set)
  • \ToolCacheClearing\ (invoked by \ListToolsAsync) only removes server-discovered tools; registered tools survive cache clears
  • Fast path: when no tools are registered, _toolCache.Clear()\ is called directly
  • Tools with invalid \x-mcp-header\ annotations are validated and rejected with logging
  • Re-registering a tool with the same name overwrites the previous definition (last write wins)
  • If the server returns a tool with the same name as a registered tool, the server's definition overwrites in the cache, but the tool retains its registered/pinned status

Cache Interaction Behavior

  • Register → ListToolsAsync: registered tools survive the clear, server tools added alongside
  • ListToolsAsync → Register: both coexist in cache
  • Register → ListToolsAsync → Register: all registered tools survive, server tools refreshed

Tests

Unit Tests (11 tests)

  • Register then list — server tools repopulated correctly
  • Multiple ListToolsAsync cycles with registered tools
  • List then register ordering
  • Register → list → register again
  • Same name as server tool (server overwrites definition, pinned status preserved)
  • Invalid x-mcp-header schema rejection
  • Duplicate registration (last write wins)
  • Null argument validation
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)

HTTP Integration Tests (3 tests)

  • Core scenario: \RegisterTools\ → \CallToolAsync\ (NO \ListToolsAsync) → verify \Mcp-Param-Region\ and \Mcp-Param-Priority\ headers received by server
  • No headers sent without register or list
  • Registered tool headers survive \ListToolsAsync\ cache clear

This enables MCP clients to send Mcp-Param-* HTTP headers without
requiring a prior ListToolsAsync call, addressing issue modelcontextprotocol#1577.

Changes:
- Add RegisterTools abstract method to McpClient with XML documentation
- Implement RegisterTools in McpClientImpl with thread-safe
  ConcurrentDictionary for registered tool name tracking
- Modify ToolCacheClearing to preserve registered tools across
  ListToolsAsync calls while clearing server-discovered tools
- Add fast path optimization when no tools are registered
- Validate x-mcp-header annotations on registered tools

Tests:
- 11 unit tests covering cache interaction scenarios
- 3 HTTP integration tests verifying Mcp-Param-* headers are sent
  without ListToolsAsync
@tarekgh tarekgh requested review from halter73 and mikekistler May 20, 2026 17:46
@tarekgh tarekgh self-assigned this May 20, 2026
tarekgh and others added 2 commits May 20, 2026 10:59
Adding an abstract member to McpClient is a breaking change (CP0005)
because existing subclasses would fail to compile. Changed to virtual
with a default no-op implementation that validates the argument.
@tarekgh
Copy link
Copy Markdown
Contributor Author

tarekgh commented May 20, 2026

#1553

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.

Allow sending Mcp-Param-* headers without calling ListToolsAsync first

1 participant