// 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) } }