Mcp 初体验

我的第一个 MCP 项目开发记录

之前看到很多佬自己开发mcp,一直对这方面比较感兴趣,但是一直没有试着搭建这么一个mcp程序

所以,今天花了几个小时时间学习 MCP(Model Context Protocol),并且成功搭建了一个系统监控服务器。记录一下这个过程中的一些经历和踩过的坑,希望对后来者有帮助。

刚开始的困惑

第一次接触 MCP 时完全不知道从哪里入手:

  • 看文档说是个协议,但不知道具体怎么实现
  • 不确定是要启动一个 HTTP 服务器,还是别的什么
  • Cursor 怎么知道调用我的工具?

折腾了一阵子才搞明白,MCP 支持多种通信方式:stdin/stdout、HTTP、WebSocket 等。我选择了 stdin/stdout 方式,就是两个进程通过管道传递 JSON 消息。

项目结构的演变

一开始想偷懒,把所有代码都塞在一个 main.go 文件里。写到一半发现完全维护不了,只能重构。

最后搞了个分层的结构:

  • main.go - 程序入口
  • internal/router/ - 处理 JSON-RPC 协议
  • internal/tools/ - 各种监控工具
  • internal/storage/ - 数据存储和缓存
  • internal/types/ - 类型定义

这样每个模块职责清楚,后面添加新功能也方便。

踩过的大坑

坑1:stdout 被污染了

这个坑卡了我好久。一开始程序启动时会打印一些日志,类似这样:

🖥️ 系统监控 MCP 服务器启动中...
{"jsonrpc":"2.0","id":1,"result":{...}}

结果 Cursor 一直报错说解析 JSON 失败。后来才明白,MCP 要求 stdout 只能输出 JSON-RPC 消息,其他日志都要输出到 stderr。

解决方法就是把所有日志都改成 fmt.Fprintf(os.Stderr, ...)

坑2:通知不需要响应

JSON-RPC 有两种消息:请求和通知。请求有 ID 字段,需要返回响应;通知没有 ID,不需要响应。

一开始我对所有消息都返回响应,结果 notifications/initialized 这个通知也被我回复了,导致协议出错。

后来加了个判断:

isNotification := req.ID == nil
if response != nil && !isNotification {
    r.sendResponse(response)
}

坑3:库函数返回类型不对

gopsutil 库获取进程状态时踩了个坑:

// 我以为 Status() 返回 string,结果是 []string
status, _ := p.Status()  // 编译错误

// 正确的写法
statusSlice, _ := p.Status()
status := ""
if len(statusSlice) > 0 {
    status = statusSlice[0]
}

这种类型错误编译器会报错,但如果不仔细看文档很容易踩坑。

配置文件的优化

最开始配置文件特别复杂,需要指定一堆参数:

{
  "mcpServers": {
    "system-monitor": {
      "command": "/Users/ml/Work/Go/Mcp/system-monitor",
      "args": ["--name", "system-monitor-mcp", "--version", "1.0.0", "--data-dir", "/Users/ml/Work/Go/Mcp/data", "--cache", "true"],
      "env": {"PATH": "/usr/local/bin:/usr/bin:/bin"}
    }
  }
}

后来觉得这样对用户太不友好了,就在代码里加了默认值,配置文件简化成:

{
  "mcpServers": {
    "system-monitor": {
      "command": "/Users/ml/Work/Go/Mcp/system-monitor"
    }
  }
}

这样用户只需要指定可执行文件路径就行了。

性能还不错

做完之后用 ps 看了一下,这个服务器还挺轻量的:

  • 内存只用了 12.6 MB
  • CPU 使用率基本是 0%
  • 启动很快

因为大部分时间都在等待输入,所以资源占用很低。

另外还加了个简单的缓存,避免频繁调用系统 API:

type MemoryCache struct {
    items map[string]*CacheItem
    mutex sync.RWMutex
}

缓存 5 分钟,对于监控数据来说够用了。

最终效果

配置好之后,在 Cursor 里直接问"帮我查看系统的 CPU 使用情况",就能得到这样的回复:

🖥️ CPU 信息
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
型号: Apple M4
核心数: 10 物理核心, 10 逻辑核心
总体使用率: 18.98%

各核心使用率:
  核心 1: 42.42%
  核心 2: 35.64%
  ...

还能查看内存、磁盘、网络、进程等信息,基本的系统监控需求都能满足。

整个流程是如何工作的

经过这次开发,我对整个 MCP 通信流程有了一个简单的理解:

  1. AI 发起请求:Cursor 把用户的问题转换成 JSON-RPC 格式的消息,通过 stdin 发送给我的 MCP 服务器
  2. MCP 服务器解析:我的服务器接收到 JSON 消息后,解析出是什么类型的指令(比如 tools/call
  3. 路由到具体工具:根据指令类型,路由器找到对应的工具(比如 cpu_info
  4. 工具执行:具体的工具调用系统 API 获取数据(比如调用 gopsutil 获取 CPU 信息)
  5. 返回结果:工具把获取到的信息格式化后返回给路由器
  6. 响应 AI:路由器把结果包装成 JSON-RPC 响应,通过 stdout 发送回 Cursor
  7. AI 分析展示:Cursor 接收到数据后,分析并以用户友好的方式展示给用户

整个过程就像一个翻译和代理的过程:AI 不直接访问系统,而是通过我的 MCP 服务器作为桥梁来获取系统信息。

总结

整个过程下来,主要学到了:

  1. MCP 协议本质:支持多种通信方式(stdin/stdout、HTTP、WebSocket),本质是 JSON-RPC 消息传递
  2. 通信流程理解:AI → JSON请求 → MCP服务器解析 → 调用工具 → 获取数据 → 返回结果 → AI分析展示
  3. 调试很重要:stdout 不能有杂音,通知不需要响应
  4. 用户体验:配置越简单越好
  5. 代码结构:分层架构比单文件好维护

做完这个项目对 MCP 的理解深入了很多,也算是入门了。代码放在 GitHub 上了:

32 个赞

太强了!自己撰写MCP非常棒了

2 个赞

其实功能很鸡肋并没有什么用,只是做了一个尝试嘿嘿

1 个赞

扬帆 起航

2 个赞

[!success]不错,继续加油

5 个赞

学习了,感谢分享

1 个赞

厉害的 感谢佬友分享经验

1 个赞

感同身受,但是不一样的是我现在还在这个阶段,还是搞不太明白MCP :smiling_face_with_tear:

其实就是工具链调用,佬可以拉几个简单的mcp项目下来分析一下架构,自然就懂了

感谢分享,已Star :hand_with_index_finger_and_thumb_crossed:

1 个赞

佬站大佬太多了,感觉还要多学习 :smiling_face_with_tear:

谢谢! ! !

[!success]- 感谢分享!

佬这种绿色回复是如何打出来的?

请教一下不通过AI客户端怎么调用呢,装了几个MCP,都是给codex和cc用的,如果我自己做个了个MCP服务想测试一下该怎么做呢?

1 个赞

[!success]- 这样打出来的
>[!success]- 这样

像我做的这个直接使用
echo ‘{“jsonrpc”:“2.0”,“id”:1,“method”:“initialize”,“params”:{“protocolVersion”:“2024-11-05”,“capabilities”:{},“clientInfo”:{“name”:“test”,“version”:“1.0.0”}}}’ | ./system-monitor

就可以调用了当然需要cd到对应mcp服务器的目录才行

1 个赞

谢谢~~

[!success]-这样吗

[!success]- 这样

1 个赞