diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index 5ce4719ee5b2..d40594bace17 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -740,7 +740,7 @@ export const layer = Layer.effect(
const withClient = Effect.fnUntraced(function* (
clientName: string,
- fn: (client: MCPClient) => Promise,
+ fn: (client: MCPClient, timeout?: number) => Promise,
label: string,
meta?: Record,
) {
@@ -750,8 +750,11 @@ export const layer = Layer.effect(
yield* Effect.logWarning(`client not found for ${label}`, { clientName })
return undefined
}
+ const cfg = yield* cfgSvc.get()
+ const configured = cfg.mcp?.[clientName]
+ const staticTimeout = configured && isMcpConfigured(configured) ? configured.timeout : undefined
return yield* Effect.tryPromise({
- try: () => fn(client),
+ try: () => fn(client, s.config[clientName]?.timeout ?? staticTimeout ?? cfg.experimental?.mcp_timeout),
catch: (error) => error,
}).pipe(
Effect.tapError((error) =>
@@ -770,15 +773,21 @@ export const layer = Layer.effect(
name: string,
args?: Record,
) {
- return yield* withClient(clientName, (client) => client.getPrompt({ name, arguments: args }), "getPrompt", {
- promptName: name,
- })
+ return yield* withClient(
+ clientName,
+ (client, timeout) => client.getPrompt({ name, arguments: args }, { timeout }),
+ "getPrompt",
+ { promptName: name },
+ )
})
const readResource = Effect.fn("MCP.readResource")(function* (clientName: string, resourceUri: string) {
- return yield* withClient(clientName, (client) => client.readResource({ uri: resourceUri }), "readResource", {
- resourceUri,
- })
+ return yield* withClient(
+ clientName,
+ (client, timeout) => client.readResource({ uri: resourceUri }, { timeout }),
+ "readResource",
+ { resourceUri },
+ )
})
const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) {
diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts
index da525d166e9b..004909fb6e38 100644
--- a/packages/opencode/test/mcp/lifecycle.test.ts
+++ b/packages/opencode/test/mcp/lifecycle.test.ts
@@ -13,6 +13,8 @@ interface MockClientState {
listToolsCalls: number
listPromptsCalls: number
listResourcesCalls: number
+ getPromptTimeout?: number
+ readResourceTimeout?: number
requestCalls: number
listToolsShouldFail: boolean
listToolsError: string
@@ -206,6 +208,16 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
return { resources: this._state?.resources ?? [] }
}
+ async getPrompt(_params: unknown, options?: { timeout?: number }) {
+ if (this._state) this._state.getPromptTimeout = options?.timeout
+ return { messages: [] }
+ }
+
+ async readResource(params: { uri: string }, options?: { timeout?: number }) {
+ if (this._state) this._state.readResourceTimeout = options?.timeout
+ return { contents: [{ uri: params.uri, text: "test" }] }
+ }
+
async close() {
if (this._state) this._state.closed = true
}
@@ -758,6 +770,29 @@ it.instance(
},
)
+it.instance(
+ "uses per-server timeouts for prompt and resource requests",
+ () =>
+ MCP.Service.use((mcp: MCPNS.Interface) =>
+ Effect.gen(function* () {
+ lastCreatedClientName = "timeout-server"
+ const serverState = getOrCreateClientState("timeout-server")
+
+ yield* mcp.add("timeout-server", {
+ type: "local",
+ command: ["echo", "test"],
+ timeout: 2500,
+ })
+ yield* mcp.getPrompt("timeout-server", "test")
+ yield* mcp.readResource("timeout-server", "test://resource")
+
+ expect(serverState.getPromptTimeout).toBe(2500)
+ expect(serverState.readResourceTimeout).toBe(2500)
+ }),
+ ),
+ { config: { mcp: {}, experimental: { mcp_timeout: 5000 } } },
+)
+
it.instance(
"resource-only servers connect without listing tools",
() =>