Extending MCP Host Configuration¶
Quick Start: Create an adapter (validation + serialization), create a strategy (file I/O), add tests. Most implementations are 50-100 lines per file.
Before You Start: Integration Checklist¶
The Unified Adapter Architecture requires only 4 integration points:
| Integration Point | Required? | Files to Modify |
|---|---|---|
| ☐ Host type enum | Always | models.py |
| ☐ Adapter class | Always | adapters/your_host.py, adapters/__init__.py |
| ☐ Strategy class | Always | strategies.py |
| ☐ 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.
When You Need This¶
You want Hatch to configure MCP servers on a new host platform:
- A code editor not yet supported (Zed, Neovim, etc.)
- A custom MCP host implementation
- Cloud-based development environments
- Specialized MCP server platforms
The Pattern: Adapter + Strategy¶
The Unified Adapter Architecture separates concerns:
| Component | Responsibility | Interface |
|---|---|---|
| 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 implementvalidate_filtered()for the validate-after-filter pattern. See Architecture Doc for details.
MCPServerConfig (unified model)
│
▼
┌──────────────┐
│ Adapter │ ← Validates fields, serializes to host format
└──────────────┘
│
▼
┌──────────────┐
│ Strategy │ ← Reads/writes configuration files
└──────────────┘
│
▼
config.json
Implementation Steps¶
Step 1: Add Host Type Enum¶
Add your host to MCPHostType in hatch/mcp_host_config/models.py:
class MCPHostType(str, Enum):
# ... existing types ...
YOUR_HOST = "your-host" # Use lowercase with hyphens
Step 2: Create Host Adapter¶
Create hatch/mcp_host_config/adapters/your_host.py:
"""Your Host adapter for MCP host configuration."""
from typing import Any, Dict, FrozenSet
from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
from hatch.mcp_host_config.fields import UNIVERSAL_FIELDS
from hatch.mcp_host_config.models import MCPServerConfig
class YourHostAdapter(BaseAdapter):
"""Adapter for Your Host."""
@property
def host_name(self) -> str:
return "your-host"
def get_supported_fields(self) -> FrozenSet[str]:
"""Return fields Your Host accepts."""
# Start with universal fields, add host-specific ones
return UNIVERSAL_FIELDS | frozenset({
"type", # If your host supports transport type
# "your_specific_field",
})
def validate(self, config: MCPServerConfig) -> None:
"""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,
)
# Add any host-specific validation
# 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.
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:
from hatch.mcp_host_config.adapters.your_host import YourHostAdapter
__all__ = [
# ... existing exports ...
"YourHostAdapter",
]
And add to registry in hatch/mcp_host_config/adapters/registry.py:
from hatch.mcp_host_config.adapters.your_host import YourHostAdapter
def _register_defaults(self) -> None:
# ... existing registrations ...
self.register(YourHostAdapter())
Step 3: Create Host Strategy¶
Add to hatch/mcp_host_config/strategies.py:
@register_host_strategy(MCPHostType.YOUR_HOST)
class YourHostStrategy(MCPHostStrategy):
"""Strategy for Your Host file I/O."""
def get_config_path(self) -> Optional[Path]:
"""Return path to config file."""
return Path.home() / ".your_host" / "config.json"
def is_host_available(self) -> bool:
"""Check if host is installed."""
config_path = self.get_config_path()
return config_path is not None and config_path.parent.exists()
def get_config_key(self) -> str:
"""Return the key containing MCP servers."""
return "mcpServers" # Most hosts use this
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 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:
# 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:
{
"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:
{
"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:
c) Add reverse mappings if needed¶
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:
# 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¶
Import from hatch/mcp_host_config/fields.py:
from hatch.mcp_host_config.fields import (
UNIVERSAL_FIELDS, # command, args, env, url, headers
CLAUDE_FIELDS, # UNIVERSAL + type
VSCODE_FIELDS, # CLAUDE + envFile, inputs
CURSOR_FIELDS, # CLAUDE + envFile
)
# Compose your host's fields
YOUR_HOST_FIELDS = UNIVERSAL_FIELDS | frozenset({
"type",
"your_specific_field",
})
Adding New Host-Specific Fields¶
If your host has unique fields not in the unified model:
- Add to
MCPServerConfiginmodels.py:
# Host-specific fields
your_field: Optional[str] = Field(None, description="Your Host specific field")
- Add to field constants in
fields.py:
- Add CLI argument (optional) in
hatch/cli/__main__.py:
Field Mappings (Optional)¶
If your host uses different names for standard fields, override apply_transformations():
# In your adapter
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:
Common Patterns¶
Multiple Transport Support¶
Some hosts (like Gemini) support multiple transports:
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")
# Gemini requires exactly one transport (not multiple)
if transport_count > 1:
raise AdapterValidationError(
"Only one transport allowed: command, url, or httpUrl"
)
Strict Single Transport¶
Some hosts (like Claude) require exactly one transport:
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")
if has_command and has_url:
raise AdapterValidationError("Cannot have both command and url")
Custom Serialization¶
Override serialize() for custom output format:
def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
filtered = self.filter_fields(config)
self.validate_filtered(filtered)
# Transform to your host's expected structure
if "command" in filtered:
filtered["transport"] = {"type": "stdio", "command": filtered.pop("command")}
return filtered
Testing Your Implementation¶
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:
- Fixture entry in
tests/test_data/mcp_adapters/canonical_configs.json - Field set mapping in
tests/test_data/mcp_adapters/host_registry.py(FIELD_SETSdict) - 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.
Cross-reference: See the Architecture Doc -- Testing Strategy for the full testing infrastructure design, including the three test tiers (unit, integration, regression).
Test File Location¶
tests/
├── unit/mcp/
│ ├── 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¶
Common Issues¶
| Issue | Cause | Solution |
|---|---|---|
| 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_filtered() |
Check conditions |
| Name appears in output | Not filtering excluded fields | Use filter_fields() |
Debugging Tips¶
# Print what adapter sees
adapter = get_adapter("your-host")
print(f"Supported fields: {adapter.get_supported_fields()}")
config = MCPServerConfig(name="test", command="python")
print(f"Filtered: {adapter.filter_fields(config)}")
print(f"Serialized: {adapter.serialize(config)}")
Reference: Existing Adapters¶
Study these for patterns:
| Adapter | Notable Features |
|---|---|
ClaudeAdapter |
Variant support (desktop/code), strict transport validation |
VSCodeAdapter |
Extended fields (envFile, inputs) |
GeminiAdapter |
Multiple transport support, many host-specific fields |
KiroAdapter |
Disabled/autoApprove fields |
CodexAdapter |
Field mappings (args→arguments) |
Summary¶
Adding a new host is now a 4-step process:
- Add enum to
MCPHostType - Create adapter with
validate_filtered()+serialize()+get_supported_fields() - Create strategy with
get_config_path()+ file I/O methods - Register test fixtures in
canonical_configs.jsonandhost_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.