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
This commit is contained in:
logan committed 2026-10-15 11:14:08 +08:00
1 parent 5c17d32647
commit 0b68c8344d
19 files changed
+1989 -79

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
package aitools
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"opsxdb/server/internal/db"
)
// SavedConnection 与桌面 store SavedConnection JSON 对齐(磁盘形态,密码可为 ox1. 混淆)。 // Logan.Li
type SavedConnection struct {
ID string `json:"id"`
Name string `json:"name"`
Driver string `json:"driver"`
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
Database string `json:"database"`
Service string `json:"service,omitempty"`
SSL bool `json:"ssl,omitempty"`
SSLMode string `json:"sslMode,omitempty"`
SSLCA string `json:"sslCA,omitempty"`
SSLCert string `json:"sslCert,omitempty"`
SSLKey string `json:"sslKey,omitempty"`
RedisMode string `json:"redisMode,omitempty"`
RedisNodes string `json:"redisNodes,omitempty"`
MasterName string `json:"masterName,omitempty"`
SentinelPassword string `json:"sentinelPassword,omitempty"`
SSHEnabled bool `json:"sshEnabled,omitempty"`
SSHHost string `json:"sshHost,omitempty"`
SSHPort int `json:"sshPort,omitempty"`
SSHUser string `json:"sshUser,omitempty"`
SSHAuth string `json:"sshAuth,omitempty"`
SSHPassword string `json:"sshPassword,omitempty"`
SSHPrivateKey string `json:"sshPrivateKey,omitempty"`
SSHPassphrase string `json:"sshPassphrase,omitempty"`
SavePassword *bool `json:"savePassword,omitempty"`
}
// ConnectionSummary 列表对外字段,永不含密码。
type ConnectionSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Driver string `json:"driver"`
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
}
func ConnectionsPath() (string, error) {
dir, err := opsxdbDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "connections.json"), nil
}
// LoadConnections 读 ~/.opsxdb/connections.json。
func LoadConnections() ([]SavedConnection, error) {
path, err := ConnectionsPath()
if err != nil {
return nil, err
}
b, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var list []SavedConnection
if err := json.Unmarshal(b, &list); err != nil {
return nil, fmt.Errorf("connections.json: %w", err)
}
return list, nil
}
// SaveConnections 写 ~/.opsxdb/connections.json(0600);供桌面双写 API 调用。
func SaveConnections(list []SavedConnection) error {
path, err := ConnectionsPath()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
b, err := json.MarshalIndent(list, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, b, 0600)
}
func FindConnection(id string) (*SavedConnection, error) {
list, err := LoadConnections()
if err != nil {
return nil, err
}
for i := range list {
if list[i].ID == id {
c := list[i]
return &c, nil
}
}
return nil, fmt.Errorf("connection not found: %s", id)
}
// ToConnReq 映射为 db.ConnReq(解码 ox1. 密码)。
func (c SavedConnection) ToConnReq() db.ConnReq {
sslOn := c.SSL || (c.SSLMode != "" && c.SSLMode != "disable")
mode := c.SSLMode
if sslOn {
if mode == "" || mode == "disable" {
mode = "require"
}
} else {
mode = "disable"
}
return db.ConnReq{
Driver: c.Driver,
Host: c.Host,
Port: c.Port,
User: c.User,
Password: DecodeSecret(c.Password),
Database: c.Database,
Service: c.Service,
SSL: sslOn,
SSLMode: mode,
SSLCA: c.SSLCA,
SSLCert: c.SSLCert,
SSLKey: c.SSLKey,
RedisMode: orDefault(c.RedisMode, "standalone"),
RedisNodes: c.RedisNodes,
MasterName: c.MasterName,
SentinelPassword: DecodeSecret(c.SentinelPassword),
SSHEnabled: c.SSHEnabled,
SSHHost: c.SSHHost,
SSHPort: c.SSHPort,
SSHUser: c.SSHUser,
SSHAuth: c.SSHAuth,
SSHPassword: DecodeSecret(c.SSHPassword),
SSHPrivateKey: c.SSHPrivateKey,
SSHPassphrase: DecodeSecret(c.SSHPassphrase),
}
}
func (c SavedConnection) Summary() ConnectionSummary {
return ConnectionSummary{
ID: c.ID,
Name: c.Name,
Driver: c.Driver,
Host: c.Host,
Port: c.Port,
Database: c.Database,
}
}
func orDefault(s, def string) string {
if strings.TrimSpace(s) == "" {
return def
}
return s
}