feat: add constrained read-only diagnostics

This commit is contained in:
kory
2026-10-02 14:32:40 +08:00
parent 2a072f9cc9
commit 5e6281ae99
8 changed files with 219 additions and 1 deletions
+24
View File
@@ -12,6 +12,7 @@ import (
"time"
"github.com/wjyhk/vps-commander/internal/auth"
"github.com/wjyhk/vps-commander/internal/executor"
)
// MCP Protocol / JSON-RPC 2.0 Types
@@ -77,6 +78,10 @@ type ActionBackend interface {
EditBlock(ctx context.Context, clientIP, device, path, oldText, newText string) (any, error)
}
type DiagnosticBackend interface {
DiagnosticCommand(ctx context.Context, clientIP, device string, req executor.DiagnosticRequest) (any, error)
}
type ExtendedBackend interface {
ReadMultipleFiles(ctx context.Context, clientIP, device string, paths []string, limit int64) (any, error)
CreateDirectory(ctx context.Context, clientIP, device, path string) (any, error)
@@ -280,6 +285,7 @@ func (s *Server) handleRequest(ctx context.Context, r *http.Request, req JSONRPC
{Name: "create_directory", Description: "创建目录", InputSchema: InputSchema{Type: "object", Properties: map[string]PropertyDef{"device": {Type: "string", Description: "目标设备"}, "path": {Type: "string", Description: "目录路径"}}, Required: []string{"path"}}},
{Name: "move_file", Description: "移动或重命名文件", InputSchema: InputSchema{Type: "object", Properties: map[string]PropertyDef{"device": {Type: "string", Description: "目标设备"}, "source": {Type: "string", Description: "源路径"}, "destination": {Type: "string", Description: "目标路径"}}, Required: []string{"source", "destination"}}},
{Name: "list_processes", Description: "列出受控节点当前进程", InputSchema: InputSchema{Type: "object", Properties: map[string]PropertyDef{"device": {Type: "string", Description: "目标设备"}}}},
{Name: "diagnostic_command", Description: "执行严格白名单的只读系统诊断,不接受任意 Shell", InputSchema: InputSchema{Type: "object", Properties: map[string]PropertyDef{"device": {Type: "string", Description: "目标设备"}, "action": {Type: "string", Description: "docker_ps|docker_logs|docker_inspect|systemctl_status|journalctl|ss_listen|df|free|uptime"}, "target": {Type: "string", Description: "容器名或 systemd unit(按 action 使用)"}, "lines": {Type: "integer", Description: "docker_logs 最大行数,1-500"}}, Required: []string{"action"}}},
{
Name: "edit_block",
Description: "对指定文件执行唯一文本块的原子替换;old_text 必须恰好匹配一次",
@@ -430,6 +436,24 @@ func (s *Server) callTool(ctx context.Context, clientIP string, params CallToolP
return map[string]string{"error": err.Error()}, true
}
return res, false
case "diagnostic_command":
var a struct {
Device string `json:"device"`
Action string `json:"action"`
Target string `json:"target"`
Lines int `json:"lines"`
}
_ = json.Unmarshal(params.Arguments, &a)
b, ok := s.Backend.(DiagnosticBackend)
if !ok {
return map[string]string{"error": "diagnostic tools unavailable"}, true
}
res, err := b.DiagnosticCommand(ctx, clientIP, a.Device, executor.DiagnosticRequest{Action: a.Action, Target: a.Target, Lines: a.Lines})
if err != nil {
return map[string]string{"error": err.Error()}, true
}
return res, false
case "list_processes":
var a struct {
Device string `json:"device"`
+1 -1
View File
@@ -79,7 +79,7 @@ func TestMCPInitializeAndListTools(t *testing.T) {
t.Fatalf("expected map result")
}
tools, ok := resMap["tools"].([]any)
if !ok || len(tools) != 11 {
if !ok || len(tools) != 12 {
t.Fatalf("expected 11 tools, got %d", len(tools))
}
foundWriteFile := false