Skip to content

feat: mcp support - #151

Draft
SunYanbox wants to merge 9 commits into
mainfrom
feat/mcp-support
Draft

SunYanbox wants to merge 9 commits into
mainfrom
feat/mcp-support

Conversation

@SunYanbox

Copy link
Copy Markdown
Owner

Summary

Related Issue

Checklist

  • I have updated CHANGELOG_ZH_CN.md and docs/changelog/CHANGELOG_ZH_CN.md if applicable.
  • I have added labels matching the change type (e.g. bug, enhancement, documentation, refactor, prompt).
  • I have verified the changes with the appropriate checks.

中文

概述

相关 Issue

检查清单

  • 如适用,已更新 CHANGELOG_ZH_CN.mddocs/changelog/CHANGELOG_ZH_CN.md
  • 已根据变更类型添加对应 label(如 bugenhancementdocumentationrefactorprompt)。
  • 已通过适当的检查验证变更。

`ToolKind` is a compile-time enum whose names are `&'static str`, so a tool
discovered from an MCP server at runtime has no representation in it. Rather
than widen the enum and push dynamic names through every match arm, the MCP
client state lives in a parallel runtime layer that the existing tool chain
consults without knowing where a tool came from.

This commit adds that layer on its own: server configuration with transport
validation, JSON Schema to parameter conversion, the exposed-name scheme
`mcp_<server>_<tool>`, and a process-wide store that de-duplicates exposed
names across servers and remembers why a server contributed no tools.

Nothing reaches the layer yet. `connect_all` validates the declarations and
installs an empty tool list per server, `call_tool` reports every known tool
as not connected, and no configuration section feeds either of them, so the
behaviour of the CLI is unchanged. The `rmcp` client is not wired up here.

`text::t_fmt` mirrors the copies already living in the CLI and the workspace
crate; those are left untouched because consolidating them is a separate
change, and the MCP messages need the helper before that happens.
…lKind

The prompt builder needs a call template for every tool it lists, and MCP
tools have no `ToolKind` to hand it. Passing `&ToolKind` to the parsers
also made each of them reach for `ToolKind::parameters()` on their own,
which kept the enum's ownership model inside the rendering path.

A `ToolTemplate` view carries only what a template needs — the tool name
and, per parameter, its name, kind and required flag — and is built either
from a `ToolKind` or, later, from a discovered MCP tool. The parsers take
the view and stop caring where the tool came from. Parameters are held in
a `Vec` rather than a slice because `ToolKind::parameters()` returns an
owned `Vec`; a slice would force a temporary to outlive the call.

The registry still hands its parser a `&ToolKind`, so this commit alone
does not build; the registry call site and the tool set follow in the next
change.
The parsers accept only names present in `EnabledToolSet`, so a tool a
server exposes is invisible to them — and to the model — until the set
learns about it. Two name kinds now share one set: built-in names stay
`&'static str` from the compile-time enum, MCP names are owned strings
resolved at runtime. The parsers keep reading a single uniform set and
remain unaware of a tool's origin.

The cache fingerprint puts built-ins in canonical order and MCP names
sorted, so an unchanged set always produces the same sequence and the
registry can keep reusing its lookup structures.

The prompt lists MCP tools after the built-ins, rendered through the same
template entry point. They are switched on per server rather than per
tool, so the `[tools]` flags do not apply to them.

A parsed MCP call still cannot run: the executor routes by `ToolKind` and
does not consult the MCP store yet, so it reports such a call as an
unknown tool. Routing and approval follow in the next change.
A parsed MCP call had nowhere to go: routing started from `ToolKind`, and
`from_name` returns `None` for a runtime-discovered tool, so the call fell
into the unknown-tool branch even though the parsers had just accepted it.

The executor now tries the MCP store once the built-in lookup misses. Both
paths validate and coerce parameters through the same `ParamSpec` view,
which is what let the built-in specs move out of the `ToolKind` methods
without duplicating the rules for MCP tools.

Every MCP call is marked `NeedsApproval`, and no MCP call is pre-checked,
so each one reaches the user before it runs. A server's tool descriptions
are untrusted input, and a description that reads as an instruction must
not be able to execute anything on its own; the approval gate is the
control that holds regardless of what a server claims about its tools.

`call_tool` still reports a known tool as not connected because no client
is wired up yet, and no configuration section produces servers, so nothing
observable changes for a workspace without MCP declarations.
There was no way to tell the program which MCP servers to connect to, so
the runtime layer added earlier had nothing to consume.

The `[[mcp.servers]]` entries parse into the runtime configuration, with
project declarations replacing global ones as a whole rather than merging
per server; a half-overridden global list would be harder to reason about
than an explicit either-or. The transport label is kept as a raw string
until validation, so a typo becomes a reported issue that skips only that
entry instead of failing the whole file during deserialization. Unknown
labels fall back to stdio for the remaining checks, so one bad field does
not hide a second problem in the same entry.

Each rejected entry surfaces as a warning line naming the offending key,
rendered in both locales. `save_mcp_servers` writes the section back on its
own, leaving `save_project` and every other table untouched.

