chore: merge dev into v2 (#35591)

Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
This commit is contained in:
Aiden Cline
2026-07-06 16:05:29 -05:00
committed by GitHub
co-authored by Frank Aarav Sareen Brendan Allan opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Jack Brendan Allan Shoubhit Dash opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> James Long Dustin Deus starptech Luke Parker 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 Dax usrnk1 Jay runvip opencode Julian Coy Vladimir Glafirov Adam Kit Langton Simon Klee Jay David Hill
parent f87998f37f
commit 9e0d3976e1
332 changed files with 24650 additions and 4497 deletions
+190 -208
View File
@@ -1,152 +1,138 @@
import { expect, mock, beforeEach } from "bun:test"
import { EventEmitter } from "events"
import { expect } from "bun:test"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Deferred, Effect, Layer, Option } from "effect"
import { Config } from "../../src/config/config"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { McpAuth } from "../../src/mcp/auth"
import { McpBrowser } from "../../src/mcp/browser"
import { MCP } from "../../src/mcp/index"
import { McpOAuthCallback } from "../../src/mcp/oauth-callback"
import { awaitWithTimeout, testEffect } from "../lib/effect"
import type { MCP as MCPNS } from "../../src/mcp/index"
// Track open() calls and control failure behavior
let openShouldFail = false
let openCalledWith: string | undefined
let openDeferred: Deferred.Deferred<string> | undefined
const browsers = new Map<string, { opened: Deferred.Deferred<string>; fail: boolean }>()
void mock.module("open", () => ({
default: async (url: string) => {
openCalledWith = url
if (openDeferred) Effect.runSync(Deferred.succeed(openDeferred, url).pipe(Effect.ignore))
// Return a mock subprocess that emits an error if openShouldFail is true
const subprocess = new EventEmitter()
if (openShouldFail) {
// Emit error asynchronously like a real subprocess would
setTimeout(() => {
subprocess.emit("error", new Error("spawn xdg-open ENOENT"))
}, 10)
}
return subprocess
},
}))
// Mock UnauthorizedError
class MockUnauthorizedError extends Error {
constructor() {
super("Unauthorized")
this.name = "UnauthorizedError"
}
}
// Track what options were passed to each transport constructor
const transportCalls: Array<{
type: "streamable" | "sse"
url: string
options: { authProvider?: unknown; requestInit?: RequestInit }
}> = []
// Mock the transport constructors
void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: class MockStreamableHTTP {
url: string
authProvider: { redirectToAuthorization?: (url: URL) => Promise<void> } | undefined
constructor(
url: URL,
options?: { authProvider?: { redirectToAuthorization?: (url: URL) => Promise<void> }; requestInit?: RequestInit },
) {
this.url = url.toString()
this.authProvider = options?.authProvider
transportCalls.push({
type: "streamable",
url: url.toString(),
options: options ?? {},
})
}
async start() {
// Simulate OAuth redirect by calling the authProvider's redirectToAuthorization
if (this.authProvider?.redirectToAuthorization) {
await this.authProvider.redirectToAuthorization(new URL("https://auth.example.com/authorize?client_id=test"))
}
throw new MockUnauthorizedError()
}
async finishAuth(_code: string) {
// Mock successful auth completion
}
},
}))
void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
SSEClientTransport: class MockSSE {
constructor(url: URL) {
transportCalls.push({
type: "sse",
url: url.toString(),
options: {},
})
}
async start() {
throw new Error("Mock SSE transport cannot connect")
}
},
}))
// Mock the MCP SDK Client to trigger OAuth flow
void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
Client: class MockClient {
setRequestHandler() {}
async connect(transport: { start: () => Promise<void> }) {
await transport.start()
}
getServerCapabilities() {
return { tools: {} }
}
},
}))
// Mock UnauthorizedError in the auth module
void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
UnauthorizedError: MockUnauthorizedError,
}))
beforeEach(() => {
openShouldFail = false
openCalledWith = undefined
openDeferred = undefined
transportCalls.length = 0
})
// Import modules after mocking
const { MCP } = await import("../../src/mcp/index")
const { EventV2Bridge } = await import("../../src/event-v2-bridge")
const { Config } = await import("../../src/config/config")
const { McpAuth } = await import("../../src/mcp/auth")
const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
const { FSUtil } = await import("@opencode-ai/core/fs-util")
const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner")
const mcpTest = testEffect(
LayerNode.compile(
LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node]),
),
const browserLayer = Layer.succeed(
McpBrowser.Service,
McpBrowser.Service.of({
open: (url) =>
Effect.gen(function* () {
const browser = browsers.get(new URL(url).origin)
if (!browser) return yield* Effect.fail(new Error(`Unexpected browser URL: ${url}`))
Deferred.doneUnsafe(browser.opened, Effect.succeed(url))
if (browser.fail) return yield* Effect.fail(new Error("spawn xdg-open ENOENT"))
yield* Effect.tryPromise({
try: () => fetch(url).then((response) => response.body?.cancel()),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
}),
}),
)
const service = MCP.Service as unknown as Effect.Effect<MCPNS.Interface, never, never>
const config = (name: string, headers?: Record<string, string>) => ({
mcp: {
[name]: {
type: "remote" as const,
url: "https://example.com/mcp",
headers,
},
},
})
const mcpTest = testEffect(
LayerNode.compile(LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node]), [
[McpBrowser.node, browserLayer],
]),
)
const serveOAuthMcp = Effect.acquireRelease(
Effect.promise(async () => {
const requests: Array<{ pathname: string; headers: Headers }> = []
const protocol = new Server({ name: "oauth-browser", version: "1.0.0" }, { capabilities: { tools: {} } })
protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
enableJsonResponse: true,
})
await protocol.connect(transport)
const http = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
const url = new URL(request.url)
requests.push({ pathname: url.pathname, headers: new Headers(request.headers) })
if (url.pathname === "/mcp") {
if (request.headers.get("authorization") === "Bearer test-access-token") {
return transport.handleRequest(request)
}
return new Response("Unauthorized", {
status: 401,
headers: {
"WWW-Authenticate": `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource/mcp", scope="mcp"`,
},
})
}
if (url.pathname === "/.well-known/oauth-protected-resource/mcp") {
return Response.json({
resource: `${url.origin}/mcp`,
authorization_servers: [url.origin],
scopes_supported: ["mcp"],
})
}
if (url.pathname === "/.well-known/oauth-authorization-server") {
return Response.json({
issuer: url.origin,
authorization_endpoint: `${url.origin}/authorize`,
token_endpoint: `${url.origin}/token`,
registration_endpoint: `${url.origin}/register`,
scopes_supported: ["mcp"],
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
token_endpoint_auth_methods_supported: ["none"],
code_challenge_methods_supported: ["S256"],
})
}
if (url.pathname === "/register") {
const metadata = await request.json()
if (!metadata || typeof metadata !== "object") return new Response("Invalid metadata", { status: 400 })
return Response.json({ ...metadata, client_id: "test-client" }, { status: 201 })
}
if (url.pathname === "/authorize") {
const redirect = new URL(url.searchParams.get("redirect_uri") ?? "")
redirect.searchParams.set("code", "test-code")
const state = url.searchParams.get("state")
if (state) redirect.searchParams.set("state", state)
return Response.redirect(redirect, 302)
}
if (url.pathname === "/token") {
return Response.json({ access_token: "test-access-token", token_type: "Bearer", scope: "mcp" })
}
return new Response("Not found", { status: 404 })
},
})
return {
requests,
url: new URL("/mcp", http.url).toString(),
close: async () => {
await http.stop(true)
await protocol.close()
},
}
}),
(server) => Effect.promise(server.close),
)
const withCallbackStop = Effect.addFinalizer(() => Effect.promise(() => McpOAuthCallback.stop()).pipe(Effect.ignore))
const trackBrowserOpen = Effect.gen(function* () {
const opened = yield* Deferred.make<string>()
openDeferred = opened
yield* Effect.addFinalizer(() => Effect.sync(() => (openDeferred = undefined)))
return opened
})
const trackBrowserOpen = (url: string, fail = false) =>
Effect.gen(function* () {
const origin = new URL(url).origin
const opened = yield* Deferred.make<string>()
browsers.set(origin, { opened, fail })
yield* Effect.addFinalizer(() => Effect.sync(() => browsers.delete(origin)))
return opened
})
const trackBrowserOpenFailed = Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@@ -160,84 +146,80 @@ const trackBrowserOpenFailed = Effect.gen(function* () {
return event
})
const authenticateScoped = (name: string, onAuthorization?: (authorizationUrl: string) => void) =>
const addServer = Effect.fnUntraced(function* (name: string, url: string, headers?: Record<string, string>) {
const mcp = yield* MCP.Service
const result = yield* mcp.add(name, { type: "remote", url, headers })
expect(result.status).toMatchObject({ [name]: { status: "needs_auth" } })
return mcp
})
mcpTest.instance("BrowserOpenFailed event is published when browser launch fails", () =>
Effect.gen(function* () {
const mcp = yield* service
yield* mcp.authenticate(name, onAuthorization).pipe(
Effect.ignore,
Effect.catchCause(() => Effect.void),
Effect.forkScoped,
yield* withCallbackStop
const server = yield* serveOAuthMcp
yield* trackBrowserOpen(server.url, true)
const event = yield* trackBrowserOpenFailed
const mcp = yield* addServer("test-oauth-server", server.url)
yield* mcp.authenticate("test-oauth-server").pipe(Effect.ignore, Effect.forkScoped)
const failure = yield* awaitWithTimeout(
Deferred.await(event),
"Timed out waiting for BrowserOpenFailed event",
"5 seconds",
)
})
mcpTest.instance(
"BrowserOpenFailed event is published when open() throws",
() =>
Effect.gen(function* () {
yield* withCallbackStop
openShouldFail = true
const event = yield* trackBrowserOpenFailed
yield* authenticateScoped("test-oauth-server")
const failure = yield* awaitWithTimeout(
Deferred.await(event),
"Timed out waiting for BrowserOpenFailed event",
"5 seconds",
)
expect(failure.mcpName).toBe("test-oauth-server")
expect(failure.url).toContain("https://")
}),
{ config: config("test-oauth-server") },
expect(failure.mcpName).toBe("test-oauth-server")
expect(failure.url).toStartWith(new URL("/authorize", server.url).toString())
}),
)
mcpTest.instance(
"BrowserOpenFailed event is NOT published when open() succeeds",
() =>
Effect.gen(function* () {
yield* withCallbackStop
openShouldFail = false
mcpTest.instance("BrowserOpenFailed event is not published when browser launch succeeds", () =>
Effect.gen(function* () {
yield* withCallbackStop
const server = yield* serveOAuthMcp
const opened = yield* trackBrowserOpen
const event = yield* trackBrowserOpenFailed
yield* authenticateScoped("test-oauth-server-2")
const opened = yield* trackBrowserOpen(server.url)
const event = yield* trackBrowserOpenFailed
const mcp = yield* addServer("test-oauth-server-2", server.url)
const status = yield* awaitWithTimeout(
mcp.authenticate("test-oauth-server-2"),
"Timed out completing OAuth authentication",
"5 seconds",
)
const url = yield* Deferred.await(opened)
const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis"))
yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()", "5 seconds")
const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis"))
expect(failure).toEqual(Option.none())
expect(openCalledWith).toBeDefined()
}),
{ config: config("test-oauth-server-2") },
expect(status).toEqual({ status: "connected" })
expect(failure).toEqual(Option.none())
expect(new URL(url).origin).toBe(new URL(server.url).origin)
}),
)
mcpTest.instance(
"open() is called with the authorization URL",
() =>
Effect.gen(function* () {
yield* withCallbackStop
openShouldFail = false
openCalledWith = undefined
mcpTest.instance("browser launch receives the discovered authorization URL", () =>
Effect.gen(function* () {
yield* withCallbackStop
const server = yield* serveOAuthMcp
const opened = yield* trackBrowserOpen
const event = yield* trackBrowserOpenFailed
const authorization = yield* Deferred.make<string>()
yield* authenticateScoped("test-oauth-server-3", (url) => Deferred.doneUnsafe(authorization, Effect.succeed(url)))
const opened = yield* trackBrowserOpen(server.url)
const authorization = yield* Deferred.make<string>()
const mcp = yield* addServer("test-oauth-server-3", server.url, { "X-Custom-Header": "custom-value" })
const status = yield* awaitWithTimeout(
mcp.authenticate("test-oauth-server-3", (url) => Deferred.doneUnsafe(authorization, Effect.succeed(url))),
"Timed out completing OAuth authentication",
"5 seconds",
)
const url = yield* Deferred.await(opened)
const authorizationUrl = yield* Deferred.await(authorization)
const url = yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()", "5 seconds")
const authorizationUrl = yield* awaitWithTimeout(
Deferred.await(authorization),
"Timed out waiting for authorization URL",
"5 seconds",
)
const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis"))
expect(failure).toEqual(Option.none())
expect(authorizationUrl).toBe(url)
expect(typeof url).toBe("string")
expect(url).toContain("https://")
expect(transportCalls.at(-1)?.options.requestInit?.headers).toEqual({ "X-Custom-Header": "custom-value" })
}),
{ config: config("test-oauth-server-3", { "X-Custom-Header": "custom-value" }) },
expect(status).toEqual({ status: "connected" })
expect(authorizationUrl).toBe(url)
expect(new URL(url).pathname).toBe("/authorize")
expect(new URL(url).searchParams.get("client_id")).toBe("test-client")
expect(
server.requests.some(
(request) => request.pathname === "/mcp" && request.headers.get("x-custom-header") === "custom-value",
),
).toBe(true)
}),
)