Skip to content

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

Merged
tarekgh merged 13 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
May 26, 2026
Merged

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

Conversation

@tarekgh

@tarekghtarekgh commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds AddKnownTools, RemoveKnownTools, and ClearKnownTools APIs to McpClient that allow 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 Additions on McpClient

  • AddKnownTools(IEnumerable<Tool>) — registers tool definitions in the client's tool cache with all-or-nothing validation
  • RemoveKnownTools(IEnumerable<string>) — removes specific known tools by name
  • ClearKnownTools() — removes all known tools

Base virtual methods throw NotSupportedException (not abstract, to preserve API compatibility with PackageValidationBaselineVersion=1.0.0).

Implementation Details

  • Known tools are added to the same _toolCache used by ListToolsAsync
  • A thread-safe ConcurrentDictionary<string, byte> tracks known tool names (used as a concurrent set)
  • ToolCacheClearing (invoked by ListToolsAsync) only removes server-discovered tools; known tools survive cache clears
  • Fast path: when no known tools exist, _toolCache.Clear() is called directly
  • All-or-nothing validation: tools are validated for x-mcp-header correctness before any are added; ArgumentException thrown on invalid schemas
  • 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 known tool, the server's definition overwrites in the cache, but the tool retains its known/pinned status
  • LogDebug on cache miss during tools/call to help diagnose missing Mcp-Param-* headers

Cache Interaction Behavior

  • AddKnownTools -> ListToolsAsync: known tools survive the clear, server tools added alongside
  • ListToolsAsync -> AddKnownTools: both coexist in cache
  • AddKnownTools -> ListToolsAsync -> AddKnownTools: all known tools survive, server tools refreshed
  • RemoveKnownTools/ClearKnownTools: removes from both _registeredToolNames and _toolCache

Documentation

  • Conceptual docs section "Pre-loading tool definitions on the client" in docs/concepts/tools/tools.md
  • XML docs on all three methods with cache interaction details

Tests

Unit Tests (20 tests in McpClientAddKnownToolsTests.cs)

  • 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 (all-or-nothing)
  • Duplicate registration (last write wins)
  • Null argument validation (AddKnownTools, RemoveKnownTools)
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)
  • RemoveKnownTools — removed tool no longer survives ListToolsAsync
  • RemoveKnownTools — non-existent name is no-op
  • RemoveKnownTools — partial remove, other tools survive
  • ClearKnownTools — removes all, server tools unaffected
  • ClearKnownTools — empty is no-op
  • ClearKnownTools then AddKnownTools — works correctly
  • Partial-failure atomicity — [valid, invalid, valid] throws, nothing cached
  • Null element atomicity — null at index 1 throws, element 0 not cached

HTTP Integration Tests (6 tests in AddKnownToolsHeaderTests.cs)

  • Core scenario: AddKnownTools -> CallToolAsync (NO ListToolsAsync) -> verify Mcp-Param-Region and Mcp-Param-Priority headers received by server
  • No headers sent without register or list
  • Known tool headers survive ListToolsAsync cache clear
  • RemoveKnownTools -> no headers sent after removal
  • Staleness: register -> server returns [] -> ListToolsAsync -> call -> headers still sent
  • Last-write-wins: register schema A -> register schema B -> call -> headers reflect schema B only

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
@tarekghtarekgh self-assigned this May 20, 2026
tarekghand 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
ContributorAuthor

#1553

@halter73halter73 left a comment

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.

Thanks for tackling this! CI checked, and we're ahead of the other tier 1 SDKs here. TypeScript (modelcontextprotocol/typescript-sdk#2069) only landed Mcp-Method/Mcp-Name, Python has no SEP-2243 implementation at all, and Go shipped x-mcp-header (modelcontextprotocol/go-sdk#915) with the same silent-omission gap that #1577 calls out. So this PR is paving the path for everyone.

One thing I'd like to see addressed before merge but couldn't comment on inline because it's out of the diff range: the silent-miss path in McpClientImpl.SendRequestAsync. If the caller forgets to call either RegisterTools or ListToolsAsync, the cache lookup fails, no Tool is attached, and StreamableHttpClientSessionTransport ships the tools/call with no Mcp-Param-* headers — no warning, no log, no error. The server then sees a tools/call with no infra headers and may misroute (e.g. a proxy keyed on Mcp-Param-Region). Worth a LogDebug (or LogInformation, gated on the negotiated protocol version supporting SEP-2243) on the cache miss so it's at least diagnosable. Other SDKs have the same gap, but logging is cheap and a real footgun-mitigator.

Follow-up, not for this PR: I'd also like us to eventually offer a fully stateless way to flow parameter values into Mcp-Param-* headers — e.g. a Tool (or just a param → header map) passed directly on CallToolAsync / CallToolRequestOptions, with no client-wide cache mutation. The cache-priming approach in this PR is the right answer for the "I already know the schema from a previous session" scenario, but for callers who want per-call control (multi-tenant proxies, request-scoped routing, callers who don't want any client-global mutable state) the stateless variant is a cleaner fit and dovetails with what the Python folks are circling in modelcontextprotocol/python-sdk#1509. I'm happy to file a separate issue once this lands.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadtests/ModelContextProtocol.Tests/Client/McpClientRegisterToolsTests.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Tarek Mahmoud Sayed added 8 commits May 21, 2026 10:51
Clarifies that this is client-side cache priming, not server-side
tool registration. Renamed method, test classes, and test files.
…hen-commit
- Add RemoveKnownTools(IEnumerable<string>) to remove specific known tools
- Add ClearKnownTools() to remove all known tools
- Change AddKnownTools to validate all tools first, then commit all-or-nothing
- Throw ArgumentException on invalid x-mcp-header annotations instead of
silently skipping (ToolRejected still fires for logging parity)
- Remove trailing blank line in McpClient.cs
- Add 7 unit tests for Remove/Clear scenarios
- Add HTTP integration test for RemoveKnownTools header verification
Validate all tool names for null before removing any, matching the
all-or-nothing pattern used in AddKnownTools.
Add 'Pre-loading tool definitions on the client' section to
docs/concepts/tools/tools.md covering usage, cache behavior,
removal APIs, and validation semantics.
Prevents subclasses that don't override from silently swallowing
calls. All three methods (AddKnownTools, RemoveKnownTools,
ClearKnownTools) now throw with a descriptive message.
Clarify that known status is sticky for the McpClient lifetime and
point to RemoveKnownTools/ClearKnownTools for explicit removal.
- Staleness: register → server returns [] → headers still sent
- Partial-failure atomicity: [valid, invalid, valid] → nothing cached
- Null element atomicity: null at index 1 → nothing cached
- Last-write-wins: re-register with schema B → headers reflect B
When SendRequestAsync handles a tools/call request and the tool is not
found in the cache, log a Debug message suggesting AddKnownTools or
ListToolsAsync to populate the cache. This makes missing Mcp-Param-*
headers diagnosable without breaking existing behavior.
@tarekgh

Copy link
Copy Markdown
ContributorAuthor

