mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-20 09:14:22 +01:00
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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user