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