Skip to content

Commit 4d9833c

Browse files
feat(adapters): create BaseAdapter abstract class
- Added adapters subpackage with base adapter module - BaseAdapter defines abstract interface: host_name, get_supported_fields, validate, serialize - Added AdapterValidationError for host-specific validation errors - Added filter_fields helper method for common serialization logic - Full docstrings with usage examples
1 parent c4eabd2 commit 4d9833c

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""MCP Host Config Adapters.
2+
3+
This module provides host-specific adapters for the Unified Adapter Architecture.
4+
Each adapter handles validation and serialization for a specific MCP host.
5+
"""
6+
7+
from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
8+
9+
__all__ = ["AdapterValidationError", "BaseAdapter"]
10+
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""Base adapter class for MCP host configurations.
2+
3+
This module defines the abstract BaseAdapter class that all host-specific
4+
adapters must implement. The adapter pattern allows for:
5+
- Host-specific validation rules
6+
- Host-specific serialization format
7+
- Unified interface across all hosts
8+
"""
9+
10+
from abc import ABC, abstractmethod
11+
from typing import Any, Dict, FrozenSet, List, Optional
12+
13+
from hatch.mcp_host_config.models import MCPServerConfig
14+
from hatch.mcp_host_config.fields import EXCLUDED_ALWAYS
15+
16+
17+
class AdapterValidationError(Exception):
18+
"""Raised when adapter validation fails.
19+
20+
Attributes:
21+
message: Human-readable error message
22+
field: The field that caused the error (if applicable)
23+
host_name: The host adapter that raised the error
24+
"""
25+
26+
def __init__(
27+
self,
28+
message: str,
29+
field: Optional[str] = None,
30+
host_name: Optional[str] = None
31+
):
32+
self.message = message
33+
self.field = field
34+
self.host_name = host_name
35+
super().__init__(self._format_message())
36+
37+
def _format_message(self) -> str:
38+
"""Format the error message with optional context."""
39+
parts = []
40+
if self.host_name:
41+
parts.append(f"[{self.host_name}]")
42+
if self.field:
43+
parts.append(f"Field '{self.field}':")
44+
parts.append(self.message)
45+
return " ".join(parts)
46+
47+
48+
class BaseAdapter(ABC):
49+
"""Abstract base class for host-specific MCP configuration adapters.
50+
51+
Each host (Claude Desktop, VSCode, Gemini, etc.) has different requirements
52+
for MCP server configuration. Adapters handle:
53+
54+
1. **Validation**: Host-specific rules (e.g., "command and url are mutually
55+
exclusive" for Claude, but not for Gemini which supports triple transport)
56+
57+
2. **Serialization**: Converting MCPServerConfig to the host's expected format
58+
(field names, structure, excluded fields)
59+
60+
3. **Field Support**: Declaring which fields the host supports
61+
62+
Subclasses must implement:
63+
- host_name: The identifier for this host
64+
- get_supported_fields(): Fields this host accepts
65+
- validate(): Host-specific validation logic
66+
- serialize(): Convert config to host format
67+
68+
Example:
69+
>>> class ClaudeAdapter(BaseAdapter):
70+
... @property
71+
... def host_name(self) -> str:
72+
... return "claude-desktop"
73+
...
74+
... def get_supported_fields(self) -> FrozenSet[str]:
75+
... return frozenset({"command", "args", "env", "url", "headers", "type"})
76+
...
77+
... def validate(self, config: MCPServerConfig) -> None:
78+
... if config.command and config.url:
79+
... raise AdapterValidationError("Cannot have both command and url")
80+
...
81+
... def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
82+
... return {k: v for k, v in config.model_dump().items() if v is not None}
83+
"""
84+
85+
@property
86+
@abstractmethod
87+
def host_name(self) -> str:
88+
"""Return the identifier for this host.
89+
90+
Returns:
91+
Host identifier string (e.g., "claude-desktop", "vscode", "gemini")
92+
"""
93+
...
94+
95+
@abstractmethod
96+
def get_supported_fields(self) -> FrozenSet[str]:
97+
"""Return the set of fields supported by this host.
98+
99+
Returns:
100+
FrozenSet of field names that this host accepts.
101+
Fields not in this set will be filtered during serialization.
102+
"""
103+
...
104+
105+
@abstractmethod
106+
def validate(self, config: MCPServerConfig) -> None:
107+
"""Validate the configuration for this host.
108+
109+
This method should check host-specific rules and raise
110+
AdapterValidationError if the configuration is invalid.
111+
112+
Args:
113+
config: The MCPServerConfig to validate
114+
115+
Raises:
116+
AdapterValidationError: If validation fails
117+
"""
118+
...
119+
120+
@abstractmethod
121+
def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
122+
"""Serialize the configuration for this host.
123+
124+
This method should convert the MCPServerConfig to the format
125+
expected by the host's configuration file.
126+
127+
Args:
128+
config: The MCPServerConfig to serialize
129+
130+
Returns:
131+
Dictionary in the host's expected format
132+
"""
133+
...
134+
135+
def get_excluded_fields(self) -> FrozenSet[str]:
136+
"""Return fields that should always be excluded from serialization.
137+
138+
By default, returns EXCLUDED_ALWAYS (e.g., 'name' which is Hatch metadata).
139+
Subclasses can override to add host-specific exclusions.
140+
141+
Returns:
142+
FrozenSet of field names to exclude
143+
"""
144+
return EXCLUDED_ALWAYS
145+
146+
def filter_fields(self, config: MCPServerConfig) -> Dict[str, Any]:
147+
"""Filter config to only include supported, non-excluded, non-None fields.
148+
149+
This is a helper method for serialization that:
150+
1. Gets all fields from the config
151+
2. Filters to only supported fields
152+
3. Removes excluded fields
153+
4. Removes None values
154+
155+
Args:
156+
config: The MCPServerConfig to filter
157+
158+
Returns:
159+
Dictionary with only valid fields for this host
160+
"""
161+
supported = self.get_supported_fields()
162+
excluded = self.get_excluded_fields()
163+
164+
result = {}
165+
for field, value in config.model_dump(exclude_none=True).items():
166+
if field in supported and field not in excluded:
167+
result[field] = value
168+
169+
return result
170+

0 commit comments

Comments
 (0)