Skip to content

Commit 174ab58

Browse files
authored
fix(mcp): apply timeouts to prompts and resources (anomalyco#31612)
1 parent 954d618 commit 174ab58

2 files changed

Lines changed: 52 additions & 8 deletions

File tree

packages/opencode/src/mcp/index.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ export const layer = Layer.effect(
740740

741741
const withClient = Effect.fnUntraced(function* <A>(
742742
clientName: string,
743-
fn: (client: MCPClient) => Promise<A>,
743+
fn: (client: MCPClient, timeout?: number) => Promise<A>,
744744
label: string,
745745
meta?: Record<string, unknown>,
746746
) {
@@ -750,8 +750,11 @@ export const layer = Layer.effect(
750750
yield* Effect.logWarning(`client not found for ${label}`, { clientName })
751751
return undefined
752752
}
753+
const cfg = yield* cfgSvc.get()
754+
const configured = cfg.mcp?.[clientName]
755+
const staticTimeout = configured && isMcpConfigured(configured) ? configured.timeout : undefined
753756
return yield* Effect.tryPromise({
754-
try: () => fn(client),
757+
try: () => fn(client, s.config[clientName]?.timeout ?? staticTimeout ?? cfg.experimental?.mcp_timeout),
755758
catch: (error) => error,
756759
}).pipe(
757760
Effect.tapError((error) =>
@@ -770,15 +773,21 @@ export const layer = Layer.effect(
770773
name: string,
771774
args?: Record<string, string>,
772775
) {
773-
return yield* withClient(clientName, (client) => client.getPrompt({ name, arguments: args }), "getPrompt", {
774-
promptName: name,
775-
})
776+
return yield* withClient(
777+
clientName,
778+
(client, timeout) => client.getPrompt({ name, arguments: args }, { timeout }),
779+
"getPrompt",
780+
{ promptName: name },
781+
)
776782
})
777783

778784
const readResource = Effect.fn("MCP.readResource")(function* (clientName: string, resourceUri: string) {
779-
return yield* withClient(clientName, (client) => client.readResource({ uri: resourceUri }), "readResource", {
780-
resourceUri,
781-
})
785+
return yield* withClient(
786+
clientName,
787+
(client, timeout) => client.readResource({ uri: resourceUri }, { timeout }),
788+
"readResource",
789+
{ resourceUri },
790+
)
782791
})
783792

784793
const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) {

packages/opencode/test/mcp/lifecycle.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface MockClientState {
1313
listToolsCalls: number
1414
listPromptsCalls: number
1515
listResourcesCalls: number
16+
getPromptTimeout?: number
17+
readResourceTimeout?: number
1618
requestCalls: number
1719
listToolsShouldFail: boolean
1820
listToolsError: string
@@ -206,6 +208,16 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
206208
return { resources: this._state?.resources ?? [] }
207209
}
208210

211+
async getPrompt(_params: unknown, options?: { timeout?: number }) {
212+
if (this._state) this._state.getPromptTimeout = options?.timeout
213+
return { messages: [] }
214+
}
215+
216+
async readResource(params: { uri: string }, options?: { timeout?: number }) {
217+
if (this._state) this._state.readResourceTimeout = options?.timeout
218+
return { contents: [{ uri: params.uri, text: "test" }] }
219+
}
220+
209221
async close() {
210222
if (this._state) this._state.closed = true
211223
}
@@ -758,6 +770,29 @@ it.instance(
758770
},
759771
)
760772

773+
it.instance(
774+
"uses per-server timeouts for prompt and resource requests",
775+
() =>
776+
MCP.Service.use((mcp: MCPNS.Interface) =>
777+
Effect.gen(function* () {
778+
lastCreatedClientName = "timeout-server"
779+
const serverState = getOrCreateClientState("timeout-server")
780+
781+
yield* mcp.add("timeout-server", {
782+
type: "local",
783+
command: ["echo", "test"],
784+
timeout: 2500,
785+
})
786+
yield* mcp.getPrompt("timeout-server", "test")
787+
yield* mcp.readResource("timeout-server", "test://resource")
788+
789+
expect(serverState.getPromptTimeout).toBe(2500)
790+
expect(serverState.readResourceTimeout).toBe(2500)
791+
}),
792+
),
793+
{ config: { mcp: {}, experimental: { mcp_timeout: 5000 } } },
794+
)
795+
761796
it.instance(
762797
"resource-only servers connect without listing tools",
763798
() =>

0 commit comments

Comments
 (0)