@halter73 thanks for your feedback. I believe I addressed all your feedback. The remaining open discussions looks can be addressed outside this PR. Let me know if you have any more feedback.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs Outdated
Log a Warning (instead of Debug) when a tools/call request has no
cached tool definition, but only for StreamableHttpClientSessionTransport
where Mcp-Param-* headers are relevant. Pipe/stdio transports do not
emit this warning since headers do not apply.
Added tests:
- Pipe transport: cache miss does NOT log a warning
- HTTP transport: cache miss DOES log a warning
@tarekgh
tarekgh merged commit 157f855 into modelcontextprotocol:mainMay 26, 2026
17 of 18 checks passed
@jeffhandleyjeffhandley mentioned this pull request Jul 28, 2026
Sign up for freeto 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

3 participants

@tarekgh@halter73@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add RegisterTools API to McpClient for pre-populating tool cache by tarekgh · Pull Request #1590 · modelcontextprotocol/csharp-sdk · GitHub
Skip to content

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

Merged
tarekgh merged 13 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
May 26, 2026
Merged

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

Conversation

@tarekgh

@tarekghtarekgh commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds AddKnownTools, RemoveKnownTools, and ClearKnownTools APIs to McpClient that allow 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 Additions on McpClient

  • AddKnownTools(IEnumerable<Tool>) — registers tool definitions in the client's tool cache with all-or-nothing validation
  • RemoveKnownTools(IEnumerable<string>) — removes specific known tools by name
  • ClearKnownTools() — removes all known tools

Base virtual methods throw NotSupportedException (not abstract, to preserve API compatibility with PackageValidationBaselineVersion=1.0.0).

Implementation Details

  • Known tools are added to the same _toolCache used by ListToolsAsync
  • A thread-safe ConcurrentDictionary<string, byte> tracks known tool names (used as a concurrent set)
  • ToolCacheClearing (invoked by ListToolsAsync) only removes server-discovered tools; known tools survive cache clears
  • Fast path: when no known tools exist, _toolCache.Clear() is called directly
  • All-or-nothing validation: tools are validated for x-mcp-header correctness before any are added; ArgumentException thrown on invalid schemas
  • 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 known tool, the server's definition overwrites in the cache, but the tool retains its known/pinned status
  • LogDebug on cache miss during tools/call to help diagnose missing Mcp-Param-* headers

Cache Interaction Behavior

  • AddKnownTools -> ListToolsAsync: known tools survive the clear, server tools added alongside
  • ListToolsAsync -> AddKnownTools: both coexist in cache
  • AddKnownTools -> ListToolsAsync -> AddKnownTools: all known tools survive, server tools refreshed
  • RemoveKnownTools/ClearKnownTools: removes from both _registeredToolNames and _toolCache

Documentation

  • Conceptual docs section "Pre-loading tool definitions on the client" in docs/concepts/tools/tools.md
  • XML docs on all three methods with cache interaction details

Tests

Unit Tests (20 tests in McpClientAddKnownToolsTests.cs)

  • 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 (all-or-nothing)
  • Duplicate registration (last write wins)
  • Null argument validation (AddKnownTools, RemoveKnownTools)
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)
  • RemoveKnownTools — removed tool no longer survives ListToolsAsync
  • RemoveKnownTools — non-existent name is no-op
  • RemoveKnownTools — partial remove, other tools survive
  • ClearKnownTools — removes all, server tools unaffected
  • ClearKnownTools — empty is no-op
  • ClearKnownTools then AddKnownTools — works correctly
  • Partial-failure atomicity — [valid, invalid, valid] throws, nothing cached
  • Null element atomicity — null at index 1 throws, element 0 not cached

HTTP Integration Tests (6 tests in AddKnownToolsHeaderTests.cs)

  • Core scenario: AddKnownTools -> CallToolAsync (NO ListToolsAsync) -> verify Mcp-Param-Region and Mcp-Param-Priority headers received by server
  • No headers sent without register or list
  • Known tool headers survive ListToolsAsync cache clear
  • RemoveKnownTools -> no headers sent after removal
  • Staleness: register -> server returns [] -> ListToolsAsync -> call -> headers still sent
  • Last-write-wins: register schema A -> register schema B -> call -> headers reflect schema B only

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
@tarekghtarekgh self-assigned this May 20, 2026
tarekghand 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
ContributorAuthor

#1553

@halter73halter73 left a comment

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.

Thanks for tackling this! CI checked, and we're ahead of the other tier 1 SDKs here. TypeScript (modelcontextprotocol/typescript-sdk#2069) only landed Mcp-Method/Mcp-Name, Python has no SEP-2243 implementation at all, and Go shipped x-mcp-header (modelcontextprotocol/go-sdk#915) with the same silent-omission gap that #1577 calls out. So this PR is paving the path for everyone.

One thing I'd like to see addressed before merge but couldn't comment on inline because it's out of the diff range: the silent-miss path in McpClientImpl.SendRequestAsync. If the caller forgets to call either RegisterTools or ListToolsAsync, the cache lookup fails, no Tool is attached, and StreamableHttpClientSessionTransport ships the tools/call with no Mcp-Param-* headers — no warning, no log, no error. The server then sees a tools/call with no infra headers and may misroute (e.g. a proxy keyed on Mcp-Param-Region). Worth a LogDebug (or LogInformation, gated on the negotiated protocol version supporting SEP-2243) on the cache miss so it's at least diagnosable. Other SDKs have the same gap, but logging is cheap and a real footgun-mitigator.

Follow-up, not for this PR: I'd also like us to eventually offer a fully stateless way to flow parameter values into Mcp-Param-* headers — e.g. a Tool (or just a param → header map) passed directly on CallToolAsync / CallToolRequestOptions, with no client-wide cache mutation. The cache-priming approach in this PR is the right answer for the "I already know the schema from a previous session" scenario, but for callers who want per-call control (multi-tenant proxies, request-scoped routing, callers who don't want any client-global mutable state) the stateless variant is a cleaner fit and dovetails with what the Python folks are circling in modelcontextprotocol/python-sdk#1509. I'm happy to file a separate issue once this lands.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadtests/ModelContextProtocol.Tests/Client/McpClientRegisterToolsTests.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Tarek Mahmoud Sayed added 8 commits May 21, 2026 10:51
Clarifies that this is client-side cache priming, not server-side
tool registration. Renamed method, test classes, and test files.
…hen-commit
- Add RemoveKnownTools(IEnumerable<string>) to remove specific known tools
- Add ClearKnownTools() to remove all known tools
- Change AddKnownTools to validate all tools first, then commit all-or-nothing
- Throw ArgumentException on invalid x-mcp-header annotations instead of
silently skipping (ToolRejected still fires for logging parity)
- Remove trailing blank line in McpClient.cs
- Add 7 unit tests for Remove/Clear scenarios
- Add HTTP integration test for RemoveKnownTools header verification
Validate all tool names for null before removing any, matching the
all-or-nothing pattern used in AddKnownTools.
Add 'Pre-loading tool definitions on the client' section to
docs/concepts/tools/tools.md covering usage, cache behavior,
removal APIs, and validation semantics.
Prevents subclasses that don't override from silently swallowing
calls. All three methods (AddKnownTools, RemoveKnownTools,
ClearKnownTools) now throw with a descriptive message.
Clarify that known status is sticky for the McpClient lifetime and
point to RemoveKnownTools/ClearKnownTools for explicit removal.
- Staleness: register → server returns [] → headers still sent
- Partial-failure atomicity: [valid, invalid, valid] → nothing cached
- Null element atomicity: null at index 1 → nothing cached
- Last-write-wins: re-register with schema B → headers reflect B
When SendRequestAsync handles a tools/call request and the tool is not
found in the cache, log a Debug message suggesting AddKnownTools or
ListToolsAsync to populate the cache. This makes missing Mcp-Param-*
headers diagnosable without breaking existing behavior.
@tarekgh

