diff --git a/docs/15-AI-MCP-Agent.md b/docs/15-AI-MCP-Agent.md index 4161da2..b5bda9b 100644 --- a/docs/15-AI-MCP-Agent.md +++ b/docs/15-AI-MCP-Agent.md @@ -191,3 +191,8 @@ go build -o ../bin/opsxdb-mcp ./cmd/opsxdb-mcp 4. **MCP 状态**:设置 → AI/MCP(开关+白名单);有活跃外部会话时标题旁小点「MCP」,悬停摘要,点开最近调用可断开;无活跃不显示。 落地次序:MCP 开关+白名单 → Ask|Agent Seg → 时间线与插入 → MCP 状态点。 + + +### 真库验收 2026-10-15 + +MySQL 3306 demo + Redis 6379:`go run ./cmd/opsxdb-mcp-accept/` 全过。实践文档:`~/Downloads/AI-Agent接入GUI工具开发实践.md`。 diff --git a/server/cmd/opsxdb-mcp-accept/main.go b/server/cmd/opsxdb-mcp-accept/main.go new file mode 100644 index 0000000..4641dae --- /dev/null +++ b/server/cmd/opsxdb-mcp-accept/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "opsxdb/server/internal/aitools" +) + +func must(ok bool, msg string) { + if !ok { + fmt.Println("FAIL:", msg) + os.Exit(1) + } + fmt.Println("OK:", msg) +} + +func dump(v any) string { + b, _ := json.MarshalIndent(v, "", " ") + return string(b) +} + +func main() { + rt, err := aitools.DefaultRuntime() + if err != nil { + panic(err) + } + mysqlID := "accept-mysql-demo" + redisID := "accept-redis-demo" + ctx := context.Background() + + out, err := rt.Call(ctx, aitools.ToolListConnections, map[string]any{}) + s := dump(out) + must(err == nil && strings.Contains(s, mysqlID) && !strings.Contains(s, `"password"`), "list_connections") + + out, err = rt.Call(ctx, aitools.ToolListDatabases, map[string]any{"connection_id": mysqlID}) + s = dump(out) + must(err == nil && strings.Contains(s, "demo"), "list_databases: "+fmt.Sprint(err)+" "+s) + + out, err = rt.Call(ctx, aitools.ToolListTables, map[string]any{"connection_id": mysqlID, "database": "demo"}) + s = dump(out) + must(err == nil, "list_tables: "+fmt.Sprint(err)+" "+s) + fmt.Println(s[:min(600, len(s))]) + + var tables []string + if m, ok := out.(map[string]any); ok { + if arr, ok := m["tables"].([]any); ok { + for _, x := range arr { + tables = append(tables, fmt.Sprint(x)) + } + } + } + if len(tables) > 0 { + t := tables[0] + _, err = rt.Call(ctx, aitools.ToolDescribeTable, map[string]any{"connection_id": mysqlID, "database": "demo", "table": t}) + must(err == nil, "describe_table "+t+": "+fmt.Sprint(err)) + out, err = rt.Call(ctx, aitools.ToolGetSchemaContext, map[string]any{"connection_id": mysqlID, "database": "demo"}) + must(err == nil && len(dump(out)) > 20, "get_schema_context") + _ = out + } + + out, err = rt.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ + "connection_id": mysqlID, + "database": "demo", + "sql": "SELECT 1 AS n, DATABASE() AS db", + }) + s = dump(out) + must(err == nil && strings.Contains(s, "demo"), "execute_query SELECT: "+fmt.Sprint(err)+" "+s) + fmt.Println(s[:min(500, len(s))]) + + _, err = rt.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ + "connection_id": mysqlID, + "sql": "DROP TABLE IF EXISTS __opsxdb_should_deny__", + }) + must(err != nil, "DROP must fail, got nil") + + out, err = rt.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ + "connection_id": redisID, + "command": "PING", + }) + s = dump(out) + must(err == nil && strings.Contains(strings.ToUpper(s), "PONG"), "redis PING: "+fmt.Sprint(err)+" "+s) + + _, err = rt.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ + "connection_id": redisID, + "command": "FLUSHDB", + }) + must(err != nil, "FLUSHDB must fail") + + _, err = rt.Call(ctx, aitools.ToolListTables, map[string]any{"connection_id": "not-allowed"}) + must(err != nil, "whitelist reject") + + fmt.Println("ALL ACCEPTANCE PASSED") +} + +func min(a, b int) int { + if a < b { + return a + } + return b +}