The section is parsed but not yet consumed: `connect_all` still has no
caller in the CLI, so declaring a server changes nothing observable.
@SunYanbox SunYanbox added the enhancement New feature or request label Sep 15, 2026
@github-actions

Copy link
Copy Markdown

ChangeLog Check Report

👋 Thank you for your PR! Changes have been detected in the crates/, Cargo.toml, Cargo.lock, or .github/ directories, but no updates to CHANGELOG*.md files were found.

If this PR has an impact on users or developers, please consider updating the CHANGELOG. If not, you may ignore this message.


👋 感谢你的 PR!检测到 crates/Cargo.tomlCargo.lock.github/ 目录有变更,但未发现 CHANGELOG*.md 文件的更新。

如果此 PR 对用户或开发者有影响,请考虑更新 CHANGELOG。如果不需要,可以忽略此消息。

The MCP layer could read a server declaration but never talk to one:
`connect_all` validated the configuration and installed an empty tool list
per server, and every call was answered with "not connected". Declaring a
working server changed nothing.

`connect_all` now spawns each enabled server over stdio, completes the
handshake and lists its tools, which are converted through the existing
schema parser. Spawning, the handshake and discovery share a ten-second
budget, and one `tools/call` gets thirty; a server that hangs is recorded
as failed rather than blocking the session. Every declaration still keeps
its slot with the reason it contributed nothing, so invalid, disabled and
unreachable servers stay distinguishable for the management menu.

`rmcp` is added without its default features, since a client needs only
`client` and the stdio transport; the defaults would pull in the whole
server-side handler stack.

Results are mapped onto the chain's own result type: text passes through,
while images, audio and resources become markers naming what was dropped,
so the model learns a payload exists without the chain pretending to carry
it. A result the server marked as an error becomes a failed call, keeping a
server-side failure from reading as success.

Calls use the single-request entry point, so SEP-2322 `input_required`
rounds are not driven and a server that needs them surfaces as a failed
call instead of hanging.

The HTTP transport is declared and validated but still rejected at connect
time with its own message; its client follows separately. No CLI entry
point calls `connect_all` yet, so a workspace that declares a server still
behaves as before.

Verified with `cargo fmt`, `cargo clippy --workspace --all-targets -- -D
warnings` and `cargo test --workspace`. Tests cover the result mapping;
spawning a server and the handshake are not covered yet.
The delivery section told contributors not to run the full `./scripts/ci.*`
locally, but never named `cargo test --workspace`. That gap left the heaviest
local command unmentioned, so a full workspace run — which builds and
executes every test binary — still looked sanctioned.

Local verification is now limited to the crates a change actually touches,
with the full suite left to PR CI. The coverage bullet is reworded to match:
reading the cached report stays the default, and a local recount goes through
`cargo llvm-cov` rather than a separate full test run followed by a second
pass over the results.

The coverage thresholds are unchanged; only where they are measured moved.
An `http` server could be declared and passed validation, but connecting to
it was refused outright: the transport was not wired up, so the entry only
ever produced a failure naming the transport as unsupported. A remote server
was therefore impossible to use.

The HTTP client is now wired up behind the same connection path as stdio. The
handshake, the tool discovery call and the conversion of what a server reports
are shared by both transports, so a change to how a server's tools are read
cannot apply to one and miss the other.

`rmcp` gains its reqwest-backed HTTP client along with the `reqwest` feature,
which selects rustls for TLS; rustls keeps the build free of a system TLS
library dependency on every platform. This pulls a substantial dependency
chain (reqwest, hyper, rustls, tower) into the crate for the first time, which
is the bulk of the lockfile change.

The error message for an unsupported transport is removed along with its
two locale entries: no path can produce it any more, and keeping an unreachable
string in the catalogue invites it to be mistranslated later.

Tool results, timeouts, per-server failure recording and the approval gate are
unchanged; only how the connection is established differs.

Verified with `cargo fmt`, `cargo clippy --workspace --all-targets -- -D
warnings`, `cargo check -p manualaid-core --all-targets` and
`cargo test -p manualaid-core --lib` (374 passed). The HTTP path itself is not
yet covered by a test that reaches a server.
@SunYanbox

SunYanbox commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author
image

它为什么出现在你的项目里

这个依赖链不是你直接引入的,而是被 reqwest 悄悄拉进来的:

reqwestrustlsaws-lc-rsaws-lc-sys

rustls 从 0.22 版本开始支持两种密码学后端:ringaws-lc-rs。而 reqwest 从 0.13 版本起,默认把 rustls 的后端从 ring 换成了 aws-lc-rs。给 rmcp 加的 reqwest feature,正是顺着这条链把 aws-lc-sys 带进来的。

一个值得考虑的替代

如果你想让编译负担回到可控范围,可以把 reqwest 的 TLS 后端换回 ring。有人在 Selenium 项目里就这么做过:改用 rustls-no-provider 再显式启用 ring整条 aws-lc-rs → aws-lc-sys 依赖链就消失了

代价是放弃后量子算法支持——对本地 MCP 客户端的 HTTP 传输来说,这个取舍大概率是划算的。


