精简项目:只保留 docker_logs + health + info 三个工具

- 移除 database/middleware 等复杂模块,先聚焦核心功能
- docker_logs:封装 docker logs --tail N <container>
- 修复 go.mod 版本为 1.24 匹配 Docker 构建镜像
- Dockerfile 改用 alpine + docker-cli,避免 scratch 无 shell 问题
- docker-compose.yml 简化为单服务定义

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yangzhaohan
2026-07-06 10:42:23 +08:00
parent fb2c3a6cae
commit 093511bbdd
23 changed files with 144 additions and 1352 deletions
+41 -231
View File
@@ -3,276 +3,86 @@ package tool
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"os/exec"
"regexp"
"strings"
"time"
"strconv"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// DockerTool 提供安全的 Docker 容器信息查询工具
// 使用 docker CLI 而非 SDK,避免跨平台编译问题,且更轻量。
type DockerTool struct {
containerRegex *regexp.Regexp
maxLogBytes int64
maxLogLines int
maxLogSince time.Duration
}
// DockerTool 提供 docker logs 查询
type DockerTool struct{}
type DockerOpts struct {
ContainerPattern string
MaxLogBytes int64
MaxLogLines int
MaxLogSince time.Duration
}
func NewDockerTool(opts DockerOpts) (*DockerTool, error) {
pattern := opts.ContainerPattern
if pattern == "" {
pattern = ".*"
}
re, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("compile container pattern: %w", err)
}
return &DockerTool{
containerRegex: re,
maxLogBytes: opts.MaxLogBytes,
maxLogLines: opts.MaxLogLines,
maxLogSince: opts.MaxLogSince,
}, nil
func NewDockerTool() *DockerTool {
return &DockerTool{}
}
func (d *DockerTool) Name() string { return "docker" }
func (d *DockerTool) Description() string { return "Docker 容器日志和安全查询" }
func (d *DockerTool) Description() string { return "Docker 容器日志查询" }
func (d *DockerTool) Initialize(_ context.Context) error {
if _, err := exec.LookPath("docker"); err != nil {
return fmt.Errorf("docker CLI not found in PATH")
return fmt.Errorf("docker CLI not found: %w", err)
}
slog.Info("docker CLI found")
slog.Info("docker CLI ready")
return nil
}
func (d *DockerTool) Shutdown(_ context.Context) error { return nil }
func (d *DockerTool) HealthCheck(ctx context.Context) error {
return d.dockerCmd(ctx, "version").Run()
return exec.CommandContext(ctx, "docker", "version").Run()
}
func (d *DockerTool) Register(mcpServer *server.MCPServer) error {
mcpServer.AddTool(mcp.NewTool("docker_ps",
mcp.WithDescription("列出 Docker 容器"),
mcp.WithString("filter", mcp.Description("按名称过滤(支持正则)")),
mcp.WithBoolean("all", mcp.Description("是否包含已停止的容器,默认 false")),
), d.handleList)
mcpServer.AddTool(mcp.NewTool("docker_logs",
mcp.WithDescription("获取容器日志(受白名单和大小限制保护)"),
mcp.WithString("container", mcp.Required(), mcp.Description("容器名称或 ID")),
mcp.WithNumber("tail", mcp.Description("返回最后 N 行,默认 100")),
mcp.WithString("since", mcp.Description("从多久前开始,如 15m、1h,默认 15m")),
mcp.WithDescription("获取 Docker 容器日志,等价于 docker logs --tail N <container>"),
mcp.WithString("container",
mcp.Required(),
mcp.Description("容器名称或 ID"),
),
mcp.WithNumber("tail",
mcp.Description("返回最后 N 行日志,默认 100"),
),
), d.handleLogs)
mcpServer.AddTool(mcp.NewTool("docker_inspect",
mcp.WithDescription("查看容器详细信息(受白名单保护)"),
mcp.WithString("container", mcp.Required(), mcp.Description("容器名称或 ID")),
), d.handleInspect)
return nil
}
// --- 安全校验 ---
func (d *DockerTool) validateContainer(name string) error {
if !d.containerRegex.MatchString(name) {
return fmt.Errorf("container %q not in allowed pattern", name)
}
return nil
}
// --- docker CLI 封装 ---
func (d *DockerTool) dockerCmd(ctx context.Context, args ...string) *exec.Cmd {
return exec.CommandContext(ctx, "docker", args...)
}
func (d *DockerTool) dockerOutput(ctx context.Context, args ...string) ([]byte, error) {
cmd := d.dockerCmd(ctx, args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("docker %s: %v\n%s", strings.Join(args, " "), err, stderr.String())
}
return output, nil
}
// --- Handlers ---
func (d *DockerTool) handleList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := getArgs(req)
filterName, _ := args["filter"].(string)
showAll, _ := args["all"].(bool)
dockerArgs := []string{"ps", "--format", "{{json .}}"}
if showAll {
dockerArgs = append(dockerArgs, "-a")
}
output, err := d.dockerOutput(ctx, dockerArgs...)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
type dockerPsRow struct {
Names string `json:"Names"`
Image string `json:"Image"`
Status string `json:"Status"`
CreatedAt string `json:"CreatedAt"`
Ports string `json:"Ports"`
}
var result []dockerPsRow
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
if line == "" {
continue
}
var row dockerPsRow
if err := json.Unmarshal([]byte(line), &row); err != nil {
continue
}
// 应用名称过滤
if filterName != "" {
re, err := regexp.Compile(filterName)
if err != nil {
continue
}
if !re.MatchString(row.Names) {
continue
}
}
// 白名单检查
if err := d.validateContainer(row.Names); err != nil {
continue
}
result = append(result, row)
}
data, _ := json.MarshalIndent(result, "", " ")
return mcp.NewToolResultText(fmt.Sprintf("```json\n%s\n```", string(data))), nil
}
func (d *DockerTool) handleLogs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := getArgs(req)
containerName, _ := args["container"].(string)
if err := d.validateContainer(containerName); err != nil {
return mcp.NewToolResultError(err.Error()), nil
container, _ := args["container"].(string)
if container == "" {
return mcp.NewToolResultError("container 参数必填"), nil
}
tail := "100"
if t, ok := args["tail"].(float64); ok {
tail = fmt.Sprintf("%d", int(t))
tail := 100
if t, ok := args["tail"].(float64); ok && t > 0 {
tail = int(t)
}
since := "15m"
if s, ok := args["since"].(string); ok && s != "" {
dur, err := time.ParseDuration(s)
if err == nil && d.maxLogSince > 0 && dur > d.maxLogSince {
since = d.maxLogSince.String()
} else {
since = s
}
cmd := exec.CommandContext(ctx, "docker", "logs",
"--tail", strconv.Itoa(tail),
"--timestamps",
container,
)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return mcp.NewToolResultError(
fmt.Sprintf("docker logs 失败: %v\n%s", err, stderr.String()),
), nil
}
output, err := d.dockerOutput(ctx, "logs", "--tail", tail, "--since", since, "--timestamps", containerName)
if err != nil {
// docker logs 返回非 0 时容器可能不存在
return mcp.NewToolResultError(fmt.Sprintf("docker logs failed: %v\nOutput: %s", err, string(output))), nil
output := stdout.String()
if output == "" {
output = "(容器没有日志输出)"
}
// 截断字节数
text := string(output)
if len(text) > int(d.maxLogBytes) {
text = text[len(text)-int(d.maxLogBytes):]
}
// 截断行数
lines := strings.Split(text, "\n")
if len(lines) > d.maxLogLines {
lines = lines[len(lines)-d.maxLogLines:]
}
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
}
func (d *DockerTool) handleInspect(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := getArgs(req)
containerName, _ := args["container"].(string)
if err := d.validateContainer(containerName); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
output, err := d.dockerOutput(ctx, "inspect", containerName)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// 只提取安全字段
var inspects []struct {
Name string `json:"Name"`
ID string `json:"Id"`
Image string `json:"Image"`
State struct {
Status string `json:"Status"`
Running bool `json:"Running"`
StartedAt string `json:"StartedAt"`
Pid int `json:"Pid"`
} `json:"State"`
Created string `json:"Created"`
Config struct {
Image string `json:"Image"`
Env []string `json:"Env"`
} `json:"Config"`
Mounts []struct {
Source string `json:"Source"`
Destination string `json:"Destination"`
Mode string `json:"Mode"`
} `json:"Mounts"`
}
if err := json.Unmarshal(output, &inspects); err != nil {
return mcp.NewToolResultError(fmt.Sprintf("parse inspect: %v", err)), nil
}
if len(inspects) == 0 {
return mcp.NewToolResultError("container not found"), nil
}
insp := inspects[0]
// 不暴露 Env(含密钥),只暴露安全信息
safe := map[string]any{
"name": strings.TrimPrefix(insp.Name, "/"),
"id": insp.ID[:12],
"image": insp.Config.Image,
"state": insp.State,
"created": insp.Created,
"mounts": insp.Mounts,
}
data, _ := json.MarshalIndent(safe, "", " ")
return mcp.NewToolResultText(fmt.Sprintf("```json\n%s\n```", string(data))), nil
return mcp.NewToolResultText(output), nil
}