Copy link
Copy Markdown
ContributorAuthor

@halter73 thanks for your feedback. I believe I addressed all your feedback. The remaining open discussions looks can be addressed outside this PR. Let me know if you have any more feedback.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs Outdated
Log a Warning (instead of Debug) when a tools/call request has no
cached tool definition, but only for StreamableHttpClientSessionTransport
where Mcp-Param-* headers are relevant. Pipe/stdio transports do not
emit this warning since headers do not apply.
Added tests:
- Pipe transport: cache miss does NOT log a warning
- HTTP transport: cache miss DOES log a warning
@tarekgh
tarekgh merged commit 157f855 into modelcontextprotocol:mainMay 26, 2026
17 of 18 checks passed
@jeffhandleyjeffhandley mentioned this pull request Jul 28, 2026
Sign up for freeto 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

3 participants

@tarekgh@halter73@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add RegisterTools API to McpClient for pre-populating tool cache by tarekgh · Pull Request #1590 · modelcontextprotocol/csharp-sdk · GitHub
Skip to content

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

Merged
tarekgh merged 13 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
May 26, 2026
Merged

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

Conversation

@tarekgh

@tarekghtarekgh commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds AddKnownTools, RemoveKnownTools, and ClearKnownTools APIs to McpClient that allow 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 Additions on McpClient

  • AddKnownTools(IEnumerable<Tool>) — registers tool definitions in the client's tool cache with all-or-nothing validation
  • RemoveKnownTools(IEnumerable<string>) — removes specific known tools by name
  • ClearKnownTools() — removes all known tools

Base virtual methods throw NotSupportedException (not abstract, to preserve API compatibility with PackageValidationBaselineVersion=1.0.0).

Implementation Details

  • Known tools are added to the same _toolCache used by ListToolsAsync
  • A thread-safe ConcurrentDictionary<string, byte> tracks known tool names (used as a concurrent set)
  • ToolCacheClearing (invoked by ListToolsAsync) only removes server-discovered tools; known tools survive cache clears
  • Fast path: when no known tools exist, _toolCache.Clear() is called directly
  • All-or-nothing validation: tools are validated for x-mcp-header correctness before any are added; ArgumentException thrown on invalid schemas
  • 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 known tool, the server's definition overwrites in the cache, but the tool retains its known/pinned status
  • LogDebug on cache miss during tools/call to help diagnose missing Mcp-Param-* headers

Cache Interaction Behavior

  • AddKnownTools -> ListToolsAsync: known tools survive the clear, server tools added alongside
  • ListToolsAsync -> AddKnownTools: both coexist in cache
  • AddKnownTools -> ListToolsAsync -> AddKnownTools: all known tools survive, server tools refreshed
  • RemoveKnownTools/ClearKnownTools: removes from both _registeredToolNames and _toolCache

Documentation

  • Conceptual docs section "Pre-loading tool definitions on the client" in docs/concepts/tools/tools.md
  • XML docs on all three methods with cache interaction details

Tests

Unit Tests (20 tests in McpClientAddKnownToolsTests.cs)

  • 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 (all-or-nothing)
  • Duplicate registration (last write wins)
  • Null argument validation (AddKnownTools, RemoveKnownTools)
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)
  • RemoveKnownTools — removed tool no longer survives ListToolsAsync
  • RemoveKnownTools — non-existent name is no-op
  • RemoveKnownTools — partial remove, other tools survive
  • ClearKnownTools — removes all, server tools unaffected
  • ClearKnownTools — empty is no-op
  • ClearKnownTools then AddKnownTools — works correctly
  • Partial-failure atomicity — [valid, invalid, valid] throws, nothing cached
  • Null element atomicity — null at index 1 throws, element 0 not cached

HTTP Integration Tests (6 tests in AddKnownToolsHeaderTests.cs)

  • Core scenario: AddKnownTools -> CallToolAsync (NO ListToolsAsync) -> verify Mcp-Param-Region and Mcp-Param-Priority headers received by server
  • No headers sent without register or list
  • Known tool headers survive ListToolsAsync cache clear
  • RemoveKnownTools -> no headers sent after removal
  • Staleness: register -> server returns [] -> ListToolsAsync -> call -> headers still sent
  • Last-write-wins: register schema A -> register schema B -> call -> headers reflect schema B only

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
@tarekghtarekgh self-assigned this May 20, 2026
tarekghand 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
ContributorAuthor

#1553

@halter73halter73 left a comment

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.

Thanks for tackling this! CI checked, and we're ahead of the other tier 1 SDKs here. TypeScript (modelcontextprotocol/typescript-sdk#2069) only landed Mcp-Method/Mcp-Name, Python has no SEP-2243 implementation at all, and Go shipped x-mcp-header (modelcontextprotocol/go-sdk#915) with the same silent-omission gap that #1577 calls out. So this PR is paving the path for everyone.

One thing I'd like to see addressed before merge but couldn't comment on inline because it's out of the diff range: the silent-miss path in McpClientImpl.SendRequestAsync. If the caller forgets to call either RegisterTools or ListToolsAsync, the cache lookup fails, no Tool is attached, and StreamableHttpClientSessionTransport ships the tools/call with no Mcp-Param-* headers — no warning, no log, no error. The server then sees a tools/call with no infra headers and may misroute (e.g. a proxy keyed on Mcp-Param-Region). Worth a LogDebug (or LogInformation, gated on the negotiated protocol version supporting SEP-2243) on the cache miss so it's at least diagnosable. Other SDKs have the same gap, but logging is cheap and a real footgun-mitigator.

