|
| 1 | +# Example: Progressive Tool Discovery Server |
| 2 | + |
| 3 | +This is a working example of an MCP server that uses **progressive tool discovery** to organize tools into semantic groups and lazy-load them on demand. |
| 4 | + |
| 5 | +All tool groups are defined directly in Python code with **no schema.json files needed**. This is the recommended approach for building production MCP servers with progressive disclosure. |
| 6 | + |
| 7 | +## What This Demonstrates |
| 8 | + |
| 9 | +This server showcases how to: |
| 10 | + |
| 11 | +1. **Organize tools into semantic groups** - Math tools and Weather tools |
| 12 | +2. **Enable progressive disclosure** - Only gateway tools are exposed by default (~500 tokens) |
| 13 | +3. **Lazy-load tool groups** - When an LLM asks about weather, math tools stay out of context |
| 14 | +4. **Save context tokens** - ~77% reduction for servers with many tools |
| 15 | +5. **Hybrid mode** - Mix direct tools (e.g., divide) with grouped tools |
| 16 | +6. **Real API integration** - Weather tools use live Open-Meteo API and IP geolocation |
| 17 | + |
| 18 | +## Directory Structure |
| 19 | + |
| 20 | +``` |
| 21 | +discovery/ |
| 22 | +├── progressive_discovery_server.py # Main server with discovery enabled (recommended) |
| 23 | +├── ai_agent.py # Claude-powered agent demonstrating progressive discovery |
| 24 | +└── README.md # This file |
| 25 | +``` |
| 26 | + |
| 27 | +## Tool Groups |
| 28 | + |
| 29 | +### Math Tools Group |
| 30 | + |
| 31 | +Provides basic mathematical operations: |
| 32 | +- **add** - Add two numbers |
| 33 | +- **subtract** - Subtract two numbers |
| 34 | +- **multiply** - Multiply two numbers |
| 35 | + |
| 36 | +The **divide** tool is exposed as a direct tool (always visible, not in a group) to demonstrate **hybrid mode**. |
| 37 | + |
| 38 | +### Weather Tools Group |
| 39 | + |
| 40 | +Provides weather and location services using **real APIs**: |
| 41 | +- **get_user_location** - Auto-detect user's location using IP geolocation (ipapi.co) |
| 42 | +- **geocode_address** - Convert address/city names to coordinates (Open-Meteo Geocoding API) |
| 43 | +- **get_forecast** - Get real weather forecast for any coordinates (Open-Meteo Weather API) |
| 44 | + |
| 45 | +## How Progressive Tool Discovery Works |
| 46 | + |
| 47 | +### Traditional Approach (All Tools Upfront) |
| 48 | +``` |
| 49 | +Client: listTools() |
| 50 | +Server: [tool1, tool2, tool3, ..., tool100] |
| 51 | + All tool definitions in context (~4,000+ tokens) |
| 52 | +LLM: Must consider all tools for every decision |
| 53 | +Result: Context bloat, inefficient token usage |
| 54 | +``` |
| 55 | + |
| 56 | +### Progressive Discovery Approach |
| 57 | +``` |
| 58 | +Step 1: Client calls listTools() |
| 59 | +Server: [gateway_tool_1, gateway_tool_2, gateway_tool_3] |
| 60 | + Only group summaries (~300-500 tokens) |
| 61 | +
|
| 62 | +Step 2: LLM reads descriptions and decides which group to load |
| 63 | +Step 3: LLM calls gateway tool |
| 64 | +
|
| 65 | +Step 4: Server returns actual tools from that group |
| 66 | + (~200-400 tokens added, domain-specific) |
| 67 | +
|
| 68 | +Step 5: LLM uses the actual tools |
| 69 | +Other groups remain unloaded (tokens saved!) |
| 70 | +``` |
| 71 | + |
| 72 | +### Key Benefit |
| 73 | + |
| 74 | +**Only relevant tools are in context at any time.** When you ask weather questions, math tools stay hidden. This achieves ~77% token savings for large tool sets. |
| 75 | + |
| 76 | +## Running the Server |
| 77 | + |
| 78 | +### Prerequisites |
| 79 | +- Python 3.10+ |
| 80 | +- uv package manager |
| 81 | + |
| 82 | +### Start the Server |
| 83 | + |
| 84 | +```bash |
| 85 | +cd examples/discovery |
| 86 | +uv run progressive_discovery_server.py |
| 87 | +``` |
| 88 | + |
| 89 | +The server will start listening on stdio for MCP protocol messages. |
| 90 | + |
| 91 | +## Core Architecture |
| 92 | + |
| 93 | +### Three Main Components |
| 94 | + |
| 95 | +#### 1. Tool Groups |
| 96 | +Semantic collections of related tools: |
| 97 | +- Organized by function (math, weather, payments, etc.) |
| 98 | +- Defined in Python with all tools in one place |
| 99 | +- Can contain nested sub-groups |
| 100 | + |
| 101 | +#### 2. Gateway Tools |
| 102 | +Auto-generated entry points for each group: |
| 103 | +- No input parameters (just presence indicates what's available) |
| 104 | +- LLM reads descriptions to understand what tools are in each group |
| 105 | +- Calling a gateway tool loads that group's tools into the client's context |
| 106 | + |
| 107 | +#### 3. Server Integration |
| 108 | +The MCP Server handles discovery automatically: |
| 109 | +- When `enable_discovery_with_groups()` is called, discovery is enabled |
| 110 | +- `listTools()` returns only gateway tools initially |
| 111 | +- Gateway tool calls trigger loading of actual tools |
| 112 | +- `is_discovery_enabled` property tracks whether discovery is active |
| 113 | + |
| 114 | +### Sample Implementation |
| 115 | + |
| 116 | +```python |
| 117 | +from mcp.server import Server |
| 118 | +from mcp import ToolGroup, Tool |
| 119 | + |
| 120 | +# Define tool groups programmatically |
| 121 | +math_group = ToolGroup( |
| 122 | + name="math", |
| 123 | + description="Mathematical operations", |
| 124 | + tools=[ |
| 125 | + Tool(name="add", description="Add numbers", inputSchema={...}), |
| 126 | + Tool(name="subtract", description="Subtract numbers", inputSchema={...}), |
| 127 | + ] |
| 128 | +) |
| 129 | + |
| 130 | +# Enable discovery |
| 131 | +server = Server("my-service") |
| 132 | +server.enable_discovery_with_groups([math_group]) |
| 133 | + |
| 134 | +# listTools() now returns only gateway tools |
| 135 | +# Actual tools load when gateway is called |
| 136 | +``` |
| 137 | + |
| 138 | +### First `listTools()` Call Example |
| 139 | + |
| 140 | +Server returns **only gateway tools**: |
| 141 | +```json |
| 142 | +[ |
| 143 | + { |
| 144 | + "name": "get_math_tools", |
| 145 | + "description": "Mathematical operations including addition, subtraction, multiplication, and division", |
| 146 | + "inputSchema": {"type": "object", "properties": {}, "required": []} |
| 147 | + }, |
| 148 | + { |
| 149 | + "name": "get_weather_tools", |
| 150 | + "description": "Weather information tools including forecasts and alerts", |
| 151 | + "inputSchema": {"type": "object", "properties": {}, "required": []} |
| 152 | + } |
| 153 | +] |
| 154 | +``` |
| 155 | + |
| 156 | +LLM reads descriptions and understands what each group provides. |
| 157 | + |
| 158 | +## Client-Side Experience |
| 159 | + |
| 160 | +When a client connects to a progressive discovery server: |
| 161 | + |
| 162 | +1. **Initial state**: Client gets only gateway tools (~300-500 tokens) |
| 163 | +2. **User request**: LLM decides which group is relevant based on descriptions |
| 164 | +3. **Gateway call**: LLM calls the gateway tool with no parameters |
| 165 | +4. **Tool loading**: Server automatically loads that group's tools |
| 166 | +5. **Tool refresh**: Client receives the new tools and updates its context |
| 167 | +6. **Tool usage**: LLM uses actual tools from the loaded group |
| 168 | +7. **Isolation**: Other groups remain hidden from context |
| 169 | + |
| 170 | +## Is Discovery Enabled? |
| 171 | + |
| 172 | +The Server class provides a property to check discovery status: |
| 173 | + |
| 174 | +```python |
| 175 | +server = Server("my-service") |
| 176 | +print(server.is_discovery_enabled) # False by default |
| 177 | + |
| 178 | +# Enable discovery |
| 179 | +server.enable_discovery_with_groups([group1, group2]) |
| 180 | +print(server.is_discovery_enabled) # True when enabled |
| 181 | +``` |
| 182 | + |
| 183 | +## Hybrid Mode (Optional) |
| 184 | + |
| 185 | +You can mix approaches: |
| 186 | +- **Gateway tools**: Domain-specific tools loaded on demand |
| 187 | +- **Direct tools**: High-frequency operations always visible |
| 188 | + |
| 189 | +Example: |
| 190 | +- `divide` tool visible everywhere (direct tool) |
| 191 | +- `add`, `subtract`, `multiply` in math group (gateway tool) |
| 192 | + |
| 193 | +## Extending the System |
| 194 | + |
| 195 | +To add more tool groups: |
| 196 | + |
| 197 | +1. Define a new `ToolGroup` with related tools |
| 198 | +2. Add it to `enable_discovery_with_groups()` |
| 199 | +3. The server automatically creates gateway tools |
| 200 | +4. No additional handler code needed |
| 201 | + |
| 202 | +## Benefits Demonstrated |
| 203 | + |
| 204 | +- **Token Efficiency** - Only relevant tools in context |
| 205 | +- **Scalability** - Easy to add many tool groups |
| 206 | +- **LLM Autonomy** - LLM decides which tools to load |
| 207 | +- **Clean Architecture** - Semantic grouping is explicit |
| 208 | +- **Backward Compatible** - No changes to existing MCP protocol |
| 209 | + |
| 210 | +## Further Reading |
| 211 | + |
| 212 | +- [CLAUDE.md](../../.claude/CLAUDE.md) - Full specification |
| 213 | +- [PHASE_1_IMPLEMENTATION.md](../../.claude/PHASE_1_IMPLEMENTATION.md) - Core system |
| 214 | +- [PHASE_2_IMPLEMENTATION.md](../../.claude/PHASE_2_IMPLEMENTATION.md) - Server integration |
| 215 | + |
| 216 | +## Key Takeaways |
| 217 | + |
| 218 | +- **Progressive discovery is optional** - `is_discovery_enabled` controls whether it's active |
| 219 | +- **Backward compatible** - Existing MCP servers work unchanged |
| 220 | +- **Tool groups are flexible** - Define any semantic grouping that makes sense for your domain |
| 221 | +- **Client handling is automatic** - Refresh happens transparently via notifications |
| 222 | +- **Hybrid mode possible** - Mix direct and grouped tools as needed |
0 commit comments