diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1b318abaa..9412af4e8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added server-name autocomplete for `/mcp enable`, `disable`, `test`, `remove`, `reconnect`, `reauth`, and `unauth`, sourced from configured and runtime-discovered MCP servers ([#5654](https://github.com/can1357/oh-my-pi/issues/5654)). + ## [17.1.0] - 2026-07-24 ### Breaking Changes @@ -23,7 +27,6 @@ - Added a toggle-list editor in `/settings` for managing array-of-enum settings like search and image provider orders. - Added `models.yml` Bedrock Converse prompt-cache capability overrides for bundled and opaque inference profiles. - Documented Vibe mode (`/vibe`) in `docs/vibe-mode.md` and the `/fresh` provider-stream reset in the session-operations doc, and linked both from the README's new "Session controls" section ([#6440](https://github.com/can1357/oh-my-pi/issues/6440)). -- Added server-name autocomplete for `/mcp enable`, `disable`, `test`, `remove`, `reconnect`, `reauth`, and `unauth`, sourced from configured and runtime-discovered MCP servers ([#5654](https://github.com/can1357/oh-my-pi/issues/5654)). - Added `getServiceTiers()` and `setServiceTier()` extension APIs for reading and changing the live per-family service tier used by subsequent session requests ([#5860](https://github.com/can1357/oh-my-pi/issues/5860)). - Added opt-in `omp bench --cache` independent cold/warm prompt-cache pairs with stable-prefix controls, sequential-by-default execution, mechanism-specific provider proof, and privacy-safe JSON output; it rejects `openai-codex-responses`, whose WebSocket transport chains turns. - Added `tools.xdevDocs` prompt-doc modes (`inline`, `builtins`, `catalog`) and the `tools.xdevInlineDevices` glob allowlist controlling which mounted `xd://` device docs are inlined into the system prompt; dynamic-device catalog summaries and mid-session mount notices are capped to one line, and changing the mode in `/settings` refreshes the active prompt ([#6063](https://github.com/can1357/oh-my-pi/issues/6063)). diff --git a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts index bc71477fc..fb6241592 100644 --- a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts @@ -254,16 +254,21 @@ type MCPSearchParsed = { /** * Collect the de-duplicated union of every MCP server name we know about: - * user config, project config, disabled-server entries, and any - * runtime-discovered servers not already present in either config - * (`ctx.mcpManager.getAllServerNames()` covers connections, pending - * connections, and discovered-but-not-yet-connected sources). Disabled - * servers stay in this list — disabling a server only flips its config - * `enabled` flag, it doesn't remove the config entry, so a disabled server - * is still a valid `/mcp enable ` target. This also covers a - * discovered (non-config) server that was disabled: `loadAllMCPConfigs` - * filters it out of `getAllServerNames()`, but its name survives in - * `userConfig.disabledServers`. + * user config, project config, and any runtime-discovered servers not + * already present in either config (`ctx.mcpManager.getAllServerNames()` + * covers connections, pending connections, and discovered-but-not-yet- + * connected sources). + * + * When `includeDisabled` is true (the default), disabled-server entries + * are unioned in too — disabling a server only flips its config `enabled` + * flag, it doesn't remove the config entry, so a disabled server is still + * a valid `/mcp enable ` target. This also covers a discovered + * (non-config) server that was disabled: `loadAllMCPConfigs` filters it + * out of `getAllServerNames()`, but its name survives in + * `userConfig.disabledServers`. Callers whose target operation needs a + * live connection or config entry (`/mcp test`/`reconnect`/`reauth`/ + * `unauth`) — which a disabled-only name can never satisfy — must pass + * `includeDisabled: false`. * * This is the single source of truth for "every known server name": both * `MCPCommandController#handleList()` and the `/mcp` slash-command argument @@ -276,6 +281,7 @@ type MCPSearchParsed = { export async function collectMcpServerNames( ctx: InteractiveModeContext, preloaded?: { userConfig: MCPConfigFile; projectConfig: MCPConfigFile }, + includeDisabled = true, ): Promise { let userConfig: MCPConfigFile; let projectConfig: MCPConfigFile; @@ -292,7 +298,7 @@ export async function collectMcpServerNames( const names = new Set([ ...Object.keys(userConfig.mcpServers ?? {}), ...Object.keys(projectConfig.mcpServers ?? {}), - ...(userConfig.disabledServers ?? []), + ...(includeDisabled ? (userConfig.disabledServers ?? []) : []), ]); if (ctx.mcpManager) { for (const name of ctx.mcpManager.getAllServerNames()) { diff --git a/packages/coding-agent/src/slash-commands/builtin-registry.ts b/packages/coding-agent/src/slash-commands/builtin-registry.ts index 1a983db77..e73ea0a12 100644 --- a/packages/coding-agent/src/slash-commands/builtin-registry.ts +++ b/packages/coding-agent/src/slash-commands/builtin-registry.ts @@ -2457,6 +2457,18 @@ const MCP_SERVER_NAME_SUBCOMMANDS: ReadonlySet = new Set([ "unauth", ]); +/** + * `/mcp` subcommands where a discovered server disabled via `/mcp disable` + * (name only in `userConfig.disabledServers`, dropped from + * `mcpManager.getAllServerNames()` by `loadAllMCPConfigs`) is still a valid + * completion target: `enable` is the primary re-enable path, and offering + * it for `disable` is a harmless no-op (`#handleSetEnabled` reports + * "already disabled"). The rest (`test`/`reconnect`/`reauth`/`unauth`) need + * a live connection or config entry that a disabled-only name never has — + * `#resolveServerForAuth`/`reconnectServer` would report it as not found. + */ +const MCP_DISABLED_ONLY_ELIGIBLE_SUBCOMMANDS: ReadonlySet = new Set(["enable", "disable"]); + /** * Build getArgumentCompletions for /mcp. Delegates to the generic * declarative subcommand completer while the subcommand name itself is @@ -2489,7 +2501,11 @@ function buildMcpArgumentCompletions( let serverNames: string[]; try { - serverNames = await collectMcpServerNames(runtime.ctx); + serverNames = await collectMcpServerNames( + runtime.ctx, + undefined, + MCP_DISABLED_ONLY_ELIGIBLE_SUBCOMMANDS.has(lowerSubcommand), + ); } catch (error) { logger.warn("MCP server-name autocomplete failed to read config", { error }); return null; diff --git a/packages/coding-agent/test/mcp-name-autocomplete.test.ts b/packages/coding-agent/test/mcp-name-autocomplete.test.ts index 051ee940e..953c159e5 100644 --- a/packages/coding-agent/test/mcp-name-autocomplete.test.ts +++ b/packages/coding-agent/test/mcp-name-autocomplete.test.ts @@ -136,6 +136,30 @@ describe("MCP server-name autocomplete", () => { expect(filtered?.[0]?.value).toBe("enable my-server "); }); + test("/mcp getArgumentCompletions offers a disabled-only discovered name for enable/disable but not test/reconnect/reauth/unauth", async () => { + // "discovered-disabled" is a third-party server that was /mcp disable'd: + // present only in userConfig.disabledServers, absent from mcpServers, and + // no longer reported by the manager (loadAllMCPConfigs drops disabled + // sources). #resolveServerForAuth/reconnectServer can't resolve it, so + // test/reconnect/reauth/unauth must not suggest it. + await Bun.write( + getMCPConfigPath("user", projectDir), + `${JSON.stringify({ mcpServers: {}, disabledServers: ["discovered-disabled"] }, null, 2)}\n`, + ); + await writeConfig("project", projectDir, {}); + const { ctx } = createFakeCtx([]); + const runtime: TuiSlashCommandRuntime = { ctx }; + const mcp = buildTuiBuiltinSlashCommands(runtime).find(c => c.name === "mcp"); + if (!mcp?.getArgumentCompletions) throw new Error("expected /mcp command with getArgumentCompletions"); + + expect((await mcp.getArgumentCompletions("enable "))?.map(item => item.label)).toEqual(["discovered-disabled"]); + expect((await mcp.getArgumentCompletions("disable "))?.map(item => item.label)).toEqual(["discovered-disabled"]); + expect(await mcp.getArgumentCompletions("test ")).toBeNull(); + expect(await mcp.getArgumentCompletions("reconnect ")).toBeNull(); + expect(await mcp.getArgumentCompletions("reauth ")).toBeNull(); + expect(await mcp.getArgumentCompletions("unauth ")).toBeNull(); + }); + test("/mcp getArgumentCompletions returns null for subcommands that don't take a server name", async () => { await writeConfig("user", projectDir, { "my-server": { type: "stdio", command: "one" } }); const { ctx } = createFakeCtx([]);