Files
multica/server/internal/daemon/execenv/omp_mcp_test.go
feig a92e8f6f7e MUL-6689: feat(agents): support MCP config for the Oh-My-Pi runtime (#7565)
Adds MCP configuration support to the Oh-My-Pi (omp) runtime. The MCP tab is
now visible for omp agents, and the managed config is materialised as
`{workDir}/.omp/mcp.json` — omp's highest-precedence project-level path —
before the runtime starts.

Materialisation lives in `execenv` and is recorded in `sidecarManifest`, so it
inherits refuse-to-clobber on a pre-existing file plus daemon-side teardown and
reuse cleanup. `Reuse` declines (returns nil, forcing a fresh Prepare) when the
config cannot be written, matching the Cursor path, so a leftover file from a
crashed run cannot silently carry stale MCP servers into the next task.
`.pi` and `.omp` are added to the repo-cache git excludes so runtime sidecars —
which for MCP routinely embed third-party tokens — cannot be committed by an
agent; `.pi/skills/` was already being written unexcluded before this change.
`listRuntimeLocalMcpServers` reports omp's user-scope inventory from
`~/.omp/agent/mcp.json`, with a comment recording that this is deliberately
narrower than omp's full discovery chain.

Pi itself is excluded by design. Upstream states the position in its README
("**No MCP.** Build CLI tools with READMEs, or build an extension that adds MCP
support."), confirmed against pi 0.67.2: no MCP code in the published bundle,
no MCP flag or subcommand, and a probe server placed in a project config is
never launched. There is no `.pi/mcp.json` convention to write into, so the
negative assertion pinning `pi` out of the allowlist is kept and now carries
that reasoning.

Closes #7558 for omp only; Pi would require authoring a Pi extension for the
daemon to inject, which is separate work.
2026-08-28 15:53:09 +08:00

83 lines
2.3 KiB
Go

package execenv
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestPrepareOmpMcpConfigUsesProviderNamespace(t *testing.T) {
workDir := t.TempDir()
manifest := &sidecarManifest{}
raw := json.RawMessage(`{"mcpServers":{"fetch":{"command":"uvx"}}}`)
if err := prepareOmpMcpConfig(workDir, "omp", raw, manifest); err != nil {
t.Fatalf("prepareOmpMcpConfig: %v", err)
}
path := filepath.Join(workDir, ".omp", "mcp.json")
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
if string(data) != string(raw) {
t.Fatalf("config = %s, want %s", data, raw)
}
if !containsPath(manifest.Files, path) || !containsPath(manifest.Dirs, filepath.Dir(path)) {
t.Fatalf("manifest = %#v, want config file and directory", manifest)
}
}
func TestPrepareOmpMcpConfigRefusesExistingManagedPath(t *testing.T) {
workDir := t.TempDir()
path := filepath.Join(workDir, ".omp", "mcp.json")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
existing := []byte(`{"mcpServers":{"user":{"command":"echo"}}}`)
if err := os.WriteFile(path, existing, 0o644); err != nil {
t.Fatal(err)
}
err := prepareOmpMcpConfig(workDir, "omp", json.RawMessage(`{"mcpServers":{"managed":{}}}`), &sidecarManifest{})
if err == nil || !strings.Contains(err.Error(), "would overwrite") {
t.Fatalf("error = %v, want overwrite error", err)
}
data, readErr := os.ReadFile(path)
if readErr != nil {
t.Fatal(readErr)
}
if string(data) != string(existing) {
t.Fatalf("existing config changed to %s", data)
}
}
func TestPrepareOmpMcpConfigCleanupRemovesManagedFiles(t *testing.T) {
workDir := t.TempDir()
envRoot := t.TempDir()
manifest := &sidecarManifest{}
if err := prepareOmpMcpConfig(workDir, "omp", json.RawMessage(`{"mcpServers":{"fetch":{}}}`), manifest); err != nil {
t.Fatal(err)
}
if err := writeSidecarManifest(envRoot, manifest); err != nil {
t.Fatal(err)
}
if err := CleanupSidecars(envRoot); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(workDir, ".omp", "mcp.json")); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("managed config still exists, stat error = %v", err)
}
}
func containsPath(paths []string, want string) bool {
for _, path := range paths {
if path == want {
return true
}
}
return false
}