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
This commit is contained in:
Dax Raad
2025-10-07 04:04:19 -04:00
parent 27c211ef86
commit a440e09cfe
4 changed files with 82 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import z from "zod/v4"
import { Session } from "../session" import { Session } from "../session"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { withTimeout } from "@/util/timeout"
export namespace MCP { export namespace MCP {
const log = Log.create({ service: "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( const state = Instance.state(
async () => { async () => {
const cfg = await Config.get() const cfg = await Config.get()
const clients: { const clients: {
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>> [name: string]: MCPClient
} = {} } = {}
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) { for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
if (mcp.enabled === false) { 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 { return {
clients, clients,
config: cfg.mcp ?? {},
} }
}, },
async (state) => { 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() { export async function clients() {
return state().then((state) => state.clients) return state().then((state) => state.clients)
} }

View File

@@ -31,6 +31,7 @@ import { SessionRevert } from "../session/revert"
import { lazy } from "../util/lazy" import { lazy } from "../util/lazy"
import { Todo } from "../session/todo" import { Todo } from "../session/todo"
import { InstanceBootstrap } from "../project/bootstrap" import { InstanceBootstrap } from "../project/bootstrap"
import { MCP } from "../mcp"
const ERRORS = { const ERRORS = {
400: { 400: {
@@ -1183,6 +1184,26 @@ export namespace Server {
return c.json(modes) 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( .post(
"/tui/append-prompt", "/tui/append-prompt",
describeRoute({ describeRoute({

View File

@@ -82,6 +82,8 @@ import type {
AppLogResponses, AppLogResponses,
AppAgentsData, AppAgentsData,
AppAgentsResponses, AppAgentsResponses,
McpStatusData,
McpStatusResponses,
TuiAppendPromptData, TuiAppendPromptData,
TuiAppendPromptResponses, TuiAppendPromptResponses,
TuiOpenHelpData, 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 { class Tui extends _HeyApiClient {
/** /**
* Append prompt to the TUI * Append prompt to the TUI
@@ -724,6 +738,7 @@ export class OpencodeClient extends _HeyApiClient {
find = new Find({ client: this._client }) find = new Find({ client: this._client })
file = new File({ client: this._client }) file = new File({ client: this._client })
app = new App({ client: this._client }) app = new App({ client: this._client })
mcp = new Mcp({ client: this._client })
tui = new Tui({ client: this._client }) tui = new Tui({ client: this._client })
auth = new Auth({ client: this._client }) auth = new Auth({ client: this._client })
event = new Event({ client: this._client }) event = new Event({ client: this._client })

View File

@@ -2070,6 +2070,22 @@ export type AppAgentsResponses = {
export type AppAgentsResponse = AppAgentsResponses[keyof 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 = { export type TuiAppendPromptData = {
body?: { body?: {
text: string text: string