先更正我上一轮的错误:我说"把 reqwest 改成基于 ring 的版本,改动只涉及一行",这个判断是错的。查了源码后确认:

  • reqwest 0.13.4 只提供三个 TLS feature:rustls(→ aws-lc-rs)、rustls-no-providernative-tls
  • 没有 ring 选项。 我全文检索了 ring,没有任何 feature 命中。
  • 也就是说,reqwest 0.13 这一代已经把 ring 后端去掉了,不能直接换成 ring"。

@SunYanbox

Copy link
Copy Markdown
Owner Author

To Continue Development

Install manualaid-cli and run it in ur project folder.

Use System prompts with this compress text. The compress text should be placed in /compress-fence.
Then download the files and save them to the path it provides. After that, start the session and
reference the todo file or subject.

Compress Session

核心诉求与意图

  • 初始目标:为 ManualAid-Rust 实现 MCP(Model Context Protocol)支持,使用 rmcp crate,已连接 MCP 服务器的工具以 mcp_<MCP name>_<Tool name> 注入上下文。
  • 工作流:先计划(写计划文件并等用户批准),批准后再实施;每一阶段完成后应当提交一次
  • 六项已批准决策(附理由):
    1. 配置形态:[[mcp.servers]] 数组表(用户选定数组表形式)。
    2. 传输范围:MVP 做 stdio + HTTP(rmcp 3.3.0 无独立 SSE feature,远端即 Streamable HTTP)。
    3. 启用粒度:只做服务器级。
    4. 审批策略:一律审批(服务器工具描述为不可信输入)。
    5. 菜单管理:需要。
    6. 工具描述长度:MVP 不设上限。
  • 实施中插入的要求:"可以实现一个和ws、cli一样的t_fmt;未来我们计划提取这些重复代码的,但是现在可以先实现到core里"——已在 core 新增 text::t_fmt,不顺带重构 ws/cli。
  • 提交要求:"在提交前需要基于origin/main创建新分支"、"文件比较多,应当分批提交(文件内更改不可拆分,分批允许中途不可编译)"——已全部执行。
  • 测试流程纠正(本轮新增,重要):
    • 用户明确指示:"把 cargo test --workspace 从本地流程里彻底移除",理由:"这一轮开发你都跑了十几次全量测试了;应当仅跑目标测试"
    • 用户指出应先缓存输出再检索:"你不先缓存再搜索,一出错就得重新跑测试!"
    • 据此已在 AGENTS.md 写入两条明文规则(见下)。

