Skip to content

Commit f1789de

Browse files
fix(mcp): refresh clients on reconnect and track disconnects
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
1 parent 6eed0e2 commit f1789de

1 file changed

Lines changed: 37 additions & 2 deletions

File tree

packages/opencode/src/mcp/index.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,12 @@ function isMcpConfigured(entry: McpEntry): entry is ConfigMCP.Info {
130130
const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_")
131131

132132
// Convert MCP tool definition to AI SDK Tool type
133-
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
133+
function convertMcpTool(
134+
mcpTool: MCPToolDef,
135+
getClient: () => MCPClient | undefined,
136+
clientName: string,
137+
timeout?: number,
138+
): Tool {
134139
const inputSchema = mcpTool.inputSchema
135140

136141
// Spread first, then override type to ensure it's always "object"
@@ -145,6 +150,10 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number
145150
description: mcpTool.description ?? "",
146151
inputSchema: jsonSchema(schema),
147152
execute: async (args: unknown) => {
153+
const client = getClient()
154+
if (!client) {
155+
throw new Error(`MCP server \"${clientName}\" is not connected`)
156+
}
148157
return client.callTool(
149158
{
150159
name: mcpTool.name,
@@ -473,6 +482,27 @@ export const layer = Layer.effect(
473482
)
474483

475484
function watch(s: State, name: string, client: MCPClient, bridge: EffectBridge.Shape, timeout?: number) {
485+
const prevOnClose = client.onclose
486+
client.onclose = () => {
487+
prevOnClose?.()
488+
if (s.clients[name] !== client) return
489+
490+
log.warn("mcp client disconnected", { name })
491+
delete s.clients[name]
492+
delete s.defs[name]
493+
s.status[name] = { status: "failed", error: "Connection closed" }
494+
void bridge.promise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore))
495+
}
496+
497+
const prevOnError = client.onerror
498+
client.onerror = (error) => {
499+
prevOnError?.(error)
500+
log.error("mcp client transport error", {
501+
name,
502+
error: error instanceof Error ? error.message : String(error),
503+
})
504+
}
505+
476506
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
477507
log.info("tools list changed notification received", { server: name })
478508
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
@@ -657,7 +687,12 @@ export const layer = Layer.effect(
657687

658688
const timeout = entry?.timeout ?? defaultTimeout
659689
for (const mcpTool of listed) {
660-
result[sanitize(clientName) + "_" + sanitize(mcpTool.name)] = convertMcpTool(mcpTool, client, timeout)
690+
result[sanitize(clientName) + "_" + sanitize(mcpTool.name)] = convertMcpTool(
691+
mcpTool,
692+
() => s.clients[clientName],
693+
clientName,
694+
timeout,
695+
)
661696
}
662697
}),
663698
{ concurrency: "unbounded" },

0 commit comments

Comments
 (0)