feat(mcp): support client roots (#32230)

This commit is contained in:
Aiden Cline
2026-06-13 19:13:15 -05:00
committed by GitHub
parent 42f339c681
commit f55a931f59
4 changed files with 56 additions and 7 deletions
+18 -5
View File
@@ -1,4 +1,5 @@
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"
@@ -9,6 +10,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import {
ListRootsRequestSchema,
type LoggingMessageNotification,
LoggingMessageNotificationSchema,
type Tool as MCPToolDef,
@@ -42,7 +44,7 @@ const CLIENT_OPTIONS = {
// https://github.com/anomalyco/opencode/issues/23066
// elicitation: {},
// https://github.com/anomalyco/opencode/issues/2308
// roots: {},
roots: {},
// https://github.com/anomalyco/opencode/issues/28567
// tasks: {},
},
@@ -82,6 +84,14 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP
type MCPClient = Client
function createClient(directory: string) {
const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS)
client.setRequestHandler(ListRootsRequestSchema, () =>
Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }),
)
return client
}
const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({
identifier: "MCPStatusConnected",
})
@@ -192,19 +202,21 @@ export const layer = Layer.effect(
* Connect a client via the given transport with resource safety:
* on failure the transport is closed; on success the caller owns it.
*/
const connectTransport = (transport: Transport, timeout: number) =>
Effect.acquireUseRelease(
const connectTransport = Effect.fn("MCP.connectTransport")(function* (transport: Transport, timeout: number) {
const directory = yield* InstanceState.directory
return yield* Effect.acquireUseRelease(
Effect.succeed(transport),
(t) =>
Effect.tryPromise({
try: () => {
const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS)
const client = createClient(directory)
return withTimeout(client.connect(t), timeout).then(() => client)
},
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
}),
(t, exit) => (Exit.isFailure(exit) ? Effect.tryPromise(() => t.close()).pipe(Effect.ignore) : Effect.void),
)
})
const DISABLED_RESULT: CreateResult = { status: { status: "disabled" } }
@@ -777,10 +789,11 @@ export const layer = Layer.effect(
authProvider,
requestInit: mcpConfig.headers ? { headers: mcpConfig.headers } : undefined,
})
const directory = yield* InstanceState.directory
return yield* Effect.tryPromise({
try: () => {
const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS)
const client = createClient(directory)
return client
.connect(transport)
.then(() => ({ authorizationUrl: "", oauthState, client }) satisfies AuthResult)