Files

178 lines
5.3 KiB
Go

package mcp
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type mockBackend struct{}
func (m *mockBackend) ListDevices(ctx context.Context) (any, error) {
return []map[string]any{{"name": "local", "status": "online"}}, nil
}
func (m *mockBackend) ExecCommand(ctx context.Context, clientIP, device, command, workdir string, timeout int) (any, error) {
return map[string]any{"device": device, "exit_code": 0, "stdout": "ok"}, nil
}
func (m *mockBackend) ReadFile(ctx context.Context, clientIP, device, path string, offset, limit int64) (any, error) {
return map[string]any{"device": device, "path": path, "content": "hello"}, nil
}
func (m *mockBackend) WriteFile(ctx context.Context, clientIP, device, path, content string) (any, error) {
return map[string]any{"device": device, "path": path, "bytes": len(content)}, nil
}
func (m *mockBackend) EditBlock(ctx context.Context, clientIP, device, path, oldText, newText string) (any, error) {
return map[string]any{"device": device, "path": path, "bytes_written": len(newText)}, nil
}
func TestMCPInitializeAndListTools(t *testing.T) {
srv := NewServer(&mockBackend{}, nil)
// Test Initialize
initReq := JSONRPCRequest{
JSONRPC: "2.0",
ID: 1,
Method: "initialize",
}
body, _ := json.Marshal(initReq)
req := httptest.NewRequest("POST", "/mcp/message", bytes.NewReader(body))
w := httptest.NewRecorder()
srv.HandleMessage(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var resp JSONRPCResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error)
}
// Test tools/list
listReq := JSONRPCRequest{
JSONRPC: "2.0",
ID: 2,
Method: "tools/list",
}
body, _ = json.Marshal(listReq)
req = httptest.NewRequest("POST", "/mcp/message", bytes.NewReader(body))
w = httptest.NewRecorder()
srv.HandleMessage(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var listResp JSONRPCResponse
if err := json.Unmarshal(w.Body.Bytes(), &listResp); err != nil {
t.Fatal(err)
}
resMap, ok := listResp.Result.(map[string]any)
if !ok {
t.Fatalf("expected map result")
}
tools, ok := resMap["tools"].([]any)
if !ok || len(tools) != 12 {
t.Fatalf("expected 11 tools, got %d", len(tools))
}
foundWriteFile := false
for _, raw := range tools {
tool, ok := raw.(map[string]any)
if !ok {
t.Fatalf("tool entry is not an object: %#v", raw)
}
name, _ := tool["name"].(string)
if name == "" {
t.Fatalf("tool entry has empty name: %#v", tool)
}
if name == "write_file" {
foundWriteFile = true
}
}
if !foundWriteFile {
t.Fatalf("write_file missing from tools/list")
}
}
func TestMCPCallTool(t *testing.T) {
srv := NewServer(&mockBackend{}, nil)
callReq := JSONRPCRequest{
JSONRPC: "2.0",
ID: 3,
Method: "tools/call",
Params: json.RawMessage(`{"name":"exec_command","arguments":{"command":"echo test"}}`),
}
body, _ := json.Marshal(callReq)
req := httptest.NewRequest("POST", "/mcp/message", bytes.NewReader(body))
w := httptest.NewRecorder()
srv.HandleMessage(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var resp JSONRPCResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error)
}
contentStr := string(w.Body.Bytes())
if !strings.Contains(contentStr, "stdout") {
t.Fatalf("expected stdout in result, got %s", contentStr)
}
}
type gatedMockBackend struct {
mockBackend
deniedAction string
}
func (g *gatedMockBackend) AuthorizeMCP(device, action, clientIP string) error {
if action == g.deniedAction {
return fmt.Errorf("denied %s", action)
}
return nil
}
func (g *gatedMockBackend) ListAgentMCP(ctx context.Context, device string) (any, error) {
return map[string]any{"device": device}, nil
}
func (g *gatedMockBackend) CallAgentMCP(ctx context.Context, clientIP, device, server, tool string, args any) (any, error) {
return map[string]any{"device": device, "server": server, "tool": tool}, nil
}
func TestMCPAgentCallUsesSecurityGate(t *testing.T) {
backend := &gatedMockBackend{deniedAction: "mcp"}
srv := NewServer(backend, nil)
params := json.RawMessage("{\"name\":\"call_agent_mcp\",\"arguments\":{\"device\":\"node1\",\"server\":\"s\",\"tool\":\"t\"}}")
req := JSONRPCRequest{JSONRPC: "2.0", ID: 10, Method: "tools/call", Params: params}
resp := srv.HandleDirectRequest(context.Background(), req)
result, ok := resp.Result.(CallToolResult)
if !ok || !result.IsError || !strings.Contains(result.Content[0].Text, "denied mcp") {
t.Fatalf("expected MCP denial, got %#v", resp)
}
}
func TestMCPListAgentUsesReadGate(t *testing.T) {
backend := &gatedMockBackend{deniedAction: "read"}
srv := NewServer(backend, nil)
params := json.RawMessage("{\"name\":\"list_agent_mcp\",\"arguments\":{\"device\":\"node1\"}}")
req := JSONRPCRequest{JSONRPC: "2.0", ID: 11, Method: "tools/call", Params: params}
resp := srv.HandleDirectRequest(context.Background(), req)
result, ok := resp.Result.(CallToolResult)
if !ok || !result.IsError || !strings.Contains(result.Content[0].Text, "denied read") {
t.Fatalf("expected MCP list denial, got %#v", resp)
}
}