Skip to content

Commit f213971

Browse files
feat(mcp-hosts): let hatch manage mistral vibe configs
Add the adapter, strategy, model wiring, and core validations needed to treat Mistral Vibe as a first-class MCP host inside the shared host-configuration layer.
1 parent db0fb91 commit f213971

File tree

14 files changed

+470
-5
lines changed

14 files changed

+470
-5
lines changed

hatch/mcp_host_config/adapters/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from hatch.mcp_host_config.adapters.gemini import GeminiAdapter
1313
from hatch.mcp_host_config.adapters.kiro import KiroAdapter
1414
from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter
15+
from hatch.mcp_host_config.adapters.mistral_vibe import MistralVibeAdapter
1516
from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter
1617
from hatch.mcp_host_config.adapters.registry import (
1718
AdapterRegistry,
@@ -36,6 +37,7 @@
3637
"GeminiAdapter",
3738
"KiroAdapter",
3839
"LMStudioAdapter",
40+
"MistralVibeAdapter",
3941
"OpenCodeAdapter",
4042
"VSCodeAdapter",
4143
]
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Mistral Vibe adapter for MCP host configuration.
2+
3+
Mistral Vibe uses TOML `[[mcp_servers]]` entries with an explicit `transport`
4+
field instead of the Claude-style `type` discriminator.
5+
"""
6+
7+
from typing import Any, Dict, FrozenSet
8+
9+
from hatch.mcp_host_config.adapters.base import AdapterValidationError, BaseAdapter
10+
from hatch.mcp_host_config.fields import MISTRAL_VIBE_FIELDS
11+
from hatch.mcp_host_config.models import MCPServerConfig
12+
13+
14+
class MistralVibeAdapter(BaseAdapter):
15+
"""Adapter for Mistral Vibe MCP server configuration."""
16+
17+
@property
18+
def host_name(self) -> str:
19+
"""Return the host identifier."""
20+
return "mistral-vibe"
21+
22+
def get_supported_fields(self) -> FrozenSet[str]:
23+
"""Return fields supported by Mistral Vibe."""
24+
return MISTRAL_VIBE_FIELDS
25+
26+
def validate(self, config: MCPServerConfig) -> None:
27+
"""Deprecated compatibility wrapper for legacy adapter tests."""
28+
self.validate_filtered(self.filter_fields(config))
29+
30+
def validate_filtered(self, filtered: Dict[str, Any]) -> None:
31+
"""Validate Mistral Vibe transport rules on filtered fields."""
32+
has_command = "command" in filtered
33+
has_url = "url" in filtered
34+
transport_count = sum([has_command, has_url])
35+
36+
if transport_count == 0:
37+
raise AdapterValidationError(
38+
"Either 'command' or 'url' must be specified",
39+
host_name=self.host_name,
40+
)
41+
42+
if transport_count > 1:
43+
raise AdapterValidationError(
44+
"Cannot specify multiple transports - choose exactly one of 'command' or 'url'",
45+
host_name=self.host_name,
46+
)
47+
48+
transport = filtered.get("transport")
49+
if transport == "stdio" and not has_command:
50+
raise AdapterValidationError(
51+
"transport='stdio' requires 'command' field",
52+
field="transport",
53+
host_name=self.host_name,
54+
)
55+
if transport in ("http", "streamable-http") and not has_url:
56+
raise AdapterValidationError(
57+
f"transport='{transport}' requires 'url' field",
58+
field="transport",
59+
host_name=self.host_name,
60+
)
61+
62+
def apply_transformations(
63+
self, filtered: Dict[str, Any], transport_hint: str | None = None
64+
) -> Dict[str, Any]:
65+
"""Apply Mistral Vibe field/value transformations."""
66+
result = dict(filtered)
67+
68+
transport = (
69+
result.get("transport") or transport_hint or self._infer_transport(result)
70+
)
71+
result["transport"] = transport
72+
73+
return result
74+
75+
def serialize(self, config: MCPServerConfig) -> Dict[str, Any]:
76+
"""Serialize configuration for Mistral Vibe format."""
77+
filtered = self.filter_fields(config)
78+
79+
# Support cross-host sync hints without advertising these as native fields.
80+
if (
81+
"command" not in filtered
82+
and "url" not in filtered
83+
and config.httpUrl is not None
84+
):
85+
filtered["url"] = config.httpUrl
86+
87+
transport_hint = self._infer_transport(filtered, config=config)
88+
if transport_hint is not None:
89+
filtered["transport"] = transport_hint
90+
91+
self.validate_filtered(filtered)
92+
return self.apply_transformations(filtered)
93+
94+
def _infer_transport(
95+
self, filtered: Dict[str, Any], config: MCPServerConfig | None = None
96+
) -> str | None:
97+
"""Infer Vibe transport from canonical MCP fields."""
98+
if "transport" in filtered:
99+
return filtered["transport"]
100+
if "command" in filtered:
101+
return "stdio"
102+
103+
config_type = config.type if config is not None else None
104+
if config_type == "stdio":
105+
return "stdio"
106+
if config_type == "http":
107+
return "http"
108+
if config_type == "sse":
109+
return "streamable-http"
110+
111+
if config is not None and config.httpUrl is not None:
112+
return "http"
113+
if "url" in filtered:
114+
return "streamable-http"
115+
116+
return None

hatch/mcp_host_config/adapters/registry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from hatch.mcp_host_config.adapters.gemini import GeminiAdapter
1515
from hatch.mcp_host_config.adapters.kiro import KiroAdapter
1616
from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter
17+
from hatch.mcp_host_config.adapters.mistral_vibe import MistralVibeAdapter
1718
from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter
1819
from hatch.mcp_host_config.adapters.vscode import VSCodeAdapter
1920

@@ -34,7 +35,7 @@ class AdapterRegistry:
3435
'claude-desktop'
3536
3637
>>> registry.get_supported_hosts()
37-
['augment', 'claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'kiro', 'lmstudio', 'opencode', 'vscode']
38+
['augment', 'claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'kiro', 'lmstudio', 'mistral-vibe', 'opencode', 'vscode']
3839
"""
3940

