From 0c1124bb94509e1d85a3b7bf52aec49e4295126a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 29 Jun 2026 09:14:10 -0500 Subject: [PATCH] mcp skeleton --- packages/core/src/location-services.ts | 2 + packages/core/src/mcp.ts | 227 +++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 packages/core/src/mcp.ts diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index a0ae0228c7..ea417cbd77 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -16,6 +16,7 @@ import { Integration } from "./integration" import { Location } from "./location" import { LocationMutation } from "./location-mutation" import { LocationServiceMap } from "./location-service-map" +import { MCP } from "./mcp" import { PermissionV2 } from "./permission" import { PluginV2 } from "./plugin" import { PluginInternal } from "./plugin/internal" @@ -66,6 +67,7 @@ export const locationServices = LayerNode.group([ SystemContextBuiltIns.node, LocationMutation.node, FileMutation.node, + MCP.node, PermissionV2.node, ToolOutputStore.node, ToolRegistry.node, diff --git a/packages/core/src/mcp.ts b/packages/core/src/mcp.ts new file mode 100644 index 0000000000..029f174a89 --- /dev/null +++ b/packages/core/src/mcp.ts @@ -0,0 +1,227 @@ +export * as MCP from "./mcp" + +import { Context, Effect, Layer, Schema } from "effect" +import { makeLocationNode } from "./effect/app-node" +import { ConfigMCP } from "./config/mcp" +import { Integration } from "./integration" +import { IntegrationConnection } from "./integration/connection" + +export const ServerName = Schema.String.pipe(Schema.brand("MCP.ServerName")) +export type ServerName = typeof ServerName.Type + +const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({ + identifier: "MCP.Status.Connected", +}) +const StatusDisconnected = Schema.Struct({ status: Schema.Literal("disconnected") }).annotate({ + identifier: "MCP.Status.Disconnected", +}) +const StatusDisabled = Schema.Struct({ status: Schema.Literal("disabled") }).annotate({ + identifier: "MCP.Status.Disabled", +}) +const StatusFailed = Schema.Struct({ status: Schema.Literal("failed"), error: Schema.String }).annotate({ + identifier: "MCP.Status.Failed", +}) +const StatusNeedsAuth = Schema.Struct({ status: Schema.Literal("needs_auth") }).annotate({ + identifier: "MCP.Status.NeedsAuth", +}) +const StatusNeedsClientRegistration = Schema.Struct({ + status: Schema.Literal("needs_client_registration"), + error: Schema.String, +}).annotate({ identifier: "MCP.Status.NeedsClientRegistration" }) + +export const Status = Schema.Union([ + StatusConnected, + StatusDisconnected, + StatusDisabled, + StatusFailed, + StatusNeedsAuth, + StatusNeedsClientRegistration, +]).pipe(Schema.toTaggedUnion("status")) +export type Status = typeof Status.Type + +export class ServerInfo extends Schema.Class("MCP.ServerInfo")({ + name: ServerName, + config: ConfigMCP.Server, + status: Status, + integrationID: Integration.ID.pipe(Schema.optional), + connection: IntegrationConnection.Info.pipe(Schema.optional), +}) {} + +export class ServerInstructions extends Schema.Class("MCP.ServerInstructions")({ + server: ServerName, + instructions: Schema.String, + tools: Schema.Array(Schema.String), +}) {} + +export class PromptArgument extends Schema.Class("MCP.PromptArgument")({ + name: Schema.String, + description: Schema.String.pipe(Schema.optional), + required: Schema.Boolean.pipe(Schema.optional), +}) {} + +export class Prompt extends Schema.Class("MCP.Prompt")({ + server: ServerName, + name: Schema.String, + description: Schema.String.pipe(Schema.optional), + arguments: Schema.Array(PromptArgument).pipe(Schema.optional), +}) {} + +export class PromptMessage extends Schema.Class("MCP.PromptMessage")({ + role: Schema.String, + content: Schema.Unknown, +}) {} + +export class PromptResult extends Schema.Class("MCP.PromptResult")({ + server: ServerName, + name: Schema.String, + messages: Schema.Array(PromptMessage), +}) {} + +export class Resource extends Schema.Class("MCP.Resource")({ + server: ServerName, + name: Schema.String, + uri: Schema.String, + description: Schema.String.pipe(Schema.optional), + mimeType: Schema.String.pipe(Schema.optional), +}) {} + +export class ResourceTemplate extends Schema.Class("MCP.ResourceTemplate")({ + server: ServerName, + name: Schema.String, + uriTemplate: Schema.String, + description: Schema.String.pipe(Schema.optional), + mimeType: Schema.String.pipe(Schema.optional), +}) {} + +export class ResourceCatalog extends Schema.Class("MCP.ResourceCatalog")({ + resources: Schema.Array(Resource), + templates: Schema.Array(ResourceTemplate), +}) {} + +export const ResourceContentPart = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("text"), + uri: Schema.String, + text: Schema.String, + mimeType: Schema.String.pipe(Schema.optional), + }), + Schema.Struct({ + type: Schema.Literal("blob"), + uri: Schema.String, + blob: Schema.String, + mimeType: Schema.String.pipe(Schema.optional), + }), +]).pipe(Schema.toTaggedUnion("type")) +export type ResourceContentPart = typeof ResourceContentPart.Type + +export class ResourceContent extends Schema.Class("MCP.ResourceContent")({ + server: ServerName, + uri: Schema.String, + contents: Schema.Array(ResourceContentPart), +}) {} + +export class NotFoundError extends Schema.TaggedErrorClass()("MCP.NotFoundError", { + server: ServerName, +}) {} + +type ServerEntry = { + readonly config: typeof ConfigMCP.Server.Type + readonly status: Status + readonly integrationID?: Integration.ID + readonly connection?: IntegrationConnection.Info +} + +export interface Interface { + readonly servers: () => Effect.Effect + readonly add: (server: ServerName | string, config: typeof ConfigMCP.Server.Type) => Effect.Effect + readonly connect: (server: ServerName | string) => Effect.Effect + readonly disconnect: (server: ServerName | string) => Effect.Effect + readonly instructions: () => Effect.Effect + readonly prompts: (input?: { readonly server?: ServerName | string }) => Effect.Effect + readonly prompt: (input: { + readonly server: ServerName | string + readonly name: string + readonly args?: Record + }) => Effect.Effect + readonly resourceCatalog: (input?: { + readonly server?: ServerName | string + }) => Effect.Effect + readonly readResource: (input: { + readonly server: ServerName | string + readonly uri: string + }) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/MCP") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const runtime = new Map() + + const requireServer = Effect.fnUntraced(function* (server: ServerName | string) { + const name = ServerName.make(server) + const entry = runtime.get(name) + if (!entry) return yield* new NotFoundError({ server: name }) + return { name, entry } + }) + + const visibleStatus = (entry: ServerEntry): Status => (entry.config.disabled ? { status: "disabled" } : entry.status) + + const info = (name: ServerName, entry: ServerEntry) => + new ServerInfo({ + name, + config: entry.config, + status: visibleStatus(entry), + integrationID: entry.integrationID, + connection: entry.connection, + }) + + return Service.of({ + servers: Effect.fn("MCP.servers")(function* () { + return Array.from(runtime, ([name, entry]) => info(name, entry)).toSorted((a, b) => + a.name.localeCompare(b.name), + ) + }), + add: Effect.fn("MCP.add")(function* (server, config) { + const name = ServerName.make(server) + const status: Status = config.disabled ? { status: "disabled" } : { status: "disconnected" } + const entry = { config, status } + runtime.set(name, entry) + return info(name, entry) + }), + connect: Effect.fn("MCP.connect")(function* (server) { + const current = yield* requireServer(server) + return info(current.name, current.entry) + }), + disconnect: Effect.fn("MCP.disconnect")(function* (server) { + const current = yield* requireServer(server) + const status: Status = current.entry.config.disabled ? { status: "disabled" } : { status: "disconnected" } + const entry = { ...current.entry, status } + runtime.set(current.name, entry) + return info(current.name, entry) + }), + instructions: Effect.fn("MCP.instructions")(function* () { + return [] + }), + prompts: Effect.fn("MCP.prompts")(function* (input) { + if (input?.server !== undefined) yield* requireServer(input.server) + return [] + }), + prompt: Effect.fn("MCP.prompt")(function* (input) { + yield* requireServer(input.server) + return undefined + }), + resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* (input) { + if (input?.server !== undefined) yield* requireServer(input.server) + return new ResourceCatalog({ resources: [], templates: [] }) + }), + readResource: Effect.fn("MCP.readResource")(function* (input) { + yield* requireServer(input.server) + return undefined + }), + }) + }), +) + +export const node = makeLocationNode({ service: Service, layer, deps: [] })