Skip to content

Commit 7821062

Browse files
docs(mcp-host-config): write new extension guide
Create streamlined extension guide for the Unified Adapter Architecture. Replaces the legacy 10-step process with a simplified 4-step approach. Key sections: - Integration Checklist: 4 points vs legacy 6 (no model registry, no from_omni) - The Pattern: Adapter + Strategy separation of concerns - Implementation Steps: 1. Add host type enum 2. Create adapter (validate + serialize + get_supported_fields) 3. Create strategy (file I/O) 4. Add tests - Declaring Field Support: Using field constants, adding new fields - Field Mappings: Optional field name transformations - Common Patterns: Multiple transports, strict single transport, custom serialization - Testing Your Implementation: Categories and file locations - Troubleshooting: Common issues and debugging tips - Reference: Existing adapter patterns The new guide is significantly shorter (~400 lines vs ~860 lines) while providing complete coverage of the implementation process.
1 parent ff05ad5 commit 7821062

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed
Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
# Extending MCP Host Configuration
2+
3+
**Quick Start:** Create an adapter (validation + serialization), create a strategy (file I/O), add tests. Most implementations are 50-100 lines per file.
4+
5+
## Before You Start: Integration Checklist
6+
7+
The Unified Adapter Architecture requires only **4 integration points**:
8+
9+
| Integration Point | Required? | Files to Modify |
10+
|-------------------|-----------|-----------------|
11+
| ☐ Host type enum | Always | `models.py` |
12+
| ☐ Adapter class | Always | `adapters/your_host.py`, `adapters/__init__.py` |
13+
| ☐ Strategy class | Always | `strategies.py` |
14+
| ☐ Test infrastructure | Always | `tests/unit/mcp/`, `tests/integration/mcp/` |
15+
16+
> **Note:** No host-specific models, no `from_omni()` conversion, no model registry integration. The unified model handles all fields.
17+
18+
## When You Need This
19+
20+
You want Hatch to configure MCP servers on a new host platform:
21+
22+
- A code editor not yet supported (Zed, Neovim, etc.)
23+
- A custom MCP host implementation
24+
- Cloud-based development environments
25+
- Specialized MCP server platforms
26+
27+
## The Pattern: Adapter + Strategy
28+
29+
The Unified Adapter Architecture separates concerns:
30+
31+
| Component | Responsibility | Interface |
32+
|-----------|----------------|-----------|
33+
| **Adapter** | Validation + Serialization | `validate()`, `serialize()`, `get_supported_fields()` |
34+
| **Strategy** | File I/O | `read_configuration()`, `write_configuration()`, `get_config_path()` |
35+
36+
```
37+
MCPServerConfig (unified model)
38+
39+
40+
┌──────────────┐
41+
│ Adapter │ ← Validates fields, serializes to host format
42+
└──────────────┘
43+
44+
45+
┌──────────────┐
46+
│ Strategy │ ← Reads/writes configuration files
47+
└──────────────┘
48+
49+
50+
config.json
51+
```
52+
53+
## Implementation Steps
54+
55+
### Step 1: Add Host Type Enum
56+
57+
Add your host to `MCPHostType` in `hatch/mcp_host_config/models.py`:
58+
59+
```python
60+
class MCPHostType(str, Enum):
61+
# ... existing types ...
62+
YOUR_HOST = "your-host" # Use lowercase with hyphens
63+
```
64+
65+
### Step 2: Create Host Adapter
66+
67+
Create `hatch/mcp_host_config/adapters/your_host.py`:
68+
69+
```python
70+
"""Your Host adapter for MCP host configuration."""
71+
72+
from typing import Any, Dict, FrozenSet
73+
74+
from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
75+
from hatch.mcp_host_config.fields import UNIVERSAL_FIELDS
76+
from hatch.mcp_host_config.models import MCPServerConfig
77+
78+
79+
class YourHostAdapter(BaseAdapter):
80+
"""Adapter for Your Host."""
81+
82+
@property
83+
def host_name(self) -> str:
84+
return "your-host"
85+
86+
def get_supported_fields(self) -> FrozenSet[str]:
87+
"""Return fields Your Host accepts."""
88+
# Start with universal fields, add host-specific ones
89+
return UNIVERSAL_FIELDS | frozenset({
90+
"type", # If your host supports transport type
91+
# "your_specific_field",
92+
})
93+
94+
def validate(self, config: MCPServerConfig) -> None:
95+
"""Validate configuration for Your Host."""
96+
# Check transport requirements
97+
if not config.command and not config.url:
98+
raise AdapterValidationError(
99+
"Either 'command' (local) or 'url' (remote) required",
100+
host_name=self.host_name
101+
)
102+
103+
# Add any host-specific validation
104+
# if config.command and config.url:
105+
# raise AdapterValidationError("Cannot have both", ...)
106+
107+
def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
108+
"""Serialize configuration for Your Host format."""
109+
self.validate(config)
110+
return self.filter_fields(config)
111+
```
112+
113+
**Then register in `hatch/mcp_host_config/adapters/__init__.py`:**
114+
115+
```python
116+
from hatch.mcp_host_config.adapters.your_host import YourHostAdapter
117+
118+
__all__ = [
119+
# ... existing exports ...
120+
"YourHostAdapter",
121+
]
122+
```
123+
124+
**And add to registry in `hatch/mcp_host_config/adapters/registry.py`:**
125+
126+
```python
127+
from hatch.mcp_host_config.adapters.your_host import YourHostAdapter
128+
129+
def _register_defaults(self) -> None:
130+
# ... existing registrations ...
131+
self.register(YourHostAdapter())
132+
```
133+
134+
### Step 3: Create Host Strategy
135+
136+
Add to `hatch/mcp_host_config/strategies.py`:
137+
138+
```python
139+
@register_host_strategy(MCPHostType.YOUR_HOST)
140+
class YourHostStrategy(MCPHostStrategy):
141+
"""Strategy for Your Host file I/O."""
142+
143+
def get_config_path(self) -> Optional[Path]:
144+
"""Return path to config file."""
145+
return Path.home() / ".your_host" / "config.json"
146+
147+
def is_host_available(self) -> bool:
148+
"""Check if host is installed."""
149+
config_path = self.get_config_path()
150+
return config_path is not None and config_path.parent.exists()
151+
152+
def get_config_key(self) -> str:
153+
"""Return the key containing MCP servers."""
154+
return "mcpServers" # Most hosts use this
155+
156+
# read_configuration() and write_configuration()
157+
# can inherit from a base class or implement from scratch
158+
```
159+
160+
**Inheriting from existing strategy families:**
161+
162+
```python
163+
# If similar to Claude (standard JSON format)
164+
class YourHostStrategy(ClaudeHostStrategy):
165+
def get_config_path(self) -> Optional[Path]:
166+
return Path.home() / ".your_host" / "config.json"
167+
168+
# If similar to Cursor (flexible path handling)
169+
class YourHostStrategy(CursorBasedHostStrategy):
170+
def get_config_path(self) -> Optional[Path]:
171+
return Path.home() / ".your_host" / "config.json"
172+
```
173+
174+
### Step 4: Add Tests
175+
176+
**Unit tests** (`tests/unit/mcp/test_your_host_adapter.py`):
177+
178+
```python
179+
class TestYourHostAdapter(unittest.TestCase):
180+
def setUp(self):
181+
self.adapter = YourHostAdapter()
182+
183+
def test_host_name(self):
184+
self.assertEqual(self.adapter.host_name, "your-host")
185+
186+
def test_supported_fields(self):
187+
fields = self.adapter.get_supported_fields()
188+
self.assertIn("command", fields)
189+
190+
def test_validate_requires_transport(self):
191+
config = MCPServerConfig(name="test")
192+
with self.assertRaises(AdapterValidationError):
193+
self.adapter.validate(config)
194+
195+
def test_serialize_filters_unsupported(self):
196+
config = MCPServerConfig(name="test", command="python", httpUrl="http://x")
197+
result = self.adapter.serialize(config)
198+
self.assertNotIn("httpUrl", result) # Assuming not supported
199+
```
200+
201+
## Declaring Field Support
202+
203+
### Using Field Constants
204+
205+
Import from `hatch/mcp_host_config/fields.py`:
206+
207+
```python
208+
from hatch.mcp_host_config.fields import (
209+
UNIVERSAL_FIELDS, # command, args, env, url, headers
210+
CLAUDE_FIELDS, # UNIVERSAL + type
211+
VSCODE_FIELDS, # CLAUDE + envFile, inputs
212+
CURSOR_FIELDS, # CLAUDE + envFile
213+
)
214+
215+
# Compose your host's fields
216+
YOUR_HOST_FIELDS = UNIVERSAL_FIELDS | frozenset({
217+
"type",
218+
"your_specific_field",
219+
})
220+
```
221+
222+
### Adding New Host-Specific Fields
223+
224+
If your host has unique fields not in the unified model:
225+
226+
1. **Add to `MCPServerConfig`** in `models.py`:
227+
228+
```python
229+
# Host-specific fields
230+
your_field: Optional[str] = Field(None, description="Your Host specific field")
231+
```
232+
233+
2. **Add to field constants** in `fields.py`:
234+
235+
```python
236+
YOUR_HOST_FIELDS = UNIVERSAL_FIELDS | frozenset({
237+
"your_field",
238+
})
239+
```
240+
241+
3. **Add CLI argument** (optional) in `hatch/cli/__main__.py`:
242+
243+
```python
244+
mcp_configure_parser.add_argument(
245+
"--your-field",
246+
help="Your Host specific field"
247+
)
248+
```
249+
250+
## Field Mappings (Optional)
251+
252+
If your host uses different names for standard fields:
253+
254+
```python
255+
# In your adapter
256+
def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
257+
self.validate(config)
258+
result = self.filter_fields(config)
259+
260+
# Apply mappings (e.g., 'args' → 'arguments')
261+
if "args" in result:
262+
result["arguments"] = result.pop("args")
263+
264+
return result
265+
```
266+
267+
Or define mappings centrally in `fields.py`:
268+
269+
```python
270+
YOUR_HOST_FIELD_MAPPINGS = {
271+
"args": "arguments",
272+
"headers": "http_headers",
273+
}
274+
```
275+
276+
## Common Patterns
277+
278+
### Multiple Transport Support
279+
280+
Some hosts (like Gemini) support multiple transports:
281+
282+
```python
283+
def validate(self, config: MCPServerConfig) -> None:
284+
transports = sum([
285+
config.command is not None,
286+
config.url is not None,
287+
config.httpUrl is not None,
288+
])
289+
290+
if transports == 0:
291+
raise AdapterValidationError("At least one transport required")
292+
293+
# Allow multiple transports if your host supports it
294+
```
295+
296+
### Strict Single Transport
297+
298+
Some hosts (like Claude) require exactly one transport:
299+
300+
```python
301+
def validate(self, config: MCPServerConfig) -> None:
302+
has_command = config.command is not None
303+
has_url = config.url is not None
304+
305+
if not has_command and not has_url:
306+
raise AdapterValidationError("Need command or url")
307+
308+
if has_command and has_url:
309+
raise AdapterValidationError("Cannot have both command and url")
310+
```
311+
312+
### Custom Serialization
313+
314+
Override `serialize()` for custom output format:
315+
316+
```python
317+
def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
318+
self.validate(config)
319+
result = self.filter_fields(config)
320+
321+
# Transform to your host's expected structure
322+
if config.type == "stdio":
323+
result["transport"] = {"type": "stdio", "command": result.pop("command")}
324+
325+
return result
326+
```
327+
328+
## Testing Your Implementation
329+
330+
### Test Categories
331+
332+
| Category | What to Test |
333+
|----------|--------------|
334+
| **Protocol** | `host_name`, `get_supported_fields()` return correct values |
335+
| **Validation** | `validate()` accepts valid configs, rejects invalid |
336+
| **Serialization** | `serialize()` produces correct format, filters fields |
337+
| **Integration** | Adapter works with registry, strategy reads/writes files |
338+
339+
### Test File Location
340+
341+
```
342+
tests/
343+
├── unit/mcp/
344+
│ └── test_your_host_adapter.py # Protocol + validation + serialization
345+
└── integration/mcp/
346+
└── test_your_host_strategy.py # File I/O + end-to-end
347+
```
348+
349+
## Troubleshooting
350+
351+
### Common Issues
352+
353+
| Issue | Cause | Solution |
354+
|-------|-------|----------|
355+
| Adapter not found | Not registered in registry | Add to `_register_defaults()` |
356+
| Field not serialized | Not in `get_supported_fields()` | Add field to set |
357+
| Validation always fails | Logic error in `validate()` | Check conditions |
358+
| Name appears in output | Not filtering excluded fields | Use `filter_fields()` |
359+
360+
### Debugging Tips
361+
362+
```python
363+
# Print what adapter sees
364+
adapter = get_adapter("your-host")
365+
print(f"Supported fields: {adapter.get_supported_fields()}")
366+
367+
config = MCPServerConfig(name="test", command="python")
368+
print(f"Filtered: {adapter.filter_fields(config)}")
369+
print(f"Serialized: {adapter.serialize(config)}")
370+
```
371+
372+
## Reference: Existing Adapters
373+
374+
Study these for patterns:
375+
376+
| Adapter | Notable Features |
377+
|---------|------------------|
378+
| `ClaudeAdapter` | Variant support (desktop/code), strict transport validation |
379+
| `VSCodeAdapter` | Extended fields (envFile, inputs) |
380+
| `GeminiAdapter` | Multiple transport support, many host-specific fields |
381+
| `KiroAdapter` | Disabled/autoApprove fields |
382+
| `CodexAdapter` | Field mappings (args→arguments) |
383+
384+
## Summary
385+
386+
Adding a new host is now a **4-step process**:
387+
388+
1. **Add enum** to `MCPHostType`
389+
2. **Create adapter** with `validate()` + `serialize()` + `get_supported_fields()`
390+
3. **Create strategy** with `get_config_path()` + file I/O methods
391+
4. **Add tests** for adapter and strategy
392+
393+
The unified model handles all fields. Adapters filter and validate. Strategies handle files. No model conversion needed.
394+

0 commit comments

Comments
 (0)