diff --git a/.claude/skills/adding-mcp-hosts/SKILL.md b/.claude/skills/adding-mcp-hosts/SKILL.md new file mode 100644 index 0000000..dea6fe1 --- /dev/null +++ b/.claude/skills/adding-mcp-hosts/SKILL.md @@ -0,0 +1,202 @@ +--- +name: adding-mcp-hosts +description: | + Adds support for a new MCP host platform to the Hatch CLI multi-host + configuration system. Use when asked to add, integrate, or extend MCP host + support for a new IDE, editor, or AI coding tool (e.g., Windsurf, Zed, + Copilot). Follows a 5-step workflow: discover host requirements via web + research or user questionnaire, add enum and field set declarations, create + adapter and strategy implementations, wire integration points across 4 + registration files, and register test fixtures that auto-generate 20+ test + cases without writing test code. +--- + +## Workflow Checklist + +``` +- [ ] Step 1: Discover host requirements +- [ ] Step 2: Add enum and field set +- [ ] Step 3: Create adapter and strategy +- [ ] Step 4: Wire integration points +- [ ] Step 5: Register test fixtures +``` + +--- + +## Step 1: Discover Host Requirements + +Read [references/discovery-guide.md](references/discovery-guide.md) for the full discovery workflow. + +Use web search, Context7, and codebase retrieval to find the target host's MCP +configuration: config file path per platform, format (JSON/JSONC/TOML), top-level key, +every supported field name and type, and any field name differences from the universal +set (`command`, `args`, `env`, `url`, `headers`). + +If research leaves blockers unresolved, present the structured questionnaire from the +discovery guide to the user. + +Write `__reports__//00-parameter_analysis_v0.md` (field-level discovery) and +`__reports__//01-architecture_analysis_v0.md` (integration analysis and NO-GO +assessment). Also produce the Host Spec YAML block — it feeds all subsequent steps. + +--- + +## Step 2: Add Enum and Field Set + +Add `MCPHostType` enum value in `hatch/mcp_host_config/models.py`: + +```python +class MCPHostType(str, Enum): + # ... existing members ... + YOUR_HOST = "your-host" # lowercase-hyphenated, matching Host Spec slug +``` + +Add field set constant in `hatch/mcp_host_config/fields.py`: + +```python +YOUR_HOST_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset( + { + # host-specific fields from Host Spec + } +) +``` + +Include `"type"` (via `CLAUDE_FIELDS` base) only if the host uses a transport type +discriminator. If the host uses different field names for universal concepts, add a +mappings dict (see `CODEX_FIELD_MAPPINGS` pattern in `fields.py`). + +If the host introduces fields not in `MCPServerConfig`, add them as `Optional` fields +with `Field(None, description="...")` under a new section comment block in `models.py`. + +Verify: + +```bash +python -c "from hatch.mcp_host_config.models import MCPHostType; print(MCPHostType.YOUR_HOST)" +python -c "from hatch.mcp_host_config.fields import YOUR_HOST_FIELDS; print(YOUR_HOST_FIELDS)" +``` + +--- + +## Step 3: Create Adapter and Strategy + +Read [references/adapter-contract.md](references/adapter-contract.md) for the `BaseAdapter` +interface, the `validate_filtered()` pipeline, and field mapping details. + +Read [references/strategy-contract.md](references/strategy-contract.md) for the +`MCPHostStrategy` interface, `@register_host_strategy` decorator, platform path resolution, +and config serialization. + +### Adapter + +Create `hatch/mcp_host_config/adapters/your_host.py`. Implement `BaseAdapter` with: + +- `host_name` property returning the slug +- `get_supported_fields()` returning the field set from Step 2 +- `validate_filtered(filtered)` enforcing host-specific transport rules +- `serialize(config)` calling `filter_fields()` then `validate_filtered()` then returning + the dict (apply field mappings if needed) + +**Variant shortcut:** If the new host is functionally identical to an existing host, +register it as a variant instead of creating a new file. See +`ClaudeAdapter(variant=...)` in `hatch/mcp_host_config/adapters/claude.py`. + +### Strategy + +Add a strategy class in `hatch/mcp_host_config/strategies.py` decorated with +`@register_host_strategy(MCPHostType.YOUR_HOST)`. Decide the family: + +- `ClaudeHostStrategy` -- JSON format with `mcpServers` key +- `CursorBasedHostStrategy` -- `.cursor/mcp.json`-like layout +- `MCPHostStrategy` (direct) -- standalone hosts with unique formats + +Implement `get_config_path()`, `get_config_key()`, `validate_server_config()`, +`read_config()`, and `write_config()`. + +Verify: + +```bash +python -c "from hatch.mcp_host_config.adapters.your_host import YourHostAdapter; print(YourHostAdapter().host_name)" +``` + +--- + +## Step 4: Wire Integration Points + +Four files need one-liner additions. + +**`hatch/mcp_host_config/adapters/__init__.py`** -- Import and add to `__all__`: + +```python +from hatch.mcp_host_config.adapters.your_host import YourHostAdapter +# Append "YourHostAdapter" to __all__ +``` + +**`hatch/mcp_host_config/adapters/registry.py`** -- Import adapter, add +`self.register(YourHostAdapter())` inside `_register_defaults()`. + +**`hatch/mcp_host_config/backup.py`** -- Add `"your-host"` to the `supported_hosts` set +in `BackupInfo.validate_hostname()`. Also update the `supported_hosts` set in +`EnvironmentPackageEntry.validate_host_names()` in `models.py`. + +**`hatch/mcp_host_config/reporting.py`** -- Add `MCPHostType.YOUR_HOST: "your-host"` to +the `mapping` dict in `_get_adapter_host_name()`. + +Verify: + +```bash +python -c " +from hatch.mcp_host_config.adapters.registry import AdapterRegistry +r = AdapterRegistry() +print('your-host' in r.get_supported_hosts()) +" +``` + +--- + +## Step 5: Register Test Fixtures + +Read [references/testing-fixtures.md](references/testing-fixtures.md) for fixture schemas, +auto-generated test case details, and pytest commands. + +Add canonical config entry in `tests/test_data/mcp_adapters/canonical_configs.json`: + +```json +"your-host": { + "command": "python", + "args": ["-m", "mcp_server"], + "env": {"API_KEY": "test_key"}, + "url": null, + "headers": null +} +``` + +Include all host-specific fields with representative values. Use `null` for unused +transport fields. + +Add host registry entries in `tests/test_data/mcp_adapters/host_registry.py`: + +1. Import the new field set and adapter. +2. Add `FIELD_SETS` entry: `"your-host": YOUR_HOST_FIELDS`. +3. Add `adapter_map` entry in `HostSpec.get_adapter()`. +4. Add reverse mappings if the host has field name mappings. +5. Add the new field set to `all_possible_fields` in `generate_unsupported_field_test_cases()`. + +Verify: + +```bash +python -m pytest tests/integration/mcp/ tests/unit/mcp/ tests/regression/mcp/ -v +``` + +All existing tests must pass. The new host auto-generates test cases for cross-host sync +(N x N matrix), field filtering, transport validation, and property checks. + +--- + +## Cross-References + +| Reference | Covers | Read when | +|---|---|---| +| [references/discovery-guide.md](references/discovery-guide.md) | Host research, questionnaire, Host Spec YAML | Step 1 (always) | +| [references/adapter-contract.md](references/adapter-contract.md) | BaseAdapter interface, field sets, registry wiring | Step 3 (always) | +| [references/strategy-contract.md](references/strategy-contract.md) | MCPHostStrategy interface, families, platform paths | Step 3 (always) | +| [references/testing-fixtures.md](references/testing-fixtures.md) | Fixture schema, auto-generated tests, pytest commands | Step 5 (always) | diff --git a/.claude/skills/adding-mcp-hosts/references/adapter-contract.md b/.claude/skills/adding-mcp-hosts/references/adapter-contract.md new file mode 100644 index 0000000..6100db9 --- /dev/null +++ b/.claude/skills/adding-mcp-hosts/references/adapter-contract.md @@ -0,0 +1,157 @@ +# Adapter Contract Reference + +Interface contract for implementing a new MCP host adapter in the Hatch CLI. + +## 1. MCPHostType Enum + +File: `hatch/mcp_host_config/models.py`. Convention: `UPPER_SNAKE = "kebab-case"`. + +```python +class MCPHostType(str, Enum): + # ... existing members ... + NEW_HOST = "new-host" +``` + +The enum value string is the canonical host identifier used everywhere. + +## 2. Field Set Declaration + +File: `hatch/mcp_host_config/fields.py`. Define a `_FIELDS` frozenset. + +```python +# Without 'type' support — build from UNIVERSAL_FIELDS +NEW_HOST_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset({"host_specific_field"}) + +# With 'type' support — build from CLAUDE_FIELDS (which is UNIVERSAL_FIELDS | {"type"}) +NEW_HOST_FIELDS: FrozenSet[str] = CLAUDE_FIELDS | frozenset({"host_specific_field"}) +``` + +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. + +## 3. MCPServerConfig Fields + +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`. + +```python +disabled: Optional[bool] = Field(None, description="Whether server is disabled") +``` + +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. + +## 4. Adapter Class + +File: `hatch/mcp_host_config/adapters/.py`. Extend `BaseAdapter`. + +```python +from typing import Any, Dict, FrozenSet +from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter +from hatch.mcp_host_config.fields import NEW_HOST_FIELDS +from hatch.mcp_host_config.models import MCPServerConfig + +class NewHostAdapter(BaseAdapter): + + @property + def host_name(self) -> str: + return "new-host" + + def get_supported_fields(self) -> FrozenSet[str]: + return NEW_HOST_FIELDS + + def validate(self, config: MCPServerConfig) -> None: + pass # DEPRECATED — kept for ABC compliance until v0.9.0 + + def validate_filtered(self, filtered: Dict[str, Any]) -> None: + has_command = "command" in filtered + has_url = "url" in filtered + if not has_command and not has_url: + raise AdapterValidationError( + "Either 'command' (local) or 'url' (remote) must be specified", + host_name=self.host_name, + ) + if has_command and has_url: + raise AdapterValidationError( + "Cannot specify both 'command' and 'url' - choose one transport", + host_name=self.host_name, + ) + + def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: + filtered = self.filter_fields(config) + self.validate_filtered(filtered) + return filtered # add apply_transformations() call if field mappings exist +``` + +**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.). + +**serialize() pipeline:** Always `filter_fields` -> `validate_filtered` -> optionally `apply_transformations` -> return. + +## 5. Field Mappings + +File: `hatch/mcp_host_config/fields.py`. Define only when the host uses different field names. Pattern: `{"universal_name": "host_name"}`. Canonical example: + +```python +CODEX_FIELD_MAPPINGS: dict[str, str] = { + "args": "arguments", + "headers": "http_headers", + "includeTools": "enabled_tools", + "excludeTools": "disabled_tools", +} +``` + +Reference in `apply_transformations()`: + +```python +def apply_transformations(self, filtered: Dict[str, Any]) -> Dict[str, Any]: + result = filtered.copy() + for universal_name, host_name in NEW_HOST_FIELD_MAPPINGS.items(): + if universal_name in result: + result[host_name] = result.pop(universal_name) + return result +``` + +Skip entirely if the host uses standard field names (most do). + +## 6. Variant Pattern + +Reuse one adapter class with a `variant` parameter when two host identifiers share identical fields and validation. Canonical example: + +```python +class ClaudeAdapter(BaseAdapter): + def __init__(self, variant: str = "desktop"): + if variant not in ("desktop", "code"): + raise ValueError(f"Invalid Claude variant: {variant}") + self._variant = variant + + @property + def host_name(self) -> str: + return f"claude-{self._variant}" +``` + +Use when field set, validation, and serialization are identical. If any diverge, create a separate class. + +## 7. Wiring and Integration Points + +Four files require one-liner additions for every new host. + +**`hatch/mcp_host_config/adapters/__init__.py`** -- Add import and `__all__` entry: +```python +from hatch.mcp_host_config.adapters.new_host import NewHostAdapter +# add "NewHostAdapter" to __all__ +``` + +**`hatch/mcp_host_config/adapters/registry.py`** -- Add to `_register_defaults()`: +```python +self.register(NewHostAdapter()) # import at top of file +``` + +**`hatch/mcp_host_config/backup.py`** -- Add hostname string to `supported_hosts` set in `BackupInfo.validate_hostname()`: +```python +supported_hosts = { + # ... existing hosts ... + "new-host", +} +``` + +**`hatch/mcp_host_config/reporting.py`** -- Add entry to mapping dict in `_get_adapter_host_name()`: +```python +MCPHostType.NEW_HOST: "new-host", +``` diff --git a/.claude/skills/adding-mcp-hosts/references/discovery-guide.md b/.claude/skills/adding-mcp-hosts/references/discovery-guide.md new file mode 100644 index 0000000..407dcec --- /dev/null +++ b/.claude/skills/adding-mcp-hosts/references/discovery-guide.md @@ -0,0 +1,203 @@ +# Discovery Guide: Host Requirement Research + +Reference for the discovery step when adding a new MCP host to Hatch. +Produces a Host Spec YAML artifact consumed by all subsequent steps. + +--- + +## 1. Research Tools + +Use all available tools. Web search and Context7 are complementary — do not treat either as a fallback for the other. + +| Tool | What to find | +|------|-------------| +| Web search (multiple queries) | Official docs; changelog; known issues mentioning config changes | +| Page fetch | Config type definitions in source (`types.ts`, `*.schema.json`) — source code beats docs pages | +| Context7 (`resolve-library-id` + query) | SDK-level field names, types, validation rules | +| Codebase retrieval (Hatch repo) | Fields and strategy families already in `models.py`, `fields.py`, `strategies.py` | + +A single docs page is not enough. After finding the docs, locate the config type definition in the host's source and use it to verify field names, types, and optionality. If two sources disagree, fetch a third or escalate — never guess. + +Use the questionnaire (§2) only for information that research could not resolve. + +--- + +## 2. Structured Questionnaire + +17 questions across 4 categories. + +### Category A: Host Identity & Config Location + +| ID | Question | Why It Matters | Files Affected | +|----|----------|----------------|----------------| +| A1 | What is the host's canonical name? (lowercase, hyphens, e.g. `"kiro"`) | Becomes `MCPHostType` enum value and adapter `host_name`. | `models.py`, all files | +| A2 | Where is the config file on each platform? (macOS, Linux, Windows paths) | Strategy `get_config_path()` requires platform-specific path logic. | `strategies.py` | +| A3 | What is the config file format? (JSON or TOML) | Determines strategy read/write implementation and which strategy family to inherit. | `strategies.py` | +| A4 | What is the root key for MCP servers in the config file? | Strategy `get_config_key()`. Known values: `mcpServers`, `servers`, `mcp_servers`. | `strategies.py` | +| A5 | How to detect if the host is installed on the system? | Strategy `is_host_available()`. Most hosts check for a directory's existence. | `strategies.py` | + +### Category B: Field Support + +| ID | Question | Why It Matters | Files Affected | +|----|----------|----------------|----------------| +| B1 | Which transport types does the host support? (stdio, sse, http) | Drives validation rules in `validate_filtered()`. | adapter | +| B2 | Does the host support the `type` discriminator field? (`"type": "stdio"` / `"sse"`) | Determines membership in `TYPE_SUPPORTING_HOSTS` in `fields.py`. | `fields.py` | +| B3 | What host-specific fields exist beyond the universal set? (name, type, description, required/optional for each) | Defines the field set constant in `fields.py` and new `MCPServerConfig` field declarations. | `fields.py`, `models.py` | +| B4 | Does the host use different names for standard fields? (e.g. Codex: `arguments` instead of `args`) | Determines whether a `FIELD_MAPPINGS` dict and `apply_transformations()` override are needed. | `fields.py`, adapter | +| B5 | Are there fields semantically equivalent to another host's fields? (e.g. Gemini `includeTools` = Codex `enabled_tools`) | Cross-host sync field mappings. Without mappings, sync silently drops the field. | `fields.py`, adapter | + +### Category C: Validation & Serialization + +| ID | Question | Why It Matters | Files Affected | +|----|----------|----------------|----------------| +| C1 | Can the host have multiple transports simultaneously, or exactly one? | Core validation in `validate_filtered()`. Most hosts require exactly one. | adapter | +| C2 | Are any fields mutually exclusive? (beyond transports) | Additional validation rules. | adapter | +| C3 | Are any fields conditionally required? (e.g. `oauth_enabled=true` requires `oauth_clientId`) | Additional validation rules. | adapter | +| C4 | Does serialization require structural transformation beyond field renaming? | Whether a custom `serialize()` override is needed. | adapter | +| C5 | Does the config file contain non-MCP sections that must be preserved on write? | Strategy `write_configuration()` must read-before-write and merge. | `strategies.py` | + +### Category D: Architectural Fit + +| ID | Question | Why It Matters | Files Affected | +|----|----------|----------------|----------------| +| D1 | Is this host functionally identical to an existing host? (same fields, same validation, different name only) | Variant pattern: reuse an existing adapter with a `variant` parameter instead of a new class. | adapter, `registry.py` | +| D2 | Does this host share config format and I/O logic with an existing host? | Strategy family: inherit from `ClaudeHostStrategy` or `CursorBasedHostStrategy` instead of bare `MCPHostStrategy`. | `strategies.py` | + +--- + +## 3. Escalation Tiers + +Present questions progressively. Do not ask Tier 2 or 3 unless triggered. + +### Tier 1: Blocking -- cannot proceed without answers (A1, A2, A3, A4, B1, B3) + +| ID | Summary | +|----|---------| +| A1 | Host canonical name | +| A2 | Config file path per platform | +| A3 | Config file format (JSON/TOML) | +| A4 | Root key for MCP servers | +| B1 | Supported transport types | +| B3 | Host-specific fields beyond universal set | + +### Tier 2: Complexity-triggered -- ask if Tier 1 reveals non-standard behavior + +| ID | Trigger condition | +|----|-------------------| +| B4 | Host uses different names for standard fields | +| B5 | Host has tool filtering fields that map to another host's equivalents | +| C1 | Unclear whether transports are mutually exclusive | +| C4 | Config format requires structural nesting beyond flat key-value | +| C5 | Config file has non-MCP sections | + +### Tier 3: Ambiguity-only -- ask only if reading existing adapters leaves it unclear + +| ID | Trigger condition | +|----|-------------------| +| A5 | Host detection mechanism is non-obvious | +| B2 | Unclear whether host uses `type` discriminator | +| C2 | Possible field mutual exclusion beyond transports | +| C3 | Possible conditional field requirements | +| D1 | Host looks identical to an existing one | +| D2 | Host I/O looks similar to an existing strategy family | + +--- + +## 4. Existing Host Reference Table + +| Host | Format | Root Key | macOS Path | Detection | +|------|--------|----------|------------|-----------| +| `claude-desktop` | JSON | `mcpServers` | `~/Library/Application Support/Claude/claude_desktop_config.json` | Config parent dir exists | +| `claude-code` | JSON | `mcpServers` | `~/.claude.json` | File exists | +| `vscode` | JSON | `servers` | `~/Library/Application Support/Code/User/mcp.json` | Code User dir exists | +| `cursor` | JSON | `mcpServers` | `~/.cursor/mcp.json` | `.cursor/` exists | +| `lmstudio` | JSON | `mcpServers` | `~/.lmstudio/mcp.json` | `.lmstudio/` exists | +| `gemini` | JSON | `mcpServers` | `~/.gemini/settings.json` | `.gemini/` exists | +| `kiro` | JSON | `mcpServers` | `~/.kiro/settings/mcp.json` | `.kiro/settings/` exists | +| `codex` | TOML | `mcp_servers` | `~/.codex/config.toml` | `.codex/` exists | + +### Strategy Families + +| Family Base Class | Members | Provides | +|-------------------|---------|----------| +| `ClaudeHostStrategy` | `ClaudeDesktopStrategy`, `ClaudeCodeStrategy` | Shared JSON read/write, `_preserve_claude_settings()` | +| `CursorBasedHostStrategy` | `CursorHostStrategy`, `LMStudioHostStrategy` | Shared Cursor-format JSON read/write | +| `MCPHostStrategy` (standalone) | `VSCodeHostStrategy`, `GeminiHostStrategy`, `KiroHostStrategy`, `CodexHostStrategy` | No shared logic -- each owns its I/O | + +### Type Discriminator Support + +Hosts in `TYPE_SUPPORTING_HOSTS`: `claude-desktop`, `claude-code`, `vscode`, `cursor`. + +All other hosts (`lmstudio`, `gemini`, `kiro`, `codex`) do NOT emit the `type` field. + +--- + +## 5. Host Spec YAML Output Format + +Fill every field; use `null` or `[]` for inapplicable values. + +```yaml +host: + name: "" # A1 + config_format: "json" # A3 — "json" or "toml" + config_key: "mcpServers" # A4 + +paths: # A2 + darwin: "~/path/to/config.json" + linux: "~/path/to/config.json" + windows: "~/path/to/config.json" + +detection: # A5 + method: "directory_exists" # "directory_exists" | "file_exists" + path: "~/./" + +transports: # B1 + supported: ["stdio", "sse"] + mutual_exclusion: true # C1 + +fields: # B2, B3 + type_discriminator: true # B2 — join TYPE_SUPPORTING_HOSTS? + host_specific: # B3 — list each non-universal field + - name: "field_name" + type: "Optional[str]" + description: "What this field does" + - name: "another_field" + type: "Optional[bool]" + description: "What this field does" + +field_mappings: # B4, B5 + args: "arguments" # universal name -> host name (B4) + includeTools: "enabled_tools" # cross-host equivalent (B5) + +validation: # C2, C3 + mutual_exclusions: [] # field pairs that cannot coexist + conditional_requirements: [] # {if: "field=value", then: "required_field"} + +serialization: # C4 + structural_transform: false # true if custom serialize() needed + +config_file: # C5 + preserved_sections: [] # non-MCP keys to preserve on write + +architecture: # D1, D2 + variant_of: null # existing adapter to reuse, or null + strategy_family: null # base class to inherit, or null +``` + +Validate the completed spec against these rules before proceeding: +- `host.name` matches lowercase-with-hyphens pattern +- `paths` has at least `darwin` or `linux` defined +- `transports.supported` is non-empty +- If `field_mappings` is non-empty, verify each source field exists in another host's field set +- If `architecture.variant_of` is set, confirm the named adapter exists in the registry +- If `architecture.strategy_family` is set, confirm the named base class exists in `strategies.py` + +--- + +## 6. Output Files + +Write both files to `__reports__//` before proceeding to Step 2. Do not summarize findings only in chat. + +**`00-parameter_analysis_v0.md`** — field-level discovery: what the host config actually looks like, field names and types per transport, serialization requirements, and the resulting `HOSTNAME_FIELDS` contract. + +**`01-architecture_analysis_v0.md`** — integration analysis: current state inventory of the Hatch codebase, how the new host fits the adapter/strategy pattern, NO-GO assessment with any implementation-critical invariants, component contracts, and risk register. diff --git a/.claude/skills/adding-mcp-hosts/references/strategy-contract.md b/.claude/skills/adding-mcp-hosts/references/strategy-contract.md new file mode 100644 index 0000000..c932e9b --- /dev/null +++ b/.claude/skills/adding-mcp-hosts/references/strategy-contract.md @@ -0,0 +1,226 @@ +# Strategy Contract Reference + +## 1. MCPHostStrategy Interface + +Implement all methods from the abstract base class in `hatch/mcp_host_config/host_management.py`: + +```python +class MCPHostStrategy: + def get_config_path(self) -> Optional[Path]: + """Get configuration file path for this host.""" + raise NotImplementedError + + def get_config_key(self) -> str: + """Get the root configuration key for MCP servers.""" + return "mcpServers" # Default for most platforms + + def read_configuration(self) -> HostConfiguration: + """Read and parse host configuration.""" + raise NotImplementedError + + def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool: + """Write configuration to host file.""" + raise NotImplementedError + + def is_host_available(self) -> bool: + """Check if host is available on system.""" + raise NotImplementedError + + def validate_server_config(self, server_config: MCPServerConfig) -> bool: + """Validate server configuration for this host.""" + raise NotImplementedError +``` + +Use `platform.system()` inside `get_config_path()` to dispatch per OS (`"Darwin"`, `"Windows"`, `"Linux"`). Return `None` for unsupported platforms. + +Override `get_config_key()` only when the host uses a non-default root key (e.g., `"servers"` for VS Code, `"mcp_servers"` for Codex). + +Every strategy must also define `get_adapter_host_name() -> str` to return the adapter identifier used by `get_adapter()` for serialization. + +## 2. @register_host_strategy Decorator + +Register each concrete strategy with the host registry in `strategies.py`: + +```python +@register_host_strategy(MCPHostType.YOUR_HOST) +class YourHostStrategy(MCPHostStrategy): + ... +``` + +The decorator calls `MCPHostRegistry.register(host_type)`, which maps the `MCPHostType` enum value to the strategy class. `MCPHostConfigurationManager` discovers strategies through this registry at runtime -- no manual wiring required. + +Add the new host to the `MCPHostType` enum in `hatch/mcp_host_config/models.py` before using it: + +```python +class MCPHostType(str, Enum): + YOUR_HOST = "your-host" +``` + +## 3. Strategy Families + +Choose a base class based on how the host's config file behaves: + +### ClaudeHostStrategy + +Inherit when the host shares Claude's JSON format with settings preservation. + +**Members:** `ClaudeDesktopStrategy`, `ClaudeCodeStrategy`. + +**Provides for free:** +- `get_config_key()` returns `"mcpServers"` +- `validate_server_config()` accepting command or URL transports +- `_preserve_claude_settings()` -- copies all non-MCP keys from existing config before writing +- `read_configuration()` and `write_configuration()` with JSON I/O and atomic writes + +**Choose when:** the host stores MCP servers under `"mcpServers"` in a JSON file that also contains non-MCP settings (theme, auto_update, etc.) that must survive writes. + +### CursorBasedHostStrategy + +Inherit when the host shares Cursor's simple JSON-only format. + +**Members:** `CursorHostStrategy`, `LMStudioHostStrategy`. + +**Provides for free:** +- `get_config_key()` returns `"mcpServers"` +- `validate_server_config()` accepting command or URL transports +- `read_configuration()` and `write_configuration()` with JSON I/O, atomic writes, and existing-config preservation + +**Choose when:** the host uses a dedicated `mcp.json` file (or similar) where the entire file is MCP config in simple JSON format, keyed by `"mcpServers"`. + +### MCPHostStrategy (standalone) + +Inherit directly when the host has unique I/O needs that neither family covers. + +**Members:** `VSCodeHostStrategy`, `GeminiHostStrategy`, `KiroHostStrategy`, `CodexHostStrategy`. + +**Provides for free:** only the default `get_config_key()` returning `"mcpServers"`. + +**Choose when:** +- The config key differs (VS Code uses `"servers"`, Codex uses `"mcp_servers"`) +- The file format is not JSON (Codex uses TOML) +- The host needs custom atomic write logic (Kiro uses `AtomicFileOperations`) +- The host needs write verification (Gemini reads back JSON after writing) + +## 4. Platform Path Patterns + +### Simple home-relative + +Flat dotfile directory under `$HOME`. No platform dispatch needed. + +```python +# CursorHostStrategy +def get_config_path(self) -> Optional[Path]: + return Path.home() / ".cursor" / "mcp.json" + +# LMStudioHostStrategy +def get_config_path(self) -> Optional[Path]: + return Path.home() / ".lmstudio" / "mcp.json" + +# GeminiHostStrategy +def get_config_path(self) -> Optional[Path]: + return Path.home() / ".gemini" / "settings.json" + +# CodexHostStrategy +def get_config_path(self) -> Optional[Path]: + return Path.home() / ".codex" / "config.toml" + +# KiroHostStrategy +def get_config_path(self) -> Optional[Path]: + return Path.home() / ".kiro" / "settings" / "mcp.json" + +# ClaudeCodeStrategy +def get_config_path(self) -> Optional[Path]: + return Path.home() / ".claude.json" +``` + +### macOS Application Support + cross-platform dispatch + +Use `platform.system()` to select OS-appropriate paths. + +```python +# ClaudeDesktopStrategy +def get_config_path(self) -> Optional[Path]: + system = platform.system() + if system == "Darwin": + return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json" + elif system == "Windows": + return Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json" + elif system == "Linux": + return Path.home() / ".config" / "Claude" / "claude_desktop_config.json" + return None + +# VSCodeHostStrategy +def get_config_path(self) -> Optional[Path]: + system = platform.system() + if system == "Windows": + return Path.home() / "AppData" / "Roaming" / "Code" / "User" / "mcp.json" + elif system == "Darwin": + return Path.home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json" + elif system == "Linux": + return Path.home() / ".config" / "Code" / "User" / "mcp.json" + return None +``` + +## 5. Config Preservation + +Every `write_configuration()` must follow the read-before-write pattern when the config file contains non-MCP sections. The merge flow: + +1. Read the existing file into a dict +2. Update only the MCP servers section (keyed by `get_config_key()`) +3. Write the full dict back atomically (write to `.tmp`, then `replace()`) + +### Claude family -- _preserve_claude_settings + +Preserves keys like theme and auto_update alongside `mcpServers`: + +```python +# ClaudeHostStrategy._preserve_claude_settings +def _preserve_claude_settings(self, existing_config: Dict, new_servers: Dict) -> Dict: + preserved_config = existing_config.copy() + preserved_config[self.get_config_key()] = new_servers + return preserved_config +``` + +### Gemini -- preserve other JSON keys + +Reads existing config, sets `mcpServers`, writes back. Adds a verification step: + +```python +# GeminiHostStrategy.write_configuration (excerpt) +existing_config = {} +if config_path.exists(): + with open(config_path, "r") as f: + existing_config = json.load(f) + +existing_config[self.get_config_key()] = servers_dict + +# Write then verify +with open(temp_path, "w") as f: + json.dump(existing_config, f, indent=2, ensure_ascii=False) +with open(temp_path, "r") as f: + json.load(f) # Verify valid JSON +temp_path.replace(config_path) +``` + +### Codex -- TOML with [features] preservation + +Reads existing TOML, preserves the `[features]` section and all other top-level keys: + +```python +# CodexHostStrategy.write_configuration (excerpt) +existing_data = {} +if config_path.exists(): + with open(config_path, "rb") as f: + existing_data = tomllib.load(f) + +if "features" in existing_data: + self._preserved_features = existing_data["features"] + +final_data = {} +if self._preserved_features: + final_data["features"] = self._preserved_features +final_data[self.get_config_key()] = servers_data +for key, value in existing_data.items(): + if key not in ("features", self.get_config_key()): + final_data[key] = value +``` diff --git a/.claude/skills/adding-mcp-hosts/references/testing-fixtures.md b/.claude/skills/adding-mcp-hosts/references/testing-fixtures.md new file mode 100644 index 0000000..b83e5a3 --- /dev/null +++ b/.claude/skills/adding-mcp-hosts/references/testing-fixtures.md @@ -0,0 +1,121 @@ +# Testing Fixtures Reference + +Register test fixtures for a new MCP host so that all data-driven tests auto-generate. + +## 1. canonical_configs.json entry + +Add one entry to `tests/test_data/mcp_adapters/canonical_configs.json`. + +Schema rules: +- Key is the host name string (e.g., `"newhost"`). +- Use host-native field names (post-mapping). If the host has `FIELD_MAPPINGS` that rename `args` to `arguments`, write `"arguments"` in the fixture. +- Set `null` for unsupported transport fields so the fixture documents their absence. +- Include at least one transport field (`command`, `url`, or `httpUrl`) with a non-null value. + +Minimal example (modeled on the `lmstudio` entry, which uses `CLAUDE_FIELDS`): + +```json +"newhost": { + "command": "python", + "args": ["-m", "mcp_server"], + "env": {"API_KEY": "test_key"}, + "url": null, + "headers": null, + "type": "stdio" +} +``` + +For hosts with extra fields, add them alongside the universals (see `gemini` or `codex` entries for examples with `httpUrl`, `timeout`, `includeTools`, `cwd`, etc.). + +## 2. host_registry.py entries + +Make three additions in `tests/test_data/mcp_adapters/host_registry.py`. + +**A. `FIELD_SETS` dict** -- map host name string to the `fields.py` constant: + +```python +FIELD_SETS: Dict[str, FrozenSet[str]] = { + # ... existing entries ... + "newhost": NEWHOST_FIELDS, +} +``` + +Import `NEWHOST_FIELDS` from `hatch.mcp_host_config.fields` at the top of the file. + +**B. `adapter_map` in `HostSpec.get_adapter()`** -- map host name to adapter factory: + +```python +adapter_map = { + # ... existing entries ... + "newhost": NewHostAdapter, +} +``` + +Import `NewHostAdapter` from `hatch.mcp_host_config.adapters.newhost` at the top of the file. + +**C. Reverse mappings (conditional)** -- only required if the host defines `FIELD_MAPPINGS` in `fields.py`. Add a reverse dict and wire it into `HostSpec.load_config()`. Follow the Codex pattern: + +```python +# At module level +NEWHOST_REVERSE_MAPPINGS: Dict[str, str] = {v: k for k, v in NEWHOST_FIELD_MAPPINGS.items()} + +# In HostRegistry.__init__, inside the loop +if host_name == "newhost": + mappings = dict(NEWHOST_FIELD_MAPPINGS) + +# In HostSpec.load_config(), extend the reverse lookup +universal_key = CODEX_REVERSE_MAPPINGS.get(key, key) +universal_key = NEWHOST_REVERSE_MAPPINGS.get(universal_key, universal_key) +``` + +Skip this step entirely if the new host uses standard field names with no mappings. + +## 3. What auto-generates + +Adding one host (going from 8 to 9 hosts) produces these new test cases without writing any test code: + +| Test file | Generator | Current (8 hosts) | New cases added | +|---|---|---|---| +| `test_host_configuration.py` | `ALL_HOSTS` parametrize | 8 | +1 (serialization roundtrip) | +| `test_cross_host_sync.py` | `generate_sync_test_cases` | 64 (8x8) | +17 (9x9 - 8x8 = 17 new pairs) | +| `test_validation_bugs.py` (transport) | `generate_validation_test_cases` | 8 | +1 (transport mutual exclusion) | +| `test_validation_bugs.py` (tool lists) | `generate_validation_test_cases` | 2 (gemini, codex) | +1 if host has tool lists, else +0 | +| `test_field_filtering_v2.py` | `generate_unsupported_field_test_cases` | 211 | +N (one per unsupported field for the new host) | + +**Minimum new test cases**: 1 + 17 + 1 + 0 + N = **19 + N** (where N = total_possible_fields - host_supported_fields). With the current 36-field union, a host supporting 6 fields adds 30 filtering tests, totaling **49** new test cases. + +## 4. Verification commands + +Run from the repository root. + +Full MCP test suite: +``` +python -m pytest tests/integration/mcp/ tests/unit/mcp/ tests/regression/mcp/ -v +``` + +Quick smoke (host configuration roundtrip only): +``` +python -m pytest tests/integration/mcp/test_host_configuration.py -v +``` + +Protocol compliance (adapter contract checks): +``` +python -m pytest tests/unit/mcp/test_adapter_protocol.py -v +``` + +Cross-host sync (all pair combinations): +``` +python -m pytest tests/integration/mcp/test_cross_host_sync.py -v +``` + +Field filtering regression: +``` +python -m pytest tests/regression/mcp/test_field_filtering_v2.py -v +``` + +## 5. Expected results + +- The new host name appears in parametrized test IDs (e.g., `test_configure_host[newhost]`, `sync_claude-desktop_to_newhost`, `newhost_filters_envFile`). +- All tests pass. Zero failures, zero errors. +- Existing test IDs remain unchanged. No regressions in prior host tests. +- Total test count increases by the amounts in section 3. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f6d613..8acdc4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,273 @@ +## 0.8.0 (2026-02-20) + +* Merge pull request #44 from LittleCoinCoin/dev ([1157922](https://github.com/CrackingShells/Hatch/commit/1157922)), closes [#44](https://github.com/CrackingShells/Hatch/issues/44) +* Merge pull request #45 from LittleCoinCoin/dev ([0ed9010](https://github.com/CrackingShells/Hatch/commit/0ed9010)), closes [#45](https://github.com/CrackingShells/Hatch/issues/45) +* Merge pull request #46 from CrackingShells/dev ([514f2c7](https://github.com/CrackingShells/Hatch/commit/514f2c7)), closes [#46](https://github.com/CrackingShells/Hatch/issues/46) +* chore: update entry point to hatch.cli module ([cf81671](https://github.com/CrackingShells/Hatch/commit/cf81671)) +* chore: update submodule `cracking-shells-playbook` ([222b357](https://github.com/CrackingShells/Hatch/commit/222b357)) +* chore(deps): add pytest to dev dependencies ([2761afe](https://github.com/CrackingShells/Hatch/commit/2761afe)) +* chore(dev-infra): add code quality tools to dev dependencies ([f76c5c1](https://github.com/CrackingShells/Hatch/commit/f76c5c1)) +* chore(dev-infra): add pre-commit configuration ([67da239](https://github.com/CrackingShells/Hatch/commit/67da239)) +* chore(dev-infra): apply black formatting to entire codebase ([2daa89d](https://github.com/CrackingShells/Hatch/commit/2daa89d)) +* chore(dev-infra): apply ruff linting fixes to codebase ([6681ee6](https://github.com/CrackingShells/Hatch/commit/6681ee6)) +* chore(dev-infra): install pre-commit hooks and document initial state ([eb81ea4](https://github.com/CrackingShells/Hatch/commit/eb81ea4)) +* chore(dev-infra): verify pre-commit hooks pass on entire codebase ([ed90350](https://github.com/CrackingShells/Hatch/commit/ed90350)) +* chore(docs): remove deprecated CLI api doc ([12a22c0](https://github.com/CrackingShells/Hatch/commit/12a22c0)) +* chore(docs): remove deprecated MCP documentation files ([5ca09a3](https://github.com/CrackingShells/Hatch/commit/5ca09a3)) +* chore(release): 0.8.0-dev.1 ([f787c93](https://github.com/CrackingShells/Hatch/commit/f787c93)) +* chore(release): 0.8.0-dev.2 ([2d30523](https://github.com/CrackingShells/Hatch/commit/2d30523)) +* chore(tests): remove deprecated MCP test files ([29a5ec5](https://github.com/CrackingShells/Hatch/commit/29a5ec5)) +* fix(backup): support different config filenames in backup listing ([06eb53a](https://github.com/CrackingShells/Hatch/commit/06eb53a)), closes [#2](https://github.com/CrackingShells/Hatch/issues/2) +* fix(ci): pre-release installation instructions ([0206dc0](https://github.com/CrackingShells/Hatch/commit/0206dc0)) +* fix(cli-version): use correct package name for version lookup ([76c3364](https://github.com/CrackingShells/Hatch/commit/76c3364)) +* fix(cli): remove obsolete handle_mcp_show import ([388ca01](https://github.com/CrackingShells/Hatch/commit/388ca01)) +* fix(docs): add missing return type annotations for mkdocs build ([da78682](https://github.com/CrackingShells/Hatch/commit/da78682)) +* fix(instructions): purge stale Phase terminology ([dba119a](https://github.com/CrackingShells/Hatch/commit/dba119a)) +* fix(mcp-adapters): add missing strategies import ([533a66d](https://github.com/CrackingShells/Hatch/commit/533a66d)) +* fix(mcp-adapters): add transport mutual exclusion to GeminiAdapter ([319d067](https://github.com/CrackingShells/Hatch/commit/319d067)) +* fix(mcp-adapters): allow enabled_tools/disabled_tools coexistence ([ea6471c](https://github.com/CrackingShells/Hatch/commit/ea6471c)) +* fix(mcp-adapters): allow includeTools/excludeTools coexistence ([d8f8a56](https://github.com/CrackingShells/Hatch/commit/d8f8a56)) +* fix(mcp-adapters): remove type field rejection from CodexAdapter ([0627352](https://github.com/CrackingShells/Hatch/commit/0627352)) +* fix(mcp-adapters): remove type field rejection from GeminiAdapter ([2d8e0a3](https://github.com/CrackingShells/Hatch/commit/2d8e0a3)) +* fix(ruff): resolve F821 errors and consolidate imports ([0be9fc8](https://github.com/CrackingShells/Hatch/commit/0be9fc8)), closes [hi#priority](https://github.com/hi/issues/priority) +* docs: fix broken link in MCP host configuration architecture ([e9f89f1](https://github.com/CrackingShells/Hatch/commit/e9f89f1)) +* docs(api): restructure CLI API documentation to modular architecture ([318d212](https://github.com/CrackingShells/Hatch/commit/318d212)) +* docs(cli-ref): mark package list as deprecated and update filters ([06f5b75](https://github.com/CrackingShells/Hatch/commit/06f5b75)) +* docs(cli-ref): update environment commands section ([749d992](https://github.com/CrackingShells/Hatch/commit/749d992)) +* docs(cli-ref): update MCP commands section with new list/show commands ([1c812fd](https://github.com/CrackingShells/Hatch/commit/1c812fd)) +* docs(cli-ref): update mcp sync command documentation ([17ae770](https://github.com/CrackingShells/Hatch/commit/17ae770)) +* docs(cli): add module docstrings for refactored CLI ([8d7de20](https://github.com/CrackingShells/Hatch/commit/8d7de20)) +* docs(cli): update documentation for handler-based architecture ([f95c5d0](https://github.com/CrackingShells/Hatch/commit/f95c5d0)) +* docs(devs): add CLI architecture and implementation guide ([a3152e1](https://github.com/CrackingShells/Hatch/commit/a3152e1)) +* docs(guide): add quick reference for viewing commands ([5bf5d01](https://github.com/CrackingShells/Hatch/commit/5bf5d01)) +* docs(guide): add viewing host configurations section ([6c381d1](https://github.com/CrackingShells/Hatch/commit/6c381d1)) +* docs(mcp-adapters): update architecture for new pattern ([693665c](https://github.com/CrackingShells/Hatch/commit/693665c)) +* docs(mcp-host-config): deprecate legacy architecture doc ([d8618a5](https://github.com/CrackingShells/Hatch/commit/d8618a5)) +* docs(mcp-host-config): deprecate legacy extension guide ([f172a51](https://github.com/CrackingShells/Hatch/commit/f172a51)) +* docs(mcp-host-config): write new architecture documentation ([ff05ad5](https://github.com/CrackingShells/Hatch/commit/ff05ad5)) +* docs(mcp-host-config): write new extension guide ([7821062](https://github.com/CrackingShells/Hatch/commit/7821062)) +* docs(mcp-reporting): document metadata field exclusion behavior ([5ccb7f9](https://github.com/CrackingShells/Hatch/commit/5ccb7f9)) +* docs(mcp): update error message examples ([5988b3a](https://github.com/CrackingShells/Hatch/commit/5988b3a)) +* docs(testing): add tests/README.md with testing strategy ([08162ce](https://github.com/CrackingShells/Hatch/commit/08162ce)) +* docs(testing): update README - all test issues resolved ([5c60ef2](https://github.com/CrackingShells/Hatch/commit/5c60ef2)) +* docs(tutorial): fix command syntax in environment sync tutorial ([b2f40bf](https://github.com/CrackingShells/Hatch/commit/b2f40bf)) +* docs(tutorial): fix verification commands in checkpoint tutorial ([59b2485](https://github.com/CrackingShells/Hatch/commit/59b2485)) +* docs(tutorial): update env list output in create environment tutorial ([443607c](https://github.com/CrackingShells/Hatch/commit/443607c)) +* docs(tutorial): update package installation tutorial outputs ([588bab3](https://github.com/CrackingShells/Hatch/commit/588bab3)) +* docs(tutorials): fix command syntax in 04-mcp-host-configuration ([2ac1058](https://github.com/CrackingShells/Hatch/commit/2ac1058)) +* docs(tutorials): fix outdated env list output format in 02-environments ([d38ae24](https://github.com/CrackingShells/Hatch/commit/d38ae24)) +* docs(tutorials): fix validation output in 03-author-package ([776d40f](https://github.com/CrackingShells/Hatch/commit/776d40f)) +* test(cli): add ConversionReport fixtures for reporter tests ([eeccff6](https://github.com/CrackingShells/Hatch/commit/eeccff6)) +* test(cli): add failing integration test for MCP handler ([acf7c94](https://github.com/CrackingShells/Hatch/commit/acf7c94)) +* test(cli): add failing test for host-centric mcp list servers ([0fcb8fd](https://github.com/CrackingShells/Hatch/commit/0fcb8fd)) +* test(cli): add failing tests for ConversionReport integration ([8e6efc0](https://github.com/CrackingShells/Hatch/commit/8e6efc0)) +* test(cli): add failing tests for env list hosts ([454b0e4](https://github.com/CrackingShells/Hatch/commit/454b0e4)) +* test(cli): add failing tests for env list servers ([7250387](https://github.com/CrackingShells/Hatch/commit/7250387)) +* test(cli): add failing tests for host-centric mcp list hosts ([3ec0617](https://github.com/CrackingShells/Hatch/commit/3ec0617)) +* test(cli): add failing tests for mcp show hosts ([8c8f3e9](https://github.com/CrackingShells/Hatch/commit/8c8f3e9)) +* test(cli): add failing tests for mcp show servers ([fac85fe](https://github.com/CrackingShells/Hatch/commit/fac85fe)) +* test(cli): add failing tests for TableFormatter ([90f3953](https://github.com/CrackingShells/Hatch/commit/90f3953)) +* test(cli): add test directory structure for CLI reporter ([7044b47](https://github.com/CrackingShells/Hatch/commit/7044b47)) +* test(cli): add test utilities for handler testing ([55322c7](https://github.com/CrackingShells/Hatch/commit/55322c7)) +* test(cli): add tests for Color enum and color enable/disable logic ([f854324](https://github.com/CrackingShells/Hatch/commit/f854324)) +* test(cli): add tests for Consequence dataclass and ResultReporter ([127575d](https://github.com/CrackingShells/Hatch/commit/127575d)) +* test(cli): add tests for ConsequenceType enum ([a3f0204](https://github.com/CrackingShells/Hatch/commit/a3f0204)) +* test(cli): add tests for error reporting methods ([2561532](https://github.com/CrackingShells/Hatch/commit/2561532)) +* test(cli): add tests for HatchArgumentParser ([8b192e5](https://github.com/CrackingShells/Hatch/commit/8b192e5)) +* test(cli): add tests for ValidationError and utilities ([a2a5c29](https://github.com/CrackingShells/Hatch/commit/a2a5c29)) +* test(cli): add true color detection tests ([79f6faa](https://github.com/CrackingShells/Hatch/commit/79f6faa)) +* test(cli): update backup tests for cli_mcp module ([8174bef](https://github.com/CrackingShells/Hatch/commit/8174bef)) +* test(cli): update color tests for HCL palette ([a19780c](https://github.com/CrackingShells/Hatch/commit/a19780c)) +* test(cli): update direct_management tests for cli_mcp module ([16f8520](https://github.com/CrackingShells/Hatch/commit/16f8520)) +* test(cli): update discovery tests for cli_mcp module ([de75cf0](https://github.com/CrackingShells/Hatch/commit/de75cf0)) +* test(cli): update for new cli architecture ([64cf74e](https://github.com/CrackingShells/Hatch/commit/64cf74e)) +* test(cli): update host config integration tests for cli_mcp module ([ea5c6b6](https://github.com/CrackingShells/Hatch/commit/ea5c6b6)) +* test(cli): update host_specific_args tests for cli_mcp module ([8f477f6](https://github.com/CrackingShells/Hatch/commit/8f477f6)) +* test(cli): update list tests for cli_mcp module ([e21ecc0](https://github.com/CrackingShells/Hatch/commit/e21ecc0)) +* test(cli): update mcp list servers tests for --pattern removal ([9bb5fe5](https://github.com/CrackingShells/Hatch/commit/9bb5fe5)) +* test(cli): update partial_updates tests for cli_mcp module ([4484e67](https://github.com/CrackingShells/Hatch/commit/4484e67)) +* test(cli): update remaining MCP tests for cli_mcp module ([a655775](https://github.com/CrackingShells/Hatch/commit/a655775)) +* test(cli): update sync_functionality tests for cli_mcp module ([eeb2d6d](https://github.com/CrackingShells/Hatch/commit/eeb2d6d)) +* test(cli): update tests for cli_utils module ([7d72f76](https://github.com/CrackingShells/Hatch/commit/7d72f76)) +* test(cli): update tests for mcp show removal ([a0e730b](https://github.com/CrackingShells/Hatch/commit/a0e730b)) +* test(deprecate): rename 28 legacy MCP tests to .bak for rebuild ([e7f9c50](https://github.com/CrackingShells/Hatch/commit/e7f9c50)) +* test(docker-loader): mock docker and online package loader tests ([df5533e](https://github.com/CrackingShells/Hatch/commit/df5533e)) +* test(env-manager): mock conda/mamba detection tests ([ce82350](https://github.com/CrackingShells/Hatch/commit/ce82350)) +* test(env-manager): mock environment creation tests ([8bf3289](https://github.com/CrackingShells/Hatch/commit/8bf3289)) +* test(env-manager): mock remaining integration tests ([5a4d215](https://github.com/CrackingShells/Hatch/commit/5a4d215)) +* test(env-manip): mock advanced package dependency tests ([1878751](https://github.com/CrackingShells/Hatch/commit/1878751)) +* test(env-manip): mock advanced package dependency tests ([9a945ad](https://github.com/CrackingShells/Hatch/commit/9a945ad)) +* test(env-manip): mock basic environment operations ([0b4ed74](https://github.com/CrackingShells/Hatch/commit/0b4ed74)) +* test(env-manip): mock basic environment operations ([675a67d](https://github.com/CrackingShells/Hatch/commit/675a67d)) +* test(env-manip): mock package addition tests ([0f99f4c](https://github.com/CrackingShells/Hatch/commit/0f99f4c)) +* test(env-manip): mock package addition tests ([04cb79f](https://github.com/CrackingShells/Hatch/commit/04cb79f)) +* test(env-manip): mock remaining 3 slow tests ([df7517c](https://github.com/CrackingShells/Hatch/commit/df7517c)) +* test(env-manip): mock system, docker, and MCP server tests ([63084c4](https://github.com/CrackingShells/Hatch/commit/63084c4)) +* test(env-manip): mock system, docker, and MCP server tests ([9487ef8](https://github.com/CrackingShells/Hatch/commit/9487ef8)) +* test(env-manip): remove remaining @slow_test decorators ([0403a7d](https://github.com/CrackingShells/Hatch/commit/0403a7d)) +* test(installer): add shared venv fixture for integration tests ([095f6ce](https://github.com/CrackingShells/Hatch/commit/095f6ce)) +* test(installer): mock pip installation tests (batch 1) ([45bdae0](https://github.com/CrackingShells/Hatch/commit/45bdae0)) +* test(installer): mock pip installation tests (batch 2) ([1650442](https://github.com/CrackingShells/Hatch/commit/1650442)) +* test(installer): refactor integration test to use shared venv ([bd979be](https://github.com/CrackingShells/Hatch/commit/bd979be)) +* test(mcp-adapters): add canonical configs fixture ([46f54a6](https://github.com/CrackingShells/Hatch/commit/46f54a6)) +* test(mcp-adapters): add cross-host sync tests (64 pairs) ([c77f448](https://github.com/CrackingShells/Hatch/commit/c77f448)) +* test(mcp-adapters): add field filtering regression tests ([bc3e631](https://github.com/CrackingShells/Hatch/commit/bc3e631)) +* test(mcp-adapters): add host configuration tests (8 hosts) ([b3e640e](https://github.com/CrackingShells/Hatch/commit/b3e640e)) +* test(mcp-adapters): add validation bug regression tests ([8eb6f7a](https://github.com/CrackingShells/Hatch/commit/8eb6f7a)) +* test(mcp-adapters): deprecate old tests for data-driven ([8177520](https://github.com/CrackingShells/Hatch/commit/8177520)) +* test(mcp-adapters): fix registry test for new abstract method ([32aa3cb](https://github.com/CrackingShells/Hatch/commit/32aa3cb)) +* test(mcp-adapters): implement HostRegistry with fields.py ([127c1f7](https://github.com/CrackingShells/Hatch/commit/127c1f7)) +* test(mcp-adapters): implement property-based assertions ([4ac17ef](https://github.com/CrackingShells/Hatch/commit/4ac17ef)) +* test(mcp-host-config): add adapter registry unit tests ([bc8f455](https://github.com/CrackingShells/Hatch/commit/bc8f455)) +* test(mcp-host-config): add integration tests for adapter serialization ([6910120](https://github.com/CrackingShells/Hatch/commit/6910120)) +* test(mcp-host-config): add regression tests for field filtering ([d6ce817](https://github.com/CrackingShells/Hatch/commit/d6ce817)) +* test(mcp-host-config): add unit tests ([c1a0fa4](https://github.com/CrackingShells/Hatch/commit/c1a0fa4)) +* test(mcp-host-config): create three-tier test directory structure ([d78681b](https://github.com/CrackingShells/Hatch/commit/d78681b)) +* test(mcp-host-config): update integration tests for adapter architecture ([acd7871](https://github.com/CrackingShells/Hatch/commit/acd7871)) +* test(mcp-sync): use canonical fixture data in detailed flag tests ([c2f35e4](https://github.com/CrackingShells/Hatch/commit/c2f35e4)) +* test(non-tty): remove slow_test from integration tests ([772de01](https://github.com/CrackingShells/Hatch/commit/772de01)) +* test(system-installer): mock system installer tests ([23de568](https://github.com/CrackingShells/Hatch/commit/23de568)) +* test(validation): add pytest pythonpath config ([9924374](https://github.com/CrackingShells/Hatch/commit/9924374)) +* feat(adapters): create AdapterRegistry for host-adapter mapping ([a8e3dfb](https://github.com/CrackingShells/Hatch/commit/a8e3dfb)) +* feat(adapters): create BaseAdapter abstract class ([4d9833c](https://github.com/CrackingShells/Hatch/commit/4d9833c)) +* feat(adapters): create host-specific adapters ([7b725c8](https://github.com/CrackingShells/Hatch/commit/7b725c8)) +* feat(cli): add --dry-run to env and package commands ([4a0f3e5](https://github.com/CrackingShells/Hatch/commit/4a0f3e5)) +* feat(cli): add --dry-run to env use, package add, create commands ([79da44c](https://github.com/CrackingShells/Hatch/commit/79da44c)) +* feat(cli): add --host and --pattern flags to mcp list servers ([29f86aa](https://github.com/CrackingShells/Hatch/commit/29f86aa)) +* feat(cli): add --json flag to list commands ([73f62ed](https://github.com/CrackingShells/Hatch/commit/73f62ed)) +* feat(cli): add --pattern filter to env list ([6deff84](https://github.com/CrackingShells/Hatch/commit/6deff84)) +* feat(cli): add Color, ConsequenceType, Consequence, ResultReporter ([10cdb71](https://github.com/CrackingShells/Hatch/commit/10cdb71)) +* feat(cli): add confirmation prompt to env remove ([b1156e7](https://github.com/CrackingShells/Hatch/commit/b1156e7)) +* feat(cli): add confirmation prompt to package remove ([38d9051](https://github.com/CrackingShells/Hatch/commit/38d9051)) +* feat(cli): add ConversionReport to ResultReporter bridge ([4ea999e](https://github.com/CrackingShells/Hatch/commit/4ea999e)) +* feat(cli): add format_info utility ([b1f33d4](https://github.com/CrackingShells/Hatch/commit/b1f33d4)) +* feat(cli): add format_validation_error utility ([f28b841](https://github.com/CrackingShells/Hatch/commit/f28b841)) +* feat(cli): add format_warning utility ([28ec610](https://github.com/CrackingShells/Hatch/commit/28ec610)) +* feat(cli): add hatch env show command ([2bc96bc](https://github.com/CrackingShells/Hatch/commit/2bc96bc)) +* feat(cli): add hatch mcp show command ([9ab53bc](https://github.com/CrackingShells/Hatch/commit/9ab53bc)) +* feat(cli): add HatchArgumentParser with formatted errors ([1fb7006](https://github.com/CrackingShells/Hatch/commit/1fb7006)) +* feat(cli): add highlight utility for entity names ([c25631a](https://github.com/CrackingShells/Hatch/commit/c25631a)) +* feat(cli): add parser for env list hosts command ([a218dea](https://github.com/CrackingShells/Hatch/commit/a218dea)) +* feat(cli): add parser for env list servers command ([851c866](https://github.com/CrackingShells/Hatch/commit/851c866)) +* feat(cli): add parser for mcp show hosts command ([f7abe61](https://github.com/CrackingShells/Hatch/commit/f7abe61)) +* feat(cli): add report_error method to ResultReporter ([e0f89e1](https://github.com/CrackingShells/Hatch/commit/e0f89e1)) +* feat(cli): add report_partial_success method to ResultReporter ([1ce4fd9](https://github.com/CrackingShells/Hatch/commit/1ce4fd9)) +* feat(cli): add TableFormatter for aligned table output ([658f48a](https://github.com/CrackingShells/Hatch/commit/658f48a)) +* feat(cli): add true color terminal detection ([aa76bfc](https://github.com/CrackingShells/Hatch/commit/aa76bfc)) +* feat(cli): add unicode terminal detection ([91d7c30](https://github.com/CrackingShells/Hatch/commit/91d7c30)) +* feat(cli): add ValidationError exception class ([af63b46](https://github.com/CrackingShells/Hatch/commit/af63b46)) +* feat(cli): display server list in mcp sync pre-prompt ([96d7f56](https://github.com/CrackingShells/Hatch/commit/96d7f56)) +* feat(cli): implement env list hosts command ([bebe6ab](https://github.com/CrackingShells/Hatch/commit/bebe6ab)) +* feat(cli): implement env list servers command ([0c7a744](https://github.com/CrackingShells/Hatch/commit/0c7a744)) +* feat(cli): implement HCL color palette with true color support ([d70b4f2](https://github.com/CrackingShells/Hatch/commit/d70b4f2)) +* feat(cli): implement mcp show hosts command ([2c716bb](https://github.com/CrackingShells/Hatch/commit/2c716bb)) +* feat(cli): implement mcp show servers command ([e6df7b4](https://github.com/CrackingShells/Hatch/commit/e6df7b4)) +* feat(cli): update mcp list hosts JSON output ([a6f5994](https://github.com/CrackingShells/Hatch/commit/a6f5994)) +* feat(cli): update mcp list hosts parser with --server flag ([c298d52](https://github.com/CrackingShells/Hatch/commit/c298d52)) +* feat(mcp-adapters): implement field transformations in CodexAdapter ([59cc931](https://github.com/CrackingShells/Hatch/commit/59cc931)) +* feat(mcp-host-config): add field support constants ([1e81a24](https://github.com/CrackingShells/Hatch/commit/1e81a24)) +* feat(mcp-host-config): add transport detection to MCPServerConfig ([c4eabd2](https://github.com/CrackingShells/Hatch/commit/c4eabd2)) +* feat(mcp-host-config): implement LMStudioAdapter ([0662b14](https://github.com/CrackingShells/Hatch/commit/0662b14)) +* feat(mcp-reporting): metadata fields exclusion from cli reports ([41db3da](https://github.com/CrackingShells/Hatch/commit/41db3da)) +* feat(mcp-sync): add --detailed flag for field-level sync output ([dea1541](https://github.com/CrackingShells/Hatch/commit/dea1541)) +* feat(mcp): add preview_sync method for server name resolution ([52bdc10](https://github.com/CrackingShells/Hatch/commit/52bdc10)) +* refactor(cli): add deprecation warning to cli_hatch shim ([f9adf0a](https://github.com/CrackingShells/Hatch/commit/f9adf0a)) +* refactor(cli): create cli package structure ([bc80e29](https://github.com/CrackingShells/Hatch/commit/bc80e29)) +* refactor(cli): deprecate `mcp discover servers` and `package list` ([9ce5be0](https://github.com/CrackingShells/Hatch/commit/9ce5be0)) +* refactor(cli): extract argument parsing and implement clean routing ([efeae24](https://github.com/CrackingShells/Hatch/commit/efeae24)) +* refactor(cli): extract environment handlers to cli_env ([d00959f](https://github.com/CrackingShells/Hatch/commit/d00959f)) +* refactor(cli): extract handle_mcp_configure to cli_mcp ([9b9bc4d](https://github.com/CrackingShells/Hatch/commit/9b9bc4d)) +* refactor(cli): extract handle_mcp_sync to cli_mcp ([f69be90](https://github.com/CrackingShells/Hatch/commit/f69be90)) +* refactor(cli): extract MCP backup handlers to cli_mcp ([ca65e2b](https://github.com/CrackingShells/Hatch/commit/ca65e2b)) +* refactor(cli): extract MCP discovery handlers to cli_mcp ([887b96e](https://github.com/CrackingShells/Hatch/commit/887b96e)) +* refactor(cli): extract MCP list handlers to cli_mcp ([e518e90](https://github.com/CrackingShells/Hatch/commit/e518e90)) +* refactor(cli): extract MCP remove handlers to cli_mcp ([4e84be7](https://github.com/CrackingShells/Hatch/commit/4e84be7)) +* refactor(cli): extract package handlers to cli_package ([ebecb1e](https://github.com/CrackingShells/Hatch/commit/ebecb1e)) +* refactor(cli): extract shared utilities to cli_utils ([0b0dc92](https://github.com/CrackingShells/Hatch/commit/0b0dc92)) +* refactor(cli): extract system handlers to cli_system ([2f7d715](https://github.com/CrackingShells/Hatch/commit/2f7d715)) +* refactor(cli): integrate backup path into ResultReporter ([fd9a1f4](https://github.com/CrackingShells/Hatch/commit/fd9a1f4)) +* refactor(cli): integrate sync statistics into ResultReporter ([cc5a8b2](https://github.com/CrackingShells/Hatch/commit/cc5a8b2)) +* refactor(cli): normalize cli_utils warning messages ([6e9b983](https://github.com/CrackingShells/Hatch/commit/6e9b983)) +* refactor(cli): normalize MCP warning messages ([b72c6a4](https://github.com/CrackingShells/Hatch/commit/b72c6a4)) +* refactor(cli): normalize operation cancelled messages ([ab0b611](https://github.com/CrackingShells/Hatch/commit/ab0b611)) +* refactor(cli): normalize package warning messages ([c7463b3](https://github.com/CrackingShells/Hatch/commit/c7463b3)) +* refactor(cli): remove --pattern from mcp list servers ([b8baef9](https://github.com/CrackingShells/Hatch/commit/b8baef9)) +* refactor(cli): remove legacy mcp show command ([fd2c290](https://github.com/CrackingShells/Hatch/commit/fd2c290)) +* refactor(cli): rewrite mcp list hosts for host-centric design ([ac88a84](https://github.com/CrackingShells/Hatch/commit/ac88a84)) +* refactor(cli): rewrite mcp list servers for host-centric design ([c2de727](https://github.com/CrackingShells/Hatch/commit/c2de727)) +* refactor(cli): simplify CLI to use unified MCPServerConfig with adapters ([d97b99e](https://github.com/CrackingShells/Hatch/commit/d97b99e)) +* refactor(cli): simplify env list to show package count only ([3045718](https://github.com/CrackingShells/Hatch/commit/3045718)) +* refactor(cli): standardize backup restore failure error ([9a8377f](https://github.com/CrackingShells/Hatch/commit/9a8377f)) +* refactor(cli): standardize configure failure error ([1065c32](https://github.com/CrackingShells/Hatch/commit/1065c32)) +* refactor(cli): standardize mcp sync failure error reporting ([82a2d3b](https://github.com/CrackingShells/Hatch/commit/82a2d3b)) +* refactor(cli): standardize package configure exception warning ([b1bde91](https://github.com/CrackingShells/Hatch/commit/b1bde91)) +* refactor(cli): standardize package configure failure warning ([b14e9f4](https://github.com/CrackingShells/Hatch/commit/b14e9f4)) +* refactor(cli): standardize package invalid host error ([7f448a1](https://github.com/CrackingShells/Hatch/commit/7f448a1)) +* refactor(cli): standardize remove failure error ([023c64f](https://github.com/CrackingShells/Hatch/commit/023c64f)) +* refactor(cli): standardize remove-host failure error ([b2de533](https://github.com/CrackingShells/Hatch/commit/b2de533)) +* refactor(cli): standardize remove-server failure error ([2d40d09](https://github.com/CrackingShells/Hatch/commit/2d40d09)) +* refactor(cli): update env execution errors to use report_error ([8021ba2](https://github.com/CrackingShells/Hatch/commit/8021ba2)) +* refactor(cli): update env validation error to use ValidationError ([101eba7](https://github.com/CrackingShells/Hatch/commit/101eba7)) +* refactor(cli): update MCP exception handlers to use report_error ([edec31d](https://github.com/CrackingShells/Hatch/commit/edec31d)) +* refactor(cli): update MCP validation errors to use ValidationError ([20b165a](https://github.com/CrackingShells/Hatch/commit/20b165a)) +* refactor(cli): update package errors to use report_error ([4d0ab73](https://github.com/CrackingShells/Hatch/commit/4d0ab73)) +* refactor(cli): update system errors to use report_error ([b205032](https://github.com/CrackingShells/Hatch/commit/b205032)) +* refactor(cli): use HatchArgumentParser for all parsers ([4b750fa](https://github.com/CrackingShells/Hatch/commit/4b750fa)) +* refactor(cli): use ResultReporter in env create/remove handlers ([d0991ba](https://github.com/CrackingShells/Hatch/commit/d0991ba)) +* refactor(cli): use ResultReporter in env python handlers ([df14f66](https://github.com/CrackingShells/Hatch/commit/df14f66)) +* refactor(cli): use ResultReporter in handle_env_python_add_hatch_mcp ([0ec6b6a](https://github.com/CrackingShells/Hatch/commit/0ec6b6a)) +* refactor(cli): use ResultReporter in handle_env_use ([b7536fb](https://github.com/CrackingShells/Hatch/commit/b7536fb)) +* refactor(cli): use ResultReporter in handle_mcp_configure ([5f3c60c](https://github.com/CrackingShells/Hatch/commit/5f3c60c)) +* refactor(cli): use ResultReporter in handle_mcp_sync ([9d52d24](https://github.com/CrackingShells/Hatch/commit/9d52d24)) +* refactor(cli): use ResultReporter in handle_package_add ([49585fa](https://github.com/CrackingShells/Hatch/commit/49585fa)) +* refactor(cli): use ResultReporter in handle_package_remove ([58ffdf1](https://github.com/CrackingShells/Hatch/commit/58ffdf1)) +* refactor(cli): use ResultReporter in handle_package_sync ([987b9d1](https://github.com/CrackingShells/Hatch/commit/987b9d1)) +* refactor(cli): use ResultReporter in MCP backup handlers ([9ec9e7b](https://github.com/CrackingShells/Hatch/commit/9ec9e7b)) +* refactor(cli): use ResultReporter in MCP remove handlers ([e727324](https://github.com/CrackingShells/Hatch/commit/e727324)) +* refactor(cli): use ResultReporter in system handlers ([df64898](https://github.com/CrackingShells/Hatch/commit/df64898)) +* refactor(cli): use TableFormatter in handle_env_list ([0f18682](https://github.com/CrackingShells/Hatch/commit/0f18682)) +* refactor(cli): use TableFormatter in handle_mcp_backup_list ([17dd96a](https://github.com/CrackingShells/Hatch/commit/17dd96a)) +* refactor(cli): use TableFormatter in handle_mcp_discover_hosts ([6bef0fa](https://github.com/CrackingShells/Hatch/commit/6bef0fa)) +* refactor(cli): use TableFormatter in handle_mcp_list_hosts ([3b465bb](https://github.com/CrackingShells/Hatch/commit/3b465bb)) +* refactor(cli): use TableFormatter in handle_mcp_list_servers ([3145e47](https://github.com/CrackingShells/Hatch/commit/3145e47)) +* refactor(mcp-adapters): add validate_filtered to BaseAdapter ([b1f542a](https://github.com/CrackingShells/Hatch/commit/b1f542a)) +* refactor(mcp-adapters): convert ClaudeAdapter to validate-after-filter ([13933a5](https://github.com/CrackingShells/Hatch/commit/13933a5)) +* refactor(mcp-adapters): convert CodexAdapter to validate-after-filter ([7ac8de1](https://github.com/CrackingShells/Hatch/commit/7ac8de1)) +* refactor(mcp-adapters): convert CursorAdapter to validate-after-filter ([93aa631](https://github.com/CrackingShells/Hatch/commit/93aa631)) +* refactor(mcp-adapters): convert GeminiAdapter to validate-after-filter ([cb5d98e](https://github.com/CrackingShells/Hatch/commit/cb5d98e)) +* refactor(mcp-adapters): convert KiroAdapter to validate-after-filter ([0eb7d46](https://github.com/CrackingShells/Hatch/commit/0eb7d46)) +* refactor(mcp-adapters): convert LMStudioAdapter to validate-after-filter ([1bd3780](https://github.com/CrackingShells/Hatch/commit/1bd3780)) +* refactor(mcp-adapters): convert VSCodeAdapter to validate-after-filter ([5c78df9](https://github.com/CrackingShells/Hatch/commit/5c78df9)) +* refactor(mcp-host-config): unified MCPServerConfig ([ca0e51c](https://github.com/CrackingShells/Hatch/commit/ca0e51c)) +* refactor(mcp-host-config): update module exports ([5371a43](https://github.com/CrackingShells/Hatch/commit/5371a43)) +* refactor(mcp-host-config): wire all strategies to use adapters ([528e5f5](https://github.com/CrackingShells/Hatch/commit/528e5f5)) +* refactor(mcp): deprecate display_report in favor of ResultReporter ([3880ea3](https://github.com/CrackingShells/Hatch/commit/3880ea3)) +* refactor(models): remove legacy host-specific models from models.py ([ff92280](https://github.com/CrackingShells/Hatch/commit/ff92280)) + + +### BREAKING CHANGE + +* Remove all legacy host-specific configuration models +that are now replaced by the unified adapter architecture. + +Removed models: +- MCPServerConfigBase (abstract base class) +- MCPServerConfigGemini +- MCPServerConfigVSCode +- MCPServerConfigCursor +- MCPServerConfigClaude +- MCPServerConfigKiro +- MCPServerConfigCodex +- MCPServerConfigOmni +- HOST_MODEL_REGISTRY + +The unified MCPServerConfig model plus host-specific adapters now +handle all MCP server configuration. See: +- hatch/mcp_host_config/adapters/ for host adapters + +This is part of Milestone 3.1: Legacy Removal in the adapter architecture +refactoring. Tests will need to be updated in subsequent commits. + ## 0.8.0-dev.2 (2026-02-20) * Merge pull request #45 from LittleCoinCoin/dev ([0ed9010](https://github.com/CrackingShells/Hatch/commit/0ed9010)), closes [#45](https://github.com/CrackingShells/Hatch/issues/45) diff --git a/__reports__/CLI-refactoring/05-documentation_deprecation_analysis_v0.md b/__reports__/CLI-refactoring/05-documentation_deprecation_analysis_v0.md deleted file mode 100644 index c552c7e..0000000 --- a/__reports__/CLI-refactoring/05-documentation_deprecation_analysis_v0.md +++ /dev/null @@ -1,194 +0,0 @@ -# Documentation Deprecation Analysis: CLI Refactoring Impact - -**Date**: 2026-01-01 -**Phase**: Post-Implementation Documentation Review -**Scope**: Identifying deprecated documentation after CLI handler-based architecture refactoring -**Reference**: `__design__/cli-refactoring-milestone-v0.7.2-dev.1.md` - ---- - -## Executive Summary - -The CLI refactoring from monolithic `cli_hatch.py` (2,850 LOC) to handler-based architecture in `hatch/cli/` package has rendered several documentation references outdated. This report identifies affected files and specifies required updates. - -**Architecture Change Summary:** -``` -BEFORE: AFTER: -hatch/cli_hatch.py (2,850 LOC) hatch/cli/ - ├── __init__.py (57 LOC) - ├── __main__.py (840 LOC) - ├── cli_utils.py (270 LOC) - ├── cli_mcp.py (1,222 LOC) - ├── cli_env.py (375 LOC) - ├── cli_package.py (552 LOC) - └── cli_system.py (92 LOC) - - hatch/cli_hatch.py (136 LOC) ← backward compat shim -``` - ---- - -## Affected Documentation Files - -### Category 1: API Documentation (HIGH PRIORITY) - -| File | Issue | Impact | -|------|-------|--------| -| `docs/articles/api/cli.md` | References `hatch.cli_hatch` only | mkdocstrings generates incomplete API docs | - -**Current Content:** -```markdown -# CLI Module -::: hatch.cli_hatch -``` - -**Required Update:** Expand to document the full `hatch.cli` package structure with all submodules. - ---- - -### Category 2: User Documentation (HIGH PRIORITY) - -| File | Line | Issue | -|------|------|-------| -| `docs/articles/users/CLIReference.md` | 3 | States "implemented in `hatch/cli_hatch.py`" | - -**Current Content (Line 3):** -```markdown -This document is a compact reference of all Hatch CLI commands and options implemented in `hatch/cli_hatch.py` presented as tables for quick lookup. -``` - -**Required Update:** Reference the new `hatch/cli/` package structure. - ---- - -### Category 3: Developer Implementation Guides (HIGH PRIORITY) - -| File | Lines | Issue | -|------|-------|-------| -| `docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md` | 605, 613-626 | References `cli_hatch.py` for CLI integration | - -**Affected Sections:** - -1. **Line 605** - "Add CLI arguments in `cli_hatch.py`" -2. **Lines 613-626** - CLI Integration for Host-Specific Fields section - -**Current Content:** -```markdown -4. **Add CLI arguments** in `cli_hatch.py` (see next section) -... -1. **Update function signature** in `handle_mcp_configure()`: -```python -def handle_mcp_configure( - # ... existing params ... - your_field: Optional[str] = None, # Add your field -): -``` -``` - -**Required Update:** -- Argument parsing → `hatch/cli/__main__.py` -- Handler modifications → `hatch/cli/cli_mcp.py` - ---- - -### Category 4: Architecture Documentation (MEDIUM PRIORITY) - -| File | Line | Issue | -|------|------|-------| -| `docs/articles/devs/architecture/mcp_host_configuration.md` | 158 | References `cli_hatch.py` | - -**Current Content (Line 158):** -```markdown -1. Extend `handle_mcp_configure()` function signature in `cli_hatch.py` -``` - -**Required Update:** Reference new module locations. - ---- - -### Category 5: Architecture Diagrams (MEDIUM PRIORITY) - -| File | Line | Issue | -|------|------|-------| -| `docs/resources/diagrams/architecture.puml` | 9 | Shows CLI as single `cli_hatch` component | - -**Current Content:** -```plantuml -Container_Boundary(cli, "CLI Layer") { - Component(cli_hatch, "CLI Interface", "Python", "Command-line interface\nArgument parsing and validation") -} -``` - -**Required Update:** Reflect modular CLI architecture with handler modules. - ---- - -### Category 6: Instruction Templates (LOW PRIORITY) - -| File | Lines | Issue | -|------|-------|-------| -| `cracking-shells-playbook/instructions/documentation-api.instructions.md` | 37-41 | Uses `hatch/cli_hatch.py` as example | - -**Current Content:** -```markdown -**For a module `hatch/cli_hatch.py`, create `docs/articles/api/cli.md`:** -```markdown -# CLI Module -::: hatch.cli_hatch -``` -``` - -**Required Update:** Update example to show new CLI package pattern. - ---- - -## Files NOT to Modify - -| Category | Files | Reason | -|----------|-------|--------| -| Historical Analysis | `__reports__/CLI-refactoring/00-04*.md` | Document pre-refactoring state | -| Design Documents | `__design__/cli-refactoring-*.md` | Document refactoring plan | -| Handover Documents | `__design__/handover-*.md` | Document session context | - ---- - -## Update Strategy - -### Handler Location Mapping - -| Handler/Function | Old Location | New Location | -|------------------|--------------|--------------| -| `main()` | `hatch.cli_hatch` | `hatch.cli.__main__` | -| `handle_mcp_configure()` | `hatch.cli_hatch` | `hatch.cli.cli_mcp` | -| `handle_mcp_*()` | `hatch.cli_hatch` | `hatch.cli.cli_mcp` | -| `handle_env_*()` | `hatch.cli_hatch` | `hatch.cli.cli_env` | -| `handle_package_*()` | `hatch.cli_hatch` | `hatch.cli.cli_package` | -| `handle_create()`, `handle_validate()` | `hatch.cli_hatch` | `hatch.cli.cli_system` | -| `parse_host_list()`, utilities | `hatch.cli_hatch` | `hatch.cli.cli_utils` | -| Argument parsing | `hatch.cli_hatch` | `hatch.cli.__main__` | - -### Backward Compatibility Note - -`hatch/cli_hatch.py` remains as a backward compatibility shim that re-exports all public symbols. External consumers can still import from `hatch.cli_hatch`, but new code should use `hatch.cli.*`. - ---- - -## Implementation Checklist - -- [x] Update `docs/articles/api/cli.md` - Expand API documentation -- [x] Update `docs/articles/users/CLIReference.md` - Fix intro paragraph -- [x] Update `docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md` - Fix CLI integration section -- [x] Update `docs/articles/devs/architecture/mcp_host_configuration.md` - Fix CLI reference -- [x] Update `docs/resources/diagrams/architecture.puml` - Update CLI component -- [x] Update `cracking-shells-playbook/instructions/documentation-api.instructions.md` - Update example - ---- - -## Risk Assessment - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| Broken mkdocstrings generation | High | Medium | Test docs build after changes | -| Developer confusion from outdated guides | Medium | High | Prioritize implementation guide updates | -| Diagram regeneration issues | Low | Low | Verify PlantUML syntax | - diff --git a/__reports__/mcp-docs-refresh/docs-vs-codebase-gap-analysis.md b/__reports__/mcp-docs-refresh/docs-vs-codebase-gap-analysis.md new file mode 100644 index 0000000..c82d5b6 --- /dev/null +++ b/__reports__/mcp-docs-refresh/docs-vs-codebase-gap-analysis.md @@ -0,0 +1,118 @@ +# Gap Analysis: MCP Host Config Dev Docs vs Codebase + +## Problem Statement + +The developer documentation for the MCP host configuration system (`mcp_host_configuration.md` and `mcp_host_configuration_extension.md`) has fallen behind the codebase, particularly in the testing infrastructure sections. This creates misleading guidance for contributors adding new host support. + +## Evidence + +Systematic comparison of the three dev docs against the current codebase state (commit `c544cb3` on `dev`). + +### Tested Docs + +| Doc | Path | +|:----|:-----| +| Architecture | `docs/articles/devs/architecture/mcp_host_configuration.md` | +| Extension Guide | `docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md` | +| Testing Standards | `docs/articles/devs/development_processes/testing_standards.md` | + +--- + +## Findings + +### 1. Testing Infrastructure (Severity: High) + +The testing section in both docs is significantly behind the actual implementation. + +#### 1a. Data-driven infrastructure undocumented + +| Component | Location | Lines | Docs Coverage | +|:----------|:---------|------:|:--------------| +| `HostSpec` dataclass | `tests/test_data/mcp_adapters/host_registry.py` | ~95 | Brief mention, no detail | +| `HostRegistry` class (5+ methods) | `tests/test_data/mcp_adapters/host_registry.py` | ~77 | Brief mention, no detail | +| 3 generator functions | `tests/test_data/mcp_adapters/host_registry.py` | ~83 | Not documented | +| 8 assertion functions | `tests/test_data/mcp_adapters/assertions.py` | ~143 | Not documented | +| Canonical fixture data | `tests/test_data/mcp_adapters/canonical_configs.json` | full | Not documented | +| `CODEX_REVERSE_MAPPINGS` | `tests/test_data/mcp_adapters/host_registry.py:61` | ~3 | Not documented | + +#### 1b. "Zero test code changes" claim is misleading + +The extension guide claims adding a new host requires zero test code changes. In reality it requires: + +- Adding an entry to `canonical_configs.json` +- Adding a `HostSpec` entry in `host_registry.py` (lines 226-242 in `FIELD_SETS` dict + `HostRegistry._build_specs()`) + +#### 1c. Test count undersold + +- Docs say "211+ tests" +- Actual auto-generated count: ~285 (64 sync pairs + 10 validation + 211 field filtering) + +#### 1d. Deprecated test files unacknowledged + +Two files marked `@pytest.mark.skip` and scheduled for removal in v0.9.0: + +- `tests/integration/mcp/test_adapter_serialization.py` +- `tests/regression/mcp/test_field_filtering.py` + +Neither mentioned in any documentation. + +--- + +### 2. Architecture Doc Gaps (Severity: Medium-High) + +#### 2a. Field support matrix incomplete + +Missing from the matrix: + +- **Gemini**: ~10 OAuth fields (`oauth_enabled`, `oauth_clientId`, `oauth_clientSecret`, `oauth_scopes`, `oauth_redirectUrl`, `oauth_tokenUrl`, `oauth_authUrl`, `oauth_extraParams`) +- **Codex**: advanced fields (`cwd`, `env_vars`, `startup_timeout_sec`, `bearer_token_env_var`, `http_headers`) + +#### 2b. `CODEX_FIELD_MAPPINGS` incomplete + +Docs show 2 mappings (`args`->`arguments`, `headers`->`http_headers`). +Actual code has 4 (also `includeTools`->`enabled_tools`, `excludeTools`->`disabled_tools`). + +#### 2c. Missing architectural patterns + +| Pattern | Status | +|:--------|:-------| +| `@register_host_strategy(MCPHostType.X)` decorator | Not documented | +| `MCPHostStrategy` base class interface | Not documented | +| `ClaudeAdapter` variant parameter (`desktop` vs `code`) | Not documented | + +#### 2d. `validate()` deprecation inconsistency + +- Architecture doc marks `validate()` as deprecated (v0.9.0) +- Extension guide Step 2 template still uses `validate()`, not `validate_filtered()` +- No migration guidance provided + +--- + +### 3. What Is Accurate + +- Module organization matches exactly +- 8 host names/enum values correct +- `UNIVERSAL_FIELDS`, per-host field constants match +- `BaseAdapter` protocol methods match +- Three-layer architecture description accurate +- `EXCLUDED_ALWAYS` pattern correct + +--- + +## Root Cause + +The testing infrastructure was overhauled to a data-driven architecture (HostRegistry, generators, property-based assertions) but the documentation was not updated to reflect these changes. The architecture doc and extension guide were written against the pre-overhaul state. + +## Impact Assessment + +- **Scope**: Architecture doc testing section, extension guide Step 2-4, field support matrix +- **Dependencies**: Any contributor following the extension guide to add a new host will get incomplete guidance on testing requirements +- **Risk**: New host contributions may skip fixture/registry setup, causing CI gaps + +## Recommendations + +1. Rewrite the architecture doc testing section to document the full data-driven infrastructure +2. Rewrite the extension guide Step 4 with actual testing requirements +3. Fix validate() → validate_filtered() in templates +4. Complete the field support matrix +5. Document missing architectural patterns (strategy decorator, MCPHostStrategy interface, ClaudeAdapter variant) diff --git a/__reports__/mcp_support_extension_skill/discovery-questionnaire.md b/__reports__/mcp_support_extension_skill/discovery-questionnaire.md new file mode 100644 index 0000000..8214efa --- /dev/null +++ b/__reports__/mcp_support_extension_skill/discovery-questionnaire.md @@ -0,0 +1,205 @@ +# Discovery Questionnaire: Adding a New MCP Host + +## Purpose + +Exhaustive list of information the agent needs to add support for a new MCP host platform. Derived from tracing every codepath that makes a host-specific decision across 10+ source files. + +This questionnaire serves two purposes: +1. **Discovery checklist** — what the agent must find via web research or codebase retrieval +2. **User escalation template** — what to ask the user when discovery tools are unavailable or insufficient + +--- + +## Category A: Host Identity & Config Location + +Discoverable via official docs, GitHub repos, or config file examples. + +| # | Question | Why it matters | File(s) affected | +|---|----------|----------------|------------------| +| A1 | What is the host's canonical name? (e.g., `"kiro"`, `"claude-desktop"`) | Becomes the `MCPHostType` enum value and the adapter `host_name`. Convention: lowercase with hyphens. | `models.py`, every other file | +| A2 | Where is the config file on each platform? (macOS, Linux, Windows paths) | Strategy `get_config_path()` — every existing host has platform-specific path logic. | `strategies.py` | +| A3 | What is the config file format? (JSON or TOML) | Determines strategy `read_configuration()` / `write_configuration()` implementation and which strategy family to inherit from. Only Codex uses TOML; all others use JSON. | `strategies.py` | +| A4 | What is the root key for MCP servers in the config file? | Strategy `get_config_key()`. Known values: `"mcpServers"` (most hosts), `"servers"` (VSCode), `"mcp_servers"` (Codex). | `strategies.py` | +| A5 | How to detect if host is installed on the system? | Strategy `is_host_available()`. Most hosts check for a directory's existence (e.g., `~/.kiro/settings/`). | `strategies.py` | + +### Existing host examples for reference + +| Host | Format | Root Key | macOS Path | Detection | +|------|--------|----------|------------|-----------| +| claude-desktop | JSON | `mcpServers` | `~/Library/Application Support/Claude/claude_desktop_config.json` | Config parent dir exists | +| claude-code | JSON | `mcpServers` | `~/.claude.json` | File exists | +| vscode | JSON | `servers` | `~/Library/Application Support/Code/User/mcp.json` | Code dir exists | +| cursor | JSON | `mcpServers` | `~/.cursor/mcp.json` | `.cursor/` exists | +| lmstudio | JSON | `mcpServers` | `~/.lmstudio/mcp.json` | `.lmstudio/` exists | +| gemini | JSON | `mcpServers` | `~/.gemini/settings.json` | `.gemini/` exists | +| kiro | JSON | `mcpServers` | `~/.kiro/settings/mcp.json` | `.kiro/settings/` exists | +| codex | TOML | `mcp_servers` | `~/.codex/config.toml` | `.codex/` exists | + +--- + +## Category B: Field Support + +Partially discoverable from host documentation. This is where web research helps most — host docs usually list supported config fields. + +| # | Question | Why it matters | File(s) affected | +|---|----------|----------------|------------------| +| B1 | Which transport types does the host support? (stdio, sse, http) | Drives validation rules in `validate_filtered()`. Most hosts support stdio + sse. Gemini also supports http via `httpUrl`. | adapter | +| B2 | Does the host support the `type` discriminator field? (the `"type": "stdio"` / `"sse"` field) | Determines whether host joins `TYPE_SUPPORTING_HOSTS` in `fields.py`. Claude, VSCode, Cursor, LM Studio support it. Gemini, Kiro, Codex do not. | `fields.py` | +| B3 | What host-specific fields exist beyond the universal set? (List each with: field name, type, description, required/optional) | Defines the field set constant in `fields.py` and potentially new `MCPServerConfig` field declarations in `models.py`. | `fields.py`, `models.py` | +| B4 | Does the host use different names for standard fields? (e.g., Codex uses `arguments` instead of `args`, `http_headers` instead of `headers`) | Determines whether a `FIELD_MAPPINGS` dict and `apply_transformations()` override are needed. | `fields.py`, adapter | +| B5 | Are there fields semantically equivalent to another host's fields? (e.g., Gemini `includeTools` ≈ Codex `enabled_tools`) | Cross-host sync field mappings. Codex maps `includeTools` → `enabled_tools` and `excludeTools` → `disabled_tools` to enable transparent Gemini→Codex sync. Without mappings, sync silently drops the field. | `fields.py`, adapter | + +### Universal fields (supported by ALL hosts) + +Every host inherits these 5 fields from `UNIVERSAL_FIELDS`: + +| Field | Type | Description | +|-------|------|-------------| +| `command` | `str` | stdio transport — command to execute | +| `args` | `List[str]` | Command arguments | +| `env` | `Dict[str, str]` | Environment variables | +| `url` | `str` | sse/http transport — server URL | +| `headers` | `Dict[str, str]` | HTTP headers for remote transports | + +--- + +## Category C: Validation & Serialization Rules + +Often discoverable from host documentation, sometimes ambiguous. + +| # | Question | Why it matters | File(s) affected | +|---|----------|----------------|------------------| +| C1 | Transport mutual exclusion: can the host have multiple transports simultaneously, or exactly one? | Core validation logic in `validate_filtered()`. Most hosts require exactly one. Gemini supports three (`command`, `url`, `httpUrl`) but still requires exactly one at a time. | adapter | +| C2 | Are any fields mutually exclusive? (beyond transports) | Additional validation rules in `validate_filtered()`. | adapter | +| C3 | Are any fields conditionally required? (e.g., "if `oauth_enabled` is true, then `oauth_clientId` is required") | Additional validation rules in `validate_filtered()`. | adapter | +| C4 | Does serialization require structural transformation beyond field renaming? (e.g., nesting fields under a sub-key, wrapping transport in a sub-object) | Whether a custom `serialize()` override is needed instead of the standard filter→validate→transform pipeline. | adapter | +| C5 | Does the config file contain non-MCP sections that must be preserved on write? (e.g., Codex preserves `[features]`, Gemini preserves other settings keys) | Strategy `write_configuration()` must read-before-write and merge, not overwrite. | `strategies.py` | + +--- + +## Category D: Architectural Fit + +Requires judgment based on comparing the new host against existing implementations. Rarely discoverable from external docs alone. + +| # | Question | Why it matters | File(s) affected | +|---|----------|----------------|------------------| +| D1 | Is this host functionally identical to an existing host? (same fields, same validation, different name only) | Variant pattern: reuse an existing adapter class with a `variant` constructor parameter (like `ClaudeAdapter(variant="desktop")` / `ClaudeAdapter(variant="code")`) instead of creating a new class. | adapter, `registry.py` | +| D2 | Does this host share config format and I/O logic with an existing host? | Strategy family: inherit from `ClaudeHostStrategy` or `CursorBasedHostStrategy` instead of bare `MCPHostStrategy`, getting `read_configuration()` and `write_configuration()` for free. | `strategies.py` | + +### Existing strategy families + +| Family Base Class | Members | What it provides | +|-------------------|---------|------------------| +| `ClaudeHostStrategy` | `ClaudeDesktopStrategy`, `ClaudeCodeStrategy` | Shared JSON read/write, `_preserve_claude_settings()` | +| `CursorBasedHostStrategy` | `CursorHostStrategy`, `LMStudioHostStrategy` | Shared Cursor-format JSON read/write | +| `MCPHostStrategy` (standalone) | `VSCodeHostStrategy`, `GeminiHostStrategy`, `KiroHostStrategy`, `CodexHostStrategy` | No shared logic — each implements its own I/O | + +--- + +## 5. Escalation Tiers + +When the agent must ask the user (because discovery tools are unavailable or returned insufficient data), present questions in tiers to avoid overwhelming with a single wall of questions. + +### Tier 1: Blocking — cannot proceed without answers + +Ask these first. Every answer feeds directly into a required file modification. + +| Question IDs | Summary | +|--------------|---------| +| A1 | Host canonical name | +| A2 | Config file path per platform | +| A3 | Config file format (JSON/TOML) | +| A4 | Root key for MCP servers | +| B1 | Supported transport types | +| B3 | Host-specific fields (names, types, descriptions) | + +### Tier 2: Important — ask if Tier 1 reveals complexity + +Ask these after Tier 1 if the host has non-standard behavior. + +| Question IDs | Trigger condition | +|--------------|-------------------| +| B4 | Host uses different names for standard fields | +| B5 | Host has tool filtering fields that map to another host's equivalents | +| C1 | Unclear whether transports are mutually exclusive | +| C4 | Config format requires structural nesting beyond flat key-value | +| C5 | Config file has non-MCP sections | + +### Tier 3: Clarification — ask only if ambiguous + +Ask these only if reading existing adapters and strategies leaves the answer unclear. + +| Question IDs | Trigger condition | +|--------------|-------------------| +| A5 | Host detection mechanism is non-obvious | +| B2 | Unclear whether host uses `type` discriminator field | +| C2 | Possible field mutual exclusion beyond transports | +| C3 | Possible conditional field requirements | +| D1 | Host looks identical to an existing one | +| D2 | Host I/O looks similar to an existing strategy family | + +--- + +## 6. Discovery Output Format + +Whether the information comes from web research or user answers, the agent should produce a structured **Host Spec** before proceeding to implementation. This artifact feeds steps 2-5 of the skill workflow. + +```yaml +host: + name: "your-host" # A1 + config_format: "json" # A3 + config_key: "mcpServers" # A4 + +paths: # A2 + darwin: "~/.your-host/config.json" + linux: "~/.config/your-host/config.json" + windows: "~/.your-host/config.json" + +detection: # A5 + method: "directory_exists" + path: "~/.your-host/" + +transports: # B1 + supported: ["stdio", "sse"] + mutual_exclusion: true # C1 + +fields: # B2, B3 + type_discriminator: true + host_specific: + - name: "your_field" + type: "Optional[str]" + description: "Description" + - name: "another_field" + type: "Optional[bool]" + description: "Description" + +field_mappings: {} # B4, B5 (empty if no mappings) + +validation: # C2, C3 + mutual_exclusions: [] + conditional_requirements: [] + +serialization: # C4 + structural_transform: false + +config_file: # C5 + preserved_sections: [] + +architecture: # D1, D2 + variant_of: null + strategy_family: null +``` + +--- + +## 7. Key Design Decisions for Skill Authoring + +| Decision | Recommendation | Rationale | +|----------|---------------|-----------| +| Architecture doc content in skill? | No — stays as developer documentation | Agent doesn't need architectural understanding to follow the recipe | +| Field support matrix in skill? | No — agent reads `fields.py` directly | Avoids stale duplication; agent can inspect the source of truth | +| MCPServerConfig model listing? | No — agent reads `models.py` directly | Same rationale | +| Testing infrastructure deep-dive? | Minimal — just "add these fixtures, run these commands" | Agent doesn't need to understand generators to add fixture data | +| Discovery step as first step? | Yes | Biggest bottleneck is knowing what fields the host supports; makes the rest mechanical | +| Structured output from discovery? | Yes — Host Spec YAML | Decouples information gathering from implementation; same spec whether from web or user | +| Progressive disclosure? | Yes — adapter/strategy/testing contracts in `references/` | Keeps SKILL.md lean; loaded only when host has non-standard needs | diff --git a/__reports__/mcp_support_extension_skill/skill-design-analysis.md b/__reports__/mcp_support_extension_skill/skill-design-analysis.md new file mode 100644 index 0000000..0621fc9 --- /dev/null +++ b/__reports__/mcp_support_extension_skill/skill-design-analysis.md @@ -0,0 +1,124 @@ +# Skill Design Analysis: MCP Host Configuration Extension + +## Purpose + +Design report for converting the MCP host configuration extension workflow into a Claude Code agent skill. The skill enables an LLM agent to autonomously add support for a new MCP host platform to the Hatch CLI. + +--- + +## 1. Skill Relevance Assessment + +### Why it fits + +The core use case — "add support for a new MCP host" — is a bounded, repeatable, multi-step procedure with low tolerance for deviation. That is the sweet spot for skills. An agent needs: + +1. Procedural steps it cannot infer (which files to touch, in what order) +2. Codebase-specific contracts (field set declaration, registry wiring, decorator usage) +3. Template code that must follow exact patterns (the `validate_filtered()` pipeline, `@register_host_strategy`) +4. Testing fixture requirements (`canonical_configs.json` structure, `FIELD_SETS` mapping) + +None of this is general knowledge. An LLM cannot derive it from first principles. + +### What does not translate directly from existing docs + +The two developer docs (`mcp_host_configuration.md` and `mcp_host_configuration_extension.md`) cannot be transplanted as-is. They need restructuring to match how skills work. + +#### 1. Architecture doc is reference material, not instructions + +It explains *how the system works*, not *what to do*. In skill terms, it belongs in `references/`, not in SKILL.md. Much of it is context Claude already handles well — what Pydantic does, what ABC means, what "declarative" means. The field support matrix and testing infrastructure sections are the only parts an agent genuinely needs. + +#### 2. Extension guide has too much "why" + +Sections like "When You Need This", "The Pattern: Adapter + Strategy" overview, the ASCII diagram, the troubleshooting table, and the "Reference: Existing Adapters" table are developer onboarding material. An agent skill should be imperative: "do X, then Y." The existing doc explains concepts; a skill should prescribe actions. + +#### 3. Both docs use relative file paths + +They say `models.py` and `adapters/your_host.py`. A skill needs absolute paths from the repo root (`hatch/mcp_host_config/models.py`) so the agent can operate without ambiguity. + +#### 4. No verification workflow + +The extension guide says *what* to create but does not tell the agent how to verify its work. A skill should include the exact commands to run after each step. + +#### 5. Information that should be discovered, not loaded + +The full field support matrix (35 rows), the MCPServerConfig model (50 lines), and the HostSpec/HostRegistry documentation are heavyweight. An agent adding a new host does not need all of that in context — it needs to know *where to look* and *what to do*. The skill should point to files, not reproduce them. + +#### 6. Extension guide understates the integration surface + +The guide advertises 4 files to modify. The actual count is 10-11 (see Section 4). Two of the missing files (`backup.py`, `reporting.py`) are boilerplate one-liners but the agent will miss them without explicit instructions. + +--- + +## 2. Proposed Skill Structure + +``` +add-mcp-host/ +├── SKILL.md # ~150 lines: trigger, 5-step workflow, verification +└── references/ + ├── discovery-guide.md # How to research host requirements using available tools + ├── adapter-contract.md # BaseAdapter interface, validate_filtered pattern, + │ # field mappings, variant pattern + ├── strategy-contract.md # MCPHostStrategy interface, decorator, families + └── testing-fixtures.md # canonical_configs.json schema, FIELD_SETS, + # reverse mappings, what gets auto-generated +``` + +### SKILL.md scope + +- The 5-step checklist (discover → enum → adapter+strategy → wiring → test fixtures) with exact file paths +- Template snippets (lean — just the required method signatures, not full docstrings) +- Verification commands per step +- Conditional reads: "Read `references/adapter-contract.md` if the host needs field mappings or custom serialization" + +### What stays outside the skill + +The architecture doc (`mcp_host_configuration.md`) remains as developer documentation. The skill should reference it for humans but an agent does not need architectural understanding to follow the recipe. + +--- + +## 3. Proposed Workflow: 5 Steps + +The current extension guide has 4 steps. The skill adds a prior discovery step, making 5: + +| Step | Name | Description | +|------|------|-------------| +| 1 | **Discover host requirements** | Research the target host's MCP config spec using web tools, Context7, or user escalation | +| 2 | **Add enum and field set** | `models.py` enum + `fields.py` field constant + optionally new MCPServerConfig fields | +| 3 | **Create adapter and strategy** | Adapter class + strategy class with `@register_host_strategy` | +| 4 | **Wire integration points** | `adapters/__init__.py`, `adapters/registry.py`, `backup.py`, `reporting.py` | +| 5 | **Register test fixtures** | `canonical_configs.json` + `host_registry.py` entries | + +Step 1's output (a structured field spec) feeds all decisions in steps 2-5. + +### Discovery step: tool priority ladder + +The agent should try discovery tools in order and fall through gracefully: + +1. **Web search + fetch** — find the host's official MCP config docs +2. **Context7** — query library documentation +3. **Codebase retrieval** — check if the host's config format is already partially documented in the repo +4. **Escalate to user** — structured questionnaire (see Section 5) + +If web tools are unavailable in the agent's environment, it must escalate immediately. + +--- + +## 4. Complete File Modification Surface + +Every file the agent must touch when adding a new host: + +| # | File (from repo root) | Always? | What to add | +|---|------|---------|-------------| +| 1 | `hatch/mcp_host_config/models.py` | Yes | `MCPHostType` enum value | +| 2 | `hatch/mcp_host_config/fields.py` | Yes | Field set constant (e.g., `NEW_HOST_FIELDS`), optionally field mappings dict | +| 3 | `hatch/mcp_host_config/adapters/new_host.py` | Yes | New adapter class (or variant registration if identical to existing host) | +| 4 | `hatch/mcp_host_config/adapters/__init__.py` | Yes | Export new adapter class | +| 5 | `hatch/mcp_host_config/adapters/registry.py` | Yes | `_register_defaults()` entry | +| 6 | `hatch/mcp_host_config/strategies.py` | Yes | Strategy class with `@register_host_strategy` decorator | +| 7 | `hatch/mcp_host_config/backup.py` | Yes | Add hostname string to `supported_hosts` set in `BackupInfo.validate_hostname()` | +| 8 | `hatch/mcp_host_config/reporting.py` | Yes | Add `MCPHostType → host_name` entry in `_get_adapter_host_name()` mapping | +| 9 | `tests/test_data/mcp_adapters/canonical_configs.json` | Yes | Canonical config fixture using host-native field names | +| 10 | `tests/test_data/mcp_adapters/host_registry.py` | Yes | `FIELD_SETS` entry, `adapter_map` entry in `HostSpec.get_adapter()`, optionally reverse mappings | +| 11 | `hatch/mcp_host_config/models.py` (MCPServerConfig) | Conditional | New field declarations — only if host introduces fields not already in the model | + +Files 7 and 8 are boilerplate one-liners but are absent from the current extension guide. The agent will miss them without explicit instructions. diff --git a/__reports__/standards-retrospective/02-fresh_eye_review_v0.md b/__reports__/standards-retrospective/02-fresh_eye_review_v0.md deleted file mode 100644 index be058a1..0000000 --- a/__reports__/standards-retrospective/02-fresh_eye_review_v0.md +++ /dev/null @@ -1,82 +0,0 @@ -# Fresh-Eye Review — Post-Implementation Gap Analysis (v0) - -Date: 2026-02-19 -Follows: `01-instructions_redesign_v3.md` implementation via `__roadmap__/instructions-redesign/` - -## Executive Summary - -After the instruction files were rewritten/edited per the v3 redesign, a fresh-eye review reveals **residual stale terminology** in 6 files that were NOT in the §11 affected list, **1 stale cross-reference** in a file that WAS edited, and **1 useful addition** (the `roadmap-execution.instructions.md`) that emerged during implementation but wasn't anticipated in the architecture report. A companion JSON schema (`roadmap-document-schema.json`) is proposed and delivered alongside this report. - -## Findings - -### F1: Stale "Phase N" Terminology in Edited Files - -These files were in the §11 scope and were edited, but retain stale Phase references: - -| File | Location | Stale Text | Suggested Fix | -|:-----|:---------|:-----------|:--------------| -| `reporting.instructions.md` | §2 "Default artifacts" | "Phase 1: Mermaid diagrams…" / "Phase 2: Risk-driven test matrix…" | Replace with "Architecture reports:" / "Test definition reports:" (drop phase numbering) | -| `reporting.instructions.md` | §"Specialized reporting guidance" | "Phase 1 architecture guidance" / "Phase 2 test definition reports" | "Architecture reporting guidance" / "Test definition reporting guidance" | -| `reporting.instructions.md` | §"Where reports go" | "Use `__design__/` for durable design/roadmaps." | "Use `__design__/` for durable architectural decisions." (roadmaps go in `__roadmap__/`, already stated in reporting-structure) | -| `reporting-architecture.instructions.md` | Title + front-matter + opening line | "Phase 1" in title, description, and body | "Stage 1" or simply "Architecture Reporting" | -| `reporting-structure.instructions.md` | §3 README convention | "Phase 1/2/3 etc." | "Stage 1/2/3 etc." or "Analysis/Roadmap/Execution" | - -**Severity**: Low — cosmetic inconsistency, but agents parsing these instructions may be confused by mixed terminology. - -### F2: Stale "Phase N" Terminology in Files Outside §11 Scope - -These files were NOT listed in §11 and were not touched during the campaign: - -| File | Location | Stale Text | Suggested Fix | -|:-----|:---------|:-----------|:--------------| -| `reporting-tests.instructions.md` | Title, front-matter, §body (6+ occurrences) | "Phase 2" throughout | "Stage 1" or "Test Definition Reporting" (tests are defined during Analysis, not a separate phase) | -| `reporting-templates.instructions.md` | Front-matter + section headers | "Phase 1" / "Phase 2" template headers | "Architecture Analysis" / "Test Definition" | -| `reporting-templates.instructions.md` | §Roadmap Recommendation | "create `__design__/_roadmap_vN.md`" | "create a roadmap directory tree under `__roadmap__//`" | -| `reporting-knowledge-transfer.instructions.md` | §"What not to do" | "link to Phase 1 artifacts" | "link to Stage 1 analysis artifacts" | -| `analytic-behavior.instructions.md` | §"Two-Phase Work Process" | "Phase 1: Analysis and Documentation" / "Phase 2: Implementation with Context Refresh" | This is a different "phase" concept (analysis vs implementation within a single session), not the old 7-phase model. **Ambiguous but arguably fine** — the two-phase work process here is about agent behavior, not the code-change workflow. Consider renaming to "Two-Step Work Process" or "Analysis-First Work Process" to avoid confusion. | -| `testing.instructions.md` | §2.3 | "Phase 2 report format" | "Test definition report format" | -| `testing.instructions.md` | §2.3 reference text | "Phase 2 in code change phases" | "Stage 1 (Analysis) in code change phases" | - -**Severity**: Medium for `reporting-tests.instructions.md` and `reporting-templates.instructions.md` (heavily used during Stage 1 work). Low for the others. - -### F3: Missing Cross-Reference in `code-change-phases.instructions.md` - -Stage 3 (Execution) describes the breadth-first algorithm but does NOT link to `roadmap-execution.instructions.md`, which contains the detailed operational manual (failure handling escalation ladder, subagent dispatch protocol, status update discipline, completion checklist). - -**Suggested fix**: Add a reference in Stage 3: -```markdown -For the detailed operational manual (failure handling, subagent dispatch, status updates), see [roadmap-execution.instructions.md](./roadmap-execution.instructions.md). -``` - -### F4: `roadmap-execution.instructions.md` — Unanticipated but Valuable - -This file was created during the campaign but was not listed in v3 §11. It fills a genuine gap: the v3 report describes WHAT the execution model is, but the execution manual describes HOW an agent should operationally navigate it (including the escalation ladder, subagent dispatch, and status update discipline). - -**Recommendation**: Acknowledge in the v3 report's §11 table as an addition, or simply note it in the campaign's amendment log. No action needed — the file is well-written and consistent with the model. - -### F5: Schema Companion Delivered - -A JSON Schema (`roadmap-document-schema.json`) has been created alongside this report. It formally defines the required and optional fields for: -- `README.md` (directory-level entry point) -- Leaf Task files -- Steps within leaf tasks -- Supporting types (status values, amendment log entries, progress entries, Mermaid node definitions) - -Location: `cracking-shells-playbook/instructions/roadmap-document-schema.json` - ---- - -## Prioritized Fix List - -| Priority | Finding | Files Affected | Effort | -|:---------|:--------|:---------------|:-------| -| 1 | F1: Stale terminology in edited files | 3 files | ~15 min (surgical text replacements) | -| 2 | F3: Missing cross-reference | 1 file | ~2 min | -| 3 | F2: Stale terminology in unscoped files | 5 files | ~45 min (more occurrences, some require judgment) | -| 4 | F4: Acknowledge execution manual | 1 file (v3 report or amendment log) | ~5 min | - -## Decision Required - -- **F1 + F3**: Straightforward fixes, recommend immediate application. -- **F2**: Larger scope. The `reporting-tests.instructions.md` and `reporting-templates.instructions.md` files have "Phase" deeply embedded. A dedicated task or amendment may be warranted. -- **F2 (analytic-behavior)**: The "Two-Phase Work Process" is arguably a different concept. Stakeholder judgment needed on whether to rename. diff --git a/__roadmap__/adding-mcp-hosts-skill/README.md b/__roadmap__/adding-mcp-hosts-skill/README.md new file mode 100644 index 0000000..b942597 --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/README.md @@ -0,0 +1,82 @@ +# Adding MCP Hosts Skill + +## Context + +Standalone skill authoring campaign. Converts the MCP host configuration extension workflow into a Claude Code agent skill. The skill enables an LLM agent to autonomously add support for a new MCP host platform to the Hatch CLI — from discovery through implementation to test verification. + +## Reference Documents + +- [R01 Skill Design Analysis](../../__reports__/mcp_support_extension_skill/skill-design-analysis.md) — Skill relevance assessment, proposed structure, 5-step workflow, complete 10-11 file modification surface +- [R02 Discovery Questionnaire](../../__reports__/mcp_support_extension_skill/discovery-questionnaire.md) — 17 questions across 4 categories, 3 escalation tiers, Host Spec YAML output format +- [R03 Best Practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) — Official skill authoring best practices (web) + +## Goal + +Produce a packaged `adding-mcp-hosts.skill` file that an agent can use to add support for any new MCP host platform. + +## Pre-conditions + +- [x] Design reports reviewed (R01, R02) +- [x] Best practices consulted (R03) +- [x] MCP docs refresh campaign completed (architecture doc + extension guide up to date) + +## Success Gates + +- SKILL.md under 500 lines with 5-step workflow checklist +- 4 reference files with progressive disclosure (discovery, adapter, strategy, testing) +- Skill passes `package_skill.py` validation (frontmatter, naming, description) +- Packaged `.skill` file produced + +## Gotchas + +- No `init_skill.py` step — each parallel task creates its target file directly. Tasks must `mkdir -p` the skill directory before writing. +- `package_skill.py` imports `quick_validate` from its own directory — must set PYTHONPATH accordingly. +- Skill name must be kebab-case, max 64 chars, no reserved words ("anthropic", "claude"). Using `adding-mcp-hosts`. +- Description must be third-person, max 1024 chars, no angle brackets. +- Reference files should be one level deep from SKILL.md (no nested references). +- All 5 content leaves target different files in `__design__/skills/adding-mcp-hosts/` — worktree merges will be conflict-free. + +## Status + +```mermaid +graph TD + write_discovery_guide[Write Discovery Guide]:::done + write_adapter_contract[Write Adapter Contract]:::done + write_strategy_contract[Write Strategy Contract]:::done + write_testing_fixtures[Write Testing Fixtures]:::done + write_skill_md[Write Skill MD]:::done + package[Package]:::done + + classDef done fill:#166534,color:#bbf7d0 + classDef inprogress fill:#854d0e,color:#fef08a + classDef planned fill:#374151,color:#e5e7eb + classDef amendment fill:#1e3a5f,color:#bfdbfe + classDef blocked fill:#7f1d1d,color:#fecaca +``` + +## Nodes + +| Node | Type | Status | +|:-----|:-----|:-------| +| `write_discovery_guide.md` | Leaf Task | Done | +| `write_adapter_contract.md` | Leaf Task | Done | +| `write_strategy_contract.md` | Leaf Task | Done | +| `write_testing_fixtures.md` | Leaf Task | Done | +| `write_skill_md.md` | Leaf Task | Done | +| `package/` | Directory | Done | + +## Amendment Log + +| ID | Date | Source | Nodes Added | Rationale | +|:---|:-----|:-------|:------------|:----------| + +## Progress + +| Node | Branch | Commits | Notes | +|:-----|:-------|:--------|:------| +| `write_discovery_guide.md` | `task/write-discovery-guide` | 1 | 218 lines, 5 sections | +| `write_adapter_contract.md` | `task/write-adapter-contract` | 1 | 157 lines, 7 subsections | +| `write_strategy_contract.md` | `task/write-strategy-contract` | 1 | 226 lines, 5 sections | +| `write_testing_fixtures.md` | `task/write-testing-fixtures` | 1 | 121 lines, 5 sections | +| `write_skill_md.md` | `task/write-skill-md` | 1 | 202 lines, 5-step workflow | +| `package/package_skill.md` | `task/package-skill` | 1 | Validated + packaged .skill | diff --git a/__roadmap__/adding-mcp-hosts-skill/package/README.md b/__roadmap__/adding-mcp-hosts-skill/package/README.md new file mode 100644 index 0000000..cb3eac5 --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/package/README.md @@ -0,0 +1,48 @@ +# Package + +## Context + +Final validation and packaging of the adding-mcp-hosts skill. All 5 content files (SKILL.md + 4 references) must be complete and merged before packaging can validate the full skill structure. + +## Goal + +Validate skill structure and produce the distributable `.skill` package. + +## Pre-conditions + +- [x] All 5 depth-0 leaves merged into milestone + +## Success Gates + +- `package_skill.py` validation passes +- `adding-mcp-hosts.skill` file produced + +## Status + +```mermaid +graph TD + package_skill[Package Skill]:::done + + classDef done fill:#166534,color:#bbf7d0 + classDef inprogress fill:#854d0e,color:#fef08a + classDef planned fill:#374151,color:#e5e7eb + classDef amendment fill:#1e3a5f,color:#bfdbfe + classDef blocked fill:#7f1d1d,color:#fecaca +``` + +## Nodes + +| Node | Type | Status | +|:-----|:-----|:-------| +| `package_skill.md` | Leaf Task | Done | + +## Amendment Log + +| ID | Date | Source | Nodes Added | Rationale | +|:---|:-----|:-------|:------------|:----------| + +## Progress + +| Node | Branch | Commits | Notes | +|:-----|:-------|:--------|:------| +| `package_skill.md` | `task/package-skill` | 1 | Validated + packaged | diff --git a/__roadmap__/adding-mcp-hosts-skill/package/package_skill.md b/__roadmap__/adding-mcp-hosts-skill/package/package_skill.md new file mode 100644 index 0000000..60cb425 --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/package/package_skill.md @@ -0,0 +1,43 @@ +# Package Skill + +**Goal**: Validate and package the skill into a distributable `.skill` file. +**Pre-conditions**: +- [ ] Branch `task/package-skill` created from `milestone/adding-mcp-hosts-skill` +- [ ] All 5 content files present in `__design__/skills/adding-mcp-hosts/` +**Success Gates**: +- `package_skill.py` exits 0 +- `.skill` file contains exactly 5 files: SKILL.md + 4 references +**References**: Skill creator scripts at `~/.claude/plugins/cache/anthropic-agent-skills/example-skills/*/skills/skill-creator/scripts/` + +--- + +## Step 1: Validate and package + +**Goal**: Run the packaging script and verify the output. + +**Implementation Logic**: +1. Locate the skill-creator scripts directory (glob for `**/skill-creator/scripts/package_skill.py` under `~/.claude/plugins/cache/`) +2. Create output directory: `mkdir -p __design__/skills/dist/` +3. Run packaging with PYTHONPATH set for the `quick_validate` import: + ```bash + PYTHONPATH= python /package_skill.py __design__/skills/adding-mcp-hosts/ __design__/skills/dist/ + ``` +4. If validation fails: + - Read the error message + - Fix the issue in SKILL.md (most likely frontmatter problem) + - Re-run packaging +5. Verify the produced `.skill` file: + ```bash + python -c "import zipfile; [print(f) for f in zipfile.ZipFile('__design__/skills/dist/adding-mcp-hosts.skill').namelist()]" + ``` + Expected contents: + - `adding-mcp-hosts/SKILL.md` + - `adding-mcp-hosts/references/discovery-guide.md` + - `adding-mcp-hosts/references/adapter-contract.md` + - `adding-mcp-hosts/references/strategy-contract.md` + - `adding-mcp-hosts/references/testing-fixtures.md` +6. Report the `.skill` file path to the user. + +**Deliverables**: `__design__/skills/dist/adding-mcp-hosts.skill` +**Consistency Checks**: `package_skill.py` exit code 0; zip contains exactly 5 files +**Commit**: `chore(skill): package adding-mcp-hosts skill` diff --git a/__roadmap__/adding-mcp-hosts-skill/write_adapter_contract.md b/__roadmap__/adding-mcp-hosts-skill/write_adapter_contract.md new file mode 100644 index 0000000..53fe401 --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/write_adapter_contract.md @@ -0,0 +1,54 @@ +# Write Adapter Contract + +**Goal**: Document the adapter interface contract for implementing a new host adapter. +**Pre-conditions**: +- [ ] Branch `task/write-adapter-contract` created from `milestone/adding-mcp-hosts-skill` +**Success Gates**: +- `__design__/skills/adding-mcp-hosts/references/adapter-contract.md` exists +- Covers all 7 subsections and references all 10 files from R01 §4 +**References**: [R01 §4](../../__reports__/mcp_support_extension_skill/skill-design-analysis.md) — Complete file modification surface + +--- + +## Step 1: Write adapter-contract.md + +**Goal**: Create the reference file documenting everything an agent needs to implement a host adapter. + +**Implementation Logic**: +Create `__design__/skills/adding-mcp-hosts/references/adapter-contract.md` (`mkdir -p` the path first). Derive from R01 §4 cross-referenced with the codebase. Read the following files to extract exact patterns: +- `hatch/mcp_host_config/fields.py` — field set constants, UNIVERSAL_FIELDS, TYPE_SUPPORTING_HOSTS, existing FIELD_MAPPINGS +- `hatch/mcp_host_config/models.py` — MCPHostType enum, MCPServerConfig model +- `hatch/mcp_host_config/adapters/` — BaseAdapter ABC, existing adapter implementations +- `hatch/mcp_host_config/adapters/__init__.py` — export pattern +- `hatch/mcp_host_config/adapters/registry.py` — `_register_defaults()` pattern +- `hatch/mcp_host_config/backup.py` — `supported_hosts` set in `BackupInfo.validate_hostname()` +- `hatch/mcp_host_config/reporting.py` — `MCPHostType → host_name` mapping in `_get_adapter_host_name()` + +Structure: + +1. **MCPHostType enum** — How to add the enum value. Convention: `UPPER_SNAKE = "kebab-case"`. File: `hatch/mcp_host_config/models.py`. + +2. **Field set declaration** — How to define `_FIELDS` frozenset in `hatch/mcp_host_config/fields.py`. Pattern: `UNIVERSAL_FIELDS | {host-specific fields}`. Include `TYPE_SUPPORTING_HOSTS` membership decision. + +3. **MCPServerConfig fields** — When to add new field declarations to `MCPServerConfig`. Only needed if host introduces fields not already in the model. File: `hatch/mcp_host_config/models.py`. + +4. **Adapter class** — `BaseAdapter` interface. Lean template with required method signatures: + - `get_supported_fields()` → return the field set constant + - `validate_filtered()` → transport mutual exclusion + host-specific rules + - `apply_transformations()` → field renaming via mappings dict (if applicable) + - `serialize()` → standard pipeline (filter → validate → transform), override only if structural transformation needed + Show the `validate_filtered()` template snippet from the extension guide. + +5. **Field mappings** — When to define `_FIELD_MAPPINGS` dict. Pattern: `{"standard_name": "host_name"}`. Reference `CODEX_FIELD_MAPPINGS` as canonical example. + +6. **Variant pattern** — When to reuse an existing adapter with a variant parameter instead of a new class. Reference `ClaudeAdapter(variant="desktop"|"code")` as canonical example. + +7. **Wiring and integration points** — All 4 one-liner integration files: + - `adapters/__init__.py` — export new adapter class + - `adapters/registry.py` — `_register_defaults()` entry mapping `MCPHostType → adapter instance` + - `backup.py` — add hostname string to `supported_hosts` set + - `reporting.py` — add `MCPHostType → host_name` entry in mapping dict + +**Deliverables**: `__design__/skills/adding-mcp-hosts/references/adapter-contract.md` (~120-160 lines) +**Consistency Checks**: File covers all 7 subsections; references all source files listed above +**Commit**: `feat(skill): add adapter contract reference` diff --git a/__roadmap__/adding-mcp-hosts-skill/write_discovery_guide.md b/__roadmap__/adding-mcp-hosts-skill/write_discovery_guide.md new file mode 100644 index 0000000..ad2cd6b --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/write_discovery_guide.md @@ -0,0 +1,45 @@ +# Write Discovery Guide + +**Goal**: Write the discovery workflow reference for researching a new host's MCP config requirements. +**Pre-conditions**: +- [ ] Branch `task/write-discovery-guide` created from `milestone/adding-mcp-hosts-skill` +**Success Gates**: +- `__design__/skills/adding-mcp-hosts/references/discovery-guide.md` exists +- Contains all 4 question categories, 3 escalation tiers, and Host Spec YAML template +**References**: [R02](../../__reports__/mcp_support_extension_skill/discovery-questionnaire.md) — Primary source for all content + +--- + +## Step 1: Write discovery-guide.md + +**Goal**: Create the reference file that guides an agent through host requirement discovery. + +**Implementation Logic**: +Create `__design__/skills/adding-mcp-hosts/references/discovery-guide.md` (`mkdir -p` the path first). Derive content from R02. Structure: + +1. **Tool priority ladder** — Ordered fallback chain: + - Web search + fetch → find official MCP config docs for the target host + - Context7 → query library documentation for the host + - Codebase retrieval → check if host config format is already partially documented + - User escalation → structured questionnaire (see below) + For each level: what to search for, what "success" looks like, when to fall through. + +2. **Structured questionnaire** — All 17 questions from R02 across 4 categories: + - **Category A: Host Identity & Config Location** (A1-A5) — canonical name, config paths per platform, format (JSON/TOML), root key, detection method + - **Category B: Field Support** (B1-B5) — transport types, type discriminator, host-specific fields, field name mappings, cross-host equivalents + - **Category C: Validation & Serialization** (C1-C5) — transport mutual exclusion, field mutual exclusions, conditional requirements, structural transforms, preserved config sections + - **Category D: Architectural Fit** (D1-D2) — variant of existing host, strategy family match + Each question: ID, question text, why it matters, which file(s) it affects. + +3. **Escalation tiers** — Progressive disclosure for user questioning: + - **Tier 1 (Blocking)**: A1, A2, A3, A4, B1, B3 — cannot proceed without these + - **Tier 2 (Complexity-triggered)**: B4, B5, C1, C4, C5 — ask if Tier 1 reveals non-standard behavior + - **Tier 3 (Ambiguity-only)**: A5, B2, C2, C3, D1, D2 — ask only if reading existing code leaves answer unclear + +4. **Existing host reference table** — All 8 current hosts (claude-desktop, claude-code, vscode, cursor, lmstudio, gemini, kiro, codex) with: format, root key, macOS path, detection method. Gives the agent comparison points. + +5. **Host Spec YAML output format** — The structured artifact the discovery step produces. Include the full YAML template from R02 §6 with all fields annotated by question ID. + +**Deliverables**: `__design__/skills/adding-mcp-hosts/references/discovery-guide.md` (~150-200 lines) +**Consistency Checks**: File contains sections for all 4 categories, all 3 tiers, reference table, and YAML template +**Commit**: `feat(skill): add discovery guide reference` diff --git a/__roadmap__/adding-mcp-hosts-skill/write_skill_md.md b/__roadmap__/adding-mcp-hosts-skill/write_skill_md.md new file mode 100644 index 0000000..fdb7cb5 --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/write_skill_md.md @@ -0,0 +1,84 @@ +# Write Skill MD + +**Goal**: Write the main SKILL.md with frontmatter and 5-step workflow body. +**Pre-conditions**: +- [ ] Branch `task/write-skill-md` created from `milestone/adding-mcp-hosts-skill` +**Success Gates**: +- `__design__/skills/adding-mcp-hosts/SKILL.md` exists with valid frontmatter +- Body under 500 lines with 5-step workflow +- Links to all 4 reference files by relative path +**References**: +- [R01 §2-3](../../__reports__/mcp_support_extension_skill/skill-design-analysis.md) — Proposed structure and workflow +- [R03](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) — Frontmatter rules, description guidelines + +--- + +## Step 1: Write SKILL.md + +**Goal**: Create the main skill file with frontmatter and workflow body. + +**Implementation Logic**: +Create `__design__/skills/adding-mcp-hosts/SKILL.md` (`mkdir -p` the path first). + +**Frontmatter** (YAML): +- `name`: `adding-mcp-hosts` +- `description`: Third-person, ~200-300 chars. Must cover: + - WHAT: Adds support for a new MCP host platform to the Hatch CLI multi-host configuration system + - WHEN: When asked to add, integrate, or extend MCP host support for a new IDE, editor, or AI coding tool (e.g., Windsurf, Zed, Copilot) + - HOW: 5-step workflow from discovery through test verification + No angle brackets. No reserved words. Max 1024 chars. + +**Body** (Markdown, target 150-200 lines). Use imperative form throughout. Structure: + +1. **Workflow checklist** — Copy-paste progress tracker: + ``` + - [ ] Step 1: Discover host requirements + - [ ] Step 2: Add enum and field set + - [ ] Step 3: Create adapter and strategy + - [ ] Step 4: Wire integration points + - [ ] Step 5: Register test fixtures + ``` + +2. **Step 1: Discover host requirements** (~15 lines): + - "Read [references/discovery-guide.md](references/discovery-guide.md) for the full discovery workflow." + - Summarize: use web tools to research, fall back to user questionnaire, produce Host Spec YAML. + - Output: structured Host Spec feeding all subsequent steps. + +3. **Step 2: Add enum and field set** (~20 lines, inline): + - Add `MCPHostType` enum value in `hatch/mcp_host_config/models.py` + - Add `_FIELDS` frozenset in `hatch/mcp_host_config/fields.py` (pattern: `UNIVERSAL_FIELDS | {extras}`) + - Optionally add new `MCPServerConfig` fields if host introduces novel fields + - Verification: `python -c "from hatch.mcp_host_config.models import MCPHostType; print(MCPHostType.YOUR_HOST)"` + +4. **Step 3: Create adapter and strategy** (~20 lines): + - "Read [references/adapter-contract.md](references/adapter-contract.md) for the adapter interface." + - "Read [references/strategy-contract.md](references/strategy-contract.md) for the strategy interface." + - Mention variant pattern shortcut (if host is functionally identical to existing host) + - Mention strategy family decision (inherit from ClaudeHostStrategy, CursorBasedHostStrategy, or standalone) + - Verification: import and instantiate the adapter. + +5. **Step 4: Wire integration points** (~20 lines, inline): + - `adapters/__init__.py` — export new adapter + - `adapters/registry.py` — add `_register_defaults()` entry + - `backup.py` — add hostname to `supported_hosts` set + - `reporting.py` — add `MCPHostType → host_name` mapping + - Verification: `python -c "from hatch.mcp_host_config.adapters.registry import AdapterRegistry; ..."` + +6. **Step 5: Register test fixtures** (~15 lines): + - "Read [references/testing-fixtures.md](references/testing-fixtures.md) for fixture schemas and registration." + - Add entry to `tests/test_data/mcp_adapters/canonical_configs.json` + - Add entries to `tests/test_data/mcp_adapters/host_registry.py` + - Verification: `python -m pytest tests/integration/mcp/ tests/unit/mcp/ tests/regression/mcp/ -v` + +7. **Cross-references table** (~10 lines): + | Reference | Covers | Read when | + | discovery-guide.md | Host research, questionnaire, Host Spec YAML | Step 1 (always) | + | adapter-contract.md | BaseAdapter interface, field sets, registry wiring | Step 3 (always) | + | strategy-contract.md | MCPHostStrategy interface, families, platform paths | Step 3 (always) | + | testing-fixtures.md | Fixture schema, auto-generated tests, pytest commands | Step 5 (always) | + +No conceptual explanations. No "what is Pydantic." No architecture overview. Just the recipe. + +**Deliverables**: `__design__/skills/adding-mcp-hosts/SKILL.md` (~150-200 lines) +**Consistency Checks**: `python quick_validate.py __design__/skills/adding-mcp-hosts/` (expected: PASS) +**Commit**: `feat(skill): write SKILL.md with 5-step workflow` diff --git a/__roadmap__/adding-mcp-hosts-skill/write_strategy_contract.md b/__roadmap__/adding-mcp-hosts-skill/write_strategy_contract.md new file mode 100644 index 0000000..f435526 --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/write_strategy_contract.md @@ -0,0 +1,50 @@ +# Write Strategy Contract + +**Goal**: Document the strategy interface contract for implementing host file I/O. +**Pre-conditions**: +- [ ] Branch `task/write-strategy-contract` created from `milestone/adding-mcp-hosts-skill` +**Success Gates**: +- `__design__/skills/adding-mcp-hosts/references/strategy-contract.md` exists +- Documents all 5 abstract methods, decorator pattern, and all 3 strategy families +**References**: Codebase `hatch/mcp_host_config/strategies.py` — all existing strategy implementations + +--- + +## Step 1: Write strategy-contract.md + +**Goal**: Create the reference file documenting everything an agent needs to implement a host strategy. + +**Implementation Logic**: +Create `__design__/skills/adding-mcp-hosts/references/strategy-contract.md` (`mkdir -p` the path first). Derive from codebase inspection of `hatch/mcp_host_config/strategies.py`. Read the file to extract exact patterns for all existing strategies. + +Structure: + +1. **MCPHostStrategy interface** — Abstract methods to implement: + - `get_config_path() → Path` — platform-specific config file location (`sys.platform` dispatch) + - `get_config_key() → str` — root key for MCP servers in config (e.g., `"mcpServers"`, `"servers"`) + - `read_configuration() → dict` — read and parse the config file + - `write_configuration(config: dict)` — write config, preserving non-MCP sections if applicable + - `is_host_available() → bool` — detect whether host is installed on the system + Include lean method signature template. + +2. **@register_host_strategy decorator** — Usage: `@register_host_strategy(MCPHostType.YOUR_HOST)`. Explain that this auto-registers the strategy so `HostConfigurationManager` can discover it by host type. File: `hatch/mcp_host_config/strategies.py`. + +3. **Strategy families** — Decision tree for base class selection: + - `ClaudeHostStrategy` → if host shares Claude's JSON format with settings preservation. Members: `ClaudeDesktopStrategy`, `ClaudeCodeStrategy`. Provides `_preserve_claude_settings()`. + - `CursorBasedHostStrategy` → if host shares Cursor's simple JSON format (flat JSON, `mcpServers` key). Members: `CursorHostStrategy`, `LMStudioHostStrategy`. + - `MCPHostStrategy` (standalone) → if host has unique I/O needs. Members: `VSCodeHostStrategy`, `GeminiHostStrategy`, `KiroHostStrategy`, `CodexHostStrategy`. + +4. **Platform path patterns** — Common patterns from existing strategies: + - Simple home-relative: `Path.home() / ".host-dir" / "config.json"` (Cursor, LM Studio, Gemini, Kiro, Codex) + - macOS Application Support: `Path.home() / "Library" / "Application Support" / "AppName" / "config.json"` (Claude Desktop, VSCode) + - XDG on Linux: `Path.home() / ".config" / "host-dir" / "config.json"` (VSCode) + +5. **Config preservation** — Read-before-write pattern for files with non-MCP sections: + - Codex: preserves `[features]` and other TOML sections + - Gemini: preserves other keys in `settings.json` + - Claude Desktop: preserves non-mcpServers keys + Describe the merge pattern: read existing → update MCP section → write back. + +**Deliverables**: `__design__/skills/adding-mcp-hosts/references/strategy-contract.md` (~80-120 lines) +**Consistency Checks**: File documents all 5 abstract methods, decorator, 3 families with members, path patterns, preservation pattern +**Commit**: `feat(skill): add strategy contract reference` diff --git a/__roadmap__/adding-mcp-hosts-skill/write_testing_fixtures.md b/__roadmap__/adding-mcp-hosts-skill/write_testing_fixtures.md new file mode 100644 index 0000000..feac990 --- /dev/null +++ b/__roadmap__/adding-mcp-hosts-skill/write_testing_fixtures.md @@ -0,0 +1,54 @@ +# Write Testing Fixtures + +**Goal**: Document the test fixture registration process and verification commands. +**Pre-conditions**: +- [ ] Branch `task/write-testing-fixtures` created from `milestone/adding-mcp-hosts-skill` +**Success Gates**: +- `__design__/skills/adding-mcp-hosts/references/testing-fixtures.md` exists +- Documents fixture schema, registry entries, auto-generation counts, and verification commands +**References**: Codebase `tests/test_data/mcp_adapters/` — canonical_configs.json, host_registry.py, assertions.py + +--- + +## Step 1: Write testing-fixtures.md + +**Goal**: Create the reference file documenting how to register test fixtures for a new host. + +**Implementation Logic**: +Create `__design__/skills/adding-mcp-hosts/references/testing-fixtures.md` (`mkdir -p` the path first). Derive from inspection of `tests/test_data/mcp_adapters/`. Read the following files: +- `tests/test_data/mcp_adapters/canonical_configs.json` — fixture structure and existing entries +- `tests/test_data/mcp_adapters/host_registry.py` — `HostSpec`, `HostRegistry`, `FIELD_SETS`, generator functions +- `tests/test_data/mcp_adapters/assertions.py` — property-based assertion library +- `tests/integration/mcp/test_host_configuration.py` — how canonical configs drive parametrized tests +- `tests/integration/mcp/test_cross_host_sync.py` — how sync test matrix auto-expands + +Structure: + +1. **canonical_configs.json entry** — Schema for the fixture. Each host entry: + - Uses host-native field names (post-mapping if host has FIELD_MAPPINGS) + - Sets `null` for unsupported fields + - Must include at least one transport (command/url/httpUrl) + Show a minimal example entry derived from an existing host. + +2. **host_registry.py entries** — Three additions: + - `FIELD_SETS` dict — maps host name string → `fields.py` field set constant (e.g., `"your-host": YOUR_HOST_FIELDS`) + - `adapter_map` in `HostSpec.get_adapter()` — maps host name → adapter instance (e.g., `"your-host": YourHostAdapter()`) + - Reverse mappings (conditional) — only for hosts with `FIELD_MAPPINGS`. Maps host-native names back to canonical names for test verification. + +3. **What auto-generates** — Adding fixture data produces ~20+ test cases without writing any test code: + - 1 host configuration test (serialization roundtrip per host) + - 16 new cross-host sync tests (8 from-host + 8 to-host pair combinations) + - Validation property tests (transport mutual exclusion, tool list coexistence if applicable) + - Field filtering regression tests (one per unsupported field) + +4. **Verification commands** — Exact pytest invocations to run after registration: + - Full suite: `python -m pytest tests/integration/mcp/ tests/unit/mcp/ tests/regression/mcp/ -v` + - Quick smoke: `python -m pytest tests/integration/mcp/test_host_configuration.py -v` + - Protocol compliance: `python -m pytest tests/unit/mcp/test_adapter_protocol.py -v` + - Cross-host sync: `python -m pytest tests/integration/mcp/test_cross_host_sync.py -v` + +5. **Expected results** — New host name appears in parametrized test IDs (e.g., `test_configure_host[your-host]`). All tests pass. No regressions in existing host tests. + +**Deliverables**: `__design__/skills/adding-mcp-hosts/references/testing-fixtures.md` (~80-120 lines) +**Consistency Checks**: File documents fixture schema, all 3 registry entries, auto-gen counts, 4 pytest commands +**Commit**: `feat(skill): add testing fixtures reference` diff --git a/__roadmap__/mcp-docs-refresh/README.md b/__roadmap__/mcp-docs-refresh/README.md new file mode 100644 index 0000000..13114ed --- /dev/null +++ b/__roadmap__/mcp-docs-refresh/README.md @@ -0,0 +1,64 @@ +# MCP Host Config Dev Docs Refresh + +## Context + +Standalone doc cleanup campaign. The developer documentation for the MCP host configuration system has diverged from the codebase, primarily in the testing infrastructure section but also in the field support matrix, architectural patterns, and extension guide templates. This campaign updates both the architecture reference doc and the extension guide to match codebase reality. + +## Reference Documents + +- [R01 Gap Analysis](../../__reports__/mcp-docs-refresh/docs-vs-codebase-gap-analysis.md) — Full docs-vs-codebase comparison with severity ratings + +## Goal + +Align MCP host config dev docs with the current codebase state so contributors get accurate guidance when adding new hosts. + +## Pre-conditions + +- [x] Gap analysis report reviewed (R01) +- [x] `dev` branch checked out + +## Success Gates + +- All documented field support matches `fields.py` constants +- All documented patterns match codebase implementations +- Testing section accurately describes `tests/test_data/mcp_adapters/` infrastructure +- Extension guide Step 2-4 templates produce working implementations when followed literally + +## Gotchas + +- `validate()` is deprecated but still abstract in `BaseAdapter` — document the migration path without removing it yet (that's a code change for v0.9.0, not a doc change). +- LM Studio is missing from the field support matrix entirely — verify whether it needs its own column or shares Claude's field set. +- Both tasks edit separate files (`mcp_host_configuration.md` vs `mcp_host_configuration_extension.md`), so they can safely execute in parallel. Cross-references between the two docs should be verified after both complete. + +## Status + +```mermaid +graph TD + update_architecture_doc[Update Architecture Doc]:::done + update_extension_guide[Update Extension Guide]:::done + + classDef done fill:#166534,color:#bbf7d0 + classDef inprogress fill:#854d0e,color:#fef08a + classDef planned fill:#374151,color:#e5e7eb + classDef amendment fill:#1e3a5f,color:#bfdbfe + classDef blocked fill:#7f1d1d,color:#fecaca +``` + +## Nodes + +| Node | Type | Status | +|:-----|:-----|:-------| +| `update_architecture_doc.md` | Leaf Task | Done | +| `update_extension_guide.md` | Leaf Task | Done | + +## Amendment Log + +| ID | Date | Source | Nodes Added | Rationale | +|:---|:-----|:-------|:------------|:----------| + +## Progress + +| Node | Branch | Commits | Notes | +|:-----|:-------|:--------|:------| +| `update_architecture_doc.md` | `task/update-architecture-doc` | 3 | Field matrix, strategy/variant patterns, testing section | +| `update_extension_guide.md` | `task/update-extension-guide` | 3 | validate_filtered template, strategy interface docs, data-driven testing | diff --git a/__roadmap__/mcp-docs-refresh/update_architecture_doc.md b/__roadmap__/mcp-docs-refresh/update_architecture_doc.md new file mode 100644 index 0000000..d01eb6b --- /dev/null +++ b/__roadmap__/mcp-docs-refresh/update_architecture_doc.md @@ -0,0 +1,71 @@ +# Update Architecture Doc + +**Goal**: Bring `docs/articles/devs/architecture/mcp_host_configuration.md` into alignment with the current codebase. +**Pre-conditions**: +- [ ] Branch `task/update-architecture-doc` created from `milestone/mcp-docs-refresh` +**Success Gates**: +- Field support matrix matches all per-host field sets in `hatch/mcp_host_config/fields.py` +- `CODEX_FIELD_MAPPINGS` shows all 4 entries (not 2) +- Strategy layer, `MCPHostStrategy` interface, and `@register_host_strategy` decorator are documented +- `ClaudeAdapter` variant pattern documented +- Testing section documents `HostSpec`, `HostRegistry`, generator functions, assertion functions, and `canonical_configs.json` structure +**References**: [R01 Gap Analysis](../../__reports__/mcp-docs-refresh/docs-vs-codebase-gap-analysis.md) — findings 1a-1d, 2a-2d + +--- + +## Step 1: Update field support matrix and field mapping documentation + +**Goal**: Make the field support matrix and field mapping examples match the actual field constants in `fields.py` and the actual mapping dicts in adapter modules. + +**Implementation Logic**: + +1. Read `hatch/mcp_host_config/fields.py` and extract every per-host field set (`CLAUDE_FIELDS`, `VSCODE_FIELDS`, `CURSOR_FIELDS`, `GEMINI_FIELDS`, `KIRO_FIELDS`, `CODEX_FIELDS`, `LMSTUDIO_FIELDS`). +2. Rebuild the Field Support Matrix table in the architecture doc to include ALL fields present in any host's set. Add an LM Studio column. Add the missing Gemini OAuth fields (`oauth_enabled`, `oauth_clientId`, etc.) and missing Codex fields (`cwd`, `env_vars`, `startup_timeout_sec`, etc.). +3. Read `hatch/mcp_host_config/adapters/codex.py` and extract the actual `CODEX_FIELD_MAPPINGS` dict. Update the "Field Mappings (Optional)" section to show all 4 mappings, not just 2. +4. Update the `MCPServerConfig` model snippet to reflect the actual field set (currently shows `~12` fields with `# ... additional fields per host` — expand to show the full set or at minimum group by host with accurate counts). + +**Deliverables**: Updated Field Support Matrix section, updated Field Mappings section, updated model snippet in `docs/articles/devs/architecture/mcp_host_configuration.md` +**Consistency Checks**: Diff the field names in the matrix against `fields.py` constants — every field in every host set must appear in the matrix (expected: PASS) +**Commit**: `docs(mcp): update field support matrix and field mapping documentation` + +--- + +## Step 2: Document missing architectural patterns + +**Goal**: Add documentation for the strategy layer interface, the strategy registration decorator, and the Claude adapter variant pattern. + +**Implementation Logic**: + +1. Read `hatch/mcp_host_config/strategies.py` and extract the `MCPHostStrategy` base class interface (methods: `get_config_path()`, `is_host_available()`, `get_config_key()`, `read_configuration()`, `write_configuration()`, `validate_server_config()`). Also extract the `@register_host_strategy` decorator and explain its role in auto-registration. +2. Add a new subsection under "Key Components" (after BaseAdapter Protocol) titled "MCPHostStrategy Interface" that documents the strategy base class and its methods, analogous to the BaseAdapter Protocol section. +3. Read `hatch/mcp_host_config/adapters/claude.py` and document the variant pattern: a single `ClaudeAdapter` class serving both `claude-desktop` and `claude-code` via a `variant` constructor parameter. Explain this in the "Design Patterns" section. +4. In the BaseAdapter Protocol section, clarify the `validate()` deprecation: state that `validate()` is retained for backward compatibility but `validate_filtered()` is the current contract used by `serialize()`. The extension guide template (Step 2) should implement `validate_filtered()` as the primary validation path. +5. In the Error Handling section, update the example to use `validate_filtered()` instead of `validate()`. + +**Deliverables**: New "MCPHostStrategy Interface" subsection, updated "Design Patterns" section, clarified deprecation note in BaseAdapter Protocol, updated Error Handling example in `docs/articles/devs/architecture/mcp_host_configuration.md` +**Consistency Checks**: Verify every method documented in the MCPHostStrategy section exists in `hatch/mcp_host_config/strategies.py` (expected: PASS) +**Commit**: `docs(mcp): document strategy interface, registration decorator, and adapter variant pattern` + +--- + +## Step 3: Rewrite testing infrastructure section + +**Goal**: Replace the brief testing section with comprehensive documentation of the data-driven testing architecture. + +**Implementation Logic**: + +1. Read `tests/test_data/mcp_adapters/host_registry.py`, `assertions.py`, and `canonical_configs.json` to understand the full infrastructure. +2. Rewrite the "Testing Strategy" section to cover: + - **Three-tier table** (keep, but update test counts to ~285 total auto-generated). + - **Data-driven infrastructure** subsection: Explain the module at `tests/test_data/mcp_adapters/` with its three files and their roles. + - **`HostSpec` dataclass**: Document its attributes (`name`, `adapter`, `fields`, `field_mappings`) and key methods (`load_config()`, `get_adapter()`, `compute_expected_fields()`). + - **`HostRegistry` class**: Document how it derives metadata from `fields.py` at import time and provides `all_hosts()`, `get_host()`, `all_pairs()`, `hosts_supporting_field()`. + - **Generator functions**: Document `generate_sync_test_cases()`, `generate_validation_test_cases()`, `generate_unsupported_field_test_cases()` and how they feed `pytest.mark.parametrize`. + - **Assertion functions**: List the 8 `assert_*` functions in `assertions.py` and explain they encode adapter contracts as reusable property checks. + - **`canonical_configs.json` structure**: Show the JSON schema (host name -> field name -> value) and note the reverse mapping mechanism for Codex. +3. Fix the "zero test code changes" claim. Replace with accurate guidance: adding a new host requires (a) a new entry in `canonical_configs.json`, (b) adding the host's field set to `FIELD_SETS` in `host_registry.py`, and (c) updating `fields.py`. No changes to actual test files are needed — the generators pick up the new host automatically. +4. Acknowledge the two deprecated test files (`test_adapter_serialization.py`, `test_field_filtering.py`) with a note that they are `@pytest.mark.skip` and scheduled for removal in v0.9.0. + +**Deliverables**: Rewritten "Testing Strategy" section in `docs/articles/devs/architecture/mcp_host_configuration.md` +**Consistency Checks**: Verify every class/function name referenced in the testing section exists in `tests/test_data/mcp_adapters/` (expected: PASS) +**Commit**: `docs(mcp): rewrite testing section with data-driven infrastructure documentation` diff --git a/__roadmap__/mcp-docs-refresh/update_extension_guide.md b/__roadmap__/mcp-docs-refresh/update_extension_guide.md new file mode 100644 index 0000000..b2f8e0a --- /dev/null +++ b/__roadmap__/mcp-docs-refresh/update_extension_guide.md @@ -0,0 +1,68 @@ +# Update Extension Guide + +**Goal**: Bring `docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md` into alignment with the current codebase. +**Pre-conditions**: +- [ ] Branch `task/update-extension-guide` created from `milestone/mcp-docs-refresh` +**Success Gates**: +- Step 2 adapter template uses `validate_filtered()` in the `serialize()` method (not `validate()`) +- Step 3 strategy template includes `@register_host_strategy` decorator and documents `MCPHostStrategy` interface +- Step 4 documents the actual test data fixture requirements (`canonical_configs.json`, `host_registry.py`) +- "Testing Your Implementation" section cross-references the architecture doc's testing section +**References**: [R01 Gap Analysis](../../__reports__/mcp-docs-refresh/docs-vs-codebase-gap-analysis.md) — findings 1b, 2d + +--- + +## Step 1: Fix Step 2 adapter template to use validate_filtered() + +**Goal**: Replace the deprecated `validate()` pattern in the adapter template with the current `validate_filtered()` contract. + +**Implementation Logic**: + +1. In the Step 2 template code block, change the `serialize()` method from calling `self.validate(config)` to calling `self.filter_fields(config)` then `self.validate_filtered(filtered)` then returning the filtered result. This matches the actual pattern used by all current adapters. +2. Update the `validate()` method stub to include a docstring marking it as deprecated with a pointer to `validate_filtered()`. Keep it as a pass-through since it's still abstract in `BaseAdapter`. +3. Add a `validate_filtered()` method to the template with the transport validation logic that's currently only in `validate()`. +4. Update the "Interface" table at the top of the guide: change `validate()` to `validate_filtered()` in the Adapter row, or list both with a deprecation note. +5. In "Common Patterns" section, update the "Multiple Transport Support" and "Strict Single Transport" examples to use `validate_filtered(self, filtered)` signatures (checking `"command" in filtered` instead of `config.command is not None`). +6. In "Field Mappings (Optional)" section, update the `serialize()` example to use `validate_filtered()`. + +**Deliverables**: Updated Step 2 template, updated Common Patterns section, updated Field Mappings section in `docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md` +**Consistency Checks**: Confirm every adapter in `hatch/mcp_host_config/adapters/` uses `validate_filtered()` inside `serialize()` (expected: PASS) +**Commit**: `docs(mcp): update extension guide adapter template to use validate_filtered()` + +--- + +## Step 2: Update Step 3 strategy template with registration decorator + +**Goal**: Document the `@register_host_strategy` decorator and the `MCPHostStrategy` base class interface in the strategy template. + +**Implementation Logic**: + +1. The Step 3 template already shows `@register_host_strategy(MCPHostType.YOUR_HOST)` — verify it's correct and add a brief explanation of what the decorator does (registers the strategy in a global dict so `get_strategy_for_host()` can look it up). +2. Add a brief list of `MCPHostStrategy` methods that can be overridden vs inherited. Currently the template shows `get_config_path()`, `is_host_available()`, `get_config_key()` but doesn't mention `read_configuration()`, `write_configuration()`, or `validate_server_config()`. Add a table showing which methods typically need overriding vs which inherit well from base/family classes. +3. Cross-reference the architecture doc's "MCPHostStrategy Interface" subsection. + +**Deliverables**: Updated Step 3 section in `docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md` +**Consistency Checks**: Verify the decorator name and signature match `hatch/mcp_host_config/strategies.py` (expected: PASS) +**Commit**: `docs(mcp): update extension guide strategy template with interface documentation` + +--- + +## Step 3: Rewrite Step 4 and Testing Your Implementation section + +**Goal**: Replace the current testing guidance with accurate documentation of the data-driven testing infrastructure requirements. + +**Implementation Logic**: + +1. Rewrite Step 4 to explain what's actually needed when adding a new host: + - **a)** Add a fixture entry to `tests/test_data/mcp_adapters/canonical_configs.json` — show the JSON structure (host name key, field-name-to-value mapping using host-native field names). + - **b)** Add the host's field set to `FIELD_SETS` in `tests/test_data/mcp_adapters/host_registry.py` — one line mapping host name to the `fields.py` constant. + - **c)** If the host uses field mappings (like Codex), add reverse mappings to `REVERSE_MAPPINGS` in `host_registry.py`. + - **d)** Explain that the generator functions (`generate_sync_test_cases`, `generate_validation_test_cases`, `generate_unsupported_field_test_cases`) will automatically pick up the new host and generate parameterized test cases. No changes to test files themselves. +2. Remove the misleading unit test template (Step 4 currently shows a `TestYourHostAdapter` class with handwritten test methods). Replace with a note that unit tests for adapter protocol compliance, field filtering, and cross-host sync are all auto-generated. Only add bespoke unit tests if the adapter has unusual behavior (e.g., complex field transformations). +3. Update the "Testing Your Implementation" section to cross-reference the architecture doc's testing section. Replace the "Test Categories" table with a table showing what's auto-generated vs what needs manual tests. +4. Update the "Test File Location" tree to include `tests/test_data/mcp_adapters/` and show the actual regression test directory. +5. Fix the "zero test code changes" claim in both the extension guide and any cross-references. State the accurate requirement: fixture data updates in `canonical_configs.json` and `host_registry.py`, but zero changes to test functions. + +**Deliverables**: Rewritten Step 4 section, rewritten "Testing Your Implementation" section, updated test file tree in `docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md` +**Consistency Checks**: Follow the documented steps mentally for a hypothetical new host and verify each instruction points to real files that exist in the codebase (expected: PASS) +**Commit**: `docs(mcp): rewrite extension guide testing section with data-driven infrastructure` diff --git a/cracking-shells-playbook b/cracking-shells-playbook index fd768bf..afc6c1e 160000 --- a/cracking-shells-playbook +++ b/cracking-shells-playbook @@ -1 +1 @@ -Subproject commit fd768bf6bc67c1ce916552521f781826acc65926 +Subproject commit afc6c1ed54aaa01e4666d3c2837fc9a39dcb25e8 diff --git a/docs/articles/devs/architecture/mcp_host_configuration.md b/docs/articles/devs/architecture/mcp_host_configuration.md index 79cfc4b..d8a7977 100644 --- a/docs/articles/devs/architecture/mcp_host_configuration.md +++ b/docs/articles/devs/architecture/mcp_host_configuration.md @@ -63,9 +63,9 @@ class MCPServerConfig(BaseModel): name: Optional[str] = None # Transport fields - command: Optional[str] = None # stdio transport - url: Optional[str] = None # sse transport - httpUrl: Optional[str] = None # http transport (Gemini) + command: Optional[str] = None # stdio transport + url: Optional[str] = None # sse transport + httpUrl: Optional[str] = None # http transport (Gemini) # Universal fields (all hosts) args: Optional[List[str]] = None @@ -73,11 +73,42 @@ class MCPServerConfig(BaseModel): headers: Optional[Dict[str, str]] = None type: Optional[Literal["stdio", "sse", "http"]] = None - # Host-specific fields - envFile: Optional[str] = None # VSCode/Cursor - disabled: Optional[bool] = None # Kiro - trust: Optional[bool] = None # Gemini - # ... additional fields per host + # VSCode/Cursor fields + envFile: Optional[str] = None # Path to environment file + inputs: Optional[List[Dict]] = None # Input variable definitions (VSCode only) + + # Gemini fields (16 total including OAuth) + cwd: Optional[str] = None # Working directory (Gemini/Codex) + timeout: Optional[int] = None # Request timeout in milliseconds + trust: Optional[bool] = None # Bypass tool call confirmations + includeTools: Optional[List[str]] = None # Tools to include (allowlist) + excludeTools: Optional[List[str]] = None # Tools to exclude (blocklist) + oauth_enabled: Optional[bool] = None # Enable OAuth for this server + oauth_clientId: Optional[str] = None # OAuth client identifier + oauth_clientSecret: Optional[str] = None # OAuth client secret + oauth_authorizationUrl: Optional[str] = None # OAuth authorization endpoint + oauth_tokenUrl: Optional[str] = None # OAuth token endpoint + oauth_scopes: Optional[List[str]] = None # Required OAuth scopes + oauth_redirectUri: Optional[str] = None # Custom redirect URI + oauth_tokenParamName: Optional[str] = None # Query parameter name for tokens + oauth_audiences: Optional[List[str]] = None # OAuth audiences + authProviderType: Optional[str] = None # Authentication provider type + + # Kiro fields + disabled: Optional[bool] = None # Whether server is disabled + autoApprove: Optional[List[str]] = None # Auto-approved tool names + disabledTools: Optional[List[str]] = None # Disabled tool names + + # Codex fields (10 host-specific) + env_vars: Optional[List[str]] = None # Environment variables to whitelist/forward + startup_timeout_sec: Optional[int] = None # Server startup timeout + tool_timeout_sec: Optional[int] = None # Tool execution timeout + enabled: Optional[bool] = None # Enable/disable server + enabled_tools: Optional[List[str]] = None # Allow-list of tools + disabled_tools: Optional[List[str]] = None # Deny-list of tools + bearer_token_env_var: Optional[str] = None # Env var containing bearer token + http_headers: Optional[Dict[str, str]] = None # HTTP headers (Codex naming) + env_http_headers: Optional[Dict[str, str]] = None # Header names to env var names ``` **Design principles:** @@ -148,8 +179,18 @@ class BaseAdapter(ABC): def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: """Convert config to host's expected format.""" ... + + def filter_fields(self, config: MCPServerConfig) -> Dict[str, Any]: + """Filter config to only include supported, non-excluded, non-None fields.""" + ... + + def get_excluded_fields(self) -> FrozenSet[str]: + """Return fields that should always be excluded (default: EXCLUDED_ALWAYS).""" + ... ``` +**Validation migration note:** `validate()` is retained as an abstract method for backward compatibility, but `validate_filtered()` is the current contract used by `serialize()`. All existing adapters implement both methods, but new adapters should implement `validate_filtered()` as the primary validation path. The `validate()` method will be removed in v0.9.0. + **Serialization pattern (validate-after-filter):** ``` @@ -159,24 +200,102 @@ filter_fields(config) → validate_filtered(filtered) → apply_transformations( This pattern ensures validation only checks fields the host actually supports, preventing false rejections during cross-host sync operations. +### MCPHostStrategy Interface + +The strategy layer handles file I/O and host detection. All strategy classes inherit from `MCPHostStrategy` (defined in `host_management.py`) and are auto-registered using the `@register_host_strategy` decorator: + +```python +class MCPHostStrategy: + """Abstract base class for host configuration strategies.""" + + def get_config_path(self) -> Optional[Path]: + """Get configuration file path for this host.""" + ... + + def is_host_available(self) -> bool: + """Check if host is available on system.""" + ... + + def get_config_key(self) -> str: + """Get the root configuration key for MCP servers (default: 'mcpServers').""" + ... + + def read_configuration(self) -> HostConfiguration: + """Read and parse host configuration.""" + ... + + def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool: + """Write configuration to host file.""" + ... + + def validate_server_config(self, server_config: MCPServerConfig) -> bool: + """Validate server configuration for this host.""" + ... +``` + +**Auto-registration with `@register_host_strategy`:** + +The `@register_host_strategy` decorator (a convenience wrapper around `MCPHostRegistry.register()`) registers a strategy class at import time. When `strategies.py` is imported, each decorated class is automatically added to the `MCPHostRegistry`, making it available via `MCPHostRegistry.get_strategy(host_type)`: + +```python +from hatch.mcp_host_config.host_management import MCPHostStrategy, register_host_strategy +from hatch.mcp_host_config.models import MCPHostType + +@register_host_strategy(MCPHostType.YOUR_HOST) +class YourHostStrategy(MCPHostStrategy): + def get_config_path(self) -> Optional[Path]: + return Path.home() / ".your-host" / "config.json" + + def is_host_available(self) -> bool: + return self.get_config_path().parent.exists() + + # ... remaining methods +``` + +This decorator-based registration follows the same pattern used throughout Hatch. No manual registry wiring is needed — adding the decorator is sufficient. + +**Strategy families:** + +Some strategies share implementation through base classes: + +- `ClaudeHostStrategy`: Base for `ClaudeDesktopStrategy` and `ClaudeCodeStrategy` (shared JSON read/write, `_preserve_claude_settings()`) +- `CursorBasedHostStrategy`: Base for `CursorHostStrategy` and `LMStudioHostStrategy` (shared Cursor-format JSON read/write) + ### Field Constants -Field support is defined in `fields.py`: +Field support is defined in `fields.py` as the single source of truth. Every host's field set is built by extending `UNIVERSAL_FIELDS` with host-specific additions: ```python -# Universal fields (all hosts) +# Universal fields (supported by ALL hosts) — 5 fields UNIVERSAL_FIELDS = frozenset({"command", "args", "env", "url", "headers"}) -# Host-specific field sets -CLAUDE_FIELDS = UNIVERSAL_FIELDS | frozenset({"type"}) -VSCODE_FIELDS = CLAUDE_FIELDS | frozenset({"envFile", "inputs"}) -GEMINI_FIELDS = UNIVERSAL_FIELDS | frozenset({"httpUrl", "timeout", "trust", ...}) -KIRO_FIELDS = UNIVERSAL_FIELDS | frozenset({"disabled", "autoApprove", ...}) +# Hosts that support the 'type' discriminator field +TYPE_SUPPORTING_HOSTS = frozenset({"claude-desktop", "claude-code", "vscode", "cursor"}) + +# Host-specific field sets — 7 constants, 8 hosts +CLAUDE_FIELDS = UNIVERSAL_FIELDS | {"type"} # 6 fields +VSCODE_FIELDS = CLAUDE_FIELDS | {"envFile", "inputs"} # 8 fields +CURSOR_FIELDS = CLAUDE_FIELDS | {"envFile"} # 7 fields +LMSTUDIO_FIELDS = CLAUDE_FIELDS # 6 fields (alias) +GEMINI_FIELDS = UNIVERSAL_FIELDS | {"httpUrl", "timeout", "trust", "cwd", + "includeTools", "excludeTools", + "oauth_enabled", "oauth_clientId", "oauth_clientSecret", + "oauth_authorizationUrl", "oauth_tokenUrl", "oauth_scopes", + "oauth_redirectUri", "oauth_tokenParamName", + "oauth_audiences", "authProviderType"} # 21 fields +KIRO_FIELDS = UNIVERSAL_FIELDS | {"disabled", "autoApprove", + "disabledTools"} # 8 fields +CODEX_FIELDS = UNIVERSAL_FIELDS | {"cwd", "env_vars", "startup_timeout_sec", + "tool_timeout_sec", "enabled", "enabled_tools", + "disabled_tools", "bearer_token_env_var", + "http_headers", "env_http_headers"} # 15 fields # Metadata fields (never serialized or reported) EXCLUDED_ALWAYS = frozenset({"name"}) ``` +Note that `LMSTUDIO_FIELDS` is a direct alias for `CLAUDE_FIELDS` — LM Studio supports the same field set as Claude Desktop and Claude Code. + ### Reporting System The reporting system (`reporting.py`) provides user-friendly feedback for MCP configuration operations. It respects adapter exclusion semantics to ensure consistency between what's reported and what's actually written to host configuration files. @@ -210,17 +329,53 @@ This ensures that: ## Field Support Matrix -| Field | Claude | VSCode | Cursor | Gemini | Kiro | Codex | -|-------|--------|--------|--------|--------|------|-------| -| command, args, env | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| url, headers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| type | ✓ | ✓ | ✓ | - | - | - | -| envFile | - | ✓ | ✓ | - | - | - | -| inputs | - | ✓ | - | - | - | - | -| httpUrl | - | - | - | ✓ | - | - | -| trust, timeout | - | - | - | ✓ | - | - | -| disabled, autoApprove | - | - | - | - | ✓ | - | -| enabled, enabled_tools | - | - | - | - | - | ✓ | +The matrix below lists every field present in any host's field set (defined in `fields.py`). Claude Desktop, Claude Code, and LM Studio share the same field set (`CLAUDE_FIELDS`), so LM Studio is shown in its own column to make this explicit. + +| Field | Claude Desktop/Code | VSCode | Cursor | LM Studio | Gemini | Kiro | Codex | +|-------|:-------------------:|:------:|:------:|:---------:|:------:|:----:|:-----:| +| **Universal fields** | | | | | | | | +| command | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| args | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| env | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| url | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| headers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| **Transport discriminator** | | | | | | | | +| type | ✓ | ✓ | ✓ | ✓ | - | - | - | +| **VSCode/Cursor fields** | | | | | | | | +| envFile | - | ✓ | ✓ | - | - | - | - | +| inputs | - | ✓ | - | - | - | - | - | +| **Gemini fields** | | | | | | | | +| httpUrl | - | - | - | - | ✓ | - | - | +| timeout | - | - | - | - | ✓ | - | - | +| trust | - | - | - | - | ✓ | - | - | +| cwd | - | - | - | - | ✓ | - | ✓ | +| includeTools | - | - | - | - | ✓ | - | - | +| excludeTools | - | - | - | - | ✓ | - | - | +| **Gemini OAuth fields** | | | | | | | | +| oauth_enabled | - | - | - | - | ✓ | - | - | +| oauth_clientId | - | - | - | - | ✓ | - | - | +| oauth_clientSecret | - | - | - | - | ✓ | - | - | +| oauth_authorizationUrl | - | - | - | - | ✓ | - | - | +| oauth_tokenUrl | - | - | - | - | ✓ | - | - | +| oauth_scopes | - | - | - | - | ✓ | - | - | +| oauth_redirectUri | - | - | - | - | ✓ | - | - | +| oauth_tokenParamName | - | - | - | - | ✓ | - | - | +| oauth_audiences | - | - | - | - | ✓ | - | - | +| authProviderType | - | - | - | - | ✓ | - | - | +| **Kiro fields** | | | | | | | | +| disabled | - | - | - | - | - | ✓ | - | +| autoApprove | - | - | - | - | - | ✓ | - | +| disabledTools | - | - | - | - | - | ✓ | - | +| **Codex fields** | | | | | | | | +| env_vars | - | - | - | - | - | - | ✓ | +| startup_timeout_sec | - | - | - | - | - | - | ✓ | +| tool_timeout_sec | - | - | - | - | - | - | ✓ | +| enabled | - | - | - | - | - | - | ✓ | +| enabled_tools | - | - | - | - | - | - | ✓ | +| disabled_tools | - | - | - | - | - | - | ✓ | +| bearer_token_env_var | - | - | - | - | - | - | ✓ | +| http_headers | - | - | - | - | - | - | ✓ | +| env_http_headers | - | - | - | - | - | - | ✓ | ## Integration Points @@ -345,15 +500,49 @@ The base class provides `filter_fields()` which: ### Field Mappings (Optional) -If your host uses different field names: +If your host uses different field names, define a mapping dict in `fields.py`. During serialization, the adapter's `apply_transformations()` method renames fields from the universal schema to the host-native names. Codex is currently the only host that requires this: ```python CODEX_FIELD_MAPPINGS = { - "args": "arguments", # Universal → Codex naming - "headers": "http_headers", # Universal → Codex naming + "args": "arguments", # Universal → Codex naming + "headers": "http_headers", # Universal → Codex naming + "includeTools": "enabled_tools", # Gemini naming → Codex naming (cross-host sync) + "excludeTools": "disabled_tools", # Gemini naming → Codex naming (cross-host sync) } ``` +The last two entries (`includeTools` -> `enabled_tools`, `excludeTools` -> `disabled_tools`) enable transparent cross-host sync from Gemini to Codex: a Gemini config containing `includeTools` will be serialized as `enabled_tools` in the Codex output. + +### Adapter Variant Pattern + +When two hosts share the same field set and validation logic but differ only in identity, a single adapter class can serve both via a `variant` constructor parameter. This avoids code duplication without introducing an inheritance hierarchy. + +`ClaudeAdapter` demonstrates this pattern. Claude Desktop and Claude Code share identical field support (`CLAUDE_FIELDS`) and validation rules, so a single class handles both: + +```python +class ClaudeAdapter(BaseAdapter): + def __init__(self, variant: str = "desktop"): + if variant not in ("desktop", "code"): + raise ValueError(f"Invalid Claude variant: {variant}") + self._variant = variant + + @property + def host_name(self) -> str: + return f"claude-{self._variant}" # "claude-desktop" or "claude-code" + + def get_supported_fields(self) -> FrozenSet[str]: + return CLAUDE_FIELDS # Same field set for both variants +``` + +The `AdapterRegistry` registers two entries pointing to different instances of the same class: + +```python +ClaudeAdapter(variant="desktop") # registered as "claude-desktop" +ClaudeAdapter(variant="code") # registered as "claude-code" +``` + +Use this pattern when adding a new host that is functionally identical to an existing one but requires a distinct host name in the registry. + ### Atomic Operations Pattern All configuration changes use atomic operations: @@ -396,26 +585,137 @@ The system uses both exceptions and result objects: ```python try: - adapter.validate(config) + filtered = adapter.filter_fields(config) + adapter.validate_filtered(filtered) except AdapterValidationError as e: print(f"Validation failed: {e.message}") print(f"Field: {e.field}, Host: {e.host_name}") ``` +In practice, calling `adapter.serialize(config)` is preferred since it executes the full filter-validate-transform pipeline and will raise `AdapterValidationError` on validation failure. + ## Testing Strategy -The test architecture uses a data-driven approach with property-based assertions: +The test architecture uses a data-driven approach with property-based assertions. Approximately 285 test cases are auto-generated from metadata in `fields.py` and fixture data in `canonical_configs.json`. + +### Three-Tier Test Structure | Tier | Location | Purpose | Approach | |------|----------|---------|----------| | Unit | `tests/unit/mcp/` | Adapter protocol, model validation, registry | Traditional | | Integration | `tests/integration/mcp/` | Cross-host sync (64 pairs), host config (8 hosts) | Data-driven | -| Regression | `tests/regression/mcp/` | Validation bugs, field filtering (211+ tests) | Data-driven | +| Regression | `tests/regression/mcp/` | Validation bugs, field filtering (~285 auto-generated) | Data-driven | + +### Data-Driven Infrastructure + +The module at `tests/test_data/mcp_adapters/` contains three files that form the data-driven test infrastructure: + +| File | Role | +|------|------| +| `canonical_configs.json` | Fixture data: canonical config values for all 8 hosts | +| `host_registry.py` | Registry: derives host metadata from `fields.py`, generates test cases | +| `assertions.py` | Assertions: reusable property checks encoding adapter contracts | + +### `HostSpec` Dataclass + +`HostSpec` is the per-host test specification. It combines minimal fixture data (config values) with complete metadata derived from `fields.py`: + +```python +@dataclass +class HostSpec: + host_name: str # e.g., "claude-desktop", "codex" + canonical_config: Dict[str, Any] # Raw config values from fixture (host-native names) + supported_fields: FrozenSet[str] # From fields.py (e.g., CLAUDE_FIELDS) + field_mappings: Dict[str, str] # From fields.py (e.g., CODEX_FIELD_MAPPINGS) +``` + +Key methods: + +- `load_config()` -- Builds an `MCPServerConfig` from canonical config values, applying reverse field mappings for hosts with non-standard names (e.g., Codex `arguments` -> `args`) +- `get_adapter()` -- Instantiates the correct adapter for this host (handles `ClaudeAdapter` variant dispatch) +- `compute_expected_fields(input_fields)` -- Returns `(input_fields & supported_fields) - EXCLUDED_ALWAYS`, predicting which fields should survive filtering + +### `HostRegistry` Class + +`HostRegistry` bridges fixture data with `fields.py` metadata. At construction time, it loads `canonical_configs.json` and derives each host's `HostSpec` by looking up the corresponding field set in the `FIELD_SETS` mapping (which maps host names to `fields.py` constants like `CLAUDE_FIELDS`, `GEMINI_FIELDS`, etc.): + +```python +registry = HostRegistry(Path("tests/test_data/mcp_adapters/canonical_configs.json")) +``` + +Methods: + +- `all_hosts()` -- Returns all `HostSpec` instances sorted by name +- `get_host(name)` -- Returns a specific `HostSpec` by host name +- `all_pairs()` -- Generates all `(from_host, to_host)` combinations for O(n^2) cross-host sync testing (8 x 8 = 64 pairs) +- `hosts_supporting_field(field_name)` -- Finds hosts that support a specific field (e.g., all hosts supporting `httpUrl`) + +### Generator Functions + +Three generator functions create parameterized test cases from registry data. These are called at module level and fed directly to `pytest.mark.parametrize`: + +- `generate_sync_test_cases(registry)` -- Produces one `SyncTestCase` per (from, to) host pair (64 cases for 8 hosts) +- `generate_validation_test_cases(registry)` -- Produces `ValidationTestCase` entries for transport mutual exclusion (all hosts) and tool list coexistence (hosts with tool list support) +- `generate_unsupported_field_test_cases(registry)` -- For each host, computes the set of fields it does NOT support (from the union of all host field sets) and produces one `FilterTestCase` per unsupported field + +### Assertion Functions + +The `assertions.py` module contains 7 `assert_*` functions that encode adapter contracts as reusable property checks. Tests call these functions instead of writing inline assertions: + +| Function | Contract Verified | +|----------|-------------------| +| `assert_only_supported_fields()` | Result contains only fields from `fields.py` for this host (including mapped names) | +| `assert_excluded_fields_absent()` | `EXCLUDED_ALWAYS` fields (e.g., `name`) are not in result | +| `assert_transport_present()` | At least one transport field (`command`, `url`, `httpUrl`) is present | +| `assert_transport_mutual_exclusion()` | Exactly one transport field is present | +| `assert_field_mappings_applied()` | Universal field names are replaced by host-native names (e.g., no `args` in Codex output) | +| `assert_tool_lists_coexist()` | Both allowlist and denylist fields are present when applicable | +| `assert_unsupported_field_absent()` | A specific unsupported field was filtered out | + +### `canonical_configs.json` Structure + +The fixture file uses a flat JSON schema mapping host names to field-value pairs: + +```json +{ + "claude-desktop": { + "command": "python", + "args": ["-m", "mcp_server"], + "env": {"API_KEY": "test_key"}, + "url": null, + "headers": null, + "type": "stdio" + }, + "codex": { + "command": "python", + "arguments": ["-m", "mcp_server"], + "env": {"API_KEY": "test_key"}, + "url": null, + "http_headers": null, + "cwd": "/app", + "enabled_tools": ["tool1", "tool2"], + "disabled_tools": ["tool3"] + } +} +``` + +Note that Codex entries use host-native field names (e.g., `arguments` instead of `args`, `http_headers` instead of `headers`). The `HostSpec.load_config()` method applies reverse mappings (`CODEX_REVERSE_MAPPINGS`) to convert these back to universal names when constructing `MCPServerConfig` objects. + +### Adding a New Host to Tests + +Adding a new host does not require changes to any test files. The generators automatically pick up the new host. The required steps are: + +1. Add a new entry in `canonical_configs.json` with representative config values using the host's native field names +2. Add the host's field set to the `FIELD_SETS` mapping in `host_registry.py` (mapping the host name to the corresponding constant from `fields.py`) +3. Update `fields.py` with the new host's field set constant + +No changes to actual test files (`test_cross_host_sync.py`, `test_host_configuration.py`, etc.) are needed -- the generators pick up the new host automatically via the registry. + +### Deprecated Test Files -**Data-driven infrastructure** (`tests/test_data/mcp_adapters/`): +Two legacy test files are marked with `@pytest.mark.skip` and scheduled for removal in v0.9.0: -- `canonical_configs.json`: Canonical config values for all 8 hosts -- `host_registry.py`: HostRegistry derives metadata from fields.py -- `assertions.py`: Property-based assertions verify adapter contracts +- `tests/integration/mcp/test_adapter_serialization.py` -- Replaced by `test_host_configuration.py` (per-host) and `test_cross_host_sync.py` (cross-host) +- `tests/regression/mcp/test_field_filtering.py` -- Replaced by `test_field_filtering_v2.py` (data-driven) -Adding a new host requires zero test code changes — only a fixture entry and fields.py update. +These files remain in the codebase for reference during the migration period but are not executed in CI. diff --git a/docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md b/docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md index 101be90..8f32061 100644 --- a/docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md +++ b/docs/articles/devs/implementation_guides/mcp_host_configuration_extension.md @@ -11,7 +11,7 @@ The Unified Adapter Architecture requires only **4 integration points**: | ☐ Host type enum | Always | `models.py` | | ☐ Adapter class | Always | `adapters/your_host.py`, `adapters/__init__.py` | | ☐ Strategy class | Always | `strategies.py` | -| ☐ Test infrastructure | Always | `tests/unit/mcp/`, `tests/integration/mcp/` | +| ☐ Test fixtures | Always | `tests/test_data/mcp_adapters/canonical_configs.json`, `host_registry.py` | > **Note:** No host-specific models, no `from_omni()` conversion, no model registry integration. The unified model handles all fields. @@ -30,9 +30,11 @@ The Unified Adapter Architecture separates concerns: | Component | Responsibility | Interface | |-----------|----------------|-----------| -| **Adapter** | Validation + Serialization | `validate()`, `serialize()`, `get_supported_fields()` | +| **Adapter** | Validation + Serialization | `validate_filtered()`, `serialize()`, `get_supported_fields()` | | **Strategy** | File I/O | `read_configuration()`, `write_configuration()`, `get_config_path()` | +> **Note:** `validate()` is deprecated (will be removed in v0.9.0). All new adapters should implement `validate_filtered()` for the validate-after-filter pattern. See [Architecture Doc](../architecture/mcp_host_configuration.md#baseadapter-protocol) for details. + ``` MCPServerConfig (unified model) │ @@ -92,22 +94,44 @@ class YourHostAdapter(BaseAdapter): }) def validate(self, config: MCPServerConfig) -> None: - """Validate configuration for Your Host.""" - # Check transport requirements - if not config.command and not config.url: + """DEPRECATED: Will be removed in v0.9.0. Use validate_filtered() instead. + + Still required by BaseAdapter's abstract interface. Implement as a + pass-through until the abstract method is removed. + """ + pass + + def validate_filtered(self, filtered: Dict[str, Any]) -> None: + """Validate ONLY fields that survived filtering. + + This is the primary validation method. It receives a dictionary + of fields that have already been filtered to only those this host + supports, with None values and excluded fields removed. + """ + has_command = "command" in filtered + has_url = "url" in filtered + + if not has_command and not has_url: raise AdapterValidationError( "Either 'command' (local) or 'url' (remote) required", - host_name=self.host_name + host_name=self.host_name, ) # Add any host-specific validation - # if config.command and config.url: + # if has_command and has_url: # raise AdapterValidationError("Cannot have both", ...) def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: - """Serialize configuration for Your Host format.""" - self.validate(config) - return self.filter_fields(config) + """Serialize configuration for Your Host format. + + Follows the validate-after-filter pattern: + 1. Filter to supported fields + 2. Validate filtered fields + 3. Return filtered (or apply transformations if needed) + """ + filtered = self.filter_fields(config) + self.validate_filtered(filtered) + return filtered ``` **Then register in `hatch/mcp_host_config/adapters/__init__.py`:** @@ -153,51 +177,140 @@ class YourHostStrategy(MCPHostStrategy): """Return the key containing MCP servers.""" return "mcpServers" # Most hosts use this - # read_configuration() and write_configuration() - # can inherit from a base class or implement from scratch + def get_adapter_host_name(self) -> str: + """Return the adapter host name for registry lookup.""" + return "your-host" + + def validate_server_config(self, server_config: MCPServerConfig) -> bool: + """Basic transport validation before adapter processing.""" + return server_config.command is not None or server_config.url is not None + + def read_configuration(self) -> HostConfiguration: + """Read and parse host configuration file.""" + # Implement JSON/TOML parsing for your host's config format + ... + + def write_configuration( + self, config: HostConfiguration, no_backup: bool = False + ) -> bool: + """Write configuration using adapter serialization.""" + # Use get_adapter(self.get_adapter_host_name()) for serialization + ... ``` +**The `@register_host_strategy` decorator** registers the strategy class in a global dictionary (`MCPHostRegistry._strategies`) keyed by `MCPHostType`. This enables `MCPHostRegistry.get_strategy(host_type)` to look up and instantiate the correct strategy at runtime. The decorator is defined in `host_management.py` as a convenience wrapper around `MCPHostRegistry.register()`. + +#### MCPHostStrategy Interface + +The base `MCPHostStrategy` class (defined in `host_management.py`) provides the full strategy interface. The table below shows which methods typically need overriding vs which can be inherited from family base classes. + +| Method | Must Override | Can Inherit | Notes | +|--------|:------------:|:-----------:|-------| +| `get_config_path()` | Always | -- | Platform-specific path to config file | +| `is_host_available()` | Always | -- | Check if host is installed on system | +| `get_config_key()` | Usually | From family | Most hosts use `"mcpServers"` (default) | +| `get_adapter_host_name()` | Usually | From family | Maps strategy to adapter registry entry | +| `validate_server_config()` | Usually | From family | Basic transport presence check | +| `read_configuration()` | Sometimes | From family | JSON read is identical across families | +| `write_configuration()` | Sometimes | From family | JSON write with adapter serialization | + +> **Cross-reference:** See the [Architecture Doc -- MCPHostStrategy](../architecture/mcp_host_configuration.md#key-components) for the full interface specification. + **Inheriting from existing strategy families:** +If your host uses a standard JSON format, inherit from an existing family base class to get `read_configuration()`, `write_configuration()`, and shared validation for free: + ```python -# If similar to Claude (standard JSON format) +# If similar to Claude (standard JSON format with mcpServers key) +@register_host_strategy(MCPHostType.YOUR_HOST) class YourHostStrategy(ClaudeHostStrategy): def get_config_path(self) -> Optional[Path]: return Path.home() / ".your_host" / "config.json" + def is_host_available(self) -> bool: + return self.get_config_path().parent.exists() + # If similar to Cursor (flexible path handling) +@register_host_strategy(MCPHostType.YOUR_HOST) class YourHostStrategy(CursorBasedHostStrategy): def get_config_path(self) -> Optional[Path]: return Path.home() / ".your_host" / "config.json" + + def is_host_available(self) -> bool: + return self.get_config_path().parent.exists() +``` + +### Step 4: Register Test Fixtures + +Hatch uses a **data-driven test infrastructure** that auto-generates parameterized tests for all adapters. Adding a new host requires fixture data updates, but **zero changes to test functions** themselves. + +#### a) Add canonical config to `tests/test_data/mcp_adapters/canonical_configs.json` + +Add an entry keyed by your host name, using **host-native field names** (i.e., the names your host's config file uses, after any field mappings). Values should represent a valid stdio-transport configuration: + +```json +{ + "your-host": { + "command": "python", + "args": ["-m", "mcp_server"], + "env": {"API_KEY": "test_key"}, + "url": null, + "headers": null, + "type": "stdio" + } +} +``` + +For hosts with field mappings (like Codex, which uses `arguments` instead of `args`), use the host-native names in the fixture: + +```json +{ + "codex": { + "command": "python", + "arguments": ["-m", "mcp_server"], + "env": {"API_KEY": "test_key"}, + "url": null, + "http_headers": null + } +} +``` + +#### b) Add field set to `FIELD_SETS` in `tests/test_data/mcp_adapters/host_registry.py` + +Map your host name to its field set constant from `fields.py`: + +```python +FIELD_SETS: Dict[str, FrozenSet[str]] = { + # ... existing hosts ... + "your-host": YOUR_HOST_FIELDS, +} ``` -### Step 4: Add Tests +#### c) Add reverse mappings if needed -**Unit tests** (`tests/unit/mcp/test_your_host_adapter.py`): +If your host uses field mappings (like Codex), add the reverse mappings so `HostSpec.load_config()` can convert host-native names back to `MCPServerConfig` field names: ```python -class TestYourHostAdapter(unittest.TestCase): - def setUp(self): - self.adapter = YourHostAdapter() - - def test_host_name(self): - self.assertEqual(self.adapter.host_name, "your-host") - - def test_supported_fields(self): - fields = self.adapter.get_supported_fields() - self.assertIn("command", fields) - - def test_validate_requires_transport(self): - config = MCPServerConfig(name="test") - with self.assertRaises(AdapterValidationError): - self.adapter.validate(config) - - def test_serialize_filters_unsupported(self): - config = MCPServerConfig(name="test", command="python", httpUrl="http://x") - result = self.adapter.serialize(config) - self.assertNotIn("httpUrl", result) # Assuming not supported +# Already defined for Codex: +CODEX_REVERSE_MAPPINGS: Dict[str, str] = {v: k for k, v in CODEX_FIELD_MAPPINGS.items()} + +# Add similar for your host if it has field mappings ``` +#### d) Auto-generated test coverage + +Once you add the fixture entry and field set mapping, the generator functions in `host_registry.py` will automatically pick up your new host and generate parameterized test cases: + +| Generator Function | What It Generates | Coverage | +|--------------------|-------------------|----------| +| `generate_sync_test_cases()` | All cross-host sync pairs (N x N) | Your host syncing to/from every other host | +| `generate_validation_test_cases()` | Transport mutual exclusion, tool list coexistence | Validation contract tests for your host | +| `generate_unsupported_field_test_cases()` | One test per unsupported field | Verifies your adapter filters correctly | + +No changes to test files (`test_cross_host_sync.py`, `test_field_filtering.py`, etc.) are needed. The tests consume data from the registry and assertions library. + +> **When to add bespoke tests:** Only write custom unit tests if your adapter has unusual behavior not covered by the data-driven infrastructure (e.g., complex field transformations, multi-step validation, variant support like `ClaudeAdapter`'s desktop/code split). + ## Declaring Field Support ### Using Field Constants @@ -249,19 +362,22 @@ mcp_configure_parser.add_argument( ## Field Mappings (Optional) -If your host uses different names for standard fields: +If your host uses different names for standard fields, override `apply_transformations()`: ```python # In your adapter -def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: - self.validate(config) - result = self.filter_fields(config) - - # Apply mappings (e.g., 'args' → 'arguments') +def apply_transformations(self, filtered: Dict[str, Any]) -> Dict[str, Any]: + """Apply field name mappings after validation.""" + result = filtered.copy() if "args" in result: result["arguments"] = result.pop("args") - return result + +def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: + filtered = self.filter_fields(config) + self.validate_filtered(filtered) + transformed = self.apply_transformations(filtered) + return transformed ``` Or define mappings centrally in `fields.py`: @@ -280,17 +396,21 @@ YOUR_HOST_FIELD_MAPPINGS = { Some hosts (like Gemini) support multiple transports: ```python -def validate(self, config: MCPServerConfig) -> None: - transports = sum([ - config.command is not None, - config.url is not None, - config.httpUrl is not None, - ]) - - if transports == 0: +def validate_filtered(self, filtered: Dict[str, Any]) -> None: + has_command = "command" in filtered + has_url = "url" in filtered + has_http_url = "httpUrl" in filtered + + transport_count = sum([has_command, has_url, has_http_url]) + + if transport_count == 0: raise AdapterValidationError("At least one transport required") - # Allow multiple transports if your host supports it + # Gemini requires exactly one transport (not multiple) + if transport_count > 1: + raise AdapterValidationError( + "Only one transport allowed: command, url, or httpUrl" + ) ``` ### Strict Single Transport @@ -298,9 +418,9 @@ def validate(self, config: MCPServerConfig) -> None: Some hosts (like Claude) require exactly one transport: ```python -def validate(self, config: MCPServerConfig) -> None: - has_command = config.command is not None - has_url = config.url is not None +def validate_filtered(self, filtered: Dict[str, Any]) -> None: + has_command = "command" in filtered + has_url = "url" in filtered if not has_command and not has_url: raise AdapterValidationError("Need command or url") @@ -315,35 +435,61 @@ Override `serialize()` for custom output format: ```python def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: - self.validate(config) - result = self.filter_fields(config) + filtered = self.filter_fields(config) + self.validate_filtered(filtered) # Transform to your host's expected structure - if config.type == "stdio": - result["transport"] = {"type": "stdio", "command": result.pop("command")} + if "command" in filtered: + filtered["transport"] = {"type": "stdio", "command": filtered.pop("command")} - return result + return filtered ``` ## Testing Your Implementation -### Test Categories +### What Is Auto-Generated vs Manual + +| Category | Auto-Generated | Manual (if needed) | +|----------|:--------------:|:------------------:| +| **Adapter protocol** (host_name, fields) | Data-driven via `host_registry.py` | -- | +| **Validation contracts** (transport rules) | `generate_validation_test_cases()` | Complex multi-field validation | +| **Field filtering** (unsupported fields dropped) | `generate_unsupported_field_test_cases()` | -- | +| **Cross-host sync** (N x N pairs) | `generate_sync_test_cases()` | -- | +| **Serialization format** | Property-based assertions | Custom output structure | +| **Strategy file I/O** | -- | Always manual (host-specific paths) | + +### Fixture Requirements + +To integrate with the data-driven test infrastructure, you need: + +1. **Fixture entry** in `tests/test_data/mcp_adapters/canonical_configs.json` +2. **Field set mapping** in `tests/test_data/mcp_adapters/host_registry.py` (`FIELD_SETS` dict) +3. **Reverse mappings** in `host_registry.py` (only if your host uses field mappings) + +Zero changes to test functions are needed for standard adapter behavior. The test infrastructure derives all expectations from `fields.py` through the `HostSpec` dataclass and property-based assertions in `assertions.py`. -| Category | What to Test | -|----------|--------------| -| **Protocol** | `host_name`, `get_supported_fields()` return correct values | -| **Validation** | `validate()` accepts valid configs, rejects invalid | -| **Serialization** | `serialize()` produces correct format, filters fields | -| **Integration** | Adapter works with registry, strategy reads/writes files | +> **Cross-reference:** See the [Architecture Doc -- Testing Strategy](../architecture/mcp_host_configuration.md#testing-strategy) for the full testing infrastructure design, including the three test tiers (unit, integration, regression). ### Test File Location ``` tests/ ├── unit/mcp/ -│ └── test_your_host_adapter.py # Protocol + validation + serialization -└── integration/mcp/ - └── test_your_host_strategy.py # File I/O + end-to-end +│ ├── test_adapter_protocol.py # Protocol compliance (data-driven) +│ ├── test_adapter_registry.py # Registry operations +│ └── test_config_model.py # Unified model validation +├── integration/mcp/ +│ ├── test_cross_host_sync.py # N×N cross-host sync (data-driven) +│ ├── test_host_configuration.py # Strategy file I/O +│ └── test_adapter_serialization.py # Serialization correctness +├── regression/mcp/ +│ ├── test_field_filtering.py # Unsupported field filtering (data-driven) +│ ├── test_field_filtering_v2.py # Extended field filtering +│ └── test_validation_bugs.py # Validation edge cases +└── test_data/mcp_adapters/ + ├── canonical_configs.json # Fixture: canonical config per host + ├── host_registry.py # HostRegistry + test case generators + └── assertions.py # Property-based assertion library ``` ## Troubleshooting @@ -354,7 +500,7 @@ tests/ |-------|-------|----------| | Adapter not found | Not registered in registry | Add to `_register_defaults()` | | Field not serialized | Not in `get_supported_fields()` | Add field to set | -| Validation always fails | Logic error in `validate()` | Check conditions | +| Validation always fails | Logic error in `validate_filtered()` | Check conditions | | Name appears in output | Not filtering excluded fields | Use `filter_fields()` | ### Debugging Tips @@ -386,8 +532,8 @@ Study these for patterns: Adding a new host is now a **4-step process**: 1. **Add enum** to `MCPHostType` -2. **Create adapter** with `validate()` + `serialize()` + `get_supported_fields()` +2. **Create adapter** with `validate_filtered()` + `serialize()` + `get_supported_fields()` 3. **Create strategy** with `get_config_path()` + file I/O methods -4. **Add tests** for adapter and strategy +4. **Register test fixtures** in `canonical_configs.json` and `host_registry.py` (zero test code changes for standard adapters) The unified model handles all fields. Adapters filter and validate. Strategies handle files. No model conversion needed. diff --git a/hatch/mcp_host_config/adapters/__init__.py b/hatch/mcp_host_config/adapters/__init__.py index a949b0e..ece02f1 100644 --- a/hatch/mcp_host_config/adapters/__init__.py +++ b/hatch/mcp_host_config/adapters/__init__.py @@ -11,6 +11,7 @@ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter from hatch.mcp_host_config.adapters.kiro import KiroAdapter from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter +from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter from hatch.mcp_host_config.adapters.registry import ( AdapterRegistry, get_adapter, @@ -33,5 +34,6 @@ "GeminiAdapter", "KiroAdapter", "LMStudioAdapter", + "OpenCodeAdapter", "VSCodeAdapter", ] diff --git a/hatch/mcp_host_config/adapters/opencode.py b/hatch/mcp_host_config/adapters/opencode.py new file mode 100644 index 0000000..e1f4af5 --- /dev/null +++ b/hatch/mcp_host_config/adapters/opencode.py @@ -0,0 +1,160 @@ +"""OpenCode adapter for MCP host configuration. + +OpenCode uses a discriminated-union format with structural differences +from the universal schema: +- 'type' field is 'local' or 'remote' (not 'stdio'/'sse'/'http') +- 'command' is an array: [executable, ...args] (not a string) +- 'env' is renamed to 'environment' +- OAuth is nested under an 'oauth' key, or set to false to disable +""" + +from typing import Any, Dict, FrozenSet + +from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter +from hatch.mcp_host_config.fields import OPENCODE_FIELDS +from hatch.mcp_host_config.models import MCPServerConfig + + +class OpenCodeAdapter(BaseAdapter): + """Adapter for OpenCode MCP host. + + OpenCode uses a discriminated-union format where transport type is + derived from configuration presence: + - Local (stdio): command string + args list merged into command array, + env renamed to environment, type set to 'local' + - Remote (sse): url preserved, headers preserved, type set to 'remote' + + OAuth configuration is nested: + - opencode_oauth_disable=True serializes as oauth: false + - oauth_clientId/clientSecret/opencode_oauth_scope serialize as + oauth: {clientId, clientSecret, scope} (omitting null values) + """ + + @property + def host_name(self) -> str: + """Return the host identifier.""" + return "opencode" + + def get_supported_fields(self) -> FrozenSet[str]: + """Return fields supported by OpenCode.""" + return OPENCODE_FIELDS + + def validate(self, config: MCPServerConfig) -> None: + """Validate configuration for OpenCode. + + DEPRECATED: This method is deprecated and will be removed in v0.9.0. + Use validate_filtered() instead. + + OpenCode requires exactly one transport (command XOR url). + """ + has_command = config.command is not None + has_url = config.url is not None + + if not has_command and not has_url: + raise AdapterValidationError( + "Either 'command' (local) or 'url' (remote) must be specified", + host_name=self.host_name, + ) + + if has_command and has_url: + raise AdapterValidationError( + "Cannot specify both 'command' and 'url' - choose one transport", + host_name=self.host_name, + ) + + def validate_filtered(self, filtered: Dict[str, Any]) -> None: + """Validate filtered configuration for OpenCode. + + Validates only fields that survived filtering (supported by OpenCode). + OpenCode requires exactly one transport (command XOR url). + + Args: + filtered: Dictionary of filtered fields + + Raises: + AdapterValidationError: If validation fails + """ + has_command = "command" in filtered + has_url = "url" in filtered + + if not has_command and not has_url: + raise AdapterValidationError( + "Either 'command' (local) or 'url' (remote) must be specified", + host_name=self.host_name, + ) + + if has_command and has_url: + raise AdapterValidationError( + "Cannot specify both 'command' and 'url' - choose one transport", + host_name=self.host_name, + ) + + def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: + """Serialize configuration for OpenCode (canonical form). + + Returns a filtered, validated dict using MCPServerConfig field names. + Structural transforms (command array merge, env→environment rename, + type derivation, oauth nesting) are applied by the strategy's + write_configuration() via to_native_format(). + + Args: + config: The MCPServerConfig to serialize + + Returns: + Filtered dict with MCPServerConfig-canonical field names + """ + filtered = self.filter_fields(config) + self.validate_filtered(filtered) + return filtered + + @staticmethod + def to_native_format(filtered: Dict[str, Any]) -> Dict[str, Any]: + """Convert canonical-form dict to OpenCode-native file format. + + Applies OpenCode structural transforms: + - Derives type: 'local' (command present) or 'remote' (url present) + - Local: merges command + args into command array, renames env→environment + - Remote: preserves url and headers as-is + - Handles enabled, timeout if present + - OAuth: emits oauth: false or oauth: {clientId, clientSecret, scope} + + Args: + filtered: Canonical-form dict from serialize() + + Returns: + Dict in OpenCode's native file format + """ + result: Dict[str, Any] = {} + + if "command" in filtered: + result["type"] = "local" + command = filtered["command"] + args = filtered.get("args") or [] + result["command"] = [command] + args + if "env" in filtered: + result["environment"] = filtered["env"] + else: + result["type"] = "remote" + result["url"] = filtered["url"] + if "headers" in filtered: + result["headers"] = filtered["headers"] + + if "enabled" in filtered: + result["enabled"] = filtered["enabled"] + if "timeout" in filtered: + result["timeout"] = filtered["timeout"] + + if filtered.get("opencode_oauth_disable"): + result["oauth"] = False + else: + oauth: Dict[str, Any] = {} + if filtered.get("oauth_clientId"): + oauth["clientId"] = filtered["oauth_clientId"] + if filtered.get("oauth_clientSecret"): + oauth["clientSecret"] = filtered["oauth_clientSecret"] + if filtered.get("opencode_oauth_scope"): + oauth["scope"] = filtered["opencode_oauth_scope"] + if oauth: + result["oauth"] = oauth + + return result diff --git a/hatch/mcp_host_config/adapters/registry.py b/hatch/mcp_host_config/adapters/registry.py index 39065b4..88e4458 100644 --- a/hatch/mcp_host_config/adapters/registry.py +++ b/hatch/mcp_host_config/adapters/registry.py @@ -13,6 +13,7 @@ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter from hatch.mcp_host_config.adapters.kiro import KiroAdapter from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter +from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter from hatch.mcp_host_config.adapters.vscode import VSCodeAdapter @@ -53,6 +54,7 @@ def _register_defaults(self) -> None: self.register(GeminiAdapter()) self.register(KiroAdapter()) self.register(CodexAdapter()) + self.register(OpenCodeAdapter()) def register(self, adapter: BaseAdapter) -> None: """Register an adapter instance. diff --git a/hatch/mcp_host_config/backup.py b/hatch/mcp_host_config/backup.py index 26ab840..7d6ff1d 100644 --- a/hatch/mcp_host_config/backup.py +++ b/hatch/mcp_host_config/backup.py @@ -48,6 +48,7 @@ def validate_hostname(cls, v): "gemini", "kiro", "codex", + "opencode", } if v not in supported_hosts: raise ValueError(f"Unsupported hostname: {v}. Supported: {supported_hosts}") diff --git a/hatch/mcp_host_config/fields.py b/hatch/mcp_host_config/fields.py index 2531cd0..880dbe2 100644 --- a/hatch/mcp_host_config/fields.py +++ b/hatch/mcp_host_config/fields.py @@ -116,6 +116,21 @@ ) +# Fields supported by OpenCode (no type field; uses local/remote type derivation, +# command array merge, environment rename, and oauth nesting) +OPENCODE_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset( + { + "args", # Merged with command into command array (I-2: must stay in field set) + "enabled", # Enable/disable server without deleting config + "timeout", # Request timeout in milliseconds + "oauth_clientId", # OAuth client identifier (nested under 'oauth' key) + "oauth_clientSecret", # OAuth client secret (nested under 'oauth' key) + "opencode_oauth_scope", # OAuth scope (nested under 'oauth.scope') + "opencode_oauth_disable", # Disable OAuth (serializes as oauth: false) + } +) + + # ============================================================================ # Field Mappings (universal name → host-specific name) # ============================================================================ diff --git a/hatch/mcp_host_config/models.py b/hatch/mcp_host_config/models.py index b7d146f..ecc6218 100644 --- a/hatch/mcp_host_config/models.py +++ b/hatch/mcp_host_config/models.py @@ -30,6 +30,7 @@ class MCPHostType(str, Enum): GEMINI = "gemini" KIRO = "kiro" CODEX = "codex" + OPENCODE = "opencode" class MCPServerConfig(BaseModel): @@ -166,6 +167,17 @@ class MCPServerConfig(BaseModel): None, description="Header names to env var names" ) + # ======================================================================== + # OpenCode-Specific Fields + # ======================================================================== + opencode_oauth_scope: Optional[str] = Field( + None, description="OAuth scope for OpenCode server (maps to oauth.scope)" + ) + opencode_oauth_disable: Optional[bool] = Field( + None, + description="Disable OAuth for OpenCode server (serializes as oauth: false)", + ) + # ======================================================================== # Minimal Validators (host-specific validation is in adapters) # ======================================================================== @@ -353,6 +365,7 @@ def validate_host_names(cls, v): "lmstudio", "gemini", "kiro", + "opencode", } for host_name in v.keys(): if host_name not in supported_hosts: diff --git a/hatch/mcp_host_config/reporting.py b/hatch/mcp_host_config/reporting.py index 8791f93..22a9f8e 100644 --- a/hatch/mcp_host_config/reporting.py +++ b/hatch/mcp_host_config/reporting.py @@ -73,6 +73,7 @@ def _get_adapter_host_name(host_type: MCPHostType) -> str: MCPHostType.GEMINI: "gemini", MCPHostType.KIRO: "kiro", MCPHostType.CODEX: "codex", + MCPHostType.OPENCODE: "opencode", } return mapping.get(host_type, host_type.value) diff --git a/hatch/mcp_host_config/strategies.py b/hatch/mcp_host_config/strategies.py index 5f1523d..1e11e64 100644 --- a/hatch/mcp_host_config/strategies.py +++ b/hatch/mcp_host_config/strategies.py @@ -8,6 +8,7 @@ import platform import json +import re import tomllib # Python 3.11+ built-in import tomli_w # TOML writing from pathlib import Path @@ -18,6 +19,7 @@ from .models import MCPHostType, MCPServerConfig, HostConfiguration from .backup import MCPHostConfigBackupManager, AtomicFileOperations from .adapters import get_adapter +from .adapters.opencode import OpenCodeAdapter logger = logging.getLogger(__name__) @@ -880,3 +882,174 @@ def _to_toml_server_from_dict(self, data: Dict[str, Any]) -> Dict[str, Any]: result["http_headers"] = result.pop("headers") return result + + +@register_host_strategy(MCPHostType.OPENCODE) +class OpenCodeHostStrategy(MCPHostStrategy): + """Configuration strategy for OpenCode AI editor. + + OpenCode stores MCP configuration in opencode.json under the 'mcp' key. + The config file may contain JSONC-style // comments which are stripped + before JSON parsing. + + OpenCode uses a discriminated-union format that differs from canonical form: + - 'type' is 'local' or 'remote' (not 'stdio'/'sse') + - 'command' is an array: [executable, ...args] + - 'env' is 'environment' in the file + - OAuth is nested under an 'oauth' key, or set to false to disable + + The pre-processor in read_configuration() normalises raw server data back + into MCPServerConfig-compatible form before Pydantic construction. + """ + + def get_adapter_host_name(self) -> str: + """Return the adapter host name for OpenCode.""" + return "opencode" + + def get_config_path(self) -> Optional[Path]: + """Get OpenCode configuration path (platform-aware).""" + system = platform.system() + if system == "Windows": + return Path.home() / "AppData" / "Roaming" / "opencode" / "opencode.json" + # macOS and Linux both use XDG-style ~/.config/ + return Path.home() / ".config" / "opencode" / "opencode.json" + + def get_config_key(self) -> str: + """OpenCode uses 'mcp' key.""" + return "mcp" + + def is_host_available(self) -> bool: + """Check if OpenCode is available by checking for its config directory.""" + config_path = self.get_config_path() + return config_path is not None and config_path.parent.exists() + + def validate_server_config(self, server_config: MCPServerConfig) -> bool: + """OpenCode validation - supports both local and remote servers.""" + return server_config.command is not None or server_config.url is not None + + @staticmethod + def _pre_process_server(raw: Dict[str, Any]) -> Dict[str, Any]: + """Normalise a raw OpenCode server entry into MCPServerConfig-compatible form. + + Transforms: + - Strips 'type' key (raw values 'local'/'remote' are invalid for MCPServerConfig) + - Splits command array: command[0] → command str, command[1:] → args list + - Renames 'environment' → 'env' + - Unnests 'oauth' dict → oauth_clientId, oauth_clientSecret, opencode_oauth_scope + - oauth: false → opencode_oauth_disable=True + + Args: + raw: Raw server dict read from opencode.json + + Returns: + Dict suitable for MCPServerConfig(**result) construction + """ + data = dict(raw) + + # Strip transport type discriminator (opencode uses 'local'/'remote') + data.pop("type", None) + + # Split command array into command string + args list + if "command" in data and isinstance(data["command"], list): + command_list = data["command"] + data["command"] = command_list[0] if command_list else "" + if len(command_list) > 1: + data["args"] = command_list[1:] + + # Rename environment → env + if "environment" in data: + data["env"] = data.pop("environment") + + # Unnest oauth + oauth_value = data.pop("oauth", None) + if oauth_value is False: + data["opencode_oauth_disable"] = True + elif isinstance(oauth_value, dict): + if "clientId" in oauth_value: + data["oauth_clientId"] = oauth_value["clientId"] + if "clientSecret" in oauth_value: + data["oauth_clientSecret"] = oauth_value["clientSecret"] + if "scope" in oauth_value: + data["opencode_oauth_scope"] = oauth_value["scope"] + + return data + + def read_configuration(self) -> HostConfiguration: + """Read OpenCode configuration file with JSONC comment stripping.""" + config_path = self.get_config_path() + if not config_path or not config_path.exists(): + return HostConfiguration() + + try: + raw_text = config_path.read_text(encoding="utf-8") + + # Strip // line comments (JSONC support) — only strip lines that + # START with optional whitespace + //, never inside string values + stripped = re.sub(r"(?m)^\s*//[^\n]*", "", raw_text) + + config_data = json.loads(stripped) + mcp_servers = config_data.get(self.get_config_key(), {}) + + servers = {} + for name, server_data in mcp_servers.items(): + try: + processed = self._pre_process_server(server_data) + servers[name] = MCPServerConfig(**processed) + except Exception as e: + logger.warning(f"Invalid OpenCode server config for {name}: {e}") + continue + + return HostConfiguration(servers=servers) + + except Exception as e: + logger.error(f"Failed to read OpenCode configuration: {e}") + return HostConfiguration() + + def write_configuration( + self, config: HostConfiguration, no_backup: bool = False + ) -> bool: + """Write OpenCode configuration with read-before-write to preserve other keys.""" + config_path = self.get_config_path() + if not config_path: + return False + + try: + config_path.parent.mkdir(parents=True, exist_ok=True) + + # Read existing config to preserve non-mcp keys (theme, model, etc.) + existing_data: Dict[str, Any] = {} + if config_path.exists(): + try: + raw_text = config_path.read_text(encoding="utf-8") + stripped = re.sub(r"(?m)^\s*//[^\n]*", "", raw_text) + existing_data = json.loads(stripped) + except Exception: + pass + + # Serialize all servers using the OpenCode adapter, then apply + # structural transforms to produce OpenCode-native file format + adapter = get_adapter(self.get_adapter_host_name()) + servers_dict = {} + for name, server_config in config.servers.items(): + canonical = adapter.serialize(server_config) + servers_dict[name] = OpenCodeAdapter.to_native_format(canonical) + + existing_data[self.get_config_key()] = servers_dict + + # Write atomically with backup support + backup_manager = MCPHostConfigBackupManager() + atomic_ops = AtomicFileOperations() + + atomic_ops.atomic_write_with_backup( + file_path=config_path, + data=existing_data, + backup_manager=backup_manager, + hostname="opencode", + skip_backup=no_backup, + ) + + return True + + except Exception as e: + logger.error(f"Failed to write OpenCode configuration: {e}") + return False diff --git a/pyproject.toml b/pyproject.toml index 5aa11b4..a486279 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hatch-xclam" -version = "0.8.0-dev.2" +version = "0.8.0" description = "Package manager for the Cracking Shells ecosystem" readme = "README.md" requires-python = ">=3.12" diff --git a/tests/regression/mcp/test_field_filtering_v2.py b/tests/regression/mcp/test_field_filtering_v2.py index 7ba770d..c511532 100644 --- a/tests/regression/mcp/test_field_filtering_v2.py +++ b/tests/regression/mcp/test_field_filtering_v2.py @@ -77,6 +77,9 @@ def regression_test(func): "env_vars": ["VAR1"], "enabled_tools": ["tool1"], "disabled_tools": ["tool2"], + # OpenCode-specific fields + "opencode_oauth_disable": False, + "opencode_oauth_scope": "read", # Dict fields "env": {"TEST": "value"}, "headers": {"X-Test": "value"}, diff --git a/tests/test_data/mcp_adapters/canonical_configs.json b/tests/test_data/mcp_adapters/canonical_configs.json index 49bc2ac..b90299f 100644 --- a/tests/test_data/mcp_adapters/canonical_configs.json +++ b/tests/test_data/mcp_adapters/canonical_configs.json @@ -74,5 +74,16 @@ "cwd": "/app", "enabled_tools": ["tool1", "tool2"], "disabled_tools": ["tool3"] + }, + "opencode": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], + "env": {"MY_VAR": "value"}, + "url": null, + "headers": null, + "enabled": true, + "timeout": 5000, + "opencode_oauth_scope": null, + "opencode_oauth_disable": null } } diff --git a/tests/test_data/mcp_adapters/host_registry.py b/tests/test_data/mcp_adapters/host_registry.py index 34d6a49..f1e6ff1 100644 --- a/tests/test_data/mcp_adapters/host_registry.py +++ b/tests/test_data/mcp_adapters/host_registry.py @@ -26,6 +26,7 @@ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter from hatch.mcp_host_config.adapters.kiro import KiroAdapter from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter +from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter from hatch.mcp_host_config.adapters.vscode import VSCodeAdapter from hatch.mcp_host_config.fields import ( CLAUDE_FIELDS, @@ -36,6 +37,7 @@ GEMINI_FIELDS, KIRO_FIELDS, LMSTUDIO_FIELDS, + OPENCODE_FIELDS, TYPE_SUPPORTING_HOSTS, VSCODE_FIELDS, ) @@ -55,6 +57,7 @@ "gemini": GEMINI_FIELDS, "kiro": KIRO_FIELDS, "codex": CODEX_FIELDS, + "opencode": OPENCODE_FIELDS, } # Reverse mappings for Codex (host-native name → universal name) @@ -93,6 +96,7 @@ def get_adapter(self) -> BaseAdapter: "gemini": GeminiAdapter, "kiro": KiroAdapter, "codex": CodexAdapter, + "opencode": OpenCodeAdapter, } factory = adapter_map[self.host_name] return factory() @@ -350,6 +354,7 @@ def generate_unsupported_field_test_cases( | GEMINI_FIELDS | KIRO_FIELDS | CODEX_FIELDS + | OPENCODE_FIELDS ) cases: List[FilterTestCase] = [] diff --git a/tests/unit/mcp/test_adapter_registry.py b/tests/unit/mcp/test_adapter_registry.py index 9408835..b6b7736 100644 --- a/tests/unit/mcp/test_adapter_registry.py +++ b/tests/unit/mcp/test_adapter_registry.py @@ -38,6 +38,7 @@ def test_AR01_registry_has_all_default_hosts(self): "gemini", "kiro", "lmstudio", + "opencode", "vscode", }