Compare commits
3 Commits
093511bbdd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 795ad7660c | |||
| 88939869ff | |||
| 843024d7da |
+1
-1
@@ -1 +1 @@
|
|||||||
OPS_MCP_TRANSPORT=sse
|
OPS_MCP_PORT=9090
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,7 @@
|
|||||||
FROM golang:1.24-alpine AS builder
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
ENV GOPROXY=https://goproxy.cn,direct
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
@@ -15,5 +17,4 @@ RUN apk add --no-cache docker-cli ca-certificates
|
|||||||
COPY --from=builder /server /server
|
COPY --from=builder /server /server
|
||||||
COPY config/ /config/
|
COPY config/ /config/
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
ENTRYPOINT ["/server"]
|
ENTRYPOINT ["/server"]
|
||||||
|
|||||||
@@ -23,13 +23,17 @@ make build && ./ops-mcp
|
|||||||
## Docker Compose 部署
|
## Docker Compose 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 默认端口 9090
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# 自定义端口
|
||||||
|
OPS_MCP_PORT=3000 docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
然后 Claude Desktop 通过 SSE 连接:
|
Claude Desktop SSE 连接(端口改成你设置的):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "mcpServers": { "ops-mcp": { "url": "http://<server>:8080/sse" } } }
|
{ "mcpServers": { "ops-mcp": { "url": "http://<server>:9090/sse" } } }
|
||||||
```
|
```
|
||||||
|
|
||||||
## MCP 工具
|
## MCP 工具
|
||||||
|
|||||||
+2
-2
@@ -17,10 +17,10 @@ func main() {
|
|||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("config load failed, using defaults", "err", err)
|
slog.Warn("config load failed, using defaults", "err", err)
|
||||||
cfg = &config.Config{Transport: "stdio"}
|
cfg = &config.Config{Transport: "stdio", Addr: ":8080"}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Run(cfg.Transport, tool.NewDockerTool()); err != nil {
|
if err := server.Run(cfg.Transport, cfg.Addr, tool.NewDockerTool()); err != nil {
|
||||||
slog.Error("fatal", "err", err)
|
slog.Error("fatal", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Transport string `mapstructure:"transport"` // stdio | sse
|
Transport string `mapstructure:"transport"` // stdio | sse
|
||||||
|
Addr string `mapstructure:"addr"` // 监听地址,默认 :8080
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
@@ -20,8 +21,8 @@ func Load() (*Config, error) {
|
|||||||
v.SetEnvPrefix("OPS_MCP")
|
v.SetEnvPrefix("OPS_MCP")
|
||||||
v.AutomaticEnv()
|
v.AutomaticEnv()
|
||||||
v.SetDefault("transport", "stdio")
|
v.SetDefault("transport", "stdio")
|
||||||
|
v.SetDefault("addr", ":8080")
|
||||||
|
|
||||||
// 配置文件不存在不算错误,直接用默认值 + 环境变量
|
|
||||||
if err := v.ReadInConfig(); err != nil {
|
if err := v.ReadInConfig(); err != nil {
|
||||||
slog.Warn("no config file found, using defaults and env vars", "err", err)
|
slog.Warn("no config file found, using defaults and env vars", "err", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ services:
|
|||||||
image: ops-mcp:latest
|
image: ops-mcp:latest
|
||||||
container_name: ops-mcp
|
container_name: ops-mcp
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${OPS_MCP_PORT:-9090}:${OPS_MCP_PORT:-9090}"
|
||||||
environment:
|
environment:
|
||||||
- OPS_MCP_TRANSPORT=sse
|
- OPS_MCP_TRANSPORT=sse
|
||||||
|
- OPS_MCP_ADDR=:${OPS_MCP_PORT:-9090}
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
const version = "0.1.0"
|
const version = "0.1.0"
|
||||||
|
|
||||||
// Run 启动 MCP Server。
|
// Run 启动 MCP Server。
|
||||||
func Run(transport string, tools ...tool.Tool) error {
|
func Run(transport, addr string, tools ...tool.Tool) error {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@ func Run(transport string, tools ...tool.Tool) error {
|
|||||||
slog.Info("starting MCP server", "transport", "stdio")
|
slog.Info("starting MCP server", "transport", "stdio")
|
||||||
return server.ServeStdio(mcpServer)
|
return server.ServeStdio(mcpServer)
|
||||||
case "sse":
|
case "sse":
|
||||||
slog.Info("starting MCP server", "transport", "sse", "addr", ":8080")
|
slog.Info("starting MCP server", "transport", "sse", "addr", addr)
|
||||||
return server.NewSSEServer(mcpServer).Start(":8080")
|
return server.NewSSEServer(mcpServer).Start(addr)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown transport: %s", transport)
|
return fmt.Errorf("unknown transport: %s", transport)
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-3
@@ -1,6 +1,7 @@
|
|||||||
package tool
|
package tool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,7 +13,7 @@ import (
|
|||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DockerTool 提供 docker logs 查询。
|
// DockerTool 提供 docker logs 查询与实时跟踪。
|
||||||
type DockerTool struct{}
|
type DockerTool struct{}
|
||||||
|
|
||||||
func NewDockerTool() *DockerTool {
|
func NewDockerTool() *DockerTool {
|
||||||
@@ -20,7 +21,7 @@ func NewDockerTool() *DockerTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DockerTool) Name() string { return "docker" }
|
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 {
|
func (d *DockerTool) Initialize(_ context.Context) error {
|
||||||
if _, err := exec.LookPath("docker"); err != nil {
|
if _, err := exec.LookPath("docker"); err != nil {
|
||||||
@@ -38,7 +39,7 @@ func (d *DockerTool) HealthCheck(ctx context.Context) error {
|
|||||||
|
|
||||||
func (d *DockerTool) Register(mcpServer *server.MCPServer) error {
|
func (d *DockerTool) Register(mcpServer *server.MCPServer) error {
|
||||||
mcpServer.AddTool(mcp.NewTool("docker_logs",
|
mcpServer.AddTool(mcp.NewTool("docker_logs",
|
||||||
mcp.WithDescription("获取 Docker 容器日志,等价于 docker logs --tail N <container>"),
|
mcp.WithDescription("获取 Docker 容器日志,等价于 docker logs --tail N <container>;follow 模式通过 SSE 实时推送日志流"),
|
||||||
mcp.WithString("container",
|
mcp.WithString("container",
|
||||||
mcp.Required(),
|
mcp.Required(),
|
||||||
mcp.Description("容器名称或 ID"),
|
mcp.Description("容器名称或 ID"),
|
||||||
@@ -46,6 +47,9 @@ func (d *DockerTool) Register(mcpServer *server.MCPServer) error {
|
|||||||
mcp.WithNumber("tail",
|
mcp.WithNumber("tail",
|
||||||
mcp.Description("返回最后 N 行日志,默认 100"),
|
mcp.Description("返回最后 N 行日志,默认 100"),
|
||||||
),
|
),
|
||||||
|
mcp.WithBoolean("follow",
|
||||||
|
mcp.Description("是否持续跟踪日志输出(类似 docker logs -f),默认 false。SSE 模式下日志行通过 notifications/docker_logs/stream 推送"),
|
||||||
|
),
|
||||||
), d.handleLogs)
|
), d.handleLogs)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -63,6 +67,18 @@ func (d *DockerTool) handleLogs(ctx context.Context, req mcp.CallToolRequest) (*
|
|||||||
tail = int(t)
|
tail = int(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
follow := false
|
||||||
|
if f, ok := args["follow"].(bool); ok {
|
||||||
|
follow = f
|
||||||
|
}
|
||||||
|
|
||||||
|
if !follow {
|
||||||
|
return d.oneShotLogs(ctx, container, tail)
|
||||||
|
}
|
||||||
|
return d.followLogs(ctx, container, tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerTool) oneShotLogs(ctx context.Context, container string, tail int) (*mcp.CallToolResult, error) {
|
||||||
cmd := exec.CommandContext(ctx, "docker", "logs",
|
cmd := exec.CommandContext(ctx, "docker", "logs",
|
||||||
"--tail", strconv.Itoa(tail),
|
"--tail", strconv.Itoa(tail),
|
||||||
"--timestamps",
|
"--timestamps",
|
||||||
@@ -86,3 +102,82 @@ func (d *DockerTool) handleLogs(ctx context.Context, req mcp.CallToolRequest) (*
|
|||||||
|
|
||||||
return mcp.NewToolResultText(output), nil
|
return mcp.NewToolResultText(output), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DockerTool) followLogs(ctx context.Context, container string, tail int) (*mcp.CallToolResult, error) {
|
||||||
|
srv := server.ServerFromContext(ctx)
|
||||||
|
|
||||||
|
followCtx, cancelFollow := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(followCtx, "docker", "logs",
|
||||||
|
"--tail", strconv.Itoa(tail),
|
||||||
|
"--timestamps",
|
||||||
|
"-f",
|
||||||
|
container,
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
cancelFollow()
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("创建管道失败: %v", err)), nil
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
cancelFollow()
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("创建管道失败: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
cancelFollow()
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("docker logs 启动失败: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后台读取 stderr
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
slog.Warn("docker logs stderr", "container", container, "line", scanner.Text())
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
slog.Warn("docker logs stderr scanner error", "container", container, "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 后台流式推送 stdout
|
||||||
|
go func() {
|
||||||
|
defer cancelFollow()
|
||||||
|
defer func() {
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
slog.Warn("docker logs -f exited with error", "container", container, "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if srv == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := srv.SendNotificationToClient(ctx, "notifications/docker_logs/stream", map[string]any{
|
||||||
|
"container": container,
|
||||||
|
"line": line,
|
||||||
|
}); err != nil {
|
||||||
|
slog.Debug("docker logs notification failed, stopping stream", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
slog.Warn("docker logs stdout scanner error", "container", container, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv != nil {
|
||||||
|
_ = srv.SendNotificationToClient(ctx, "notifications/docker_logs/stream", map[string]any{
|
||||||
|
"container": container,
|
||||||
|
"line": "--- 日志流结束 ---",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(
|
||||||
|
fmt.Sprintf("开始跟踪容器 %s 日志(tail=%d, follow=true)…", container, tail),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user