Follow-up, not for this PR: I'd also like us to eventually offer a fully stateless way to flow parameter values into Mcp-Param-* headers — e.g. a Tool (or just a param → header map) passed directly on CallToolAsync / CallToolRequestOptions, with no client-wide cache mutation. The cache-priming approach in this PR is the right answer for the "I already know the schema from a previous session" scenario, but for callers who want per-call control (multi-tenant proxies, request-scoped routing, callers who don't want any client-global mutable state) the stateless variant is a cleaner fit and dovetails with what the Python folks are circling in modelcontextprotocol/python-sdk#1509. I'm happy to file a separate issue once this lands.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadtests/ModelContextProtocol.Tests/Client/McpClientRegisterToolsTests.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Tarek Mahmoud Sayed added 8 commits May 21, 2026 10:51
Clarifies that this is client-side cache priming, not server-side
tool registration. Renamed method, test classes, and test files.
…hen-commit
- Add RemoveKnownTools(IEnumerable<string>) to remove specific known tools
- Add ClearKnownTools() to remove all known tools
- Change AddKnownTools to validate all tools first, then commit all-or-nothing
- Throw ArgumentException on invalid x-mcp-header annotations instead of
silently skipping (ToolRejected still fires for logging parity)
- Remove trailing blank line in McpClient.cs
- Add 7 unit tests for Remove/Clear scenarios
- Add HTTP integration test for RemoveKnownTools header verification
Validate all tool names for null before removing any, matching the
all-or-nothing pattern used in AddKnownTools.
Add 'Pre-loading tool definitions on the client' section to
docs/concepts/tools/tools.md covering usage, cache behavior,
removal APIs, and validation semantics.
Prevents subclasses that don't override from silently swallowing
calls. All three methods (AddKnownTools, RemoveKnownTools,
ClearKnownTools) now throw with a descriptive message.
Clarify that known status is sticky for the McpClient lifetime and
point to RemoveKnownTools/ClearKnownTools for explicit removal.
- Staleness: register → server returns [] → headers still sent
- Partial-failure atomicity: [valid, invalid, valid] → nothing cached
- Null element atomicity: null at index 1 → nothing cached
- Last-write-wins: re-register with schema B → headers reflect B
When SendRequestAsync handles a tools/call request and the tool is not
found in the cache, log a Debug message suggesting AddKnownTools or
ListToolsAsync to populate the cache. This makes missing Mcp-Param-*
headers diagnosable without breaking existing behavior.
@tarekgh

Copy link
Copy Markdown
ContributorAuthor

@halter73 thanks for your feedback. I believe I addressed all your feedback. The remaining open discussions looks can be addressed outside this PR. Let me know if you have any more feedback.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs Outdated
Log a Warning (instead of Debug) when a tools/call request has no
cached tool definition, but only for StreamableHttpClientSessionTransport
where Mcp-Param-* headers are relevant. Pipe/stdio transports do not
emit this warning since headers do not apply.
Added tests:
- Pipe transport: cache miss does NOT log a warning
- HTTP transport: cache miss DOES log a warning
@tarekgh
tarekgh merged commit 157f855 into modelcontextprotocol:mainMay 26, 2026
17 of 18 checks passed
@jeffhandleyjeffhandley mentioned this pull request Jul 28, 2026
Sign up for freeto 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

3 participants

@tarekgh@halter73@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add RegisterTools API to McpClient for pre-populating tool cache by tarekgh · Pull Request #1590 · modelcontextprotocol/csharp-sdk · GitHub
Skip to content

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

Merged
tarekgh merged 13 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
May 26, 2026
Merged

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

Conversation

@tarekgh

@tarekghtarekgh commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds AddKnownTools, RemoveKnownTools, and ClearKnownTools APIs to McpClient that allow 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 Additions on McpClient

  • AddKnownTools(IEnumerable<Tool>) — registers tool definitions in the client's tool cache with all-or-nothing validation
  • RemoveKnownTools(IEnumerable<string>) — removes specific known tools by name
  • ClearKnownTools() — removes all known tools

Base virtual methods throw NotSupportedException (not abstract, to preserve API compatibility with PackageValidationBaselineVersion=1.0.0).

Implementation Details

  • Known tools are added to the same _toolCache used by ListToolsAsync
  • A thread-safe ConcurrentDictionary<string, byte> tracks known tool names (used as a concurrent set)
  • ToolCacheClearing (invoked by ListToolsAsync) only removes server-discovered tools; known tools survive cache clears
  • Fast path: when no known tools exist, _toolCache.Clear() is called directly
  • All-or-nothing validation: tools are validated for x-mcp-header correctness before any are added; ArgumentException thrown on invalid schemas
  • 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 known tool, the server's definition overwrites in the cache, but the tool retains its known/pinned status
  • LogDebug on cache miss during tools/call to help diagnose missing Mcp-Param-* headers

Cache Interaction Behavior

  • AddKnownTools -> ListToolsAsync: known tools survive the clear, server tools added alongside
  • ListToolsAsync -> AddKnownTools: both coexist in cache
  • AddKnownTools -> ListToolsAsync -> AddKnownTools: all known tools survive, server tools refreshed
  • RemoveKnownTools/ClearKnownTools: removes from both _registeredToolNames and _toolCache

Documentation

  • Conceptual docs section "Pre-loading tool definitions on the client" in docs/concepts/tools/tools.md
  • XML docs on all three methods with cache interaction details

Tests

Unit Tests (20 tests in McpClientAddKnownToolsTests.cs)

  • 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 (all-or-nothing)
  • Duplicate registration (last write wins)
  • Null argument validation (AddKnownTools, RemoveKnownTools)
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)
  • RemoveKnownTools — removed tool no longer survives ListToolsAsync
  • RemoveKnownTools — non-existent name is no-op
  • RemoveKnownTools — partial remove, other tools survive
  • ClearKnownTools — removes all, server tools unaffected
  • ClearKnownTools — empty is no-op
  • ClearKnownTools then AddKnownTools — works correctly
  • Partial-failure atomicity — [valid, invalid, valid] throws, nothing cached
  • Null element atomicity — null at index 1 throws, element 0 not cached

HTTP Integration Tests (6 tests in AddKnownToolsHeaderTests.cs)

  • Core scenario: AddKnownTools -> CallToolAsync (NO ListToolsAsync) -> verify Mcp-Param-Region and Mcp-Param-Priority headers received by server
  • No headers sent without register or list
  • Known tool headers survive ListToolsAsync cache clear
  • RemoveKnownTools -> no headers sent after removal
  • Staleness: register -> server returns [] -> ListToolsAsync -> call -> headers still sent
  • Last-write-wins: register schema A -> register schema B -> call -> headers reflect schema B only

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
@tarekghtarekgh self-assigned this May 20, 2026
tarekghand 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
ContributorAuthor

#1553

@halter73halter73 left a comment

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.

Thanks for tackling this! CI checked, and we're ahead of the other tier 1 SDKs here. TypeScript (modelcontextprotocol/typescript-sdk#2069) only landed Mcp-Method/Mcp-Name, Python has no SEP-2243 implementation at all, and Go shipped x-mcp-header (modelcontextprotocol/go-sdk#915) with the same silent-omission gap that #1577 calls out. So this PR is paving the path for everyone.

