RFC-035: Поддержка MCP Server (интеграция с AI Agent)
Краткое содержание
Добавить в YaoXiang сервер MCP (Model Context Protocol), чтобы AI-агенты (Claude Code, Continue, Cody, Zed и др.) могли напрямую запрашивать у исходного кода YaoXiang AST, ошибки разбора, типы, символы, ссылки, результаты форматирования. Повторно использовать бэкенд World, уже реализованный в RFC-017, добавить подкоманду yaoxiang mcp, единый бинарник с двумя режимами, изолированные процессы с независимыми World.
Мотивация
Зачем нужна эта возможность?
RFC-017 позволил редакторам понимать YaoXiang (hover / goto-def / completion). Но LSP — это позиционно-ориентированный протокол:
- каждый запрос сильно зависит от URI
textDocumentиPosition - редактор должен сначала открыть файл, сохранить, поддерживать длительное соединение с LSP-сервером
- рабочий процесс AI-агента — это фрагменты кода: «вставить кусок кода» в диалоге и задать вопрос, без предварительного сохранения
Реально доступные LSP-клиенты для AI-агентов (vscode-langservers-extracted, проекты типа mcp-lsp-bridge) переводят только L1: goto-def, hover. AI хочет делать:
- «правильно ли разбирается этот код» — требуется parse + полный поток диагностики
- «как этот символ используется в файле» — требуется lookup_symbol для поиска по имени
- «как этот код выглядит после форматирования» — требуется format_source
- «где все ошибки типов» — требуется typecheck с прогоном по всему рабочему пространству
Эти возможности перевода LSP уровня L1 не справляются, потому что LSP не поддерживает их по дизайну.
Текущие проблемы
- AI-агенты плохо работают с LSP: нужны mock-документы, огромный JSON, жёсткая зависимость от URI
- В проекте YaoXiang отсутствует «AI-First» интерфейсный уровень: люди используют LSP в IDE, а AI-агенты не могут использовать LSP
- Основные AI-агенты вроде Claude Code / Continue уже по умолчанию поддерживают MCP, но для YaoXiang эта экосистема пуста
Что такое MCP?
MCP (Model Context Protocol) — это протокол вызова инструментов AI-агентов, выпущенный и открытый в 2024-2025 годах под руководством Anthropic, ставший де-факто стандартом (OpenAI, Google, Microsoft, Zed, Continue, Cody и др. подключились). Особенности:
- основан на JSON-RPC 2.0 (того же происхождения, что и LSP)
- три основные примитивы: Tools (действия), Resources (данные), Prompts (шаблоны)
- транспорт:
stdio(подпроцесс) / streamableHTTP/ SSE - входы и выходы инструментов имеют строгую типизацию JSON Schema (удобно для LLM)
- в 2025-06+ выпущена спецификация streamable HTTP, этот RFC также совместим со старым SSE
Этот RFC использует только примитив Tools — выровнен с «предоставлением сервиса» в LSP, не вводит сложность файловой модели Resources.
Предложение
Основной дизайн
Единый бинарник с двумя режимами:
┌─────────────────────────────────────────────────────────┐
│ yaoxiang(v0.7.7+) │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ yaoxiang lsp │ │ yaoxiang mcp │ │
│ │ (stdio JSON-RPC)│ │ (stdio default │ │
│ │ RFC-017 已实现 │ │ + HTTP 可选) │ │
│ └────────┬────────┘ └──────────┬───────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 共享 lib crate(`yaoxiang`) │ │
│ │ src/lsp/{server,session,world}.rs │ │
│ │ src/frontend/{lexer,parser,core}/... │ │
│ │ src/middle/... │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ src/mcp/ ← 新增 │ │
│ │ ├── mod.rs (模块入口 + 启动函数) │ │
│ │ ├── transport/ (stdio + HTTP/SSE) │ │
│ │ ├── server.rs (JSON-RPC 消息循环) │ │
│ │ ├── tools/ (6 个 tool handler) │ │
│ │ ├── schema.rs (输入输出 JSON Schema) │ │
│ │ └── project.rs (项目根识别 + 路径解析) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘Ключевые решения:
- Один и тот же бинарник:
yaoxiangпереключается через подкоманды; процессы LSP и MCP не сосуществуют в одной среде выполнения - Изолированные процессы с независимыми World: каждый процесс
yaoxiang mcpсодержит одинWorld; не влияет друг на друга с процессом LSP и другими процессами MCP (без конкуренции за блокировки, независимая изоляция от сбоев) - stdio по умолчанию: избегает конфликтов портов, нулевая сетевая конфигурация; HTTP как опциональный запасной вариант
- Повторное использование вместо дублирования: напрямую вызывать API библиотеки
yaoxiang::frontend/yaoxiang::middle/yaoxiang::lsp::handlers, не проходить через LSP-клиент
Набор инструментов (8 инструментов, 3 этапа поставки)
Спроектировано по принципу «устранить особые случаи + поэтапность»: чисто-исходные инструменты (stateless) идут первыми, инструменты рабочей области разделяют World из LSP, инструменты AST-перезаписи добавляются отдельно.
| Имя инструмента | Вход | Выход | Повторное использование | Этап |
|---|---|---|---|---|
parse_source | source: String, tab_size?: u32 | {ast: Node, diagnostics: Diagnostic[]} | напрямую вызывает frontend::parse | v0.8.x |
format_source | source: String, tab_size?: u32 | {formatted: String, diff: Hunk[]} | напрямую вызывает formatter::format | v0.8.x |
lookup_symbol | query: String, workspace_root?: String, kind?: SymbolKind[] | {symbols: Symbol[]} | повторно использует lsp::handlers::workspace_symbol (нечёткий поиск по query) | v0.8.x |
find_references | query: String, workspace_root?: String | {locations: Location[]} | повторно использует lsp::handlers::references (по query, а не по позиции) | v0.8.x |
typecheck | file_paths: String[], project_root: String | {diagnostics: Diagnostic[], summary: Counts} | повторно использует lsp::world::typecheck_full | v0.8.x |
explain_diagnostic | code: String (например, E0001), lang?: String | {code, category, title, description, example, help} | напрямую вызывает util::diagnostic::command::render_explain_output | v0.9.x |
list_imports | file_path: String, project_root?: String | {imports: [{module, items, source_file}]} | повторно использует middle::passes::module::ModuleGraph::validate_imports | v0.9.x |
rename_symbol | source: String, old_name: String, new_name: String, scope?: "module" | "function:name" | {source: String, edits: Edit[], diagnostics: Diagnostic[]} | новый src/middle/rename.rs (перезапись AST) | v0.10.x |
Границы 8 инструментов:
parse_source/format_source— чисто исходные, stateless, не загружаются в Worldlookup_symbol/find_references— принимаютworkspace_root(если не передан, используется--project-rootпри запуске)typecheck—file_pathsобязателен, обеспечивает целостность рабочей областиexplain_diagnostic— нулевая зависимость от файлов, чисто строковый запрос к реестру кодов ошибокlist_imports—file_pathфизический файл, выводит результат разбора import для этого файлаrename_symbol— чисто исходное переписывание AST, не выполняет позиционных запросов в стиле LSP (семантика отличается от существующегоlsp::handlers::rename)— все отброшены: AI-агенты не выполняют «позиционно-чувствительную» семантику, вместо этого используется поиск по имени черезhover/completion/signature_helplookup_symbol
Когда загружается World: при запуске сервера сканирует yaoxiang.toml и src/**/*.yx по --project-root, повторно использует уже реализованный в LSP-017 API World::load_* для однократной загрузки в World.documents. Не добавляет никаких новых API в библиотеку.
Контракт инструмента
Вход: описывается с помощью JSON Schema, каждое поле имеет description + examples (LLM автоматически разберётся).
Выход: структурированный JSON, единообразно с полем schemaVersion: "1.0":
// успешный ответ
{
"schemaVersion": "1.0",
"isError": false,
"content": [
{ "type": "json", "json": { /* данные конкретного инструмента */ } }
]
}
// диагностика возвращается структурированно (не считается ошибкой инструмента)
{
"schemaVersion": "1.0",
"isError": false,
"content": [{ "type": "json", "json": {
"ast": {...},
"diagnostics": [
{ "code": "E0001", "severity": "error", "message": "...", "span": [12, 4, 12, 18] }
]
}}]
}
// ошибка уровня инструмента (например, parse_source получает невалидный UTF-8)
{
"schemaVersion": "1.0",
"isError": true,
"content": [{ "type": "text", "text": "MCP-INVALID-INPUT: source 不是合法 UTF-8" }],
"errorCode": "MCP-INVALID-INPUT"
}Система ошибок:
- Диагностика (diagnostic): ошибки разбора/типов, продолжает использовать RFC-013 (
E0001и др.) — не считается ошибкой инструмента - Ошибки уровня инструмента: с префиксом
MCP-(MCP-INVALID-INPUT,MCP-PROJECT-NOT-FOUND,MCP-INTERNAL) — рассматриваются какisError: true - panic/crash: JSON-RPC
-32603 Internal error, сервер не завершается
Правила разбора путей (применяются к workspace_root для lookup_symbol / find_references, file_paths для typecheck):
- флаг командной строки
--project-root <dir>имеет наивысший приоритет (перекрывает умолчание) - иначе: подниматься вверх от cwd в поисках
yaoxiang.tomlдо корня файловой системы (согласно RFC-015) - иначе: сам cwd
file_pathsдолжен находиться внутри корня проекта (защита от обхода); за пределами →MCP-PATH-OUTSIDE-PROJECT
Транспортный уровень
stdio (по умолчанию):
yaoxiang mcp
# после запуска читает JSON-RPC из stdin, пишет в stdout, stderr используется для логовКонфигурация AI-агента (Claude Code .mcp.json / Continue config.json):
{
"mcpServers": {
"yaoxiang": {
"command": "yaoxiang",
"args": ["mcp", "--project-root", "${workspaceFolder}"],
},
},
}streamable HTTP (опционально):
yaoxiang mcp --http --addr 127.0.0.1:7325 # один HTTP-порт, новая спецификация MCP
yaoxiang mcp --http --sse --addr 127.0.0.1:7325 # совместимость со старым SSE(v0.10)Ограничения безопасности:
- слушает только loopback (127.0.0.1 / ::1); публичная привязка явно отклоняется с выходом по ошибке
- HTTP без аутентификации (loopback по умолчанию доверенный); в будущем добавить поле
--require-token <hex> - подпроцессный режим stdio изолирован по своей природе (родительский процесс контролирует права)
Многопроцессность и параллелизм
Каждый процесс yaoxiang mcp содержит один World, не разделяемый между процессами:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ yaoxiang │ │ yaoxiang │ │ yaoxiang │
│ lsp │ │ mcp │ │ mcp │
│ (Editor 1) │ │ (Claude 1) │ │ (Claude 2) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ stdio/stdout │ stdio │ stdio
┌───┴────┐ ┌───┴────┐ ┌───┴────┐
│ Editor │ │ Claude │ │ Claude │
└────────┘ └────────┘ └────────┘Конфликты портов: конфигурация AI-агента «запускает подпроцессы» — нулевые конфликты портов по природе. В режиме HTTP пользователь сам управляет распределением портов. Изоляция World: каждый процесс имеет независимое LSP-состояние синхронизации — сбой одного MCP-процесса не влияет на LSP/другие MCP-процессы. Будущие Sessions: распределение по нескольким рабочим областям (несколько Session в одном процессе) рассматривается только в v2, этот RFC этого не делает.
Детальный дизайн
Структуры данных
Добавить src/mcp/project.rs:
pub struct ProjectRoot {
/// 绝对路径
pub root: PathBuf,
/// 加载时识别项目根的策略来源
pub source: ProjectRootSource,
}
pub enum ProjectRootSource {
CliFlag, // yaoxiang mcp --project-root
AutoDetected, // 向上找 yaoxiang.toml
FallbackCwd, // fallback 到 cwd
}
pub struct ResolvedPath {
/// 相对项目根的相对路径(推荐给 AI 读)
pub relative: String,
/// 解析后的绝对路径(用于 World 操作)
pub absolute: PathBuf,
}
impl ProjectRoot {
/// 把"file_path"解析为安全路径——防穿越
pub fn resolve(&self, file_path: &str) -> Result<ResolvedPath, McpError>;
}ProjectRoot — одиночка + автоматическая генерация схемы инструмента в src/mcp/schema.rs:
pub struct ProjectRoot {
/// 绝对路径(必含 `yaoxiang.toml` 或向下兼容回退)
pub root: PathBuf,
pub source: ProjectRootSource,
}
impl ProjectRoot {
/// CLI 启动时识别一次,结果缓存在 `McpServer` 上下文里——所有工具复用
pub fn detect(cli_override: Option<PathBuf>) -> Result<Self, McpError>;
}Схемы инструментов автоматически генерируются из input struct с помощью крейта schemars, чтобы избежать дрейфа при ручном написании JSON Schema:
#[derive(Deserialize, schemars::JsonSchema)]
pub struct ParseSourceInput {
/// 完整 YaoXiang 源码片段——**不**保存到磁盘,纯 transient
pub source: String,
pub tab_size: Option<u32>,
}В схеме инструментов parse_source / format_source нет поля file_path — эти два инструмента принимают только строковый источник, не участвуют в семантике проекта. lookup_symbol / find_references / typecheck принимают workspace_root или file_paths (обязательность см. в таблице инструментов).
Изменения в компиляторе
| Модуль | Изменения |
|---|---|
src/lsp/world.rs | без изменений — при запуске MCP вызывает существующий API World::load_* из LSP для однократной загрузки рабочей области |
src/lsp/handlers/workspace_symbol.rs | без изменений — mcp/tools/lookup.rs оборачивает, преобразуя query в LSP-параметры |
src/lsp/handlers/references.rs | без изменений — то же |
src/lsp/handlers/formatter.rs | без изменений — format_source вызывается напрямую |
src/main.rs | добавить ветку подкоманды Mcp |
Cargo.toml | добавить feature mcp-server (или всегда включать в основной бинарник) |
src/util/diagnostic/ | без изменений (уже реализовано в RFC-017) |
Ключевое ограничение: src/mcp/ не может обратно зависеть от приватных символов src/lsp/ — можно вызывать handlers только через публичный API crate::lsp::.
Обратная совместимость
- ✅ Полная обратная совместимость: новая подкоманда
yaoxiang mcp, не изменяет никакого существующего поведенияyaoxiang/yaoxiang lsp - ✅ LSP-сервер не изменяется: все возможности, API, внутренние состояния, реализованные в RFC-017, не меняются
- ✅ Публичный API lib-крейта не изменяется: все пути
pubне меняются; MCP только потребляет существующий API — ноль новых методовpub
Интеграция с существующими системами
| Существующий модуль | Способ интеграции с MCP |
|---|---|
src/frontend/lexer | parse_source напрямую вызывает lexer |
src/frontend/core/parser | parse_source напрямую вызывает parser; при неудаче выдаёт узлы Missing* (RFC-017) |
src/frontend/core/typecheck/inference/* | typecheck повторно использует шаблон collect_diagnostics (RFC-017 §проблема 1) |
src/middle/ | typecheck прогоняет все middle-pass'ы (анализ зависимостей и др.) |
src/lsp/world.rs | при запуске вызывает API World::load_* (уже есть); World не принимает никаких «виртуальных документов» |
src/lsp/handlers/workspace_symbol.rs | mcp/tools/lookup.rs оборачивает, преобразуя query: String в LSP-параметры (поиск по имени) |
src/lsp/handlers/references.rs | mcp/tools/find_refs.rs оборачивает, преобразуя query: String в LSP-параметры |
src/lsp/handlers/formatter.rs | mcp/tools/format.rs вызывается напрямую (если не реализовано, добавить formatter::format_with_diff) |
src/util/i18n/ | сообщения об ошибках идут через многоязычные файлы ресурсов (zh-CN/en) |
Обработка ошибок
| Источник | Обработка |
|---|---|
| Ошибка разбора | Diagnostic{code:"E0xxx", severity, message, span} (не ошибка инструмента, возвращается в content) |
| Ошибка типа | то же |
file_paths за пределами (инструмент typecheck) | ошибка уровня инструмента MCP-PATH-OUTSIDE-PROJECT |
source невалидный UTF-8 | ошибка уровня инструмента MCP-INVALID-INPUT |
| panic инструмента | JSON-RPC -32603 Internal error; сервер не завершается |
| Клиент отправляет не JSON-RPC | обрыв потока (stdio EOF), перезапуск = новая сессия |
Уровни серьёзности диагностики продолжают использовать RFC-017 (уже реализованный) enum ErrorKind { Error, Warning, Note }.
Стратегия тестирования
| Уровень | Тесты |
|---|---|
| Unit | обход пути в src/mcp/project.rs::resolve, валидация схемы в src/mcp/schema.rs |
| Integration | mock stdio: запустить сервер, записать JSON-RPC в stdin, прочитать ответ из stdout, сравнить с фикстурой |
| E2E | запуск реального процесса yaoxiang mcp, цепочка вызовов в стиле Claude Code: parse → исправить → format → typecheck |
| Fuzz | cargo-fuzz для разбора MCP JSON-RPC (libFuzzer harness) |
Каждый инструмент должен иметь как минимум 1 happy path + 1 сценарий с диагностикой + 1 сценарий ошибки инструмента в интеграционных тестах.
Компромиссы
Преимущества
- Очень низкая стоимость повторного использования:
World/Session/handlers/ сбор диагностики уже реализованы (RFC-017), этот RFC — «добавить слой MCP-оболочки» - AI-First интерфейс: контракт инструмента в 3-5 раз интуитивнее LSP; LLM читает схему напрямую
- Изоляция процессов: развязан с LSP-сессией редактора и с другими MCP-процессами, нулевая конкуренция за блокировки
- stdio-дружественность: все основные AI-агенты по умолчанию используют подпроцессный режим, подключение без конфигурации
- YAGNI соблюдён: этот RFC отбросил Resources, Sessions, межпроцессное состояние, удалённый MCP — откроем в v2
Недостатки
- Фрагментация протоколов: в будущем три протокола LSP / MCP / DAP будут развиваться независимо, затраты на поддержание консистентности
- HTTP-режим как гражданин второго сорта: ограничение loopback позиционирует его как локальный инструмент, удалённые сценарии требуют перепроектирования в v2
- Затраты на повторный parse: AI многократно подправляет код и вызывает
parse_source, что вызывает повторные lexer+parser. Смягчение:DocumentCacheиз RFC-017 всё ещё может ускорить повторный разбор одного и того же source на диске; для чисто транзитного source однократный разбор неизбежен - Затраты на тестовое покрытие: 5 инструментов × 3 сценария = минимум 15 интеграционных тестов
Альтернативы
| Вариант | Почему не выбран |
|---|---|
| Встраивание двух протоколов в процесс (LSP+MCPlistener сосуществуют) | stdin/stdout может иметь только одного потребителя; HTTP тоже пришлось бы сосуществовать — сложность > выгода |
| MCP как мост к LSP-клиенту | лишний слой IPC; LSP по дизайну не поддерживает поиск символов по имени — возможности, нужные MCP, LSP дать не может |
| Использовать gRPC / кастомный протокол | отход от де-факто стандарта; у сообщества уже есть MCP SDK (TypeScript, Python, Rust) с собственной экосистемой |
| Повторно использовать все возможности LSP handler (набор инструментов L3) | много работы по адаптации position↔intent; убывающая маржинальная выгода |
| Только HTTP в первой версии (без stdio) | Claude Code / Continue и др. по умолчанию используют stdio, порог входа слишком высок |
Стратегия реализации
Зависимости
- Сильная зависимость: реализация LSP в RFC-017 (уже выполнена)
- Сильная зависимость: система кодов ошибок в RFC-013 (уже выполнена)
- Сильная зависимость: идентификация корня проекта в RFC-014 / RFC-015 (частично выполнена)
- Новые зависимости (Rust крейты):
mcp-rust-sdk(подлежит оценке, см. modelcontextprotocol/rust-sdk)tokio(уже есть, опциональный feature)axum(HTTP-режим) илиhyperнапрямую — подлежит оценке
- Нулевые изменения спецификации языка: чистое инкрементирование инструментария
Этапы реализации
| Этап | Содержание | Оценка длительности |
|---|---|---|
| v0.8.x (MVP) | src/mcp/{mod.rs, server.rs, transport/stdio.rs, project.rs, schema.rs} + parse_source + format_source + lookup_symbol + find_references + typecheck (5 инструментов) + подкоманда yaoxiang mcp + World::load_* при запуске | 3-4 недели |
| v0.9.x (YaoXiang Intelligence) | + explain_diagnostic (напрямую вызывает render_explain_output) + + list_imports (обёртывает ModuleGraph::validate_imports) + модульные/интеграционные тесты | 1-2 недели |
| v0.10.x (AST + HTTP) | + rename_symbol (новый src/middle/rename.rs, перезапись AST) + streamable HTTP-транспорт + оптимизация производительности (parse_source P99 < 100ms) | 2-3 недели |
Почему 3 этапа: сначала MVP прогоняет stdio + 5 инструментов для проверки разумности дизайна интерфейса; v0.9.x добавляет низкорисковые, не требующие адаптации «YaoXiang-специфичные» инструменты для проверки правильности интеграции; v0.10.x открывает высокорисковый новый модуль «AST-перезаписи» (отдельное PR-ревью для фокусировки).
Риски
- Активность поддержки
mcp-rust-sdk: выпущен только в 2025 году, API может резко меняться. Смягчение: если оценка покажет нестабильность, написать свой лёгкий JSON-RPC 2.0 + диспетчер инструментов (< 500 строк) - Затраты на повторный parse: AI многократно подправляет код и вызывает
parse_source, что вызывает повторные lexer+parser. Смягчение:DocumentCacheиз RFC-017 всё ещё может ускорить повторный разбор одного и того же source на диске; для чисто транзитного source однократный разбор неизбежен - Совместимость схем AI-агентов: разные агенты имеют разную строгость MCP-схемы. Смягчение: использовать крейт
schemarsдля автоматической генерации схемы из входных структур Rust, ноль ручных дрейфов - Кроссплатформенный разбор путей: в Windows пути нечувствительны к регистру, UNC-пути, границы
\\. Смягчение: для разбора путей использоватьcamino::Utf8Pathвместоstd::path - Схема инструмента MCP не 1:1 с LSP-параметрами: LSP
workspace_symbolпринимает(query); для передачи внутрь LSP нужно обернуть в позицию+URI, чтобы существующий handler мог быть повторно использован. Смягчение: вmcp/tools/lookup.rsсделать адаптер, инкапсулирующий детали на стороне MCP - AST-перезапись
rename_symbolимеет другую семантику, чем LSPrename: LSPtextDocument/rename— URI + позиция + new_name → WorkspaceEdit; MCPrename_symbol— source + old_name + new_name → новый source. Нельзя повторно использовать напрямую. Смягчение: отдельная реализацияsrc/middle/rename.rs, scope-aware перезапись ссылок, не мешающая реализации LSP handler
Открытые вопросы
- [ ] Выбор
mcp-rust-sdk/ собственная реализация? (@Чэнь Сюй: сначала оценить версию rust-sdk за июнь, затем решать) - [ ] Путь аутентификации HTTP? (открыть в RFC для v0.10)
- [ ] Нужно ли
MCPвыводитьtools/listпри запуске для активного обнаружения AI? (требуется стандартом MCP, реализовано по умолчанию) - [ ] Поддерживает ли
typecheckрежимmode: "fast|full"(fast = только подмножество текущего файла, full = вся рабочая область)? - [ ] Реалистичен ли бюджет производительности parse_source P99 < 100ms? (нужен бенчмарк реальных затрат
DocumentCacheиз RFC-017 в режиме source-string)
Ссылки
- RFC-017: Дизайн поддержки Language Server Protocol (LSP)
- RFC-013: Дизайн спецификации кодов ошибок
- RFC-014: Дизайн системы управления пакетами
- RFC-015: Дизайн системы конфигурации YaoXiang
- Спецификация MCP
- MCP Rust SDK
- JSON-RPC 2.0
- Спецификация LSP 3.18
- Rust Analyzer — справка по интеграции M2 / MCP
- Реализация MCP в zed-industries/zed
