Files
multica/server/internal/daemon/execenv/cursor_mcp_test.go
Bohan Jiang f415099c4a MUL-3263: support managed MCP config for Cursor (#4081)
* feat: support managed MCP config for Cursor

Co-authored-by: multica-agent <github@multica.ai>

* fix: address Cursor MCP review feedback

Co-authored-by: multica-agent <github@multica.ai>

* docs: include Cursor in skills MCP support

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-13 02:07:00 +08:00

164 lines
5.1 KiB
Go

package execenv
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestCursorMcpApprovalKeyMatchesCursorAgent(t *testing.T) {
t.Parallel()
keys, err := cursorMcpApprovalKeys("/tmp/work", map[string]json.RawMessage{
"fetch": json.RawMessage(`{"command":"uvx","args":["mcp-server-fetch"]}`),
})
if err != nil {
t.Fatalf("cursorMcpApprovalKeys: %v", err)
}
want := []string{"fetch-b3a6127d3cbd8e52"}
if !reflect.DeepEqual(keys, want) {
t.Fatalf("approval keys = %v, want %v", keys, want)
}
}
func TestPrepareCursorMcpConfigWritesProjectConfigAndApprovals(t *testing.T) {
t.Parallel()
envRoot := t.TempDir()
workDir := filepath.Join(envRoot, "workdir")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatalf("mkdir workDir: %v", err)
}
manifest := &sidecarManifest{}
mcpConfig := json.RawMessage(`{
"mcpServers": {
"fetch": {"command":"uvx","args":["mcp-server-fetch"]},
"http": {"url":"https://mcp.example.com","type":"http"}
}
}`)
cursorDataDir, err := prepareCursorMcpConfig(envRoot, workDir, mcpConfig, manifest)
if err != nil {
t.Fatalf("prepareCursorMcpConfig: %v", err)
}
if cursorDataDir != filepath.Join(envRoot, "cursor-data") {
t.Fatalf("CursorDataDir = %q, want envRoot/cursor-data", cursorDataDir)
}
rawConfig, err := os.ReadFile(filepath.Join(workDir, ".cursor", "mcp.json"))
if err != nil {
t.Fatalf("read .cursor/mcp.json: %v", err)
}
var cfg cursorMcpConfigFile
if err := json.Unmarshal(rawConfig, &cfg); err != nil {
t.Fatalf("unmarshal .cursor/mcp.json: %v\n%s", err, rawConfig)
}
if len(cfg.McpServers) != 2 {
t.Fatalf("mcpServers length = %d, want 2: %s", len(cfg.McpServers), rawConfig)
}
if mode := filePerm(t, filepath.Join(workDir, ".cursor", "mcp.json")); mode != 0o600 {
t.Fatalf(".cursor/mcp.json mode = %#o, want 0600", mode)
}
projectRoot := cursorProjectRoot(workDir)
projectDataDir := filepath.Join(cursorDataDir, "projects", cursorSlugifyPath(projectRoot))
rawApprovals, err := os.ReadFile(filepath.Join(projectDataDir, "mcp-approvals.json"))
if err != nil {
t.Fatalf("read mcp-approvals.json: %v", err)
}
var approvals []string
if err := json.Unmarshal(rawApprovals, &approvals); err != nil {
t.Fatalf("unmarshal mcp-approvals.json: %v\n%s", err, rawApprovals)
}
wantApprovals, err := cursorMcpApprovalKeys(projectRoot, cfg.McpServers)
if err != nil {
t.Fatalf("expected approvals: %v", err)
}
if !reflect.DeepEqual(approvals, wantApprovals) {
t.Fatalf("approvals = %v, want %v", approvals, wantApprovals)
}
if mode := filePerm(t, filepath.Join(projectDataDir, "mcp-approvals.json")); mode != 0o600 {
t.Fatalf("mcp-approvals.json mode = %#o, want 0600", mode)
}
if _, err := os.Stat(filepath.Join(projectDataDir, cursorWorkspaceTrustedFile)); err != nil {
t.Fatalf("workspace trust file missing: %v", err)
}
if len(manifest.Files) == 0 {
t.Fatal("manifest did not record .cursor/mcp.json")
}
}
func TestPrepareCursorMcpConfigManagedEmptySet(t *testing.T) {
t.Parallel()
envRoot := t.TempDir()
workDir := filepath.Join(envRoot, "workdir")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatalf("mkdir workDir: %v", err)
}
cursorDataDir, err := prepareCursorMcpConfig(envRoot, workDir, json.RawMessage(`{"mcpServers":{}}`), &sidecarManifest{})
if err != nil {
t.Fatalf("prepareCursorMcpConfig: %v", err)
}
if cursorDataDir == "" {
t.Fatal("managed empty mcp_config should still isolate CursorDataDir")
}
projectRoot := cursorProjectRoot(workDir)
rawApprovals, err := os.ReadFile(filepath.Join(cursorDataDir, "projects", cursorSlugifyPath(projectRoot), "mcp-approvals.json"))
if err != nil {
t.Fatalf("read mcp-approvals.json: %v", err)
}
var approvals []string
if err := json.Unmarshal(rawApprovals, &approvals); err != nil {
t.Fatalf("unmarshal approvals: %v", err)
}
if len(approvals) != 0 {
t.Fatalf("approvals length = %d, want 0: %v", len(approvals), approvals)
}
}
func TestPrepareCursorMcpConfigNilDoesNotTakeOwnership(t *testing.T) {
t.Parallel()
envRoot := t.TempDir()
workDir := filepath.Join(envRoot, "workdir")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatalf("mkdir workDir: %v", err)
}
cursorDataDir, err := prepareCursorMcpConfig(envRoot, workDir, nil, &sidecarManifest{})
if err != nil {
t.Fatalf("prepareCursorMcpConfig: %v", err)
}
if cursorDataDir != "" {
t.Fatalf("CursorDataDir = %q, want empty", cursorDataDir)
}
if _, err := os.Stat(filepath.Join(workDir, ".cursor", "mcp.json")); !os.IsNotExist(err) {
t.Fatalf(".cursor/mcp.json should not exist, stat err=%v", err)
}
}
func TestPrepareCursorMcpConfigRejectsMalformedConfig(t *testing.T) {
t.Parallel()
envRoot := t.TempDir()
workDir := filepath.Join(envRoot, "workdir")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatalf("mkdir workDir: %v", err)
}
_, err := prepareCursorMcpConfig(envRoot, workDir, json.RawMessage(`{"mcpServers":{"bad":42}}`), &sidecarManifest{})
if err == nil {
t.Fatal("expected malformed server config to fail")
}
}
func filePerm(t *testing.T, path string) os.FileMode {
t.Helper()
info, err := os.Stat(path)
if err != nil {
t.Fatalf("stat %s: %v", path, err)
}
return info.Mode().Perm()
}