One thing I'd like to see addressed before merge but couldn't comment on inline because it's out of the diff range: the silent-miss path in McpClientImpl.SendRequestAsync. If the caller forgets to call either RegisterTools or ListToolsAsync, the cache lookup fails, no Tool is attached, and StreamableHttpClientSessionTransport ships the tools/call with no Mcp-Param-* headers — no warning, no log, no error. The server then sees a tools/call with no infra headers and may misroute (e.g. a proxy keyed on Mcp-Param-Region). Worth a LogDebug (or LogInformation, gated on the negotiated protocol version supporting SEP-2243) on the cache miss so it's at least diagnosable. Other SDKs have the same gap, but logging is cheap and a real footgun-mitigator.

Follow-up, not for this PR: I'd also like us to eventually offer a fully stateless way to flow parameter values into Mcp-Param-* headers — e.g. a Tool (or just a param → header map) passed directly on CallToolAsync / CallToolRequestOptions, with no client-wide cache mutation. The cache-priming approach in this PR is the right answer for the "I already know the schema from a previous session" scenario, but for callers who want per-call control (multi-tenant proxies, request-scoped routing, callers who don't want any client-global mutable state) the stateless variant is a cleaner fit and dovetails with what the Python folks are circling in modelcontextprotocol/python-sdk#1509. I'm happy to file a separate issue once this lands.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadtests/ModelContextProtocol.Tests/Client/McpClientRegisterToolsTests.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Tarek Mahmoud Sayed added 8 commits May 21, 2026 10:51
Clarifies that this is client-side cache priming, not server-side
tool registration. Renamed method, test classes, and test files.
…hen-commit
- Add RemoveKnownTools(IEnumerable<string>) to remove specific known tools
- Add ClearKnownTools() to remove all known tools
- Change AddKnownTools to validate all tools first, then commit all-or-nothing
- Throw ArgumentException on invalid x-mcp-header annotations instead of
silently skipping (ToolRejected still fires for logging parity)
- Remove trailing blank line in McpClient.cs
- Add 7 unit tests for Remove/Clear scenarios
- Add HTTP integration test for RemoveKnownTools header verification
Validate all tool names for null before removing any, matching the
all-or-nothing pattern used in AddKnownTools.
Add 'Pre-loading tool definitions on the client' section to
docs/concepts/tools/tools.md covering usage, cache behavior,
removal APIs, and validation semantics.
Prevents subclasses that don't override from silently swallowing
calls. All three methods (AddKnownTools, RemoveKnownTools,
ClearKnownTools) now throw with a descriptive message.
Clarify that known status is sticky for the McpClient lifetime and
point to RemoveKnownTools/ClearKnownTools for explicit removal.
- Staleness: register → server returns [] → headers still sent
- Partial-failure atomicity: [valid, invalid, valid] → nothing cached
- Null element atomicity: null at index 1 → nothing cached
- Last-write-wins: re-register with schema B → headers reflect B
When SendRequestAsync handles a tools/call request and the tool is not
found in the cache, log a Debug message suggesting AddKnownTools or
ListToolsAsync to populate the cache. This makes missing Mcp-Param-*
headers diagnosable without breaking existing behavior.
@tarekgh

Copy link
Copy Markdown
ContributorAuthor

@halter73 thanks for your feedback. I believe I addressed all your feedback. The remaining open discussions looks can be addressed outside this PR. Let me know if you have any more feedback.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs Outdated
Log a Warning (instead of Debug) when a tools/call request has no
cached tool definition, but only for StreamableHttpClientSessionTransport
where Mcp-Param-* headers are relevant. Pipe/stdio transports do not
emit this warning since headers do not apply.
Added tests:
- Pipe transport: cache miss does NOT log a warning
- HTTP transport: cache miss DOES log a warning
@tarekgh
tarekgh merged commit 157f855 into modelcontextprotocol:mainMay 26, 2026
17 of 18 checks passed
@jeffhandleyjeffhandley mentioned this pull request Jul 28, 2026
Sign up for freeto 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

3 participants

@tarekgh@halter73@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add RegisterTools API to McpClient for pre-populating tool cache by tarekgh · Pull Request #1590 · modelcontextprotocol/csharp-sdk · GitHub
Skip to content

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

Merged
tarekgh merged 13 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
May 26, 2026
Merged

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

Conversation

@tarekgh

@tarekghtarekgh commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds AddKnownTools, RemoveKnownTools, and ClearKnownTools APIs to McpClient that allow 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 Additions on McpClient

  • AddKnownTools(IEnumerable<Tool>) — registers tool definitions in the client's tool cache with all-or-nothing validation
  • RemoveKnownTools(IEnumerable<string>) — removes specific known tools by name
  • ClearKnownTools() — removes all known tools

Base virtual methods throw NotSupportedException (not abstract, to preserve API compatibility with PackageValidationBaselineVersion=1.0.0).

Implementation Details

  • Known tools are added to the same _toolCache used by ListToolsAsync
  • A thread-safe ConcurrentDictionary<string, byte> tracks known tool names (used as a concurrent set)
  • ToolCacheClearing (invoked by ListToolsAsync) only removes server-discovered tools; known tools survive cache clears
  • Fast path: when no known tools exist, _toolCache.Clear() is called directly
  • All-or-nothing validation: tools are validated for x-mcp-header correctness before any are added; ArgumentException thrown on invalid schemas
  • 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 known tool, the server's definition overwrites in the cache, but the tool retains its known/pinned status
  • LogDebug on cache miss during tools/call to help diagnose missing Mcp-Param-* headers

Cache Interaction Behavior

  • AddKnownTools -> ListToolsAsync: known tools survive the clear, server tools added alongside
  • ListToolsAsync -> AddKnownTools: both coexist in cache
  • AddKnownTools -> ListToolsAsync -> AddKnownTools: all known tools survive, server tools refreshed
  • RemoveKnownTools/ClearKnownTools: removes from both _registeredToolNames and _toolCache

Documentation

  • Conceptual docs section "Pre-loading tool definitions on the client" in docs/concepts/tools/tools.md
  • XML docs on all three methods with cache interaction details

Tests

Unit Tests (20 tests in McpClientAddKnownToolsTests.cs)

  • 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 (all-or-nothing)
  • Duplicate registration (last write wins)
  • Null argument validation (AddKnownTools, RemoveKnownTools)
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)
  • RemoveKnownTools — removed tool no longer survives ListToolsAsync
  • RemoveKnownTools — non-existent name is no-op
  • RemoveKnownTools — partial remove, other tools survive
  • ClearKnownTools — removes all, server tools unaffected
  • ClearKnownTools — empty is no-op
  • ClearKnownTools then AddKnownTools — works correctly
  • Partial-failure atomicity — [valid, invalid, valid] throws, nothing cached
  • Null element atomicity — null at index 1 throws, element 0 not cached

HTTP Integration Tests (6 tests in AddKnownToolsHeaderTests.cs)

  • Core scenario: AddKnownTools -> CallToolAsync (NO ListToolsAsync) -> verify Mcp-Param-Region and Mcp-Param-Priority headers received by server
  • No headers sent without register or list
  • Known tool headers survive ListToolsAsync cache clear
  • RemoveKnownTools -> no headers sent after removal
  • Staleness: register -> server returns [] -> ListToolsAsync -> call -> headers still sent
  • Last-write-wins: register schema A -> register schema B -> call -> headers reflect schema B only

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
@tarekghtarekgh self-assigned this May 20, 2026
tarekghand 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
ContributorAuthor

