diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index dc90dfe5..dc5bb8b8 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -9,6 +9,7 @@ import z from "zod/v4" import { Session } from "../session" import { Bus } from "../bus" import { Instance } from "../project/instance" +import { withTimeout } from "@/util/timeout" export namespace MCP { const log = Log.create({ service: "mcp" }) @@ -20,11 +21,13 @@ export namespace MCP { }), ) + type MCPClient = Awaited> + const state = Instance.state( async () => { const cfg = await Config.get() const clients: { - [name: string]: Awaited> + [name: string]: MCPClient } = {} for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) { if (mcp.enabled === false) { @@ -128,8 +131,17 @@ export namespace MCP { } } + for (const [key, client] of Object.entries(clients)) { + const result = await withTimeout(client.tools(), 5000).catch(() => {}) + if (!result) { + log.warn("mcp client verification failed, removing client", { key }) + delete clients[key] + } + } + return { clients, + config: cfg.mcp ?? {}, } }, async (state) => { @@ -139,6 +151,23 @@ export namespace MCP { }, ) + export async function status() { + return state().then((state) => { + const result: Record = {} + for (const [key, client] of Object.entries(state.config)) { + if (client.enabled === false) { + result[key] = "disabled" + continue + } + if (state.clients[key]) { + result[key] = "connected" + } + result[key] = "failed" + } + return result + }) + } + export async function clients() { return state().then((state) => state.clients) } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 26cbb5d7..ee04b1f9 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -31,6 +31,7 @@ import { SessionRevert } from "../session/revert" import { lazy } from "../util/lazy" import { Todo } from "../session/todo" import { InstanceBootstrap } from "../project/bootstrap" +import { MCP } from "../mcp" const ERRORS = { 400: { @@ -1183,6 +1184,26 @@ export namespace Server { return c.json(modes) }, ) + .get( + "/mcp", + describeRoute({ + description: "Get MCP server status", + operationId: "mcp.status", + responses: { + 200: { + description: "MCP server status", + content: { + "application/json": { + schema: resolver(z.any()), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await MCP.status()) + }, + ) .post( "/tui/append-prompt", describeRoute({ diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index aea90dae..6bb1e115 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -82,6 +82,8 @@ import type { AppLogResponses, AppAgentsData, AppAgentsResponses, + McpStatusData, + McpStatusResponses, TuiAppendPromptData, TuiAppendPromptResponses, TuiOpenHelpData, @@ -567,6 +569,18 @@ class App extends _HeyApiClient { } } +class Mcp extends _HeyApiClient { + /** + * Get MCP server status + */ + public status(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/mcp", + ...options, + }) + } +} + class Tui extends _HeyApiClient { /** * Append prompt to the TUI @@ -724,6 +738,7 @@ export class OpencodeClient extends _HeyApiClient { find = new Find({ client: this._client }) file = new File({ client: this._client }) app = new App({ client: this._client }) + mcp = new Mcp({ client: this._client }) tui = new Tui({ client: this._client }) auth = new Auth({ client: this._client }) event = new Event({ client: this._client }) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index c255cc69..cc94c1f1 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -2070,6 +2070,22 @@ export type AppAgentsResponses = { export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] +export type McpStatusData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/mcp" +} + +export type McpStatusResponses = { + /** + * MCP server status + */ + 200: unknown +} + export type TuiAppendPromptData = { body?: { text: string