Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ jobs:
- name: Lint (ruff)
run: ruff check src/ tests/

- name: Client snippets cold start (G5)
# Каждый сниппет из `install.py --print-config` запускается на чистом
# раннере: initialize + tools/list. Покрывает все 5 клиентов, включая
# не-Zed/не-OpenCode (generic, claude-code, cursor). Таймаут 300s: на
# холодной машине сервер поднимает провайдеров дольше 60s (ONNX-ожидание
# в логах), и 120s оказалось мало на windows-latest (2026-11-12).
run: python scripts/verify_client_snippets.py --install-root "${{ github.workspace }}" --timeout 300

- name: Layer-boundary gate (Фаза 1 Universal Engine)
# Три оси ТЗ: ADAPTER → TRANSPORT → SOURCE → CORE. Гейт не даёт
# mcp/tools импортировать adapters/src.sources и core — adapters.*
Expand Down
20 changes: 20 additions & 0 deletions AGENT_DIARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1737,3 +1737,23 @@ BREACH: MSC-P-19 | date: 2026-10-03 | rule: §12 | guard_at_time: no | detected_
**Дефект (воспроизведён):** `lance error: query dim(768) doesn't match the column vector dim(384)`. Модель `embeddinggemma-2` отдаёт 768 (проверено `/v1/embeddings`), схема таблицы — 384 (осталась от e5-small). `.env`: `EMBEDDING_MODEL=embeddinggemma-2` при `MSCODEBASE_DENSE_OFF=true`.
**Следствие:** переключать `DENSE_OFF=false` сейчас нельзя — сначала размерность из сервера + переиндексация, потом повторный `run_curve.py` в dense. Записано в KNOWN_ISSUES с точным сообщением.
**Урок:** «включить dense и станет лучше» — непроверенная гипотеза; первый же замер дал паритет и конфигурационный дефект.
## [2026-11-12] G5: установка отвязана от Zed — сниппет на клиента вместо ручного конфига
**Симптом:** `install.py` v3.1 импортировал `adapters.zed.zed_config` на верхнем уровне и всегда писал конфиг Zed. На машине без Zed установка всё равно создавала `%APPDATA%/Zed`, а для Claude Code / Cursor / любого `mcpServers`-хоста команду и env приходилось собирать вручную по README (и она расходилась с кодом).
**Корень:** установщик и хост-адаптер были одним целым: каталог установки = каталог расширения Zed, шаг 11 = патч `settings.json`, generic-клиентам нечего было скопировать.
**Fix (v3.2):**
1. `MSCODEBASE_INSTALL_ROOT` + `resolve_install_root()`; историческое имя папки остаётся только default, содержимое — обычный venv.
2. Zed-адаптер импортируется лениво внутри `step_zed`/`step_zedcfg`, отказ адаптера не роняет установку; `--no-zed` — полностью нейтральный режим.
3. `--print-config --client {generic,opencode,zed,claude-code,cursor}` → один JSON-объект; формы конвертов в `adapters/clients/snippets.py` (zed-ключи сверены с `_make_server_entry`).
4. `scripts/verify_client_snippets.py`: рендерит сниппет каждого клиента, запускает его во временном каталоге и делает `initialize` + `tools/list`. Замер 2026-11-12: 5/5 `PASS ... tools=17`.
**Проверка:** `tests/test_install_cli.py` (16 тестов, в т.ч. реальный хендшейк из напечатанного сниппета); мутационная проба — переименование `PYTHONPATH` в env дало 5 красных.
**Найденный по ходу баг:** неинтерактивные режимы наследовали cp1251-stdout и падали на первом не-ASCII символе (`UnicodeEncodeError` на пути с кириллицей) — добавлен `_utf8_stdout()` для всех режимов, маркеры в отчёте сделаны ASCII.
**Урок:** «поддержка любого клиента» проверяется запуском сниппета, а не валидностью JSON: часть конвертов отвечала только после того, как запрос пошёл после `initialized`.
## [2026-11-12] G5: CI нашёл две вещи, которые локальный прогон не видел
**1. Платформенная проверка в тесте.** `test_print_config_is_valid_json_per_client` требовал, чтобы команда оканчивалась на `python.exe|python3|mscodebase-mcp*`. На ubuntu `sys.executable` = `.../venv/bin/python` → 5 красных в `test` и в `clean-state` (там тот же suite из чистого клона). Fix: проверять `Path(command[0]).name.startswith(("python","mscodebase-mcp"))` — платформенно-нейтрально; реализация не менялась.
**2. Холодная машина медленнее, чем ноутбук.** `scripts/verify_client_snippets.py` с таймаутом 120 s упал на `windows-latest` (шаг шёл ровно 121 s): первый запуск сервера на чистом раннере дольше 60 s (в логах — ожидание ONNX-сервера). Fix: таймаут по умолчанию 300 s, в CI передаётся `--timeout 300`; тот же запас в тесте-хендшейке.
**Урок:** «зелёный локально» не переносится на CI автоматически: POSIX-имя интерпретатора и холодный старт — две вещи, которые видны только на чужой машине. Оба места были бы пойманы раньше, если бы холодный старт гонялся в CI с первого коммита (теперь гоняется — шаг `Client snippets cold start (G5)`).
## [2026-11-12] G5: третья находка CI — забитый stderr-пайп, и зелёный прогон
**Симптом:** шаг `Client snippets cold start (G5)` на `windows-latest` висел 20+ минут. Причина в самом скрипте: `proc.stderr.readline()` в ветке FAIL блокировал чтение, если сервер ничего не писал в stderr, а `stderr=PIPE` никто не вычитывал — при старте сервер пишет лог больше буфера пайпа (64 KB) и встаёт на записи в stderr, так и не ответив на `initialize`.
**Fix:** `_stderr_drain()` — фоновый поток с ring-buffer на 20 строк (пайп всегда вычитывается, хвост попадает в сообщение FAIL), плюс `LOG_LEVEL=WARNING` для пробы (`src/main.py:126` читает эту переменную) — сервер не заливает пайп на холодной машине. `_read_message` остался ограниченным по времени.
**Проверка:** локально 5/5 PASS, 13 s; CI run `43092017919` (head `20f287b`) — **7/7 jobs success**: `test` (ubuntu + windows), `clean-state`, `manifest-parity`, `secret-scan`, `docker-build`, `transport-equiv-e2e`.
**Урок:** дочерний процесс с `stdout/stderr=PIPE` обязан иметь читателя на каждом пайпе; «я прочитаю stderr только при ошибке» — это дедлок, который воспроизводится только там, где логов больше буфера.
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,35 @@ number and the command that regenerates it are listed in

