summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-10-07 04:04:19 -0400
committerDax Raad <[email protected]>2025-10-07 04:04:19 -0400
commita440e09cfe57b955c3cbbdcc43eaf84d33d9c6dc (patch)
tree7c6c7f8e998ebb833ffba6ae3d0041364c98cc2d
parent27c211ef869cae9d18b3fefc36ba787af75ebde0 (diff)
downloadopencode-a440e09cfe57b955c3cbbdcc43eaf84d33d9c6dc.tar.gz
opencode-a440e09cfe57b955c3cbbdcc43eaf84d33d9c6dc.zip
core: improve MCP reliability and add status monitoring
- Added 5-second timeout to MCP client verification to prevent hanging connections - New GET /mcp endpoint to monitor server connection status - Automatically removes unresponsive MCP clients during initialization
-rw-r--r--packages/opencode/src/mcp/index.ts31
-rw-r--r--packages/opencode/src/server/server.ts21
-rw-r--r--packages/sdk/js/src/gen/sdk.gen.ts15
-rw-r--r--packages/sdk/js/src/gen/types.gen.ts16
4 files changed, 82 insertions, 1 deletions
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index dc90dfe5f..dc5bb8b86 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<ReturnType<typeof experimental_createMCPClient>>
+
const state = Instance.state(
async () => {
const cfg = await Config.get()
const clients: {
- [name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
+ [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<string, "connected" | "failed" | "disabled"> = {}
+ 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 26cbb5d71..ee04b1f92 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 aea90daec..6bb1e115f 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<ThrowOnError extends boolean = false>(options?: Options<McpStatusData, ThrowOnError>) {
+ return (options?.client ?? this._client).get<McpStatusResponses, unknown, ThrowOnError>({
+ 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 c255cc69c..cc94c1f1f 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