diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 24609dd81e4f..3c49bc988db0 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -362,6 +362,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ break } + case "tui.mcp.refresh": { + sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))) + break + } + case "vcs.branch.updated": { setStore("vcs", { branch: event.properties.branch }) break diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index fbe5ce7f9ffd..2f95196475a0 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -7,6 +7,7 @@ const DEFAULT_TOAST_DURATION = 5000 export const TuiEvent = { PromptAppend: BusEvent.define("tui.prompt.append", Schema.Struct({ text: Schema.String })), + McpRefresh: BusEvent.define("tui.mcp.refresh", Schema.Struct({})), CommandExecute: BusEvent.define( "tui.command.execute", Schema.Struct({ diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index fe7180238851..8e43bfd06b50 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -583,6 +583,13 @@ export const layer = Layer.effect( result[key] = s.status[key] ?? { status: "disabled" } } + // Include dynamically registered MCPs not in config + for (const [key, v] of Object.entries(s.status)) { + if (!(key in result)) { + result[key] = v + } + } + return result }) @@ -608,6 +615,7 @@ export const layer = Layer.effect( const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCP.Info) { yield* createAndStore(name, mcp) const s = yield* InstanceState.get(state) + yield* bus.publish(TuiEvent.McpRefresh, {}) return { status: s.status } }) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 10547c9f0821..cb6620a2f109 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -174,6 +174,8 @@ const { MCP } = await import("../../src/mcp/index") const { Instance } = await import("../../src/project/instance") const { WithInstance } = await import("../../src/project/with-instance") const { tmpdir } = await import("../fixture/fixture") +const { Bus } = await import("../../src/bus") +const { TuiEvent } = await import("../../src/cli/cmd/tui/event") // --- Helper --- @@ -786,3 +788,53 @@ test( }), ), ) + +// ======================================================================== +// Test: dynamically added server appears in MCP.status() +// ======================================================================== + +test( + "dynamically added server appears in status", + withInstance({}, (mcp) => + Effect.gen(function* () { + lastCreatedClientName = "dynamic-server" + getOrCreateClientState("dynamic-server") + + yield* mcp.add("dynamic-server", { + type: "local", + command: ["echo", "test"], + }) + + const status = yield* mcp.status() + expect(status["dynamic-server"]).toBeDefined() + expect(status["dynamic-server"]?.status).toBe("connected") + }), + ), +) + +// ======================================================================== +// Test: MCP.add() publishes TuiEvent.McpRefresh +// ======================================================================== + +test( + "add publishes TuiEvent.McpRefresh", + withInstance({}, (mcp) => + Effect.gen(function* () { + const events: Array> = [] + const unsubscribe = Bus.subscribe(TuiEvent.McpRefresh, (evt) => { + events.push(evt.properties) + }) + + lastCreatedClientName = "refresh-server" + getOrCreateClientState("refresh-server") + + yield* mcp.add("refresh-server", { + type: "local", + command: ["echo", "test"], + }) + + unsubscribe() + expect(events).toHaveLength(1) + }), + ), +) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 79ef42d9e171..1f01a981e654 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -25,6 +25,7 @@ export type Event = | EventSessionIdle | EventSessionCompacted | EventTuiPromptAppend + | EventTuiMcpRefresh | EventTuiCommandExecute | EventTuiToastShow1 | EventTuiSessionSelect @@ -791,6 +792,7 @@ export type GlobalEvent = { | EventSessionIdle | EventSessionCompacted | EventTuiPromptAppend + | EventTuiMcpRefresh | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect @@ -2416,6 +2418,14 @@ export type EventSessionCompacted = { } } +export type EventTuiMcpRefresh = { + id: string + type: "tui.mcp.refresh" + properties: { + [key: string]: unknown + } +} + export type EventMcpToolsChanged = { id: string type: "mcp.tools.changed"