Files
gh-resolve-io-prism/skills/skill-builder/reference/dynamic-manifests.md
2025-11-30 08:51:34 +08:00

1001 lines
24 KiB
Markdown

# Dynamic Manifests
> **Definition**: A runtime capability discovery system that allows clients to query available tools, resources, and features dynamically without requiring restarts or static configuration.
**Navigation**: [← Progressive Disclosure](./progressive-disclosure.md) | [↑ Best Practices](../README.md) | [Deferred Loading →](./deferred-loading.md)
---
## Table of Contents
- [What Is It?](#what-is-it) ← Start here
- [Quick Start](#quick-start) ← Get it working in 5 minutes
- [Why Dynamic?](#why-dynamic)
- [Configuration](#configuration) ← For practitioners
- [Server Implementation](#server-implementation)
- [Client Implementation](#client-implementation)
- [Capability Systems](#capability-systems) ← For architects
- [MCP-Specific Setup](#mcp-specific-setup)
- [Troubleshooting](#troubleshooting)
---
## What Is It?
Traditional systems load capabilities at startup. Dynamic manifests query them at runtime.
### Visual Comparison
```
STATIC MANIFEST (Old Way)
─────────────────────────────────────
Startup:
Load manifest.json → Parse all tools → Cache forever
Tools: [A, B, C, D, E]
Hour 1: User needs Tool A
✓ Use cached Tool A
Hour 2: Server adds Tool F
✗ Client doesn't know about Tool F
✗ Requires restart to see Tool F
Hour 3: Server removes Tool D
✗ Client still tries to use Tool D
✗ Error: Tool not found
DYNAMIC MANIFEST (New Way)
─────────────────────────────────────
Startup:
Connect to server → Register for updates
Initial query: Tools = [A, B, C, D, E]
Hour 1: User needs Tool A
Query server → Tools available: [A, B, C, D, E]
✓ Use Tool A
Hour 2: Server adds Tool F
Server notifies → Client updates
Query server → Tools available: [A, B, C, D, E, F]
✓ Tool F immediately available
Hour 3: Server removes Tool D
Server notifies → Client updates
Query server → Tools available: [A, B, C, E, F]
✓ Client never tries to use Tool D
```
### Key Difference
| Aspect | Static | Dynamic |
|--------|--------|---------|
| **When capabilities are discovered** | Startup only | Runtime, continuously |
| **Adding new features** | Requires restart | Available immediately |
| **Removing features** | Clients break until restart | Graceful degradation |
| **Conditional availability** | Not possible | Context-aware |
| **Memory usage** | All features loaded | On-demand loading |
---
## Quick Start
Get dynamic manifests working in 5 minutes.
### Step 1: Server Configuration
Create manifest file at standard location:
**.well-known/mcp/manifest.json**
```json
{
"id": "my-mcp-server",
"name": "My MCP Server",
"version": "1.0.0",
"last_updated": "2025-10-20T10:00:00Z",
"capabilities": {
"tools": true,
"resources": true,
"prompts": true,
"dynamic_discovery": true
},
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
}
},
"required": ["location"]
}
}
]
}
```
### Step 2: Client Configuration
**For Claude Desktop** (claude_desktop_config.json):
```json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["path/to/server.js"],
"dynamicDiscovery": true,
"discoveryInterval": 5000
}
}
}
```
**For Spring AI**:
```java
@Bean
public McpClient mcpClient() {
return McpClient.builder()
.dynamicToolDiscovery(true)
.discoveryIntervalMs(5000)
.build();
}
```
### Step 3: Test
Add new tool to manifest (without restart):
```json
{
"tools": [
{
"name": "get_weather",
...
},
{
"name": "get_forecast",
"description": "Get 5-day forecast",
"parameters": { ... }
}
]
}
```
Client should see new tool within 5 seconds!
---
## Why Dynamic?
### Problem 1: Static Configuration is Brittle
```
Dev adds new feature → Commit server code
Deploy server → New feature available
❌ But: Clients don't know about it
Users restart apps → Feature finally available
```
This creates delays and poor UX.
### Problem 2: Context Awareness
Static manifests can't adapt:
```
User A (beginner): Static manifest shows all 50 tools ❌
User B (expert): Static manifest shows all 50 tools ❌
User C (admin): Static manifest shows all 50 tools ❌
Everyone sees everything, regardless of skill/permissions.
```
Dynamic manifests adapt:
```
User A (beginner): Query → 5 basic tools ✓
User B (expert): Query → 25 tools (basic + advanced) ✓
User C (admin): Query → 50 tools (all features) ✓
Each user sees appropriate capabilities.
```
### Problem 3: Performance
Static manifests load everything:
```
Startup: Load 50 tools → Initialize all dependencies
Memory: 500MB
Time: 10 seconds
```
Dynamic manifests load on-demand:
```
Startup: Connect to server
Memory: 50MB
Time: 1 second
Later: User requests Tool X → Load Tool X dependencies
```
---
## Configuration
### Server-Side: Expose Discovery Endpoints
Your MCP server must implement:
```
GET /.well-known/mcp/manifest.json → Full manifest
GET /tools/list → Available tools
GET /resources/list → Available resources
GET /prompts/list → Available prompts
```
**Example Express.js Server**:
```javascript
const express = require('express');
const app = express();
// Manifest endpoint
app.get('/.well-known/mcp/manifest.json', (req, res) => {
res.json({
id: 'my-server',
version: '1.0.0',
last_updated: new Date().toISOString(),
capabilities: {
tools: true,
dynamic_discovery: true
},
tools: getAvailableTools(req.user) // Context-aware!
});
});
// Tools list endpoint
app.get('/tools/list', (req, res) => {
const tools = getAvailableTools(req.user);
res.json({ tools });
});
function getAvailableTools(user) {
const tools = [
{ name: 'basic_tool', description: '...' }
];
if (user.isAdmin) {
tools.push({ name: 'admin_tool', description: '...' });
}
return tools;
}
app.listen(3000);
```
### Client-Side: Enable Dynamic Discovery
**Key Configuration Options**:
| Option | Description | Default | Recommended |
|--------|-------------|---------|-------------|
| `dynamicDiscovery` | Enable runtime queries | `false` | `true` |
| `discoveryInterval` | How often to check (ms) | `10000` | `5000` |
| `cacheManifest` | Cache between queries | `true` | `true` |
| `cacheTimeout` | Cache duration (ms) | `300000` | `60000` |
**Example: Python Client**:
```python
from mcp import MCPClient
client = MCPClient(
server_url="http://localhost:3000",
dynamic_discovery=True,
discovery_interval=5000, # Check every 5 seconds
)
# Don't cache tools - always query fresh
async def execute_tool(tool_name, params):
# This queries server each time
tools = await client.get_available_tools()
tool = tools.get(tool_name)
if not tool:
raise Exception(f"Tool {tool_name} not available")
return await tool.execute(params)
```
### Optimization: Smart Caching
Don't query on every request - use smart caching:
```python
import hashlib
import json
from datetime import datetime, timedelta
class SmartManifestCache:
def __init__(self, client, ttl_seconds=60):
self.client = client
self.ttl = timedelta(seconds=ttl_seconds)
self.cached_manifest = None
self.cached_hash = None
self.last_fetch = None
async def get_tools(self):
now = datetime.now()
# Check if cache is still valid
if (self.cached_manifest and self.last_fetch and
now - self.last_fetch < self.ttl):
return self.cached_manifest
# Fetch new manifest
manifest = await self.client.fetch_manifest()
manifest_hash = hashlib.md5(
json.dumps(manifest, sort_keys=True).encode()
).hexdigest()
# Only update if manifest changed
if manifest_hash != self.cached_hash:
print("Manifest changed! New tools available.")
self.cached_manifest = manifest
self.cached_hash = manifest_hash
self.last_fetch = now
return self.cached_manifest
```
---
## Server Implementation
### Pattern 1: Basic Dynamic Server
Minimal implementation with runtime queries:
```python
from fastapi import FastAPI
from typing import List, Dict
app = FastAPI()
# Dynamic tool registry
tool_registry = {}
def register_tool(name: str, config: Dict):
"""Register a tool at runtime"""
tool_registry[name] = config
def unregister_tool(name: str):
"""Remove a tool at runtime"""
if name in tool_registry:
del tool_registry[name]
@app.get("/.well-known/mcp/manifest.json")
async def get_manifest():
return {
"id": "dynamic-server",
"version": "1.0.0",
"last_updated": datetime.now().isoformat(),
"capabilities": {
"tools": True,
"dynamic_discovery": True
},
"tools": list(tool_registry.values())
}
@app.get("/tools/list")
async def list_tools():
return {"tools": list(tool_registry.values())}
# Add/remove tools dynamically
@app.post("/admin/register_tool")
async def admin_register_tool(tool_config: Dict):
register_tool(tool_config["name"], tool_config)
return {"status": "registered"}
# Initial tools
register_tool("get_weather", {
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {...}
})
```
### Pattern 2: Context-Aware Capabilities
Adapt capabilities based on user/context:
```python
from fastapi import FastAPI, Request
from typing import Optional
@app.get("/tools/list")
async def list_tools(request: Request):
user = get_user_from_request(request)
context = detect_context(request)
tools = []
# Basic tools for everyone
tools.extend(get_basic_tools())
# Skill-based tools
if user.skill_level >= "intermediate":
tools.extend(get_advanced_tools())
# Permission-based tools
if user.has_permission("admin"):
tools.extend(get_admin_tools())
# Context-based tools
if context.project_type == "data_science":
tools.extend(get_data_tools())
if context.language == "python":
tools.extend(get_python_tools())
return {"tools": tools}
def detect_context(request: Request) -> Context:
"""Detect context from request headers/params"""
return Context(
project_type=request.headers.get("X-Project-Type"),
language=request.headers.get("X-Language"),
working_dir=request.headers.get("X-Working-Dir")
)
```
### Pattern 3: Change Notifications
Notify clients when capabilities change:
```python
from fastapi import FastAPI, WebSocket
from typing import Set
import asyncio
app = FastAPI()
active_connections: Set[WebSocket] = set()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
active_connections.add(websocket)
try:
while True:
await websocket.receive_text()
except:
active_connections.remove(websocket)
async def notify_tools_changed():
"""Notify all connected clients that tools changed"""
message = {
"event": "tools_changed",
"timestamp": datetime.now().isoformat()
}
disconnected = set()
for connection in active_connections:
try:
await connection.send_json(message)
except:
disconnected.add(connection)
# Clean up disconnected clients
active_connections.difference_update(disconnected)
# When tools change
@app.post("/admin/register_tool")
async def admin_register_tool(tool_config: Dict):
register_tool(tool_config["name"], tool_config)
await notify_tools_changed() # ← Notify clients!
return {"status": "registered"}
```
---
## Client Implementation
### Pattern 1: Polling
Periodically check for changes:
```python
import asyncio
from typing import Optional
class PollingMCPClient:
def __init__(self, server_url: str, poll_interval: int = 5):
self.server_url = server_url
self.poll_interval = poll_interval
self.cached_tools = None
self._polling_task = None
async def start(self):
"""Start polling for manifest changes"""
self._polling_task = asyncio.create_task(self._poll_loop())
async def stop(self):
"""Stop polling"""
if self._polling_task:
self._polling_task.cancel()
async def _poll_loop(self):
"""Poll server for changes"""
while True:
try:
tools = await self._fetch_tools()
if tools != self.cached_tools:
print("Tools changed!")
self.cached_tools = tools
await self._on_tools_changed(tools)
except Exception as e:
print(f"Error polling: {e}")
await asyncio.sleep(self.poll_interval)
async def _fetch_tools(self):
"""Fetch tools from server"""
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.server_url}/tools/list") as resp:
data = await resp.json()
return data["tools"]
async def _on_tools_changed(self, tools):
"""Handle tool changes"""
# Update UI, clear caches, etc.
pass
```
### Pattern 2: WebSocket Subscriptions
Real-time updates via WebSocket:
```python
import asyncio
import websockets
import json
class WebSocketMCPClient:
def __init__(self, server_url: str):
self.server_url = server_url
self.ws = None
self.cached_tools = None
async def connect(self):
"""Connect to server WebSocket"""
ws_url = self.server_url.replace("http", "ws") + "/ws"
self.ws = await websockets.connect(ws_url)
# Start listening for events
asyncio.create_task(self._listen())
# Fetch initial tools
await self.refresh_tools()
async def _listen(self):
"""Listen for server events"""
async for message in self.ws:
event = json.loads(message)
if event["event"] == "tools_changed":
print("Server notified: tools changed")
await self.refresh_tools()
async def refresh_tools(self):
"""Fetch latest tools from server"""
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.server_url}/tools/list") as resp:
data = await resp.json()
self.cached_tools = data["tools"]
print(f"Refreshed {len(self.cached_tools)} tools")
```
### Pattern 3: Lazy Query
Don't cache - always query fresh:
```python
class LazyMCPClient:
"""Client that always queries server for latest capabilities"""
def __init__(self, server_url: str):
self.server_url = server_url
async def get_tool(self, tool_name: str):
"""Get a specific tool (always fresh from server)"""
tools = await self._fetch_tools()
return next((t for t in tools if t["name"] == tool_name), None)
async def execute_tool(self, tool_name: str, params: dict):
"""Execute a tool (checks availability first)"""
tool = await self.get_tool(tool_name)
if not tool:
raise Exception(f"Tool {tool_name} not available")
# Execute tool...
return await self._execute(tool, params)
async def _fetch_tools(self):
"""Always fetch fresh from server"""
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.server_url}/tools/list") as resp:
data = await resp.json()
return data["tools"]
```
---
## Capability Systems
### Tiered Capabilities
Implement progressive disclosure through capability tiers:
```python
class CapabilityTier(Enum):
BASIC = 1 # Everyone gets these
INTERMEDIATE = 2 # Requires skill level
ADVANCED = 3 # Requires permission
EXPERT = 4 # Requires admin
class CapabilityManager:
def __init__(self):
self.capabilities = {
CapabilityTier.BASIC: [
"search", "file_read", "file_write"
],
CapabilityTier.INTERMEDIATE: [
"bulk_operations", "advanced_search"
],
CapabilityTier.ADVANCED: [
"system_config", "deployment"
],
CapabilityTier.EXPERT: [
"debug_mode", "raw_sql"
]
}
def get_capabilities_for_user(self, user):
"""Return capabilities based on user tier"""
capabilities = []
# Basic: everyone
capabilities.extend(self.capabilities[CapabilityTier.BASIC])
# Intermediate: skill-based
if user.skill_level >= "intermediate":
capabilities.extend(self.capabilities[CapabilityTier.INTERMEDIATE])
# Advanced: permission-based
if user.has_permission("advanced"):
capabilities.extend(self.capabilities[CapabilityTier.ADVANCED])
# Expert: admin only
if user.is_admin:
capabilities.extend(self.capabilities[CapabilityTier.EXPERT])
return capabilities
```
### Conditional Capabilities
Enable features based on runtime conditions:
```python
class ConditionalCapabilities:
def get_capabilities(self, context):
caps = self.get_base_capabilities()
# Environment-based
if context.environment == "production":
caps.remove("debug_tools")
else:
caps.append("mock_data_generator")
# Project-based
if context.has_file("package.json"):
caps.extend(["npm_tools", "node_debugger"])
if context.has_file("requirements.txt"):
caps.extend(["pip_tools", "python_debugger"])
# Time-based
if context.is_business_hours():
caps.append("live_support_chat")
# Resource-based
if context.disk_space > 10_000_000_000: # 10GB
caps.append("video_processing")
return caps
```
### Gradual Feature Rollout
Use dynamic manifests for A/B testing and gradual rollouts:
```python
class FeatureRollout:
def __init__(self):
self.features = {
"new_search_algorithm": {
"rollout_percentage": 10, # 10% of users
"enabled_for": set()
},
"beta_ui": {
"rollout_percentage": 5,
"enabled_for": set()
}
}
def should_enable_feature(self, feature_name, user_id):
"""Determine if feature should be enabled for user"""
feature = self.features.get(feature_name)
if not feature:
return False
# Already enabled?
if user_id in feature["enabled_for"]:
return True
# Roll dice
import random
if random.random() * 100 < feature["rollout_percentage"]:
feature["enabled_for"].add(user_id)
return True
return False
def get_capabilities_for_user(self, user_id):
caps = ["basic_tool_1", "basic_tool_2"]
if self.should_enable_feature("new_search_algorithm", user_id):
caps.append("new_search_tool")
if self.should_enable_feature("beta_ui", user_id):
caps.append("beta_ui_tools")
return caps
```
---
## MCP-Specific Setup
### Standard MCP Manifest Location
```
project_root/
├── .well-known/
│ └── mcp/
│ └── manifest.json ← Must be here
```
### MCP Manifest Schema
```json
{
"$schema": "https://spec.modelcontextprotocol.io/manifest/v1",
"id": "unique-server-id",
"name": "Human Readable Name",
"version": "1.0.0",
"description": "What this server does",
"last_updated": "2025-10-20T10:00:00Z",
"capabilities": {
"tools": true,
"resources": true,
"prompts": true,
"sampling": false,
"dynamic_discovery": true
},
"tools": [
{
"name": "tool_name",
"description": "What the tool does",
"inputSchema": {
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "Parameter description"
}
},
"required": ["param1"]
}
}
],
"resources": [
{
"uri": "resource://path/to/resource",
"name": "Resource Name",
"description": "What the resource provides",
"mimeType": "application/json"
}
]
}
```
### Claude Desktop Configuration
```json
{
"mcpServers": {
"my-dynamic-server": {
"command": "node",
"args": ["path/to/server.js"],
// Enable dynamic discovery
"dynamicDiscovery": true,
// How often to check for changes (ms)
"discoveryInterval": 5000,
// Optional: Environment variables
"env": {
"API_KEY": "your-key-here"
}
}
}
}
```
### Known Limitations
⚠️ **Claude Code Issue #4110**: Dynamic resources may not be discovered properly in Claude Code. Workarounds:
- Use static resources
- Use tools instead of resources
- Poll for changes manually
---
## Troubleshooting
### Problem: Changes Not Appearing
**Symptoms**: Server updated, client doesn't see changes
**Check**:
1. Is `dynamicDiscovery` enabled in client config?
```json
{ "dynamicDiscovery": true }
```
2. Is manifest endpoint returning updated timestamp?
```bash
curl http://localhost:3000/.well-known/mcp/manifest.json
# Check "last_updated" field
```
3. Is cache timeout too long?
```json
{ "cacheTimeout": 60000 } // Try lower value
```
4. Check client logs:
```bash
tail -f ~/.config/claude/logs/mcp.log
```
### Problem: Too Many Requests
**Symptoms**: Server overwhelmed with manifest queries
**Solutions**:
1. Increase discovery interval:
```json
{ "discoveryInterval": 30000 } // 30 seconds
```
2. Implement rate limiting:
```python
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
@app.get("/tools/list")
@limiter.limit("10/minute")
async def list_tools():
...
```
3. Use ETags for conditional requests:
```python
@app.get("/tools/list")
async def list_tools(request: Request):
tools = get_tools()
tools_hash = hashlib.md5(json.dumps(tools).encode()).hexdigest()
if request.headers.get("If-None-Match") == tools_hash:
return Response(status_code=304) # Not Modified
return Response(
content=json.dumps(tools),
headers={"ETag": tools_hash}
)
```
### Problem: Stale Cache
**Symptoms**: Client sees old capabilities after server update
**Solutions**:
1. Add cache-busting timestamp:
```json
{
"last_updated": "2025-10-20T10:30:00Z",
"cache_key": "v1-20251020-103000"
}
```
2. Implement version-based invalidation:
```python
@app.get("/tools/list")
async def list_tools(client_version: Optional[str] = None):
current_version = "v1.2.3"
if client_version != current_version:
# Force full refresh
return {
"version": current_version,
"invalidate_cache": True,
"tools": get_tools()
}
return {"tools": get_tools()}
```
---
## Related Concepts
### Dynamic Manifests ↔ [Progressive Disclosure](./progressive-disclosure.md)
- Progressive disclosure: **Design pattern** for revealing information gradually
- Dynamic manifests: **Technical implementation** for discovering capabilities
Example flow:
1. User opens app (Progressive disclosure: show basic UI)
2. Client queries server (Dynamic manifest: get available tools)
3. Server returns capabilities based on user tier (Progressive disclosure: only relevant features)
4. UI updates to show available features (Progressive disclosure: tiered feature set)
### Dynamic Manifests → [Deferred Loading](./deferred-loading.md)
Dynamic manifests tell you **what's available**.
Deferred loading determines **when to load it**.
```
Dynamic Manifest Query:
Server: "Tool X is available"
Deferred Loading Decision:
Client: "Don't load Tool X yet, wait until user calls it"
User Invokes Tool X:
Client: "Now load Tool X's dependencies"
```
See: [Deferred Loading: Lazy Initialization](./deferred-loading.md#lazy-initialization)
---
**Navigation**: [← Progressive Disclosure](./progressive-disclosure.md) | [↑ Best Practices](../README.md) | [Deferred Loading →](./deferred-loading.md)
**Last Updated**: 2025-10-20