Просмотр исходного кода

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
Dax Raad 4 месяцев назад
Родитель
Сommit
a440e09cfe

+ 30 - 1
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)
   }

+ 21 - 0
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({

+ 15 - 0
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 })

+ 16 - 0
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