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..3785cfe --- /dev/null +++ b/.claude/skills/adding-mcp-hosts/references/testing-fixtures.md @@ -0,0 +1,147 @@ +# 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. test_adapter_protocol.py entries + +`tests/unit/mcp/test_adapter_protocol.py` has two **static** lists that are NOT auto-updated by the data-driven infrastructure. Both must be updated manually: + +**`ALL_ADAPTERS`** -- append the new adapter class: + +```python +ALL_ADAPTERS = [ + # ... existing entries ... + NewHostAdapter, +] +``` + +**`HOST_ADAPTER_MAP`** -- add the `MCPHostType → adapter class` mapping: + +```python +HOST_ADAPTER_MAP = { + # ... existing entries ... + MCPHostType.NEW_HOST: NewHostAdapter, +} +``` + +Import `NewHostAdapter` and `MCPHostType.NEW_HOST` at the top of the file alongside the existing imports. Missing either entry means the AP-01…AP-06 protocol compliance tests silently skip the new adapter — they pass without covering it. + +--- + +## 3. 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/.github/workflows/prerelease-discord-notification.yml b/.github/workflows/prerelease-discord-notification.yml deleted file mode 100644 index 44e6090..0000000 --- a/.github/workflows/prerelease-discord-notification.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Discord Pre-release Notification - -on: - release: - types: [prereleased] - -jobs: - notify-discord: - runs-on: ubuntu-latest - if: github.event.release.target_commitish == 'dev' - steps: - - name: Send Discord Pre-release Notification - uses: sarisia/actions-status-discord@v1 - with: - webhook: ${{ secrets.DISCORD_HATCH_ANNOUNCEMENTS }} - nodetail: true - # No content field = no mention for pre-releases - title: "🧪 Hatch Pre-release Available for Testing" - description: | - **Version `${{ github.event.release.tag_name }}`** is now available for testing! - - ⚠️ **This is a pre-release** - expect potential bugs and breaking changes - 🔬 Perfect for testing new features and providing feedback - 📋 Click [here](${{ github.event.release.html_url }}) to view what's new and download - - 💻 Install with pip: - ```bash - pip install hatch-xclam==${{ github.event.release.tag_name }} - ``` - - Help us make *Hatch!* better by testing and reporting [issues](https://github.com/CrackingShells/Hatch/issues)! 🐛➡️✨ - color: 0xff9500 # Orange color for pre-release - username: "Cracking Shells Pre-release Bot" - image: "https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_icon_dark_bg_transparent.png" - avatar_url: "https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/cs_core_dark_bg.png" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7f49df7..ef44050 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,30 +1,112 @@ -name: Publish to PyPI +name: Semantic Release on: push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+*' - workflow_dispatch: - inputs: - tag: - description: 'Git tag to publish (e.g., v1.0.0)' - required: true - type: string - ref: - description: 'Branch or commit to checkout' - required: false - default: 'main' - type: string + branches: + - main + - dev jobs: test: + if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ github.event.inputs.ref || github.ref }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Run import test + run: | + python -c "import hatch; print('Hatch package imports successfully')" + + release: + if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }} + needs: test + runs-on: ubuntu-latest + outputs: + published: ${{ steps.semantic_release.outputs.published }} + tag: ${{ steps.semantic_release.outputs.tag }} + steps: + - name: Generate GitHub App Token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.SEMANTIC_RELEASE_APP_ID }} + private_key: ${{ secrets.SEMANTIC_RELEASE_PRIVATE_KEY }} + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + + - name: Install Node dependencies + run: npm ci + + - name: Verify npm audit + run: npm audit signatures + + - name: Release + id: semantic_release + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + node <<'EOF' + const fs = require('fs'); + + (async () => { + const semanticReleaseModule = await import('semantic-release'); + const semanticRelease = semanticReleaseModule.default || semanticReleaseModule; + const result = await semanticRelease(); + + if (!process.env.GITHUB_OUTPUT) { + throw new Error('GITHUB_OUTPUT is not set'); + } + + if (!result) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, 'published=false\n'); + fs.appendFileSync(process.env.GITHUB_OUTPUT, 'tag=\n'); + return; + } + + fs.appendFileSync(process.env.GITHUB_OUTPUT, 'published=true\n'); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${result.nextRelease.gitTag}\n`); + })().catch((error) => { + console.error(error); + process.exit(1); + }); + EOF + + publish-test: + name: Test released package + needs: release + if: ${{ needs.release.outputs.published == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.release.outputs.tag }} - name: Setup Python uses: actions/setup-python@v5 @@ -43,7 +125,10 @@ jobs: publish-pypi: name: Publish to PyPI runs-on: ubuntu-latest - needs: test + needs: [release, publish-test] + if: ${{ needs.release.outputs.published == 'true' }} + outputs: + tag: ${{ steps.published_tag.outputs.tag }} environment: name: pypi url: https://pypi.org/project/hatch-xclam/ @@ -55,11 +140,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ github.event.inputs.ref || github.ref }} - - - name: Checkout specific tag for manual dispatch - if: github.event_name == 'workflow_dispatch' - run: git checkout ${{ github.event.inputs.tag }} + ref: ${{ needs.release.outputs.tag }} - name: Setup Python uses: actions/setup-python@v5 @@ -80,3 +161,79 @@ jobs: print-hash: true verbose: true skip-existing: true + + - name: Record published tag + id: published_tag + run: | + echo "tag=${{ needs.release.outputs.tag }}" >> "$GITHUB_OUTPUT" + + notify-discord: + name: Notify Discord + needs: + - release + - publish-pypi + if: ${{ needs.release.outputs.published == 'true' && needs.publish-pypi.result == 'success' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Resolve GitHub release + id: release + uses: actions/github-script@v8 + env: + TAG_NAME: ${{ needs.publish-pypi.outputs.tag }} + with: + script: | + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: process.env.TAG_NAME, + }); + + core.setOutput('tag_name', release.tag_name); + core.setOutput('html_url', release.html_url); + core.setOutput('is_prerelease', String(release.prerelease)); + + - name: Build Discord payload + id: discord + uses: actions/github-script@v8 + env: + TAG_NAME: ${{ steps.release.outputs.tag_name }} + HTML_URL: ${{ steps.release.outputs.html_url }} + IS_PRERELEASE: ${{ steps.release.outputs.is_prerelease }} + with: + script: | + const isPrerelease = process.env.IS_PRERELEASE === 'true'; + const tagName = process.env.TAG_NAME; + const htmlUrl = process.env.HTML_URL; + + core.setOutput('content', isPrerelease ? '' : '<@&1418053865818951721>'); + core.setOutput('title', isPrerelease + ? '🧪 Hatch Pre-release Available for Testing' + : '🎉 New *Hatch!* Release Available!'); + core.setOutput('description', isPrerelease + ? `**Version \`${tagName}\`** is now available for testing!\n\n⚠️ **This is a pre-release** - expect potential bugs and breaking changes\n🔬 Perfect for testing new features and providing feedback\n📋 Click [here](${htmlUrl}) to view what's new and download\n\n💻 Install with pip:\n\`\`\`bash\npip install hatch-xclam==${tagName}\n\`\`\`\n\nHelp us make *Hatch!* better by testing and reporting [issues](https://github.com/CrackingShells/Hatch/issues)! 🐛➡️✨` + : `**Version \`${tagName}\`** has been released!\n\n🚀 Get the latest features and improvements\n📚 Click [here](${htmlUrl}) to view the changelog and download\n\n💻 Install with pip:\n\`\`\`bash\npip install hatch-xclam\n\`\`\`\n\nHappy MCP coding with *Hatch!* 🐣`); + core.setOutput('color', isPrerelease ? '0xff9500' : '0x00ff88'); + core.setOutput('username', isPrerelease + ? 'Cracking Shells Pre-release Bot' + : 'Cracking Shells Release Bot'); + core.setOutput('image', isPrerelease + ? 'https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_icon_dark_bg_transparent.png' + : 'https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_icon_light_bg_transparent.png'); + core.setOutput('avatar_url', isPrerelease + ? 'https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/cs_core_dark_bg.png' + : 'https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/cs_icon_light_bg.png'); + + - name: Send Discord notification + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_HATCH_ANNOUNCEMENTS }} + nodetail: true + content: ${{ steps.discord.outputs.content }} + title: ${{ steps.discord.outputs.title }} + description: ${{ steps.discord.outputs.description }} + color: ${{ steps.discord.outputs.color }} + username: ${{ steps.discord.outputs.username }} + image: ${{ steps.discord.outputs.image }} + avatar_url: ${{ steps.discord.outputs.avatar_url }} diff --git a/.github/workflows/release-discord-notification.yml b/.github/workflows/release-discord-notification.yml deleted file mode 100644 index fe9261f..0000000 --- a/.github/workflows/release-discord-notification.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Discord Release Notification - -on: - release: - types: [released] - -jobs: - notify-discord: - runs-on: ubuntu-latest - if: github.event.release.target_commitish == 'main' - steps: - - name: Send Discord Notification - uses: sarisia/actions-status-discord@v1 - with: - webhook: ${{ secrets.DISCORD_HATCH_ANNOUNCEMENTS }} - nodetail: true - content: "<@&1418053865818951721>" - title: "🎉 New *Hatch!* Release Available!" - description: | - **Version `${{ github.event.release.tag_name }}`** has been released! - - 🚀 Get the latest features and improvements - 📚 Click [here](${{ github.event.release.html_url }}) to view the changelog and download - - 💻 Install with pip: - ```bash - pip install hatch-xclam - ``` - - Happy MCP coding with *Hatch!* 🐣 - color: 0x00ff88 - username: "Cracking Shells Release Bot" - image: "https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_icon_light_bg_transparent.png" - avatar_url: "https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/cs_icon_light_bg.png" diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml deleted file mode 100644 index b6647b6..0000000 --- a/.github/workflows/semantic-release.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Semantic Release - -on: - push: - branches: - - main - - dev - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install -e . - - - name: Run import test - run: | - python -c "import hatch; print('Hatch package imports successfully')" - - release: - needs: test - runs-on: ubuntu-latest - steps: - - name: Generate GitHub App Token - id: generate_token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ secrets.SEMANTIC_RELEASE_APP_ID }} - private_key: ${{ secrets.SEMANTIC_RELEASE_PRIVATE_KEY }} - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ steps.generate_token.outputs.token }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "lts/*" - - - name: Install Node dependencies - run: npm ci - - - name: Verify npm audit - run: npm audit signatures - - - name: Release - env: - GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - GH_TOKEN: ${{ steps.generate_token.outputs.token }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - npx semantic-release diff --git a/.gitignore b/.gitignore index 7a12ef8..9b25ba9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ __temp__/ ## VS Code .vscode/ +## macOS +.DS_Store + # vvvvvvv Default Python Ignore vvvvvvvv # Byte-compiled / optimized / DLL files diff --git a/.releaserc.json b/.releaserc.json index ed0509c..61963ca 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -51,7 +51,7 @@ "@semantic-release/git", { "assets": ["CHANGELOG.md", "pyproject.toml"], - "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ], [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acdc4a..c22c538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,99 @@ +## 0.8.1-dev.6 (2026-04-01) + +* Merge pull request #50 from LittleCoinCoin/dev ([8f6c5f8](https://github.com/CrackingShells/Hatch/commit/8f6c5f8)), closes [#50](https://github.com/CrackingShells/Hatch/issues/50) +* docs(cli): update to match current CLI state ([2d75bcd](https://github.com/CrackingShells/Hatch/commit/2d75bcd)) +* docs(entry-docs): refresh content for primary use case identification ([d4d0bb9](https://github.com/CrackingShells/Hatch/commit/d4d0bb9)) +* docs(known-issues): sync appendix with v0.8.1 codebase state ([ed98ea4](https://github.com/CrackingShells/Hatch/commit/ed98ea4)) +* docs(mcp-host-config): surface opencode and augment in all host lists ([dbdba35](https://github.com/CrackingShells/Hatch/commit/dbdba35)) +* style(docs): add CrackingShells brand theme stylesheets for MkDocs ([c5cec5b](https://github.com/CrackingShells/Hatch/commit/c5cec5b)) +* style(docs): apply CrackingShells org theme to MkDocs ([7749d48](https://github.com/CrackingShells/Hatch/commit/7749d48)) +* chore(git): add .DS_Store to .gitignore ([cff376a](https://github.com/CrackingShells/Hatch/commit/cff376a)) + +## 0.8.1-dev.5 (2026-03-23) + +* fix(ci): inline pypi publish jobs to satisfy trusted publishing ([fc81e78](https://github.com/CrackingShells/Hatch/commit/fc81e78)) + +## 0.8.1-dev.4 (2026-03-23) + +* fix(ci): rename caller workflow to match pypi trusted publisher ([7d2634d](https://github.com/CrackingShells/Hatch/commit/7d2634d)) + +## 0.8.1-dev.3 (2026-03-23) + +* Merge pull request #49 from LittleCoinCoin/dev ([73666d9](https://github.com/CrackingShells/Hatch/commit/73666d9)), closes [#49](https://github.com/CrackingShells/Hatch/issues/49) +* fix(mcp-hosts): add explicit HTTP transport type for Claude URL-based co ([62f99cf](https://github.com/CrackingShells/Hatch/commit/62f99cf)) +* fix(mcp-hosts): always set HTTP transport type for Claude URL-based conf ([904f22b](https://github.com/CrackingShells/Hatch/commit/904f22b)) +* fix(mcp-hosts): remove redundant Claude Desktop/Code URL validation ([d6a75a8](https://github.com/CrackingShells/Hatch/commit/d6a75a8)) +* test(mcp-hosts): keep mistral vibe in shared adapter coverage ([1a81ae0](https://github.com/CrackingShells/Hatch/commit/1a81ae0)) +* test(mcp): add fixture for claude remote setup ([d1cc2b0](https://github.com/CrackingShells/Hatch/commit/d1cc2b0)) +* test(mcp): fix whitespace in Claude transport serialization test ([b5c7191](https://github.com/CrackingShells/Hatch/commit/b5c7191)) +* docs: correct Mistral Vibe terminology to CLI coding agent ([bfa8b9b](https://github.com/CrackingShells/Hatch/commit/bfa8b9b)) +* docs(mcp-hosts): add mistral vibe to supported platforms ([5130c84](https://github.com/CrackingShells/Hatch/commit/5130c84)) +* docs(mcp-hosts): capture mistral vibe host analysis ([4ff2758](https://github.com/CrackingShells/Hatch/commit/4ff2758)) +* feat(cli): let shared mcp configure target mistral vibe ([0e801d0](https://github.com/CrackingShells/Hatch/commit/0e801d0)) +* feat(mcp-hosts): let hatch manage mistral vibe configs ([f213971](https://github.com/CrackingShells/Hatch/commit/f213971)) +* ci: semantic-release streamlining ([db0fb91](https://github.com/CrackingShells/Hatch/commit/db0fb91)) + +## 0.8.1-dev.2 (2026-03-04) + +* Merge branch 'feat/augment-mcp-host-support' into dev ([67bb767](https://github.com/CrackingShells/Hatch/commit/67bb767)) +* Merge branch `milestone/fix-logging-clutter` into dev ([5fd15dd](https://github.com/CrackingShells/Hatch/commit/5fd15dd)) +* Merge pull request #48 from LittleCoinCoin/dev ([0bc06fb](https://github.com/CrackingShells/Hatch/commit/0bc06fb)), closes [#48](https://github.com/CrackingShells/Hatch/issues/48) +* docs(adding-mcp-hosts): add test_adapter_protocol.py to fixture guide ([3a58908](https://github.com/CrackingShells/Hatch/commit/3a58908)) +* docs(logging): expose --log-level flag in CLI reference global options ([5aa2e9d](https://github.com/CrackingShells/Hatch/commit/5aa2e9d)) +* feat(cli): add --log-level flag and default log output to WARNING ([1e3817f](https://github.com/CrackingShells/Hatch/commit/1e3817f)) +* feat(mcp-augment): add enum value and constant ([8b22594](https://github.com/CrackingShells/Hatch/commit/8b22594)) +* feat(mcp-augment): implement AugmentAdapter ([5af34d1](https://github.com/CrackingShells/Hatch/commit/5af34d1)) +* feat(mcp-augment): implement AugmentHostStrategy ([b13d9d0](https://github.com/CrackingShells/Hatch/commit/b13d9d0)) +* feat(mcp-augment): wire AugmentAdapter into integration points ([367b736](https://github.com/CrackingShells/Hatch/commit/367b736)) +* feat(registry): add transient dim status on cache refresh ([09dd517](https://github.com/CrackingShells/Hatch/commit/09dd517)) +* refactor(logging): remove forced setLevel(INFO) from all module loggers ([fb2ee4c](https://github.com/CrackingShells/Hatch/commit/fb2ee4c)) +* refactor(registry): demote startup and fetch INFO logs to DEBUG ([df97e58](https://github.com/CrackingShells/Hatch/commit/df97e58)) +* fix(mcp-hosts): close validation and test coverage gaps ([9d7f0e5](https://github.com/CrackingShells/Hatch/commit/9d7f0e5)) +* test(mcp-augment): register test fixtures and update tests ([294d0d8](https://github.com/CrackingShells/Hatch/commit/294d0d8)) +* chore: clean up temporary reports ([038be8c](https://github.com/CrackingShells/Hatch/commit/038be8c)) + +## 0.8.1-dev.1 (2026-02-26) + +* Merge branch 'feat/opencode-mcp-host-support' into dev ([793707d](https://github.com/CrackingShells/Hatch/commit/793707d)) +* Merge branch 'milestone/adding-mcp-hosts-skill' into dev ([bce3851](https://github.com/CrackingShells/Hatch/commit/bce3851)) +* Merge branch 'task/update-extension-guide' into milestone/mcp-docs-refresh ([d2a0df9](https://github.com/CrackingShells/Hatch/commit/d2a0df9)) +* Merge branch 'task/write-adapter-contract' into milestone/adding-mcp-hosts-skill ([c639322](https://github.com/CrackingShells/Hatch/commit/c639322)) +* Merge branch 'task/write-skill-md' into milestone/adding-mcp-hosts-skill ([d618f71](https://github.com/CrackingShells/Hatch/commit/d618f71)) +* Merge branch 'task/write-strategy-contract' into milestone/adding-mcp-hosts-skill ([13b195c](https://github.com/CrackingShells/Hatch/commit/13b195c)) +* Merge branch 'task/write-testing-fixtures' into milestone/adding-mcp-hosts-skill ([3cc4175](https://github.com/CrackingShells/Hatch/commit/3cc4175)) +* Merge pull request #47 from LittleCoinCoin/dev ([9d873aa](https://github.com/CrackingShells/Hatch/commit/9d873aa)), closes [#47](https://github.com/CrackingShells/Hatch/issues/47) +* fix(mcp-opencode): anchor JSONC comment regex in write_configuration ([a35d3a2](https://github.com/CrackingShells/Hatch/commit/a35d3a2)) +* fix(mcp-opencode): anchor JSONC comment regex to line start ([d8f3a75](https://github.com/CrackingShells/Hatch/commit/d8f3a75)) +* fix(mcp-opencode): make serialize() canonical-form; add test fixes ([ee1d915](https://github.com/CrackingShells/Hatch/commit/ee1d915)) +* test(mcp-fixtures): add opencode entry to canonical_configs.json ([5ae3b57](https://github.com/CrackingShells/Hatch/commit/5ae3b57)) +* test(mcp-fixtures): register opencode in host_registry.py ([734b3c0](https://github.com/CrackingShells/Hatch/commit/734b3c0)) +* feat(mcp-adapter): add OpenCodeAdapter with serialize transforms ([28e3bdf](https://github.com/CrackingShells/Hatch/commit/28e3bdf)) +* feat(mcp-fields): add OPENCODE_FIELDS constant ([b9ddf43](https://github.com/CrackingShells/Hatch/commit/b9ddf43)) +* feat(mcp-models): add opencode oauth fields to MCPServerConfig ([2bae600](https://github.com/CrackingShells/Hatch/commit/2bae600)) +* feat(mcp-registry): register OpenCodeAdapter in adapter registry ([20e0fc8](https://github.com/CrackingShells/Hatch/commit/20e0fc8)) +* feat(mcp-strategy): add OpenCodeHostStrategy with JSONC read/write ([8bb590a](https://github.com/CrackingShells/Hatch/commit/8bb590a)) +* feat(mcp-wiring): add opencode to backup and reporting ([7d0b075](https://github.com/CrackingShells/Hatch/commit/7d0b075)) +* feat(skill): add adapter contract reference ([336fced](https://github.com/CrackingShells/Hatch/commit/336fced)) +* feat(skill): add discovery guide reference ([8061c5f](https://github.com/CrackingShells/Hatch/commit/8061c5f)) +* feat(skill): add strategy contract reference ([cf9b807](https://github.com/CrackingShells/Hatch/commit/cf9b807)) +* feat(skill): add testing fixtures reference ([070894c](https://github.com/CrackingShells/Hatch/commit/070894c)) +* feat(skill): write SKILL.md with 5-step workflow ([8984a3a](https://github.com/CrackingShells/Hatch/commit/8984a3a)) +* docs(adding-mcp-hosts): use parallel research over priority ladder ([6f6165a](https://github.com/CrackingShells/Hatch/commit/6f6165a)) +* docs(mcp): document strategy, registration, and variant pattern ([21c30d5](https://github.com/CrackingShells/Hatch/commit/21c30d5)) +* docs(mcp): rewrite testing section with data-driven docs ([5fc6f97](https://github.com/CrackingShells/Hatch/commit/5fc6f97)) +* docs(mcp): rewrite testing section with data-driven infra ([24c6ebf](https://github.com/CrackingShells/Hatch/commit/24c6ebf)) +* docs(mcp): update adapter template to validate_filtered() ([69d61cc](https://github.com/CrackingShells/Hatch/commit/69d61cc)) +* docs(mcp): update field support matrix and field mapping documentation ([c08e064](https://github.com/CrackingShells/Hatch/commit/c08e064)) +* docs(mcp): update strategy template with interface docs ([0b83b6e](https://github.com/CrackingShells/Hatch/commit/0b83b6e)) +* docs(roadmap): add mcp-docs-refresh task files and gap analysis ([896f4d2](https://github.com/CrackingShells/Hatch/commit/896f4d2)) +* docs(roadmap): mark adding-mcp-hosts-skill campaign as done ([b7e6c95](https://github.com/CrackingShells/Hatch/commit/b7e6c95)) +* docs(roadmap): mark mcp-docs-refresh tasks as done ([fc07cd1](https://github.com/CrackingShells/Hatch/commit/fc07cd1)) +* chore: cleanup `__reports__/` ([1056e52](https://github.com/CrackingShells/Hatch/commit/1056e52)) +* chore: move skills directory location ([f739fed](https://github.com/CrackingShells/Hatch/commit/f739fed)) +* chore: update cs-playbook submodule ([c544cb3](https://github.com/CrackingShells/Hatch/commit/c544cb3)) +* chore(roadmap): add adding-mcp-hosts-skill campaign ([e48ea10](https://github.com/CrackingShells/Hatch/commit/e48ea10)) +* chore(skill): package adding-mcp-hosts skill ([e5fbfa2](https://github.com/CrackingShells/Hatch/commit/e5fbfa2)) + ## 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) diff --git a/README.md b/README.md index b6cb5d5..18fa1b3 100644 --- a/README.md +++ b/README.md @@ -2,48 +2,36 @@ ![Hatch Logo](https://raw.githubusercontent.com/CrackingShells/Hatch/refs/heads/main/docs/resources/images/Logo/hatch_wide_dark_bg_transparent.png) -## Introduction +Hatch is a CLI tool for configuring MCP servers across AI host platforms. Adding a server to Claude Desktop, Cursor, VS Code, and others normally means editing separate JSON config files in different locations. Hatch does it from one command. -Hatch is the package manager for managing Model Context Protocol (MCP) servers with environment isolation, multi-type dependency resolution, and multi-host deployment. Deploy MCP servers to Claude Desktop, VS Code, Cursor, Kiro, Codex, and other platforms with automatic dependency management. +It also has a package system for managing MCP servers with dependency isolation, though that part is still being developed — see [Getting Started](./docs/articles/users/GettingStarted.md) for the current state. -The canonical documentation is at `docs/index.md` and published at . +**Current status:** suitable for development and trusted environments. Not hardened for production or multi-tenant use yet — see [Security and Trust](./docs/articles/users/SecurityAndTrust.md). -## Key Features +## What it does -- **Environment Isolation** — Create separate, isolated workspaces for different projects without conflicts -- **Multi-Type Dependency Resolution** — Automatically resolve and install system packages, Python packages, Docker containers, and Hatch packages -- **Multi-Host Deployment** — Configure MCP servers on multiple host platforms -- **Package Validation** — Ensure packages meet schema requirements before distribution -- **Development-Focused** — Optimized for rapid development and testing of MCP server ecosystems +- Configure MCP servers on one or more AI host platforms at once +- Discover which host platforms are installed on your machine +- List and inspect server registrations across all your tools +- Manage MCP server packages with dependency isolation (system, Python, Docker) ## Supported MCP Hosts -Hatch supports deployment to the following MCP host platforms: +Claude Desktop, Claude Code, VS Code, Cursor, Kiro, Codex, LM Studio, Google Gemini CLI, Mistral Vibe, OpenCode, Augment Code (Auggie CLI and Intent) -- **Claude Desktop** — Anthropic's desktop application for Claude with native MCP support -- **Claude Code** — Claude integration for VS Code with MCP capabilities -- **VS Code** — Visual Studio Code with the MCP extension for tool integration -- **Cursor** — AI-first code editor with built-in MCP server support -- **Kiro** — Kiro IDE with MCP support -- **Codex** — OpenAI Codex with MCP server configuration support -- **LM Studio** — Local LLM inference platform with MCP server integration -- **Google Gemini CLI** — Command-line interface for Google's Gemini model with MCP support - -## Quick Start - -### Install from PyPI +## Install ```bash pip install hatch-xclam ``` -Verify installation: +Verify: ```bash hatch --version ``` -### Install from source +Or install from source: ```bash git clone https://github.com/CrackingShells/Hatch.git @@ -51,76 +39,72 @@ cd Hatch pip install -e . ``` -### Create your first environment and *Hatch!* MCP server package +## Usage -```bash -# Create an isolated environment -hatch env create my_project +### Configure MCP servers on your hosts -# Switch to it -hatch env use my_project +```bash +# Local server via npx — register it on VS Code +hatch mcp configure context7 --host vscode \ + --command npx --args "-y @upstash/context7-mcp" -# Create a package template -hatch create my_mcp_server --description "My MCP server" +# Remote server with an auth header — register it on Gemini CLI +export GIT_PAT_TOKEN=your_github_personal_access_token +hatch mcp configure github-mcp --host gemini \ + --httpUrl https://api.github.com/mcp \ + --header Authorization="Bearer $GIT_PAT_TOKEN" -# Validate the package -hatch validate ./my_mcp_server +# Register the same server on multiple hosts at once +hatch mcp configure my-server --host claude-desktop,cursor,vscode \ + --command python --args "-m my_server" ``` -### Deploy MCP servers to your tools - -**Package-First Deployment (Recommended)** — Add a Hatch package and automatically configure it on Claude Desktop and Cursor: +### Inspect what is configured ```bash -hatch package add ./my_mcp_server --host claude-desktop,cursor +# See all servers across all hosts +hatch mcp list servers + +# See all hosts a specific server is registered on +hatch mcp show servers --server "context7" + +# Detect which MCP host platforms are installed +hatch mcp discover hosts ``` -**Direct Configuration (Advanced)** — Configure arbitrary MCP servers on your hosts: +### Package management (in development) -```bash -# Remote server example: GitHub MCP Server with authentication -export GIT_PAT_TOKEN=your_github_personal_access_token -hatch mcp configure github-mcp --host gemini \ - --httpUrl https://api.github.com/mcp \ - --header Authorization="Bearer $GIT_PAT_TOKEN" +The package system lets you install MCP servers with automatic dependency resolution and environment isolation. It is functional but being reworked for better integration with MCP registries. -# Local server example: Context7 via npx -hatch mcp configure context7 --host vscode \ - --command npx --args "-y @upstash/context7-mcp" +```bash +hatch env create my_project +hatch env use my_project +hatch package add ./my_mcp_server ``` ## Documentation -- **[Full Documentation](https://hatch.readthedocs.io/en/latest/)** — Complete reference and guides -- **[Getting Started](./docs/articles/users/GettingStarted.md)** — Quick start for users -- **[CLI Reference](./docs/articles/users/CLIReference.md)** — All commands and options -- **[Tutorials](./docs/articles/users/tutorials/)** — Step-by-step guides from installation to package authoring -- **[MCP Host Configuration](./docs/articles/users/MCPHostConfiguration.md)** — Deploy to multiple platforms -- **[Developer Docs](./docs/articles/devs/)** — Architecture, implementation guides, and contribution guidelines -- **[Troubleshooting](./docs/articles/users/Troubleshooting/ReportIssues.md)** — Common issues and solutions +- **[Full Documentation](https://hatch.readthedocs.io/en/latest/)** +- **[Getting Started](./docs/articles/users/GettingStarted.md)** +- **[CLI Reference](./docs/articles/users/CLIReference.md)** +- **[MCP Host Configuration](./docs/articles/users/MCPHostConfiguration.md)** +- **[Tutorials](./docs/articles/users/tutorials/)** +- **[Troubleshooting](./docs/articles/users/Troubleshooting/ReportIssues.md)** ## Contributing -We welcome contributions! See the [How to Contribute](./docs/articles/devs/contribution_guides/how_to_contribute.md) guide for details. - -### Quick start for developers - -1. **Fork and clone** the repository -2. **Install dependencies**: `pip install -e .` and `npm install` -3. **Create a feature branch**: `git checkout -b feat/your-feature` -4. **Make changes** and add tests -5. **Use conventional commits**: `npm run commit` for guided commits -6. **Run tests**: `wobble` -7. **Create a pull request** - -We use [Conventional Commits](https://www.conventionalcommits.org/) for automated versioning. Use `npm run commit` for guided commit messages. +We welcome contributions. See [How to Contribute](./docs/articles/devs/contribution_guides/how_to_contribute.md) for details. -## Getting Help +Quick setup: -- Search existing [GitHub Issues](https://github.com/CrackingShells/Hatch/issues) -- Read [Troubleshooting](./docs/articles/users/Troubleshooting/ReportIssues.md) for common problems -- Check [Developer Onboarding](./docs/articles/devs/development_processes/developer_onboarding.md) for setup help +1. Fork and clone the repository +2. Install dependencies: `pip install -e .` and `npm install` +3. Create a feature branch: `git checkout -b feat/your-feature` +4. Make changes and add tests +5. Use conventional commits: `npm run commit` +6. Run tests: `wobble` +7. Open a pull request ## License -This project is licensed under the GNU Affero General Public License v3 — see `LICENSE` for details. +GNU Affero General Public License v3 — see `LICENSE` for details. 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__/mistral_vibe/00-parameter_analysis_v0.md b/__reports__/mistral_vibe/00-parameter_analysis_v0.md new file mode 100644 index 0000000..750a7b7 --- /dev/null +++ b/__reports__/mistral_vibe/00-parameter_analysis_v0.md @@ -0,0 +1,55 @@ +# Mistral Vibe Parameter Analysis + +## Model + +| Item | Finding | +| --- | --- | +| Host | Mistral Vibe | +| Config path | `./.vibe/config.toml` first, fallback `~/.vibe/config.toml` | +| Config key | `mcp_servers` | +| Structure | TOML array-of-tables: `[[mcp_servers]]` | +| Server identity | Inline `name` field per entry | + +## Field Summary + +| Category | Fields | +| --- | --- | +| Transport | `transport`, `command`, `args`, `url` | +| Common | `headers`, `prompt`, `startup_timeout_sec`, `tool_timeout_sec`, `sampling_enabled` | +| Auth | `api_key_env`, `api_key_header`, `api_key_format` | +| Local-only | `env` | + +## Host Spec + +```yaml +host: mistral-vibe +format: toml +config_key: mcp_servers +config_paths: + - ./.vibe/config.toml + - ~/.vibe/config.toml +transport_discriminator: transport +supported_transports: + - stdio + - http + - streamable-http +canonical_mapping: + type_to_transport: + stdio: stdio + http: http + sse: streamable-http + httpUrl_to_url: true +extra_fields: + - prompt + - sampling_enabled + - api_key_env + - api_key_header + - api_key_format + - startup_timeout_sec + - tool_timeout_sec +``` + +## Sources + +- Mistral Vibe README and docs pages for config path precedence +- Upstream source definitions for MCP transport variants in `vibe/core/config` diff --git a/__reports__/mistral_vibe/01-architecture_analysis_v0.md b/__reports__/mistral_vibe/01-architecture_analysis_v0.md new file mode 100644 index 0000000..9cf392c --- /dev/null +++ b/__reports__/mistral_vibe/01-architecture_analysis_v0.md @@ -0,0 +1,26 @@ +# Mistral Vibe Architecture Analysis + +## Model + +| Layer | Change | +| --- | --- | +| Unified model | Add Vibe-native fields and host enum | +| Adapter | New `MistralVibeAdapter` to map canonical fields to Vibe TOML entries | +| Strategy | New TOML strategy for `[[mcp_servers]]` read/write with key preservation | +| Registries | Add adapter, strategy, backup/reporting, and fixture registration | +| Tests | Extend generic adapter suites and add focused TOML strategy tests | + +## Integration Notes + +| Concern | Decision | +| --- | --- | +| Local vs global config | Prefer existing project-local file, otherwise global fallback | +| Remote transport mapping | Canonical `type=sse` maps to Vibe `streamable-http` | +| Cross-host sync | Accept canonical `type` and `httpUrl`, serialize to `transport` + `url` | +| Non-MCP settings | Preserve other top-level TOML keys on write | + +## Assessment + +- **GO** — current adapter/strategy architecture already supports one more standalone TOML host. +- No dependency installation is required. +- Main regression surface is registry completeness and TOML round-tripping, covered by targeted tests. diff --git a/__reports__/mistral_vibe/README.md b/__reports__/mistral_vibe/README.md new file mode 100644 index 0000000..9e566e1 --- /dev/null +++ b/__reports__/mistral_vibe/README.md @@ -0,0 +1,12 @@ +# Mistral Vibe Reports + +## Status + +- Latest discovery: `00-parameter_analysis_v0.md` +- Latest architecture analysis: `01-architecture_analysis_v0.md` +- Current assessment: `GO` + +## Documents + +1. `00-parameter_analysis_v0.md` — upstream config path/schema discovery and host spec +2. `01-architecture_analysis_v0.md` — integration plan, touched files, and go/no-go assessment 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/api/cli/mcp.md b/docs/articles/api/cli/mcp.md index a0b824f..1162c74 100644 --- a/docs/articles/api/cli/mcp.md +++ b/docs/articles/api/cli/mcp.md @@ -22,8 +22,11 @@ This module provides handlers for: - vscode: Visual Studio Code with Copilot - kiro: Kiro IDE - codex: OpenAI Codex -- lm-studio: LM Studio +- lmstudio: LM Studio - gemini: Google Gemini +- mistral-vibe: Mistral Vibe CLI coding agent +- opencode: OpenCode AI coding assistant +- augment: Augment Code AI assistant ## Handler Functions diff --git a/docs/articles/appendices/LimitsAndKnownIssues.md b/docs/articles/appendices/LimitsAndKnownIssues.md index 516ab05..0fdbbf5 100644 --- a/docs/articles/appendices/LimitsAndKnownIssues.md +++ b/docs/articles/appendices/LimitsAndKnownIssues.md @@ -1,34 +1,14 @@ # Limits and Known Issues -This appendix documents current limitations and known issues in Hatch v0.4.2, organized by impact severity and architectural domain. +This appendix documents current limitations and known issues in Hatch v0.8.1, organized by impact severity and architectural domain. ## Critical Limitations (High Impact) -### Non-Interactive Environment Handling - -**Issue**: The dependency installation orchestrator can block indefinitely in non-TTY environments. - -**Code Location**: `hatch/installers/dependency_installation_orchestrator.py:501` (`_request_user_consent`) - -**Symptoms**: - -- Hangs in CI/CD pipelines when TTY is unavailable -- Docker container execution may hang indefinitely -- Programmatic integration requires foreknowledge of `--auto-approve` parameter - -**Workaround**: Use `--auto-approve` flag for automated scenarios - -```bash -hatch package add my-package --auto-approve -``` - -**Root Cause**: Blocking `input()` call without TTY detection or environment variable fallback mechanisms. - ### System Package Version Constraint Simplification **Issue**: Complex version constraints for system packages are reduced to "install latest" with only warning messages. -**Code Location**: `hatch/installers/system_installer.py:332-365` (`_build_apt_command`) +**Code Location**: `hatch/installers/system_installer.py:366-403` (`_build_apt_command`) **Symptoms**: @@ -46,8 +26,9 @@ hatch package add my-package --auto-approve **Code Locations**: -- `hatch/environment_manager.py:85-90` -- `hatch/package_loader.py:80-85` +- `hatch/environment_manager.py:172-179` (`_save_environments`) +- `hatch/environment_manager.py:220` (`set_current_environment`, `current_env` file write) +- `hatch/package_loader.py:139-145` (cache write in `download_package`) **Symptoms**: @@ -63,25 +44,25 @@ hatch package add my-package --auto-approve ### Registry Fetch Fragility -**Issue**: Registry fetching uses date-based URL construction with limited fallback robustness. +**Issue**: Registry fetching has no retry logic for transient network failures. -**Code Location**: `hatch/registry_retriever.py:45-65` +**Code Location**: `hatch/registry_retriever.py:200-231` (`_fetch_remote_registry`) **Symptoms**: -- Package discovery breaks when registry publishing is delayed +- A single transient network error causes the fetch to fail immediately with no retry - Poor error messages during network connectivity issues -- Development workflow disruption during registry maintenance +- Development workflow disruption during registry unavailability **Workaround**: Use local packages (`hatch package add ./local-package`) when registry is unavailable -**Root Cause**: Registry URL construction assumes daily publishing schedule without robust fallback strategies. +**Root Cause**: Network requests are single-attempt with no retry strategy or back-off logic. ### Package Integrity Verification Gap **Issue**: Downloaded packages are not cryptographically verified for integrity. -**Code Location**: `hatch/package_loader.py:75-125` (`download_package`) +**Code Location**: `hatch/package_loader.py:56-157` (`download_package`) **Symptoms**: @@ -95,25 +76,25 @@ hatch package add my-package --auto-approve ### Cross-Platform Python Environment Detection -**Issue**: Hard-coded path assumptions limit Python environment detection across different platforms and installations. +**Issue**: Hard-coded path assumptions limit Python environment detection for non-standard conda/mamba installations. -**Code Location**: `hatch/python_environment_manager.py:85-120` (`_detect_conda_mamba`) +**Code Location**: `hatch/python_environment_manager.py:65-125` (`_detect_manager`) **Symptoms**: -- Inconsistent behavior across different conda installations -- Silent feature degradation when Python environments unavailable -- User confusion about Python integration capabilities +- Inconsistent behavior with custom conda/mamba installation locations +- Silent feature degradation when conda/mamba is not in a standard path +- User confusion about Python integration capabilities on non-standard setups -**Workaround**: Ensure conda/mamba are in system PATH or use explicit paths +**Workaround**: Set the `CONDA_EXE` or `MAMBA_EXE` environment variable to point to your conda/mamba executable, or ensure it is in your system PATH -**Root Cause**: Platform-specific path assumptions and limited environment variable checking. +**Root Cause**: Hard-coded fallback paths cover only common installation locations (`~/miniconda3`, `~/anaconda3`, `/opt/conda`). `PATH` and `CONDA_EXE`/`MAMBA_EXE` environment variables are checked first but may not cover all installation scenarios. ### Error Recovery and Rollback Gaps **Issue**: Limited transactional semantics during multi-dependency installation. -**Code Location**: `hatch/installers/dependency_installation_orchestrator.py:550-580` (`_execute_install_plan`) +**Code Location**: `hatch/installers/dependency_installation_orchestrator.py:605-681` (`_execute_install_plan`) **Symptoms**: @@ -147,7 +128,7 @@ hatch package add my-package --auto-approve **Issue**: Templates assume specific MCP server structure and dependencies. -**Code Location**: `hatch/template_generator.py:130-140` +**Code Location**: `hatch/template_generator.py` (particularly `generate_mcp_server_py:33`, `generate_hatch_mcp_server_entry_py:63`, `generate_metadata_json:87`) **Symptoms**: @@ -163,17 +144,17 @@ hatch package add my-package --auto-approve **Issue**: Limited handling of circular dependencies and complex version constraints. -**Code Location**: `hatch/installers/dependency_installation_orchestrator.py:290-320` +**Code Location**: `hatch/installers/dependency_installation_orchestrator.py:337-358` (`_get_install_ready_hatch_dependencies`) **Symptoms**: -- Potential infinite loops during dependency resolution +- Potential failures during dependency resolution for circular or deeply nested graphs - Unclear error messages for complex dependency conflicts - Unexpected behavior with deeply nested dependency trees **Workaround**: Simplify dependency structures and avoid circular dependencies -**Root Cause**: Dependency graph builder lacks edge case handling for complex scenarios. +**Root Cause**: Dependency graph resolution is delegated to `hatch_validator.utils.hatch_dependency_graph.HatchDependencyGraphBuilder`; edge case robustness depends on that external library. ## Minor Limitations (Quality of Life) @@ -181,7 +162,7 @@ hatch package add my-package --auto-approve **Issue**: System package installation assumes `sudo` availability without proper validation. -**Code Location**: `hatch/installers/system_installer.py:365-380` +**Code Location**: `hatch/installers/system_installer.py:382-403` (`_build_apt_command`, `install`) **Symptoms**: @@ -192,53 +173,45 @@ hatch package add my-package --auto-approve ### Simulation and Dry-Run Gaps -**Issue**: Inconsistent simulation mode implementation across installers. +**Issue**: Simulation mode infrastructure exists but is not yet wired through the orchestrator. -**Code Locations**: Various installer modules +**Code Locations**: + +- `hatch/installers/dependency_installation_orchestrator.py:635` (`simulation_mode=False`, marked "Future enhancement") +- `hatch/installers/system_installer.py:152` (simulation mode fully implemented at installer level) **Symptoms**: -- No unified dry-run capability across all dependency types +- No dry-run capability reachable through normal `hatch package add` flow +- `SystemInstaller` has full `apt-get --dry-run` support ready but not yet exposed - Limited preview capabilities for complex installation plans **Workaround**: Test installations in isolated environments first +**Root Cause**: Planned feature not yet implemented. `InstallationContext` supports `simulation_mode` and individual installers handle it, but the orchestrator does not yet accept or pass through a simulation flag. + ### Cache Management Strategy **Issue**: Basic TTL-based caching without intelligent invalidation or size limits. **Code Locations**: -- `hatch/package_loader.py:40-50` -- `hatch/registry_retriever.py:35-45` +- `hatch/registry_retriever.py:37` (24-hour TTL constant) +- `hatch/package_loader.py` (presence-only caching, no TTL or size limits) **Symptoms**: -- Fixed 24-hour TTL regardless of registry update frequency +- Fixed 24-hour TTL for registry data regardless of registry update frequency +- Package cache never expires — only invalidated by `force_download=True` - No automatic cache cleanup for disk space management - Force refresh only available at operation level **Workaround**: Manually clear cache directory when needed: ```bash -rm -rf ~/.hatch/cache/* +rm -rf ~/.hatch/packages/* ``` -### External Dependency Coupling - -**Issue**: Validator dependency fetched via git URL with network requirements. - -**Code Location**: `pyproject.toml:24` - -**Details**: `hatch_validator @ git+https://github.com/CrackingShells/Hatch-Validator.git@v0.6.3` - -**Symptoms**: - -- Build-time network access required -- Dependency on repository and tag availability - -**Workaround**: Ensure network access during installation or consider local installation methods - ### Documentation and Schema Evolution **Issue**: Limited handling of package schema version transitions. @@ -257,20 +230,19 @@ rm -rf ~/.hatch/cache/* | Severity | Automation | Reliability | Development | |----------|------------|-------------|-------------| -| **Critical** | Non-interactive handling | Concurrent access, System constraints | - | +| **Critical** | - | Concurrent access, System constraints | - | | **Significant** | Registry fragility, Error recovery | Package integrity, Python detection | - | | **Moderate** | - | - | Observability, Templates, Dependency resolution | -| **Minor** | Simulation gaps | Security context, Cache strategy | External coupling, Schema evolution | +| **Minor** | Simulation gaps | Security context, Cache strategy | Schema evolution | ## Recommended Mitigation Strategies ### For Production Use -1. **Always use `--auto-approve`** for automated deployments -2. **Avoid concurrent operations** until race conditions are resolved -3. **Use exact version constraints** for system packages when possible -4. **Implement external monitoring** for installation operations -5. **Regularly backup environment configurations** +1. **Avoid concurrent operations** until race conditions are resolved +2. **Use exact version constraints** for system packages when possible +3. **Implement external monitoring** for installation operations +4. **Regularly backup environment configurations** ### For Development @@ -290,7 +262,7 @@ rm -rf ~/.hatch/cache/* The Hatch team is aware of these limitations and they are prioritized for future releases: -**Phase 1 (Stability)**: Address concurrent access, non-interactive handling, and error recovery +**Phase 1 (Stability)**: Address concurrent access and error recovery **Phase 2 (Security)**: Implement package integrity verification and security context validation **Phase 3 (Robustness)**: Improve cross-platform consistency and system package handling **Phase 4 (Quality)**: Enhance observability, caching, and template flexibility diff --git a/docs/articles/devs/architecture/mcp_backup_system.md b/docs/articles/devs/architecture/mcp_backup_system.md index 8aaafc6..34f167f 100644 --- a/docs/articles/devs/architecture/mcp_backup_system.md +++ b/docs/articles/devs/architecture/mcp_backup_system.md @@ -146,6 +146,7 @@ The system supports all MCP host platforms: | `cursor` | Cursor IDE MCP integration | | `lmstudio` | LM Studio MCP support | | `gemini` | Google Gemini MCP integration | +| `mistral-vibe` | Mistral Vibe CLI coding agent | ## Performance Characteristics diff --git a/docs/articles/devs/architecture/mcp_host_configuration.md b/docs/articles/devs/architecture/mcp_host_configuration.md index 79cfc4b..2784e51 100644 --- a/docs/articles/devs/architecture/mcp_host_configuration.md +++ b/docs/articles/devs/architecture/mcp_host_configuration.md @@ -10,7 +10,7 @@ This article covers: ## Overview -The MCP host configuration system manages Model Context Protocol server configurations across multiple host platforms (Claude Desktop, VS Code, Cursor, Gemini, Kiro, Codex, LM Studio). It uses the **Unified Adapter Architecture**: a single data model with host-specific adapters for validation and serialization. +The MCP host configuration system manages Model Context Protocol server configurations across multiple host platforms (Claude Desktop, Claude Code, VS Code, Cursor, LM Studio, Gemini, Kiro, Codex, Mistral Vibe, OpenCode, Augment). It uses the **Unified Adapter Architecture**: a single data model with host-specific adapters for validation and serialization. > **Adding a new host?** See the [Implementation Guide](../implementation_guides/mcp_host_configuration_extension.md) for step-by-step instructions. @@ -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:** @@ -112,6 +143,7 @@ supported = registry.get_supported_hosts() # List all hosts - `claude-desktop`, `claude-code` - `vscode`, `cursor`, `lmstudio` - `gemini`, `kiro`, `codex` +- `mistral-vibe` ### BaseAdapter Protocol @@ -148,8 +180,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 +201,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 +330,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 +501,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 +586,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/docs/articles/users/CLIReference.md b/docs/articles/users/CLIReference.md index 4eb6951..527c61c 100644 --- a/docs/articles/users/CLIReference.md +++ b/docs/articles/users/CLIReference.md @@ -1,1113 +1,972 @@ -# CLI Reference - -This document is a compact reference of all Hatch CLI commands and options implemented in the `hatch/cli/` package, presented as tables for quick lookup. - -## Table of Contents - -``` -- [Global options](#global-options) -- [Commands](#commands) - - [hatch create](#hatch-create) - - [hatch validate](#hatch-validate) - - [hatch env](#hatch-env-environment-management) - - [hatch env create](#hatch-env-create) - - [hatch env remove](#hatch-env-remove) - - [hatch env list](#hatch-env-list) - - [hatch env use](#hatch-env-use) - - [hatch env current](#hatch-env-current) - - [hatch env python](#hatch-env-python-advanced-python-environment-subcommands) - - [hatch env python init](#hatch-env-python-init) - - [hatch env python info](#hatch-env-python-info) - - [hatch env python add-hatch-mcp](#hatch-env-python-add-hatch-mcp) - - [hatch env python remove](#hatch-env-python-remove) - - [hatch env python shell](#hatch-env-python-shell) - - [hatch package](#hatch-package-package-management) - - [hatch package add](#hatch-package-add) - - [hatch package remove](#hatch-package-remove) - - [hatch package list](#hatch-package-list) - - [hatch package sync](#hatch-package-sync) - - [hatch mcp](#hatch-mcp) - - [hatch mcp configure](#hatch-mcp-configure) - - [hatch mcp sync](#hatch-mcp-sync) - - [hatch mcp remove server](#hatch-mcp-remove-server) - - [hatch mcp remove host](#hatch-mcp-remove-host) - - [hatch mcp list hosts](#hatch-mcp-list-hosts) - - [hatch mcp list servers](#hatch-mcp-list-servers) - - [hatch mcp discover hosts](#hatch-mcp-discover-hosts) - - [hatch mcp discover servers](#hatch-mcp-discover-servers) - - [hatch mcp backup list](#hatch-mcp-backup-list) - - [hatch mcp backup restore](#hatch-mcp-backup-restore) - - [hatch mcp backup clean](#hatch-mcp-backup-clean) -``` - -## Global options - -These flags are accepted by the top-level parser and apply to all commands unless overridden. - -| Flag | Type | Description | Default | -|------|------|-------------|---------| -| `--version` | flag | Show program version and exit | n/a | -| `--envs-dir` | path | Directory to store environments | `~/.hatch/envs` | -| `--cache-ttl` | int | Cache time-to-live in seconds | `86400` (1 day) | -| `--cache-dir` | path | Directory to store cached packages | `~/.hatch/cache` | - -Example: - -```bash -hatch --version -# Output: hatch 0.6.1 -``` - -## Commands - -Each top-level command has its own table. Use the Syntax line before the table to see how to call it. - -### `hatch create` - -Create a new package template. - -Syntax: - -`hatch create [--dir DIR] [--description DESC]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `name` | string (positional) | Package name (required) | n/a | -| `--dir`, `-d` | path | Target directory for the template | current directory | -| `--description`, `-D` | string | Package description | empty string | - -Examples: - -`hatch create my_package` - -`hatch create my_package --dir ./packages --description "My awesome package"` - ---- - -### `hatch validate` - -Validate a package structure and metadata. - -Syntax: - -`hatch validate ` - -| Argument | Type | Description | -|---:|---|---| -| `package_dir` | path (positional) | Path to package directory to validate (required) | - -Examples: - -`hatch validate ./my_package` - ---- - -### `hatch env` (environment management) - -Top-level syntax: `hatch env ...` - -#### `hatch env create` - -Create a new Hatch environment bootstrapping a Python/conda environment. - -Syntax: - -`hatch env create [--description DESC] [--python-version VER] [--no-python] [--no-hatch-mcp-server] [--hatch_mcp_server_tag TAG]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `name` | string (positional) | Environment name (required) | n/a | -| `--description`, `-D` | string | Human-readable environment description | empty string | -| `--python-version` | string | Python version to create (e.g., `3.11`) | none (manager default) | -| `--no-python` | flag | Do not create a Python environment (skip conda/mamba) | false | -| `--no-hatch-mcp-server` | flag | Do not install `hatch_mcp_server` wrapper | false | -| `--hatch-mcp-server-tag` | string | Git tag/branch for wrapper installation (e.g., `dev`, `v0.1.0`) | none | - -#### `hatch env remove` - -Syntax: - -`hatch env remove ` - -| Argument | Type | Description | -|---:|---|---| -| `name` | string (positional) | Environment name to remove (required) | - -#### `hatch env list` - -List all environments with package counts. - -Syntax: - -`hatch env list [--pattern PATTERN] [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--pattern` | string | Filter environments by name using regex pattern | none | -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch env list -Environments: - Name Python Packages - ─────────────────────────────────────── - * default 3.14.2 0 - test-env 3.11.5 3 -``` - -**Key Details**: -- Header: `"Environments:"` only -- Columns: Name (width 15), Python (width 10), Packages (width 10, right-aligned) -- Current environment marked with `"* "` prefix -- Packages column shows COUNT only -- Separator: `"─"` character (U+2500) - -#### `hatch env list hosts` - -List environment/host/server deployments from environment data. - -Syntax: - -`hatch env list hosts [--env PATTERN] [--server PATTERN] [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--env`, `-e` | string | Filter by environment name using regex pattern | none | -| `--server` | string | Filter by server name using regex pattern | none | -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch env list hosts -Environment Host Deployments: - Environment Host Server Version - ───────────────────────────────────────────────────────────────── - default claude-desktop weather-server 1.0.0 - default cursor weather-server 1.0.0 -``` - -**Description**: -Lists environment/host/server deployments from environment data. Shows only Hatch-managed packages and their host deployments. - -#### `hatch env list servers` - -List environment/server/host deployments from environment data. - -Syntax: - -`hatch env list servers [--env PATTERN] [--host PATTERN] [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--env`, `-e` | string | Filter by environment name using regex pattern | none | -| `--host` | string | Filter by host name using regex pattern (use '-' for undeployed) | none | -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch env list servers -Environment Servers: - Environment Server Host Version - ───────────────────────────────────────────────────────────────── - default weather-server claude-desktop 1.0.0 - default weather-server cursor 1.0.0 - test-env utility-pkg - 2.1.0 -``` - -**Description**: -Lists environment/server/host deployments from environment data. Shows only Hatch-managed packages. Undeployed packages show '-' in Host column. - -#### `hatch env show` - -Display detailed hierarchical view of a specific environment. - -Syntax: - -`hatch env show ` - -| Argument | Type | Description | -|---:|---|---| -| `name` | string (positional) | Environment name to show (required) | - -**Example Output**: - -```bash -$ hatch env show default -Environment: default (active) - Description: My development environment - Created: 2026-01-15 10:30:00 - - Python Environment: - Version: 3.14.2 - Executable: /path/to/python - Conda env: N/A - Status: Active - - Packages (2): - weather-server - Version: 1.0.0 - Source: registry (https://registry.example.com) - Deployed to: claude-desktop, cursor - - utility-pkg - Version: 2.1.0 - Source: local (/path/to/package) - Deployed to: (none) -``` - -**Key Details**: -- Header shows `"(active)"` suffix if current environment -- Hierarchical structure with 2-space indentation -- No separator lines between sections -- Packages section shows count in header -- Each package shows version, source, and deployed hosts - -#### `hatch env use` - -Syntax: - -`hatch env use ` - -| Argument | Type | Description | -|---:|---|---| -| `name` | string (positional) | Environment name to set as current (required) | - -#### `hatch env current` - -Syntax: - -`hatch env current` - -Description: Print the name of the current environment. - ---- - -### `hatch env python` (advanced Python environment subcommands) - -Top-level syntax: `hatch env python ...` - -#### `hatch env python init` - -Initialize or recreate a Python environment inside a Hatch environment. - -Syntax: - -`hatch env python init [--hatch_env NAME] [--python-version VER] [--force] [--no-hatch-mcp-server] [--hatch_mcp_server_tag TAG]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--hatch_env` | string | Hatch environment name (defaults to current env) | current environment | -| `--python-version` | string | Desired Python version (e.g., `3.12`) | none | -| `--force` | flag | Force recreation if it already exists | false | -| `--no-hatch-mcp-server` | flag | Skip installing `hatch_mcp_server` wrapper | false | -| `--hatch_mcp_server_tag` | string | Git tag/branch for wrapper installation | none | - -#### `hatch env python info` - -Show information about the Python environment for a Hatch environment. - -Syntax: - -`hatch env python info [--hatch_env NAME] [--detailed]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | -| `--detailed` | flag | Show additional diagnostics and package listing | false | - -When available this command prints: status, python executable, python version, conda env name, environment path, creation time, package count and package list. With `--detailed` it also prints diagnostics from the manager. - -#### `hatch env python add-hatch-mcp` - -Install the `hatch_mcp_server` wrapper into the Python environment of a Hatch env. - -Syntax: - -`hatch env python add-hatch-mcp [--hatch_env NAME] [--tag TAG]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | -| `--tag` | string | Git tag/branch for wrapper install | none | - -#### `hatch env python remove` - -Remove the Python environment associated with a Hatch environment. - -Syntax: - -`hatch env python remove [--hatch_env NAME] [--force]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | -| `--force` | flag | Skip confirmation prompt and force removal | false | - -#### `hatch env python shell` - -Launch a Python REPL or run a single command inside the Python environment. - -Syntax: - -`hatch env python shell [--hatch_env NAME] [--cmd CMD]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | -| `--cmd` | string | Command to execute inside the Python shell (optional) | none | - ---- - -### `hatch package` (package management) - -Top-level syntax: `hatch package ...` - -#### `hatch package add` - -Add a package (local path or registry name) into an environment. - -Syntax: - -`hatch package add [--env NAME] [--version VER] [--force-download] [--refresh-registry] [--auto-approve]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `package_path_or_name` | string (positional) | Path to package directory or registry package name (required) | n/a | -| `--env`, `-e` | string | Target Hatch environment name (defaults to current) | current environment | -| `--version`, `-v` | string | Version for registry packages | none | -| `--force-download`, `-f` | flag | Force fetching even if cached | false | -| `--refresh-registry`, `-r` | flag | Refresh registry metadata before resolving | false | -| `--auto-approve` | flag | Automatically approve dependency installation prompts | false | -| `--host` | string | Comma-separated list of MCP host platforms to configure (e.g., claude-desktop,cursor) | none | - -**Note:** Dependency installation prompts are also automatically approved in non-TTY environments (such as CI/CD pipelines) or when the `HATCH_AUTO_APPROVE` environment variable is set. See [Environment Variables](#environment-variables) for details. - -**MCP Host Integration:** When adding a package, if the `--host` flag is specified, Hatch will automatically configure the package's MCP servers on the specified hosts. This includes analyzing package dependencies and configuring all related MCP servers. - -**MCP Host Integration Examples:** - -```bash -# Add package and automatically configure MCP servers on specific hosts -hatch package add ./my_package --host claude-desktop,cursor - -# Add package for all available hosts -hatch package add ./my_package --host all - -# Skip host configuration (no MCP servers configured) -hatch package add ./my_package - -# Add with other flags and MCP configuration -hatch package add registry_package --version 1.0.0 --env dev-env --host gemini --auto-approve -``` - -Examples: - -`hatch package add ./my_package` - -`hatch package add registry_package --version 1.0.0 --env dev-env --auto-approve` - -#### `hatch package remove` - -Remove a package from a Hatch environment. - -Syntax: - -`hatch package remove [--env NAME]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `package_name` | string (positional) | Name of the package to remove (required) | n/a | -| `--env`, `-e` | string | Hatch environment name (defaults to current) | current environment | - -#### `hatch package list` - -**⚠️ DEPRECATED**: This command is deprecated. Use `hatch env list` to see packages inline with environment information, or `hatch env show ` for detailed package information. - -List packages installed in a Hatch environment. - -Syntax: - -`hatch package list [--env NAME]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--env`, `-e` | string | Hatch environment name (defaults to current) | current environment | - -**Example Output**: - -```bash -$ hatch package list -Warning: 'hatch package list' is deprecated. Use 'hatch env list' instead, which shows packages inline. -Packages in environment 'default': -weather-server (1.0.0) Hatch compliant: True source: https://registry.example.com location: /path/to/package -``` - -**Migration Guide**: -- For package counts: Use `hatch env list` (shows package count per environment) -- For detailed package info: Use `hatch env show ` (shows full package details) -- For deployment info: Use `hatch env list hosts` or `hatch env list servers` - -#### `hatch package sync` - -Synchronize package MCP servers to host platforms. - -Syntax: - -`hatch package sync --host [--env ENV] [--dry-run] [--auto-approve] [--no-backup]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `package_name` | string (positional) | Name of package whose MCP servers to sync | n/a | -| `--host` | string | Comma-separated list of host platforms or 'all' | n/a | -| `--env`, `-e` | string | Target Hatch environment name (defaults to current) | current environment | -| `--dry-run` | flag | Preview changes without execution | false | -| `--auto-approve` | flag | Skip confirmation prompts | false | -| `--no-backup` | flag | Disable default backup behavior of the MCP host's config file | false | - -Examples: - -`hatch package sync my-package --host claude-desktop` - -`hatch package sync weather-server --host claude-desktop,cursor --dry-run` - -# Multi-package synchronization examples -# Sync main package AND all its dependencies: -hatch package sync my-package --host all - -# Sync without creating backups -hatch package sync my-package --host claude-desktop --no-backup - ---- - -## Environment Variables - -Hatch recognizes the following environment variables to control behavior: - -| Variable | Description | Accepted Values | Default | -|----------|-------------|-----------------|---------| -| `HATCH_AUTO_APPROVE` | Automatically approve dependency installation prompts in non-interactive environments | `1`, `true`, `yes` (case-insensitive) | unset | - -### `HATCH_AUTO_APPROVE` - -When set to a truthy value (`1`, `true`, or `yes`, case-insensitive), this environment variable enables automatic approval of dependency installation prompts. This is particularly useful in CI/CD pipelines and other automated environments where user interaction is not possible. - -**Behavior:** - -- In TTY environments: User is still prompted for consent unless this variable is set -- In non-TTY environments: Installation is automatically approved regardless of this variable -- When set in any environment: Installation is automatically approved without prompting - -**Examples:** - -```bash -# Enable auto-approval for the current session -export HATCH_AUTO_APPROVE=1 -hatch package add my_package - -# Enable auto-approval for a single command -HATCH_AUTO_APPROVE=true hatch package add my_package - -# CI/CD pipeline usage -HATCH_AUTO_APPROVE=yes hatch package add production_package -``` - -**Note:** This environment variable works in conjunction with the `--auto-approve` CLI flag. Either method will enable automatic approval of installation prompts. - ---- - -## MCP Host Configuration Commands - -### `hatch mcp` - -Commands subset to manage non-hatch package MCP servers. -Top level syntax: ` ...` - -#### `hatch mcp configure` - -Configure an MCP server on a specific host platform. - -Syntax: - -`hatch mcp configure --host (--command CMD | --url URL) [--args ARGS] [--env-var ENV] [--header HEADER] [--dry-run] [--auto-approve] [--no-backup]` - -| Argument / Flag | Hosts | Type | Description | Default | -|---:|---|---|---|---| -| `server-name` | all | string (positional) | Name of the MCP server to configure | n/a | -| `--host` | all | string | Target host platform (claude-desktop, cursor, etc.) | n/a | -| `--command` | all | string | Command to execute for local servers (mutually exclusive with --url) | none | -| `--url` | all except Claude Desktop/Code | string | URL for remote MCP servers (mutually exclusive with --command) | none | -| `--http-url` | gemini | string | HTTP streaming endpoint URL | none | -| `--args` | all | string | Arguments for MCP server command (only with --command) | none | -| `--env-var` | all | string | Environment variables format: KEY=VALUE (can be used multiple times) | none | -| `--header` | all except Claude Desktop/Code | string | HTTP headers format: KEY=VALUE (only with --url) | none | -| `--timeout` | gemini | int | Request timeout in milliseconds | none | -| `--trust` | gemini | flag | Bypass tool call confirmations | false | -| `--cwd` | gemini, codex | string | Working directory for stdio transport | none | -| `--include-tools` | gemini, codex | multiple | Tool allowlist / enabled tools. Space-separated values. | none | -| `--exclude-tools` | gemini, codex | multiple | Tool blocklist / disabled tools. Space-separated values. | none | -| `--env-file` | cursor, vscode, lmstudio | string | Path to environment file | none | -| `--input` | vscode | multiple | Input variable definitions format: type,id,description[,password=true] | none | -| `--disabled` | kiro | flag | Disable the MCP server | false | -| `--auto-approve-tools` | kiro | multiple | Tool names to auto-approve. Can be used multiple times. | none | -| `--disable-tools` | kiro | multiple | Tool names to disable. Can be used multiple times. | none | -| `--env-vars` | codex | multiple | Environment variable names to whitelist/forward. Can be used multiple times. | none | -| `--startup-timeout` | codex | int | Server startup timeout in seconds (default: 10) | none | -| `--tool-timeout` | codex | int | Tool execution timeout in seconds (default: 60) | none | -| `--enabled` | codex | flag | Enable the MCP server | false | -| `--bearer-token-env-var` | codex | string | Name of env var containing bearer token for Authorization header | none | -| `--env-header` | codex | multiple | HTTP header from env var format: KEY=ENV_VAR_NAME. Can be used multiple times. | none | -| `--dry-run` | all | flag | Preview configuration without applying changes | false | -| `--auto-approve` | all | flag | Skip confirmation prompts | false | -| `--no-backup` | all | flag | Skip backup creation before configuration | false | - -**Behavior**: - -The command now displays a **conversion report** showing exactly what fields will be configured on the target host. This provides transparency about which fields are supported by the host and what values will be set. - -The conversion report shows: -- **UPDATED** fields: Fields being set with their new values (shown as `None --> value`) -- **UNSUPPORTED** fields: Fields not supported by the target host (automatically filtered out) -- **UNCHANGED** fields: Fields that already have the specified value (update operations only) - -Note: Internal metadata fields (like `name`) are not shown in the field operations list, as they are used for internal bookkeeping and are not written to host configuration files. The server name is displayed in the report header for context. - -**Example - Local Server Configuration**: - -```bash -$ hatch mcp configure my-server --host claude-desktop --command python --args server.py --env API_KEY=secret - -Server 'my-server' created for host 'claude-desktop': - command: UPDATED None --> 'python' - args: UPDATED None --> ['server.py'] - env: UPDATED None --> {'API_KEY': 'secret'} - -Configure MCP server 'my-server' on host 'claude-desktop'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'my-server' on host 'claude-desktop' -``` - -**Example - Remote Server Configuration**: - -```bash -$ hatch mcp configure api-server --host claude-desktop --url https://api.example.com --header Auth=token - -Server 'api-server' created for host 'claude-desktop': - name: UPDATED None --> 'api-server' - command: UPDATED None --> None - args: UPDATED None --> None - env: UPDATED None --> {} - url: UPDATED None --> 'https://api.example.com' - headers: UPDATED None --> {'Auth': 'token'} - -Configure MCP server 'api-server' on host 'claude-desktop'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'api-server' on host 'claude-desktop' -``` - -**Example - Advanced Gemini Configuration**: - -```bash -$ hatch mcp configure my-server --host gemini --command python --args server.py --timeout 30000 --trust --include-tools weather,calculator - -Server 'my-server' created for host 'gemini': - name: UPDATED None --> 'my-server' - command: UPDATED None --> 'python' - args: UPDATED None --> ['server.py'] - timeout: UPDATED None --> 30000 - trust: UPDATED None --> True - include_tools: UPDATED None --> ['weather', 'calculator'] - -Configure MCP server 'my-server' on host 'gemini'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'my-server' on host 'gemini' -``` - -**Example - Kiro Configuration**: - -```bash -$ hatch mcp configure my-server --host kiro --command python --args server.py --auto-approve-tools weather,calculator --disable-tools debug - -Server 'my-server' created for host 'kiro': - name: UPDATED None --> 'my-server' - command: UPDATED None --> 'python' - args: UPDATED None --> ['server.py'] - autoApprove: UPDATED None --> ['weather', 'calculator'] - disabledTools: UPDATED None --> ['debug'] - -Configure MCP server 'my-server' on host 'kiro'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'my-server' on host 'kiro' -``` - -**Example - Kiro with Disabled Server**: - -```bash -$ hatch mcp configure my-server --host kiro --command python --args server.py --disabled - -Server 'my-server' created for host 'kiro': - name: UPDATED None --> 'my-server' - command: UPDATED None --> 'python' - args: UPDATED None --> ['server.py'] - disabled: UPDATED None --> True - -Configure MCP server 'my-server' on host 'kiro'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'my-server' on host 'kiro' -``` - -**Example - Codex Configuration with Timeouts and Tool Filtering**: - -```bash -$ hatch mcp configure context7 --host codex --command npx --args "-y" "@upstash/context7-mcp" --env-vars PATH --env-vars HOME --startup-timeout 15 --tool-timeout 120 --enabled --include-tools read write --exclude-tools delete - -Server 'context7' created for host 'codex': - name: UPDATED None --> 'context7' - command: UPDATED None --> 'npx' - args: UPDATED None --> ['-y', '@upstash/context7-mcp'] - env_vars: UPDATED None --> ['PATH', 'HOME'] - startup_timeout_sec: UPDATED None --> 15 - tool_timeout_sec: UPDATED None --> 120 - enabled: UPDATED None --> True - enabled_tools: UPDATED None --> ['read', 'write'] - disabled_tools: UPDATED None --> ['delete'] - -Configure MCP server 'context7' on host 'codex'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'context7' on host 'codex' -``` - -**Example - Codex HTTP Server with Authentication**: - -```bash -$ hatch mcp configure figma --host codex --url https://mcp.figma.com/mcp --bearer-token-env-var FIGMA_OAUTH_TOKEN --env-header "X-Figma-Region=FIGMA_REGION" --header "X-Custom=static-value" - -Server 'figma' created for host 'codex': - name: UPDATED None --> 'figma' - url: UPDATED None --> 'https://mcp.figma.com/mcp' - bearer_token_env_var: UPDATED None --> 'FIGMA_OAUTH_TOKEN' - env_http_headers: UPDATED None --> {'X-Figma-Region': 'FIGMA_REGION'} - http_headers: UPDATED None --> {'X-Custom': 'static-value'} - -Configure MCP server 'figma' on host 'codex'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'figma' on host 'codex' -``` - -**Example - Remote Server Configuration**: - -```bash -$ hatch mcp configure api-server --host vscode --url https://api.example.com --header Auth=token - -Server 'api-server' created for host 'vscode': - name: UPDATED None --> 'api-server' - url: UPDATED None --> 'https://api.example.com' - headers: UPDATED None --> {'Auth': 'token'} - -Configure MCP server 'api-server' on host 'vscode'? [y/N]: y -[SUCCESS] Successfully configured MCP server 'api-server' on host 'vscode' -``` - -**Example - Dry Run Mode**: - -```bash -$ hatch mcp configure my-server --host gemini --command python --args server.py --dry-run - -[DRY RUN] Would configure MCP server 'my-server' on host 'gemini': -[DRY RUN] Command: python -[DRY RUN] Args: ['server.py'] -[DRY RUN] Backup: Enabled -[DRY RUN] Preview of changes for server 'my-server': - command: UPDATED None --> 'python' - args: UPDATED None --> ['server.py'] - -No changes were made. -``` - -**Host-Specific Field Support**: - -Different MCP hosts support different configuration fields. The conversion report automatically filters unsupported fields: - -- **Claude Desktop / Claude Code**: Supports universal fields only (command, args, env, url, headers, type) -- **Cursor / LM Studio**: Supports universal fields + envFile -- **VS Code**: Supports universal fields + envFile, inputs -- **Gemini CLI**: Supports universal fields + 14 additional fields (cwd, timeout, trust, OAuth settings, etc.) -- **Codex**: Supports universal fields + Codex-specific fields for URL-based servers (http_headers, env_http_headers, bearer_token_env_var, enabled, startup_timeout_sec, tool_timeout_sec, env_vars) - -When configuring a server with fields not supported by the target host, those fields are marked as UNSUPPORTED in the report and automatically excluded from the configuration. - -#### `hatch mcp sync` - -Synchronize MCP configurations across environments and hosts. - -The sync command displays a preview of servers to be synced before requesting confirmation, giving visibility into which servers will be affected. - -Syntax: - -`hatch mcp sync [--from-env ENV | --from-host HOST] --to-host HOSTS [--servers SERVERS | --pattern PATTERN] [--dry-run] [--auto-approve] [--no-backup]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--from-env` | string | Source Hatch environment (mutually exclusive with --from-host) | none | -| `--from-host` | string | Source host platform (mutually exclusive with --from-env) | none | -| `--to-host` | string | Target hosts (comma-separated or 'all') | n/a | -| `--servers` | string | Specific server names to sync (mutually exclusive with --pattern) | none | -| `--pattern` | string | Regex pattern for server selection (mutually exclusive with --servers) | none | -| `--dry-run` | flag | Preview synchronization without executing changes | false | -| `--auto-approve` | flag | Skip confirmation prompts | false | -| `--no-backup` | flag | Skip backup creation before synchronization | false | - -**Example Output (pre-prompt)**: - -``` -hatch mcp sync: - [INFO] Servers: weather-server, my-tool (2 total) - [SYNC] environment 'dev' → 'claude-desktop' - [SYNC] environment 'dev' → 'cursor' - Proceed? [y/N]: -``` - -When more than 3 servers match, the list is truncated: `Servers: srv1, srv2, srv3, ... (7 total)` - -**Error Output**: - -Sync failures use standardized error formatting with structured details: - -``` -[ERROR] Synchronization failed - claude-desktop: Config file not found -``` - -#### `hatch mcp remove server` - -Remove an MCP server from one or more hosts. - -Syntax: - -`hatch mcp remove server --host [--env ENV] [--dry-run] [--auto-approve] [--no-backup]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `server-name` | string (positional) | Name of the server to remove | n/a | -| `--host` | string | Target hosts (comma-separated or 'all') | n/a | -| `--env`, `-e` | string | Hatch environment name (reserved for future use) | none | -| `--dry-run` | flag | Preview removal without executing changes | false | -| `--auto-approve` | flag | Skip confirmation prompts | false | -| `--no-backup` | flag | Skip backup creation before removal | false | - -#### `hatch mcp remove host` - -Remove complete host configuration (all MCP servers from the specified host). - -Syntax: - -`hatch mcp remove host [--dry-run] [--auto-approve] [--no-backup]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `host-name` | string (positional) | Name of the host to remove | n/a | -| `--dry-run` | flag | Preview removal without executing changes | false | -| `--auto-approve` | flag | Skip confirmation prompts | false | -| `--no-backup` | flag | Skip backup creation before removal | false | - -#### `hatch mcp list hosts` - -List host/server pairs from host configuration files. - -**Purpose**: Shows ALL servers on hosts (both Hatch-managed and third-party) with Hatch management status. - -Syntax: - -`hatch mcp list hosts [--server PATTERN] [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--server` | string | Filter by server name using regex pattern | none | -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch mcp list hosts -MCP Hosts: - Host Server Hatch Environment - ───────────────────────────────────────────────────────────────── - claude-desktop weather-server ✅ default - claude-desktop third-party-tool ❌ - - cursor weather-server ✅ default -``` - -**Key Details**: -- Header: `"MCP Hosts:"` -- Columns: Host (width 18), Server (width 18), Hatch (width 8), Environment (width 15) -- Hatch column: `"✅"` for Hatch-managed, `"❌"` for third-party -- Shows ALL servers on hosts (both Hatch-managed and third-party) -- Environment column: environment name if Hatch-managed, `"-"` otherwise -- Sorted by: host (alphabetically), then server - -#### `hatch mcp list servers` - -List server/host pairs from host configuration files. - -**Purpose**: Shows ALL servers on hosts (both Hatch-managed and third-party) with Hatch management status. - -Syntax: - -`hatch mcp list servers [--host PATTERN] [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--host` | string | Filter by host name using regex pattern | none | -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch mcp list servers -MCP Servers: - Server Host Hatch Environment - ───────────────────────────────────────────────────────────────── - third-party-tool claude-desktop ❌ - - weather-server claude-desktop ✅ default - weather-server cursor ✅ default -``` - -**Key Details**: -- Header: `"MCP Servers:"` -- Columns: Server (width 18), Host (width 18), Hatch (width 8), Environment (width 15) -- Hatch column: `"✅"` for Hatch-managed, `"❌"` for third-party -- Shows ALL servers on hosts (both Hatch-managed and third-party) -- Environment column: environment name if Hatch-managed, `"-"` otherwise -- Sorted by: server (alphabetically), then host - -#### `hatch mcp show hosts` - -Show detailed hierarchical view of all MCP host configurations. - -**Purpose**: Displays comprehensive configuration details for all hosts with their servers. - -Syntax: - -`hatch mcp show hosts [--server PATTERN] [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--server` | string | Filter by server name using regex pattern | none | -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch mcp show hosts -═══════════════════════════════════════════════════════════════════════════════ -MCP Host: claude-desktop - Config Path: /Users/user/.config/claude/claude_desktop_config.json - Last Modified: 2026-02-01 15:30:00 - Backup Available: Yes (3 backups) - - Configured Servers (2): - weather-server (Hatch-managed: default) - Command: python - Args: ['-m', 'weather_server'] - Environment Variables: - API_KEY: ****** (hidden) - DEBUG: true - Last Synced: 2026-02-01 15:30:00 - Package Version: 1.0.0 - - third-party-tool (Not Hatch-managed) - Command: node - Args: ['server.js'] - -═══════════════════════════════════════════════════════════════════════════════ -MCP Host: cursor - Config Path: /Users/user/.cursor/mcp.json - Last Modified: 2026-02-01 14:20:00 - Backup Available: No - - Configured Servers (1): - weather-server (Hatch-managed: default) - Command: python - Args: ['-m', 'weather_server'] - Last Synced: 2026-02-01 14:20:00 - Package Version: 1.0.0 -``` - -**Key Details**: -- Separator: `"═" * 79` (U+2550) between hosts -- Host and server names highlighted (bold + amber when colors enabled) -- Hatch-managed servers show: `"(Hatch-managed: {environment})"` -- Third-party servers show: `"(Not Hatch-managed)"` -- Sensitive environment variables shown as `"****** (hidden)"` -- Hierarchical structure with 2-space indentation per level - -#### `hatch mcp show servers` - -Show detailed hierarchical view of all MCP server configurations across hosts. - -**Purpose**: Displays comprehensive configuration details for all servers across their host deployments. - -Syntax: - -`hatch mcp show servers [--host PATTERN] [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--host` | string | Filter by host name using regex pattern | none | -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch mcp show servers -═══════════════════════════════════════════════════════════════════════════════ -MCP Server: weather-server - Hatch Managed: Yes (default) - Package Version: 1.0.0 - - Host Configurations (2): - claude-desktop: - Command: python - Args: ['-m', 'weather_server'] - Environment Variables: - API_KEY: ****** (hidden) - DEBUG: true - Last Synced: 2026-02-01 15:30:00 - - cursor: - Command: python - Args: ['-m', 'weather_server'] - Last Synced: 2026-02-01 14:20:00 - -═══════════════════════════════════════════════════════════════════════════════ -MCP Server: third-party-tool - Hatch Managed: No - - Host Configurations (1): - claude-desktop: - Command: node - Args: ['server.js'] -``` - -**Key Details**: -- Separator: `"═" * 79` between servers -- Server and host names highlighted (bold + amber when colors enabled) -- Hatch-managed servers show: `"Hatch Managed: Yes ({environment})"` -- Third-party servers show: `"Hatch Managed: No"` -- Hierarchical structure with 2-space indentation per level - -#### `hatch mcp discover hosts` - -Discover available MCP host platforms on the system. - -**Purpose**: Shows ALL host platforms (both available and unavailable) with system detection status. - -Syntax: - -`hatch mcp discover hosts [--json]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--json` | flag | Output in JSON format | false | - -**Example Output**: - -```bash -$ hatch mcp discover hosts -Available MCP Host Platforms: - Host Status Config Path - ───────────────────────────────────────────────────────────────── - claude-desktop ✓ Available /Users/user/.config/claude/... - cursor ✓ Available /Users/user/.cursor/mcp.json - vscode ✗ Not Found - -``` - -**Key Details**: -- Header: `"Available MCP Host Platforms:"` -- Columns: Host (width 18), Status (width 15), Config Path (width "auto") -- Status: `"✓ Available"` or `"✗ Not Found"` -- Shows ALL host types (MCPHostType enum), not just available ones - -Syntax: - -`hatch mcp discover hosts` - -**Example Output**: - -```text -Available MCP host platforms: - claude-desktop: ✓ Available - Config path: ~/.claude/config.json - cursor: ✓ Available - Config path: ~/.cursor/config.json - vscode: ✗ Not detected - Config path: ~/.vscode/config.json -``` - -#### `hatch mcp discover servers` - -Discover MCP servers in Hatch environments. - -Syntax: - -`hatch mcp discover servers [--env ENV]` - -| Flag | Type | Description | Default | -|---:|---|---|---| -| `--env` | string | Specific environment to discover servers in | current environment | - -#### `hatch mcp backup list` - -List available configuration backups for a specific host. - -Syntax: - -`hatch mcp backup list [--detailed]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `host` | string (positional) | Host platform to list backups for (e.g., claude-desktop, cursor) | n/a | -| `--detailed`, `-d` | flag | Show detailed backup information | false | - -#### `hatch mcp backup restore` - -Restore host configuration from a backup file. - -Syntax: - -`hatch mcp backup restore [--backup-file FILE] [--dry-run] [--auto-approve]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `host` | string (positional) | Host platform to restore (e.g., claude-desktop, cursor) | n/a | -| `--backup-file`, `-f` | string | Specific backup file to restore (defaults to latest) | latest backup | -| `--dry-run` | flag | Preview restore without executing changes | false | -| `--auto-approve` | flag | Skip confirmation prompts | false | - -#### `hatch mcp backup clean` - -Clean old backup files for a specific host based on retention criteria. - -Syntax: - -`hatch mcp backup clean [--older-than-days DAYS] [--keep-count COUNT] [--dry-run] [--auto-approve]` - -| Argument / Flag | Type | Description | Default | -|---:|---|---|---| -| `host` | string (positional) | Host platform to clean backups for (e.g., claude-desktop, cursor) | n/a | -| `--older-than-days` | integer | Remove backups older than specified days | none | -| `--keep-count` | integer | Keep only the most recent N backups | none | -| `--dry-run` | flag | Preview cleanup without executing changes | false | -| `--auto-approve` | flag | Skip confirmation prompts | false | - -**Note:** At least one of `--older-than-days` or `--keep-count` must be specified. - ---- - -## Exit codes - -| Code | Meaning | -|---:|---| -| `0` | Success | -| `1` | Error or failure | - -## Notes - -- The CLI is implemented in the `hatch/cli/` package with modular handler modules. Use `hatch --help` to inspect available commands and options. -- This reference mirrors the command names and option names implemented in the CLI handlers. If you change CLI arguments in code, update this file to keep documentation in sync. +# CLI Reference + +This document is a compact reference of all Hatch CLI commands and options implemented in the `hatch/cli/` package, presented as tables for quick lookup. + +## Table of Contents + +- [Global options](#global-options) +- [Commands](#commands) + - [hatch create](#hatch-create) + - [hatch validate](#hatch-validate) + - [hatch env](#hatch-env-environment-management) + - [hatch env create](#hatch-env-create) + - [hatch env remove](#hatch-env-remove) + - [hatch env list](#hatch-env-list) + - [hatch env use](#hatch-env-use) + - [hatch env current](#hatch-env-current) + - [hatch env show](#hatch-env-show) + - [hatch env python](#hatch-env-python-advanced-python-environment-subcommands) + - [hatch env python init](#hatch-env-python-init) + - [hatch env python info](#hatch-env-python-info) + - [hatch env python add-hatch-mcp](#hatch-env-python-add-hatch-mcp) + - [hatch env python remove](#hatch-env-python-remove) + - [hatch env python shell](#hatch-env-python-shell) + - [hatch package](#hatch-package-package-management) + - [hatch package add](#hatch-package-add) + - [hatch package remove](#hatch-package-remove) + - [hatch package list](#hatch-package-list) + - [hatch package sync](#hatch-package-sync) + - [hatch mcp](#hatch-mcp) + - [hatch mcp discover hosts](#hatch-mcp-discover-hosts) + - [hatch mcp discover servers](#hatch-mcp-discover-servers) + - [hatch mcp list hosts](#hatch-mcp-list-hosts) + - [hatch mcp list servers](#hatch-mcp-list-servers) + - [hatch mcp show hosts](#hatch-mcp-show-hosts) + - [hatch mcp show servers](#hatch-mcp-show-servers) + - [hatch mcp configure](#hatch-mcp-configure) + - [hatch mcp sync](#hatch-mcp-sync) + - [hatch mcp remove server](#hatch-mcp-remove-server) + - [hatch mcp remove host](#hatch-mcp-remove-host) + - [hatch mcp backup list](#hatch-mcp-backup-list) + - [hatch mcp backup restore](#hatch-mcp-backup-restore) + - [hatch mcp backup clean](#hatch-mcp-backup-clean) + +## Global options + +These flags are accepted by the top-level parser and apply to all commands unless overridden. + +| Flag | Type | Description | Default | +|------|------|-------------|---------| +| `--version` | flag | Show program version and exit | n/a | +| `--envs-dir` | path | Directory to store environments | `~/.hatch/envs` | +| `--cache-ttl` | int | Cache time-to-live in seconds | `86400` (1 day) | +| `--cache-dir` | path | Directory to store cached packages | `~/.hatch/cache` | +| `--log-level` | choice | Log verbosity: `DEBUG`, `INFO`, `WARNING`, `ERROR` | `WARNING` | + +Example: + +```bash +hatch --version +# Output: hatch 0.6.1 +``` + +## Commands + +Each top-level command has its own table. Use the Syntax line before the table to see how to call it. + +### `hatch create` + + +Create a new package template. + +Syntax: + +`hatch create [--dir DIR] [--description DESC]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `name` | string (positional) | Package name (required) | n/a | +| `--dir`, `-d` | path | Target directory for the template | current directory | +| `--description`, `-D` | string | Package description | empty string | + +Examples: + +`hatch create my_package` + +`hatch create my_package --dir ./packages --description "My awesome package"` + + +--- + + +### `hatch validate` + + +Validate a package structure and metadata. + +Syntax: + +`hatch validate ` + + +| Argument | Type | Description | +|---:|---|---| +| `package_dir` | path (positional) | Path to package directory to validate (required) | + +Examples: + +`hatch validate ./my_package` + +--- + +### `hatch env` (environment management) + + +Top-level syntax: `hatch env ...` + + +#### `hatch env create` + + +Create a new Hatch environment bootstrapping a Python/conda environment. + +Syntax: + +`hatch env create [--description DESC] [--python-version VER] [--no-python] [--no-hatch-mcp-server] [--hatch-mcp-server-tag TAG]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `name` | string (positional) | Environment name (required) | n/a | +| `--description`, `-D` | string | Human-readable environment description | empty string | +| `--python-version` | string | Python version to create (e.g., `3.11`) | none (manager default) | +| `--no-python` | flag | Do not create a Python environment (skip conda/mamba) | false | +| `--no-hatch-mcp-server` | flag | Do not install `hatch_mcp_server` wrapper | false | +| `--hatch-mcp-server-tag` | string | Git tag/branch for wrapper installation | none | + +--- + +#### `hatch env remove` + + +Syntax: + +`hatch env remove ` + + +| Argument | Type | Description | +|---:|---|---| +| `name` | string (positional) | Environment name to remove (required) | + +--- + +#### `hatch env list` + + +List all environments with package counts. + +**Example Output**: + +```bash +$ hatch env list +Environments: + Name Python Packages + ─────────────────────────────────────── + * default 3.14.2 0 + test-env 3.11.5 3 +``` + +**Key Details**: +- Header: `"Environments:"` only +- Columns: Name (width 15), Python (width 10), Packages (width 10, right-aligned) +- Current environment marked with `"* "` prefix +- Packages column shows COUNT only +- Separator: `"─"` character (U+2500) + +Syntax: + +`hatch env list [--pattern PATTERN] [--json]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--pattern` | string | Filter environments by name using regex pattern | none | +| `--json` | flag | Output in JSON format | false | + +--- + +#### `hatch env use` + +Syntax: + +`hatch env use ` + + +| Argument | Type | Description | +|---:|---|---| +| `name` | string (positional) | Environment name to set as current (required) | + +--- + +#### `hatch env current` + +Syntax: + +`hatch env current` + + +Description: Print the name of the current environment. + +--- + +#### `hatch env show` + + +Display detailed hierarchical view of a specific environment. + +Syntax: + +`hatch env show ` + + +| Argument | Type | Description | +|---:|---|---| +| `name` | string (positional) | Environment name to show (required) | + +**Example Output**: + +```bash +$ hatch env show default +Environment: default (active) + Description: My development environment + Created: 2026-01-15 10:30:00 + + Python Environment: + Version: 3.14.2 + Executable: /path/to/python + Conda env: N/A + Status: Active + + Packages (2): + weather-server + Version: 1.0.0 + Source: registry (https://registry.example.com) + Deployed to: claude-desktop, cursor + + utility-pkg + Version: 2.1.0 + Source: local (/path/to/package) + Deployed to: (none) +``` + +**Key Details**: +- Header shows `"(active)"` suffix if current environment +- Hierarchical structure with 2-space indentation +- No separator lines between sections +- Packages section shows count in header +- Each package shows version, source, and deployed hosts + +--- + +### `hatch env python` (advanced Python environment subcommands) + +Top-level syntax: `hatch env python ...` + + +#### `hatch env python init` + +Initialize or recreate a Python environment inside a Hatch environment. + +Syntax: + +`hatch env python init [--hatch_env NAME] [--python-version VER] [--force] [--no-hatch-mcp-server] [--hatch_mcp_server_tag TAG]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--hatch_env` | string | Hatch environment name (defaults to current env) | current environment | +| `--python-version` | string | Desired Python version (e.g., `3.12`) | none | +| `--force` | flag | Force recreation if it already exists | false | +| `--no-hatch-mcp-server` | flag | Skip installing `hatch_mcp_server` wrapper | false | +| `--hatch_mcp_server_tag` | string | Git tag/branch for wrapper installation | none | + +--- + +#### `hatch env python info` + +Show information about the Python environment for a Hatch environment. + +Syntax: + +`hatch env python info [--hatch_env NAME] [--detailed]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | +| `--detailed` | flag | Show additional diagnostics and package listing | false | + +When available this command prints: status, python executable, python version, conda env name, environment path, creation time, package count and package list. With `--detailed` it also prints diagnostics from the manager. + + +--- + +#### `hatch env python add-hatch-mcp` + + +Install the `hatch_mcp_server` wrapper into the Python environment of a Hatch env. + + +Syntax: + +`hatch env python add-hatch-mcp [--hatch_env NAME] [--tag TAG]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | +| `--tag` | string | Git tag/branch for wrapper install | none | + +--- + +#### `hatch env python remove` + + +Remove the Python environment associated with a Hatch environment. + + +Syntax: + +`hatch env python remove [--hatch_env NAME] [--force]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | +| `--force` | flag | Skip confirmation prompt and force removal | false | + +--- + +#### `hatch env python shell` + +Launch a Python REPL or run a single command inside the Python environment. + + +Syntax: + +`hatch env python shell [--hatch_env NAME] [--cmd CMD]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--hatch_env` | string | Hatch environment name (defaults to current) | current environment | +| `--cmd` | string | Command to execute inside the Python shell (optional) | none | + +--- + +### `hatch package` (package management) + +Top-level syntax: `hatch package ...` + + +#### `hatch package add` + +Add a package (local path or registry name) into an environment. + +Syntax: + +`hatch package add [--env NAME] [--version VER] [--force-download] [--refresh-registry] [--auto-approve] [--host HOSTS]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `package_path_or_name` | string (positional) | Path to package directory or registry package name (required) | n/a | +| `--env`, `-e` | string | Target Hatch environment name (defaults to current) | current environment | +| `--version`, `-v` | string | Version for registry packages | none | +| `--force-download`, `-f` | flag | Force fetching even if cached | false | +| `--refresh-registry`, `-r` | flag | Refresh registry metadata before resolving | false | +| `--auto-approve` | flag | Automatically approve dependency installation prompts | false | +| `--host` | string | Comma-separated list of MCP host platforms to configure (e.g., claude-desktop,cursor) | none | + +**Note:** Dependency installation prompts are also automatically approved in non-TTY environments (such as CI/CD pipelines) or when the `HATCH_AUTO_APPROVE` environment variable is set. + +**MCP Host Integration:** When adding a package, if the `--host` flag is specified, Hatch will automatically configure the package's MCP servers on the specified hosts. + +Examples: + +`hatch package add ./my_package` + +`hatch package add registry_package --version 1.0.0 --env dev-env --host gemini --auto-approve` + +--- + +#### `hatch package remove` + +Remove a package from a Hatch environment. + +Syntax: + +`hatch package remove [--env NAME]` + + +| Argument / Flag | Type | Description | +|---:|---|---| +| `package_name` | string (positional) | Name of the package to remove (required) | n/a | +| `--env`, `-e` | string | Hatch environment name (defaults to current) | current environment | + +--- + +#### `hatch package list` + +**⚠️ DEPRECATED**: This command is deprecated. Use `hatch env list` to see packages inline with environment information, or `hatch env show ` for detailed package information. + +List packages installed in a Hatch environment. + +Syntax: + +`hatch package list [--env NAME]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--env`, `-e` | string | Target Hatch environment name (defaults to current) | current environment | + +**Example Output**: + +```bash +$ hatch package list +Warning: 'hatch package list' is deprecated. Use 'hatch env list' instead, which shows packages inline. +Packages in environment 'default': +weather-server (1.0.0) Hatch compliant: True source: https://registry.example.com location: /path/to/package +``` + +**Migration Guide**: +- For package counts: Use `hatch env list` (shows package count per environment) +- For detailed package info: Use `hatch env show ` (shows full package details) + +--- + +#### `hatch package sync` + +Synchronize package MCP servers to host platforms. + +Syntax: + +`hatch package sync --host [--env ENV] [--dry-run] [--auto-approve] [--no-backup]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `package_name` | string (positional) | Name of package whose MCP servers to sync | n/a | +| `--host` | string | Comma-separated list of host platforms or 'all' | n/a | +| `--env`, `-e` | string | Target Hatch environment name (defaults to current) | current environment | +| `--dry-run` | flag | Preview changes without execution | false | +| `--auto-approve` | flag | Skip confirmation prompts | false | +| `--no-backup` | flag | Disable default backup behavior of the MCP host's config file | false | + +Examples: + +`hatch package sync my-package --host claude-desktop` + +`hatch package sync weather-server --host claude-desktop,cursor --dry-run --no-backup` + + +--- + +## Environment Variables + +Hatch recognizes the following environment variables to control behavior: + +| Variable | Description | Accepted Values | Default | +|----------|-------------|-----------------|---------| +| `HATCH_AUTO_APPROVE` | Automatically approve dependency installation prompts in non-interactive environments | `1`, `true`, `yes` (case-insensitive) | unset | + +### `HATCH_AUTO_APPROVE` + +When set to a truthy value (`1`, `true`, or `yes`, case-insensitive), this environment variable enables automatic approval of dependency installation prompts. This is particularly useful in CI/CD pipelines and other automated environments where user interaction is not possible. + +**Behavior:** + +- In TTY environments: User is still prompted for consent unless this variable is set +- In non-TTY environments: Installation is automatically approved regardless of this variable +- When set in any environment: Installation is automatically approved without prompting + + +Examples: + +```bash +# Enable auto-approval for the current session +export HATCH_AUTO_APPROVE=1 +hatch package add my_package + +# Enable auto-approval for a single command +HATCH_AUTO_APPROVE=true hatch package add my_package + +# CI/CD pipeline usage +HATCH_AUTO_APPROVE=yes hatch package add production_package +``` + +--- + +## MCP Host Configuration Commands + +### `hatch mcp` + + +Commands subset to manage non-hatch package MCP servers. +Top level syntax: ` ...` + + +#### `hatch mcp discover hosts` + + +Discover available MCP host platforms on the system. + +**Purpose**: Shows ALL host platforms (both available and unavailable) with system detection status. + + +Syntax: + +`hatch mcp discover hosts [--json]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--json` | flag | Output in JSON format | false | + + +**Example Output**: + +```bash +$ hatch mcp discover hosts +Available MCP Host Platforms: + Host Status Config Path + ───────────────────────────────────────────────────────────────── + claude-desktop ✓ Available /Users/user/.config/claude/... + cursor ✓ Available /Users/user/.cursor/mcp.json + vscode ✗ Not Found - + mistral-vibe ✓ Available /Users/user/.config/mistral/mcp.toml +``` + +**Key Details**: +- Header: `"Available MCP Host Platforms:"` +- Columns: Host (width 18), Status (width 15), Config Path (width "auto") +- Status: `"✓ Available"` or `"✗ Not Found"` +- Shows ALL host types (MCPHostType enum), not just available ones + +--- + +#### `hatch mcp discover servers` + + +Discover MCP servers in Hatch environments. + + +Syntax: + +`hatch mcp discover servers [--env ENV]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--env` | string | Specific environment to discover servers in | current environment | + +--- + +#### `hatch mcp list hosts` + + +List host/server pairs from host configuration files. + + +**Purpose**: Shows ALL servers on hosts (both Hatch-managed and third-party) with Hatch management status. + + +Syntax: + +`hatch mcp list hosts [--server PATTERN] [--json]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--server` | string | Filter by server name using regex pattern | none | +| `--json` | flag | Output in JSON format | false | + + +**Example Output**: + +```bash +$ hatch mcp list hosts +MCP Hosts: + Host Server Hatch Environment + ───────────────────────────────────────────────────────────────── + claude-desktop weather-server ✅ default + claude-desktop third-party-tool ❌ - + cursor weather-server ✅ default +``` + +**Key Details**: +- Header: `"MCP Hosts:"` +- Columns: Host (width 18), Server (width 18), Hatch (width 8), Environment (width 15) +- Hatch column: `"✅"` for Hatch-managed, `"❌"` for third-party +- Shows ALL servers on hosts (both Hatch-managed and third-party) +- Environment column: environment name if Hatch-managed, `"-"` otherwise +- Sorted by: host (alphabetically), then server + +--- + +#### `hatch mcp list servers` + + +List server/host pairs from host configuration files. + +**Purpose**: Shows ALL servers on hosts (both Hatch-managed and third-party) with Hatch management status. + +Syntax: + +`hatch mcp list servers [--host PATTERN] [--json]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--host` | string | Filter by host name using regex pattern | none | +| `--json` | flag | Output in JSON format | false | + +**Example Output**: + +```bash +$ hatch mcp list servers +MCP Servers: + Server Host Hatch Environment + ───────────────────────────────────────────────────────────────── + third-party-tool claude-desktop ❌ - + weather-server claude-desktop ✅ default + weather-server cursor ✅ default +``` + +**Key Details**: +- Header: `"MCP Servers:"` +- Columns: Server (width 18), Host (width 18), Hatch (width 8), Environment (width 15) +- Hatch column: `"✅"` for Hatch-managed, `"❌"` for third-party +- Shows ALL servers on hosts (both Hatch-managed and third-party) +- Environment column: environment name if Hatch-managed, `"-"` otherwise +- Sorted by: server (alphabetically), then host + +--- + +#### `hatch mcp show hosts` + + +Show detailed hierarchical view of all MCP host configurations. + +**Purpose**: Displays comprehensive configuration details for all hosts with their servers. + +Syntax: + +`hatch mcp show hosts [--server PATTERN] [--json]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--server` | string | Filter by server name using regex pattern | none | +| `--json` | flag | Output in JSON format | false | + +**Example Output**: + +```bash +$ hatch mcp show hosts +═══════════════════════════════════════════════════════════════════════════════ +MCP Host: claude-desktop + Config Path: /Users/user/.config/claude/claude_desktop_config.json + Last Modified: 2026-02-01 15:30:00 + Backup Available: Yes (3 backups) + + Configured Servers (2): + weather-server (Hatch-managed: default) + Command: python + Args: ['-m', 'weather_server'] + Environment Variables: + API_KEY: ****** (hidden) + DEBUG: true + Last Synced: 2026-02-01 15:30:00 + Package Version: 1.0.0 + + third-party-tool (Not Hatch-managed) + Command: node + Args: ['server.js'] + +═══════════════════════════════════════════════════════════════════════════════ +MCP Host: cursor + Config Path: /Users/user/.config/cursor/mcp.json + Last Modified: 2026-02-01 14:20:00 + Backup Available: No + + Configured Servers (1): + weather-server (Hatch-managed: default) + Command: python + Args: ['-m', 'weather_server'] + Last Synced: 2026-02-01 14:20:00 + Package Version: 1.0.0 +``` + +**Key Details**: +- Separator: `"═" * 79` (U+2550) between hosts +- Host and server names highlighted (bold + amber when colors enabled) +- Hatch-managed servers show: `"(Hatch-managed: {environment})"` +- Third-party servers show: `"(Not Hatch-managed)"` +- Sensitive environment variables shown as `"****** (hidden)"` +- Hierarchical structure with 2-space indentation per level + +--- + +#### `hatch mcp show servers` + + +Show detailed hierarchical view of all MCP server configurations across hosts. + +**Purpose**: Displays comprehensive configuration details for all servers across their host deployments. + +Syntax: + +`hatch mcp show servers [--host PATTERN] [--json]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--host` | string | Filter by host name using regex pattern | none | +| `--json` | flag | Output in JSON format | false | + +**Example Output**: + +```bash +$ hatch mcp show servers +═══════════════════════════════════════════════════════════════════════════════ +MCP Server: weather-server + Hatch Managed: Yes (default) + Package Version: 1.0.0 + + Host Configurations (2): + claude-desktop: + Command: python + Args: ['-m', 'weather_server'] + Environment Variables: + API_KEY: ****** (hidden) + DEBUG: true + Last Synced: 2026-02-01 15:30:00 + + cursor: + Command: python + Args: ['-m', 'weather_server'] + Last Synced: 2026-02-01 14:20:00 + +═══════════════════════════════════════════════════════════════════════════════ +MCP Server: third-party-tool + Hatch Managed: No + + Host Configurations (1): + claude-desktop: + Command: node + Args: ['server.js'] +``` + +**Key Details**: +- Separator: `"═" * 79` between servers +- Server and host names highlighted (bold + amber when colors enabled) +- Hatch-managed servers show: `"Hatch Managed: Yes ({environment})"` +- Third-party servers show: `"Hatch Managed: No"` +- Hierarchical structure with 2-space indentation per level + +--- + +#### `hatch mcp configure` + + +Configure an MCP server on a specific host platform. + + +Syntax: + +`hatch mcp configure --host (--command CMD | --url URL) [--args ARGS] [--env-var ENV] [--header HEADER] [--http-url URL] [--timeout MS] [--trust] [--cwd DIR] [--include-tools TOOLS] [--exclude-tools TOOLS] [--env-file FILE] [--input INPUTS] [--disabled] [--auto-approve-tools TOOLS] [--disable-tools TOOLS] [--env-vars VARS] [--startup-timeout SEC] [--tool-timeout SEC] [--enabled] [--bearer-token-env-var VAR] [--env-header HEADERS] [--dry-run] [--auto-approve] [--no-backup]` + + +| Argument / Flag | Hosts | Type | Description | Default | +|---:|---:|---|---|---| +| `server-name` | all | string (positional) | Name of the MCP server to configure | n/a | +| `--host` | all | string | Target host platform (claude-desktop, cursor, etc.) | n/a | +| `--command` | all | string | Command to execute for local servers (mutually exclusive with --url) | none | +| `--url` | all except Claude Desktop/Code | string | URL for remote MCP servers (mutually exclusive with --command) | none | +| `--http-url` | gemini | string | HTTP streaming endpoint URL | none | +| `--args` | all | string list | Arguments for MCP server command (only with --command) | none | +| `--env-var` | all | string | Environment variables format: KEY=VALUE (can be used multiple times) | none | +| `--header` | all except Claude Desktop/Code | string | HTTP headers format: KEY=VALUE (only with --url) | none | +| `--timeout` | gemini | int | Request timeout in milliseconds | none | +| `--trust` | gemini | flag | Bypass tool call confirmations | false | +| `--cwd` | gemini, codex | string | Working directory for stdio transport | none | +| `--include-tools` | gemini, codex | multiple | Tool allowlist / enabled tools. Space-separated values. | none | +| `--exclude-tools` | gemini, codex | multiple | Tool blocklist / disabled tools. Space-separated values. | none | +| `--env-file` | cursor, vscode, lmstudio | string | Path to environment file | none | +| `--input` | vscode | multiple | Input variable definitions format: type,id,description[,password=true] | none | +| `--disabled` | kiro | flag | Disable the MCP server | false | +| `--auto-approve-tools` | kiro | multiple | Tool names to auto-approve. Can be used multiple times. | none | +| `--disable-tools` | kiro | multiple | Tool names to disable. Can be used multiple times. | none | +| `--env-vars` | codex | multiple | Environment variable names to whitelist/forward. Can be used multiple times. | none | +| `--startup-timeout` | codex | int | Server startup timeout in seconds (default: 10) | 10 | +| `--tool-timeout` | codex | int | Tool execution timeout in seconds (default: 60) | 60 | +| `--enabled` | codex | flag | Enable the MCP server | false | +| `--bearer-token-env-var` | codex | string | Name of env var containing bearer token for Authorization header | none | +| `--env-header` | codex | multiple | HTTP header from env var format: KEY=ENV_VAR_NAME. Can be used multiple times. | none | +| `--dry-run` | all | flag | Preview configuration without applying changes | false | +| `--auto-approve` | all | flag | Skip confirmation prompts | false | +| `--no-backup` | all | flag | Skip backup creation before configuration | false | + +**Behavior**: + +The command displays a **conversion report** showing exactly what fields will be configured on the target host. This provides transparency about which fields are supported by the host and what values will be set. + +The conversion report shows: +- **UPDATED** fields: Fields being set with their new values (shown as `None --> value`) +- **UNSUPPORTED** fields: Fields not supported by the target host (automatically filtered out) + + +Note: Internal metadata fields (like `name`) are not shown in the field operations list. + +--- + +#### `hatch mcp sync` + + +Synchronize MCP configurations across environments and hosts. + +The sync command displays a preview of servers to be synced before requesting confirmation, giving visibility into which servers will be affected. + +Syntax: + +`hatch mcp sync [--from-env ENV | --from-host HOST] --to-host HOSTS [--servers SERVERS | --pattern PATTERN] [--dry-run] [--auto-approve] [--no-backup]` + + +| Flag | Type | Description | Default | +|---:|---|---|---| +| `--from-env` | string | Source Hatch environment (mutually exclusive with --from-host) | none | +| `--from-host` | string | Source host platform (mutually exclusive with --from-env) | none | +| `--to-host` | string | Target hosts (comma-separated or 'all') | n/a | +| `--servers` | string | Specific server names to sync (mutually exclusive with --pattern) | none | +| `--pattern` | string | Regex pattern for server selection (mutually exclusive with --servers) | none | +| `--dry-run` | flag | Preview synchronization without executing changes | false | +| `--auto-approve` | flag | Skip confirmation prompts | false | +| `--no-backup` | flag | Skip backup creation before synchronization | false | + +**Example Output (pre-prompt)**: + +``` +hatch mcp sync: + [INFO] Servers: weather-server, my-tool (2 total) + [SYNC] environment 'dev' → 'claude-desktop' + [SYNC] environment 'dev' → 'cursor' + Proceed? [y/N]: +``` + +When more than 3 servers match, the list is truncated: `Servers: srv1, srv2, srv3, ... (7 total)` + + +--- + +#### `hatch mcp remove server` + + +Remove an MCP server from one or more hosts. + +Syntax: + +`hatch mcp remove server --host [--env ENV] [--dry-run] [--auto-approve] [--no-backup]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `server-name` | string (positional) | Name of the server to remove | n/a | +| `--host` | string | Target hosts (comma-separated or 'all') | n/a | +| `--env`, `-e` | string | Hatch environment name (reserved for future use) | none | +| `--dry-run` | flag | Preview removal without executing changes | false | +| `--auto-approve` | flag | Skip confirmation prompts | false | +| `--no-backup` | flag | Skip backup creation before removal | false | + +--- + +#### `hatch mcp remove host` + + +Remove complete host configuration (all MCP servers from the specified host). + +Syntax: + +`hatch mcp remove host [--dry-run] [--auto-approve] [--no-backup]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `host-name` | string (positional) | Name of the host to remove | n/a | +| `--dry-run` | flag | Preview removal without executing changes | false | +| `--auto-approve` | flag | Skip confirmation prompts | false | +| `--no-backup` | flag | Skip backup creation before removal | false | + +--- + +#### `hatch mcp backup list` + + +List available configuration backups for a specific host. + +Syntax: + +`hatch mcp backup list [--detailed]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `host` | string (positional) | Host platform to list backups for (e.g., claude-desktop, cursor) | n/a | +| `--detailed`, `-d` | flag | Show detailed backup information | false | + +--- + +#### `hatch mcp backup restore` + + +Restore host configuration from a backup file. + +Syntax: + +`hatch mcp backup restore [--backup-file FILE] [--dry-run] [--auto-approve]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `host` | string (positional) | Host platform to restore (e.g., claude-desktop, cursor) | n/a | +| `--backup-file`, `-f` | string | Specific backup file to restore (defaults to latest) | latest backup | +| `--dry-run` | flag | Preview restore without executing changes | false | +| `--auto-approve` | flag | Skip confirmation prompts | false | + +--- + +#### `hatch mcp backup clean` + + +Clean old backup files for a specific host based on retention criteria. + +Syntax: + +`hatch mcp backup clean [--older-than-days DAYS] [--keep-count COUNT] [--dry-run] [--auto-approve]` + + +| Argument / Flag | Type | Description | Default | +|---:|---|---|---| +| `host` | string (positional) | Host platform to clean backups for (e.g., claude-desktop, cursor) | n/a | +| `--older-than-days` | integer | Remove backups older than specified days | none | +| `--keep-count` | integer | Keep only the most recent N backups | none | +| `--dry-run` | flag | Preview cleanup without executing changes | false | +| `--auto-approve` | flag | Skip confirmation prompts | false | + +**Note:** At least one of `--older-than-days` or `--keep-count` must be specified. + +--- + +## Exit codes + +| Code | Meaning | +|---:|---| +| `0` | Success | +| `1` | Error or failure | + +## Notes + +- The CLI is implemented in the `hatch/cli/` package with modular handler modules. Use `hatch --help` to inspect available commands and options. +- This reference mirrors the command names and option names implemented in the CLI handlers. If you change CLI arguments in code, update this file to keep documentation in sync. diff --git a/docs/articles/users/MCPHostConfiguration.md b/docs/articles/users/MCPHostConfiguration.md index 9e2d2bf..9af7204 100644 --- a/docs/articles/users/MCPHostConfiguration.md +++ b/docs/articles/users/MCPHostConfiguration.md @@ -23,6 +23,9 @@ Hatch currently supports configuration for these MCP host platforms: - **Codex** - OpenAI Codex with MCP server configuration support - **LM Studio** - Local language model interface - **Gemini** - Google's AI development environment +- **Mistral Vibe** - Mistral Vibe CLI coding agent +- **OpenCode** - OpenCode AI coding assistant +- **Augment** - Augment Code AI assistant ## Hands-on Learning diff --git a/docs/articles/users/tutorials/04-mcp-host-configuration/01-host-platform-overview.md b/docs/articles/users/tutorials/04-mcp-host-configuration/01-host-platform-overview.md index 05a14f9..479a521 100644 --- a/docs/articles/users/tutorials/04-mcp-host-configuration/01-host-platform-overview.md +++ b/docs/articles/users/tutorials/04-mcp-host-configuration/01-host-platform-overview.md @@ -57,6 +57,9 @@ Hatch currently supports configuration for these MCP host platforms: - [**Codex**](https://github.com/openai/codex) - OpenAI Codex with MCP server configuration support - [**LM Studio**](https://lmstudio.ai/) - Local language model interface - [**Gemini**](https://github.com/google-gemini/gemini-cli) - Google's AI Command Line Interface +- [**Mistral Vibe**](https://mistral.ai/vibe) - Mistral Vibe CLI coding agent +- [**OpenCode**](https://opencode.ai) - OpenCode AI coding assistant +- [**Augment**](https://www.augmentcode.com/) - Augment Code AI assistant ## Configuration Management Workflow diff --git a/docs/index.md b/docs/index.md index 78eb741..379db89 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,21 +1,26 @@ # Hatch Documentation -Welcome to the documentation for Hatch, the official package manager for the Hatch! ecosystem. - ## Overview -Hatch provides powerful tools for managing MCP server packages, environments, and interacting with the Hatch registry. It serves as the package management foundation for [Hatchling](https://github.com/CrackingShells/Hatchling) and other projects in the ecosystem. +Hatch is a CLI tool for configuring MCP servers across AI host platforms. Instead of editing JSON config files for each tool separately, you register servers from the command line — once, on as many hosts as you need. + +```bash +hatch mcp configure context7 --host claude-desktop,cursor,vscode \ + --command npx --args "-y @upstash/context7-mcp" +``` + +Hatch also has a package system for installing MCP servers with dependency isolation (Python, system packages, Docker). That part is still being developed and will eventually integrate with MCP registries. -Hatch also supports MCP host configuration across popular platforms including Claude Desktop/Code, VS Code, Cursor, Kiro, Codex, LM Studio, and Gemini. +Supported hosts: Claude Desktop, Claude Code, VS Code, Cursor, Kiro, Codex, LM Studio, Google Gemini CLI, Mistral Vibe, OpenCode, Augment Code (Auggie CLI and Intent). ## Documentation Sections ### For Users -- **[Getting Started](./articles/users/GettingStarted.md)** - Quick start guide for using Hatch -- **[Command Reference](./articles/users/CLIReference.md)** - Complete CLI command documentation -- **[MCP Host Configuration](./articles/users/MCPHostConfiguration.md)** - Configure MCP servers across different host platforms -- **[Tutorials Start](./articles/users/tutorials/01-getting-started/01-installation.md)** - Step-by-step guides for your journey from installation to authoring Hatch packages for MCP server easy sharing. +- **[Getting Started](./articles/users/GettingStarted.md)** - Installation and first steps +- **[MCP Host Configuration](./articles/users/MCPHostConfiguration.md)** - Configure MCP servers across host platforms +- **[Command Reference](./articles/users/CLIReference.md)** - Complete CLI reference +- **[Tutorials](./articles/users/tutorials/01-getting-started/01-installation.md)** - Step-by-step guides, including package authoring ### For Developers diff --git a/docs/stylesheets/brand.css b/docs/stylesheets/brand.css new file mode 100644 index 0000000..e408c0e --- /dev/null +++ b/docs/stylesheets/brand.css @@ -0,0 +1,199 @@ +/* ============================================================= + CrackingShells Brand Themes + Two custom MkDocs Material palette schemes: + - egg-shell (light) — custom scheme, fully standalone + - slate (dark) — Material built-in base + brand overrides + ============================================================= */ + +/* ── Light: Egg Shell ──────────────────────────────────────── */ +[data-md-color-scheme="egg-shell"] { + + /* Primary — egg yolk amber nav/header, dark green text on it */ + --md-primary-fg-color: #E8B84B; + --md-primary-fg-color--light: #F0D060; + --md-primary-fg-color--dark: #D4952A; + --md-primary-bg-color: #1D3328; + --md-primary-bg-color--light: rgba(29, 51, 40, 0.7); + + /* Accent — warm amber */ + --md-accent-fg-color: #D4952A; + --md-accent-fg-color--transparent: rgba(212, 149, 42, 0.15); + --md-accent-bg-color: #F0E8C8; + --md-accent-bg-color--light: #f5f0e0; + + /* Page & surface backgrounds — warm cream */ + --md-default-bg-color: #F7F3EA; + --md-default-bg-color--light: #EDE8DC; + --md-default-bg-color--lighter: #F0EBE0; + --md-default-bg-color--lightest: #faf7f2; + + /* Body text — deep dark green */ + --md-default-fg-color: #1D3328; + --md-default-fg-color--light: #3D5148; + --md-default-fg-color--lighter: #4A6B58; + --md-default-fg-color--lightest: rgba(29, 51, 40, 0.12); + + /* Links */ + --md-typeset-a-color: #D4952A; + + /* Code blocks */ + --md-code-fg-color: #1D3328; + --md-code-bg-color: #EDE5CE; + + /* Admonitions */ + --md-admonition-fg-color: #1D3328; + --md-admonition-bg-color: #f3edd8; + + /* Footer */ + --md-footer-fg-color: #F0E8C8; + --md-footer-fg-color--light: rgba(240, 232, 200, 0.7); + --md-footer-fg-color--lighter: rgba(240, 232, 200, 0.45); + --md-footer-bg-color: #1D3328; + --md-footer-bg-color--dark: #111D18; + + /* Keyboard key */ + --md-typeset-kbd-color: #EDE8DC; + --md-typeset-kbd-accent-color: #D4952A; + --md-typeset-kbd-border-color: #C8B898; + + /* Tables */ + --md-typeset-table-color: rgba(29, 51, 40, 0.12); + --md-typeset-table-color--light: rgba(29, 51, 40, 0.035); + + /* Sidebar scroll track */ + --md-scrollbar-thumb-bg-color: rgba(61, 81, 72, 0.35); +} + +/* Override logo for light scheme */ +[data-md-color-scheme="egg-shell"] .md-header__button.md-logo img { + content: url(https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_wide_light_bg_transparent.png); +} + + +/* ── Dark: Hatch Night (scheme: slate) ────────────────────── */ +[data-md-color-scheme="slate"] { + + /* Primary — deep green nav/header */ + --md-primary-fg-color: #2A3D32; + --md-primary-fg-color--light: #3D5148; + --md-primary-fg-color--dark: #1D2B24; + --md-primary-bg-color: #F0E8C8; + --md-primary-bg-color--light: rgba(240, 232, 200, 0.7); + + /* Accent — golden amber */ + --md-accent-fg-color: #E8B84B; + --md-accent-fg-color--transparent: rgba(232, 184, 75, 0.15); + --md-accent-bg-color: #1D2B24; + --md-accent-bg-color--light: #243525; + + /* Page & surface backgrounds — near-black green */ + --md-default-bg-color: #111D18; + --md-default-bg-color--light: #1D2B24; + --md-default-bg-color--lighter: #162318; + --md-default-bg-color--lightest: rgba(255, 255, 255, 0.05); + + /* Body text — warm off-white */ + --md-default-fg-color: #E8DFC8; + --md-default-fg-color--light: rgba(232, 223, 200, 0.75); + --md-default-fg-color--lighter: rgba(232, 223, 200, 0.45); + --md-default-fg-color--lightest: rgba(232, 223, 200, 0.12); + + /* Links */ + --md-typeset-a-color: #E8B84B; + + /* Code blocks */ + --md-code-fg-color: #F0E8C8; + --md-code-bg-color: #1A2B22; + + /* Admonitions */ + --md-admonition-fg-color: #E8DFC8; + --md-admonition-bg-color: #1D2B24; + + /* Footer */ + --md-footer-fg-color: #F0E8C8; + --md-footer-fg-color--light: rgba(240, 232, 200, 0.7); + --md-footer-fg-color--lighter: rgba(240, 232, 200, 0.45); + --md-footer-bg-color: #0D1710; + --md-footer-bg-color--dark: #080F0A; + + /* Keyboard key */ + --md-typeset-kbd-color: #1D2B24; + --md-typeset-kbd-accent-color: #E8B84B; + --md-typeset-kbd-border-color: #3D5148; + + /* Tables */ + --md-typeset-table-color: rgba(232, 184, 75, 0.12); + --md-typeset-table-color--light: rgba(232, 184, 75, 0.035); + + /* Sidebar scroll track */ + --md-scrollbar-thumb-bg-color: rgba(232, 184, 75, 0.25); +} + +/* Override logo for dark scheme */ +[data-md-color-scheme="slate"] .md-header__button.md-logo img { + content: url(https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_wide_dark_bg_transparent.png); +} + +/* Explicit link/nav/toc overrides — variables alone lose to Material's slate defaults */ + +/* Body content links */ +[data-md-color-scheme="slate"] .md-typeset a { + color: #E8B84B; +} +[data-md-color-scheme="slate"] .md-typeset a:hover, +[data-md-color-scheme="slate"] .md-typeset a:focus { + color: #F0D060; +} + +/* Left sidebar nav — active, hover, focus states */ +[data-md-color-scheme="slate"] .md-nav__link:focus, +[data-md-color-scheme="slate"] .md-nav__link:hover, +[data-md-color-scheme="slate"] .md-nav__link--active, +[data-md-color-scheme="slate"] .md-nav__item--active > .md-nav__link { + color: #E8B84B; +} + +/* Right TOC — active / focused section */ +[data-md-color-scheme="slate"] .md-nav--secondary .md-nav__link:focus, +[data-md-color-scheme="slate"] .md-nav--secondary .md-nav__link:hover, +[data-md-color-scheme="slate"] .md-nav--secondary .md-nav__link--active { + color: #E8B84B; +} + +/* Top navigation tabs — inactive, active, hover */ +[data-md-color-scheme="slate"] .md-tabs__link { + color: rgba(240, 232, 200, 0.65); + opacity: 1; +} +[data-md-color-scheme="slate"] .md-tabs__link--active, +[data-md-color-scheme="slate"] .md-tabs__link:hover { + color: #F0D060; + opacity: 1; +} + + +/* ── Shared tweaks ─────────────────────────────────────────── */ + +/* Slightly reduce logo height in the header so it sits comfortably */ +.md-header__button.md-logo img { + height: 1.8rem; + width: auto; +} + +/* Admonition title bars — use brand green for note, amber for warning/tip */ +[data-md-color-scheme="egg-shell"] .admonition.note > .admonition-title, +[data-md-color-scheme="egg-shell"] .admonition.info > .admonition-title { + background-color: rgba(61, 81, 72, 0.18); +} +[data-md-color-scheme="egg-shell"] .admonition.warning > .admonition-title, +[data-md-color-scheme="egg-shell"] .admonition.tip > .admonition-title { + background-color: rgba(212, 149, 42, 0.2); +} +[data-md-color-scheme="slate"] .admonition.note > .admonition-title, +[data-md-color-scheme="slate"] .admonition.info > .admonition-title { + background-color: rgba(61, 81, 72, 0.45); +} +[data-md-color-scheme="slate"] .admonition.warning > .admonition-title, +[data-md-color-scheme="slate"] .admonition.tip > .admonition-title { + background-color: rgba(232, 184, 75, 0.22); +} diff --git a/hatch/cli/__main__.py b/hatch/cli/__main__.py index f1d6e98..746ada7 100644 --- a/hatch/cli/__main__.py +++ b/hatch/cli/__main__.py @@ -585,10 +585,10 @@ def _setup_mcp_commands(subparsers): ) server_type_group.add_argument( "--url", - help="Server URL for remote MCP servers (SSE transport) [hosts: all except claude-desktop, claude-code]", + help="Server URL for remote MCP servers (SSE/streamable transport) [hosts: all except claude-desktop, claude-code]", ) server_type_group.add_argument( - "--http-url", help="HTTP streaming endpoint URL [hosts: gemini]" + "--http-url", help="HTTP streaming endpoint URL [hosts: gemini, mistral-vibe]" ) mcp_configure_parser.add_argument( @@ -667,12 +667,12 @@ def _setup_mcp_commands(subparsers): mcp_configure_parser.add_argument( "--startup-timeout", type=int, - help="Server startup timeout in seconds (default: 10) [hosts: codex]", + help="Server startup timeout in seconds (default: 10) [hosts: codex, mistral-vibe]", ) mcp_configure_parser.add_argument( "--tool-timeout", type=int, - help="Tool execution timeout in seconds (default: 60) [hosts: codex]", + help="Tool execution timeout in seconds (default: 60) [hosts: codex, mistral-vibe]", ) mcp_configure_parser.add_argument( "--enabled", @@ -683,12 +683,35 @@ def _setup_mcp_commands(subparsers): mcp_configure_parser.add_argument( "--bearer-token-env-var", type=str, - help="Name of environment variable containing bearer token for Authorization header [hosts: codex]", + help="Name of environment variable containing bearer token for Authorization header [hosts: codex, mistral-vibe]", ) mcp_configure_parser.add_argument( "--env-header", action="append", - help="HTTP header from environment variable in KEY=ENV_VAR_NAME format [hosts: codex]", + help="HTTP header from environment variable in KEY=ENV_VAR_NAME format [hosts: codex, mistral-vibe]", + ) + + # Mistral Vibe-specific arguments + mcp_configure_parser.add_argument( + "--prompt", help="Per-server prompt override [hosts: mistral-vibe]" + ) + mcp_configure_parser.add_argument( + "--sampling-enabled", + action="store_true", + default=None, + help="Enable model sampling for tool calls [hosts: mistral-vibe]", + ) + mcp_configure_parser.add_argument( + "--api-key-env", + help="Environment variable containing API key for remote auth [hosts: mistral-vibe]", + ) + mcp_configure_parser.add_argument( + "--api-key-header", + help="HTTP header name used for API key injection [hosts: mistral-vibe]", + ) + mcp_configure_parser.add_argument( + "--api-key-format", + help="Formatting template for API key header values [hosts: mistral-vibe]", ) mcp_configure_parser.add_argument( @@ -972,7 +995,7 @@ def main() -> int: """ # Configure logging logging.basicConfig( - level=logging.INFO, + level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) @@ -1010,8 +1033,15 @@ def main() -> int: default=Path.home() / ".hatch" / "cache", help="Directory to store cached packages", ) + parser.add_argument( + "--log-level", + default="WARNING", + choices=["DEBUG", "INFO", "WARNING", "ERROR"], + help="Log verbosity level (default: WARNING)", + ) args = parser.parse_args() + logging.getLogger().setLevel(getattr(logging, args.log_level)) # Initialize managers (lazy - only when needed) from hatch.environment_manager import HatchEnvironmentManager diff --git a/hatch/cli/cli_mcp.py b/hatch/cli/cli_mcp.py index 457e2f4..a5c1fc3 100644 --- a/hatch/cli/cli_mcp.py +++ b/hatch/cli/cli_mcp.py @@ -13,6 +13,7 @@ - codex: OpenAI Codex - lm-studio: LM Studio - gemini: Google Gemini + - mistral-vibe: Mistral Vibe CLI Command Groups: Discovery: @@ -71,6 +72,72 @@ ) +def _apply_mistral_vibe_cli_mappings( + config_data: dict, + *, + command: Optional[str], + url: Optional[str], + http_url: Optional[str], + bearer_token_env_var: Optional[str], + env_header: Optional[list], + api_key_env: Optional[str], + api_key_header: Optional[str], + api_key_format: Optional[str], +) -> dict: + """Map generic CLI flags to Mistral Vibe's host-native MCP fields.""" + result = dict(config_data) + result.pop("cwd", None) + + if command is not None: + result["transport"] = "stdio" + elif http_url is not None: + result.pop("httpUrl", None) + result["url"] = http_url + result["transport"] = "http" + elif url is not None: + result["transport"] = "streamable-http" + + if env_header and len(env_header) > 1: + raise ValidationError( + "mistral-vibe supports at most one --env-header mapping", + field="--env-header", + suggestion=( + "Use a single KEY=ENV_VAR pair or the dedicated --api-key-* flags" + ), + ) + + mapped_api_key_env = api_key_env + mapped_api_key_header = api_key_header + mapped_api_key_format = api_key_format + + if env_header: + header_name, env_var_name = env_header[0].split("=", 1) + if mapped_api_key_header is None: + mapped_api_key_header = header_name + if mapped_api_key_env is None: + mapped_api_key_env = env_var_name + + if bearer_token_env_var is not None: + if mapped_api_key_env is None: + mapped_api_key_env = bearer_token_env_var + if mapped_api_key_header is None: + mapped_api_key_header = "Authorization" + if mapped_api_key_format is None: + mapped_api_key_format = "Bearer {api_key}" + + if mapped_api_key_env is not None: + result["api_key_env"] = mapped_api_key_env + if mapped_api_key_header is not None: + result["api_key_header"] = mapped_api_key_header + if mapped_api_key_format is not None: + result["api_key_format"] = mapped_api_key_format + + result.pop("bearer_token_env_var", None) + result.pop("env_http_headers", None) + + return result + + def handle_mcp_discover_hosts(args: Namespace) -> int: """Handle 'hatch mcp discover hosts' command. @@ -1493,6 +1560,11 @@ def handle_mcp_configure(args: Namespace) -> int: startup_timeout: Optional[int] = getattr(args, "startup_timeout", None) tool_timeout: Optional[int] = getattr(args, "tool_timeout", None) enabled: Optional[bool] = getattr(args, "enabled", None) + prompt: Optional[str] = getattr(args, "prompt", None) + sampling_enabled: Optional[bool] = getattr(args, "sampling_enabled", None) + api_key_env: Optional[str] = getattr(args, "api_key_env", None) + api_key_header: Optional[str] = getattr(args, "api_key_header", None) + api_key_format: Optional[str] = getattr(args, "api_key_format", None) bearer_token_env_var: Optional[str] = getattr( args, "bearer_token_env_var", None ) @@ -1514,18 +1586,6 @@ def handle_mcp_configure(args: Namespace) -> int: ) return EXIT_ERROR - # Validate Claude Desktop/Code transport restrictions (Issue 2) - if host_type in (MCPHostType.CLAUDE_DESKTOP, MCPHostType.CLAUDE_CODE): - if url is not None: - format_validation_error( - ValidationError( - f"{host} does not support remote servers (--url)", - field="--url", - suggestion="Only local servers with --command are supported for this host", - ) - ) - return EXIT_ERROR - # Validate argument dependencies if command and header: format_validation_error( @@ -1604,7 +1664,7 @@ def handle_mcp_configure(args: Namespace) -> int: config_data["timeout"] = timeout if trust: config_data["trust"] = trust - if cwd is not None: + if cwd is not None and host_type != MCPHostType.MISTRAL_VIBE: config_data["cwd"] = cwd if http_url is not None: config_data["httpUrl"] = http_url @@ -1636,11 +1696,21 @@ def handle_mcp_configure(args: Namespace) -> int: config_data["startup_timeout_sec"] = startup_timeout if tool_timeout is not None: config_data["tool_timeout_sec"] = tool_timeout + if prompt is not None: + config_data["prompt"] = prompt + if sampling_enabled is not None: + config_data["sampling_enabled"] = sampling_enabled + if api_key_env is not None: + config_data["api_key_env"] = api_key_env + if api_key_header is not None: + config_data["api_key_header"] = api_key_header + if api_key_format is not None: + config_data["api_key_format"] = api_key_format if enabled is not None: config_data["enabled"] = enabled - if bearer_token_env_var is not None: + if bearer_token_env_var is not None and host_type != MCPHostType.MISTRAL_VIBE: config_data["bearer_token_env_var"] = bearer_token_env_var - if env_header is not None: + if env_header is not None and host_type != MCPHostType.MISTRAL_VIBE: env_http_headers = {} for header_spec in env_header: if "=" in header_spec: @@ -1649,6 +1719,19 @@ def handle_mcp_configure(args: Namespace) -> int: if env_http_headers: config_data["env_http_headers"] = env_http_headers + if host_type == MCPHostType.MISTRAL_VIBE: + config_data = _apply_mistral_vibe_cli_mappings( + config_data, + command=command, + url=url, + http_url=http_url, + bearer_token_env_var=bearer_token_env_var, + env_header=env_header, + api_key_env=api_key_env, + api_key_header=api_key_header, + api_key_format=api_key_format, + ) + # Partial update merge logic if is_update: existing_data = existing_config.model_dump( @@ -1661,6 +1744,7 @@ def handle_mcp_configure(args: Namespace) -> int: existing_data.pop("command", None) existing_data.pop("args", None) existing_data.pop("type", None) + existing_data.pop("transport", None) if command is not None and ( existing_config.url is not None @@ -1670,6 +1754,10 @@ def handle_mcp_configure(args: Namespace) -> int: existing_data.pop("httpUrl", None) existing_data.pop("headers", None) existing_data.pop("type", None) + existing_data.pop("transport", None) + existing_data.pop("api_key_env", None) + existing_data.pop("api_key_header", None) + existing_data.pop("api_key_format", None) merged_data = {**existing_data, **config_data} config_data = merged_data diff --git a/hatch/environment_manager.py b/hatch/environment_manager.py index 19a5e6f..70413a0 100644 --- a/hatch/environment_manager.py +++ b/hatch/environment_manager.py @@ -62,7 +62,6 @@ def __init__( """ self.logger = logging.getLogger("hatch.environment_manager") - self.logger.setLevel(logging.INFO) # Set up environment directories self.environments_dir = environments_dir or (Path.home() / ".hatch" / "envs") self.environments_dir.mkdir(exist_ok=True) diff --git a/hatch/installers/dependency_installation_orchestrator.py b/hatch/installers/dependency_installation_orchestrator.py index 3be72ba..13ecac1 100644 --- a/hatch/installers/dependency_installation_orchestrator.py +++ b/hatch/installers/dependency_installation_orchestrator.py @@ -67,7 +67,6 @@ def __init__( registry_data (Dict[str, Any]): Registry data for dependency resolution. """ self.logger = logging.getLogger("hatch.dependency_orchestrator") - self.logger.setLevel(logging.INFO) self.package_loader = package_loader self.registry_service = registry_service self.registry_data = registry_data diff --git a/hatch/installers/docker_installer.py b/hatch/installers/docker_installer.py index e3a7da7..150a2dc 100644 --- a/hatch/installers/docker_installer.py +++ b/hatch/installers/docker_installer.py @@ -20,7 +20,6 @@ from .registry import installer_registry logger = logging.getLogger("hatch.installers.docker_installer") -logger.setLevel(logging.INFO) # Handle docker-py import with graceful fallback DOCKER_AVAILABLE = False diff --git a/hatch/installers/python_installer.py b/hatch/installers/python_installer.py index 420471c..70419d2 100644 --- a/hatch/installers/python_installer.py +++ b/hatch/installers/python_installer.py @@ -31,7 +31,6 @@ class PythonInstaller(DependencyInstaller): def __init__(self): """Initialize the PythonInstaller.""" self.logger = logging.getLogger("hatch.installers.python_installer") - self.logger.setLevel(logging.INFO) @property def installer_type(self) -> str: diff --git a/hatch/installers/system_installer.py b/hatch/installers/system_installer.py index 95820bc..1c0f475 100644 --- a/hatch/installers/system_installer.py +++ b/hatch/installers/system_installer.py @@ -33,7 +33,6 @@ class SystemInstaller(DependencyInstaller): def __init__(self): """Initialize the SystemInstaller.""" self.logger = logging.getLogger("hatch.installers.system_installer") - self.logger.setLevel(logging.INFO) @property def installer_type(self) -> str: diff --git a/hatch/mcp_host_config/adapters/__init__.py b/hatch/mcp_host_config/adapters/__init__.py index a949b0e..ec750fd 100644 --- a/hatch/mcp_host_config/adapters/__init__.py +++ b/hatch/mcp_host_config/adapters/__init__.py @@ -4,6 +4,7 @@ Each adapter handles validation and serialization for a specific MCP host. """ +from hatch.mcp_host_config.adapters.augment import AugmentAdapter from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter from hatch.mcp_host_config.adapters.claude import ClaudeAdapter from hatch.mcp_host_config.adapters.codex import CodexAdapter @@ -11,6 +12,8 @@ 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.mistral_vibe import MistralVibeAdapter +from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter from hatch.mcp_host_config.adapters.registry import ( AdapterRegistry, get_adapter, @@ -27,11 +30,14 @@ "get_adapter", "get_default_registry", # Host-specific adapters + "AugmentAdapter", "ClaudeAdapter", "CodexAdapter", "CursorAdapter", "GeminiAdapter", "KiroAdapter", "LMStudioAdapter", + "MistralVibeAdapter", + "OpenCodeAdapter", "VSCodeAdapter", ] diff --git a/hatch/mcp_host_config/adapters/augment.py b/hatch/mcp_host_config/adapters/augment.py new file mode 100644 index 0000000..7853447 --- /dev/null +++ b/hatch/mcp_host_config/adapters/augment.py @@ -0,0 +1,119 @@ +"""Augment Code adapter for MCP host configuration. + +Augment Code (auggie CLI + extensions) uses the same field set as Claude: +command/args/env for stdio, url/headers for sse/http, with optional type discriminator. +Config file: ~/.augment/settings.json, root key: mcpServers. +""" + +from typing import Any, Dict, FrozenSet + +from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter +from hatch.mcp_host_config.fields import AUGMENT_FIELDS +from hatch.mcp_host_config.models import MCPServerConfig + + +class AugmentAdapter(BaseAdapter): + """Adapter for Augment Code MCP host. + + Augment Code uses the same configuration format as Claude: + - Supports 'type' field for transport discrimination + - Requires exactly one transport (command XOR url) + """ + + @property + def host_name(self) -> str: + """Return the host identifier.""" + return "augment" + + def get_supported_fields(self) -> FrozenSet[str]: + """Return fields supported by Augment Code.""" + return AUGMENT_FIELDS + + def validate(self, config: MCPServerConfig) -> None: + """Validate configuration for Augment Code. + + DEPRECATED: This method is deprecated and will be removed in v0.9.0. + Use validate_filtered() instead. + """ + 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, + ) + + if config.type is not None: + if config.type == "stdio" and not has_command: + raise AdapterValidationError( + "type='stdio' requires 'command' field", + field="type", + host_name=self.host_name, + ) + if config.type in ("sse", "http") and not has_url: + raise AdapterValidationError( + f"type='{config.type}' requires 'url' field", + field="type", + host_name=self.host_name, + ) + + def validate_filtered(self, filtered: Dict[str, Any]) -> None: + """Validate filtered configuration for Augment Code. + + Validates only fields that survived filtering (supported by Augment). + Augment Code 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, + ) + + if "type" in filtered: + config_type = filtered["type"] + if config_type == "stdio" and not has_command: + raise AdapterValidationError( + "type='stdio' requires 'command' field", + field="type", + host_name=self.host_name, + ) + if config_type in ("sse", "http") and not has_url: + raise AdapterValidationError( + f"type='{config_type}' requires 'url' field", + field="type", + host_name=self.host_name, + ) + + def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: + """Serialize configuration for Augment Code format. + + Follows the validate-after-filter pattern: + 1. Filter to supported fields + 2. Validate filtered fields + 3. Return filtered (no transformations needed) + """ + filtered = self.filter_fields(config) + self.validate_filtered(filtered) + return filtered diff --git a/hatch/mcp_host_config/adapters/claude.py b/hatch/mcp_host_config/adapters/claude.py index 9761080..a17daa5 100644 --- a/hatch/mcp_host_config/adapters/claude.py +++ b/hatch/mcp_host_config/adapters/claude.py @@ -161,5 +161,11 @@ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: # Validate filtered fields self.validate_filtered(filtered) - # Return filtered (no transformations needed for Claude) + # Claude's URL-based remote configs should explicitly declare HTTP + # transport in serialized output. + if "url" in filtered: + filtered = filtered.copy() + filtered["type"] = "http" + + # Return filtered Claude config return filtered diff --git a/hatch/mcp_host_config/adapters/mistral_vibe.py b/hatch/mcp_host_config/adapters/mistral_vibe.py new file mode 100644 index 0000000..8f51419 --- /dev/null +++ b/hatch/mcp_host_config/adapters/mistral_vibe.py @@ -0,0 +1,116 @@ +"""Mistral Vibe adapter for MCP host configuration. + +Mistral Vibe uses TOML `[[mcp_servers]]` entries with an explicit `transport` +field instead of the Claude-style `type` discriminator. +""" + +from typing import Any, Dict, FrozenSet + +from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter +from hatch.mcp_host_config.fields import MISTRAL_VIBE_FIELDS +from hatch.mcp_host_config.models import MCPServerConfig + + +class MistralVibeAdapter(BaseAdapter): + """Adapter for Mistral Vibe MCP server configuration.""" + + @property + def host_name(self) -> str: + """Return the host identifier.""" + return "mistral-vibe" + + def get_supported_fields(self) -> FrozenSet[str]: + """Return fields supported by Mistral Vibe.""" + return MISTRAL_VIBE_FIELDS + + def validate(self, config: MCPServerConfig) -> None: + """Deprecated compatibility wrapper for legacy adapter tests.""" + self.validate_filtered(self.filter_fields(config)) + + def validate_filtered(self, filtered: Dict[str, Any]) -> None: + """Validate Mistral Vibe transport rules on filtered fields.""" + has_command = "command" in filtered + has_url = "url" in filtered + transport_count = sum([has_command, has_url]) + + if transport_count == 0: + raise AdapterValidationError( + "Either 'command' or 'url' must be specified", + host_name=self.host_name, + ) + + if transport_count > 1: + raise AdapterValidationError( + "Cannot specify multiple transports - choose exactly one of 'command' or 'url'", + host_name=self.host_name, + ) + + transport = filtered.get("transport") + if transport == "stdio" and not has_command: + raise AdapterValidationError( + "transport='stdio' requires 'command' field", + field="transport", + host_name=self.host_name, + ) + if transport in ("http", "streamable-http") and not has_url: + raise AdapterValidationError( + f"transport='{transport}' requires 'url' field", + field="transport", + host_name=self.host_name, + ) + + def apply_transformations( + self, filtered: Dict[str, Any], transport_hint: str | None = None + ) -> Dict[str, Any]: + """Apply Mistral Vibe field/value transformations.""" + result = dict(filtered) + + transport = ( + result.get("transport") or transport_hint or self._infer_transport(result) + ) + result["transport"] = transport + + return result + + def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: + """Serialize configuration for Mistral Vibe format.""" + filtered = self.filter_fields(config) + + # Support cross-host sync hints without advertising these as native fields. + if ( + "command" not in filtered + and "url" not in filtered + and config.httpUrl is not None + ): + filtered["url"] = config.httpUrl + + transport_hint = self._infer_transport(filtered, config=config) + if transport_hint is not None: + filtered["transport"] = transport_hint + + self.validate_filtered(filtered) + return self.apply_transformations(filtered) + + def _infer_transport( + self, filtered: Dict[str, Any], config: MCPServerConfig | None = None + ) -> str | None: + """Infer Vibe transport from canonical MCP fields.""" + if "transport" in filtered: + return filtered["transport"] + if "command" in filtered: + return "stdio" + + config_type = config.type if config is not None else None + if config_type == "stdio": + return "stdio" + if config_type == "http": + return "http" + if config_type == "sse": + return "streamable-http" + + if config is not None and config.httpUrl is not None: + return "http" + if "url" in filtered: + return "streamable-http" + + return None 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..53ae661 100644 --- a/hatch/mcp_host_config/adapters/registry.py +++ b/hatch/mcp_host_config/adapters/registry.py @@ -6,6 +6,7 @@ from typing import Dict, List, Optional +from hatch.mcp_host_config.adapters.augment import AugmentAdapter from hatch.mcp_host_config.adapters.base import BaseAdapter from hatch.mcp_host_config.adapters.claude import ClaudeAdapter from hatch.mcp_host_config.adapters.codex import CodexAdapter @@ -13,6 +14,8 @@ 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.mistral_vibe import MistralVibeAdapter +from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter from hatch.mcp_host_config.adapters.vscode import VSCodeAdapter @@ -32,7 +35,7 @@ class AdapterRegistry: 'claude-desktop' >>> registry.get_supported_hosts() - ['claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'kiro', 'lmstudio', 'vscode'] + ['augment', 'claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'kiro', 'lmstudio', 'mistral-vibe', 'opencode', 'vscode'] """ def __init__(self): @@ -53,6 +56,9 @@ def _register_defaults(self) -> None: self.register(GeminiAdapter()) self.register(KiroAdapter()) self.register(CodexAdapter()) + self.register(MistralVibeAdapter()) + self.register(OpenCodeAdapter()) + self.register(AugmentAdapter()) 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..9c36ec8 100644 --- a/hatch/mcp_host_config/backup.py +++ b/hatch/mcp_host_config/backup.py @@ -48,6 +48,9 @@ def validate_hostname(cls, v): "gemini", "kiro", "codex", + "mistral-vibe", + "opencode", + "augment", } 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..942fef9 100644 --- a/hatch/mcp_host_config/fields.py +++ b/hatch/mcp_host_config/fields.py @@ -27,13 +27,14 @@ # ============================================================================ # Hosts that support the 'type' discriminator field (stdio/sse/http) -# Note: Gemini, Kiro, Codex do NOT support this field +# Note: Gemini, Kiro, Codex, and Mistral Vibe do NOT support this field TYPE_SUPPORTING_HOSTS: FrozenSet[str] = frozenset( { "claude-desktop", "claude-code", "vscode", "cursor", + "augment", } ) @@ -116,6 +117,40 @@ ) +# Fields supported by Mistral Vibe (TOML array-of-tables with explicit transport) +MISTRAL_VIBE_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset( + { + "transport", # Vibe transport discriminator: stdio/http/streamable-http + "prompt", # Optional per-server prompt override + "sampling_enabled", # Enable model sampling for tool calls + "api_key_env", # Env var containing API key for remote servers + "api_key_header", # Header name for API key injection + "api_key_format", # Header formatting template for API key injection + "startup_timeout_sec", # Server startup timeout + "tool_timeout_sec", # Tool execution timeout + } +) + + +# Fields supported by Augment Code (auggie CLI + extensions); same as Claude fields +# Config: ~/.augment/settings.json, key: mcpServers +AUGMENT_FIELDS: FrozenSet[str] = CLAUDE_FIELDS + +# 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/host_management.py b/hatch/mcp_host_config/host_management.py index d4177f0..1e577aa 100644 --- a/hatch/mcp_host_config/host_management.py +++ b/hatch/mcp_host_config/host_management.py @@ -30,6 +30,7 @@ class MCPHostRegistry: _family_mappings: Dict[str, List[MCPHostType]] = { "claude": [MCPHostType.CLAUDE_DESKTOP, MCPHostType.CLAUDE_CODE], "cursor": [MCPHostType.CURSOR, MCPHostType.LMSTUDIO], + "mistral": [MCPHostType.MISTRAL_VIBE], } @classmethod diff --git a/hatch/mcp_host_config/models.py b/hatch/mcp_host_config/models.py index b7d146f..79b4116 100644 --- a/hatch/mcp_host_config/models.py +++ b/hatch/mcp_host_config/models.py @@ -30,6 +30,9 @@ class MCPHostType(str, Enum): GEMINI = "gemini" KIRO = "kiro" CODEX = "codex" + MISTRAL_VIBE = "mistral-vibe" + OPENCODE = "opencode" + AUGMENT = "augment" class MCPServerConfig(BaseModel): @@ -60,6 +63,10 @@ class MCPServerConfig(BaseModel): type: Optional[Literal["stdio", "sse", "http"]] = Field( None, description="Transport type (stdio for local, sse/http for remote)" ) + transport: Optional[Literal["stdio", "http", "streamable-http"]] = Field( + None, + description="Host-native transport discriminator (e.g. Mistral Vibe)", + ) # stdio transport (local server) command: Optional[str] = Field( @@ -136,15 +143,15 @@ class MCPServerConfig(BaseModel): disabledTools: Optional[List[str]] = Field(None, description="Disabled tool names") # ======================================================================== - # Codex-Specific Fields + # Codex / Mistral Vibe-Specific Fields # ======================================================================== env_vars: Optional[List[str]] = Field( None, description="Environment variables to whitelist/forward" ) - startup_timeout_sec: Optional[int] = Field( + startup_timeout_sec: Optional[float] = Field( None, description="Server startup timeout in seconds" ) - tool_timeout_sec: Optional[int] = Field( + tool_timeout_sec: Optional[float] = Field( None, description="Tool execution timeout in seconds" ) enabled: Optional[bool] = Field( @@ -165,6 +172,30 @@ class MCPServerConfig(BaseModel): env_http_headers: Optional[Dict[str, str]] = Field( None, description="Header names to env var names" ) + prompt: Optional[str] = Field(None, description="Per-server prompt override") + sampling_enabled: Optional[bool] = Field( + None, description="Whether sampling is enabled for tool calls" + ) + api_key_env: Optional[str] = Field( + None, description="Env var containing API key for remote server auth" + ) + api_key_header: Optional[str] = Field( + None, description="HTTP header name used for API key injection" + ) + api_key_format: Optional[str] = Field( + None, description="Formatting template for API key header values" + ) + + # ======================================================================== + # 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) @@ -226,6 +257,8 @@ def is_stdio(self) -> bool: 1. Explicit type="stdio" field takes precedence 2. Otherwise, presence of 'command' field indicates stdio """ + if self.transport is not None: + return self.transport == "stdio" if self.type is not None: return self.type == "stdio" return self.command is not None @@ -240,6 +273,8 @@ def is_sse(self) -> bool: 1. Explicit type="sse" field takes precedence 2. Otherwise, presence of 'url' field indicates SSE """ + if self.transport is not None: + return False if self.type is not None: return self.type == "sse" return self.url is not None @@ -254,6 +289,8 @@ def is_http(self) -> bool: 1. Explicit type="http" field takes precedence 2. Otherwise, presence of 'httpUrl' field indicates HTTP streaming """ + if self.transport is not None: + return self.transport in ("http", "streamable-http") if self.type is not None: return self.type == "http" return self.httpUrl is not None @@ -265,8 +302,12 @@ def get_transport_type(self) -> Optional[str]: "stdio" for command-based local servers "sse" for URL-based remote servers (SSE transport) "http" for httpUrl-based remote servers (Gemini HTTP streaming) + "streamable-http" for hosts that expose that transport natively None if transport cannot be determined """ + if self.transport is not None: + return self.transport + # Explicit type takes precedence if self.type is not None: return self.type @@ -353,6 +394,10 @@ def validate_host_names(cls, v): "lmstudio", "gemini", "kiro", + "codex", + "mistral-vibe", + "opencode", + "augment", } 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..fd9724d 100644 --- a/hatch/mcp_host_config/reporting.py +++ b/hatch/mcp_host_config/reporting.py @@ -73,6 +73,9 @@ def _get_adapter_host_name(host_type: MCPHostType) -> str: MCPHostType.GEMINI: "gemini", MCPHostType.KIRO: "kiro", MCPHostType.CODEX: "codex", + MCPHostType.MISTRAL_VIBE: "mistral-vibe", + MCPHostType.OPENCODE: "opencode", + MCPHostType.AUGMENT: "augment", } 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..a6c8925 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,352 @@ 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 + + +@register_host_strategy(MCPHostType.MISTRAL_VIBE) +class MistralVibeHostStrategy(MCPHostStrategy): + """Configuration strategy for Mistral Vibe's TOML-based MCP settings.""" + + def get_adapter_host_name(self) -> str: + """Return the adapter host name for Mistral Vibe.""" + return "mistral-vibe" + + def _project_config_path(self) -> Path: + return Path.cwd() / ".vibe" / "config.toml" + + def _global_config_path(self) -> Path: + return Path.home() / ".vibe" / "config.toml" + + def get_config_path(self) -> Optional[Path]: + """Get Mistral Vibe configuration path. + + Vibe prefers project-local `./.vibe/config.toml` when it exists, and + otherwise falls back to the user-global `~/.vibe/config.toml`. + """ + project_path = self._project_config_path() + global_path = self._global_config_path() + + if project_path.exists(): + return project_path + if global_path.exists(): + return global_path + if project_path.parent.exists(): + return project_path + return global_path + + def get_config_key(self) -> str: + """Mistral Vibe uses the `mcp_servers` top-level key.""" + return "mcp_servers" + + def is_host_available(self) -> bool: + """Check if Mistral Vibe is available by checking config directories.""" + return ( + self._project_config_path().parent.exists() + or self._global_config_path().parent.exists() + ) + + def validate_server_config(self, server_config: MCPServerConfig) -> bool: + """Vibe supports local stdio and remote HTTP transports.""" + return any( + value is not None + for value in ( + server_config.command, + server_config.url, + server_config.httpUrl, + ) + ) + + def read_configuration(self) -> HostConfiguration: + """Read Mistral Vibe TOML configuration.""" + config_path = self.get_config_path() + if not config_path or not config_path.exists(): + return HostConfiguration(servers={}) + + try: + with open(config_path, "rb") as f: + toml_data = tomllib.load(f) + + raw_servers = toml_data.get(self.get_config_key(), []) + if not isinstance(raw_servers, list): + logger.warning( + "Invalid Mistral Vibe configuration: mcp_servers must be a list" + ) + return HostConfiguration(servers={}) + + servers = {} + for server_data in raw_servers: + try: + normalized = dict(server_data) + name = normalized.pop("name", None) + if not name: + logger.warning("Skipping unnamed Mistral Vibe MCP server entry") + continue + + transport = normalized.get("transport") + if transport == "stdio": + normalized.setdefault("type", "stdio") + elif transport in ("http", "streamable-http"): + normalized.setdefault("type", "http") + + servers[name] = MCPServerConfig(name=name, **normalized) + except Exception as e: + logger.warning(f"Invalid Mistral Vibe server config: {e}") + continue + + return HostConfiguration(servers=servers) + except Exception as e: + logger.error(f"Failed to read Mistral Vibe configuration: {e}") + return HostConfiguration(servers={}) + + def write_configuration( + self, config: HostConfiguration, no_backup: bool = False + ) -> bool: + """Write Mistral Vibe TOML configuration while preserving other keys.""" + config_path = self.get_config_path() + if not config_path: + return False + + try: + config_path.parent.mkdir(parents=True, exist_ok=True) + + existing_data: Dict[str, Any] = {} + if config_path.exists(): + try: + with open(config_path, "rb") as f: + existing_data = tomllib.load(f) + except Exception: + pass + + adapter = get_adapter(self.get_adapter_host_name()) + servers_data = [] + for name, server_config in config.servers.items(): + serialized = adapter.serialize(server_config) + servers_data.append({"name": name, **serialized}) + + final_data = { + key: value + for key, value in existing_data.items() + if key != self.get_config_key() + } + final_data[self.get_config_key()] = servers_data + + backup_manager = MCPHostConfigBackupManager() + atomic_ops = AtomicFileOperations() + + def toml_serializer(data: Any, f: TextIO) -> None: + f.write(tomli_w.dumps(data)) + + atomic_ops.atomic_write_with_serializer( + file_path=config_path, + data=final_data, + serializer=toml_serializer, + backup_manager=backup_manager, + hostname="mistral-vibe", + skip_backup=no_backup, + ) + + return True + except Exception as e: + logger.error(f"Failed to write Mistral Vibe configuration: {e}") + return False + + +@register_host_strategy(MCPHostType.AUGMENT) +class AugmentHostStrategy(ClaudeHostStrategy): + """Configuration strategy for Augment Code (auggie CLI + extensions). + + Augment Code stores MCP configuration in ~/.augment/settings.json under + the 'mcpServers' key -- the same format as Claude. The settings.json file + may contain other non-MCP Augment settings which are preserved via the + inherited _preserve_claude_settings() mechanism. + """ + + def get_adapter_host_name(self) -> str: + """Return the adapter host name for Augment Code.""" + return "augment" + + def get_config_path(self) -> Optional[Path]: + """Get Augment Code configuration path. + + Same path on macOS, Linux, and Windows WSL. + Native Windows (non-WSL) is not yet confirmed and returns None. + """ + system = platform.system() + if system in ("Darwin", "Linux"): + return Path.home() / ".augment" / "settings.json" + return None + + def is_host_available(self) -> bool: + """Check if Augment Code is installed by checking for ~/.augment/ directory.""" + return (Path.home() / ".augment").exists() diff --git a/hatch/package_loader.py b/hatch/package_loader.py index 5ee3a33..24c59db 100644 --- a/hatch/package_loader.py +++ b/hatch/package_loader.py @@ -31,7 +31,6 @@ def __init__(self, cache_dir: Optional[Path] = None): Defaults to ~/.hatch/packages. """ self.logger = logging.getLogger("hatch.package_loader") - self.logger.setLevel(logging.INFO) # Set up cache directory if cache_dir is None: diff --git a/hatch/python_environment_manager.py b/hatch/python_environment_manager.py index 5b4936d..c139f4f 100644 --- a/hatch/python_environment_manager.py +++ b/hatch/python_environment_manager.py @@ -41,7 +41,6 @@ def __init__(self, environments_dir: Optional[Path] = None): Defaults to ~/.hatch/envs. """ self.logger = logging.getLogger("hatch.python_environment_manager") - self.logger.setLevel(logging.INFO) # Set up environment directories self.environments_dir = environments_dir or (Path.home() / ".hatch" / "envs") diff --git a/hatch/registry_retriever.py b/hatch/registry_retriever.py index d905484..f276a6a 100644 --- a/hatch/registry_retriever.py +++ b/hatch/registry_retriever.py @@ -6,12 +6,23 @@ import json import logging +import sys import requests import datetime from pathlib import Path from typing import Dict, Any, Optional +def _print_registry_status(msg: str) -> None: + if sys.stderr.isatty(): + print(f"\033[2m{msg}\033[0m", end="\r", file=sys.stderr, flush=True) + + +def _clear_registry_status() -> None: + if sys.stderr.isatty(): + print(" " * 60, end="\r", file=sys.stderr, flush=True) + + class RegistryRetriever: """Manages the retrieval and caching of the Hatch package registry. @@ -37,7 +48,6 @@ def __init__( local_registry_cache_path (Path, optional): Path to local registry file. Defaults to None. """ self.logger = logging.getLogger("hatch.registry_retriever") - self.logger.setLevel(logging.INFO) self.cache_ttl = cache_ttl self.simulation_mode = simulation_mode @@ -59,7 +69,7 @@ def __init__( # Use file:// URL format for local files self.registry_url = f"file://{str(self.registry_cache_path.absolute())}" - self.logger.info( + self.logger.debug( f"Operating in simulation mode with registry at: {self.registry_cache_path}" ) else: @@ -69,7 +79,7 @@ def __init__( # We'll set the initial URL to today, but might fall back to yesterday self.registry_url = f"https://github.com/CrackingShells/Hatch-Registry/releases/download/{self.today_str}/hatch_packages_registry.json" - self.logger.info( + self.logger.debug( f"Operating in online mode with registry at: {self.registry_url}" ) @@ -180,7 +190,7 @@ def _fetch_remote_registry(self) -> Dict[str, Any]: """ if self.simulation_mode: try: - self.logger.info(f"Fetching registry from {self.registry_url}") + self.logger.debug(f"Fetching registry from {self.registry_url}") with open(self.registry_cache_path, "r") as f: return json.load(f) except Exception as e: @@ -193,7 +203,7 @@ def _fetch_remote_registry(self) -> Dict[str, Any]: self.registry_url = f"https://github.com/CrackingShells/Hatch-Registry/releases/download/{date}/hatch_packages_registry.json" self.is_delayed = False # Reset delayed flag for today's registry else: - self.logger.info( + self.logger.warning( f"Today's registry ({date}) not found, falling back to yesterday's" ) # Fall back to yesterday's registry @@ -211,7 +221,7 @@ def _fetch_remote_registry(self) -> Dict[str, Any]: self.is_delayed = True # Set delayed flag for yesterday's registry try: - self.logger.info(f"Fetching registry from {self.registry_url}") + self.logger.debug(f"Fetching registry from {self.registry_url}") response = requests.get(self.registry_url, timeout=30) response.raise_for_status() return response.json() @@ -298,8 +308,9 @@ def get_registry(self, force_refresh: bool = False) -> Dict[str, Any]: # In simulation mode, we must have a local registry file registry_data = self._read_local_cache() else: - # In online mode, fetch from remote URL + _print_registry_status(" Refreshing registry cache...") registry_data = self._fetch_remote_registry() + _clear_registry_status() # Update local cache # Note that in case of simulation mode AND default cache path, diff --git a/mkdocs.yml b/mkdocs.yml index 1a044b2..b5752f4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,8 +7,28 @@ docs_dir: docs theme: name: material + custom_dir: overrides + logo: https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_wide_light_bg_transparent.png + favicon: https://raw.githubusercontent.com/CrackingShells/.github/main/resources/images/hatch_icon_light_bg_transparent.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: egg-shell + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode features: - content.code.copy + - navigation.tabs + - navigation.top + - toc.follow + +extra_css: + - stylesheets/brand.css plugins: - search @@ -31,6 +51,13 @@ markdown_extensions: - admonition - tables - fenced_code + - attr_list + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.inlinehilite - toc: permalink: true diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..47cd516 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/pyproject.toml b/pyproject.toml index a486279..ca04048 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hatch-xclam" -version = "0.8.0" +version = "0.8.1-dev.6" description = "Package manager for the Cracking Shells ecosystem" readme = "README.md" requires-python = ">=3.12" diff --git a/tests/integration/cli/test_cli_reporter_integration.py b/tests/integration/cli/test_cli_reporter_integration.py index a0490f5..373ad5f 100644 --- a/tests/integration/cli/test_cli_reporter_integration.py +++ b/tests/integration/cli/test_cli_reporter_integration.py @@ -30,6 +30,46 @@ def _handler_uses_result_reporter(handler_module_source: str) -> bool: class TestMCPConfigureHandlerIntegration: """Integration tests for handle_mcp_configure → ResultReporter flow.""" + @staticmethod + def _base_configure_args(**overrides): + """Create a baseline Namespace for handle_mcp_configure tests.""" + data = dict( + host="claude-desktop", + server_name="test-server", + server_command="python", + args=["server.py"], + env_var=None, + url=None, + header=None, + timeout=None, + trust=False, + cwd=None, + env_file=None, + http_url=None, + include_tools=None, + exclude_tools=None, + input=None, + disabled=None, + auto_approve_tools=None, + disable_tools=None, + env_vars=None, + startup_timeout=None, + tool_timeout=None, + enabled=None, + prompt=None, + sampling_enabled=None, + api_key_env=None, + api_key_header=None, + api_key_format=None, + bearer_token_env_var=None, + env_header=None, + no_backup=True, + dry_run=False, + auto_approve=True, + ) + data.update(overrides) + return Namespace(**data) + def test_handler_imports_result_reporter(self): """Handler module should import ResultReporter from cli_utils. @@ -60,35 +100,7 @@ def test_handler_uses_result_reporter_for_output(self): from hatch.cli.cli_mcp import handle_mcp_configure # Create mock args for a simple configure operation - args = Namespace( - host="claude-desktop", - server_name="test-server", - server_command="python", - args=["server.py"], - env_var=None, - url=None, - header=None, - timeout=None, - trust=False, - cwd=None, - env_file=None, - http_url=None, - include_tools=None, - exclude_tools=None, - input=None, - disabled=None, - auto_approve_tools=None, - disable_tools=None, - env_vars=None, - startup_timeout=None, - tool_timeout=None, - enabled=None, - bearer_token_env_var=None, - env_header=None, - no_backup=True, - dry_run=False, - auto_approve=True, # Skip confirmation - ) + args = self._base_configure_args(auto_approve=True) # Mock the MCPHostConfigurationManager with patch( @@ -123,35 +135,7 @@ def test_handler_dry_run_shows_preview(self): """ from hatch.cli.cli_mcp import handle_mcp_configure - args = Namespace( - host="claude-desktop", - server_name="test-server", - server_command="python", - args=["server.py"], - env_var=None, - url=None, - header=None, - timeout=None, - trust=False, - cwd=None, - env_file=None, - http_url=None, - include_tools=None, - exclude_tools=None, - input=None, - disabled=None, - auto_approve_tools=None, - disable_tools=None, - env_vars=None, - startup_timeout=None, - tool_timeout=None, - enabled=None, - bearer_token_env_var=None, - env_header=None, - no_backup=True, - dry_run=True, # Dry-run enabled - auto_approve=True, - ) + args = self._base_configure_args(dry_run=True, auto_approve=True) with patch( "hatch.cli.cli_mcp.MCPHostConfigurationManager" @@ -185,35 +169,7 @@ def test_handler_shows_prompt_before_confirmation(self): """ from hatch.cli.cli_mcp import handle_mcp_configure - args = Namespace( - host="claude-desktop", - server_name="test-server", - server_command="python", - args=["server.py"], - env_var=None, - url=None, - header=None, - timeout=None, - trust=False, - cwd=None, - env_file=None, - http_url=None, - include_tools=None, - exclude_tools=None, - input=None, - disabled=None, - auto_approve_tools=None, - disable_tools=None, - env_vars=None, - startup_timeout=None, - tool_timeout=None, - enabled=None, - bearer_token_env_var=None, - env_header=None, - no_backup=True, - dry_run=False, - auto_approve=False, # Will prompt for confirmation - ) + args = self._base_configure_args(auto_approve=False) with patch( "hatch.cli.cli_mcp.MCPHostConfigurationManager" @@ -240,6 +196,112 @@ def test_handler_shows_prompt_before_confirmation(self): "hatch mcp configure" in output or "[CONFIGURE]" in output ), "Handler should show consequence preview before confirmation" + def test_mistral_vibe_maps_http_and_api_key_flags(self): + """Mistral Vibe should map reusable CLI flags to host-native fields.""" + from hatch.cli.cli_mcp import handle_mcp_configure + + args = self._base_configure_args( + host="mistral-vibe", + server_command=None, + args=None, + http_url="https://example.com/mcp", + startup_timeout=15, + tool_timeout=90, + bearer_token_env_var="MISTRAL_API_KEY", + prompt="Be concise.", + sampling_enabled=True, + auto_approve=True, + ) + + with patch( + "hatch.cli.cli_mcp.MCPHostConfigurationManager" + ) as mock_manager_class: + mock_manager = MagicMock() + mock_manager.get_server_config.return_value = None + mock_result = MagicMock(success=True, backup_path=None) + mock_manager.configure_server.return_value = mock_result + mock_manager_class.return_value = mock_manager + + result = handle_mcp_configure(args) + + assert result == EXIT_SUCCESS + + passed_config = mock_manager.configure_server.call_args.kwargs["server_config"] + assert passed_config.url == "https://example.com/mcp" + assert passed_config.transport == "http" + assert passed_config.httpUrl is None + assert passed_config.startup_timeout_sec == 15 + assert passed_config.tool_timeout_sec == 90 + assert passed_config.prompt == "Be concise." + assert passed_config.sampling_enabled is True + assert passed_config.api_key_env == "MISTRAL_API_KEY" + assert passed_config.api_key_header == "Authorization" + assert passed_config.api_key_format == "Bearer {api_key}" + assert passed_config.cwd is None + + def test_mistral_vibe_maps_env_header_to_api_key_fields(self): + """Single --env-header should map to Mistral Vibe api_key_* fields.""" + from hatch.cli.cli_mcp import handle_mcp_configure + + args = self._base_configure_args( + host="mistral-vibe", + server_command=None, + args=None, + url="https://example.com/mcp", + env_header=["X-API-Key=MISTRAL_TOKEN"], + api_key_format="Token {api_key}", + auto_approve=True, + ) + + with patch( + "hatch.cli.cli_mcp.MCPHostConfigurationManager" + ) as mock_manager_class: + mock_manager = MagicMock() + mock_manager.get_server_config.return_value = None + mock_result = MagicMock(success=True, backup_path=None) + mock_manager.configure_server.return_value = mock_result + mock_manager_class.return_value = mock_manager + + result = handle_mcp_configure(args) + + assert result == EXIT_SUCCESS + + passed_config = mock_manager.configure_server.call_args.kwargs["server_config"] + assert passed_config.url == "https://example.com/mcp" + assert passed_config.transport == "streamable-http" + assert passed_config.api_key_env == "MISTRAL_TOKEN" + assert passed_config.api_key_header == "X-API-Key" + assert passed_config.api_key_format == "Token {api_key}" + + def test_mistral_vibe_does_not_forward_cwd(self): + """Mistral Vibe should ignore --cwd because the host config has no cwd field.""" + from hatch.cli.cli_mcp import handle_mcp_configure + + args = self._base_configure_args( + host="mistral-vibe", + server_command="python", + args=["server.py"], + cwd="/tmp/mistral", + auto_approve=True, + ) + + with patch( + "hatch.cli.cli_mcp.MCPHostConfigurationManager" + ) as mock_manager_class: + mock_manager = MagicMock() + mock_manager.get_server_config.return_value = None + mock_result = MagicMock(success=True, backup_path=None) + mock_manager.configure_server.return_value = mock_result + mock_manager_class.return_value = mock_manager + + result = handle_mcp_configure(args) + + assert result == EXIT_SUCCESS + passed_config = mock_manager.configure_server.call_args.kwargs["server_config"] + assert passed_config.command == "python" + assert passed_config.transport == "stdio" + assert passed_config.cwd is None + class TestMCPSyncHandlerIntegration: """Integration tests for handle_mcp_sync → ResultReporter flow.""" diff --git a/tests/regression/mcp/test_claude_transport_serialization.py b/tests/regression/mcp/test_claude_transport_serialization.py new file mode 100644 index 0000000..ffae1de --- /dev/null +++ b/tests/regression/mcp/test_claude_transport_serialization.py @@ -0,0 +1,66 @@ +"""Regression tests for Claude-family transport serialization.""" + +import json +from pathlib import Path + +import pytest + +from hatch.mcp_host_config.adapters.claude import ClaudeAdapter +from hatch.mcp_host_config.models import MCPServerConfig + +try: + from wobble.decorators import regression_test +except ImportError: + + def regression_test(func): + return func + + +FIXTURES_PATH = ( + Path(__file__).resolve().parents[2] + / "test_data" + / "mcp_adapters" + / "claude_transport_regressions.json" +) + +with open(FIXTURES_PATH) as f: + FIXTURES = json.load(f) + + +def get_variant(host_name: str) -> str: + """Return Claude adapter variant from host name.""" + return host_name.removeprefix("claude-") + + +class TestClaudeTransportSerialization: + """Regression coverage for Claude Desktop/Code transport serialization.""" + + @pytest.mark.parametrize( + "test_case", + FIXTURES["remote_http"], + ids=lambda tc: f'{tc["host"]}-{tc["case"]}', + ) + @regression_test + def test_remote_url_defaults_to_http_type(self, test_case): + """URL-based Claude configs serialize with explicit HTTP transport.""" + adapter = ClaudeAdapter(variant=get_variant(test_case["host"])) + config = MCPServerConfig(**test_case["config"]) + + result = adapter.serialize(config) + + assert result == test_case["expected"] + + @pytest.mark.parametrize( + "test_case", + FIXTURES["stdio_without_type"], + ids=lambda tc: f'{tc["host"]}-{tc["case"]}', + ) + @regression_test + def test_stdio_config_does_not_require_type_input(self, test_case): + """Stdio Claude configs still serialize when type is omitted.""" + adapter = ClaudeAdapter(variant=get_variant(test_case["host"])) + config = MCPServerConfig(**test_case["config"]) + + result = adapter.serialize(config) + + assert result == test_case["expected"] diff --git a/tests/regression/mcp/test_field_filtering_v2.py b/tests/regression/mcp/test_field_filtering_v2.py index 7ba770d..9ca2929 100644 --- a/tests/regression/mcp/test_field_filtering_v2.py +++ b/tests/regression/mcp/test_field_filtering_v2.py @@ -57,6 +57,11 @@ def regression_test(func): "oauth_redirectUri": "http://localhost:3000/callback", "oauth_tokenParamName": "access_token", "bearer_token_env_var": "BEARER_TOKEN", + "prompt": "Be concise.", + "api_key_env": "MISTRAL_API_KEY", + "api_key_header": "Authorization", + "api_key_format": "Bearer {api_key}", + "transport": "streamable-http", # Integer fields "timeout": 30000, "startup_timeout_sec": 10, @@ -66,6 +71,7 @@ def regression_test(func): "oauth_enabled": False, "disabled": False, "enabled": True, + "sampling_enabled": True, # List[str] fields "args": ["--test"], "includeTools": ["tool1"], @@ -77,6 +83,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..1d53c30 100644 --- a/tests/test_data/mcp_adapters/canonical_configs.json +++ b/tests/test_data/mcp_adapters/canonical_configs.json @@ -74,5 +74,39 @@ "cwd": "/app", "enabled_tools": ["tool1", "tool2"], "disabled_tools": ["tool3"] + }, + "mistral-vibe": { + "command": null, + "args": null, + "env": null, + "url": "https://example.com/mcp", + "headers": {"Authorization": "Bearer test-token"}, + "transport": "streamable-http", + "prompt": "Use concise answers.", + "startup_timeout_sec": 15, + "tool_timeout_sec": 90, + "sampling_enabled": true, + "api_key_env": "MISTRAL_API_KEY", + "api_key_header": "Authorization", + "api_key_format": "Bearer {api_key}" + }, + "augment": { + "command": "python", + "args": ["-m", "mcp_server"], + "env": {"API_KEY": "test_key"}, + "url": null, + "headers": null, + "type": "stdio" + }, + "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/claude_transport_regressions.json b/tests/test_data/mcp_adapters/claude_transport_regressions.json new file mode 100644 index 0000000..f3a4589 --- /dev/null +++ b/tests/test_data/mcp_adapters/claude_transport_regressions.json @@ -0,0 +1,118 @@ +{ + "remote_http": [ + { + "case": "input_type_omitted", + "host": "claude-desktop", + "config": { + "name": "remote-server", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + } + }, + "expected": { + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + }, + "type": "http" + } + }, + { + "case": "input_type_omitted", + "host": "claude-code", + "config": { + "name": "remote-server", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + } + }, + "expected": { + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + }, + "type": "http" + } + }, + { + "case": "input_type_sse", + "host": "claude-desktop", + "config": { + "name": "remote-server", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + }, + "type": "sse" + }, + "expected": { + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + }, + "type": "http" + } + }, + { + "case": "input_type_sse", + "host": "claude-code", + "config": { + "name": "remote-server", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + }, + "type": "sse" + }, + "expected": { + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + }, + "type": "http" + } + } + ], + "stdio_without_type": [ + { + "case": "input_type_omitted", + "host": "claude-desktop", + "config": { + "name": "stdio-server", + "command": "python", + "args": ["-m", "mcp_server"], + "env": { + "API_KEY": "secret" + } + }, + "expected": { + "command": "python", + "args": ["-m", "mcp_server"], + "env": { + "API_KEY": "secret" + } + } + }, + { + "case": "input_type_omitted", + "host": "claude-code", + "config": { + "name": "stdio-server", + "command": "python", + "args": ["-m", "mcp_server"], + "env": { + "API_KEY": "secret" + } + }, + "expected": { + "command": "python", + "args": ["-m", "mcp_server"], + "env": { + "API_KEY": "secret" + } + } + } + ] +} diff --git a/tests/test_data/mcp_adapters/host_registry.py b/tests/test_data/mcp_adapters/host_registry.py index 34d6a49..e8fb5bb 100644 --- a/tests/test_data/mcp_adapters/host_registry.py +++ b/tests/test_data/mcp_adapters/host_registry.py @@ -19,6 +19,7 @@ from pathlib import Path from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple +from hatch.mcp_host_config.adapters.augment import AugmentAdapter from hatch.mcp_host_config.adapters.base import BaseAdapter from hatch.mcp_host_config.adapters.claude import ClaudeAdapter from hatch.mcp_host_config.adapters.codex import CodexAdapter @@ -26,8 +27,11 @@ 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.mistral_vibe import MistralVibeAdapter +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 ( + AUGMENT_FIELDS, CLAUDE_FIELDS, CODEX_FIELD_MAPPINGS, CODEX_FIELDS, @@ -36,6 +40,8 @@ GEMINI_FIELDS, KIRO_FIELDS, LMSTUDIO_FIELDS, + MISTRAL_VIBE_FIELDS, + OPENCODE_FIELDS, TYPE_SUPPORTING_HOSTS, VSCODE_FIELDS, ) @@ -55,6 +61,9 @@ "gemini": GEMINI_FIELDS, "kiro": KIRO_FIELDS, "codex": CODEX_FIELDS, + "mistral-vibe": MISTRAL_VIBE_FIELDS, + "opencode": OPENCODE_FIELDS, + "augment": AUGMENT_FIELDS, } # Reverse mappings for Codex (host-native name → universal name) @@ -93,6 +102,9 @@ def get_adapter(self) -> BaseAdapter: "gemini": GeminiAdapter, "kiro": KiroAdapter, "codex": CodexAdapter, + "mistral-vibe": MistralVibeAdapter, + "opencode": OpenCodeAdapter, + "augment": AugmentAdapter, } factory = adapter_map[self.host_name] return factory() @@ -350,6 +362,9 @@ def generate_unsupported_field_test_cases( | GEMINI_FIELDS | KIRO_FIELDS | CODEX_FIELDS + | MISTRAL_VIBE_FIELDS + | OPENCODE_FIELDS + | AUGMENT_FIELDS ) cases: List[FilterTestCase] = [] diff --git a/tests/unit/mcp/test_adapter_protocol.py b/tests/unit/mcp/test_adapter_protocol.py index e733a61..1e6c24e 100644 --- a/tests/unit/mcp/test_adapter_protocol.py +++ b/tests/unit/mcp/test_adapter_protocol.py @@ -9,28 +9,35 @@ from hatch.mcp_host_config.models import MCPServerConfig, MCPHostType from hatch.mcp_host_config.adapters import ( get_adapter, + AugmentAdapter, ClaudeAdapter, CodexAdapter, CursorAdapter, GeminiAdapter, KiroAdapter, LMStudioAdapter, + MistralVibeAdapter, VSCodeAdapter, ) +from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter # All adapter classes to test ALL_ADAPTERS = [ + AugmentAdapter, ClaudeAdapter, CodexAdapter, CursorAdapter, GeminiAdapter, KiroAdapter, LMStudioAdapter, + MistralVibeAdapter, + OpenCodeAdapter, VSCodeAdapter, ] # Map host types to their expected adapter classes HOST_ADAPTER_MAP = { + MCPHostType.AUGMENT: AugmentAdapter, MCPHostType.CLAUDE_DESKTOP: ClaudeAdapter, MCPHostType.CLAUDE_CODE: ClaudeAdapter, MCPHostType.CODEX: CodexAdapter, @@ -38,6 +45,8 @@ MCPHostType.GEMINI: GeminiAdapter, MCPHostType.KIRO: KiroAdapter, MCPHostType.LMSTUDIO: LMStudioAdapter, + MCPHostType.MISTRAL_VIBE: MistralVibeAdapter, + MCPHostType.OPENCODE: OpenCodeAdapter, MCPHostType.VSCODE: VSCodeAdapter, } diff --git a/tests/unit/mcp/test_adapter_registry.py b/tests/unit/mcp/test_adapter_registry.py index 9408835..204f6c7 100644 --- a/tests/unit/mcp/test_adapter_registry.py +++ b/tests/unit/mcp/test_adapter_registry.py @@ -17,6 +17,7 @@ GeminiAdapter, KiroAdapter, LMStudioAdapter, + MistralVibeAdapter, VSCodeAdapter, ) @@ -31,6 +32,7 @@ def setUp(self): def test_AR01_registry_has_all_default_hosts(self): """AR-01: Registry initializes with all default host adapters.""" expected_hosts = { + "augment", "claude-desktop", "claude-code", "codex", @@ -38,6 +40,8 @@ def test_AR01_registry_has_all_default_hosts(self): "gemini", "kiro", "lmstudio", + "mistral-vibe", + "opencode", "vscode", } @@ -55,6 +59,7 @@ def test_AR02_get_adapter_returns_correct_type(self): ("gemini", GeminiAdapter), ("kiro", KiroAdapter), ("lmstudio", LMStudioAdapter), + ("mistral-vibe", MistralVibeAdapter), ("vscode", VSCodeAdapter), ] diff --git a/tests/unit/mcp/test_config_model.py b/tests/unit/mcp/test_config_model.py index 5e60df5..b3fc0d7 100644 --- a/tests/unit/mcp/test_config_model.py +++ b/tests/unit/mcp/test_config_model.py @@ -37,6 +37,18 @@ def test_UM03_valid_http_config_gemini(self): # httpUrl is considered remote self.assertTrue(config.is_remote_server) + def test_UM03b_valid_streamable_http_transport(self): + """UM-03b: Valid remote config with native transport field.""" + config = MCPServerConfig( + name="test", + url="https://example.com/mcp", + transport="streamable-http", + ) + + self.assertEqual(config.transport, "streamable-http") + self.assertEqual(config.get_transport_type(), "streamable-http") + self.assertTrue(config.is_remote_server) + def test_UM04_allows_command_and_url(self): """UM-04: Unified model allows both command and url (adapters validate).""" # The unified model is permissive - adapters enforce host-specific rules diff --git a/tests/unit/mcp/test_mistral_vibe_adapter.py b/tests/unit/mcp/test_mistral_vibe_adapter.py new file mode 100644 index 0000000..08e4b8a --- /dev/null +++ b/tests/unit/mcp/test_mistral_vibe_adapter.py @@ -0,0 +1,41 @@ +"""Unit tests for the Mistral Vibe adapter.""" + +import unittest + +from hatch.mcp_host_config.adapters.mistral_vibe import MistralVibeAdapter +from hatch.mcp_host_config.models import MCPServerConfig + + +class TestMistralVibeAdapter(unittest.TestCase): + """Verify Mistral-specific filtering and transport mapping.""" + + def test_serialize_filters_type_but_preserves_sse_transport_hint(self): + """Canonical type hints should map to transport without serializing type.""" + result = MistralVibeAdapter().serialize( + MCPServerConfig( + name="weather", + url="https://example.com/mcp", + type="sse", + ) + ) + + self.assertEqual(result["url"], "https://example.com/mcp") + self.assertEqual(result["transport"], "streamable-http") + self.assertNotIn("type", result) + + def test_serialize_maps_http_url_without_exposing_httpUrl(self): + """httpUrl input should serialize as Mistral's url+transport format.""" + result = MistralVibeAdapter().serialize( + MCPServerConfig( + name="weather", + httpUrl="https://example.com/http", + ) + ) + + self.assertEqual(result["url"], "https://example.com/http") + self.assertEqual(result["transport"], "http") + self.assertNotIn("httpUrl", result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/mcp/test_mistral_vibe_strategy.py b/tests/unit/mcp/test_mistral_vibe_strategy.py new file mode 100644 index 0000000..7ffdc75 --- /dev/null +++ b/tests/unit/mcp/test_mistral_vibe_strategy.py @@ -0,0 +1,91 @@ +"""Unit tests for Mistral Vibe host strategy.""" + +import os +import tempfile +import tomllib +import unittest +from pathlib import Path + +from hatch.mcp_host_config.models import HostConfiguration, MCPServerConfig +from hatch.mcp_host_config.strategies import MistralVibeHostStrategy + + +class TestMistralVibeHostStrategy(unittest.TestCase): + """Verify Mistral Vibe TOML read/write behavior.""" + + def test_read_configuration_parses_array_of_tables(self): + """Reads [[mcp_servers]] entries into HostConfiguration.""" + with tempfile.TemporaryDirectory() as tmpdir: + cwd = os.getcwd() + try: + os.chdir(tmpdir) + config_dir = Path(tmpdir) / ".vibe" + config_dir.mkdir() + (config_dir / "config.toml").write_text( + 'model = "mistral-medium"\n\n' + "[[mcp_servers]]\n" + 'name = "weather"\n' + 'transport = "streamable-http"\n' + 'url = "https://example.com/mcp"\n' + 'prompt = "Be concise"\n', + encoding="utf-8", + ) + + strategy = MistralVibeHostStrategy() + result = strategy.read_configuration() + + self.assertIn("weather", result.servers) + server = result.servers["weather"] + self.assertEqual(server.transport, "streamable-http") + self.assertEqual(server.type, "http") + self.assertEqual(server.url, "https://example.com/mcp") + self.assertEqual(server.prompt, "Be concise") + finally: + os.chdir(cwd) + + def test_write_configuration_preserves_other_top_level_keys(self): + """Writes mcp_servers while preserving unrelated Vibe settings.""" + with tempfile.TemporaryDirectory() as tmpdir: + cwd = os.getcwd() + try: + os.chdir(tmpdir) + config_dir = Path(tmpdir) / ".vibe" + config_dir.mkdir() + config_path = config_dir / "config.toml" + config_path.write_text( + 'model = "mistral-medium"\n' 'theme = "dark"\n', + encoding="utf-8", + ) + + strategy = MistralVibeHostStrategy() + config = HostConfiguration( + servers={ + "weather": MCPServerConfig( + name="weather", + url="https://example.com/mcp", + transport="streamable-http", + headers={"Authorization": "Bearer token"}, + ) + } + ) + + self.assertTrue(strategy.write_configuration(config, no_backup=True)) + + with open(config_path, "rb") as f: + written = tomllib.load(f) + + self.assertEqual(written["model"], "mistral-medium") + self.assertEqual(written["theme"], "dark") + self.assertEqual(written["mcp_servers"][0]["name"], "weather") + self.assertEqual( + written["mcp_servers"][0]["transport"], "streamable-http" + ) + self.assertEqual( + written["mcp_servers"][0]["url"], "https://example.com/mcp" + ) + finally: + os.chdir(cwd) + + +if __name__ == "__main__": + unittest.main()