Skip to content

Commit 9d873aa

Browse files
Merge pull request #47 from LittleCoinCoin/dev
[v0.8.1.dev1] OpenCode MCP host, adding-mcp-hosts skill, and dev docs refresh
2 parents 2d30523 + 793707d commit 9d873aa

38 files changed

Lines changed: 3233 additions & 387 deletions
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
name: adding-mcp-hosts
3+
description: |
4+
Adds support for a new MCP host platform to the Hatch CLI multi-host
5+
configuration system. Use when asked to add, integrate, or extend MCP host
6+
support for a new IDE, editor, or AI coding tool (e.g., Windsurf, Zed,
7+
Copilot). Follows a 5-step workflow: discover host requirements via web
8+
research or user questionnaire, add enum and field set declarations, create
9+
adapter and strategy implementations, wire integration points across 4
10+
registration files, and register test fixtures that auto-generate 20+ test
11+
cases without writing test code.
12+
---
13+
14+
## Workflow Checklist
15+
16+
```
17+
- [ ] Step 1: Discover host requirements
18+
- [ ] Step 2: Add enum and field set
19+
- [ ] Step 3: Create adapter and strategy
20+
- [ ] Step 4: Wire integration points
21+
- [ ] Step 5: Register test fixtures
22+
```
23+
24+
---
25+
26+
## Step 1: Discover Host Requirements
27+
28+
Read [references/discovery-guide.md](references/discovery-guide.md) for the full discovery workflow.
29+
30+
Use web search, Context7, and codebase retrieval to find the target host's MCP
31+
configuration: config file path per platform, format (JSON/JSONC/TOML), top-level key,
32+
every supported field name and type, and any field name differences from the universal
33+
set (`command`, `args`, `env`, `url`, `headers`).
34+
35+
If research leaves blockers unresolved, present the structured questionnaire from the
36+
discovery guide to the user.
37+
38+
Write `__reports__/<host-name>/00-parameter_analysis_v0.md` (field-level discovery) and
39+
`__reports__/<host-name>/01-architecture_analysis_v0.md` (integration analysis and NO-GO
40+
assessment). Also produce the Host Spec YAML block — it feeds all subsequent steps.
41+
42+
---
43+
44+
## Step 2: Add Enum and Field Set
45+
46+
Add `MCPHostType` enum value in `hatch/mcp_host_config/models.py`:
47+
48+
```python
49+
class MCPHostType(str, Enum):
50+
# ... existing members ...
51+
YOUR_HOST = "your-host" # lowercase-hyphenated, matching Host Spec slug
52+
```
53+
54+
Add field set constant in `hatch/mcp_host_config/fields.py`:
55+
56+
```python
57+
YOUR_HOST_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset(
58+
{
59+
# host-specific fields from Host Spec
60+
}
61+
)
62+
```
63+
64+
Include `"type"` (via `CLAUDE_FIELDS` base) only if the host uses a transport type
65+
discriminator. If the host uses different field names for universal concepts, add a
66+
mappings dict (see `CODEX_FIELD_MAPPINGS` pattern in `fields.py`).
67+
68+
If the host introduces fields not in `MCPServerConfig`, add them as `Optional` fields
69+
with `Field(None, description="...")` under a new section comment block in `models.py`.
70+
71+
Verify:
72+
73+
```bash
74+
python -c "from hatch.mcp_host_config.models import MCPHostType; print(MCPHostType.YOUR_HOST)"
75+
python -c "from hatch.mcp_host_config.fields import YOUR_HOST_FIELDS; print(YOUR_HOST_FIELDS)"
76+
```
77+
78+
---
79+
80+
## Step 3: Create Adapter and Strategy
81+
82+
Read [references/adapter-contract.md](references/adapter-contract.md) for the `BaseAdapter`
83+
interface, the `validate_filtered()` pipeline, and field mapping details.
84+
85+
Read [references/strategy-contract.md](references/strategy-contract.md) for the
86+
`MCPHostStrategy` interface, `@register_host_strategy` decorator, platform path resolution,
87+
and config serialization.
88+
89+
### Adapter
90+
91+
Create `hatch/mcp_host_config/adapters/your_host.py`. Implement `BaseAdapter` with:
92+
93+
- `host_name` property returning the slug
94+
- `get_supported_fields()` returning the field set from Step 2
95+
- `validate_filtered(filtered)` enforcing host-specific transport rules
96+
- `serialize(config)` calling `filter_fields()` then `validate_filtered()` then returning
97+
the dict (apply field mappings if needed)
98+
99+
**Variant shortcut:** If the new host is functionally identical to an existing host,
100+
register it as a variant instead of creating a new file. See
101+
`ClaudeAdapter(variant=...)` in `hatch/mcp_host_config/adapters/claude.py`.
102+
103+
### Strategy
104+
105+
Add a strategy class in `hatch/mcp_host_config/strategies.py` decorated with
106+
`@register_host_strategy(MCPHostType.YOUR_HOST)`. Decide the family:
107+
108+
- `ClaudeHostStrategy` -- JSON format with `mcpServers` key
109+
- `CursorBasedHostStrategy` -- `.cursor/mcp.json`-like layout
110+
- `MCPHostStrategy` (direct) -- standalone hosts with unique formats
111+
112+
Implement `get_config_path()`, `get_config_key()`, `validate_server_config()`,
113+
`read_config()`, and `write_config()`.
114+
115+
Verify:
116+
117+
```bash
118+
python -c "from hatch.mcp_host_config.adapters.your_host import YourHostAdapter; print(YourHostAdapter().host_name)"
119+
```
120+
121+
---
122+
123+
## Step 4: Wire Integration Points
124+
125+
Four files need one-liner additions.
126+
127+
**`hatch/mcp_host_config/adapters/__init__.py`** -- Import and add to `__all__`:
128+
129+
```python
130+
from hatch.mcp_host_config.adapters.your_host import YourHostAdapter
131+
# Append "YourHostAdapter" to __all__
132+
```
133+
134+
**`hatch/mcp_host_config/adapters/registry.py`** -- Import adapter, add
135+
`self.register(YourHostAdapter())` inside `_register_defaults()`.
136+
137+
**`hatch/mcp_host_config/backup.py`** -- Add `"your-host"` to the `supported_hosts` set
138+
in `BackupInfo.validate_hostname()`. Also update the `supported_hosts` set in
139+
`EnvironmentPackageEntry.validate_host_names()` in `models.py`.
140+
141+
**`hatch/mcp_host_config/reporting.py`** -- Add `MCPHostType.YOUR_HOST: "your-host"` to
142+
the `mapping` dict in `_get_adapter_host_name()`.
143+
144+
Verify:
145+
146+
```bash
147+
python -c "
148+
from hatch.mcp_host_config.adapters.registry import AdapterRegistry
149+
r = AdapterRegistry()
150+
print('your-host' in r.get_supported_hosts())
151+
"
152+
```
153+
154+
---
155+
156+
## Step 5: Register Test Fixtures
157+
158+
Read [references/testing-fixtures.md](references/testing-fixtures.md) for fixture schemas,
159+
auto-generated test case details, and pytest commands.
160+
161+
Add canonical config entry in `tests/test_data/mcp_adapters/canonical_configs.json`:
162+
163+
```json
164+
"your-host": {
165+
"command": "python",
166+
"args": ["-m", "mcp_server"],
167+
"env": {"API_KEY": "test_key"},
168+
"url": null,
169+
"headers": null
170+
}
171+
```
172+
173+
Include all host-specific fields with representative values. Use `null` for unused
174+
transport fields.
175+
176+
Add host registry entries in `tests/test_data/mcp_adapters/host_registry.py`:
177+
178+
1. Import the new field set and adapter.
179+
2. Add `FIELD_SETS` entry: `"your-host": YOUR_HOST_FIELDS`.
180+
3. Add `adapter_map` entry in `HostSpec.get_adapter()`.
181+
4. Add reverse mappings if the host has field name mappings.
182+
5. Add the new field set to `all_possible_fields` in `generate_unsupported_field_test_cases()`.
183+
184+
Verify:
185+
186+
```bash
187+
python -m pytest tests/integration/mcp/ tests/unit/mcp/ tests/regression/mcp/ -v
188+
```
189+
190+
All existing tests must pass. The new host auto-generates test cases for cross-host sync
191+
(N x N matrix), field filtering, transport validation, and property checks.
192+
193+
---
194+
195+
## Cross-References
196+
197+
| Reference | Covers | Read when |
198+
|---|---|---|
199+
| [references/discovery-guide.md](references/discovery-guide.md) | Host research, questionnaire, Host Spec YAML | Step 1 (always) |
200+
| [references/adapter-contract.md](references/adapter-contract.md) | BaseAdapter interface, field sets, registry wiring | Step 3 (always) |
201+
| [references/strategy-contract.md](references/strategy-contract.md) | MCPHostStrategy interface, families, platform paths | Step 3 (always) |
202+
| [references/testing-fixtures.md](references/testing-fixtures.md) | Fixture schema, auto-generated tests, pytest commands | Step 5 (always) |
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Adapter Contract Reference
2+
3+
Interface contract for implementing a new MCP host adapter in the Hatch CLI.
4+
5+
## 1. MCPHostType Enum
6+
7+
File: `hatch/mcp_host_config/models.py`. Convention: `UPPER_SNAKE = "kebab-case"`.
8+
9+
```python
10+
class MCPHostType(str, Enum):
11+
# ... existing members ...
12+
NEW_HOST = "new-host"
13+
```
14+
15+
The enum value string is the canonical host identifier used everywhere.
16+
17+
## 2. Field Set Declaration
18+
19+
File: `hatch/mcp_host_config/fields.py`. Define a `<HOST>_FIELDS` frozenset.
20+
21+
```python
22+
# Without 'type' support — build from UNIVERSAL_FIELDS
23+
NEW_HOST_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset({"host_specific_field"})
24+
25+
# With 'type' support — build from CLAUDE_FIELDS (which is UNIVERSAL_FIELDS | {"type"})
26+
NEW_HOST_FIELDS: FrozenSet[str] = CLAUDE_FIELDS | frozenset({"host_specific_field"})
27+
```
28+
29+
If the host supports the `type` discriminator, also add its kebab-case name to `TYPE_SUPPORTING_HOSTS`. Hosts without `type` support (Gemini, Kiro, Codex) omit this.
30+
31+
## 3. MCPServerConfig Fields
32+
33+
File: `hatch/mcp_host_config/models.py`. Add new fields to `MCPServerConfig` only when the host introduces fields not already in the model. Every field: `Optional`, default `None`.
34+
35+
```python
36+
disabled: Optional[bool] = Field(None, description="Whether server is disabled")
37+
```
38+
39+
If the host reuses existing fields only (e.g., LMStudio reuses `CLAUDE_FIELDS`), skip this step. The model uses `extra="allow"` but explicit declarations are preferred.
40+
41+
## 4. Adapter Class
42+
43+
File: `hatch/mcp_host_config/adapters/<host>.py`. Extend `BaseAdapter`.
44+
45+
```python
46+
from typing import Any, Dict, FrozenSet
47+
from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
48+
from hatch.mcp_host_config.fields import NEW_HOST_FIELDS
49+
from hatch.mcp_host_config.models import MCPServerConfig
50+
51+
class NewHostAdapter(BaseAdapter):
52+
53+
@property
54+
def host_name(self) -> str:
55+
return "new-host"
56+
57+
def get_supported_fields(self) -> FrozenSet[str]:
58+
return NEW_HOST_FIELDS
59+
60+
def validate(self, config: MCPServerConfig) -> None:
61+
pass # DEPRECATED — kept for ABC compliance until v0.9.0
62+
63+
def validate_filtered(self, filtered: Dict[str, Any]) -> None:
64+
has_command = "command" in filtered
65+
has_url = "url" in filtered
66+
if not has_command and not has_url:
67+
raise AdapterValidationError(
68+
"Either 'command' (local) or 'url' (remote) must be specified",
69+
host_name=self.host_name,
70+
)
71+
if has_command and has_url:
72+
raise AdapterValidationError(
73+
"Cannot specify both 'command' and 'url' - choose one transport",
74+
host_name=self.host_name,
75+
)
76+
77+
def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
78+
filtered = self.filter_fields(config)
79+
self.validate_filtered(filtered)
80+
return filtered # add apply_transformations() call if field mappings exist
81+
```
82+
83+
**validate_filtered() rules:** Transport mutual exclusion (`command` XOR `url` for most hosts; Gemini enforces exactly-one-of-three including `httpUrl`). If host supports `type`, verify consistency (`type='stdio'` requires `command`, etc.).
84+
85+
**serialize() pipeline:** Always `filter_fields` -> `validate_filtered` -> optionally `apply_transformations` -> return.
86+
87+
## 5. Field Mappings
88+
89+
File: `hatch/mcp_host_config/fields.py`. Define only when the host uses different field names. Pattern: `{"universal_name": "host_name"}`. Canonical example:
90+
91+
```python
92+
CODEX_FIELD_MAPPINGS: dict[str, str] = {
93+
"args": "arguments",
94+
"headers": "http_headers",
95+
"includeTools": "enabled_tools",
96+
"excludeTools": "disabled_tools",
97+
}
98+
```
99+
100+
Reference in `apply_transformations()`:
101+
102+
```python
103+
def apply_transformations(self, filtered: Dict[str, Any]) -> Dict[str, Any]:
104+
result = filtered.copy()
105+
for universal_name, host_name in NEW_HOST_FIELD_MAPPINGS.items():
106+
if universal_name in result:
107+
result[host_name] = result.pop(universal_name)
108+
return result
109+
```
110+
111+
Skip entirely if the host uses standard field names (most do).
112+
113+
## 6. Variant Pattern
114+
115+
Reuse one adapter class with a `variant` parameter when two host identifiers share identical fields and validation. Canonical example:
116+
117+
```python
118+
class ClaudeAdapter(BaseAdapter):
119+
def __init__(self, variant: str = "desktop"):
120+
if variant not in ("desktop", "code"):
121+
raise ValueError(f"Invalid Claude variant: {variant}")
122+
self._variant = variant
123+
124+
@property
125+
def host_name(self) -> str:
126+
return f"claude-{self._variant}"
127+
```
128+
129+
Use when field set, validation, and serialization are identical. If any diverge, create a separate class.
130+
131+
## 7. Wiring and Integration Points
132+
133+
Four files require one-liner additions for every new host.
134+
135+
**`hatch/mcp_host_config/adapters/__init__.py`** -- Add import and `__all__` entry:
136+
```python
137+
from hatch.mcp_host_config.adapters.new_host import NewHostAdapter
138+
# add "NewHostAdapter" to __all__
139+
```
140+
141+
**`hatch/mcp_host_config/adapters/registry.py`** -- Add to `_register_defaults()`:
142+
```python
143+
self.register(NewHostAdapter()) # import at top of file
144+
```
145+
146+
**`hatch/mcp_host_config/backup.py`** -- Add hostname string to `supported_hosts` set in `BackupInfo.validate_hostname()`:
147+
```python
148+
supported_hosts = {
149+
# ... existing hosts ...
150+
"new-host",
151+
}
152+
```
153+
154+
**`hatch/mcp_host_config/reporting.py`** -- Add entry to mapping dict in `_get_adapter_host_name()`:
155+
```python
156+
MCPHostType.NEW_HOST: "new-host",
157+
```

0 commit comments

Comments
 (0)