0908
This commit is contained in:
@@ -8,67 +8,120 @@ aio-system 的自定义 MCP 服务器(Go)。作为 AI Agent 的「手」,
|
||||
|
||||
```
|
||||
aio-mcp/
|
||||
├── main.go # 入口:选择 stdio / http 传输
|
||||
├── cmd/
|
||||
│ └── aio-mcp/
|
||||
│ └── main.go # 入口:解析配置 → 初始化日志 → 装配并启动服务器
|
||||
└── internal/
|
||||
├── config/
|
||||
│ └── config.go # 启动配置:transport / addr / log-level / data-dir
|
||||
├── logging/
|
||||
│ └── logging.go # 日志初始化(固定输出 stderr,详见注意事项)
|
||||
├── deps/
|
||||
│ └── deps.go # 工具共享依赖容器(数据目录,后续 SQLite 连接等)
|
||||
├── server/
|
||||
│ └── server.go # 服务器装配(创建实例 + 注册工具)
|
||||
│ ├── server.go # 服务器装配:创建实例 + 注册工具 + 注入依赖
|
||||
│ └── serve.go # 传输层:stdio / streamable HTTP 的启动逻辑
|
||||
└── tools/
|
||||
├── registry.go # 工具注册总表 RegisterAll
|
||||
├── ping.go # 示例工具:健康检查(无参数)
|
||||
└── echo.go # 示例工具:参数传递(有参数)
|
||||
├── registry.go # 工具注册表:按业务域分组,统一 RegisterAll
|
||||
└── system/ # 工具组:系统类
|
||||
├── system.go # 组内注册入口
|
||||
├── ping.go # 健康检查(无参数)
|
||||
└── echo.go # 参数传递演示(有参数)
|
||||
```
|
||||
|
||||
分层约定:
|
||||
|
||||
- `cmd/aio-mcp`:只有一个可执行文件,所有编排逻辑放在 `internal/` 下,`main.go` 保持极薄。
|
||||
- `internal/config` / `internal/logging`:与 MCP 无关的横切能力,独立成包便于加测试。
|
||||
- `internal/deps`:**共享依赖不放在 `internal/tools`**——工具子包要 import 它,若放在 `tools` 会与 `registry.go` 形成循环依赖。
|
||||
- `internal/tools/<组>`:一个业务域一个子包,组内工具各自一个文件;注册入口统一叫 `Register(s *mcp.Server, d *deps.Deps)`。
|
||||
- 暂无 `pkg/`:当前没有需要对外复用的公共 API。若将来要被 membank 其他 Go 项目(如 auto-check)直接 import,再新增 `pkg/` 并把稳定接口下沉过去。
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
go build -o aio-mcp.exe . # 构建
|
||||
go build -o aio-mcp.exe ./cmd/aio-mcp # 构建
|
||||
|
||||
# stdio 模式(默认,供本地 Agent 拉起,如 Reasonix / Claude)
|
||||
# http 模式(默认,streamable HTTP,远程客户端访问)
|
||||
aio-mcp.exe
|
||||
aio-mcp.exe -transport http -addr localhost:8000 # 同上,显式指定监听地址
|
||||
|
||||
# http 模式(远程客户端访问)
|
||||
aio-mcp.exe -transport http -addr localhost:8000
|
||||
# stdio 模式(本地 Agent 直接拉起进程时使用)
|
||||
aio-mcp.exe -transport stdio
|
||||
|
||||
# 其他参数
|
||||
aio-mcp.exe -log-level debug -data-dir D:\data\aio-mcp
|
||||
```
|
||||
|
||||
服务以 **streamable HTTP** 为主要对外方式:POST `/mcp`,请求头需同时接受
|
||||
`application/json` 与 `text/event-stream`,响应为 SSE(`event: message` + `data: {...}`)。默认监听 `localhost:8000`。
|
||||
|
||||
依赖拉取走内网代理,若需直连官方源:`go env -w GOPROXY=https://proxy.golang.org,direct`。
|
||||
|
||||
## 新增一个工具
|
||||
|
||||
1. 在 `internal/tools/` 新建文件(如 `weather.go`),用参数 struct + 泛型 `mcp.AddTool` 注册:
|
||||
1. 在对应业务域子包(如 `internal/tools/sqlite/`)新建文件 `query.go`,用参数 struct + 泛型 `mcp.AddTool` 注册:
|
||||
|
||||
```go
|
||||
package tools
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type weatherArgs struct {
|
||||
City string `json:"city" jsonschema:"城市名,必填"`
|
||||
Days int `json:"days,omitempty" jsonschema:"预报天数,默认 1"`
|
||||
type queryArgs struct {
|
||||
SQL string `json:"sql" jsonschema:"要执行的 SELECT 语句,必填"`
|
||||
}
|
||||
|
||||
func registerWeather(s *mcp.Server) {
|
||||
func registerQuery(s *mcp.Server, d *deps.Deps) {
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: "weather",
|
||||
Description: "查询某城市未来几天的天气。",
|
||||
}, func(_ context.Context, _ *mcp.CallToolRequest, args weatherArgs) (*mcp.CallToolResult, any, error) {
|
||||
// ...业务逻辑...
|
||||
Name: "sqlite_query",
|
||||
Description: "在指定库上执行只读 SQL 查询。",
|
||||
}, func(_ context.Context, _ *mcp.CallToolRequest, args queryArgs) (*mcp.CallToolResult, any, error) {
|
||||
// ...业务逻辑,用到共享依赖时取 d.DataDir / d.DB...
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "晴,25℃"}},
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "..."}},
|
||||
}, nil, nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
2. 在 `internal/tools/registry.go` 的 `RegisterAll` 中追加一行:`registerWeather(s)`。
|
||||
2. 在该组的 `Register`(如 `sqlite/sqlite.go`)中追加一行:`registerQuery(s, d)`。
|
||||
3. 若是一个新的业务域,再在 `internal/tools/registry.go` 的 `RegisterAll` 中追加一行 `sqlite.Register(s, d)`。
|
||||
|
||||
工具需要共享状态(SQLite 连接等)时,参考 `registry.go` 顶部注释,把 `RegisterAll` 演进为带 `deps` 参数。
|
||||
工具需要共享状态(SQLite 连接、HTTP client 等)时,在 `internal/deps/deps.go` 加字段并在 `deps.New` 中构造,各工具从 `*deps.Deps` 取用,不要自建。
|
||||
|
||||
## 冒烟测试
|
||||
|
||||
stdio 模式用换行分隔的 JSON-RPC 直接喂协议消息:
|
||||
HTTP 模式(推荐):`initialize` 响应头里的 `Mcp-Session-Id` 要带回后续请求,
|
||||
否则会报 `method "tools/list" is invalid during session initialization`。
|
||||
|
||||
```bash
|
||||
# 1) 初始化,取会话 ID
|
||||
curl -s -i -X POST http://localhost:8000/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0.0.1"}}}'
|
||||
# => Mcp-Session-Id: xxxxx ;响应体为 SSE:event: message / data: {...}
|
||||
|
||||
SID=<上一步的会话 ID>
|
||||
A="Content-Type: application/json"; B="Accept: application/json, text/event-stream"
|
||||
|
||||
# 2) 通知已初始化(返回 202)
|
||||
curl -s -X POST http://localhost:8000/mcp -H "$A" -H "$B" -H "Mcp-Session-Id: $SID" \
|
||||
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
||||
|
||||
# 3) 列工具
|
||||
curl -s -X POST http://localhost:8000/mcp -H "$A" -H "$B" -H "Mcp-Session-Id: $SID" \
|
||||
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
|
||||
|
||||
# 4) 调工具
|
||||
curl -s -X POST http://localhost:8000/mcp -H "$A" -H "$B" -H "Mcp-Session-Id: $SID" \
|
||||
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"你好","times":2}}}'
|
||||
```
|
||||
|
||||
stdio 模式(bash/WSL 下用换行分隔的 JSON-RPC 直接喂协议消息):
|
||||
|
||||
```bash
|
||||
printf '%s\n' \
|
||||
@@ -76,25 +129,28 @@ printf '%s\n' \
|
||||
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
||||
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
|
||||
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"你好","times":2}}}' \
|
||||
| aio-mcp.exe
|
||||
| aio-mcp.exe -transport stdio
|
||||
```
|
||||
|
||||
http 模式:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/mcp -H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0.0.1"}}}'
|
||||
```
|
||||
注意:Windows PowerShell 的管道会给字节流加 BOM/转码,导致服务端报
|
||||
`invalid character 'ï' looking for beginning of value`,stdio 自检请用 bash/WSL 或直接用 MCP 客户端。
|
||||
|
||||
## 接入 Agent(以 Reasonix / Claude 为例)
|
||||
|
||||
在客户端 MCP 配置中注册,指向构建产物:
|
||||
HTTP 模式:先启动服务(或交由 systemd / 容器守护),再在客户端 MCP 配置中指向地址:
|
||||
|
||||
```json
|
||||
{ "mcpServers": { "aio-mcp": { "command": "D:\\workspace\\membank\\aio-mcp\\aio-mcp.exe" } } }
|
||||
{ "mcpServers": { "aio-mcp": { "url": "http://localhost:8000/mcp" } } }
|
||||
```
|
||||
|
||||
stdio 模式(客户端自行拉起进程):
|
||||
|
||||
```json
|
||||
{ "mcpServers": { "aio-mcp": { "command": "D:\\workspace\\membank\\aio-mcp\\aio-mcp.exe", "args": ["-transport", "stdio"] } } }
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- stdio 模式下 **stdout 是协议通道**:日志已固定输出到 stderr,业务代码请勿往 stdout 打印。
|
||||
- HTTP 模式下客户端必须带 `Accept: application/json, text/event-stream`,否则服务端返回 400。
|
||||
- 工具名只允许 `a-z A-Z 0-9 _ - .`。
|
||||
|
||||
Reference in New Issue
Block a user