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
+37
View File
@@ -0,0 +1,37 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Effect, Layer } from "effect"
import open from "open"
export interface Interface {
readonly open: (url: string) => Effect.Effect<void, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/McpBrowser") {}
const layer = Layer.succeed(
Service,
Service.of({
open: Effect.fn("McpBrowser.open")(function* (url: string) {
const subprocess = yield* Effect.tryPromise({
try: () => open(url),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
yield* Effect.callback<void, Error>((resume) => {
const timer = setTimeout(() => resume(Effect.void), 500)
subprocess.on("error", (error) => {
clearTimeout(timer)
resume(Effect.fail(error))
})
subprocess.on("exit", (code) => {
if (code === null || code === 0) return
clearTimeout(timer)
resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`)))
})
})
}),
}),
)
export const node = LayerNode.make({ service: Service, layer, deps: [] })
export * as McpBrowser from "./browser"
+16 -24
View File
@@ -1,7 +1,6 @@
import path from "node:path"
import { pathToFileURL } from "node:url"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { type Tool } from "ai"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
@@ -27,7 +26,6 @@ import { McpOAuthCallback } from "./oauth-callback"
import { McpAuth } from "./auth"
import { EventV2Bridge } from "@/event-v2-bridge"
import { TuiEvent } from "@/server/tui-event"
import open from "open"
import { Cause, Effect, Exit, Layer, Context, Schema, Stream } from "effect"
import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state"
@@ -35,6 +33,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { McpCatalog } from "./catalog"
import { McpEvent } from "@opencode-ai/schema/mcp-event"
import { McpBrowser } from "./browser"
const DEFAULT_TIMEOUT = 30_000
const CLIENT_OPTIONS = {
@@ -154,11 +153,19 @@ export interface ServerInstructions {
tools: string[]
}
/** An MCP tool in its native shape; consumers adapt it to their own tool format. */
export interface McpTool {
/** Shared cached definition; consumers must copy rather than mutate it. */
readonly def: MCPToolDef
readonly client: MCPClient
readonly timeout?: number
}
export interface Interface {
readonly status: () => Effect.Effect<Record<string, Status>>
readonly clients: () => Effect.Effect<Record<string, MCPClient>>
readonly instructions: () => Effect.Effect<ServerInstructions[]>
readonly tools: () => Effect.Effect<Record<string, Tool>>
readonly tools: () => Effect.Effect<Record<string, McpTool>>
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
readonly resources: (clientName?: string) => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
readonly resourceTemplates: (
@@ -200,6 +207,7 @@ const layer = Layer.effect(
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const auth = yield* McpAuth.Service
const events = yield* EventV2Bridge.Service
const browser = yield* McpBrowser.Service
type Transport = StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport
@@ -656,7 +664,7 @@ const layer = Layer.effect(
}
const tools = Effect.fn("MCP.tools")(function* () {
const result: Record<string, Tool> = {}
const result: Record<string, McpTool> = {}
const s = yield* InstanceState.get(state)
const cfg = yield* cfgSvc.get()
@@ -672,9 +680,8 @@ const layer = Layer.effect(
continue
}
const timeout = requestTimeout(s, clientName, mcpConfig, defaultTimeout)
for (const mcpTool of listed) {
const key = McpCatalog.toolName(clientName, mcpTool.name)
result[key] = McpCatalog.convertTool(mcpTool, client, timeout)
for (const def of listed) {
result[McpCatalog.toolName(clientName, def.name)] = { def, client, timeout }
}
}
return result
@@ -891,22 +898,7 @@ const layer = Layer.effect(
const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName)
onAuthorization?.(result.authorizationUrl)
yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe(
Effect.flatMap((subprocess) =>
Effect.callback<void, Error>((resume) => {
const timer = setTimeout(() => resume(Effect.void), 500)
subprocess.on("error", (err) => {
clearTimeout(timer)
resume(Effect.fail(err))
})
subprocess.on("exit", (code) => {
if (code !== null && code !== 0) {
clearTimeout(timer)
resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`)))
}
})
}),
),
yield* browser.open(result.authorizationUrl).pipe(
Effect.catch(() => {
return events.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore)
}),
@@ -1006,7 +998,7 @@ export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
export const node = LayerNode.make({
service: Service,
layer: layer,
deps: [CrossSpawnSpawner.node, McpAuth.node, EventV2Bridge.node, Config.node],
deps: [CrossSpawnSpawner.node, McpAuth.node, EventV2Bridge.node, Config.node, McpBrowser.node],
})
export * as MCP from "."