## Quick start

Requirements: Windows is the validated platform; Python 3.14+; an MCP host
(**OpenCode is the primary one**, the Zed extension still works).
Requirements: Windows is the validated platform; Python 3.14+; any MCP host
(OpenCode, Zed, Claude Code, Cursor, or a generic `mcpServers` client).

```bash
cd <repo-root>
python install.py # full install: venv, pip, optional models
python install.py --sync # code-only sync into the installed copy
python install.py # venv + deps + code (model steps ask first)
python install.py --sync # code-only sync after a source change
```

Restart the host after `install.py` finishes (the OpenCode session, or the Zed
window). The installed copy lives in `%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence`
and both hosts launch that venv. Verify from the agent panel:
The install is host-neutral: it writes a plain Python environment (default
`%LOCALAPPDATA%\Zed\extensions\mscodebase-intelligence` — the name is
historical). Point it anywhere with `MSCODEBASE_INSTALL_ROOT`. Then print the
config for your host instead of writing it by hand:

```bash
python install.py --print-config --client generic # mcpServers-style hosts
python install.py --print-config --client opencode
python install.py --print-config --client claude-code
python install.py --print-config --client cursor
python install.py --print-config --client zed
```

Each prints one JSON object containing the interpreter, `-u -m src.main`,
`PYTHONPATH`, `PROJECT_PATH` and `PYTHONUTF8=1`; paste it into the host's MCP
config. Per-client copy-paste sections and the launch check
(`python scripts/verify_client_snippets.py`) are in
[docs/en/CLIENTS.md](docs/en/CLIENTS.md). Restart the host after configuring it.

