
1. 项目概述当Shell脚本遇上MCP一个“WCGW”的实践探索最近在GitHub上看到一个挺有意思的项目叫rusiaaman/wcgw。光看这个标题你可能会有点摸不着头脑。“WCGW”是“What Could Go Wrong”的缩写直译过来就是“能出什么岔子呢”带着点自嘲和探索的意味。结合相关的热搜词比如“MCP”、“Shell忽略错误继续执行”、“Shell脚本入门”这个项目的轮廓就清晰了——它很可能是一个围绕Shell脚本实践特别是与MCPModel Context Protocol协议结合时探讨那些“坑”与“最佳实践”的代码仓库或经验总结。MCP协议是近期AI应用开发领域的一个热点它旨在为AI助手如Claude、Cursor等提供一个标准化的方式来调用外部工具、数据和功能。简单来说它就像给AI装上了一套标准的“瑞士军刀”接口让AI能更安全、更可控地操作你的系统、查询数据库或调用API。而Shell脚本作为系统管理和自动化的基石其与MCP的结合自然充满了想象空间也布满了“陷阱”。这个rusiaaman/wcgw项目在我看来就是一位实践者rusiaaman在尝试用Shell脚本来实现或对接MCP Server时记录下的“踩坑实录”与“解决方案集”。它不仅仅是一份代码更是一份珍贵的、来自一线的调试笔记和经验沉淀。对于任何想要涉足MCP开发尤其是想用自己熟悉的Shell脚本来快速构建MCP工具的朋友来说这个项目及其背后的思路价值巨大。接下来我就结合自己的经验来深度拆解一下这里面的门道告诉你如何玩转Shell与MCP以及如何避开那些让你头疼的“WCGW”时刻。2. MCP协议与Shell脚本为何是“天作之合”与“潜在雷区”2.1 MCP协议的核心思想为AI打造可扩展的“手和脚”在深入Shell之前我们必须先理解MCP到底解决了什么问题。你可以把没有MCP的AI助手想象成一个博学但被“困在”聊天窗口里的顾问。他知道很多知识但无法直接操作你的电脑、无法读取你本地某个特定格式的日志文件、也无法调用你公司内部那个没有公开文档的API。MCP协议的出现就是为了打破这层壁垒。它定义了一套简单的、基于JSON-RPC的通信标准。一个MCP Server就是一个提供了特定“能力”的后台服务比如“文件操作”、“数据库查询”、“发送邮件”。AI客户端如集成了MCP的代码编辑器可以发现并连接这些Server然后以自然语言的方式要求AI去使用这些能力。例如你可以对AI说“帮我分析一下项目根目录下error.log文件中最近一小时的错误。” AI会理解你的意图通过MCP调用对应的“文件读取”Server获取日志内容再进行分析。这种架构的优势在于解耦和安全。工具开发者和AI应用开发者可以各司其职通过标准协议通信。同时AI客户端无需获得直接执行系统命令的至高权限它只能通过MCP Server暴露的、经过精心设计和权限控制的“工具”来间接操作安全性大大提升。2.2 Shell脚本作为MCP Server的天然载体为什么Shell脚本特别适合用来快速搭建MCP Server的原型甚至生产级工具呢原因有以下几点强大的系统交互能力Shell脚本天生就是为操作Linux/Unix系统而生的。文件管理ls,cp,rm,find、进程控制ps,kill,nohup、文本处理grep,awk,sed、网络工具curl,wget,nc等等这些命令的组合能实现几乎所有的系统级自动化任务。这正是许多MCP工具需要提供的核心能力。开发效率极高对于一个熟悉Shell的开发者用几十行脚本实现一个功能完整的MCP Server可能比用Python或Go写一个HTTP服务还要快。特别是处理一些简单的胶水逻辑Shell脚本的简洁性无与伦比。易于集成现有资产很多团队已经有大量维护良好的Shell脚本用于备份、部署、监控等。通过为这些脚本包裹一层MCP协议可以几乎零成本地将它们“AI化”让AI助手能够安全地调用这些成熟的工作流。2.3 “WCGW”Shell脚本在MCP场景下的典型挑战然而正如项目名所暗示的“能出什么岔子呢” 岔子可太多了。将Shell脚本置于MCP Server这样一个需要长期运行、稳定响应、安全可控的守护进程中会放大Shell脚本本身的许多弱点错误处理薄弱默认情况下Shell脚本遇到错误命令返回非零值会继续执行这可能导致状态不一致。而MCP Server需要清晰地报告错误给客户端。环境依赖与可移植性脚本可能依赖特定的$PATH、环境变量、或特定版本的工具如jqvsyq在MCP Server的运行环境中可能缺失。安全性问题如果不加处理用户输入可能被直接拼接成命令造成命令注入漏洞。这在AI自动调用工具的场景下风险极高。资源管理与超时一个执行时间过长的脚本可能会阻塞整个MCP Server导致其他请求无法响应。输出解析Shell命令的输出通常是面向人类的文本而MCP协议需要结构化的JSON数据。如何稳定、准确地解析ps、df等命令的输出是一大挑战。rusiaaman/wcgw项目的价值就在于它直面了这些挑战并提供了经过实战检验的解决方案模式。3. 构建一个健壮的Shell MCP Server从原理到实践3.1 基础架构STDIN/STDOUT作为通信桥梁一个最简单的MCP Server可以就是一个从标准输入STDIN读取JSON-RPC请求向标准输出STDOUT写入JSON-RPC响应的命令行程序。Shell脚本完全可以胜任。核心通信循环结构如下#!/bin/bash # 一个简单的Shell MCP Server框架 # 设置无缓冲的IO确保即时输出 stdbuf -i0 -o0 -e0 # 主循环持续读取STDIN while read -r line; do # 解析收到的JSON-RPC请求这里简化实际应用需要用jq等工具 # 假设请求格式为{jsonrpc:2.0,method:echo,params:[hello],id:1} # 提取方法名和参数 method$(echo $line | jq -r .method) params$(echo $line | jq -r .params) req_id$(echo $line | jq -r .id) # 根据方法名路由到不同的处理函数 case $method in echo) result$(handle_echo $params) ;; listFiles) result$(handle_list_files $params) ;; *) # 方法不存在 error_response $req_id Method not found continue ;; esac # 构建并发送成功的JSON-RPC响应 send_response $req_id $result done这个框架的核心是while read -r line循环和jq工具。jq是处理JSON的瑞士军刀是Shell MCP Server的必备依赖。没有它解析复杂的JSON请求将是一场噩梦。重要提示在read循环中务必使用-r选项来防止反斜杠被转义。同时使用stdbuf或类似工具如unbuffer来禁用IO缓冲这对于MCP客户端和Server之间的即时通信至关重要否则可能会因为缓冲导致请求/响应被延迟或粘包。3.2 核心工具链jq与timeout的不可或缺性基于上述框架两个工具的地位陡然提升jq如前所述用于请求解析和响应构建。你必须熟练掌握其基本查询.method、字符串提取-r、数组迭代.[]、对象构造--arg,--argjson等功能。它是Shell脚本与结构化数据JSON之间的桥梁。timeout这是保证Server不被单个耗时请求拖垮的关键。在任何执行外部命令或可能长时间运行的操作时必须使用timeout命令进行包装。# 错误示范直接执行可能永远不返回 output$(some_slow_command) # 正确示范设置5秒超时 if output$(timeout 5s some_slow_command); then # 命令在5秒内成功完成 result$output else # 命令超时或失败 exit_code$? if [[ $exit_code -eq 124 ]]; then resultERROR: Command timed out after 5 seconds else resultERROR: Command failed with exit code $exit_code fi fi3.3 安全第一彻底杜绝命令注入这是Shell MCP Server的生命线。绝对不要将未经处理的用户输入来自MCP请求的params直接拼接到命令字符串中。反面教材极其危险# 假设请求是 {method:grepFile, params: {file:/etc/passwd, pattern:root}} file$(echo $params | jq -r .file) pattern$(echo $params | jq -r .pattern) # 危险如果pattern是 root; rm -rf / 呢 grep_result$(grep $pattern $file)安全实践白名单验证对文件路径、命令参数进行严格校验。例如只允许操作特定目录下的文件。allowed_base/var/log/myapp requested_file$(jq -r .file $params) # 使用realpath解析并检查是否在允许的目录下 resolved_path$(realpath -m $allowed_base/$requested_file 2/dev/null) if [[ -z $resolved_path ]] || [[ $resolved_path ! $allowed_base/* ]]; then send_error Access denied to path: $requested_file return 1 fi # 现在可以安全地使用 $resolved_path使用数组传递参数Bash中将命令及其参数存储在数组中可以安全地处理包含空格或特殊字符的参数。pattern$(jq -r .pattern $params) # 将参数放入数组 cmd_args(-E -- $pattern $resolved_path) # 安全执行 if ! grep_result$(grep ${cmd_args[]}); then # grep没有找到匹配项返回非零这里可以处理为正常情况 grep_result fi避免使用eval这是万恶之源在MCP Server中应完全禁止。3.4 健壮性提升全面的错误处理与信号管理一个生产级的MCP Server必须优雅地处理各种错误和中断。#!/bin/bash set -euo pipefail # 启用严格模式遇到错误退出未设变量报错管道中任意失败则整体失败。 # 定义陷阱用于捕获退出信号进行清理工作 cleanup() { echo Server is shutting down... 2 # 关闭后台进程删除临时文件等 rm -f $TEMP_FILE } trap cleanup EXIT INT TERM # 主循环 while read -r line; do # 使用子shell执行请求处理防止单个请求的错误导致整个Server崩溃 ( set e # 在子shell内暂时关闭-e以便我们可以自定义错误处理 handle_request $line sub_exit$? # 可以根据 sub_exit 记录日志但不要退出父进程 ) # 可选等待后台任务完成或控制并发数。对于简单Server可以去掉以串行处理。 doneset -euo pipefail这是编写健壮Shell脚本的黄金法则。它能及早发现很多潜在错误。trap用于确保Server在收到终止信号如CtrlC时能执行必要的清理工作避免留下僵尸进程或临时文件。子shell与后台执行将每个请求的处理放在子shell中可以隔离其错误。使用放入后台可以实现简单的并发但要注意资源竞争和客户端对响应顺序的预期。对于初学者建议先使用串行去掉模式。4. 实战实现一个“系统信息查询”MCP Server让我们动手实现一个实用的MCP Server它提供两个工具get_memory_usage和get_disk_usage。这个例子将综合运用上述所有技巧。4.1 项目结构与启动脚本首先创建项目目录sysinfo-mcp-server/ ├── server.sh # 主服务器脚本 ├── tools/ # 工具实现目录 │ ├── memory.sh │ └── disk.sh └── run_server # 启动包装脚本run_server(启动脚本):#!/bin/bash # 这是一个包装脚本确保环境正确并启动Server DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) cd $DIR # 检查必备工具 for cmd in jq timeout; do if ! command -v $cmd /dev/null; then echo 错误未找到命令 $cmd请先安装。 2 exit 1 fi done # 设置一个安全的临时目录 export TMPDIR${TMPDIR:-/tmp}/mcp_sysinfo_$$ mkdir -p $TMPDIR trap rm -rf $TMPDIR EXIT # 禁用输出缓冲并启动Server exec stdbuf -i0 -o0 -e0 bash ./server.shserver.sh(主服务器逻辑):#!/bin/bash set -euo pipefail # 引入工具函数 source ./tools/memory.sh source ./tools/disk.sh # 工具路由表 declare -A TOOL_HANDLERS( [get_memory_usage]handle_memory_usage [get_disk_usage]handle_disk_usage ) # 读取初始化请求MCP协议要求 read -r init_request # 这里应该解析并响应初始化请求例如声明自己提供的工具列表。 # 为简化我们直接发送一个简单的初始化响应。 echo {jsonrpc:2.0,result:{protocolVersion:2024-11-05,capabilities:{}},id:null} # 主请求处理循环 while read -r line; do # 忽略空行 [[ -z $line ]] continue # 在子shell中处理请求防止崩溃 ( # 临时关闭-e以便集中处理错误 set e # 调用请求处理器 handle_json_rpc_request $line ) # 注意这里使用了后台执行()请求处理是并发的。 # 对于需要严格顺序或状态共享的场景需要更复杂的并发控制。 done4.2 工具实现示例tools/memory.sh#!/bin/bash handle_memory_usage() { local params_json$1 local req_id$2 # 安全地解析参数这个工具可能不需要参数但演示解析过程 local unit unit$(echo $params_json | jq -r .unit // MB) # 参数白名单校验 case $unit in KB|MB|GB) # 单位有效 ;; *) send_error_response $req_id Invalid unit. Must be KB, MB, or GB. return 1 ;; esac # 使用timeout执行命令避免挂起 local meminfo if ! meminfo$(timeout 2s cat /proc/meminfo 2/dev/null); then send_error_response $req_id Failed to read memory info or command timed out. return 1 fi # 解析/proc/meminfo这里是一个简化解析 local total_mem_kb free_mem_kb available_mem_kb total_mem_kb$(echo $meminfo | awk /MemTotal:/ {print $2}) available_mem_kb$(echo $meminfo | awk /MemAvailable:/ {print $2}) # 计算使用率注意Linux内存管理复杂Available比Free更能反映可用内存 if [[ -z $total_mem_kb || -z $available_mem_kb ]]; then send_error_response $req_id Could not parse memory information. return 1 fi local used_mem_kb$((total_mem_kb - available_mem_kb)) local usage_percent$(awk -v used$used_mem_kb -v total$total_mem_kb BEGIN {printf %.1f, (used/total)*100}) # 根据请求单位转换 local divisor1 case $unit in KB) divisor1;; MB) divisor1024;; GB) divisor$((1024*1024));; esac local total_display$(awk -v v$total_mem_kb -v d$divisor BEGIN {printf %.2f, v/d}) local used_display$(awk -v v$used_mem_kb -v d$divisor BEGIN {printf %.2f, v/d}) local available_display$(awk -v v$available_mem_kb -v d$divisor BEGIN {printf %.2f, v/d}) # 构建结构化JSON响应 local result_json result_json$(jq -n \ --arg total $total_display \ --arg used $used_display \ --arg available $available_display \ --arg unit $unit \ --arg usage $usage_percent \ { total: $total, used: $used, available: $available, unit: $unit, usagePercent: $usage }) send_success_response $req_id $result_json } # 通用的响应发送函数应在server.sh或公共库中定义这里为示例 send_success_response() { local id$1 local result$2 jq -n \ --argjson id $id \ --argjson result $result \ { jsonrpc: 2.0, result: $result, id: $id } } send_error_response() { local id$1 local message$2 jq -n \ --argjson id $id \ --arg message $message \ { jsonrpc: 2.0, error: { code: -32603, message: $message }, id: $id } }4.3 请求处理器handle_json_rpc_request这个函数需要添加到server.sh或一个单独的公共库文件中它负责解析请求并路由到正确的工具函数。handle_json_rpc_request() { local request$1 # 基本JSON校验 if ! jq -e . /dev/null 21 $request; then echo {jsonrpc:2.0,error:{code:-32700,message:Parse error},id:null} return 1 fi local method req_id params method$(echo $request | jq -r .method) req_id$(echo $request | jq -r .id) params$(echo $request | jq -c .params) # 检查必需字段 if [[ $method null ]]; then send_error_response $req_id Missing method in request. return 1 fi if [[ $req_id null ]]; then # JSON-RPC允许通知没有id这里我们简单忽略或记录 return 0 fi # 查找对应的处理函数 local handler_name${TOOL_HANDLERS[$method]} if [[ -z $handler_name ]]; then send_error_response $req_id Method $method not found. return 1 fi # 调用处理函数 if declare -f $handler_name /dev/null; then $handler_name $params $req_id else send_error_response $req_id Handler for $method is not defined. return 1 fi }4.4 测试你的MCP Server你可以使用netcat(nc) 或编写一个简单的Python脚本来模拟客户端进行测试。使用socat进行手动测试推荐# 在一个终端启动Server ./run_server # 在另一个终端使用socat发送请求 echo {jsonrpc:2.0,method:get_memory_usage,params:{unit:MB},id:1} | socat EXEC:./run_server STDIO你应该会收到一个类似这样的响应{ jsonrpc: 2.0, result: { total: 15942.75, used: 3456.23, available: 12486.52, unit: MB, usagePercent: 21.7 }, id: 1 }5. 进阶技巧与“WCGW”避坑指南在真实场景中你会遇到比示例更复杂的情况。以下是一些进阶技巧和对应的“坑”5.1 处理长时间运行的任务与进度报告如果一个工具比如备份数据库需要运行几分钟你不能让客户端一直干等。MCP协议支持进度通知。虽然Shell实现起来有点棘手但可以通过在后台运行任务并定期向STDERR或一个约定的通道输出进度信息来实现。更常见的做法是让工具立即返回一个“任务已开始”的响应并提供一个check_status方法供客户端轮询。避坑提示在Shell中管理后台任务的PID并检查其状态是关键。务必使用wait命令或检查/proc/$PID来准确获取任务状态避免产生僵尸进程。5.2 输入/输出流的分离与多路复用一个复杂的工具可能需要从标准输入读取大量数据或者向标准输出和标准错误输出不同类型的信息。在Shell MCP Server中所有通信都混杂在STDIN/STDOUT这一条通道里。你需要设计自己的微协议在JSON-RPC的框架内用不同的字段来区分“数据流”、“日志流”和“控制信号”。例如可以在响应中增加一个stream字段。避坑提示不要试图直接用或21重定向工具的输出到主响应流。这会导致JSON格式被破坏。正确的做法是将工具输出捕获到变量或文件然后作为JSON字符串的一个字段值发送。5.3 依赖管理与环境隔离你的Shell脚本可能依赖awk的特定版本、jq的某个功能或者一个自定义的二进制工具。在Docker容器中运行你的MCP Server是最佳实践它能提供一致的环境。避坑提示即使在容器内也要在脚本开头检查所有命令是否存在并以友好的错误信息告知用户缺少什么。可以使用command -v或which进行检查。5.4 日志记录与调试一个无头的、长期运行的Server没有日志寸步难行。但你不能把日志打到STDOUT那是给MCP协议用的。应该重定向到系统日志如logger命令或一个指定的日志文件。log() { local level$1 local message$2 # 输出到 STDERR方便在终端运行时查看同时可重定向到文件 echo [$(date -Iseconds)] [$level] $message 2 # 也可以使用系统日志 logger -t mcp-server $level: $message } # 在工具函数中使用 handle_some_tool() { log INFO 开始处理请求参数: $params # ... 处理逻辑 ... if [[ $? -ne 0 ]]; then log ERROR 工具执行失败 fi }避坑提示注意日志轮转防止日志文件无限增大。可以使用logrotate工具进行配置。6. 集成到AI客户端以Cursor为例开发完MCP Server后你需要在AI客户端中配置它。以Cursor编辑器为例在Cursor设置中找到MCPModel Context Protocol配置部分。添加一个新的Server配置。配置方式通常是命令行形式。对于我们的Shell Server配置可能类似于{ mcpServers: { sysinfo-server: { command: /absolute/path/to/your/sysinfo-mcp-server/run_server, args: [], env: { SOME_ENV_VAR: value } } } }重启Cursor。之后当你向Cursor的AI提问时它就可以自动调用你定义的get_memory_usage等工具了。例如你可以问“当前系统的内存使用情况如何” AI会识别出意图通过MCP调用你的Server并将结果整合到回答中。最后的体会用Shell脚本构建MCP Server是一种“快速验证想法”和“杠杆化现有脚本资产”的绝佳方式。它门槛低见效快。但正如rusiaaman/wcgw这个项目名所提醒的你必须对Shell脚本的脆弱性保持高度警惕。那些在一次性脚本中可以忽略的小问题在作为一个长期运行的服务时都会被放大成致命的缺陷。我的经验是把它当作一个真正的“服务”来设计重视错误处理、输入验证、资源管理和日志记录。当你成功地将一个粗糙的Shell脚本打磨成一个健壮的MCP Server时那种让AI助手安全、可靠地操控你整个工作流的感觉绝对是物超所值的。