Skip to content

feat(mcp): init/analyze/diff lifecycle tools behind MCP (#171) - #172

Merged
c2j merged 11 commits into
mainfrom
feat/issue-171-mcp-lifecycle-tools
Sep 20, 2026
Merged

c2j merged 11 commits into
mainfrom
feat/issue-171-mcp-lifecycle-tools

Conversation

@c2j

@c2j c2j commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Closes #171.

问题

codeweb mcp 只暴露 8 个只读查询工具,冷启动链路是断的:

  1. src/mcp/server.rs 第一行 Project::find(...)?:目录里没有 codeweb.toml 时进程直接退出,客户端拿不到任何 JSON-RPC 响应。
  2. McpState 只持有加载一次的 Arc<GraphStore>,不可变。
  3. store 缺失/过期时所有工具返回 status: "empty",把操作推回给用户并要求重启进程
  4. 外部跑了 analyze 内存图也不会刷新。

变更

新增三个生命周期工具(范围经确认,不自动 analyze):

Tool 行为
codeweb_init 在服务目录创建 codeweb.toml + .codeweb/,不触发分析
codeweb_analyze 全量/增量构建,成功后热替换内存 store,无需重启
codeweb_diff 列出相对上次分析变更的文件

配套改造:

  • Project::init_at(root, dirs, name)init 原本绑定进程 cwd,MCP 无法使用;init 现在委托给它。
  • McpState 改为 Arc<Inner>permitted_root + Mutex<Option<Project>> + RwLock<GraphSnapshot>。查询取 Arc<GraphStore> 快照后立即释放锁,长查询不阻塞 analyze。
  • codeweb_analyzespawn_blocking 中运行(CPU 密集同步函数,不能阻塞 runtime)。
  • 未初始化目录不再退出:查询返回 status: "uninitialized" 并引导 codeweb_init
  • 写守卫 confine_to_root:读取允许任意目录(analysis.paths),写入必须落在服务目录内;store.path 逃逸(如 ../outside.bincode)时 analyze 直接返回错误。未引入 --allow-write 开关。
  • stdout 仍只用于 JSON-RPC。

TDD 循环(每个都有先失败后通过的测试)

# 行为 测试
1 init_at 锚定显式 root project::tests::init_at_creates_config_under_given_root_not_cwd / init_at_rejects_existing_project_root
2 写路径归一化与逃逸拒绝 mcp::tools::tests::confine_* (4)
3 未初始化不退出 test_mcp_uninitialized_project_stays_alive
4 init 建配置且不自动分析 test_mcp_init_creates_project_without_auto_analyze / test_mcp_init_is_idempotent_error
5 analyze 建图 + 热替换 test_mcp_analyze_builds_graph_and_hot_swaps
6 diff 报告变更 test_mcp_diff_reports_changes_since_last_analyze
7 写守卫拒绝 store 逃逸 test_mcp_analyze_rejects_store_path_escaping_root
8 已分析且无变更时 analyze 报 is_up_to_date 且给出真实 nodes/edges(非短路返回的 0) test_mcp_analyze_refreshes_already_analyzed_project

循环 7 的 Red 通过临时禁用守卫验证:无守卫时 analyze 返回 ready 且在服务目录外写出 outside.bincode;恢复守卫后返回 error 且不落盘。
循环 8 的 Red 通过临时改用 report.nodes/edges 验证:此时报告为 0 nodes,测试失败。

需要说明的人类测试期望更新

tests/mcp_test.rs::test_mcp_tools_list 的期望工具集由 8 个变为 11 个。这是本 feature 的必然结果;断言仍保持「精确集合」而非放宽为子集检查。

门禁

cargo build --features full                         # ok
cargo test --features full -- --skip test_path_mapping_applied --skip test_serve_   # 全绿
cargo clippy --features full -- -D warnings         # clean
cargo fmt --all -- --check                          # clean

默认构建剩余 2 个 warning(node_sub_type_tagTreeNode::has_more/more_count)为既有 mcp-gated 代码在非 mcp 构建下的 dead_code,与本次改动无关。

明确不做

  • export / merge 等其它写操作。
  • init 后自动 analyze。
  • 外部 analyze 后的自动重载(需调用 codeweb_analyze)。
  • store 原子写(临时文件 + rename),可另开 issue。

c2j added 11 commits September 20, 2026 12:07
Project::init was bound to the process cwd, which the MCP server cannot
change. init_at(root, dirs, name) anchors the project (and relative
analysis paths) at an explicit root; init() now delegates with cwd.
MCP may read user-specified source paths, but every write must stay inside
the permitted project root. confine_to_root lexically normalizes the
candidate and rejects escapes such as store.path="../../x".
…loop 3)

McpState now keeps the Project and a swappable Arc<GraphStore> snapshot, so
lifecycle tools can rebuild the graph without a server restart. Startup no
longer exits when no codeweb.toml is found; queries report
status=uninitialized with a pointer to codeweb_init.
Creates codeweb.toml + .codeweb/ under the served directory via
Project::init_at. Deliberately does not analyze; reports
already_initialized when a project is present.
Runs Project::analyze on spawn_blocking (CPU-bound, must not block the
runtime), swaps the resulting store into the query snapshot, and reports
the in-memory node/edge counts. Enforces the write guard on store.path.
Reports added/modified/deleted files relative to the project root since the
last analyze, so a caller can tell whether graph queries are stale.
…loop 7)

The analyze gate now runs store.path through confine_to_root before
Project::analyze, so a tampered codeweb.toml cannot write the store
outside the directory the MCP server was started for.
Updates the README (en + zh) and DeveloperGuide tool tables, describes the
mutable state model and write confinement, and refreshes the server
instructions so a client knows to init/analyze instead of asking the user
to restart the server.

Note: tests/mcp_test.rs test_mcp_tools_list now expects 11 tools instead of
8. That expectation update is required by this feature; the assertion stays
an exact tool-set match rather than a loose subset check.
Locks the contract that codeweb_analyze on an already-analyzed, unchanged
project reports is_up_to_date with the real in-memory node/edge counts
rather than the short-circuit's zeros, and that the graph stays queryable.
@c2j
c2j merged commit c1ae283 into main Sep 20, 2026
2 checks passed
@c2j
c2j deleted the feat/issue-171-mcp-lifecycle-tools branch September 20, 2026 04:34
c2j added a commit that referenced this pull request Sep 20, 2026
Squashing #172 with `git add -A` picked up three files that were untracked
local artifacts before this work started:

  .github/copilot-instructions.md
  .github/hooks/workmux-status/hooks.json
  .sisyphus/plans/fix-analyze-stale-store-version.md

They are unrelated to the MCP lifecycle feature. This removes them from the
index only (git rm --cached), so the files stay on disk and return to their
previous untracked state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT:MCP模式下无init等指令

1 participant