File size: 25,974 Bytes
e750673 daa3e26 e750673 daa3e26 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 |
# MCP Progressive Disclosure: Implementation Guide
**Extension Name:** Model Context Protocol (MCP) - Progressive Disclosure for Tool Descriptions
**Companion Specification:** `spec_mcp_progressive_disclosure_v2_0.md`
**Version:** 2.1
**Last Updated:** 2025-11-30
---
## Overview
This guide provides practical advice for implementing the **MCP Progressive Disclosure extension** for token-efficient tool description delivery. It covers common pitfalls, proven strategies, and real-world learnings from production deployments.
**If you're new here:**
1. Read the [MCP Progressive Disclosure specification](spec_mcp_progressive_disclosure_v2_0.md) first for protocol requirements
2. Come back here for implementation details and troubleshooting
3. Use the code examples as starting points
**What is MCP Progressive Disclosure?**
An extension to the Model Context Protocol (MCP) that enables servers to expose minimal tool descriptions initially, then provide full documentation on-demand through a standardized resource pattern.
---
## Quick Start
### Server-Side (5 Steps)
1. **Create tool descriptions directory**
```bash
mkdir tool_descriptions/
```
2. **Extract tool descriptions to JSON files**
```json
// tool_descriptions/my_tool.json
{
"name": "my_tool",
"description": "Full detailed description...",
"inputSchema": { /* complete schema */ },
"examples": [ /* usage examples */ ]
}
```
3. **Implement resource listing**
- Expose `tool_descriptions` resource via `resources/list`
- Include clear workflow guidance in description
4. **Implement resource reading**
- Parse `?tools=` query parameter
- Load requested tool descriptions
- Authorize tools for session
- Return JSON with descriptions
5. **Enforce authorization**
- Check authorization before tool execution
- Return clear error if not authorized
### Agent-Side (2 Steps)
1. **Enhance system prompt**
- Detect `tool_descriptions` resource
- Explain two-stage workflow
- Provide example URI syntax
2. **Test the workflow**
- Verify LLM picks tools from `tools/list`
- Verify LLM fetches specific tools
- Verify LLM calls tools successfully
---
## The Core Challenge: Tool Selection vs Tool Usage
### The Problem
The most common implementation issue is LLMs misunderstanding **when** to fetch tool descriptions. They consistently try one of two wrong approaches:
**Anti-Pattern 1: Fetch Everything First**
```
User: "Get some data"
β LLM: read_resource(resource_uri="resource:///tool_descriptions")
Error: Must specify tools parameter
β
LLM: read_resource(resource_uri="resource:///tool_descriptions?tools=get_data")
Success, then calls tool
```
**Anti-Pattern 2: Skip Fetching Entirely**
```
User: "Get some data"
β LLM: get_data(query="data")
Error: Tool description required
β
LLM: read_resource(resource_uri="resource:///tool_descriptions?tools=get_data")
β
LLM: get_data(query="data")
```
### Why This Happens
LLMs interpret "fetch descriptions before calling tools" as "fetch descriptions to help me **decide** which tool to use" rather than "fetch descriptions to learn **how to use** the tool I've already chosen."
**The Cognitive Model:**
- **Stage 1 (tools/list)**: WHAT does this tool do? β **Decision Point**
- **Stage 2 (tool_descriptions)**: HOW do I use this tool? β **Implementation Details**
LLMs need explicit guidance that Stage 1 descriptions are **sufficient for selection**.
---
## Server Implementation
### 1. Storage Structure
**Recommended:**
```
project/
βββ tool_descriptions/
β βββ tool_one.json
β βββ tool_two.json
β βββ tool_three.json
βββ tool_description_loader.py
βββ session_auth.py
βββ server.py
```
**tool_descriptions/tool_one.json:**
```json
{
"name": "tool_one",
"description": "Complete description with all context needed for reliable use",
"inputSchema": {
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "First parameter"
},
"param2": {
"type": "integer",
"description": "Second parameter",
"default": 10
}
},
"required": ["param1"]
},
"examples": [
{
"description": "Basic usage",
"input": {"param1": "value"},
"explanation": "Simplest form with just required parameter"
},
{
"description": "With optional parameter",
"input": {"param1": "value", "param2": 20},
"explanation": "Override default for param2"
}
],
"usage_guidance": {
"common_patterns": [
"For X scenario, use param1='special_value'",
"When Y, set param2 higher than default"
],
"important_notes": [
"Parameter validation happens server-side",
"Results are paginated by default"
]
},
"error_guidance": {
"common_errors": [
{
"error": "INVALID_PARAM1",
"cause": "param1 must match pattern X",
"solution": "Ensure param1 follows format Y"
}
]
}
}
```
### 2. Tool Description Loader
**tool_description_loader.py:**
```python
from pathlib import Path
import json
from typing import Dict, Optional, List
class ToolDescriptionLoader:
"""Loads and caches tool descriptions from JSON files"""
def __init__(self, descriptions_dir: Path):
self.descriptions_dir = descriptions_dir
self._cache: Dict[str, dict] = {}
def load(self, tool_name: str) -> Optional[dict]:
"""Load a single tool description"""
if tool_name in self._cache:
return self._cache[tool_name]
desc_file = self.descriptions_dir / f"{tool_name}.json"
if not desc_file.exists():
return None
with open(desc_file, 'r', encoding='utf-8') as f:
description = json.load(f)
self._cache[tool_name] = description
return description
def load_multiple(self, tool_names: List[str]) -> Dict[str, dict]:
"""Load multiple tool descriptions"""
descriptions = {}
for tool_name in tool_names:
desc = self.load(tool_name)
if desc:
descriptions[tool_name] = desc
else:
descriptions[tool_name] = {
"error": f"Tool '{tool_name}' not found",
"available_tools": self.list_available()
}
return descriptions
def list_available(self) -> List[str]:
"""Get list of available tool descriptions"""
if not self.descriptions_dir.exists():
return []
return [f.stem for f in self.descriptions_dir.glob("*.json")]
```
### 3. Session Authorization
**session_auth.py:**
```python
from typing import Dict, Set
import time
import logging
logger = logging.getLogger(__name__)
class SessionAuthorization:
"""Manages per-session tool authorization state"""
def __init__(self):
self._sessions: Dict[int, Dict] = {}
# session_id -> {
# 'authorized_tools': Set[str],
# 'created_at': float,
# 'last_activity': float
# }
def get_session_id(self, session) -> int:
"""Get unique session identifier from MCP session object"""
return id(session)
def authorize_tool(self, session, tool_name: str):
"""Mark a tool as authorized for this session"""
session_id = self.get_session_id(session)
if session_id not in self._sessions:
self._sessions[session_id] = {
'authorized_tools': set(),
'created_at': time.time(),
'last_activity': time.time()
}
self._sessions[session_id]['authorized_tools'].add(tool_name)
self._sessions[session_id]['last_activity'] = time.time()
logger.info(f"Session {session_id}: Authorized tool '{tool_name}'")
def is_authorized(self, session, tool_name: str) -> bool:
"""Check if tool has been authorized in this session"""
session_id = self.get_session_id(session)
if session_id not in self._sessions:
return False
self._sessions[session_id]['last_activity'] = time.time()
return tool_name in self._sessions[session_id]['authorized_tools']
def cleanup_stale_sessions(self, max_age_seconds: int = 3600):
"""Remove inactive sessions"""
now = time.time()
stale = [
sid for sid, data in self._sessions.items()
if now - data['last_activity'] > max_age_seconds
]
for session_id in stale:
del self._sessions[session_id]
if stale:
logger.info(f"Cleaned up {len(stale)} stale session(s)")
```
### 4. Server Resource Handlers
**server.py:**
```python
from mcp.server import Server
from mcp.types import Resource, Tool
from urllib.parse import urlparse, parse_qs
from pathlib import Path
import json
app = Server("my-server")
# Initialize modules
session_auth = SessionAuthorization()
tool_loader = ToolDescriptionLoader(Path(__file__).parent / "tool_descriptions")
@app.list_resources()
async def list_resources() -> list[Resource]:
"""List available resources including tool_descriptions"""
return [
Resource(
uri="resource:///tool_descriptions",
name="Tool Descriptions - Required for tool use",
description=(
"WORKFLOW:\n"
"\n"
"Step 1: PICK which tool you need from tools/list (descriptions show WHAT each tool does)\n"
"Step 2: FETCH that tool's full description from this resource (learn HOW to use it)\n"
" Example: resource:///tool_descriptions?tools=TOOL_NAME\n"
"Step 3: CALL the tool with parameters you learned\n"
"\n"
"IMPORTANT: You CANNOT call a tool until you fetch its description.\n"
"\n"
"The short descriptions in tools/list are SUFFICIENT for choosing the right tool.\n"
"This resource provides parameters, examples, and authorizes the tool for use.\n"
"\n"
"MUST include ?tools=TOOL_NAME (base URI without parameter will error)."
),
mimeType="application/json"
)
]
@app.read_resource()
async def read_resource(uri: str) -> str:
"""Read tool descriptions resource"""
uri = str(uri)
parsed = urlparse(uri)
# Parse query parameters
query_params = parse_qs(parsed.query)
tools_param = query_params.get('tools', [])
# Require tools parameter
if not tools_param:
error = {
"error": {
"code": "MISSING_TOOL_SELECTION",
"message": "You must specify one or more tool names in the 'tools' parameter.",
"examples": [
"resource:///tool_descriptions?tools=tool_one",
"resource:///tool_descriptions?tools=tool_one,tool_two"
],
"available_tools": tool_loader.list_available()
}
}
return json.dumps(error, indent=2)
# Parse comma-separated tool names
requested_tools = [t.strip() for t in tools_param[0].split(',')]
# Get session for authorization
try:
session = app.request_context.session
except LookupError:
session = None
# Load descriptions
descriptions = tool_loader.load_multiple(requested_tools)
# Authorize tools for this session
if session:
for tool_name in descriptions.keys():
if "error" not in descriptions[tool_name]:
session_auth.authorize_tool(session, tool_name)
return json.dumps(descriptions, indent=2)
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List tools with minimal descriptions"""
return [
Tool(
name="tool_one",
description="Brief description of what this tool does - sufficient for selection",
inputSchema={
"type": "object",
"additionalProperties": True
}
),
Tool(
name="tool_two",
description="Brief description of what this tool does - sufficient for selection",
inputSchema={
"type": "object",
"additionalProperties": True
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
"""Handle tool calls with authorization check"""
# Get session
try:
session = app.request_context.session
except LookupError:
return error_response("Tool call outside session context")
# Check authorization
if not session_auth.is_authorized(session, name):
error = {
"error": {
"code": "TOOL_DESCRIPTION_REQUIRED",
"message": f"Tool '{name}' requires fetching its description before use.",
"instructions": [
f"1. Fetch: read_resource(resource_uri=\"resource:///tool_descriptions?tools={name}\")",
"2. Review the parameters and examples",
"3. Then call the tool"
],
"resource_uri": f"resource:///tool_descriptions?tools={name}"
}
}
return [TextContent(type="text", text=json.dumps(error, indent=2))]
# Tool is authorized - execute
if name == "tool_one":
result = handle_tool_one(arguments)
elif name == "tool_two":
result = handle_tool_two(arguments)
else:
result = {"error": f"Unknown tool: {name}"}
return [TextContent(type="text", text=json.dumps(result, indent=2))]
```
---
## Agent Implementation
### System Prompt Enhancement
**Key Strategy:** Auto-detect progressive disclosure servers and provide explicit workflow guidance.
**conversation.py or similar:**
```python
def build_system_prompt(self, tools, resources):
"""Build system prompt with progressive disclosure detection"""
# Check if any resource is tool_descriptions
has_progressive_disclosure = any(
'tool_descriptions' in r.get('uri', '')
for r in resources
)
# Build tool list with descriptions
tool_list = "\n".join([
f" - {t['name']}: {t.get('description', 'No description')}"
for t in tools
])
prompt = f"""You are a helpful assistant with access to these tools:
{tool_list}
Use tools when needed to answer questions."""
if has_progressive_disclosure:
prompt += """
IMPORTANT - Tool Usage Workflow:
This server uses progressive disclosure for tools. Follow this exact workflow:
1. PICK the right tool based on the descriptions above (they tell you WHAT each tool does)
2. FETCH the full tool description using read_resource with the specific tool name
Example: read_resource(resource_uri="resource:///tool_descriptions?tools=TOOL_NAME")
3. CALL the tool using the parameters you just learned
DO NOT try to fetch tool_descriptions without specifying which tool you want (?tools=TOOL_NAME).
The tool descriptions above are sufficient for choosing which tool you need.
You fetch the full description to learn the parameters and authorize the tool."""
return prompt
```
---
## Resource Description Wording
### Proven Effective Pattern
Based on production testing, this structure achieves highest LLM compliance:
```
WORKFLOW:
Step 1: PICK which tool you need from tools/list based on SHORT descriptions
Step 2: FETCH full description: resource:///tool_descriptions?tools=TOOL_NAME
Step 3: CALL the tool with parameters you learned
IMPORTANT: You CANNOT call a tool until you fetch its description.
The SHORT descriptions tell you WHICH tool to use (sufficient for selection).
This resource tells you HOW to use it (parameters, examples) and authorizes it.
MUST include ?tools=TOOL_NAME (base URI without tools will error).
```
### What Makes This Work
1. **Sequential Steps**: Clear 1-2-3 progression
2. **Separation of Concerns**: WHICH vs HOW distinction
3. **Mandatory Language**: "CANNOT" not "should not"
4. **Concrete Example**: Shows exact URI format
5. **Prohibition**: States what will fail
6. **Rationale**: Explains why pattern exists (selection vs parameters)
### What DOESN'T Work
β **Too brief**: "Fetch descriptions before calling tools"
- Problem: Ambiguous when to fetch
β **Too verbose**: Multiple paragraphs of explanation
- Problem: LLMs skip/skim long descriptions
β **No examples**: Abstract description only
- Problem: LLMs don't know exact syntax
β **Missing prohibition**: Doesn't say what fails
- Problem: LLMs try base URI without tools parameter
---
## Testing Strategy
### 1. Unit Tests
Test each component independently:
```python
def test_tool_loader():
loader = ToolDescriptionLoader(Path("tool_descriptions"))
# Test single load
desc = loader.load("tool_one")
assert desc['name'] == "tool_one"
assert 'inputSchema' in desc
# Test multiple load
descs = loader.load_multiple(["tool_one", "tool_two"])
assert len(descs) == 2
# Test missing tool
descs = loader.load_multiple(["nonexistent"])
assert "error" in descs["nonexistent"]
def test_session_auth():
auth = SessionAuthorization()
# Mock session
class MockSession:
pass
session = MockSession()
# Test authorization flow
assert not auth.is_authorized(session, "tool_one")
auth.authorize_tool(session, "tool_one")
assert auth.is_authorized(session, "tool_one")
# Test session isolation
session2 = MockSession()
assert not auth.is_authorized(session2, "tool_one")
```
### 2. Integration Tests
Test the full workflow:
```python
async def test_progressive_disclosure_workflow():
# Connect to server
server = await connect_mcp_server()
# 1. List resources - should see tool_descriptions
resources = await server.list_resources()
assert any('tool_descriptions' in r['uri'] for r in resources)
# 2. List tools - should see minimal descriptions
tools = await server.list_tools()
assert len(tools) > 0
assert all('description' in t for t in tools)
# 3. Try calling without fetching - should fail
with pytest.raises(Exception) as exc:
await server.call_tool("tool_one", {})
assert "TOOL_DESCRIPTION_REQUIRED" in str(exc)
# 4. Fetch description
desc = await server.read_resource("resource:///tool_descriptions?tools=tool_one")
assert 'tool_one' in desc
assert 'inputSchema' in desc['tool_one']
# 5. Call tool - should succeed
result = await server.call_tool("tool_one", {"param1": "value"})
assert result['success'] == True
```
### 3. LLM Behavior Tests
Test actual LLM compliance:
```python
async def test_llm_workflow():
"""Test that LLM follows correct workflow"""
agent = TestAgent(server="my-server")
# Give task that requires tool use
response = await agent.query("Get some data")
# Verify LLM workflow
assert agent.trace.contains_call("read_resource")
assert "?tools=" in agent.trace.last_resource_uri
assert agent.trace.contains_call("tool_one")
# Verify no errors
assert not agent.trace.contains_error("MISSING_TOOL_SELECTION")
assert not agent.trace.contains_error("TOOL_DESCRIPTION_REQUIRED")
```
---
## Common Pitfalls
### 1. Base URI Without Tools Parameter
**Problem:** LLM calls `resource:///tool_descriptions` without `?tools=`
**Cause:** Resource description not clear about WHAT vs HOW distinction
**Solution:**
- Emphasize that tools/list is sufficient for selection
- Show incorrect example explicitly
- Use system prompt reinforcement
### 2. Calling Tool Before Fetching
**Problem:** LLM tries to call tool directly
**Cause:** Over-emphasizing "descriptions sufficient for selection"
**Solution:**
- Balance messaging: sufficient for **choosing**, not for **using**
- State clearly: "CANNOT call until fetched"
- Include authorization rationale
### 3. Session ID Issues
**Problem:** Authorization not persisting or crossing sessions
**Cause:** Using unstable session identifier
**Solution:**
- Use `id(request_context.session)` as session ID
- This is stable for the connection lifetime
- No external dependencies required
### 4. Tool Names Don't Match
**Problem:** Fetched tool name doesn't match tools/list name
**Cause:** Typo or case mismatch
**Solution:**
- Use exact same names in JSON files as in tools/list
- Include "available_tools" in error responses
- Log mismatches for debugging
### 5. Stale Sessions Accumulate
**Problem:** Memory grows over time
**Cause:** No session cleanup
**Solution:**
- Implement periodic cleanup (every 10 minutes)
- Remove sessions with no activity for 1 hour
- Run as background task
---
## Token Efficiency Analysis
### Baseline (Full Descriptions)
**Per-tool cost:** 3000-5000 tokens
**10 tools:** 30,000-50,000 tokens at startup
**Problem:** Consumes significant context before any actual work
### With Progressive Disclosure
**Minimal descriptions (all tools):** 500-1000 tokens
**Full description (when fetched):** 3000-5000 tokens per tool
**Typical usage (2 tools):** 500 + (2 Γ 4000) = 8,500 tokens
**Savings:** 75-80% token reduction for typical workflows
### Break-Even Analysis
Progressive disclosure saves tokens when:
```
Number of tools Γ (full description size - minimal size) > fetch overhead
```
For 5+ tools, progressive disclosure almost always wins.
---
## Migration Guide
### From Traditional to Progressive Disclosure
**Step 1:** Extract existing tool descriptions
```python
# Before: Full description in tools/list
Tool(
name="my_tool",
description="Long description...",
inputSchema={/* full schema */}
)
# After: Minimal in tools/list
Tool(
name="my_tool",
description="Brief description of purpose",
inputSchema={"type": "object", "additionalProperties": True}
)
# Full description moved to tool_descriptions/my_tool.json
```
**Step 2:** Implement resource handlers (see Server Implementation section)
**Step 3:** Add authorization enforcement
**Step 4:** Update agent system prompt (if you control the agent)
**Step 5:** Test with real queries
### Backwards Compatibility
To support both patterns during migration:
```python
@app.list_tools()
async def list_tools() -> list[Tool]:
# Check if client supports progressive disclosure
# (presence of read_resource capability or similar)
if supports_progressive_disclosure:
return minimal_tools()
else:
return full_tools()
```
---
## Best Practices
### β
DO
- Store tool descriptions in separate JSON files
- Use clear sequential steps in resource description
- Distinguish WHAT (selection) from HOW (parameters)
- Provide system prompt guidance for agents
- Log authorization events for debugging
- Cache parsed descriptions in memory
- Clean up stale sessions periodically
- Include concrete examples in resource description
- Test with real LLMs, not just unit tests
### β DON'T
- Don't make tool descriptions too minimal (must be sufficient for selection)
- Don't omit the ?tools= requirement from resource description
- Don't use unstable session identifiers
- Don't skip authorization checks
- Don't expose internal paths in descriptions
- Don't rely solely on resource description (use system prompt too)
- Don't optimize prematurely (measure token savings first)
---
## Troubleshooting
### LLM keeps trying base URI without tools parameter
**Diagnosis:** Resource description or system prompt not clear enough
**Fix:**
1. Add explicit prohibition in resource description
2. Show incorrect example with β
3. Enhance system prompt with workflow
4. Test wording iteratively
### Authorization failures despite fetching
**Diagnosis:** Session ID mismatch or session cleanup too aggressive
**Fix:**
1. Verify `id(session)` is stable
2. Log session IDs during fetch and call
3. Increase cleanup timeout
4. Check for session resets
### Tool descriptions not loading
**Diagnosis:** File path or JSON format issues
**Fix:**
1. Verify tool_descriptions directory exists
2. Check JSON syntax with `json.loads()`
3. Ensure file names match exactly (case-sensitive)
4. Log file paths being accessed
### Memory growth over time
**Diagnosis:** Sessions not being cleaned up
**Fix:**
1. Implement background cleanup task
2. Lower max_age_seconds threshold
3. Monitor session count in production
4. Consider LRU cache with size limit
---
## Production Checklist
Before deploying progressive disclosure:
- [ ] Tool descriptions extracted to JSON files
- [ ] Minimal descriptions sufficient for tool selection
- [ ] Resource description includes clear workflow
- [ ] System prompt enhanced (if controlling agent)
- [ ] Authorization enforcement implemented
- [ ] Session cleanup running
- [ ] Error messages include recovery URIs
- [ ] Logging enabled for debugging
- [ ] Integration tests passing
- [ ] LLM behavior tested with real queries
- [ ] Token savings measured and validated
- [ ] Documentation updated for users
---
## Further Reading
- [MCP Progressive Disclosure Specification v2.0](spec_mcp_progressive_disclosure_v2_0.md)
- [MCP Specification](https://spec.modelcontextprotocol.io/)
- [RFC 2119: Key words for RFCs](https://www.rfc-editor.org/rfc/rfc2119)
---
**Questions or Issues?**
If you encounter problems not covered in this guide, please:
1. Check the specification for normative requirements
2. Review the troubleshooting section
3. Test with minimal examples
4. Share findings with the community
**Last Updated:** 2025-11-30
**Companion Specification:** v2.1
|