diff --git a/aio-mcp/README.md b/aio-mcp/README.md index 0e3a53a..8b130a3 100644 --- a/aio-mcp/README.md +++ b/aio-mcp/README.md @@ -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 _ - .`。 diff --git a/aio-mcp/cmd/aio-mcp/main.go b/aio-mcp/cmd/aio-mcp/main.go new file mode 100644 index 0000000..ae63590 --- /dev/null +++ b/aio-mcp/cmd/aio-mcp/main.go @@ -0,0 +1,44 @@ +// aio-mcp:aio-system 的自定义 MCP 服务器。 +// +// 作为 AI Agent 的「手」,以 MCP 协议对外暴露工具。 +// 默认以 streamable HTTP 传输运行(远程客户端访问),也可切换为 +// stdio 供本地 Agent 直接拉起。 +// +// 本文件只负责「装配 + 启动」,具体逻辑分别在: +// - internal/config :启动参数解析 +// - internal/logging :日志初始化(固定 stderr) +// - internal/server :MCP 服务器装配与传输 +// - internal/tools :工具注册表 +package main + +import ( + "errors" + "flag" + "log/slog" + "os" + + "aio-mcp/internal/config" + "aio-mcp/internal/logging" + "aio-mcp/internal/server" +) + +// version 可用 -ldflags "-X main.version=..." 在构建时覆盖。 +var version = "0.1.0" + +func main() { + cfg, err := config.Parse(os.Args[1:], os.Stderr) + if err != nil { + // -h/--help 属于正常退出,参数错误才是异常退出。 + if errors.Is(err, flag.ErrHelp) { + os.Exit(0) + } + os.Exit(2) + } + + logging.Setup(cfg.LogLevel) + + if err := server.Serve(server.New(version, cfg), cfg); err != nil { + slog.Error("aio-mcp exited with error", "err", err) + os.Exit(1) + } +} diff --git a/aio-mcp/internal/config/config.go b/aio-mcp/internal/config/config.go new file mode 100644 index 0000000..30578a0 --- /dev/null +++ b/aio-mcp/internal/config/config.go @@ -0,0 +1,45 @@ +// Package config 解析 aio-mcp 的启动配置(命令行参数)。 +package config + +import ( + "flag" + "fmt" + "io" +) + +// 支持的传输方式。 +const ( + TransportStdio = "stdio" + TransportHTTP = "http" +) + +// Config 是 aio-mcp 的运行时配置。 +type Config struct { + Transport string // MCP 传输方式:stdio 或 http + Addr string // http 模式下的监听地址 + LogLevel string // 日志级别:debug / info / warn / error + DataDir string // 数据目录:SQLite 库文件与工具产物的存放位置 +} + +// Parse 解析命令行参数。args 不含程序名(通常传 os.Args[1:]), +// 用法与错误信息写到 out。 +func Parse(args []string, out io.Writer) (*Config, error) { + cfg := &Config{} + + fs := flag.NewFlagSet("aio-mcp", flag.ContinueOnError) + fs.SetOutput(out) + fs.StringVar(&cfg.Transport, "transport", TransportHTTP, "传输方式:http(默认,streamable HTTP)或 stdio(本地 Agent 拉起)") + fs.StringVar(&cfg.Addr, "addr", "localhost:8000", "http 模式下的监听地址") + fs.StringVar(&cfg.LogLevel, "log-level", "info", "日志级别:debug / info / warn / error") + fs.StringVar(&cfg.DataDir, "data-dir", "data", "数据目录:SQLite 库文件与工具产物的存放位置") + + if err := fs.Parse(args); err != nil { + return nil, err + } + + if cfg.Transport != TransportStdio && cfg.Transport != TransportHTTP { + return nil, fmt.Errorf("未知 transport: %q(支持: %s, %s)", cfg.Transport, TransportStdio, TransportHTTP) + } + + return cfg, nil +} diff --git a/aio-mcp/internal/deps/deps.go b/aio-mcp/internal/deps/deps.go new file mode 100644 index 0000000..b144978 --- /dev/null +++ b/aio-mcp/internal/deps/deps.go @@ -0,0 +1,26 @@ +// Package deps 定义工具共享的依赖容器。 +// +// 工具分组(internal/tools/xxx)在注册时接收同一个 *Deps, +// 避免各工具各自打开数据库连接、各自读配置。 +package deps + +import ( + "aio-mcp/internal/config" +) + +// Deps 是注册工具时注入的共享依赖。 +// +// 目前只有数据目录;后续按需在这里加字段即可: +// SQLite 连接(*sql.DB)、HTTP client、打印机客户端、检测服务客户端等。 +// 由 internal/deps.New 统一构造,各工具只从参数取值,不再自建。 +type Deps struct { + // DataDir 是数据目录,SQLite 库文件与工具产物的根路径。 + DataDir string +} + +// New 依据启动配置构造共享依赖。 +func New(cfg *config.Config) *Deps { + return &Deps{ + DataDir: cfg.DataDir, + } +} diff --git a/aio-mcp/internal/logging/logging.go b/aio-mcp/internal/logging/logging.go new file mode 100644 index 0000000..fcb4536 --- /dev/null +++ b/aio-mcp/internal/logging/logging.go @@ -0,0 +1,27 @@ +// Package logging 负责 aio-mcp 的全局日志初始化。 +package logging + +import ( + "log/slog" + "os" +) + +// Setup 初始化全局 slog 默认 logger。 +// +// 日志固定输出到 stderr:stdio 模式下 stdout 是 MCP 协议通道, +// 任何日志混入都会破坏 JSON-RPC 报文。业务代码同样禁止打印到 stdout。 +func Setup(level string) { + var lvl slog.Level + switch level { + case "debug": + lvl = slog.LevelDebug + case "warn": + lvl = slog.LevelWarn + case "error": + lvl = slog.LevelError + default: + lvl = slog.LevelInfo + } + + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))) +} diff --git a/aio-mcp/internal/server/serve.go b/aio-mcp/internal/server/serve.go new file mode 100644 index 0000000..8f4619e --- /dev/null +++ b/aio-mcp/internal/server/serve.go @@ -0,0 +1,32 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + "net/http" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "aio-mcp/internal/config" +) + +// Serve 按传输方式启动服务器,阻塞直到 stdin 关闭、监听失败或出错。 +func Serve(s *mcp.Server, cfg *config.Config) error { + switch cfg.Transport { + case config.TransportStdio: + slog.Info("aio-mcp starting", "transport", "stdio") + // server.Run 会阻塞,直到 stdin 关闭或出错。 + return s.Run(context.Background(), &mcp.StdioTransport{}) + + case config.TransportHTTP: + handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return s }, nil) + slog.Info("aio-mcp starting", "transport", "http", "addr", cfg.Addr) + return http.ListenAndServe(cfg.Addr, handler) + + default: + // config.Parse 已校验过,这里仅为防御性兜底。 + return fmt.Errorf("未知 transport: %q(支持: %s, %s)", + cfg.Transport, config.TransportStdio, config.TransportHTTP) + } +} diff --git a/aio-mcp/internal/server/server.go b/aio-mcp/internal/server/server.go index 349d09d..9627340 100644 --- a/aio-mcp/internal/server/server.go +++ b/aio-mcp/internal/server/server.go @@ -1,22 +1,26 @@ -// Package server 负责 MCP 服务器的装配:创建 server 实例并注册全部工具。 +// Package server 负责 MCP 服务器的装配与运行。 package server import ( "github.com/modelcontextprotocol/go-sdk/mcp" + "aio-mcp/internal/config" + "aio-mcp/internal/deps" "aio-mcp/internal/tools" ) // New 创建并装配 aio-mcp 服务器。 -// version 为服务器实现版本号,随 MCP initialize 握手上报给客户端。 -func New(version string) *mcp.Server { +// +// version 为服务器实现版本号,随 MCP initialize 握手上报给客户端; +// cfg 决定的共享依赖(数据目录、后续的连接池等)通过 deps 注入各工具。 +func New(version string, cfg *config.Config) *mcp.Server { s := mcp.NewServer(&mcp.Implementation{ Name: "aio-mcp", Version: version, }, nil) - // 注册全部工具。新增工具后在 tools.RegisterAll 里追加即可。 - tools.RegisterAll(s) + // 注册全部工具。新增工具组后在 tools.RegisterAll 里追加即可。 + tools.RegisterAll(s, deps.New(cfg)) return s } diff --git a/aio-mcp/internal/tools/registry.go b/aio-mcp/internal/tools/registry.go index ba9f04e..40c7f89 100644 --- a/aio-mcp/internal/tools/registry.go +++ b/aio-mcp/internal/tools/registry.go @@ -1,26 +1,40 @@ -// Package tools 集中管理 aio-mcp 的全部 MCP 工具。 +// Package tools 是 aio-mcp 的工具注册表:按业务域分组,统一注册。 // -// # 新增工具的固定步骤 +// # 新增一个工具组 // -// 1. 在本目录新建一个文件(如 weather.go),实现 -// func registerXXX(s *mcp.Server) -// 2. 在 RegisterAll 中追加一行 registerXXX(s) +// 1. 在 internal/tools/ 下新建子包(如 sqlite/),实现 +// func Register(s *mcp.Server, d *deps.Deps) +// 2. 在 RegisterAll 中追加一行 sqlite.Register(s, d) +// +// # 在已有组内新增一个工具 +// +// 在对应子包内新建文件(如 sqlite/query.go),实现 +// func registerQuery(s *mcp.Server, d *deps.Deps), +// 并在该组的 Register 中追加一行。 // // 工具通过泛型 mcp.AddTool[In, Out] 注册:输入参数用 struct 定义, // 加 json 与 jsonschema tag(描述、必填),SDK 会自动生成 inputSchema // 并做输入校验,无需手工写 JSON Schema。 // -// 后续工具需要共享状态(SQLite 连接、配置、http client 等)时, -// 把 RegisterAll 演进为 RegisterAll(s *mcp.Server, deps *Deps), -// 在 server.New 里构造 deps 传入即可,各工具文件只改签名。 +// 注意:共享依赖放在 internal/deps 而不是本包, +// 否则工具子包 import 本包会形成循环依赖。 package tools import ( "github.com/modelcontextprotocol/go-sdk/mcp" + + "aio-mcp/internal/deps" + "aio-mcp/internal/tools/system" ) -// RegisterAll 注册所有工具。 -func RegisterAll(s *mcp.Server) { - registerPing(s) - registerEcho(s) +// RegisterAll 注册所有工具组。 +func RegisterAll(s *mcp.Server, d *deps.Deps) { + // 系统组:健康检查与协议演示。 + system.Register(s, d) + + // 待接入的业务组(目录尚未创建,接入时在此追加): + // sqlite.Register(s, d) // 本地数据查询 + // file.Register(s, d) // 文件读写 + // print.Register(s, d) // 打印/报告输出 + // check.Register(s, d) // 硬件检测 } diff --git a/aio-mcp/internal/tools/echo.go b/aio-mcp/internal/tools/system/echo.go similarity index 98% rename from aio-mcp/internal/tools/echo.go rename to aio-mcp/internal/tools/system/echo.go index 9fe9d04..2653933 100644 --- a/aio-mcp/internal/tools/echo.go +++ b/aio-mcp/internal/tools/system/echo.go @@ -1,4 +1,4 @@ -package tools +package system import ( "context" diff --git a/aio-mcp/internal/tools/ping.go b/aio-mcp/internal/tools/system/ping.go similarity index 97% rename from aio-mcp/internal/tools/ping.go rename to aio-mcp/internal/tools/system/ping.go index 6550cb3..102c258 100644 --- a/aio-mcp/internal/tools/ping.go +++ b/aio-mcp/internal/tools/system/ping.go @@ -1,4 +1,4 @@ -package tools +package system import ( "context" diff --git a/aio-mcp/internal/tools/system/system.go b/aio-mcp/internal/tools/system/system.go new file mode 100644 index 0000000..44fe8e4 --- /dev/null +++ b/aio-mcp/internal/tools/system/system.go @@ -0,0 +1,14 @@ +// Package system 承载 aio-mcp 自身的系统类工具:健康检查、协议演示等。 +package system + +import ( + "github.com/modelcontextprotocol/go-sdk/mcp" + + "aio-mcp/internal/deps" +) + +// Register 注册本组全部工具。 +func Register(s *mcp.Server, _ *deps.Deps) { + registerPing(s) + registerEcho(s) +} diff --git a/aio-mcp/main.go b/aio-mcp/main.go deleted file mode 100644 index 3d1012c..0000000 --- a/aio-mcp/main.go +++ /dev/null @@ -1,53 +0,0 @@ -// aio-mcp:aio-system 的自定义 MCP 服务器。 -// -// 作为 AI Agent 的「手」,以 MCP 协议对外暴露工具。 -// 默认以 stdio 传输运行(本地 Agent 直接拉起),也可切换为 -// streamable HTTP 供远程客户端访问。 -package main - -import ( - "context" - "flag" - "fmt" - "log/slog" - "net/http" - "os" - - "github.com/modelcontextprotocol/go-sdk/mcp" - - "aio-mcp/internal/server" -) - -// version 可用 -ldflags "-X main.version=..." 在构建时覆盖。 -var version = "0.1.0" - -func main() { - transport := flag.String("transport", "stdio", "传输方式:stdio(默认,供本地 Agent 拉起)或 http") - addr := flag.String("addr", "localhost:8000", "http 模式下的监听地址") - flag.Parse() - - // 日志统一走 stderr:stdio 模式下 stdout 是 MCP 协议通道,绝不能混入日志。 - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil))) - - svr := server.New(version) - - switch *transport { - case "stdio": - slog.Info("aio-mcp starting", "transport", "stdio") - // server.Run 会阻塞,直到 stdin 关闭或出错。 - if err := svr.Run(context.Background(), &mcp.StdioTransport{}); err != nil { - slog.Error("stdio server failed", "err", err) - os.Exit(1) - } - case "http": - handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return svr }, nil) - slog.Info("aio-mcp starting", "transport", "http", "addr", *addr) - if err := http.ListenAndServe(*addr, handler); err != nil { - slog.Error("http server failed", "err", err) - os.Exit(1) - } - default: - fmt.Fprintf(os.Stderr, "未知 transport: %q(支持: stdio, http)\n", *transport) - os.Exit(2) - } -} diff --git a/office-site/部署说明.md b/office-site/部署说明.md index 08ea573..7f41339 100644 --- a/office-site/部署说明.md +++ b/office-site/部署说明.md @@ -1,2 +1,3 @@ 1. 构建docker 镜像,创建docker compose 文件 2. 部署到机器 100.66.1.5 用户名 admin 密码 admin123 +3. 部署完把归纳一下,部署的经验,到这个文档,方便下次部署的时候使用