From f213971f78f6e55786d87f4a30d3b314af18bd17 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Wed, 11 Mar 2026 10:53:11 +0900 Subject: [PATCH 01/11] 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. --- hatch/mcp_host_config/adapters/__init__.py | 2 + .../mcp_host_config/adapters/mistral_vibe.py | 116 ++++++++++++++ hatch/mcp_host_config/adapters/registry.py | 4 +- hatch/mcp_host_config/backup.py | 1 + hatch/mcp_host_config/fields.py | 17 +- hatch/mcp_host_config/host_management.py | 1 + hatch/mcp_host_config/models.py | 35 ++++- hatch/mcp_host_config/reporting.py | 1 + hatch/mcp_host_config/strategies.py | 148 ++++++++++++++++++ tests/unit/mcp/test_adapter_protocol.py | 3 + tests/unit/mcp/test_adapter_registry.py | 3 + tests/unit/mcp/test_config_model.py | 12 ++ tests/unit/mcp/test_mistral_vibe_adapter.py | 41 +++++ tests/unit/mcp/test_mistral_vibe_strategy.py | 91 +++++++++++ 14 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 hatch/mcp_host_config/adapters/mistral_vibe.py create mode 100644 tests/unit/mcp/test_mistral_vibe_adapter.py create mode 100644 tests/unit/mcp/test_mistral_vibe_strategy.py diff --git a/hatch/mcp_host_config/adapters/__init__.py b/hatch/mcp_host_config/adapters/__init__.py index 8e5dc32..ec750fd 100644 --- a/hatch/mcp_host_config/adapters/__init__.py +++ b/hatch/mcp_host_config/adapters/__init__.py @@ -12,6 +12,7 @@ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter from hatch.mcp_host_config.adapters.kiro import KiroAdapter from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter +from hatch.mcp_host_config.adapters.mistral_vibe import MistralVibeAdapter from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter from hatch.mcp_host_config.adapters.registry import ( AdapterRegistry, @@ -36,6 +37,7 @@ "GeminiAdapter", "KiroAdapter", "LMStudioAdapter", + "MistralVibeAdapter", "OpenCodeAdapter", "VSCodeAdapter", ] 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/registry.py b/hatch/mcp_host_config/adapters/registry.py index 22a0122..53ae661 100644 --- a/hatch/mcp_host_config/adapters/registry.py +++ b/hatch/mcp_host_config/adapters/registry.py @@ -14,6 +14,7 @@ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter from hatch.mcp_host_config.adapters.kiro import KiroAdapter from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter +from hatch.mcp_host_config.adapters.mistral_vibe import MistralVibeAdapter from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter from hatch.mcp_host_config.adapters.vscode import VSCodeAdapter @@ -34,7 +35,7 @@ class AdapterRegistry: 'claude-desktop' >>> registry.get_supported_hosts() - ['augment', 'claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'kiro', 'lmstudio', 'opencode', 'vscode'] + ['augment', 'claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'kiro', 'lmstudio', 'mistral-vibe', 'opencode', 'vscode'] """ def __init__(self): @@ -55,6 +56,7 @@ def _register_defaults(self) -> None: self.register(GeminiAdapter()) self.register(KiroAdapter()) self.register(CodexAdapter()) + self.register(MistralVibeAdapter()) self.register(OpenCodeAdapter()) self.register(AugmentAdapter()) diff --git a/hatch/mcp_host_config/backup.py b/hatch/mcp_host_config/backup.py index 5ebac87..9c36ec8 100644 --- a/hatch/mcp_host_config/backup.py +++ b/hatch/mcp_host_config/backup.py @@ -48,6 +48,7 @@ def validate_hostname(cls, v): "gemini", "kiro", "codex", + "mistral-vibe", "opencode", "augment", } diff --git a/hatch/mcp_host_config/fields.py b/hatch/mcp_host_config/fields.py index b574c9c..942fef9 100644 --- a/hatch/mcp_host_config/fields.py +++ b/hatch/mcp_host_config/fields.py @@ -27,7 +27,7 @@ # ============================================================================ # 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", @@ -117,6 +117,21 @@ ) +# 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 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 610971b..79b4116 100644 --- a/hatch/mcp_host_config/models.py +++ b/hatch/mcp_host_config/models.py @@ -30,6 +30,7 @@ class MCPHostType(str, Enum): GEMINI = "gemini" KIRO = "kiro" CODEX = "codex" + MISTRAL_VIBE = "mistral-vibe" OPENCODE = "opencode" AUGMENT = "augment" @@ -62,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( @@ -138,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( @@ -167,6 +172,19 @@ 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 @@ -239,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 @@ -253,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 @@ -267,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 @@ -278,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 @@ -367,6 +395,7 @@ def validate_host_names(cls, v): "gemini", "kiro", "codex", + "mistral-vibe", "opencode", "augment", } diff --git a/hatch/mcp_host_config/reporting.py b/hatch/mcp_host_config/reporting.py index d4744aa..fd9724d 100644 --- a/hatch/mcp_host_config/reporting.py +++ b/hatch/mcp_host_config/reporting.py @@ -73,6 +73,7 @@ def _get_adapter_host_name(host_type: MCPHostType) -> str: MCPHostType.GEMINI: "gemini", MCPHostType.KIRO: "kiro", MCPHostType.CODEX: "codex", + MCPHostType.MISTRAL_VIBE: "mistral-vibe", MCPHostType.OPENCODE: "opencode", MCPHostType.AUGMENT: "augment", } diff --git a/hatch/mcp_host_config/strategies.py b/hatch/mcp_host_config/strategies.py index 69ebcd2..a6c8925 100644 --- a/hatch/mcp_host_config/strategies.py +++ b/hatch/mcp_host_config/strategies.py @@ -1055,6 +1055,154 @@ def write_configuration( 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). diff --git a/tests/unit/mcp/test_adapter_protocol.py b/tests/unit/mcp/test_adapter_protocol.py index 4878878..1e6c24e 100644 --- a/tests/unit/mcp/test_adapter_protocol.py +++ b/tests/unit/mcp/test_adapter_protocol.py @@ -16,6 +16,7 @@ GeminiAdapter, KiroAdapter, LMStudioAdapter, + MistralVibeAdapter, VSCodeAdapter, ) from hatch.mcp_host_config.adapters.opencode import OpenCodeAdapter @@ -29,6 +30,7 @@ GeminiAdapter, KiroAdapter, LMStudioAdapter, + MistralVibeAdapter, OpenCodeAdapter, VSCodeAdapter, ] @@ -43,6 +45,7 @@ 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 a393461..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, ) @@ -39,6 +40,7 @@ def test_AR01_registry_has_all_default_hosts(self): "gemini", "kiro", "lmstudio", + "mistral-vibe", "opencode", "vscode", } @@ -57,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() From 0e801d06d092787e997b8717c8b5bccf48572faa Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Wed, 11 Mar 2026 10:53:20 +0900 Subject: [PATCH 02/11] feat(cli): let shared mcp configure target mistral vibe Reuse the existing MCP configure flags where possible and map them onto Mistral Vibe's native transport and auth fields so the host works through the same CLI workflow as the other integrations. --- hatch/cli/__main__.py | 35 ++- hatch/cli/cli_mcp.py | 106 +++++++- .../cli/test_cli_reporter_integration.py | 236 +++++++++++------- 3 files changed, 281 insertions(+), 96 deletions(-) diff --git a/hatch/cli/__main__.py b/hatch/cli/__main__.py index 18aa11b..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( diff --git a/hatch/cli/cli_mcp.py b/hatch/cli/cli_mcp.py index 457e2f4..4563fae 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 ) @@ -1604,7 +1676,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 +1708,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 +1731,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 +1756,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 +1766,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/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.""" From 1a81ae04dcfe8c67401d1dd5d68055bc16f09896 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Wed, 11 Mar 2026 10:53:30 +0900 Subject: [PATCH 03/11] test(mcp-hosts): keep mistral vibe in shared adapter coverage Extend the canonical fixture registry and generated filtering matrix so future MCP host changes exercise Mistral Vibe through the same shared coverage paths as the other supported hosts. --- tests/regression/mcp/test_field_filtering_v2.py | 6 ++++++ .../test_data/mcp_adapters/canonical_configs.json | 15 +++++++++++++++ tests/test_data/mcp_adapters/host_registry.py | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/tests/regression/mcp/test_field_filtering_v2.py b/tests/regression/mcp/test_field_filtering_v2.py index c511532..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"], diff --git a/tests/test_data/mcp_adapters/canonical_configs.json b/tests/test_data/mcp_adapters/canonical_configs.json index 63dde7f..1d53c30 100644 --- a/tests/test_data/mcp_adapters/canonical_configs.json +++ b/tests/test_data/mcp_adapters/canonical_configs.json @@ -75,6 +75,21 @@ "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"], diff --git a/tests/test_data/mcp_adapters/host_registry.py b/tests/test_data/mcp_adapters/host_registry.py index 502e2ab..e8fb5bb 100644 --- a/tests/test_data/mcp_adapters/host_registry.py +++ b/tests/test_data/mcp_adapters/host_registry.py @@ -27,6 +27,7 @@ from hatch.mcp_host_config.adapters.gemini import GeminiAdapter from hatch.mcp_host_config.adapters.kiro import KiroAdapter from hatch.mcp_host_config.adapters.lmstudio import LMStudioAdapter +from hatch.mcp_host_config.adapters.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 ( @@ -39,6 +40,7 @@ GEMINI_FIELDS, KIRO_FIELDS, LMSTUDIO_FIELDS, + MISTRAL_VIBE_FIELDS, OPENCODE_FIELDS, TYPE_SUPPORTING_HOSTS, VSCODE_FIELDS, @@ -59,6 +61,7 @@ "gemini": GEMINI_FIELDS, "kiro": KIRO_FIELDS, "codex": CODEX_FIELDS, + "mistral-vibe": MISTRAL_VIBE_FIELDS, "opencode": OPENCODE_FIELDS, "augment": AUGMENT_FIELDS, } @@ -99,6 +102,7 @@ def get_adapter(self) -> BaseAdapter: "gemini": GeminiAdapter, "kiro": KiroAdapter, "codex": CodexAdapter, + "mistral-vibe": MistralVibeAdapter, "opencode": OpenCodeAdapter, "augment": AugmentAdapter, } @@ -358,6 +362,7 @@ def generate_unsupported_field_test_cases( | GEMINI_FIELDS | KIRO_FIELDS | CODEX_FIELDS + | MISTRAL_VIBE_FIELDS | OPENCODE_FIELDS | AUGMENT_FIELDS ) From 4ff275863cce0adc2999cec06963546421cca7f5 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Wed, 11 Mar 2026 10:53:37 +0900 Subject: [PATCH 04/11] docs(mcp-hosts): capture mistral vibe host analysis Keep the implementation notes for the new host in a dedicated reports commit so the code history stays focused while the discovery context remains available for later review. --- .../mistral_vibe/00-parameter_analysis_v0.md | 55 +++++++++++++++++++ .../01-architecture_analysis_v0.md | 26 +++++++++ __reports__/mistral_vibe/README.md | 12 ++++ 3 files changed, 93 insertions(+) create mode 100644 __reports__/mistral_vibe/00-parameter_analysis_v0.md create mode 100644 __reports__/mistral_vibe/01-architecture_analysis_v0.md create mode 100644 __reports__/mistral_vibe/README.md 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 From 5130c84a4ddc387e53d140ec56c6c455ca27b55c Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Mon, 16 Mar 2026 22:49:53 +0900 Subject: [PATCH 05/11] docs(mcp-hosts): add mistral vibe to supported platforms Agent-Id: agent-7d172f15-6b95-455e-a27a-bc58509026fe --- README.md | 1 + docs/articles/api/cli/mcp.md | 1 + docs/articles/devs/architecture/mcp_backup_system.md | 1 + docs/articles/devs/architecture/mcp_host_configuration.md | 1 + docs/articles/users/CLIReference.md | 3 +++ docs/articles/users/MCPHostConfiguration.md | 1 + .../04-mcp-host-configuration/01-host-platform-overview.md | 1 + 7 files changed, 9 insertions(+) diff --git a/README.md b/README.md index b6cb5d5..f547137 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Hatch supports deployment to the following MCP host platforms: - **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 +- **Mistral Vibe** — Mistral Vibe IDE with MCP support ## Quick Start diff --git a/docs/articles/api/cli/mcp.md b/docs/articles/api/cli/mcp.md index a0b824f..bd4650e 100644 --- a/docs/articles/api/cli/mcp.md +++ b/docs/articles/api/cli/mcp.md @@ -24,6 +24,7 @@ This module provides handlers for: - codex: OpenAI Codex - lm-studio: LM Studio - gemini: Google Gemini +- mistral-vibe: Mistral Vibe IDE ## Handler Functions diff --git a/docs/articles/devs/architecture/mcp_backup_system.md b/docs/articles/devs/architecture/mcp_backup_system.md index 8aaafc6..46f705d 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 IDE MCP integration | ## Performance Characteristics diff --git a/docs/articles/devs/architecture/mcp_host_configuration.md b/docs/articles/devs/architecture/mcp_host_configuration.md index d8a7977..42de522 100644 --- a/docs/articles/devs/architecture/mcp_host_configuration.md +++ b/docs/articles/devs/architecture/mcp_host_configuration.md @@ -143,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 diff --git a/docs/articles/users/CLIReference.md b/docs/articles/users/CLIReference.md index 7c9c8d3..09ee038 100644 --- a/docs/articles/users/CLIReference.md +++ b/docs/articles/users/CLIReference.md @@ -1017,6 +1017,7 @@ Available MCP Host Platforms: 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**: @@ -1039,6 +1040,8 @@ Available MCP host platforms: Config path: ~/.cursor/config.json vscode: ✗ Not detected Config path: ~/.vscode/config.json + mistral-vibe: ✓ Available + Config path: ~/.config/mistral/mcp.toml ``` #### `hatch mcp discover servers` diff --git a/docs/articles/users/MCPHostConfiguration.md b/docs/articles/users/MCPHostConfiguration.md index 9e2d2bf..8f9f034 100644 --- a/docs/articles/users/MCPHostConfiguration.md +++ b/docs/articles/users/MCPHostConfiguration.md @@ -23,6 +23,7 @@ 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 IDE with MCP support ## 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..3cacb91 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,7 @@ 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 IDE with MCP support ## Configuration Management Workflow From bfa8b9b6ae6769aafa50bb198177323ae70703c6 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Mon, 16 Mar 2026 22:57:52 +0900 Subject: [PATCH 06/11] docs: correct Mistral Vibe terminology to CLI coding agent - Replace 'Mistral Vibe IDE with MCP support' with 'Mistral Vibe CLI coding agent' - Ensure accurate description across all documentation files - Maintain consistency in host identifier (mistral-vibe) --- README.md | 2 +- docs/articles/api/cli/mcp.md | 2 +- docs/articles/devs/architecture/mcp_backup_system.md | 2 +- docs/articles/users/MCPHostConfiguration.md | 2 +- .../04-mcp-host-configuration/01-host-platform-overview.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f547137..9890844 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Hatch supports deployment to the following MCP host platforms: - **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 -- **Mistral Vibe** — Mistral Vibe IDE with MCP support +- **Mistral Vibe** — Mistral Vibe CLI coding agent ## Quick Start diff --git a/docs/articles/api/cli/mcp.md b/docs/articles/api/cli/mcp.md index bd4650e..252c9dd 100644 --- a/docs/articles/api/cli/mcp.md +++ b/docs/articles/api/cli/mcp.md @@ -24,7 +24,7 @@ This module provides handlers for: - codex: OpenAI Codex - lm-studio: LM Studio - gemini: Google Gemini -- mistral-vibe: Mistral Vibe IDE +- mistral-vibe: Mistral Vibe CLI coding agent ## Handler Functions diff --git a/docs/articles/devs/architecture/mcp_backup_system.md b/docs/articles/devs/architecture/mcp_backup_system.md index 46f705d..34f167f 100644 --- a/docs/articles/devs/architecture/mcp_backup_system.md +++ b/docs/articles/devs/architecture/mcp_backup_system.md @@ -146,7 +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 IDE MCP integration | +| `mistral-vibe` | Mistral Vibe CLI coding agent | ## Performance Characteristics diff --git a/docs/articles/users/MCPHostConfiguration.md b/docs/articles/users/MCPHostConfiguration.md index 8f9f034..47adab5 100644 --- a/docs/articles/users/MCPHostConfiguration.md +++ b/docs/articles/users/MCPHostConfiguration.md @@ -23,7 +23,7 @@ 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 IDE with MCP support +- **Mistral Vibe** - Mistral Vibe CLI coding agent ## 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 3cacb91..ec98fe2 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,7 +57,7 @@ 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 IDE with MCP support +- [**Mistral Vibe**](https://mistral.ai/vibe) - Mistral Vibe CLI coding agent ## Configuration Management Workflow From 62f99cf4955c23983efea25c4ea8fe15ad0dc0c4 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Sat, 14 Mar 2026 00:13:00 +0900 Subject: [PATCH 07/11] fix(mcp-hosts): add explicit HTTP transport type for Claude URL-based co Agent-Id: agent-4b2c06a4-05c6-47fd-84e7-4c704eb9fa05 Linked-Note-Id: 3d4ca15e-5b56-430d-9ea4-f5b77e4207aa --- hatch/mcp_host_config/adapters/claude.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hatch/mcp_host_config/adapters/claude.py b/hatch/mcp_host_config/adapters/claude.py index 9761080..1606330 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 when callers omit the type field. + if "url" in filtered and "type" not in filtered: + filtered = filtered.copy() + filtered["type"] = "http" + + # Return filtered Claude config return filtered From b5c7191a18fca67dcccef73d6fc1d15074ea4886 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Sat, 14 Mar 2026 00:17:29 +0900 Subject: [PATCH 08/11] test(mcp): fix whitespace in Claude transport serialization test Agent-Id: agent-4b2c06a4-05c6-47fd-84e7-4c704eb9fa05 Linked-Note-Id: 3d4ca15e-5b56-430d-9ea4-f5b77e4207aa --- .../test_claude_transport_serialization.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/regression/mcp/test_claude_transport_serialization.py 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..d37904a --- /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: tc["host"], + ) + @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: tc["host"], + ) + @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"] From 904f22be42eb567e437f99513cdd25eb696b30cd Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Sat, 14 Mar 2026 00:18:09 +0900 Subject: [PATCH 09/11] fix(mcp-hosts): always set HTTP transport type for Claude URL-based conf Agent-Id: agent-3a5c1a45-c9cd-44cf-8caf-a2ba36ec4cd7 Linked-Note-Id: de87f08e-40a3-4c70-8f99-833526ed8cd5 --- hatch/mcp_host_config/adapters/claude.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hatch/mcp_host_config/adapters/claude.py b/hatch/mcp_host_config/adapters/claude.py index 1606330..a17daa5 100644 --- a/hatch/mcp_host_config/adapters/claude.py +++ b/hatch/mcp_host_config/adapters/claude.py @@ -162,8 +162,8 @@ def serialize(self, config: MCPServerConfig) -> Dict[str, Any]: self.validate_filtered(filtered) # Claude's URL-based remote configs should explicitly declare HTTP - # transport when callers omit the type field. - if "url" in filtered and "type" not in filtered: + # transport in serialized output. + if "url" in filtered: filtered = filtered.copy() filtered["type"] = "http" From d1cc2b0cb6b624e6305190e40ffa429d04806335 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Sat, 21 Mar 2026 23:54:39 +0900 Subject: [PATCH 10/11] test(mcp): add fixture for claude remote setup --- .../test_claude_transport_serialization.py | 4 +- .../claude_transport_regressions.json | 118 ++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 tests/test_data/mcp_adapters/claude_transport_regressions.json diff --git a/tests/regression/mcp/test_claude_transport_serialization.py b/tests/regression/mcp/test_claude_transport_serialization.py index d37904a..ffae1de 100644 --- a/tests/regression/mcp/test_claude_transport_serialization.py +++ b/tests/regression/mcp/test_claude_transport_serialization.py @@ -38,7 +38,7 @@ class TestClaudeTransportSerialization: @pytest.mark.parametrize( "test_case", FIXTURES["remote_http"], - ids=lambda tc: tc["host"], + ids=lambda tc: f'{tc["host"]}-{tc["case"]}', ) @regression_test def test_remote_url_defaults_to_http_type(self, test_case): @@ -53,7 +53,7 @@ def test_remote_url_defaults_to_http_type(self, test_case): @pytest.mark.parametrize( "test_case", FIXTURES["stdio_without_type"], - ids=lambda tc: tc["host"], + ids=lambda tc: f'{tc["host"]}-{tc["case"]}', ) @regression_test def test_stdio_config_does_not_require_type_input(self, test_case): 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" + } + } + } + ] +} From d6a75a8518fc26e138573bec79ed7311aff47ac7 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Sun, 22 Mar 2026 01:03:27 +0900 Subject: [PATCH 11/11] fix(mcp-hosts): remove redundant Claude Desktop/Code URL validation Agent-Id: agent-ab465f6f-4347-4a55-a0d3-064e4d929c77 --- hatch/cli/cli_mcp.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/hatch/cli/cli_mcp.py b/hatch/cli/cli_mcp.py index 4563fae..a5c1fc3 100644 --- a/hatch/cli/cli_mcp.py +++ b/hatch/cli/cli_mcp.py @@ -1586,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(