Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ man mcpx
- Cline MCP settings
- Claude Code user/local config (`~/.claude.json`)
- Codex config (`~/.codex/config.toml`, `mcp_servers.*`; when `[features].apps = true`, `mcpx` adds virtual per-app servers like `linear`/`zillow`, backed by Codex connector auth from `CODEX_CONNECTORS_TOKEN` or `~/.codex/auth.json`)
- Claude Code project config (`.mcp.json`, nearest parent)
- Kiro user config (`~/.kiro/settings/mcp.json`)
- Kiro project config (`.kiro/settings/mcp.json`, nearest parent)
- Project-local `.mcp.json` and `.kiro/settings/mcp.json` are not auto-discovered from the working directory.
- Check fallback files exist and expose either `mcpServers` (JSON sources) or `mcp_servers` (Codex TOML). Claude Code local scope uses `projects[<path>].mcpServers`.
6 changes: 2 additions & 4 deletions internal/cli/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ func maybeHandleAddCommand(args []string, cfg *config.Config, stdout, stderr io.
return false, 0
}

if cfg != nil {
if _, ok := cfg.Servers["add"]; ok {
return false, 0
}
if utilityCommandDeferredToServer(cfg, "add") {
return false, 0
}

return true, runAddCommand(args[1:], stdout, stderr)
Expand Down
69 changes: 69 additions & 0 deletions internal/cli/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,72 @@ func TestPrintAddHelpIncludesHeaderAndOverwriteGuidance(t *testing.T) {
t.Fatalf("help output missing overwrite guidance: %q", help)
}
}

func TestRunAddIgnoresFallbackServerNamedAdd(t *testing.T) {
tmp := t.TempDir()
home := tmp
xdgConfigHome := filepath.Join(tmp, "xdg-config")
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdgConfigHome)
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())

cursorDir := filepath.Join(home, ".cursor")
if err := os.MkdirAll(cursorDir, 0o700); err != nil {
t.Fatalf("MkdirAll(cursor): %v", err)
}
if err := os.WriteFile(filepath.Join(cursorDir, "mcp.json"), []byte(`{"mcpServers":{"add":{"command":"false"}}}`), 0o600); err != nil {
t.Fatalf("WriteFile(cursor mcp.json): %v", err)
}

oldSpawn := spawnOrConnectFn
defer func() { spawnOrConnectFn = oldSpawn }()
spawnOrConnectFn = func() (string, error) {
t.Fatal("spawnOrConnectFn should not run for mcpx add")
return "", errors.New("spawn should not run")
}

oldOut := rootStdout
oldErr := rootStderr
defer func() {
rootStdout = oldOut
rootStderr = oldErr
}()
var out bytes.Buffer
var errOut bytes.Buffer
rootStdout = &out
rootStderr = &errOut

code := Run([]string{"add", "--help"})
if code != ipc.ExitOK {
t.Fatalf("Run([add --help]) = %d, want %d (stderr=%q)", code, ipc.ExitOK, errOut.String())
}
if !bytes.Contains(out.Bytes(), []byte("mcpx add <source>")) {
t.Fatalf("stdout = %q, want add command help", out.String())
}
if errOut.Len() != 0 {
t.Fatalf("stderr = %q, want empty", errOut.String())
}
}

func TestMaybeHandleAddCommandDoesNotDeferToFallbackOrigin(t *testing.T) {
cfg := &config.Config{
Servers: map[string]config.ServerConfig{
"add": {Command: "false"},
},
ServerOrigins: map[string]config.ServerOrigin{
"add": config.NewServerOrigin(config.ServerOriginKindCursor, "/tmp/.cursor/mcp.json"),
},
}
var out bytes.Buffer
var errOut bytes.Buffer
handled, code := maybeHandleAddCommand([]string{"add", "--help"}, cfg, &out, &errOut)
if !handled {
t.Fatal("handled = false, want true (fallback origin must not shadow add)")
}
if code != ipc.ExitOK {
t.Fatalf("code = %d, want %d", code, ipc.ExitOK)
}
if !bytes.Contains(out.Bytes(), []byte("mcpx add <source>")) {
t.Fatalf("stdout = %q, want add help", out.String())
}
}
74 changes: 56 additions & 18 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,47 @@ func Run(args []string) int {

knownServerEntries := decodeServerListEntries(resp.Content)
knownServers := serverNamesFromEntries(knownServerEntries)
if !containsServerName(knownServers, requestServer) {
ephemeral, resolveResp := resolveEphemeralSource(requestServer)
if resolveResp != nil {
if containsServerName(knownServers, requestServer) {
printToolListHelp(rootStdout, server)
return ipc.ExitOK
}

// list_servers is config-only and omits Codex virtual servers.
// Explicit named operations resolve via list_tools instead of
// ServerNames membership.
resolveResp, err := client.Send(&ipc.Request{
Type: "list_tools",
Server: requestServer,
CWD: cwd,
})
if err != nil {
fmt.Fprintf(rootStderr, "mcpx: %v\n", err)
return ipc.ExitInternal
}
if !isUnknownServerResponse(resolveResp, requestServer) {
if resolveResp.ExitCode != ipc.ExitOK {
if resolveResp.Stderr != "" {
fmt.Fprintln(rootStderr, resolveResp.Stderr)
}
return resolveResp.ExitCode
}
if ephemeral != nil {
printToolListHelp(rootStdout, server)
return ipc.ExitOK
printToolListHelp(rootStdout, server)
return ipc.ExitOK
}

ephemeral, ephemeralResp := resolveEphemeralSource(requestServer)
if ephemeralResp != nil {
if ephemeralResp.Stderr != "" {
fmt.Fprintln(rootStderr, ephemeralResp.Stderr)
}
printUnknownServer(server, visibleServerNamesFromEntries(knownServerEntries))
return ipc.ExitUsageErr
return ephemeralResp.ExitCode
}
printToolListHelp(rootStdout, server)
return ipc.ExitOK
if ephemeral != nil {
printToolListHelp(rootStdout, server)
return ipc.ExitOK
}
printUnknownServer(server, visibleServerNamesFromEntries(knownServerEntries))
return ipc.ExitUsageErr
}

// Connect to daemon
Expand All @@ -161,24 +185,38 @@ func maybeHandleCompletionCommand(args []string, cfg *config.Config, stdout, std

switch args[0] {
case "completion":
if cfg != nil {
if _, ok := cfg.Servers["completion"]; ok {
return false, 0
}
if utilityCommandDeferredToServer(cfg, "completion") {
return false, 0
}
return true, runCompletionCommand(args[1:], stdout, stderr)
case "__complete":
if cfg != nil {
if _, ok := cfg.Servers["__complete"]; ok {
return false, 0
}
if utilityCommandDeferredToServer(cfg, "__complete") {
return false, 0
}
return true, runInternalCompletion(args[1:], stdout, stderr)
default:
return false, 0
}
}

// utilityCommandDeferredToServer reports whether a reserved CLI command name
// should yield to a configured MCP server of the same name. Only servers
// loaded from the mcpx config file (or fixtures without origin metadata)
// shadow utility commands; fallback-discovered names do not.
func utilityCommandDeferredToServer(cfg *config.Config, name string) bool {
if cfg == nil {
return false
}
if _, ok := cfg.Servers[name]; !ok {
return false
}
origin, ok := cfg.ServerOrigins[name]
if !ok {
return true
}
return config.NormalizeServerOrigin(origin).Kind == config.ServerOriginKindMCPXConfig
}

type rootServerListArgs struct {
output outputMode
verbose bool
Expand Down
Loading