Files
opsxdb/server/cmd/opsxdb-mcp/main.go
T
logan 0b68c8344d feat(mcp): P0 只读 MCP tool runtime + opsxdb-mcp stdio
- aitools 共用 runtime:list/describe/schema/query/redis(纯只读,无写确认)
- 白名单 ~/.opsxdb/mcp.json;审计 mcp-audit.jsonl;复用 danger.go
- 桌面 connections 双写 ~/.opsxdb/connections.json(ox1. 解码)
- Cursor 配置示例与任务书验收勾选更新;写操作与 Agent UI 留 P1
2026-10-15 11:14:08 +08:00

110 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// opsxdb-mcp:stdio MCP Server,暴露只读数据库工具(P0)。 // Logan.Li
//
// 配置:
// ~/.opsxdb/mcp.json — allowed_connection_ids;mode(P0 执行层强制只读)
// ~/.opsxdb/connections.json — 桌面双写的连接列表(密码 ox1. 混淆)
// ~/.opsxdb/logs/mcp-audit.jsonl
//
// Cursor 示例见 docs/mcp-cursor.example.json
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"opsxdb/server/internal/aitools"
)
func main() {
rt, err := aitools.DefaultRuntime()
if err != nil {
fmt.Fprintf(os.Stderr, "opsxdb-mcp: load policy: %v\n", err)
os.Exit(1)
}
s := server.NewMCPServer(
"opsxdb",
"0.1.0",
server.WithToolCapabilities(true),
)
register := func(name, desc string, props map[string]any, required []string) {
schema := map[string]any{
"type": "object",
"properties": props,
}
if len(required) > 0 {
schema["required"] = required
}
raw, _ := json.Marshal(schema)
t := mcp.NewToolWithRawSchema(name, desc, raw)
toolName := name
s.AddTool(t, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
out, err := rt.Call(ctx, toolName, args)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(aitools.ResultJSON(out)), nil
})
}
connID := map[string]any{"type": "string", "description": "Saved connection id from opsxdb_list_connections"}
database := map[string]any{"type": "string", "description": "Database / schema / Redis DB index"}
register(aitools.ToolListConnections,
"List whitelisted OpsxDB connections (id/name/driver/host/port/database; never passwords).",
map[string]any{}, nil)
register(aitools.ToolListDatabases,
"List databases for a connection.",
map[string]any{"connection_id": connID}, []string{"connection_id"})
register(aitools.ToolListTables,
"List tables/views (or engine equivalents) in a database.",
map[string]any{"connection_id": connID, "database": database}, []string{"connection_id"})
register(aitools.ToolDescribeTable,
"Describe columns and metadata for a table.",
map[string]any{
"connection_id": connID,
"database": database,
"table": map[string]any{"type": "string", "description": "Table name"},
}, []string{"connection_id", "table"})
register(aitools.ToolGetSchemaContext,
"Compact schema text for LLM context (capped ~4k chars).",
map[string]any{
"connection_id": connID,
"database": database,
"max_tables": map[string]any{"type": "integer", "description": "Max tables to include (default 40)"},
}, []string{"connection_id"})
register(aitools.ToolExecuteQuery,
"Execute a READ-ONLY SQL query (SELECT/SHOW/EXPLAIN/WITH…SELECT). Writes and DDL are rejected in P0. Default limit 100, max 500.",
map[string]any{
"connection_id": connID,
"sql": map[string]any{"type": "string", "description": "Read-only SQL"},
"database": database,
"limit": map[string]any{"type": "integer", "description": "Row limit (default 100, max 500)"},
}, []string{"connection_id", "sql"})
register(aitools.ToolExecuteRedisCommand,
"Execute a READ-ONLY Redis command (GET/SCAN/TYPE/TTL/…). FLUSH*/KEYS and writes are rejected in P0.",
map[string]any{
"connection_id": connID,
"command": map[string]any{"type": "string", "description": "Single Redis command line"},
"database": database,
}, []string{"connection_id", "command"})
if err := server.ServeStdio(s); err != nil {
fmt.Fprintf(os.Stderr, "opsxdb-mcp: %v\n", err)
os.Exit(1)
}
}