关键技术背景

  • 项目:ManualAid-Rust,Rust workspace,4 crate:i18nmanualaid-coremanualaid-climanualaid-ws
  • 平台:Windows,shell 为 cmd.exe。工作区根:E:\ProjectRust\ManualAid-Rust
  • 当前分支:feat/mcp-support,基 origin/main40a6a76),跟踪 origin/main
  • core.autocrlf=true,无 .gitattributes;仓库内既有文件为 CRLF,新建的 mcp/*.rstext.rstemplate.rsmcp.*.toml 为 LF(git add 打印 "LF will be replaced by CRLF" 警告,正常)。
  • 核心架构矛盾ToolKind 是编译期静态枚举(Copy&'static str),MCP 工具是运行时动态字符串名。解法:新增平行运行时层 manualaid-core::mcpToolKind 完全不动。
  • 工具链数据流:文本 → ParserFormatRegistry + EnabledToolSet)→ ParsedToolCallExecutor(路由→校验→还原掩码→审计→run→后处理)→ ToolResult
  • rmcp 3.3.0 事实(已读源码确认):
    • 依赖声明:rmcp = { version = "3.3.0", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "reqwest"] }
    • --no-default-features 关闭默认的 server/macros/schemars/base64/uuid/transport-async-rw,因客户端不需要服务端 handler 栈。
    • reqwest feature 注入 rustls TLS(避免系统 TLS 库依赖)。
    • 关键 API:impl<H: ClientHandler> Service<RoleClient> for Hhandler/client.rs:14);impl ClientHandler for ()serve_client((), transport)RunningService::peer() -> &Peer<R>service.rs:1070);Peer::list_all_tools() -> Result<Vec<Tool>, ServiceError>(自动翻页);Peer::call_tool(CallToolRequestParams) -> Result<CallToolResult, ServiceError>(经 method! 宏,直接返回最终结果);RunningService::close_with_timeout(Duration)TokioChildProcess::builder(cmd).stderr(...).spawn()StreamableHttpClientTransport::from_uri(uri)transport/common/reqwest/streamable_http_client.rs:358,免自建 reqwest client)。
    • CallToolResult{content: Vec<ContentBlock>, structured_content, is_error: Option<bool>, ...},有 inherent success(Vec<ContentBlock>) / error(Vec<ContentBlock>),故测试可直接构造。
    • ContentBlock 变体:Text(TextContent{text}) / Image(ImageContent{data,mime_type}) / Audio(AudioContent{data,mime_type}) / Resource(EmbeddedResource{resource}) / ResourceLink(Resource);是 #[non_exhaustive]
    • ResourceContents#[non_exhaustive],变体 TextResourceContents{uri,mime_type,text} / BlobResourceContents{uri,mime_type,blob}
    • CallToolRequestParams{name, arguments: Option<JsonObject>, ...}#[non_exhaustive],只能 new(name) + 字段赋值,不能字面量构造。
    • ClientInfo = InitializeRequestParamsmodel.rs:1123),有 Default
    • process-wrap 10.0.0impl From<Command> for CommandWrapgeneric_wrap.rs:320)。
  • serde_json 未启用 preserve_order,schema properties 遍历为字母序(已在 mcp/schema.rs 模块文档记录该取舍)。
  • i18n:rust_i18n::i18n!("locales", fallback = ["zh-CN", "en"]) 按目录通配加载,新增 mcp.*.toml 无需注册。locale 共 12 文件 = 6 组 × 中英(audit/cli/common/mcp/prompts/tools)。
  • t_fmt 语义(cli lib.rs:124、ws prompt.rs:717、core text.rs):i18n::t_str(key) 后循环 template.replace(&format!("%{{{name}}}"), value);未提供的占位符原样保留。
  • 暴露名规则:format!("mcp_{server}_{tool}")不 split 反解,按完整暴露名匹配(resolve_tool==)。
  • 超时(实现内常量,不做配置项):CONNECT_TIMEOUT = 10s(启动+握手+发现共用)、CALL_TIMEOUT = 30sSHUTDOWN_TIMEOUT = 3s
  • 协议覆盖取舍:用 Peer::call_tool(直接返回 CallToolResult)而非 call_tool_once(返回需分支处理的 CallToolResponse);SEP-2322 input_required 轮次不驱动,服务器需要时会呈现为失败调用而非挂起。已写入 connect.rs 模块文档。
  • Windows 命令解析:tokio::process::Command 不查 PATHEXT,配置写 npx 无效、须写 npx.cmd。已写入 connect.rs 模块文档,未自行新增 which 依赖(which crate 不在依赖树中)。

涉及的文件与代码

已提交(8 个提交,基 40a6a76

  • 6096087 feat(core): add the MCP runtime layer for dynamically discovered tools(14 files, +1133)
    • crates/manualaid-core/src/mcp/config.rs(127 行):McpTransportKind{Stdio,Http}#[serde(rename_all="lowercase")]label()/from_label() 未知返回 None);McpServerConfig{name, transport, command: Option<String>, args, env, url, enabled}enabled 默认 true);validate() -> Result<(), String>
    • crates/manualaid-core/src/mcp/tool.rs(59 行):exposed_name(server, tool)McpParam{name, kind, required, description}McpTool{server_name, tool_name, exposed_name, description, params}
    • crates/manualaid-core/src/mcp/schema.rs(96 行):parse_tool(server_name, tool_name, description, input_schema: Option<&Value>) -> McpToolparse_params(Option<&Value>) -> Vec<McpParam>;type 映射 integer/number/boolean/array/object,其余回退 "string"
    • crates/manualaid-core/src/mcp/store.rsstatic STORE: RwLock<McpStore>read_store/write_storefile_io::warn_poisoned_lock("MCP_STORE")pub(crate) ServerState{config, tools, error: Option<String>, client: Option<McpClient>}pub McpServerStatus{name, transport, enabled, tool_count, error}pub(crate) install(Vec<ServerState>) 整体替换并按 exposed_name 去重(首个服务器获胜);all_tools()/enabled_tools()/resolve_tool()/server_status()pub(crate) peer_for(server_name) -> Option<Peer<RoleClient>>pub(crate) take_clients() -> Vec<McpClient>#[doc(hidden)] reset()
    • crates/manualaid-core/src/mcp/mod.rs(113 行):connect_all/shutdown/call_tool/reset
    • crates/manualaid-core/src/mcp/tests/mod.rsstatic STORE_LOCK: Mutex<()>with_store_lockblock_onstdio()/http()/tool()/state()/install_one/install_many;注释说明「一律经 super::store::install 限定以避开同名测试子模块」)、config.rsschema.rsstore.rs
    • crates/manualaid-core/src/text.rs(28 行):pub fn t_fmt(key, args: &[(&str, &str)]) -> Stringtext_tests.rs(31 行)4 个测试。
    • crates/i18n/locales/mcp.en.toml / mcp.zh-CN.toml
    • crates/manualaid-core/src/lib.rs:加 pub mod mcp;pub mod text;
  • 49dc02c refactor(core): render call templates from a tool view instead of ToolKind(12 files, +149/−30)
    • crates/manualaid-core/src/parser/template.rs(88 行):ToolTemplateParam<'a>{name, kind, required}ToolTemplate<'a>{name, params: Vec<ToolTemplateParam<'a>>}from_kind(ToolKind) -> ToolTemplate<'static>from_mcp(&McpTool) -> ToolTemplate<'_>params 必须是 VecToolKind::parameters() 返回拥有所有权的 Vec)。
    • crates/manualaid-core/src/parser/traits.rsfn tool_call_template(&self, tool: &ToolTemplate<'_>) -> String;
  • 0e35b2c feat(core): admit MCP tools into the parser set and the prompt(3 files, +116/−31)
    • crates/manualaid-core/src/parser/tool_set.rs(整文件重写):EnabledToolSet{by_name: HashMap<&'static str, ToolKind>, params: HashMap<&'static str, HashSet<&'static str>>, mcp: HashMap<String, HashSet<String>>}from_names_and_mcp(names: &[String], mcp_tools: &[McpTool])
    • crates/manualaid-ws/src/prompt.rsrender_tools_list 新增 MCP 循环。
  • e1af8db feat(core): route and approve MCP calls in the executor(3 files, +167/−56)
    • crates/manualaid-core/src/executor.rs(整文件重写,535 行):const MCP_APPROVAL_PARAM: &str = "mcp";(:24);struct ParamSpec<'a>{name, kind, required}builtin_specs(ToolKind)/mcp_specs(&McpTool)(:322);validate_params/coerce_paramsexecuteToolKind::from_name 失败后 crate::mcp::resolve_toolexecute_mcp(:118-119);async fn execute_mcp(&self, call: &ParsedToolCall, tool: &McpTool)(:178),在 :191 调 crate::mcp::call_tool(&tool.exposed_name, &restored_params).awaitaudit() 对 MCP 返回 vec![("mcp".to_string(), AuditDecision::NeedsApproval(t_fmt("mcp.approval.required", ...)))](:217-221,安全关键);pre_check 对 MCP 返回 None
  • 05d1d90 feat(ws): declare MCP servers in the config file(4 files, +586/−4)
    • crates/manualaid-ws/src/config.rs(978 → 1521 行):ConfigIssueKind::InvalidMcpServerConfigFile.mcp: McpSectionMcpSection{servers: Vec<McpServerEntry>}McpServerEntry{name, transport: String, command, args, env, url, enabled} + 手写 DefaultConfig.mcp_servers: Vec<McpServerConfig>merge_mcp_servers整体覆盖:项目非空用项目,否则全局);enabled_tool_names() 追加 manualaid_core::mcp::enabled_tools() 的暴露名;save_mcp_servers(project_root, &[McpServerEntry])toml_edit::ArrayOfTables);save_project 保持不写 [mcp]
  • 11dfe67 feat(core): connect MCP servers over stdio and call their tools(10 files, +698/−27)
    • crates/manualaid-core/Cargo.toml:新增 rmcp = { version = "3.3.0", default-features = false, features = ["client", "transport-child-process"] }
    • crates/manualaid-core/src/mcp/connect.rs新增,284 行):pub(crate) struct McpClient{peer: Peer<RoleClient>, service: RunningService<RoleClient, ()>} + peer()(克隆句柄)+ async close()close_with_timeout(SHUTDOWN_TIMEOUT));pub(crate) async fn open(config) -> Result<(McpClient, Vec<McpTool>), String>async fn open_stdio(config)TokioChildProcess::builder(child).stderr(Stdio::null()).spawn());pub(crate) async fn call(peer, tool, params) -> ToolResult(30s 超时,失败分支 mcp.error.call_timeout / call_failed);pub(crate) fn to_tool_result(exposed_name, result) -> ToolResultfn render_content(&[ContentBlock]) -> String\n 拼接);fn render_block(&ContentBlock) -> String(Text 原样、Image/Audio [image: mime]/[audio: mime]、Resource 取文本或 [embedded resource]、ResourceLink [resource: uri]_ => "[unsupported content]");fn connect_failed/fn connect_timeout
    • crates/manualaid-core/src/mcp/tests/connect.rs新增,79 行):6 个映射测试(text 原样、is_error→failure、二进制标记、嵌入文本资源、多块换行拼接、空结果)。
    • crates/manualaid-core/src/mcp/mod.rs(重写,314 行):mod connect;connect_all 逐服务器 validate → 禁用则跳过 → connect::open,失败记录 ServerState.errorshutdownstore::take_clients() 后逐个 close().awaitstore::reset()call_toolresolve_toolstore::peer_forconnect::call
    • crates/i18n/locales/mcp.{en,zh-CN}.toml:各 13 行,[mcp.error]unknown_toolnot_connectedconnect_failedconnect_timeoutcall_failedcall_timeout[mcp.approval]required。中英严格同构。
  • e146cb9 docs(agents): require local test runs to target a single crate(1 file, +2/−1)
    • AGENTS.md:新增条目 「本地只跑目标测试:本地禁止运行 cargo test --workspace,也不运行任何会构建并执行全部测试二进制的等价命令;全量测试由 PR 的 CI 承担。需要本地验证时,只运行与本次改动相关的目标测试,例如 cargo test -p <crate>,或按模块、测试名进一步过滤。改动跨多个 crate 且确实需要一并验证时,逐个 -p 指定受影响的 crate,而不是放开到整个工作区。」;把原「全量覆盖率测试(cargo llvm-cov…)优先于仅运行全量测试」改为 「覆盖率统计由 CI 承担。本地需要查看覆盖率时读取缓存的 coverage_with_lines.txt;确需本地重新统计时使用 cargo llvm-cov…不要先用 cargo test --workspace 跑一遍再单独统计。」
  • c99c88b feat(core): connect MCP servers over streamable HTTP(5 files, +1066/−24)
    • crates/manualaid-core/Cargo.toml:rmcp features 扩为 ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "reqwest"]
    • crates/manualaid-core/src/mcp/connect.rs(314 行):use rmcp::transport::{StreamableHttpClientTransport, TokioChildProcess};openHttp 分支改为 open_http(config).await;新增 async fn open_http(config)StreamableHttpClientTransport::from_uri(url.to_string()));抽出泛型 async fn finish_handshake<T, E, A>(config, transport) where T: rmcp::transport::IntoTransport<RoleClient, E, A>, E: std::error::Error + Send + Sync + 'static(握手+发现+转换共用)。
    • crates/i18n/locales/mcp.{en,zh-CN}.toml:各删除 transport_unsupported 键(无使用点)。
    • Cargo.lock:+1040 行(reqwest 0.13.4、hyper、hyper-util、hyper-rustls、rustls、tower、tower-http、tokio-rustls、aws-lc-sys 等)。

未改动(相关)

  • crates/manualaid-cli/src/commands/loop_cli/mod.rs:186.set_enabled_tools(&config.enabled_tool_names())——尚未调 connect_all
  • crates/manualaid-cli/src/commands/copy.rs:89:同上,尚未同步初始化 MCP
  • crates/manualaid-cli/src/commands/loop_cli/preview.rsapproval_preview 需加 MCP 分支(未改)。
  • crates/manualaid-cli/src/commands/loop_cli/approval.rsexecute_round_with_approval 收集 NeedsApproval 进审批队列并逐条 decide;MCP「一律审批」的实际拦截点(未改)。
  • crates/manualaid-cli/src/commands/loop_cli/handlers.rs:611config.enabled_tool_names().join(", ")(工具列表展示,未改)。
  • docs/zh-cn/规格/MCP规范.md:既有 MCP 协议整理。
  • .ManualAid/plans/实现MCP支持.md(522 行):已批准的计划文件。4.6 节(336–395 行)配置规范;4.7 i18n;4.8 CLI 集成;4.9 菜单管理;4.10 退出清理;第 497 行起阶段划分。
  • .ManualAid/todos/implement-mcp-support.jsonlinked_plan: "实现MCP支持",进度仍记早期状态(未更新)。
  • 未纳入提交(与 MCP 无关,保持未跟踪):TODO.mdrun-setup-repeat.ps1TEST.txt 已不在未跟踪列表)。

问题与解决

  • 动态工具名 vs 静态 ToolKind:新增平行 mcp 层,ToolKind 不动;EnabledToolSet 扩展容纳动态名;解析器零改动;executor 在 from_name 失败后旁路 MCP。
  • rmcp 3.3.0 无独立 SSE feature:实现为 Streamable HTTP(StreamableHttpClientTransport::from_uri)。
  • ServerState.error never read 触发 dead_code:不用 #[allow],新增 McpServerStatus + server_status() 查询 API。
  • edit 工具多行替换因 CRLF 失败old_string/new_string 改用 \r\n 显式分隔后成功;单行编辑不受影响。
  • 测试子模块与父模块同名mcp/tests/store.rs vs mcp/store):在 tests/mod.rs 注释说明并一律写 super::store::install
  • 全局 static STORE 并行测试互相踩踏:用 static STORE_LOCK: Mutex<()> 串行化,async 调用用 block_on 在锁内驱动。
  • ServerState 新增 client 字段导致测试字面量缺字段(E0063)tests/mod.rsstate()tests/store.rs:101 各补 client: None
  • tests/connect.rs 缺导入(E0425/E0433)+ use super::* 未使用:改为 use rmcp::model::{AudioContent, CallToolResult, ContentBlock, ImageContent, ResourceContents, TextContent}; + use crate::mcp::connect::to_tool_result;。教训:supermcp::tests::connect 中指向 mcp::tests,跨模块须用 crate:: 绝对路径。
  • connect_all_installs_every_declared_server 失败tests/store.rs:179,断言 status[2].error == None):根因是测试写于 connect_all 还是占位实现时;接入真实连接后 dummy-server 不存在必然失败。改为断言 status[2].error.is_some(),并更新注释说明三种「未贡献工具」情形可区分(校验失败 / 禁用不尝试 / 连接失败)。
  • cmd.exe 下调用 coreutils 路径失败:教训——优先用内置 findstr 替代 coreutils grep
  • git status --cached 不是有效选项:改用 git diff --cached --name-only
  • rmcp 源码整体读取超长被截断service/client.rs 9.4 万字符):教训——先用 findstr /n 定位行号,再按 offset+limit 区间读。
  • 用户反馈(中性化转述)
    • 新增要求 core 版 t_fmt,并说明重复代码提取留待将来。
    • 要求阶段 2 完成后提交,提交前基于 origin/main 建新分支,因文件多需分批提交、允许中间态不可编译。
    • 指出应先缓存命令输出再检索("你不先缓存再搜索,一出错就得重新跑测试")——已改为 > 文件 2>&1 & findstr 文件 模式。
    • 指出本地反复跑全量测试超出约定,要求彻底移除 cargo test --workspace
  • 「57s 只跑一轮 core 测试」的核算(用户要求排除编译时间):
    • 缓存文件 core4.txt 中仅两行带秒数:Finished \test` profile ... in 51.54s(编译+链接)与 test result: ok. 374 passed ... finished in 0.12s`(测试执行)。
    • 合计 51.66s;shell 报 57s,差约 5.3s 为 cargo 进程启动、依赖图解析、构建目录加锁与 I/O。
    • 真正的测试执行占 0.2%;51.54s 花在重新编译 24 个 crate(reqwesthyperhyper-utilhyper-rustlstowertower-httptokio-rustlsrustls 等),根源是 cargo feature 全局联合:rmcp 的 feature 集合一变,所有下游(i18nmanualaid-core)连带过期重编。-j 2 限制的是并行度而非重编需求,反而可能拉长总时长。
    • 结论:同命令第二次跑会复用 test binary,仅剩 0.12s 执行 + cargo 启动开销。

待办事项

  • [完成] 阶段 1:mcp 模块骨架 + core 版 t_fmt + mcp.*.toml6096087)。
  • [完成] 阶段 2:工具链接入(ToolTemplateEnabledToolSet、解析器签名、registry、executor 旁路与 MCP 审批、ws 提示词 MCP 段、ws [mcp] 配置节、cli 警告渲染)——拆为 49dc02c/0e35b2c/e1af8db/05d1d90
  • [完成] 阶段 3:rmcp 接入 + connect.rs stdio 分支 + 结果转换 + 超时(11dfe67)。
  • [完成] 阶段 4:HTTP 传输(StreamableHttpClientTransport::from_uri)与共用握手(c99c88b)。
  • [完成] AGENTS.md 本地测试规则(e146cb9)。
  • [待办] CLI 集成(计划 4.8 节,我按计划推进时漏掉了,应最先做)loop_cli/mod.rsreload_skills_with_home 后、set_enabled_tools 前调 connect_all(&config.mcp_servers).awaitcopy.rs 走同一初始化路径(同步入口用 block_on);preview.rsapproval_preview 加 MCP 分支;loop_main_at 正常返回前调 mcp::shutdown().await
  • [待办] 阶段 5:菜单管理与 i18n 四份文案同步(增删启停、save_mcp_servers 接入 loop_cli/config.rsconfig_menu / menu.rsMenuAction;沿用 read_line/render_menu/t_fmt/push_test_input)。
  • [待办] 阶段 6:测试与覆盖率(mcp/connect.rs 的 stdio 假服务器端到端集成测试;覆盖率单文件 ≥85%、核心 ≥95%、总体三项 ≥80%)。已知覆盖缺口:Config::enabled_tool_names() 的 MCP 追加分支需由 core 侧覆盖(store::installpub(crate),ws 侧无法注入工具)。
  • [待办] 阶段 7:更新文档与 CHANGELOG(中英四份,[Unreleased] 下)。
  • [待办] 更新 .ManualAid/todos/implement-mcp-support.json 的进度(阶段 1–4 已完成,尚未更新)。
  • [待办] 最终汇报时须说明:计划 4.8 节的 CLI 集成被漏在阶段 3/4 之外。
  • [已否决] 本地运行 cargo test --workspace(用户明确禁止,已写入 AGENTS.md)。
  • 无关未完成 TODO(不要主动推进):refactor-todo-planning-into-todo_write-tool(93%)。

当前进展

  • 阶段 1–4 全部完成、验证并提交;AGENTS.md 测试规则已提交。分支 feat/mcp-support 上共 8 个提交:609608749dc02c0e35b2ce1af8db05d1d9011dfe67e146cb9c99c88b,基 40a6a76
  • 硬数据(最近一次验证,阶段 4 改动后):
    • cargo fmt --all:通过(无输出差异)。
    • cargo check -p manualaid-core --all-targetsFinished dev profile ... in 44.33s0 error / 0 warning
    • cargo clippy --workspace --all-targets -- -D warningsFinished dev profile ... in 4.76s无任何输出(通过)。
    • cargo test -p manualaid-core --lib -j 2test result: ok. 374 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
    • cargo test --workspace 未完成,阶段 4 从未跑过全工作区测试——按新规则也不应再跑,全量交给 PR CI。
  • 阶段 3 提交前的全量数据(历史参考):core lib 374 passed;cli lib 350;api 50;commands 71;args 16;cli 20;loop_flow 10;style 9;restore 5;debug_shell 4;i18n lib 3 + test_changelog 5 + test_i18n_lib 5;mask 2;skill 2;ws 53 + 5 + 10 + 30,均 0 failed。
  • 中间提交 49dc02c/0e35b2c/05d1d90/11dfe67/c99c88b 单独 checkout 不可编译(已逐条写入各自提交正文)。
  • Git 工作区:仅剩 TODO.mdrun-setup-repeat.ps1 两个未跟踪文件。
  • 尚未运行 cargo llvm-covcoverage_with_lines.txt 未更新。
  • 临时输出缓存位于 E:\ProjectRust\ManualAid-Rust\.ManualAid\temp\core-test.txtcore4.txtclippy.txtclippy4.txtfmt.txtcheck4.txtws-test.txtws-test4.txt

下一步

  • 已向用户提出四个选项,等待选择:(A) 先补 CLI 集成(我推荐)、(B) 直接进入阶段 5 菜单管理、(C) 先做阶段 6 测试与覆盖率、(D) 其他。同时询问是否顺带更新 .ManualAid/todos/implement-mcp-support.json 进度。
  • 用户若选 A:按计划 4.8 节补 CLI 集成,注意 connect_all 是 async 而 copy.rs 可能是同步入口,需要 block_on 或改 async;退出路径要调 mcp::shutdown().await

关键上下文

  • 用户偏好与硬性约束
    • [禁区] 本地禁止运行 cargo test --workspace 及任何等价的全量测试命令;只跑与改动相关的目标测试(cargo test -p <crate>),跨 crate 时逐个 -p
    • [禁区] 覆盖率统计归 CI;本地只读缓存的 coverage_with_lines.txt,确需重算用 cargo llvm-cov,不要先跑全量测试。
    • 提交前本地检查(含代码或 locale 变更时):cargo fmtcargo clippy -- -D warningscargo check;纯文档变更免跑。
    • 跑命令时先把输出重定向到文件> 文件 2>&1)再 findstr 检索,避免出错后重跑。
    • 每阶段完成后提交;提交规范遵循约定式提交、英文;正文承载动机与影响边界,不复述 diff;中途不可编译的边界要写进正文。
    • 注释解释「为什么」;模块/函数文档双语(英文在前中文在后);成对中英文件严格同构;CHANGELOG 四份同步且 [Unreleased] 标题必须保留。
    • 覆盖率硬约束:单文件 ≥85%、核心模块 ≥95%、总体 Function/Line/Region 三项 ≥80%。
    • 依赖通过 cargo add 添加且不指定版本号;多 crate 共同依赖提取至 [workspace.dependencies];Node 侧用 pnpm。
    • 测试组织:<name>.rs + #[path = "<name>_tests.rs"] mod tests;,或 <name>/tests/mod.rs + 子模块(mcp 用后者)。
  • 提交/PR 用 skill:commit-objectivelypr-objectively(本会话 commit-objectively 已加载,不再重复调用)。
  • 内部 API 细节:ToolResult::success(tool_name, output, read_only)(:454)/ failure(tool_name, message)(:475)/ with_params_summary(String)(:492);ToolResult 定义在 crates/manualaid-core/src/tools/tool.rs:406params_summary_of(Option<ToolKind>, &IndexMap)None 走完整 JSON 截断(PARAMS_SUMMARY_MAX_CHARS = 75),MCP 复用该路径。AuditDecision{Allowed, Denied(String), NeedsApproval(String)}CoreError 变体:Io/Config/NotFound/PermissionDenied/Parse/InvalidPath/Filter/Execution{command,exit_code,stderr}/OtherCoreResult<T> = Result<T, CoreError>
  • 退出清理策略:依赖 rmcp 子进程句柄在 drop 时终止子进程;std::process::exit 路径可能不触发 drop,MVP 接受由操作系统回收;shutdown()take_clients() + close_with_timeout(3s) 显式关闭。
  • 悬而未决:是否需要为 mcp/connect.rs 的真实进程/网络分支申请覆盖率豁免(计划 7.3 允许,按 user_dir.rscopy.rs 既有惯例);菜单管理是否需要「重连」操作(计划 4.9 列出);是否把 mcp.error.not_connected 保留(当前仍在 call_tool 无 peer 分支使用)。
  • 对计划的偏离(需在最终汇报中说明):ToolTemplate.paramsVec 而非切片;connect_all/shutdown 返回 CoreResult<()> 但恒为 Ok,逐服务器失败记录在 ServerState.errorMcpServerEntry.transportString 而非 McpTransportKind(使未知标签可被报告);rmcp 分两阶段加 feature(阶段 3 只加 stdio、阶段 4 补 HTTP),而非计划 10.2 节一次性加齐;用 Peer::call_tool 而非 call_tool_once(放弃 SEP-2322 MRTR 支持)。

Put on .ManualAid/plans/实现MCP支持.md: 实现MCP支持.md

Put on .ManualAid/todos/implement-mcp-support.json: implement-mcp-support.json

@SunYanbox

Copy link
Copy Markdown
Owner Author

直接编译origin/main是仅1.41 GB的target,而在feat/mcp-support 分支(在我 cargo clean 前)的target是124.45 GB:

Before clean (origin/feat/mcp-support: c99c88b)

TOTAL: 124.45 GB (270884 files)
debug 116.75 GB 254272 files
llvm-cov-target 3.71 GB 6191 files
codex-test 3.35 GB 7758 files
release 0.64 GB 1935 files
doc 0.01 GB 723 files
flycheck0 0.00 GB 2 files
test-complete.log 0.00 GB 1 files
.rustc_info.json 0.00 GB 1 files
.rustdoc_fingerprint.json 0.00 GB 1 files

After clean (origin/main: 40a6a76)

TOTAL: 1.42 GB (2555 files)
debug 1.42 GB 2553 files
.rustc_info.json 0.00 GB 1 files
CACHEDIR.TAG 0.00 GB 1 files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant