Repository navigation
Python: sort MCP inputSchema properties deterministically for prompt caching - #8960
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Nested schema properties remain unsorted, so prompt representations can still vary.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Adds deterministic MCP tool schema ordering to improve LLM prompt-cache stability.
Changes:
- Sorts top-level
inputSchema.propertieskeys. - Adds a regression test for top-level ordering.
| File | Description |
|---|---|
python/packages/core/agent_framework/_mcp.py |
Normalizes MCP schema property order. |
python/packages/core/tests/core/test_mcp.py |
Tests deterministic top-level sorting. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
|
@microsoft-github-policy-service agree |
|
Prognosticate X (@Prognosticate-X) The current normalization sorts only the root |
|
I agree with Eduard that sorting only the root MCP 2026-07-28 recommends deterministic ordering of the tools returned by If property ordering is affecting prompt caching, I think we should first demonstrate that at the provider request/cache level. If canonicalization is needed, the provider serialization boundary seems like a better place to apply it consistently across all tools, without mutating the core schema. I’d suggest separating this from the MCP v2 work in #8245 and revisiting the design before merging. |
|
Thanks Jose Alvarez (@jpalvarezl) and Eduard van Valkenburg (@eavanvalkenburg) for the thoughtful feedback! 1. Demonstration at the Provider Request / Wire LevelTo illustrate why key ordering matters for prompt caching in Python, we tested downstream serialization using import hashlib, json
from agent_framework._tools import FunctionTool
# When an MCP server sends dictionary keys in different insertion orders across restarts/reloads:
schema_session_1 = {
"type": "object",
"properties": {
"query": {"type": "string"},
"author": {"type": "string"},
"limit": {"type": "integer"},
},
"required": ["query"],
}
schema_session_2 = {
"type": "object",
"properties": {
"limit": {"type": "integer"},
"query": {"type": "string"},
"author": {"type": "string"},
},
"required": ["query"],
}
tool_1 = FunctionTool(name="search_db", description="search", parameters=schema_session_1, func=lambda: None)
tool_2 = FunctionTool(name="search_db", description="search", parameters=schema_session_2, func=lambda: None)
# Downstream provider serialization (Python dict preserves insertion order):
wire_1 = json.dumps(tool_1.to_json_schema_spec(), sort_keys=False)
wire_2 = json.dumps(tool_2.to_json_schema_spec(), sort_keys=False)
print(hashlib.sha256(wire_1.encode()).hexdigest())
# -> 350e265a70214b94b15aa1d1b46b21ba11a1b3491b18f7d60a1ab73739615ceb
print(hashlib.sha256(wire_2.encode()).hexdigest())
# -> 713fef60d8ccc7afa1b735f42e784d154908283f0a216b53323df684877ec6e6
# Result: Byte mismatch -> 100% Prompt Cache Miss on Claude/OpenAI prefix cachingBecause LLM prompt prefix caches (Anthropic Prompt Caching and OpenAI Prefix Caching) operate on exact byte-for-byte token stream prefixes, differing JSON field order breaks the cache boundary and leads to repeated cache misses on server reconnects. 2. Recursive Sorting (Addressing Eduard van Valkenburg (@eavanvalkenburg))I have updated the PR with a recursive
A corresponding unit test 3. Provider Serialization Boundary (Addressing Jose Alvarez (@jpalvarezl))I completely agree with Jose Alvarez (@jpalvarezl)'s architectural insight — canonicalizing at the provider serialization boundary (e.g. inside In the meantime, canonicalizing upon MCP loading guarantees that dynamically discovered external tools never invalidate downstream prompt caches. If the team prefers centralizing this at the provider / |
Eduard van Valkenburg (eavanvalkenburg)
left a comment
There was a problem hiding this comment.
Prognosticate X (@Prognosticate-X) The current head canonicalizes schemas only inside FunctionTool.to_json_schema_spec(), but FunctionTool.parameters() still returns the original insertion order and several providers serialize tool.parameters() directly (including Anthropic, Bedrock, Gemini, and Mistral). Those wire schemas therefore remain order-dependent, so the prompt-cache issue is not fixed consistently at the provider boundary.
Please put canonicalization behind a shared serialization helper used by every provider emitter, or canonicalize immediately before each provider builds its request. Keep parameters() raw only if that public behavior must remain unchanged, and add coverage for at least one direct-parameters() provider path.
|
Thanks Eduard van Valkenburg (@eavanvalkenburg) for catching that! You are completely right — providers such as Anthropic, Bedrock, Gemini, and Mistral serialize I have updated the implementation to canonicalize directly within
|
…r prompt caching - Canonicalize schema properties recursively in FunctionTool.parameters() - Ensure deterministic schema serialization across all providers for prompt cache stability - Add regression tests for root without properties, nested defs, and wire-level serialization - Update typesafe chat client test mock expectations to match deterministic argument ordering
e132c1d to
944c1ac
Compare

Motivation & Context
In MCP servers across various runtimes (Python, Rust, etc.), the dictionary iteration order of tool
inputSchemaproperties is not guaranteed to be stable across server restarts or dynamic reload notifications (notifications/tools/list_changed).When downstream LLM clients construct function-calling tools from these schemas, any variation in property key ordering alters the byte-level representation of tool definitions injected into model prompts. This causes LLM prompt caches (such as Anthropic and OpenAI prompt caching) to invalidate on restart, leading to unnecessary token overhead.
This PR addresses this problem by sorting
propertieskeys deterministically during MCP tool ingestion inMCPTool.load_tools(). This contributes to the broader MCP 2026-07-28 statelessness & caching initiative tracked in #8245.Description & Review Guide
python/packages/core/agent_framework/_mcp.py: when normalizinginput_schemaduringload_tools(), sortinput_schema["properties"]by key alphabetically if it is a dictionary.python/packages/core/tests/core/test_mcp.py: addedtest_load_tools_sorts_input_schema_properties_deterministically()to verify that property keys are alphabetically ordered in the resultingFunctionTool.parameters().input_schema["properties"]is purely additive and preserves full schema validity.Related Issue
Relates to #8245
Contribution Checklist