Skip to content

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 не поддерживает их по дизайну.

Текущие проблемы ​

  1. AI-агенты плохо работают с LSP: нужны mock-документы, огромный JSON, жёсткая зависимость от URI
  2. В проекте YaoXiang отсутствует «AI-First» интерфейсный уровень: люди используют LSP в IDE, а AI-агенты не могут использовать LSP
  3. Основные 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 (подпроцесс) / streamable HTTP / SSE
  • входы и выходы инструментов имеют строгую типизацию JSON Schema (удобно для LLM)
  • в 2025-06+ выпущена спецификация streamable HTTP, этот RFC также совместим со старым SSE

Этот RFC использует только примитив Tools — выровнен с «предоставлением сервиса» в LSP, не вводит сложность файловой модели Resources.

Предложение ​

Основной дизайн ​

Единый бинарник с двумя режимами:

text
┌─────────────────────────────────────────────────────────┐
│                    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_sourcesource: String, tab_size?: u32{ast: Node, diagnostics: Diagnostic[]}напрямую вызывает frontend::parsev0.8.x
format_sourcesource: String, tab_size?: u32{formatted: String, diff: Hunk[]}напрямую вызывает formatter::formatv0.8.x
lookup_symbolquery: String, workspace_root?: String, kind?: SymbolKind[]{symbols: Symbol[]}повторно использует lsp::handlers::workspace_symbol (нечёткий поиск по query)v0.8.x
find_referencesquery: String, workspace_root?: String{locations: Location[]}повторно использует lsp::handlers::references (по query, а не по позиции)v0.8.x
typecheckfile_paths: String[], project_root: String{diagnostics: Diagnostic[], summary: Counts}повторно использует lsp::world::typecheck_fullv0.8.x
explain_diagnosticcode: String (например, E0001), lang?: String{code, category, title, description, example, help}напрямую вызывает util::diagnostic::command::render_explain_outputv0.9.x
list_importsfile_path: String, project_root?: String{imports: [{module, items, source_file}]}повторно использует middle::passes::module::ModuleGraph::validate_importsv0.9.x
rename_symbolsource: 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, не загружаются в World
  • lookup_symbol / find_references — принимают workspace_root (если не передан, используется --project-root при запуске)
  • typecheck — file_paths обязателен, обеспечивает целостность рабочей области
  • explain_diagnostic — нулевая зависимость от файлов, чисто строковый запрос к реестру кодов ошибок
  • list_imports — file_path физический файл, выводит результат разбора import для этого файла
  • rename_symbol — чисто исходное переписывание AST, не выполняет позиционных запросов в стиле LSP (семантика отличается от существующего lsp::handlers::rename)
  • hover / completion / signature_help — все отброшены: AI-агенты не выполняют «позиционно-чувствительную» семантику, вместо этого используется поиск по имени через lookup_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":

jsonc
// успешный ответ
{
  "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):

  1. флаг командной строки --project-root <dir> имеет наивысший приоритет (перекрывает умолчание)
  2. иначе: подниматься вверх от cwd в поисках yaoxiang.toml до корня файловой системы (согласно RFC-015)
  3. иначе: сам cwd
  4. file_paths должен находиться внутри корня проекта (защита от обхода); за пределами → MCP-PATH-OUTSIDE-PROJECT

Транспортный уровень ​

stdio (по умолчанию):

bash
yaoxiang mcp
# после запуска читает JSON-RPC из stdin, пишет в stdout, stderr используется для логов

Конфигурация AI-агента (Claude Code .mcp.json / Continue config.json):

jsonc
{
  "mcpServers": {
    "yaoxiang": {
      "command": "yaoxiang",
      "args": ["mcp", "--project-root", "${workspaceFolder}"],
    },
  },
}

streamable HTTP (опционально):

bash
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, не разделяемый между процессами:

text
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│ 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:

rust
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:

rust
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:

rust
#[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/lexerparse_source напрямую вызывает lexer
src/frontend/core/parserparse_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.rsmcp/tools/lookup.rs оборачивает, преобразуя query: String в LSP-параметры (поиск по имени)
src/lsp/handlers/references.rsmcp/tools/find_refs.rs оборачивает, преобразуя query: String в LSP-параметры
src/lsp/handlers/formatter.rsmcp/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
Integrationmock stdio: запустить сервер, записать JSON-RPC в stdin, прочитать ответ из stdout, сравнить с фикстурой
E2Eзапуск реального процесса yaoxiang mcp, цепочка вызовов в стиле Claude Code: parse → исправить → format → typecheck
Fuzzcargo-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-ревью для фокусировки).

Риски ​

  1. Активность поддержки mcp-rust-sdk: выпущен только в 2025 году, API может резко меняться. Смягчение: если оценка покажет нестабильность, написать свой лёгкий JSON-RPC 2.0 + диспетчер инструментов (< 500 строк)
  2. Затраты на повторный parse: AI многократно подправляет код и вызывает parse_source, что вызывает повторные lexer+parser. Смягчение: DocumentCache из RFC-017 всё ещё может ускорить повторный разбор одного и того же source на диске; для чисто транзитного source однократный разбор неизбежен
  3. Совместимость схем AI-агентов: разные агенты имеют разную строгость MCP-схемы. Смягчение: использовать крейт schemars для автоматической генерации схемы из входных структур Rust, ноль ручных дрейфов
  4. Кроссплатформенный разбор путей: в Windows пути нечувствительны к регистру, UNC-пути, границы \\. Смягчение: для разбора путей использовать camino::Utf8Path вместо std::path
  5. Схема инструмента MCP не 1:1 с LSP-параметрами: LSP workspace_symbol принимает (query); для передачи внутрь LSP нужно обернуть в позицию+URI, чтобы существующий handler мог быть повторно использован. Смягчение: в mcp/tools/lookup.rs сделать адаптер, инкапсулирующий детали на стороне MCP
  6. AST-перезапись rename_symbol имеет другую семантику, чем LSP rename: LSP textDocument/rename — URI + позиция + new_name → WorkspaceEdit; MCP rename_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)

Ссылки ​