Nushell MCP 的 evaluate 工具:执行语义、响应结构与 Bash 命令对照全解析
本篇指南围绕 Nushell 仓库中 crates/nu-mcp/src/evaluate_tool.md 这份文档展开——它是 Nushell 内置 MCP(Model Context Protocol)服务器中 evaluate 工具的官方描述文本,会被直接内嵌进 MCP 协议的 tools 列表,供 LLM 客户端(Claude、Cursor 等)作为"如何使用这个工具"的第一手说明书。读完后你能掌握:evaluate 调用的成功/失败响应格式、长任务自动后台化的 NU_MCP_PROMOTE_AFTER 机制、Bash 与 Nushell 命令的完整对照表、polars 插件的接入姿势,以及跨调用状态持久化的正确用法,并能在 evaluation.rs 源码中找到每一处行为背后的实现证据。
evaluate 工具的定位:MCP 服务器的三个工具之一
Nushell 通过 mcp 特性编译出 MCP 服务器支持(见 nu-mcp 的 README):
cargo build --features mcp
nu --mcp # stdio 传输(默认)
nu --mcp --mcp-transport http --mcp-port 3000 # Streamable HTTP,默认端口 8080
在 server.rs 中,服务器用 #[tool_router] 宏注册了三个工具:list_commands(按名称/描述/搜索词查找原生命令)、command_help(查看命令签名与参数)、以及核心的 evaluate(执行 Nushell 源码)。evaluate_tool.md 正是 evaluate 工具的描述来源:
// crates/nu-mcp/src/server.rs
#[doc = include_str!("evaluate_tool.md")]
#[tool]
async fn evaluate(
&self,
ctx: RequestContext<RoleServer>,
Parameters(NuSourceRequest { input }): Parameters<NuSourceRequest>,
) -> CallToolResult {
self.evaluator.eval_async(&input, ctx.ct).await
}
include_str! 意味着这份 Markdown 在编译期就被烙进二进制,MCP 客户端调用 tools/list 时看到的就是这份原文。evaluate 的输入 schema 只有一个 input 字段("The Nushell source code to evaluate"),这一点被测试 evaluate_tool_input_schema_exposes_only_input_property 明确锁定:每次调用无法传超时参数,长任务控制只能通过 NU_MCP_PROMOTE_AFTER 环境变量完成。
成功响应:带元数据的 NUON 记录
evaluate_tool.md 第一句就定下契约:
Successful evaluations return a structured NUON record string with metadata fields including
cwd,history_index,timestamp, and eitheroutputornote.
这个契约在 evaluation.rs 的 eval_on_state 中逐字段实现:
// crates/nu-mcp/src/evaluation.rs(简化)
let mut response_record = nu_protocol::record! {
"cwd" => Value::string(cwd, block_span),
"history_index" => Value::int(history_index as i64, block_span),
"timestamp" => Value::date(timestamp_value, block_span),
};
if truncated {
let note = format!("output truncated, full result in $history.{history_index}");
response_record.push("note", Value::string(note, block_span));
} else {
response_record.push("output", output_value);
}
各字段含义:
| 字段 | 说明 |
|---|---|
cwd |
命令执行之后的工作目录(外部命令可能改变目录) |
history_index |
本次结果在 $history 环形缓冲区中的索引,是后续"回切"完整结果的唯一句柄 |
timestamp |
执行时刻的日期时间值 |
output |
命令输出(未超出内联大小时限的值,可为任意 NUON 结构化类型) |
note |
仅当输出被截断时出现,替代 output,提示完整结果在 $history.N 中 |
同一份响应会以两种形态回传给客户端:content 里是机器可读的 NUON 文本,structuredContent 里是等价 JSON(由 structured_json_from_value 生成)。test_evaluator_response_format 断言了四个字段齐全,evaluate_tool_computes_basic_expression 则验证了 evaluate("5 + 2") 的 output 为 7 且 structuredContent 中 output == 7。
失败响应:结构化 NUON 错误记录
文档第二段的契约是"evaluation fails 时,MCP 工具响应被标记为 error,content 包含一个结构化的 NUON 错误记录"。实现上,format_mcp_error 会把 miette 诊断错误拆成以下字段:
code:错误码,如nu::parser::unclosed_delimiter、nu::compile::前缀的编译错误、nu::shell::error的运行时错误;msg:短错误信息;severity:error/warning/advice;help、url:修复提示与文档链接(如有);labels:定位列表,每条含text(标签语义)、span(出错的源码片段)、line/column(从 1 开始的行列号)。
evaluation.rs#L21-L36 的函数注释完整描述了这一结构,并有三个测试分别钉住解析错误(test_evaluator_parse_error_nuon_format)、编译错误(test_evaluator_compile_error_nuon_format)与运行时错误(test_evaluator_runtime_error_nuon_format)的 NUON 格式,同时断言错误文本中不含 ANSI 转义码、不含 Rust Debug 格式的 Span { 残留。MCP 错误码层面,用户输入类错误(解析/编译)映射为 -32602 Invalid params,运行时错误映射为 -32603 Internal error(见 user_input_error 与 shell_error_to_mcp_error 的注释)。
大输出的正确姿势:不要限制输出,用 $history 回切
evaluate_tool.md 给出的第一条实操建议是:
Avoid commands that produce a large amount of output, and consider piping those outputs to files.
而 Nushell MCP 的设计哲学更进一步——配套的服务器级指令 instructions.md 明确要求 LLM 不要在"第一次运行"的管道里加 head/first N 之类的截断,因为:
- 每次评估的完整结果都会被存入
$history(环形缓冲区); - 工具响应可能因内联大小限被截断(此时出现
note字段),但数据不丢失; - 之后可以用
history_index精确回切,例如:
cargo build | complete # complete 把 { stdout, stderr, exit_code } 存为独立列
# 响应: { history_index: 7, note: "output truncated, full result in $history.7", ... }
$history.7.stderr | lines | where $it =~ '^error'
三个相关环境变量控制这一行为(实现见下文"超时与限流"一节):
$env.NU_MCP_OUTPUT_LIMIT = 50kb # 内联响应截断阈值(默认 10kb,0b 表示永不截断)
$env.NU_MCP_HISTORY_LIMIT = 200 # 历史条目上限(默认 100,环形语义,最旧条目被逐出)
注意 instructions.md 特别警告:取历史要用响应里给出的稳定索引 $history.7,而不是 $history | last——因为每次新评估都会先推入自己,last 会指到自己而非原始命令。
长任务:promote-after 超时与后台作业自动升级
evaluate_tool.md 中最关键的一段:
Calls that run longer than the promote-after timeout are auto-promoted to a background job; the tool then errors with a job id and you must
job recvto collect the result. The timeout defaults to 120s and can be overridden by setting$env.NU_MCP_PROMOTE_AFTER(a duration, e.g.10min) on the persistent stack. Bump it before a known long-running command so it stays synchronous; the setting persists across subsequent calls until you change it again.
源码中的对应事实链条:
- 默认值是硬编码的 120 秒,且注释解释了为什么是 2 分钟——早期 10 秒的默认值导致普通命令频繁被扔进后台、拖累了模型的使用体验(evaluation.rs#L180-L188):
/// How long an evaluation can run before being auto-promoted to a background
/// job. Overridden via the `NU_MCP_PROMOTE_AFTER` env var on the persistent
/// stack.
const DEFAULT_PROMOTE_AFTER: Duration = Duration::from_secs(120);
- 每次评估前读取当前环境值,解析失败(非 duration 类型)时回退到默认值而不是禁用升级,防止一个手误悄悄关掉保护(promote_timeout):
fn promote_timeout(engine_state: &EngineState, stack: &Stack) -> Duration {
stack
.get_env_var(engine_state, "NU_MCP_PROMOTE_AFTER")
.and_then(|v| v.as_duration().ok())
.and_then(|nanos| u64::try_from(nanos).ok())
.map(Duration::from_nanos)
.unwrap_or(DEFAULT_PROMOTE_AFTER)
}
- 执行模型是一个
tokio::select!三路竞争:客户端取消(ct.cancelled())、评估完成(result_rx)、超时(sleep(promote_after))。后两者触发promote_to_background_job:注册一个ThreadJob、向主线程邮箱(job 0)投递完整未截断的输出,然后工具立即以错误返回Operation promoted to background job (id: N). Use job list to see it and job recv to get the result.(promotion 实现):
Err(rmcp::ErrorData::internal_error(
format!(
"Operation promoted to background job (id: {}). \
Use `job list` to see it and `job recv` to get the result.",
job_id.get()
),
None,
))
测试 test_external_command_promotion_respects_promote_after_env 用 $env.NU_MCP_PROMOTE_AFTER = 100ms 把阈值压到 100 毫秒,再跑一个 sleep 0.2 的外部命令,断言响应 is_error == true 且文本含 "promoted to background job"。
由此形成的标准工作流:
# 1. 已知长命令前先放宽窗口,保持同步执行
$env.NU_MCP_PROMOTE_AFTER = 10min
# 2. 若仍被升级(或客户端取消),收集结果
job list # 查看是否还在运行
job recv # 阻塞直到结果(完整输出)
job recv --timeout 60sec # 有界等待
job kill 1 # 取消
一个容易踩坑的细节(instructions.md 的 Gotchas):job recv 只读当前作业邮箱、不带 id 参数;job send 必须带目标 id(主线程是 0);被升级的作业绕过 $history,其完整输出经 job recv 送达而不是存进历史。手动后台化则用 job spawn(文档建议的示例 job spawn { uvicorn main:app }),配套指令中还给出了投递到主线程的完整范式:
job spawn { ls | job send 0 }; job recv
job spawn { some-cmd | job send 0 }; job recv --timeout 5sec
Bash 命令对照表(完整继承)
evaluate_tool.md 的核心实用价值是一张 30 行的 Bash → Nushell 对照表,它是 LLM 客户端"翻译"Bash 习惯的主要依据。以下完整保留原文档内容:
| Bash Command | Nushell Command | Description |
|---|---|---|
mkdir -p <path> |
mkdir <path> |
Creates the given path, creating parents as necessary |
> <path> |
o> <path> |
Save command output to a file |
>> <path> |
o>> <path> |
Append command output to a file |
> /dev/null |
ignore |
Discard command output |
> /dev/null 2>&1 |
o+e>| ignore |
Discard command output, including stderr |
command 2>&1 |
command o+e>| ... |
Redirect stderr to stdout (use o+e> or out+err>) |
cmd1 | tee log.txt | cmd2 |
cmd1 | tee { save log.txt } | cmd2 |
Tee command output to a log file |
command | head -5 |
command | first 5 |
Limit the output to the first 5 rows of an internal command (see also last and skip) |
cat <path> |
open --raw <path> |
Display the contents of the given file |
cat <(<command1>) <(<command2>) |
[(command1), (command2)] | str join |
Concatenate the outputs of command1 and command2 |
cat <path> <(<command>) |
[(open --raw <path>), (command)] | str join |
Concatenate the contents of the given file and output of command |
for f in *.md; do echo $f; done |
ls *.md | each { $in.name } |
Iterate over a list and return results |
for i in $(seq 1 10); do echo $i; done |
for i in 1..10 { print $i } |
Iterate over a list and run a command on results |
cp <source> <dest> |
cp <source> <dest> |
Copy file to new location |
rm -rf <path> |
rm -r <path> |
Recursively removes the given path |
date -d <date> |
"<date>" | into datetime -f <format> |
Parse a date (format documentation) |
sed |
str replace |
Find and replace a pattern in a string |
grep <pattern> |
where $it =~ <substring> or find <substring> |
Filter strings that contain the substring |
command1 && command2 |
command1; command2 |
Run a command, and if it's successful run a second |
stat $(which git) |
stat ...(which git).path |
Use command output as argument for other command |
echo /tmp/$RANDOM |
$"/tmp/(random int)" |
Use command output in a string |
cargo b --jobs=$(nproc) |
cargo b $"--jobs=(sys cpu | length)" |
Use command output in an option |
echo $PATH |
$env.PATH (Non-Windows) or $env.Path (Windows) |
See the current path |
echo $? |
$env.LAST_EXIT_CODE |
See the exit status of the last executed command |
export |
$env |
List the current environment variables |
FOO=BAR ./bin |
FOO=BAR ./bin |
Update environment for a command |
echo $FOO |
$env.FOO |
Use environment variables |
echo ${FOO:-fallback} |
$env.FOO? | default "ABC" |
Use a fallback in place of an unset variable |
type FOO |
which FOO |
Display information about a command (builtin, alias, or executable) |
\ |
( <command> ) |
A command can span multiple lines when wrapped with ( and ) |
几个值得注意的语义差异(原文档隐含、值得展开):
&&与;:Nushell 管道式cmd1; cmd2中,第二条命令只在第一条成功时执行(非零退出码会短路),这正是它能替代&&的原因。- stderr 合并:Nushell 用
o+e>|(或等价的out+err>)显式把标准错误并入标准输出,而不是 Bash 的2>&1文件描述符技巧。 stat例子展示了"展开":...(which git).path中...把命令输出按位置参数展开——$(cmd)在 Nushell 里没有对应写法,子表达式一律用(cmd),要"散开"成多个参数就加...。- 环境变量是区分大小写的:
$env.PATH(Unix)与$env.Path(Windows);回退值用$env.FOO? | default "ABC",对应 Bash 的${FOO:-fallback}。
数据面:polars 插件与文件解析
原文档的下一段是一个强推荐:
If the polars commands are available, prefer it for working with parquet, jsonl, ndjson, csv files, and avro files. It is much more efficient than the other Nushell commands or other non-nushell commands. It exposes much of the functionality of the polars dataframe library. Start the pipeline with
plugin use polars
三个官方示例(原文档原样给出):
# Nushell 表格 -> polars DataFrame
ps | polars into-df | polars collect
# polars DataFrame -> Nushell 表格(以便继续用 Nushell 命令处理)
polars open file.parquet | polars into-nu
# 打开 parquet、选列、另存为新 parquet
polars open file.parquet | polars select name status | polars save file2
从 nu-mcp 的 Cargo.toml 可以看到 nu-mcp 本身依赖的是 nu-engine、nu-parser、nu-protocol、nuon 等核心 crate,polars 是独立插件 nu_plugin_polars(见 crates/nu_plugin_polars),所以文档才用"if available"措辞——MCP 服务器评估时若该插件已注册,polars 命令即可用。
另外 instructions.md 补了原文档没写的 HTTP 细节:http get/post/put 会按 Content-Type 自动解析 JSON 响应,不要再 | from json;-t json 不合法,必须传完整 MIME 类型 application/json:
http get https://api.example.com/users | get 0.name
http post -t application/json $url {key: "value"}
文件查找只用 glob
原文档的第二条 Important 值得单独强调:
The
globcommand should be used exclusively when you need to locate a file or a code reference, other solutions may produce too large output because of hidden files! For example do not usefindorls -r.
这与 MCP 场景强相关:ls **/* 也会遍历隐藏目录(如 .git、node_modules),输出会撑爆上下文窗口,而 glob 是 Nushell 原生的受限通配查找,可用 command_help glob 查看其参数。instructions.md 在 "Globs and file discovery" 一节重复了同一条建议。
跨调用状态持久化:REPL 语义
原文档最后一段是 evaluate 与一次性 bash -c 类工具的本质区别:
Important: Variables and environment changes persist across tool calls (REPL-style). You can set a variable in one call and access it in subsequent calls:
let x = 42in one call, then$xin the next call returns 42$env.MY_VAR = "hello"persists for later callsHowever, external processes run in their own environment. Use absolute paths when possible.
这由 Evaluator 的实现保证:它持有一个持久 EngineState + Stack + History(evaluation.rs 的架构注释明确写道 "The evaluator maintains a persistent EngineState and Stack that carry state across evaluations—just like an interactive REPL session")。每次调用:解析出 Block → working_set.render() 得到 StateDelta → merge 进持久引擎状态(这样 def/闭包定义在后续调用中可见)→ 用持久状态求值。两组测试直接验证了文档承诺:
- test_repl_variable_persistence:先
let x = 42,再$x,输出含42; - test_repl_env_persistence:先
$env.TEST_VAR = 'hello_repl',再读回。
但"外部进程在独立环境运行"这句也属实:let 绑定的 Nushell 变量不会传进子进程,只有环境变量($env)会。这也是文档建议"尽量用绝对路径"的原因——你上一个调用里 cd 出来的目录虽会持久(cwd 字段会告诉你当前在哪),但外部程序自己看到的还是启动时的环境。
另一个微妙点:持久化是fork 后提交的。每次评估在 EvalState::fork() 出的隔离副本上运行(带独立 Signals,可被取消而不污染原状态);成功才把新状态写回,被升级/取消的那次,其状态变更不会提交——测试 test_cancellation_promotes_to_background_job 验证了取消 let x = 999 后 $x 仍是 1。
超时、限流与历史缓冲的实现细节汇总
把散落各处的阈值与开关集中列表(均可在源码中逐一对应):
| 环境变量 | 默认值 | 类型 | 作用 | 实现位置 |
|---|---|---|---|---|
NU_MCP_PROMOTE_AFTER |
120sec |
duration | 超时后自动升级为后台作业 | evaluation.rs#L188、#L715-L722 |
NU_MCP_OUTPUT_LIMIT |
10kb(10 × 1024 字节) |
filesize | 内联响应截断阈值;0b 禁用截断;完整值始终在 $history |
evaluation.rs#L18-L19、#L728-L736 |
NU_MCP_HISTORY_LIMIT |
100 条 |
int | $history 环形缓冲区容量,满则逐出最旧 |
history.rs#L7-L8、#L38-L46 |
补充两点实现事实:
- 外部命令输出捕获:
Evaluator::new里Stack::new().capture_all()使外部命令的 stdout 与 stderr 都被捕获而非继承终端;process_pipeline把子进程输出合并为单个字符串(stderr 前补一个换行)。测试 test_failing_external_command_returns_captured_output 验证了退出码非 0 的外部命令仍返回捕获到的 stdout+stderr 文本。 - 环形缓冲语义:
History::push在容量满时pop_front()逐出最旧条目,新条目索引等于写入前缓冲区长度——索引不会"左移",老条目只是变得不可访问。测试 test_history_ring_buffer 用NU_MCP_HISTORY_LIMIT = 3精确推演了逐出过程。
服务器侧的配套行为
evaluate 工具并非孤立存在,整个 MCP 服务器为"无人值守执行"做了几处专门设计,理解它们能让 evaluate_tool.md 的契约读得更透:
- 脱离控制终端:lib.rs 在 Unix 下调用
setsid(),让ssh、sudo、psql这类直接开/dev/tty要密码的程序快速失败而不是永久挂起——这也解释了为什么文档强调"用绝对路径、外部进程独立环境"。 - 禁用 ANSI 着色:
NushellMcpServer::new与Evaluator::new都会把use_ansi_coloring设为False并清空color_config——机器对机器协议里色码只会污染输出(错误格式的测试也断言了"无 ANSI 转义码")。 - 外部命令不继承 stdin:
engine_state.is_mcp = true标记 MCP 模式,避免程序在等待输入时卡死(lib.rs#L72-L74)。 - HTTP 会话:
--mcp-transport http下会话保活 30 分钟、消息缓冲容量 16(lib.rs#L108-L112),适合远程接入场景。
小结:把文档当契约、把源码当证据
evaluate_tool.md 不是一份"给人看的用户手册",而是 Nushell 以 MCP 服务器身份向 LLM 客户端输出的行为契约:
- 契约层:NUON 元数据记录(
cwd/history_index/timestamp/output|note)、失败时的结构化 NUON 错误记录、大输出先落$history再回切; - 策略层:120 秒 promote-after 窗口 +
NU_MCP_PROMOTE_AFTER覆盖 +job recv收集,保证工具调用永不无限阻塞; - 翻译层:30 行 Bash 对照表 + polars 优先 +
glob限定 + REPL 式状态持久化,覆盖 LLM 写 Nushell 时最容易出错的几类场景。
由于 include_str! 把这份文档直接编进 tools/list 的响应,任何对它的修改都会改变所有 MCP 客户端对 evaluate 的理解——这也是为什么仓库用 server.rs 的测试 锁定工具集合与输入 schema,让这份"给机器读的文档"本身也处于测试覆盖之下。
适用前提说明:以上内容以当前仓库的 nu-mcp crate 为准,--mcp 相关能力需以 mcp 特性构建(cargo build --features mcp);未启用该特性时,--mcp 标志会提示需要重新编译。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00