Verify from the agent panel:

```text
tool_help() # profile, visible and hidden tools
Expand Down
64 changes: 42 additions & 22 deletions adapters/clients/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
# MSCodeBase — конфиги клиентов (Фаза 5, план §4)
# MSCodeBase — client configs (Phase 5, plan §4)

Тонкие адаптеры-конфиги для подключения движка к внешним MCP-клиентам.
Каждый конфиг имеет stdio-вариант (локальный) и http-вариант (remote, Фаза 3).
Thin config shapes for connecting the engine to external MCP clients. Each file
has a stdio variant (local) and an HTTP variant (remote, Phase 3).

## stdin/stdout (локально)
**These are shapes, not the recommended way to get a config.** The generated
snippet knows the real paths of your installation:

- **Claude Code / Desktop** — `claude.code.mcp.json` (секция `mcpServers`).
- **VS Code** — `vscode.mcp.json` (`.vscode/mcp.json`, секция `servers`).
- **Cursor** — использует тот же Claude-формат (`mcpServers`), файл `.cursor/mcp.json`
или `.mcp.json` в корне проекта.
```bash
python install.py --print-config --client generic # mcpServers
python install.py --print-config --client claude-code
python install.py --print-config --client cursor
python install.py --print-config --client opencode
python install.py --print-config --client zed
```

Заполнить плейсхолдеры:
- `<path-to-venv>/Scripts/python.exe` — venv-py движка (POSIX: `<venv>/bin/python3`);
- `PYTHONPATH=<path-to-repo>` — путь к дереву исходников (`src/`);
- `cwd=<path-to-project>` — проект, который индексируется.
The generator lives in `adapters/clients/snippets.py`; the launch check is
`scripts/verify_client_snippets.py`. Full per-client instructions:
`docs/en/CLIENTS.md`.

`command = <python> -m src.main` запускает server через `run_server` (stdio).
## stdin/stdout (local)

## HTTP (remote, Фаза 3)
- **Claude Code / Desktop** — `claude.code.mcp.json` (key `mcpServers`).
- **VS Code** — `vscode.mcp.json` (`.vscode/mcp.json`, key `servers`).
- **Cursor** — same Claude shape (`mcpServers`), file `.cursor/mcp.json` or
`.mcp.json` in the project root.

http-блок `mscodebase-remote`:
- `url = http://<host>:8089/mcp` — Streamable HTTP вход (`src/remote_main.py`);
- `headers.Authorization = Bearer <MSCODEBASE_REMOTE_TOKEN>` — обязателен для сети.
Placeholders to fill:

Сборка/запуск remote-сервера: см. `deploy/docker/README.md` (образ + compose) и
`src/remote_main.py`.
- `<path-to-venv>/Scripts/python.exe` — the engine's venv interpreter
(POSIX: `<venv>/bin/python3`);
- `PYTHONPATH=<path-to-repo>` — the tree that contains `src/` (the installed copy
or the checkout);
- `PROJECT_PATH=<path-to-project>` — the project that gets indexed
(`cwd` is not required: the server reads `PROJECT_PATH`).

## CLI (без MCP, для CI/скриптов)
`command = <python> -m src.main` starts the server through `run_server` (stdio).

`python -m src.cli <tool> '<json>'` — прямой вызов tool-класса через DI,
см. `src/cli.py`. Curated allowlist: `get_task_status`, `stale_detector`,
## HTTP (remote, Phase 3)

HTTP block `mscodebase-remote`:

- `url = http://<host>:8089/mcp` — Streamable HTTP entry (`src/remote_main.py`);
- `headers.Authorization = Bearer <MSCODEBASE_REMOTE_TOKEN>` — required on the network.

Building/running the remote server: see `deploy/docker/README.md` (image + compose)
and `src/remote_main.py`.

## CLI (no MCP, for CI/scripts)

`python -m src.cli <tool> '<json>'` — direct tool-class call through DI, see
`src/cli.py`. Curated allowlist: `get_task_status`, `stale_detector`,
`get_context`, `graph_query`, `find_similar_bugs`.
9 changes: 6 additions & 3 deletions adapters/clients/claude.code.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
"mcpServers": {
"mscodebase": {
"type": "stdio",
"command": "<path-to-venv>/Scripts/python.exe",
"command": "<install-root>/venv/Scripts/python.exe",
"args": ["-m", "src.main"],
"env": { "PYTHONPATH": "<path-to-repo>" },
"cwd": "<path-to-project>"
"env": {
"PYTHONPATH": "<install-root>",
"PROJECT_PATH": "<path-to-project>",
"PYTHONUTF8": "1"
}
},
"mscodebase-remote": {
"type": "http",
Expand Down
94 changes: 94 additions & 0 deletions adapters/clients/snippets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Ready-to-paste MCP client snippets (G5).

The server is host-neutral: one stdio command plus a few environment variables.
Hosts differ only in the JSON envelope they read, so the envelope is generated
here and `install.py --print-config --client <name>` prints the result.

Envelope shapes are not invented: each one mirrors what the corresponding
adapter/host consumes today.

generic, claude-code, cursor -> {"mcpServers": {...}}
opencode -> {"mcp": {name: {"type": "local", ...}}}
zed -> {"context_servers": {name: {command, args,
env, enabled}}} — same keys that
adapters/zed/zed_config._make_server_entry
writes into settings.json.

Usage:
from adapters.clients.snippets import render_snippet
print(render_snippet("generic", command=exe, args=["-u", "-m", "src.main"],
env={"PYTHONPATH": root, "PROJECT_PATH": proj}))
"""

from __future__ import annotations

import json
from typing import Mapping, Sequence

__all__ = ["CLIENTS", "SERVER_NAME", "render_snippet", "snapshot"]

SERVER_NAME = "mscodebase"

#: Clients `--print-config` accepts. Order is the order documented in CLIENTS.md.
CLIENTS = ("generic", "opencode", "zed", "claude-code", "cursor")


def snapshot(
client: str,
*,
command: str,
args: Sequence[str],
env: Mapping[str, str],
) -> dict:
"""Return the snippet as a dict (the JSON text is just its serialization)."""
if client not in CLIENTS:
raise ValueError(f"unknown client {client!r}; known: {', '.join(CLIENTS)}")

env_dict = dict(env)
server = {
"command": command,
"args": list(args),
"env": env_dict,
}

if client in ("generic", "claude-code", "cursor"):
return {"mcpServers": {SERVER_NAME: server}}
if client == "opencode":
# OpenCode V2: names live under mcp.servers, never directly under mcp.
# Source: https://opencode.ai/docs/mcp-servers ("V2 does not place server
# names directly under `mcp`"). The flat `mcp.<name>` layout of V1 is
# still accepted for migrated configs, but new configs use `servers`.
return {
"mcp": {
"servers": {
SERVER_NAME: {
"type": "local",
"command": [command, *args],
"environment": env_dict,
}
}
}
}
# Zed: keys match adapters/zed/zed_config._make_server_entry.
return {
"context_servers": {
SERVER_NAME: {
"command": command,
"args": list(args),
"env": env_dict,
"enabled": True,
}
}
}


def render_snippet(
client: str,
*,
command: str,
args: Sequence[str],
env: Mapping[str, str],
) -> str:
"""Pretty-printed JSON snippet, newline-terminated."""
payload = snapshot(client, command=command, args=args, env=env)
return json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
9 changes: 6 additions & 3 deletions adapters/clients/vscode.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
"servers": {
"mscodebase": {
"type": "stdio",
"command": "<path-to-venv>/Scripts/python.exe",
"command": "<install-root>/venv/Scripts/python.exe",
"args": ["-m", "src.main"],
"env": { "PYTHONPATH": "<path-to-repo>" },
"cwd": "<path-to-project>"
"env": {
"PYTHONPATH": "<install-root>",
"PROJECT_PATH": "<path-to-project>",
"PYTHONUTF8": "1"
}
},
"mscodebase-remote": {
"type": "http",
Expand Down
Loading
Loading