4041
def __init__(self):
@@ -55,6 +56,7 @@ def _register_defaults(self) -> None:
5556
self.register(GeminiAdapter())
5657
self.register(KiroAdapter())
5758
self.register(CodexAdapter())
59+
self.register(MistralVibeAdapter())
5860
self.register(OpenCodeAdapter())
5961
self.register(AugmentAdapter())
6062

hatch/mcp_host_config/backup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def validate_hostname(cls, v):
4848
"gemini",
4949
"kiro",
5050
"codex",
51+
"mistral-vibe",
5152
"opencode",
5253
"augment",
5354
}

hatch/mcp_host_config/fields.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
# ============================================================================
2828

2929
# Hosts that support the 'type' discriminator field (stdio/sse/http)
30-
# Note: Gemini, Kiro, Codex do NOT support this field
30+
# Note: Gemini, Kiro, Codex, and Mistral Vibe do NOT support this field
3131
TYPE_SUPPORTING_HOSTS: FrozenSet[str] = frozenset(
3232
{
3333
"claude-desktop",
@@ -117,6 +117,21 @@
117117
)
118118

119119

120+
# Fields supported by Mistral Vibe (TOML array-of-tables with explicit transport)
121+
MISTRAL_VIBE_FIELDS: FrozenSet[str] = UNIVERSAL_FIELDS | frozenset(
122+
{
123+
"transport", # Vibe transport discriminator: stdio/http/streamable-http
124+
"prompt", # Optional per-server prompt override
125+
"sampling_enabled", # Enable model sampling for tool calls
126+
"api_key_env", # Env var containing API key for remote servers
127+
"api_key_header", # Header name for API key injection
128+
"api_key_format", # Header formatting template for API key injection
129+
"startup_timeout_sec", # Server startup timeout
130+
"tool_timeout_sec", # Tool execution timeout
131+
}
132+
)
133+
134+
120135
# Fields supported by Augment Code (auggie CLI + extensions); same as Claude fields
121136
# Config: ~/.augment/settings.json, key: mcpServers
122137
AUGMENT_FIELDS: FrozenSet[str] = CLAUDE_FIELDS

hatch/mcp_host_config/host_management.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class MCPHostRegistry:
3030
_family_mappings: Dict[str, List[MCPHostType]] = {
3131
"claude": [MCPHostType.CLAUDE_DESKTOP, MCPHostType.CLAUDE_CODE],
3232
"cursor": [MCPHostType.CURSOR, MCPHostType.LMSTUDIO],
33+
"mistral": [MCPHostType.MISTRAL_VIBE],
3334
}
3435

3536
@classmethod

hatch/mcp_host_config/models.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class MCPHostType(str, Enum):
3030
GEMINI = "gemini"
3131
KIRO = "kiro"
3232
CODEX = "codex"
33+
MISTRAL_VIBE = "mistral-vibe"
3334
OPENCODE = "opencode"
3435
AUGMENT = "augment"
3536

@@ -62,6 +63,10 @@ class MCPServerConfig(BaseModel):
6263
type: Optional[Literal["stdio", "sse", "http"]] = Field(
6364
None, description="Transport type (stdio for local, sse/http for remote)"
6465
)
66+
transport: Optional[Literal["stdio", "http", "streamable-http"]] = Field(
67+
None,
68+
description="Host-native transport discriminator (e.g. Mistral Vibe)",
69+
)
6570

6671
# stdio transport (local server)
6772
command: Optional[str] = Field(
@@ -138,15 +143,15 @@ class MCPServerConfig(BaseModel):
138143
disabledTools: Optional[List[str]] = Field(None, description="Disabled tool names")
139144

140145
# ========================================================================
141-
# Codex-Specific Fields
146+
# Codex / Mistral Vibe-Specific Fields
142147
# ========================================================================
143148
env_vars: Optional[List[str]] = Field(
144149
None, description="Environment variables to whitelist/forward"
145150
)
146-
startup_timeout_sec: Optional[int] = Field(
151+
startup_timeout_sec: Optional[float] = Field(
147152
None, description="Server startup timeout in seconds"
148153
)
149-
tool_timeout_sec: Optional[int] = Field(
154+
tool_timeout_sec: Optional[float] = Field(
150155
None, description="Tool execution timeout in seconds"
151156
)
152157
enabled: Optional[bool] = Field(
@@ -167,6 +172,19 @@ class MCPServerConfig(BaseModel):
167172
env_http_headers: Optional[Dict[str, str]] = Field(
168173
None, description="Header names to env var names"
169174
)
175+
prompt: Optional[str] = Field(None, description="Per-server prompt override")
176+
sampling_enabled: Optional[bool] = Field(
177+
None, description="Whether sampling is enabled for tool calls"
178+
)
179+
api_key_env: Optional[str] = Field(
180+
None, description="Env var containing API key for remote server auth"
181+
)
182+
api_key_header: Optional[str] = Field(
183+
None, description="HTTP header name used for API key injection"
184+
)
185+
api_key_format: Optional[str] = Field(
186+
None, description="Formatting template for API key header values"
187+
)
170188

171189
# ========================================================================
172190
# OpenCode-Specific Fields
@@ -239,6 +257,8 @@ def is_stdio(self) -> bool:
239257
1. Explicit type="stdio" field takes precedence
240258
2. Otherwise, presence of 'command' field indicates stdio
241259
"""
260+
if self.transport is not None:
261+
return self.transport == "stdio"
242262
if self.type is not None:
243263
return self.type == "stdio"
244264
return self.command is not None
@@ -253,6 +273,8 @@ def is_sse(self) -> bool:
253273
1. Explicit type="sse" field takes precedence
254274
2. Otherwise, presence of 'url' field indicates SSE
255275
"""
276+
if self.transport is not None:
277+
return False
256278
if self.type is not None:
257279
return self.type == "sse"
258280
return self.url is not None
@@ -267,6 +289,8 @@ def is_http(self) -> bool:
267289
1. Explicit type="http" field takes precedence
268290
2. Otherwise, presence of 'httpUrl' field indicates HTTP streaming
269291
"""
292+
if self.transport is not None:
293+
return self.transport in ("http", "streamable-http")
270294
if self.type is not None:
271295
return self.type == "http"
272296
return self.httpUrl is not None
@@ -278,8 +302,12 @@ def get_transport_type(self) -> Optional[str]:
278302
"stdio" for command-based local servers
279303
"sse" for URL-based remote servers (SSE transport)
280304
"http" for httpUrl-based remote servers (Gemini HTTP streaming)
305+
"streamable-http" for hosts that expose that transport natively
281306
None if transport cannot be determined
282307
"""
308+
if self.transport is not None:
309+
return self.transport
310+
283311
# Explicit type takes precedence
284312
if self.type is not None:
285313
return self.type
@@ -367,6 +395,7 @@ def validate_host_names(cls, v):
367395
"gemini",
368396
"kiro",
369397
"codex",
398+
"mistral-vibe",
370399
"opencode",
371400
"augment",
372401
}

hatch/mcp_host_config/reporting.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def _get_adapter_host_name(host_type: MCPHostType) -> str:
7373
MCPHostType.GEMINI: "gemini",
7474
MCPHostType.KIRO: "kiro",
7575
MCPHostType.CODEX: "codex",
76+
MCPHostType.MISTRAL_VIBE: "mistral-vibe",
7677
MCPHostType.OPENCODE: "opencode",
7778
MCPHostType.AUGMENT: "augment",
7879
}

0 commit comments

Comments
 (0)