From 0b7a0b1a801c7338f490ebf18742b63f181fb77c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 24 Jun 2026 17:05:51 -0500 Subject: [PATCH] test(mcp): isolate anonymous OAuth integration --- packages/opencode/src/mcp/index.ts | 4 +- .../test/fixture/mcp-oauth-anonymous.ts | 81 +++++++++++++ .../opencode/test/mcp/oauth-anonymous.test.ts | 108 +++++------------- 3 files changed, 110 insertions(+), 83 deletions(-) create mode 100644 packages/opencode/test/fixture/mcp-oauth-anonymous.ts diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 22c6e90605..ddd8bb23ab 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -808,9 +808,6 @@ export const layer = Layer.effect( oauthConfig?.redirectUri ?? (oauthConfig?.callbackPort ? `http://127.0.0.1:${oauthConfig.callbackPort}${OAUTH_CALLBACK_PATH}` : undefined) - // Start the callback server with custom redirectUri if configured - yield* Effect.promise(() => McpOAuthCallback.ensureRunning(effectiveRedirectUri)) - const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32))) .map((b) => b.toString(16).padStart(2, "0")) .join("") @@ -827,6 +824,7 @@ export const layer = Layer.effect( }, { onRedirect: async (url) => { + await McpOAuthCallback.ensureRunning(effectiveRedirectUri) capturedUrl = url }, }, diff --git a/packages/opencode/test/fixture/mcp-oauth-anonymous.ts b/packages/opencode/test/fixture/mcp-oauth-anonymous.ts new file mode 100644 index 0000000000..531fa73af6 --- /dev/null +++ b/packages/opencode/test/fixture/mcp-oauth-anonymous.ts @@ -0,0 +1,81 @@ +import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" +import { Effect } from "effect" +import { MCP } from "../../src/mcp/index" +import { withTmpdirInstance } from "./fixture" + +const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request): Promise { + if (request.method !== "POST") return new Response(null, { status: 405 }) + + const message = (await request.json()) as { id?: number; method: string } + if (message.method === "initialize") { + return Response.json({ + jsonrpc: "2.0", + id: message.id, + result: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: { tools: {} }, + serverInfo: { name: "anonymous-oauth-test", version: "1" }, + }, + }) + } + if (message.method === "notifications/initialized") return new Response(null, { status: 202 }) + if (message.method === "tools/list") { + return Response.json({ + jsonrpc: "2.0", + id: message.id, + result: { + tools: [{ name: "protected", inputSchema: { type: "object", properties: {} } }], + }, + }) + } + if (message.method === "tools/call") { + return new Response("Authentication required", { + status: 401, + headers: { + "WWW-Authenticate": `Bearer resource_metadata="${new URL("/.well-known/oauth-protected-resource", request.url)}"`, + }, + }) + } + return Response.json({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Method not found" } }) + }, +}) + +try { + const result = await Effect.gen(function* () { + const mcp = yield* MCP.Service + const added = yield* mcp.add("anonymous-oauth", { type: "remote", url: server.url.toString() }) + const initialTools = Object.keys(yield* mcp.tools()) + const client = (yield* mcp.clients())["anonymous-oauth"] + const protectedToolFailed = yield* Effect.promise(() => + client + .callTool({ name: "protected", arguments: {} }) + .then(() => false) + .catch(() => true), + ) + const auth = yield* mcp.authenticate("anonymous-oauth") + + return { + initialStatus: "status" in added.status ? added.status.status : added.status["anonymous-oauth"]?.status, + initialTools, + protectedToolFailed, + authStatus: auth.status, + authError: auth.status === "failed" ? auth.error : undefined, + hasStoredTokens: yield* mcp.hasStoredTokens("anonymous-oauth"), + finalStatus: (yield* mcp.status())["anonymous-oauth"]?.status, + finalTools: Object.keys(yield* mcp.tools()), + } + }).pipe( + withTmpdirInstance({ + config: { mcp: { "anonymous-oauth": { type: "remote", url: server.url.toString() } } }, + }), + Effect.provide(MCP.defaultLayer), + Effect.scoped, + Effect.runPromise, + ) + process.stdout.write(`MCP_OAUTH_RESULT=${JSON.stringify(result)}`) +} finally { + server.stop(true) +} diff --git a/packages/opencode/test/mcp/oauth-anonymous.test.ts b/packages/opencode/test/mcp/oauth-anonymous.test.ts index 70a5fd96eb..02cc2488ae 100644 --- a/packages/opencode/test/mcp/oauth-anonymous.test.ts +++ b/packages/opencode/test/mcp/oauth-anonymous.test.ts @@ -1,82 +1,30 @@ -import { afterAll, expect } from "bun:test" -import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" -import { Effect } from "effect" -import { MCP } from "../../src/mcp/index" -import { testEffect } from "../lib/effect" +import path from "node:path" +import { expect, test } from "bun:test" -const server = Bun.serve({ - port: 0, - async fetch(request) { - if (request.method !== "POST") return new Response(null, { status: 405 }) +test("explicit auth fails when anonymous initialize and catalog emit no OAuth challenge", async () => { + const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-oauth-anonymous.ts")], { + cwd: path.join(import.meta.dir, "../.."), + stdout: "pipe", + stderr: "pipe", + }) + const [code, stdout, stderr] = await Promise.all([ + child.exited, + Bun.readableStreamToText(child.stdout), + Bun.readableStreamToText(child.stderr), + ]) - const message = (await request.json()) as { id?: number; method: string } - if (message.method === "initialize") { - return Response.json({ - jsonrpc: "2.0", - id: message.id, - result: { - protocolVersion: LATEST_PROTOCOL_VERSION, - capabilities: { tools: {} }, - serverInfo: { name: "anonymous-oauth-test", version: "1" }, - }, - }) - } - if (message.method === "notifications/initialized") return new Response(null, { status: 202 }) - if (message.method === "tools/list") { - return Response.json({ - jsonrpc: "2.0", - id: message.id, - result: { - tools: [{ name: "protected", inputSchema: { type: "object", properties: {} } }], - }, - }) - } - if (message.method === "tools/call") { - return new Response("Authentication required", { - status: 401, - headers: { "WWW-Authenticate": `Bearer resource_metadata="${server.url}.well-known/oauth-protected-resource"` }, - }) - } - return Response.json({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Method not found" } }) - }, -}) - -afterAll(() => server.stop(true)) - -const it = testEffect(MCP.defaultLayer) - -it.instance( - "explicit auth fails when anonymous initialize and catalog emit no OAuth challenge", - () => - MCP.Service.use((mcp) => - Effect.gen(function* () { - const added = yield* mcp.add("anonymous-oauth", { type: "remote", url: server.url.toString() }) - expect(added.status).toEqual({ "anonymous-oauth": { status: "connected" } }) - expect(Object.keys(yield* mcp.tools())).toEqual(["anonymous-oauth_protected"]) - - const protectedResponse = yield* Effect.promise(() => - fetch(server.url, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: 1, - method: "tools/call", - params: { name: "protected", arguments: {} }, - }), - }), - ) - expect(protectedResponse.status).toBe(401) - - const result = yield* mcp.authenticate("anonymous-oauth") - expect(result).toEqual({ - status: "failed", - error: - "The server did not issue a standard OAuth challenge. Anonymous MCP access remains available, but authentication was not completed. Verify the server's OAuth configuration or use credentials supported by the server.", - }) - expect(yield* mcp.hasStoredTokens("anonymous-oauth")).toBe(false) - expect(yield* mcp.status()).toEqual({ "anonymous-oauth": { status: "connected" } }) - }), - ), - { config: { mcp: { "anonymous-oauth": { type: "remote", url: server.url.toString() } } } }, -) + expect(code, stderr).toBe(0) + const marker = "MCP_OAUTH_RESULT=" + expect(stdout).toContain(marker) + expect(JSON.parse(stdout.slice(stdout.lastIndexOf(marker) + marker.length))).toEqual({ + initialStatus: "connected", + initialTools: ["anonymous-oauth_protected"], + protectedToolFailed: true, + authStatus: "failed", + authError: + "The server did not issue a standard OAuth challenge. Anonymous MCP access remains available, but authentication was not completed. Verify the server's OAuth configuration or use credentials supported by the server.", + hasStoredTokens: false, + finalStatus: "connected", + finalTools: ["anonymous-oauth_protected"], + }) +}, 30_000)