#1553

@halter73halter73 left a comment

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.

Thanks for tackling this! CI checked, and we're ahead of the other tier 1 SDKs here. TypeScript (modelcontextprotocol/typescript-sdk#2069) only landed Mcp-Method/Mcp-Name, Python has no SEP-2243 implementation at all, and Go shipped x-mcp-header (modelcontextprotocol/go-sdk#915) with the same silent-omission gap that #1577 calls out. So this PR is paving the path for everyone.

One thing I'd like to see addressed before merge but couldn't comment on inline because it's out of the diff range: the silent-miss path in McpClientImpl.SendRequestAsync. If the caller forgets to call either RegisterTools or ListToolsAsync, the cache lookup fails, no Tool is attached, and StreamableHttpClientSessionTransport ships the tools/call with no Mcp-Param-* headers — no warning, no log, no error. The server then sees a tools/call with no infra headers and may misroute (e.g. a proxy keyed on Mcp-Param-Region). Worth a LogDebug (or LogInformation, gated on the negotiated protocol version supporting SEP-2243) on the cache miss so it's at least diagnosable. Other SDKs have the same gap, but logging is cheap and a real footgun-mitigator.

Follow-up, not for this PR: I'd also like us to eventually offer a fully stateless way to flow parameter values into Mcp-Param-* headers — e.g. a Tool (or just a param → header map) passed directly on CallToolAsync / CallToolRequestOptions, with no client-wide cache mutation. The cache-priming approach in this PR is the right answer for the "I already know the schema from a previous session" scenario, but for callers who want per-call control (multi-tenant proxies, request-scoped routing, callers who don't want any client-global mutable state) the stateless variant is a cleaner fit and dovetails with what the Python folks are circling in modelcontextprotocol/python-sdk#1509. I'm happy to file a separate issue once this lands.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadtests/ModelContextProtocol.Tests/Client/McpClientRegisterToolsTests.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Tarek Mahmoud Sayed added 8 commits May 21, 2026 10:51
Clarifies that this is client-side cache priming, not server-side
tool registration. Renamed method, test classes, and test files.
…hen-commit
- Add RemoveKnownTools(IEnumerable<string>) to remove specific known tools
- Add ClearKnownTools() to remove all known tools
- Change AddKnownTools to validate all tools first, then commit all-or-nothing
- Throw ArgumentException on invalid x-mcp-header annotations instead of
silently skipping (ToolRejected still fires for logging parity)
- Remove trailing blank line in McpClient.cs
- Add 7 unit tests for Remove/Clear scenarios
- Add HTTP integration test for RemoveKnownTools header verification
Validate all tool names for null before removing any, matching the
all-or-nothing pattern used in AddKnownTools.
Add 'Pre-loading tool definitions on the client' section to
docs/concepts/tools/tools.md covering usage, cache behavior,
removal APIs, and validation semantics.
Prevents subclasses that don't override from silently swallowing
calls. All three methods (AddKnownTools, RemoveKnownTools,
ClearKnownTools) now throw with a descriptive message.
Clarify that known status is sticky for the McpClient lifetime and
point to RemoveKnownTools/ClearKnownTools for explicit removal.
- Staleness: register → server returns [] → headers still sent
- Partial-failure atomicity: [valid, invalid, valid] → nothing cached
- Null element atomicity: null at index 1 → nothing cached
- Last-write-wins: re-register with schema B → headers reflect B
When SendRequestAsync handles a tools/call request and the tool is not
found in the cache, log a Debug message suggesting AddKnownTools or
ListToolsAsync to populate the cache. This makes missing Mcp-Param-*
headers diagnosable without breaking existing behavior.
@tarekgh

Copy link
Copy Markdown
ContributorAuthor

@halter73 thanks for your feedback. I believe I addressed all your feedback. The remaining open discussions looks can be addressed outside this PR. Let me know if you have any more feedback.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs Outdated
Log a Warning (instead of Debug) when a tools/call request has no
cached tool definition, but only for StreamableHttpClientSessionTransport
where Mcp-Param-* headers are relevant. Pipe/stdio transports do not
emit this warning since headers do not apply.
Added tests:
- Pipe transport: cache miss does NOT log a warning
- HTTP transport: cache miss DOES log a warning
@tarekgh
tarekgh merged commit 157f855 into modelcontextprotocol:mainMay 26, 2026
17 of 18 checks passed
@jeffhandleyjeffhandley mentioned this pull request Jul 28, 2026
Sign up for freeto 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

3 participants

@tarekgh@halter73@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add RegisterTools API to McpClient for pre-populating tool cache by tarekgh · Pull Request #1590 · modelcontextprotocol/csharp-sdk · GitHub
Skip to content

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

Merged
tarekgh merged 13 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
May 26, 2026
Merged

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

Conversation

@tarekgh

@tarekghtarekgh commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds AddKnownTools, RemoveKnownTools, and ClearKnownTools APIs to McpClient that allow 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 Additions on McpClient

  • AddKnownTools(IEnumerable<Tool>) — registers tool definitions in the client's tool cache with all-or-nothing validation
  • RemoveKnownTools(IEnumerable<string>) — removes specific known tools by name
  • ClearKnownTools() — removes all known tools

Base virtual methods throw NotSupportedException (not abstract, to preserve API compatibility with PackageValidationBaselineVersion=1.0.0).

Implementation Details

  • Known tools are added to the same _toolCache used by ListToolsAsync
  • A thread-safe ConcurrentDictionary<string, byte> tracks known tool names (used as a concurrent set)
  • ToolCacheClearing (invoked by ListToolsAsync) only removes server-discovered tools; known tools survive cache clears
  • Fast path: when no known tools exist, _toolCache.Clear() is called directly
  • All-or-nothing validation: tools are validated for x-mcp-header correctness before any are added; ArgumentException thrown on invalid schemas
  • 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 known tool, the server's definition overwrites in the cache, but the tool retains its known/pinned status
  • LogDebug on cache miss during tools/call to help diagnose missing Mcp-Param-* headers

Cache Interaction Behavior

  • AddKnownTools -> ListToolsAsync: known tools survive the clear, server tools added alongside
  • ListToolsAsync -> AddKnownTools: both coexist in cache
  • AddKnownTools -> ListToolsAsync -> AddKnownTools: all known tools survive, server tools refreshed
  • RemoveKnownTools/ClearKnownTools: removes from both _registeredToolNames and _toolCache

Documentation

  • Conceptual docs section "Pre-loading tool definitions on the client" in docs/concepts/tools/tools.md
  • XML docs on all three methods with cache interaction details

Tests

