Repository navigation
Conversation
McpServerPrimitiveCollection<T> enumerated its ConcurrentDictionary directly, so tools/list, prompts/list, resources/list and resources/templates/list returned primitives in hash order. For resources that order changed on every process start, because UriTemplateComparer uses randomized string hashing. Each entry now records an insertion sequence number, and enumeration (GetEnumerator, ToArray, CopyTo, PrimitiveNames) sorts a snapshot by it. Primitives registered with WithTools<T>(), WithPrompts<T>() and WithResources<T>() are now listed in declaration order, matching the Python and TypeScript SDKs. Fixes modelcontextprotocol#1923 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QNCtb94vqDmohN9UWoGV3U
There was a problem hiding this comment.
🟢 Approval recommended
The implementation consistently preserves insertion order while retaining thread-safe lookup and removal semantics.
0 open findings
What changed in this PR
Preserves insertion order when enumerating registered MCP server primitives.
Changes:
- Associates primitives with monotonic insertion sequence numbers.
- Returns ordered snapshots from enumeration APIs.
- Adds unit and end-to-end ordering tests.
| File | Description |
|---|---|
src/ModelContextProtocol.Core/Server/McpServerPrimitiveCollection.cs |
Implements ordered snapshots and safe removal. |
tests/ModelContextProtocol.Tests/Server/McpServerPrimitiveCollectionTests.cs |
Tests collection ordering and re-add behavior. |
tests/ModelContextProtocol.Tests/Configuration/McpServerPrimitiveListOrderTests.cs |
Tests ordering through MCP list endpoints. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
I am wondering, instead of paying the extra cost all the time, why don't expose something like @halter73 what do you think? |
Well the problem is that the caller is an MCP client which expects a stable ordering (and could use any SDK), so the server has to make a choice whether to have that or not. So exposing this at the code level doesn't solve the issue for a protocol-level client, it just allows server authors to write a filter and sort the entries, which is extra busy work and should really be the default, imo. I think it would be good to align with the Python and TypeScript SDKs, especially now that caching has become more important than it used to be. |
Make sense to me. Thanks! |
|
The current implementation has concurrency and performance issues that work against the stability this PR is trying to provide.
Finally, Could we instead use:
Serializing mutations gives each successful add or remove a clear ordering point and removes the need for sequence numbers. Add remains amortized O(1). Removal from the ordered list is O(n), but primitive collections are normally small and mutations are uncommon. The first enumeration after a mutation would rebuild the snapshot in O(n). Subsequent enumerations would traverse the cached snapshot without locking, sorting, or additional allocation. The snapshot array must remain private:
This design would provide reliable behavior under concurrent mutations while retaining lock-free lookup and efficient repeated listing. |
Good points. It was the part of the PR I was the most uncomfortable and unsure about. I'll do an update to address this. Should I fit in the sorting by |
|
I’d leave the Within a batch, the existing discovery order seems sufficient. |
Fixes #1923
Problem
McpServerPrimitiveCollection<T>enumerated itsConcurrentDictionarydirectly, so the built-in list handlers returned primitives in hash order:resources/listandresources/templates/listchanged order on every process start, becauseUriTemplate.UriTemplateCompareruses randomized string hashing.tools/listandprompts/listwere stable across runs, but in an arbitrary order that could also change as entries were added or removed.Anything that need stable ordering (e.g. dynamically constructed context) to avoid cache-busting was affected. Systems that snapshot for integrity might also be affected.
Change
GetEnumerator,ToArray,CopyToandPrimitiveNamestake a snapshot and sort it by that number. Primitives are listed in the order they were added, which withWithTools<T>(),WithPrompts<T>()andWithResources<T>()is declaration order. This matches the Python and TypeScript SDKs.RemoveandContainsstill match on both name and instance.Enumeration now reads a snapshot (
ConcurrentDictionary.Values) instead of the live dictionary. Listing therefore briefly takes the dictionary's locks instead of being lock-free, which should be negligible at these sizes.Insertion order still depends on reflection returning methods in declaration order. .NET does this in practice but doesn't guarantee it. Sorting by
MetadataTokenin the builder extensions would make that deterministic and could be a follow-up.Tests
McpServerPrimitiveCollectionTestscover order for tools, prompts and resources, remove and re-add,Clear, and removing a different instance with the same name.McpServerPrimitiveListOrderTestschecks all four list methods end to end, usingWithTools<T>(),WithPrompts<T>()andWithResources<T>()with names deliberately not in alphabetical order.ModelContextProtocol.Testssuite passes on net10.0, apart fromDockerEverythingServerTests.Sampling_Sse_EverythingServer, which fails locally because the Docker "everything" server image no longer has atrigger-sampling-requesttool.🤖 Generated with Claude Code