import { MCP } from "@/mcp" import { ConfigMCP } from "@/config/mcp" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" const AddPayload = Schema.Struct({ name: Schema.String, config: ConfigMCP.Info, }).annotate({ identifier: "McpAddInput" }) const StatusMap = Schema.Record(Schema.String, MCP.Status) const AuthStartResponse = Schema.Struct({ authorizationUrl: Schema.String, oauthState: Schema.String, }).annotate({ identifier: "McpAuthStartResponse" }) const AuthCallbackPayload = Schema.Struct({ code: Schema.String, }).annotate({ identifier: "McpAuthCallbackInput" }) const AuthRemoveResponse = Schema.Struct({ success: Schema.Literal(true), }).annotate({ identifier: "McpAuthRemoveResponse" }) export const McpPaths = { status: "/mcp", auth: "/mcp/:name/auth", authCallback: "/mcp/:name/auth/callback", authAuthenticate: "/mcp/:name/auth/authenticate", connect: "/mcp/:name/connect", disconnect: "/mcp/:name/disconnect", } as const export const McpApi = HttpApi.make("mcp") .add( HttpApiGroup.make("mcp") .add( HttpApiEndpoint.get("status", McpPaths.status, { success: Schema.Record(Schema.String, MCP.Status), }).annotateMerge( OpenApi.annotations({ identifier: "mcp.status", summary: "Get MCP status", description: "Get the status of all Model Context Protocol (MCP) servers.", }), ), HttpApiEndpoint.post("add", McpPaths.status, { payload: AddPayload, success: StatusMap, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.add", summary: "Add MCP server", description: "Dynamically add a new Model Context Protocol (MCP) server to the system.", }), ), HttpApiEndpoint.post("authStart", McpPaths.auth, { params: { name: Schema.String }, success: AuthStartResponse, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.start", summary: "Start MCP OAuth", description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.", }), ), HttpApiEndpoint.post("authCallback", McpPaths.authCallback, { params: { name: Schema.String }, payload: AuthCallbackPayload, success: MCP.Status, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.callback", summary: "Complete MCP OAuth", description: "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", }), ), HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, { params: { name: Schema.String }, success: MCP.Status, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.authenticate", summary: "Authenticate MCP OAuth", description: "Start OAuth flow and wait for callback (opens browser).", }), ), HttpApiEndpoint.delete("authRemove", McpPaths.auth, { params: { name: Schema.String }, success: AuthRemoveResponse, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.remove", summary: "Remove MCP OAuth", description: "Remove OAuth credentials for an MCP server.", }), ), HttpApiEndpoint.post("connect", McpPaths.connect, { params: { name: Schema.String }, success: Schema.Boolean, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.connect", description: "Connect an MCP server.", }), ), HttpApiEndpoint.post("disconnect", McpPaths.disconnect, { params: { name: Schema.String }, success: Schema.Boolean, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.disconnect", description: "Disconnect an MCP server.", }), ), ) .annotateMerge( OpenApi.annotations({ title: "mcp", description: "Experimental HttpApi MCP routes.", }), ) .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ title: "opencode experimental HttpApi", version: "0.0.1", description: "Experimental HttpApi surface for selected instance routes.", }), ) export const mcpHandlers = Layer.unwrap( Effect.gen(function* () { const mcp = yield* MCP.Service const status = Effect.fn("McpHttpApi.status")(function* () { return yield* mcp.status() }) const add = Effect.fn("McpHttpApi.add")(function* (ctx: { payload: typeof AddPayload.Type }) { const payload = Schema.decodeUnknownSync(AddPayload)(ctx.payload) const result = (yield* mcp.add(payload.name, payload.config)).status return Schema.decodeUnknownSync(StatusMap)("status" in result ? { [payload.name]: result } : result) }) const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) { if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({}) return yield* mcp.startAuth(ctx.params.name) }) const authCallback = Effect.fn("McpHttpApi.authCallback")(function* (ctx: { params: { name: string } payload: typeof AuthCallbackPayload.Type }) { return yield* mcp.finishAuth(ctx.params.name, ctx.payload.code) }) const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) { if (!(yield* mcp.supportsOAuth(ctx.params.name))) return yield* new HttpApiError.BadRequest({}) return yield* mcp.authenticate(ctx.params.name) }) const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) { yield* mcp.removeAuth(ctx.params.name) return { success: true as const } }) const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) { yield* mcp.connect(ctx.params.name) return true }) const disconnect = Effect.fn("McpHttpApi.disconnect")(function* (ctx: { params: { name: string } }) { yield* mcp.disconnect(ctx.params.name) return true }) return HttpApiBuilder.group(McpApi, "mcp", (handlers) => handlers .handle("status", status) .handle("add", add) .handle("authStart", authStart) .handle("authCallback", authCallback) .handle("authAuthenticate", authAuthenticate) .handle("authRemove", authRemove) .handle("connect", connect) .handle("disconnect", disconnect), ) }), ).pipe(Layer.provide(MCP.defaultLayer))