diff --git a/AGENTS.md b/AGENTS.md index c903775..99e23f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,8 +43,8 @@ `LECODE_CONFIG_DIR`, `LECODE_SKILLS_DIR`, and project cwd under `tmp_path`. The shared `tool_ctx` fixture is not autouse and isolates only the config directory. - Reuse `tests/fakes.py` for providers/catalogs. MCP tests launch mock subprocesses - and localhost servers; disable auto servers as in `tests/test_mcp.py:mcp_config` - to avoid ambient credentials activating Exa. Keep the SSE shutdown reset in + and localhost servers, configured explicitly via `tests/test_mcp.py:mcp_config`. + Keep the SSE shutdown reset in `tests/conftest.py`; it prevents order-dependent failures across server tests. - `docs/build-plan.md` is historical, not the current spec: permissions now have two modes, config is TOML-only, and the catalog is live with an empty fallback. diff --git a/README.md b/README.md index 09d251a..2247254 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ just your terminal, an OpenAI-compatible model, and a sharp set of tools. │ – hooks none configured │ │ – pierre off │ │ ✓ lsp enabled │ -│ ! mcp exa (no EXA_API_KEY) │ +│ – mcp no servers │ ╰─────────────────── ~/github.com/you/your-project ─────────────────────╯ ``` @@ -170,8 +170,7 @@ non-tty `--setup`) · `3` max turns / max loop iterations / context overflow. `[notifications]`. - **MCP** — stdio, streamable-HTTP, and SSE servers, with optional OAuth 2.1 (`auth = "oauth"`, browser flow, tokens under `/mcp-auth/`; - `/mcp auth` to authorize, `/mcp login|logout` to manage). Exa web search is - preconfigured (needs `EXA_API_KEY`); context7 is one flag away. + `/mcp auth` to authorize, `/mcp login|logout` to manage). - **LSP** — diagnostics from real language servers appended to `write`/`edit` results; fail-open, never blocks. - **Worktrees** — `--worktree ` or `/worktree` for isolated branches, @@ -219,7 +218,7 @@ Full reference: [docs/configuration.md](docs/configuration.md). - [docs/hooks.md](docs/hooks.md) — hook events, envelope, verdict protocol - [docs/memory.md](docs/memory.md) — the memory store - [docs/agents-and-skills.md](docs/agents-and-skills.md) — custom agents and skills -- [docs/mcp.md](docs/mcp.md) — MCP servers, Exa, context7 +- [docs/mcp.md](docs/mcp.md) — MCP servers - [docs/build-plan.md](docs/build-plan.md) — the full product definition ## Development diff --git a/docs/configuration.md b/docs/configuration.md index 3209c74..24b8bb7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -200,11 +200,6 @@ Payload: `{"event", "session", "ts", …}`; failures are dropped silently. ## `[mcp]` -| field | default | meaning | -|---|---|---| -| `enable_exa` | `true` | auto-configure Exa web search when `EXA_API_KEY` is set | -| `enable_context7` | `false` | auto-configure the context7 docs server | - `[mcp.servers.]` entries: | field | default | meaning | diff --git a/docs/mcp.md b/docs/mcp.md index ecadb78..7c4e4cf 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6,16 +6,19 @@ legacy HTTP transport). Their tools show up as first-class lecode tools named `mcp::` and go through the permission system like everything else. -## Auto-configured servers +There are no built-in servers; everything comes from `[mcp.servers]`. To add +a hosted server like Exa or context7, configure it yourself: -| server | when | transport | -|---|---|---| -| **Exa** (web search) | on by default; needs `EXA_API_KEY` in the environment | http `https://mcp.exa.ai/mcp` | -| **context7** (docs lookup) | off by default; set `enable_context7 = true` | http `https://mcp.context7.com/mcp` | +```toml +[mcp.servers.exa] +transport = "http" +url = "https://mcp.exa.ai/mcp?exaApiKey=" +headers = { Authorization = "Bearer " } -Exa authentication: the key is sent as the `?exaApiKey=` query parameter and -as an `Authorization: Bearer` header. Without `EXA_API_KEY`, Exa is skipped -silently. +[mcp.servers.context7] +transport = "http" +url = "https://mcp.context7.com/mcp" +``` ## Configured servers @@ -39,9 +42,6 @@ url = "https://mcp.remote.example/sse" auth = "oauth" # interactive OAuth 2.1 authorization-code flow ``` -A `[mcp.servers]` entry named `exa` or `context7` replaces the auto-configured -definition. - ## OAuth 2.1 Remote servers (`http` / `sse`) that advertise OAuth (like GlitchTip) @@ -81,9 +81,9 @@ callback. `auth = "oauth"` conflicts with a static `Authorization` header ## Permissions -Tools from the read-only-ish servers **exa**, **context7**, and **grep-app** -are read-equivalent: allowed in both `yolo` and `readonly` modes. Every -other MCP tool follows the mode fallback (allowed in `yolo`, denied in +Tools from servers **named** `exa`, `context7`, or `grep-app` (read-only-ish +services) are read-equivalent: allowed in both `yolo` and `readonly` modes. +Every other MCP tool follows the mode fallback (allowed in `yolo`, denied in `readonly`) and can be matched by rules: ```toml diff --git a/src/lecode/cli.py b/src/lecode/cli.py index a9aa1ae..2d50d29 100644 --- a/src/lecode/cli.py +++ b/src/lecode/cli.py @@ -831,11 +831,7 @@ def run_interactive( # into the feed when they land. Headless/loop paths still block on the # fetch, as they print no feed. progress.pending("models", f"fetching live from {spec.name} in the background…") - if ( - config.mcp.enable_exa - or config.mcp.enable_context7 - or any(s.enabled for s in config.mcp.servers.values()) - ): + if any(s.enabled for s in config.mcp.servers.values()): progress.pending("mcp", "connecting in the background…") console.print() diff --git a/src/lecode/config/migrations.py b/src/lecode/config/migrations.py index 650d66f..5dfd8ee 100644 --- a/src/lecode/config/migrations.py +++ b/src/lecode/config/migrations.py @@ -12,8 +12,20 @@ from lecode.config.models import CURRENT_SCHEMA_VERSION -#: from_version -> migrator producing from_version + 1. Empty at v1. -MIGRATIONS: dict[int, Callable[[dict], dict]] = {} + +def _v1_to_v2(raw: dict) -> dict: + """v2: drop the removed ``[mcp] enable_exa`` / ``enable_context7`` flags.""" + mcp = raw.get("mcp") + if isinstance(mcp, dict): + raw = { + **raw, + "mcp": {k: v for k, v in mcp.items() if k not in ("enable_exa", "enable_context7")}, + } + return raw + + +#: from_version -> migrator producing from_version + 1. +MIGRATIONS: dict[int, Callable[[dict], dict]] = {1: _v1_to_v2} def migrate_config(raw: dict, current: int | None = None) -> tuple[dict, list[str], bool]: diff --git a/src/lecode/config/models.py b/src/lecode/config/models.py index cbccf44..fd1bf38 100644 --- a/src/lecode/config/models.py +++ b/src/lecode/config/models.py @@ -12,7 +12,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator #: Current on-disk schema version. Bump when adding a migration. -CURRENT_SCHEMA_VERSION = 1 +CURRENT_SCHEMA_VERSION = 2 ThinkingLevel = Literal["none", "low", "medium", "high"] AuthPolicy = Literal["auto", "required", "none"] @@ -189,8 +189,6 @@ class McpConfig(BaseModel): model_config = ConfigDict(extra="ignore") - enable_exa: bool = True - enable_context7: bool = False servers: dict[str, McpServerConfig] = Field(default_factory=dict) diff --git a/src/lecode/extras/mcp_client.py b/src/lecode/extras/mcp_client.py index 03d5b26..1e5f661 100644 --- a/src/lecode/extras/mcp_client.py +++ b/src/lecode/extras/mcp_client.py @@ -1,22 +1,15 @@ -"""The MCP client: connects configured + auto servers and exposes their tools. +"""The MCP client: connects configured servers and exposes their tools. Uses the official ``mcp`` SDK over stdio, streamable-HTTP, and SSE. Each server is connected lazily at session start with a ~10s budget; failures are isolated per server (one bad server never blocks the others or startup). Discovered tools are registered as lecode tools named ``mcp::`` -— the permission system already treats exa/context7/grep-app as +— the permission system treats servers named exa/context7/grep-app as read-equivalent, every other MCP tool falls back to the mode default (Allow in yolo, Deny in readonly). -Auto-configured servers (``[mcp] enable_exa`` / ``enable_context7``): - -- **Exa** (default on, needs ``EXA_API_KEY``): the hosted streamable-HTTP - endpoint ``https://mcp.exa.ai/mcp``. Auth assumption: the key goes as the - documented ``?exaApiKey=`` query param *and* an ``Authorization: Bearer`` - header — Exa's exact header scheme is not pinned down in their docs. -- **context7** (default off): ``https://mcp.context7.com/mcp``, no auth. - -OAuth 2.1 for remote servers (``transport = "http"`` or ``"sse"``; +Servers come from ``[mcp.servers]`` in the config; there are no built-in +servers. OAuth 2.1 for remote servers (``transport = "http"`` or ``"sse"``; ``[mcp.servers.] auth = "oauth"``) rides on the SDK's ``OAuthClientProvider`` (see ``lecode.extras.mcp_auth``): automatic connections reuse cached credentials and refresh them silently; interactive @@ -54,9 +47,6 @@ #: ``ctx.extras`` key under which the connected manager is installed. MCP_EXTRA = "mcp" -EXA_URL = "https://mcp.exa.ai/mcp" -CONTEXT7_URL = "https://mcp.context7.com/mcp" - #: Per-server startup budget. CONNECT_TIMEOUT_S = 10.0 @@ -65,34 +55,6 @@ INTERACTIVE_AUTH_BUDGET_S = CALLBACK_TIMEOUT_S + 30.0 -def auto_servers(config: Config, env: dict[str, str] | None = None) -> dict[str, McpServerConfig]: - """The auto-configured servers implied by the ``[mcp]`` flags.""" - env = os.environ if env is None else env - servers: dict[str, McpServerConfig] = {} - if config.mcp.enable_exa: - key = env.get("EXA_API_KEY", "").strip() - if key: - servers["exa"] = McpServerConfig( - transport="http", - url=f"{EXA_URL}?exaApiKey={key}", - headers={"Authorization": f"Bearer {key}"}, - ) - else: - log.debug("mcp: exa enabled but EXA_API_KEY is not set — skipping") - if config.mcp.enable_context7: - servers["context7"] = McpServerConfig(transport="http", url=CONTEXT7_URL) - return servers - - -def all_server_configs( - config: Config, env: dict[str, str] | None = None -) -> dict[str, McpServerConfig]: - """Auto servers merged with ``[mcp.servers]`` (user config wins).""" - merged = auto_servers(config, env) - merged.update(config.mcp.servers) - return merged - - @dataclass(frozen=True) class ServerStatus: """One server's state for ``/mcp``.""" @@ -220,8 +182,8 @@ def __init__( # -- connection ------------------------------------------------------------ async def connect(self) -> None: - """Connect every configured + auto server; failures stay per-server.""" - for name, server_config in all_server_configs(self._config).items(): + """Connect every configured server; failures stay per-server.""" + for name, server_config in self._config.mcp.servers.items(): server = _Server(name, server_config) self._servers[name] = server if server_config.enabled: diff --git a/src/lecode/slash/handlers.py b/src/lecode/slash/handlers.py index 1411010..d743565 100644 --- a/src/lecode/slash/handlers.py +++ b/src/lecode/slash/handlers.py @@ -1259,9 +1259,8 @@ async def cmd_init(app: TuiApp, args: list[str]) -> None: ), "chain": "/chain runs brainstorm → plan → code → review as one turn.", "mcp": ( - "MCP servers are configured under [mcp.servers] (stdio, http, or sse); " - "Exa web search is auto-configured when EXA_API_KEY is set, context7 " - "with enable_context7 = true. /mcp shows state; /mcp " + "MCP servers are configured under [mcp.servers] (stdio, http, or sse). " + "/mcp shows state; /mcp " "tools|reconnect|auth|login|logout . Remote servers with " 'auth = "oauth" log in via /mcp auth (opens your browser once; ' "credentials are reused afterwards; /mcp login forces a fresh login). " diff --git a/src/lecode/tui/loading.py b/src/lecode/tui/loading.py index 29c0c70..3b97218 100644 --- a/src/lecode/tui/loading.py +++ b/src/lecode/tui/loading.py @@ -11,7 +11,6 @@ from __future__ import annotations import logging -import os from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -131,34 +130,15 @@ def mcp_step(config: Config, mcp_servers: list[ServerStatus] | None = None) -> L lines.append(s.auth_hint) else: lines.append(f"{s.name}: disabled") - exa_missing = ( - config.mcp.enable_exa - and not os.environ.get("EXA_API_KEY") - and not any(s.name == "exa" for s in mcp_servers) - ) - if exa_missing: - lines.append("exa: no EXA_API_KEY") if not lines: return LoadStep("mcp", "no servers", SKIP) - degraded = exa_missing or any(s.state in ("failed", "auth_required") for s in mcp_servers) + degraded = any(s.state in ("failed", "auth_required") for s in mcp_servers) return LoadStep("mcp", "\n".join(lines), WARN if degraded else OK) # no live statuses (tests, headless): report the configuration only - mcp_bits: list[str] = [] - mcp_status = OK - if config.mcp.enable_exa: - if os.environ.get("EXA_API_KEY"): - mcp_bits.append("exa") - else: - mcp_bits.append("exa (no EXA_API_KEY)") - mcp_status = WARN - if config.mcp.enable_context7: - mcp_bits.append("context7") - configured = [n for n, s in config.mcp.servers.items() if s.enabled] + configured = sorted(n for n, s in config.mcp.servers.items() if s.enabled) if configured: - mcp_bits.append(_join_names(sorted(configured), cap=3)) - if mcp_bits: - return LoadStep("mcp", " · ".join(mcp_bits), mcp_status) + return LoadStep("mcp", _join_names(configured, cap=3), OK) return LoadStep("mcp", "no servers", SKIP) diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index 8741593..1cd5abd 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -32,7 +32,7 @@ def project(tmp_path): def test_defaults_validate_from_empty(): config = Config.model_validate({}) - assert config.schema_version == 1 + assert config.schema_version == 2 assert config.llm.provider == "openrouter" assert config.llm.thinking == "medium" assert config.llm.auth_policy == "auto" @@ -45,8 +45,6 @@ def test_defaults_validate_from_empty(): assert config.worktree.validation == [] assert config.permissions.mode == "yolo" assert config.notifications.volume == 0.5 - assert config.mcp.enable_exa is True - assert config.mcp.enable_context7 is False assert config.memory.max_bytes == 32768 assert config.telemetry.enabled is False assert config.telemetry.sentry_dsn is None @@ -164,11 +162,26 @@ def test_missing_schema_version_migrated_and_rewritten(global_dir, tmp_path): path = global_dir / "config.toml" path.write_text('[llm]\nmodel = "x"\n') result = load_config(cwd=tmp_path) - assert result.config.schema_version == 1 - assert "schema_version = 1" in path.read_text() + assert result.config.schema_version == 2 + assert "schema_version = 2" in path.read_text() assert 'model = "x"' in path.read_text() +def test_v1_migration_drops_removed_mcp_flags(global_dir, tmp_path): + path = global_dir / "config.toml" + path.write_text( + "schema_version = 1\n[mcp]\nenable_exa = true\nenable_context7 = true\n" + '[mcp.servers.mine]\ntransport = "stdio"\ncommand = "x"\n' + ) + result = load_config(cwd=tmp_path) + assert result.config.schema_version == 2 + assert list(result.config.mcp.servers) == ["mine"] + assert not any("unknown config key" in w for w in result.warnings) + rewritten = path.read_text() + assert "enable_exa" not in rewritten and "enable_context7" not in rewritten + assert "schema_version = 2" in rewritten + + def test_migration_applied_from_registry(global_dir, tmp_path, monkeypatch): monkeypatch.setattr("lecode.config.migrations.CURRENT_SCHEMA_VERSION", 2) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index acce190..756160e 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -14,14 +14,11 @@ import pytest from lecode.agent.builder import build_runtime -from lecode.agent.tools import ToolRegistry from lecode.config.models import Config, McpServerConfig from lecode.extras.mcp_client import ( MCP_EXTRA, McpManager, - all_server_configs, attach_mcp, - auto_servers, ) from lecode.permission import Decision, PermissionChecker @@ -35,9 +32,8 @@ def mock_server_config(**kwargs) -> McpServerConfig: def mcp_config(**server_kwargs) -> Config: - """A config with the mock as server "test" and no auto servers.""" + """A config with the mock as server "test".""" config = Config() - config.mcp.enable_exa = False config.mcp.servers["test"] = mock_server_config(**server_kwargs) return config @@ -226,60 +222,6 @@ def test_permission_readonly_allows_exa_denies_others(): assert checker.check("mcp:test:echo", {"text": "x"}).decision == Decision.DENY -# -- auto-config --------------------------------------------------------------------- - - -def test_exa_on_by_default_with_key(monkeypatch): - monkeypatch.setenv("EXA_API_KEY", "test-key-123") - servers = auto_servers(Config()) - exa = servers["exa"] - assert exa.transport == "http" - assert "exaApiKey=test-key-123" in exa.url - assert exa.headers["Authorization"] == "Bearer test-key-123" - - -def test_exa_skipped_silently_without_key(monkeypatch): - monkeypatch.delenv("EXA_API_KEY", raising=False) - assert auto_servers(Config()) == {} - - -def test_context7_off_by_default(monkeypatch): - monkeypatch.delenv("EXA_API_KEY", raising=False) - assert "context7" not in auto_servers(Config()) - - -def test_context7_on_with_flag(monkeypatch): - monkeypatch.delenv("EXA_API_KEY", raising=False) - config = Config() - config.mcp.enable_context7 = True - servers = auto_servers(config) - assert servers["context7"].url == "https://mcp.context7.com/mcp" - - -def test_user_config_overrides_auto(monkeypatch): - monkeypatch.setenv("EXA_API_KEY", "k") - config = Config() - config.mcp.servers["exa"] = mock_server_config() - merged = all_server_configs(config) - assert merged["exa"].transport == "stdio" # user definition wins - - -async def test_attach_captures_auto_servers_without_network(tmp_path, monkeypatch, tool_ctx): - """EXA_API_KEY set → attach attempts an Exa connect (no network in tests).""" - monkeypatch.setenv("EXA_API_KEY", "test-key") - attempted: list[tuple[str, McpServerConfig]] = [] - - async def fake_connect_one(self, server): - attempted.append((server.name, server.config)) - - monkeypatch.setattr(McpManager, "_connect_one", fake_connect_one) - tool_ctx.config = Config() - manager = await attach_mcp(ToolRegistry(), tool_ctx) - assert [name for name, _ in attempted] == ["exa"] - assert "test-key" in attempted[0][1].url - await manager.shutdown() - - # -- streamable-HTTP transport --------------------------------------------------------- @@ -319,7 +261,6 @@ async def http_port(http_server): async def test_http_transport_round_trip(http_port): config = Config() - config.mcp.enable_exa = False config.mcp.servers["web"] = McpServerConfig( transport="http", url=f"http://127.0.0.1:{http_port}/mcp", timeout_s=5.0 ) @@ -370,7 +311,6 @@ def ping() -> str: async def test_sse_transport_round_trip(sse_port): config = Config() - config.mcp.enable_exa = False config.mcp.servers["legacy"] = McpServerConfig( transport="sse", url=f"http://127.0.0.1:{sse_port}/sse", timeout_s=5.0 ) @@ -429,7 +369,6 @@ async def test_sse_shutdown_sets_process_global_exit_flag(http_server): port = http_server.servers[0].sockets[0].getsockname()[1] config = Config() - config.mcp.enable_exa = False config.mcp.servers["web"] = McpServerConfig( transport="http", url=f"http://127.0.0.1:{port}/mcp", timeout_s=5.0 ) @@ -453,7 +392,6 @@ async def test_http_server_connects_after_earlier_sse_server_shutdown(http_port) side logs "ASGI callable returned without completing response". """ config = Config() - config.mcp.enable_exa = False config.mcp.servers["web"] = McpServerConfig( transport="http", url=f"http://127.0.0.1:{http_port}/mcp", timeout_s=5.0 ) @@ -495,7 +433,6 @@ async def test_mcp_status_shows_failed_server(tmp_path, monkeypatch): app, _, out = make_app(tmp_path, monkeypatch, []) config = Config() - config.mcp.enable_exa = False config.mcp.servers["broken"] = McpServerConfig( transport="stdio", command="no-such-binary-lecode-test" ) diff --git a/tests/test_mcp_auth.py b/tests/test_mcp_auth.py index 8cd7419..45c7ab7 100644 --- a/tests/test_mcp_auth.py +++ b/tests/test_mcp_auth.py @@ -196,7 +196,6 @@ async def test_make_oauth_provider_attaches_handlers(cfg_env): async def test_oauth_conflicts_with_static_authorization_header(cfg_env): config = Config() - config.mcp.enable_exa = False config.mcp.servers["x"] = McpServerConfig( transport="http", url="https://auth.example/mcp", @@ -222,7 +221,6 @@ async def test_mcp_auth_and_logout_commands(tmp_path, monkeypatch): app, _, out = make_app(tmp_path, monkeypatch, []) config = Config() - config.mcp.enable_exa = False config.mcp.servers["test"] = mock_server_config() manager = McpManager(config) await manager.connect() @@ -257,7 +255,6 @@ async def test_mcp_status_lists_auth_required(tmp_path, monkeypatch): app, _, out = make_app(tmp_path, monkeypatch, []) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url="https://auth.example/mcp", auth="oauth" ) @@ -462,7 +459,6 @@ async def test_oauth_end_to_end(tmp_path, monkeypatch, fake_oauth_server): base, authz = fake_oauth_server monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "lecode-config")) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url=f"{base}/mcp", auth="oauth", timeout_s=5.0 ) @@ -547,7 +543,6 @@ async def test_oauth_restart_with_expired_token_refreshes_silently( base, authz = fake_oauth_server monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "lecode-config")) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url=f"{base}/mcp", auth="oauth", timeout_s=5.0 ) @@ -590,7 +585,6 @@ async def test_oauth_restart_with_rejected_refresh_stays_noninteractive( authz.reject_refresh = True monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "lecode-config")) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url=f"{base}/mcp", auth="oauth", timeout_s=5.0 ) @@ -623,7 +617,6 @@ async def test_oauth_denied_consent(tmp_path, monkeypatch, fake_oauth_server): authz.deny = True monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "lecode-config")) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url=f"{base}/mcp", auth="oauth", timeout_s=5.0 ) @@ -647,7 +640,6 @@ async def test_oauth_no_browser_available(tmp_path, monkeypatch, fake_oauth_serv base, _ = fake_oauth_server monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "lecode-config")) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url=f"{base}/mcp", auth="oauth", timeout_s=5.0 ) @@ -670,7 +662,6 @@ async def test_oauth_auth_announces_authorization_url(tmp_path, monkeypatch, fak base, _ = fake_oauth_server monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "lecode-config")) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url=f"{base}/mcp", auth="oauth", timeout_s=5.0 ) @@ -702,7 +693,6 @@ async def test_automatic_connect_does_not_splash_oauth_flow_traceback( base, _ = fake_oauth_server monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "lecode-config")) config = Config() - config.mcp.enable_exa = False config.mcp.servers["oauth"] = McpServerConfig( transport="http", url=f"{base}/mcp", auth="oauth", timeout_s=5.0 ) diff --git a/tests/test_mcp_oauth.py b/tests/test_mcp_oauth.py index 0791a49..03fd72b 100644 --- a/tests/test_mcp_oauth.py +++ b/tests/test_mcp_oauth.py @@ -187,7 +187,6 @@ def _open(url: str) -> bool: def _oauth_config(base: str) -> Config: config = Config() - config.mcp.enable_exa = False config.mcp.servers["auth"] = McpServerConfig( transport="sse", url=f"{base}/sse", auth="oauth", timeout_s=5.0 ) diff --git a/tests/test_setup_wizard.py b/tests/test_setup_wizard.py index a308f9c..85d70bb 100644 --- a/tests/test_setup_wizard.py +++ b/tests/test_setup_wizard.py @@ -536,10 +536,10 @@ def test_import_from_opencode_mcp_only(tmp_path): """MCP servers import even when the provider/model doesn't map.""" _write( tmp_path / ".config" / "opencode" / "opencode.json", - {"mcp": {"docs": {"type": "remote", "url": "https://mcp.context7.com/mcp"}}}, + {"mcp": {"docs": {"type": "remote", "url": "https://mcp.example.com/mcp"}}}, ) assert import_from_opencode(tmp_path) == { - "mcp_servers": {"docs": {"transport": "http", "url": "https://mcp.context7.com/mcp"}} + "mcp_servers": {"docs": {"transport": "http", "url": "https://mcp.example.com/mcp"}} } @@ -580,7 +580,7 @@ async def test_wizard_opencode_mcp_only_import_still_asks_provider(cfg_dir, tmp_ home = tmp_path / "home" _write( home / ".config" / "opencode" / "opencode.json", - {"mcp": {"docs": {"type": "remote", "url": "https://mcp.context7.com/mcp"}}}, + {"mcp": {"docs": {"type": "remote", "url": "https://mcp.example.com/mcp"}}}, ) # import pick, provider, key, model, notif session = FakeSession(["1", "1", "sk-manual", "1", ""]) @@ -588,7 +588,7 @@ async def test_wizard_opencode_mcp_only_import_still_asks_provider(cfg_dir, tmp_ assert "1 mcp server" in capsys.readouterr().out assert answers["provider"] == "openrouter" assert answers["mcp_servers"] == { - "docs": {"transport": "http", "url": "https://mcp.context7.com/mcp"} + "docs": {"transport": "http", "url": "https://mcp.example.com/mcp"} } diff --git a/tests/test_tui_loading.py b/tests/test_tui_loading.py index d40d4e6..9cc42cd 100644 --- a/tests/test_tui_loading.py +++ b/tests/test_tui_loading.py @@ -26,7 +26,6 @@ def env(tmp_path, monkeypatch): monkeypatch.setenv("LECODE_CONFIG_DIR", str(tmp_path / "cfg")) monkeypatch.setenv("LECODE_SKILLS_DIR", str(tmp_path / "global-skills")) - monkeypatch.delenv("EXA_API_KEY", raising=False) (tmp_path / ".git").mkdir() return tmp_path @@ -224,60 +223,41 @@ def test_memory_step_states(env): assert _step(steps, "memory").detail == "disabled" -def test_mcp_step_states(env, monkeypatch): +def test_mcp_step_states(env): steps, *_ = _make(env) - # exa on by default but no key - assert _step(steps, "mcp").detail == "exa (no EXA_API_KEY)" - monkeypatch.setenv("EXA_API_KEY", "k") - config = Config( - mcp={"enable_context7": True, "servers": {"mine": {"transport": "stdio", "command": "x"}}} - ) + assert _step(steps, "mcp").detail == "no servers" + config = Config(mcp={"servers": {"mine": {"transport": "stdio", "command": "x"}}}) steps, *_ = _make(env, config) - mcp = _step(steps, "mcp") - assert "exa" in mcp.detail and "context7" in mcp.detail and "mine" in mcp.detail + assert _step(steps, "mcp").detail == "mine" -def test_mcp_step_live_statuses(env, monkeypatch): +def test_mcp_step_live_statuses(env): from lecode.extras.mcp_client import ServerStatus from lecode.tui.loading import WARN - monkeypatch.setenv("EXA_API_KEY", "k") servers = [ - ServerStatus("context7", "connected", tools=2), - ServerStatus("exa", "connected", tools=3), + ServerStatus("docs", "connected", tools=2), + ServerStatus("web", "connected", tools=3), ServerStatus("mine", "failed", error="TimeoutError: connect"), ServerStatus("off", "disabled"), ] steps, *_ = _make(env, mcp_servers=servers) mcp = _step(steps, "mcp") assert mcp.status == WARN # one server failed - assert "exa: connected · 3 tools" in mcp.detail - assert "context7: connected · 2 tools" in mcp.detail + assert "web: connected · 3 tools" in mcp.detail + assert "docs: connected · 2 tools" in mcp.detail assert "mine: failed — TimeoutError: connect" in mcp.detail assert "off: disabled" in mcp.detail -def test_mcp_step_live_statuses_all_connected(env, monkeypatch): +def test_mcp_step_live_statuses_all_connected(env): from lecode.extras.mcp_client import ServerStatus from lecode.tui.loading import OK - monkeypatch.setenv("EXA_API_KEY", "k") - steps, *_ = _make(env, mcp_servers=[ServerStatus("exa", "connected", tools=3)]) + steps, *_ = _make(env, mcp_servers=[ServerStatus("mine", "connected", tools=3)]) mcp = _step(steps, "mcp") assert mcp.status == OK - assert mcp.detail == "exa: connected · 3 tools" - - -def test_mcp_step_live_statuses_exa_key_missing(env, monkeypatch): - from lecode.extras.mcp_client import ServerStatus - from lecode.tui.loading import WARN - - monkeypatch.delenv("EXA_API_KEY", raising=False) - steps, *_ = _make(env, mcp_servers=[ServerStatus("mine", "connected", tools=1)]) - mcp = _step(steps, "mcp") - assert mcp.status == WARN - assert "mine: connected · 1 tools" in mcp.detail - assert "exa: no EXA_API_KEY" in mcp.detail + assert mcp.detail == "mine: connected · 3 tools" def test_permissions_step(env):