diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index af970dc1455..7a838c97544 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -210,6 +210,7 @@ export const layer = Layer.effect( status: { status: "failed" as const, error: `Invalid MCP URL for "${key}"` }, } } + const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT let authProvider: McpOAuthProvider | undefined if (!oauthDisabled) { @@ -230,7 +231,10 @@ export const layer = Layer.effect( ) authProvider = provider yield* Effect.tryPromise(() => - provider.refreshTokensIfExpired(mcp.headers ? createFetchWithInit(fetch, { headers: mcp.headers }) : undefined), + withTimeout( + provider.refreshTokensIfExpired(mcp.headers ? createFetchWithInit(fetch, { headers: mcp.headers }) : undefined), + connectTimeout, + ), ).pipe(Effect.ignore) } @@ -251,7 +255,6 @@ export const layer = Layer.effect( }, ] - const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT let lastStatus: Status | undefined for (const { name, transport } of transports) { diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index b9d046b1459..a111e13632b 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -306,6 +306,41 @@ it.live("McpOAuthProvider refreshes expired stored tokens", () => }), ) +it.instance( + "remote connect bounds expired token refresh by mcp timeout", + () => + Effect.acquireUseRelease( + Effect.sync(() => { + const original = McpOAuthProvider.prototype.refreshTokensIfExpired + McpOAuthProvider.prototype.refreshTokensIfExpired = () => { + refreshAuthorizationCalls++ + return new Promise(() => {}) + } + return original + }), + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "remote-timeout" + + const result = yield* mcp.add("remote-timeout", { + type: "remote", + url: "https://mcp.example.com/mcp", + timeout: 20, + }) + + expect(statusName(result.status, "remote-timeout")).toBe("connected") + expect(refreshAuthorizationCalls).toBe(1) + }), + ), + (original) => + Effect.sync(() => { + McpOAuthProvider.prototype.refreshTokensIfExpired = original + }), + ), + { config: { mcp: {} } }, +) + it.instance( "local mcp cwd resolves relative paths against instance directory", () =>