Unit Tests (20 tests in McpClientAddKnownToolsTests.cs)

  • 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 (all-or-nothing)
  • Duplicate registration (last write wins)
  • Null argument validation (AddKnownTools, RemoveKnownTools)
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)
  • RemoveKnownTools — removed tool no longer survives ListToolsAsync
  • RemoveKnownTools — non-existent name is no-op
  • RemoveKnownTools — partial remove, other tools survive
  • ClearKnownTools — removes all, server tools unaffected
  • ClearKnownTools — empty is no-op
  • ClearKnownTools then AddKnownTools — works correctly
  • Partial-failure atomicity — [valid, invalid, valid] throws, nothing cached
  • Null element atomicity — null at index 1 throws, element 0 not cached

HTTP Integration Tests (6 tests in AddKnownToolsHeaderTests.cs)

  • Core scenario: AddKnownTools -> CallToolAsync (NO ListToolsAsync) -> verify Mcp-Param-Region and Mcp-Param-Priority headers received by server
  • No headers sent without register or list
  • Known tool headers survive ListToolsAsync cache clear
  • RemoveKnownTools -> no headers sent after removal
  • Staleness: register -> server returns [] -> ListToolsAsync -> call -> headers still sent
  • Last-write-wins: register schema A -> register schema B -> call -> headers reflect schema B only

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
@tarekghtarekgh self-assigned this May 20, 2026
tarekghand 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
ContributorAuthor

#1553

@halter73halter73 left a comment

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.

Thanks for tackling this! CI checked, and we're ahead of the other tier 1 SDKs here. TypeScript (modelcontextprotocol/typescript-sdk#2069) only landed Mcp-Method/Mcp-Name, Python has no SEP-2243 implementation at all, and Go shipped x-mcp-header (modelcontextprotocol/go-sdk#915) with the same silent-omission gap that #1577 calls out. So this PR is paving the path for everyone.

One thing I'd like to see addressed before merge but couldn't comment on inline because it's out of the diff range: the silent-miss path in McpClientImpl.SendRequestAsync. If the caller forgets to call either RegisterTools or ListToolsAsync, the cache lookup fails, no Tool is attached, and StreamableHttpClientSessionTransport ships the tools/call with no Mcp-Param-* headers — no warning, no log, no error. The server then sees a tools/call with no infra headers and may misroute (e.g. a proxy keyed on Mcp-Param-Region). Worth a LogDebug (or LogInformation, gated on the negotiated protocol version supporting SEP-2243) on the cache miss so it's at least diagnosable. Other SDKs have the same gap, but logging is cheap and a real footgun-mitigator.

Follow-up, not for this PR: I'd also like us to eventually offer a fully stateless way to flow parameter values into Mcp-Param-* headers — e.g. a Tool (or just a param → header map) passed directly on CallToolAsync / CallToolRequestOptions, with no client-wide cache mutation. The cache-priming approach in this PR is the right answer for the "I already know the schema from a previous session" scenario, but for callers who want per-call control (multi-tenant proxies, request-scoped routing, callers who don't want any client-global mutable state) the stateless variant is a cleaner fit and dovetails with what the Python folks are circling in modelcontextprotocol/python-sdk#1509. I'm happy to file a separate issue once this lands.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadtests/ModelContextProtocol.Tests/Client/McpClientRegisterToolsTests.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Tarek Mahmoud Sayed added 8 commits May 21, 2026 10:51
Clarifies that this is client-side cache priming, not server-side
tool registration. Renamed method, test classes, and test files.
…hen-commit
- Add RemoveKnownTools(IEnumerable<string>) to remove specific known tools
- Add ClearKnownTools() to remove all known tools
- Change AddKnownTools to validate all tools first, then commit all-or-nothing
- Throw ArgumentException on invalid x-mcp-header annotations instead of
silently skipping (ToolRejected still fires for logging parity)
- Remove trailing blank line in McpClient.cs
- Add 7 unit tests for Remove/Clear scenarios
- Add HTTP integration test for RemoveKnownTools header verification
Validate all tool names for null before removing any, matching the
all-or-nothing pattern used in AddKnownTools.
Add 'Pre-loading tool definitions on the client' section to
docs/concepts/tools/tools.md covering usage, cache behavior,
removal APIs, and validation semantics.
Prevents subclasses that don't override from silently swallowing
calls. All three methods (AddKnownTools, RemoveKnownTools,
ClearKnownTools) now throw with a descriptive message.
Clarify that known status is sticky for the McpClient lifetime and
point to RemoveKnownTools/ClearKnownTools for explicit removal.
- Staleness: register → server returns [] → headers still sent
- Partial-failure atomicity: [valid, invalid, valid] → nothing cached
- Null element atomicity: null at index 1 → nothing cached
- Last-write-wins: re-register with schema B → headers reflect B
When SendRequestAsync handles a tools/call request and the tool is not
found in the cache, log a Debug message suggesting AddKnownTools or
ListToolsAsync to populate the cache. This makes missing Mcp-Param-*
headers diagnosable without breaking existing behavior.
@tarekgh

Copy link
Copy Markdown
ContributorAuthor

@halter73 thanks for your feedback. I believe I addressed all your feedback. The remaining open discussions looks can be addressed outside this PR. Let me know if you have any more feedback.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs Outdated
Log a Warning (instead of Debug) when a tools/call request has no
cached tool definition, but only for StreamableHttpClientSessionTransport
where Mcp-Param-* headers are relevant. Pipe/stdio transports do not
emit this warning since headers do not apply.
Added tests:
- Pipe transport: cache miss does NOT log a warning
- HTTP transport: cache miss DOES log a warning
@tarekgh
tarekgh merged commit 157f855 into modelcontextprotocol:mainMay 26, 2026
17 of 18 checks passed
@jeffhandleyjeffhandley mentioned this pull request Jul 28, 2026
Sign up for freeto 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

3 participants

@tarekgh@halter73@jeffhandley
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Add RegisterTools API to McpClient for pre-populating tool cache by tarekgh · Pull Request #1590 · modelcontextprotocol/csharp-sdk · GitHub
Skip to content

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

Merged
tarekgh merged 13 commits into
modelcontextprotocol:mainfrom
tarekgh:feature/register-tools-api
May 26, 2026
Merged

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

Conversation

@tarekgh

@tarekghtarekgh commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds AddKnownTools, RemoveKnownTools, and ClearKnownTools APIs to McpClient that allow 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 Additions on McpClient

  • AddKnownTools(IEnumerable<Tool>) — registers tool definitions in the client's tool cache with all-or-nothing validation
  • RemoveKnownTools(IEnumerable<string>) — removes specific known tools by name
  • ClearKnownTools() — removes all known tools

Base virtual methods throw NotSupportedException (not abstract, to preserve API compatibility with PackageValidationBaselineVersion=1.0.0).

Implementation Details

  • Known tools are added to the same _toolCache used by ListToolsAsync
  • A thread-safe ConcurrentDictionary<string, byte> tracks known tool names (used as a concurrent set)
  • ToolCacheClearing (invoked by ListToolsAsync) only removes server-discovered tools; known tools survive cache clears
  • Fast path: when no known tools exist, _toolCache.Clear() is called directly
  • All-or-nothing validation: tools are validated for x-mcp-header correctness before any are added; ArgumentException thrown on invalid schemas
  • 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 known tool, the server's definition overwrites in the cache, but the tool retains its known/pinned status
  • LogDebug on cache miss during tools/call to help diagnose missing Mcp-Param-* headers

Cache Interaction Behavior

  • AddKnownTools -> ListToolsAsync: known tools survive the clear, server tools added alongside
  • ListToolsAsync -> AddKnownTools: both coexist in cache
  • AddKnownTools -> ListToolsAsync -> AddKnownTools: all known tools survive, server tools refreshed
  • RemoveKnownTools/ClearKnownTools: removes from both _registeredToolNames and _toolCache

Documentation

  • Conceptual docs section "Pre-loading tool definitions on the client" in docs/concepts/tools/tools.md
  • XML docs on all three methods with cache interaction details

Tests

Unit Tests (20 tests in McpClientAddKnownToolsTests.cs)

  • 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 (all-or-nothing)
  • Duplicate registration (last write wins)
  • Null argument validation (AddKnownTools, RemoveKnownTools)
  • No-header-annotation tools still accepted
  • Register then CallToolAsync without ListToolsAsync (cache lookup works)
  • RemoveKnownTools — removed tool no longer survives ListToolsAsync
  • RemoveKnownTools — non-existent name is no-op
  • RemoveKnownTools — partial remove, other tools survive
  • ClearKnownTools — removes all, server tools unaffected
  • ClearKnownTools — empty is no-op
  • ClearKnownTools then AddKnownTools — works correctly
  • Partial-failure atomicity — [valid, invalid, valid] throws, nothing cached
  • Null element atomicity — null at index 1 throws, element 0 not cached

HTTP Integration Tests (6 tests in AddKnownToolsHeaderTests.cs)

  • Core scenario: AddKnownTools -> CallToolAsync (NO ListToolsAsync) -> verify Mcp-Param-Region and Mcp-Param-Priority headers received by server
  • No headers sent without register or list
  • Known tool headers survive ListToolsAsync cache clear
  • RemoveKnownTools -> no headers sent after removal
  • Staleness: register -> server returns [] -> ListToolsAsync -> call -> headers still sent
  • Last-write-wins: register schema A -> register schema B -> call -> headers reflect schema B only

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
@tarekghtarekgh self-assigned this May 20, 2026
tarekghand 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
ContributorAuthor

#1553

@halter73halter73 left a comment

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.

Thanks for tackling this! CI checked, and we're ahead of the other tier 1 SDKs here. TypeScript (modelcontextprotocol/typescript-sdk#2069) only landed Mcp-Method/Mcp-Name, Python has no SEP-2243 implementation at all, and Go shipped x-mcp-header (modelcontextprotocol/go-sdk#915) with the same silent-omission gap that #1577 calls out. So this PR is paving the path for everyone.

One thing I'd like to see addressed before merge but couldn't comment on inline because it's out of the diff range: the silent-miss path in McpClientImpl.SendRequestAsync. If the caller forgets to call either RegisterTools or ListToolsAsync, the cache lookup fails, no Tool is attached, and StreamableHttpClientSessionTransport ships the tools/call with no Mcp-Param-* headers — no warning, no log, no error. The server then sees a tools/call with no infra headers and may misroute (e.g. a proxy keyed on Mcp-Param-Region). Worth a LogDebug (or LogInformation, gated on the negotiated protocol version supporting SEP-2243) on the cache miss so it's at least diagnosable. Other SDKs have the same gap, but logging is cheap and a real footgun-mitigator.

Follow-up, not for this PR: I'd also like us to eventually offer a fully stateless way to flow parameter values into Mcp-Param-* headers — e.g. a Tool (or just a param → header map) passed directly on CallToolAsync / CallToolRequestOptions, with no client-wide cache mutation. The cache-priming approach in this PR is the right answer for the "I already know the schema from a previous session" scenario, but for callers who want per-call control (multi-tenant proxies, request-scoped routing, callers who don't want any client-global mutable state) the stateless variant is a cleaner fit and dovetails with what the Python folks are circling in modelcontextprotocol/python-sdk#1509. I'm happy to file a separate issue once this lands.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs
Comment threadtests/ModelContextProtocol.Tests/Client/McpClientRegisterToolsTests.cs Outdated
Comment threadsrc/ModelContextProtocol.Core/Client/McpClient.cs Outdated
Tarek Mahmoud Sayed added 8 commits May 21, 2026 10:51
Clarifies that this is client-side cache priming, not server-side
tool registration. Renamed method, test classes, and test files.
…hen-commit
- Add RemoveKnownTools(IEnumerable<string>) to remove specific known tools
- Add ClearKnownTools() to remove all known tools
- Change AddKnownTools to validate all tools first, then commit all-or-nothing
- Throw ArgumentException on invalid x-mcp-header annotations instead of
silently skipping (ToolRejected still fires for logging parity)
- Remove trailing blank line in McpClient.cs
- Add 7 unit tests for Remove/Clear scenarios
- Add HTTP integration test for RemoveKnownTools header verification
Validate all tool names for null before removing any, matching the
all-or-nothing pattern used in AddKnownTools.
Add 'Pre-loading tool definitions on the client' section to
docs/concepts/tools/tools.md covering usage, cache behavior,
removal APIs, and validation semantics.
Prevents subclasses that don't override from silently swallowing
calls. All three methods (AddKnownTools, RemoveKnownTools,
ClearKnownTools) now throw with a descriptive message.
Clarify that known status is sticky for the McpClient lifetime and
point to RemoveKnownTools/ClearKnownTools for explicit removal.
- Staleness: register → server returns [] → headers still sent
- Partial-failure atomicity: [valid, invalid, valid] → nothing cached
- Null element atomicity: null at index 1 → nothing cached
- Last-write-wins: re-register with schema B → headers reflect B
When SendRequestAsync handles a tools/call request and the tool is not
found in the cache, log a Debug message suggesting AddKnownTools or
ListToolsAsync to populate the cache. This makes missing Mcp-Param-*
headers diagnosable without breaking existing behavior.
@tarekgh

Copy link
Copy Markdown
ContributorAuthor

@halter73 thanks for your feedback. I believe I addressed all your feedback. The remaining open discussions looks can be addressed outside this PR. Let me know if you have any more feedback.

Comment threadsrc/ModelContextProtocol.Core/Client/McpClientImpl.cs Outdated
Log a Warning (instead of Debug) when a tools/call request has no
cached tool definition, but only for StreamableHttpClientSessionTransport
where Mcp-Param-* headers are relevant. Pipe/stdio transports do not
emit this warning since headers do not apply.
Added tests:
- Pipe transport: cache miss does NOT log a warning
- HTTP transport: cache miss DOES log a warning
@tarekgh
tarekgh merged commit 157f855 into modelcontextprotocol:mainMay 26, 2026
17 of 18 checks passed
@jeffhandleyjeffhandley mentioned this pull request Jul 28, 2026
Sign up for freeto 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

3 participants

@tarekgh@halter73@jeffhandley