summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-25 19:16:19 -0400
committerGitHub <[email protected]>2026-04-25 19:16:19 -0400
commita14c22d4e9196eda3fc217213dcd7b01344087de (patch)
treeda0d75dcf164b2f0649359416bce97288585364f
parent58c65874ba6aff2f16f5310dacddc3a89eb7b2cd (diff)
downloadopencode-a14c22d4e9196eda3fc217213dcd7b01344087de.tar.gz
opencode-a14c22d4e9196eda3fc217213dcd7b01344087de.zip
feat(httpapi): bridge mcp control endpoints (#24403)
-rw-r--r--.opencode/skills/effect/SKILL.md29
-rw-r--r--packages/opencode/specs/effect/http-api.md190
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/mcp.ts58
-rw-r--r--packages/opencode/src/server/routes/instance/index.ts3
-rw-r--r--packages/opencode/test/server/httpapi-mcp.test.ts46
5 files changed, 294 insertions, 32 deletions
diff --git a/.opencode/skills/effect/SKILL.md b/.opencode/skills/effect/SKILL.md
index 475814637..78216ab01 100644
--- a/.opencode/skills/effect/SKILL.md
+++ b/.opencode/skills/effect/SKILL.md
@@ -1,21 +1,30 @@
---
name: effect
-description: Answer questions about the Effect framework
+description: Work with Effect v4 / effect-smol TypeScript code in this repo
---
# Effect
-This codebase uses Effect, a framework for writing typescript.
+This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows.
-## How to Answer Effect Questions
+## Source Of Truth
-1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
- `.opencode/references/effect-smol` in this project NOT the skill folder.
-2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
-3. Provide responses based on the actual Effect source code and documentation
+Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
+
+1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder.
+2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
+3. Also inspect existing repo code for local house style before introducing new patterns.
+4. Prefer answers and implementations backed by specific source files or nearby repo examples.
## Guidelines
-- Always use the explore agent with the cloned repository when answering Effect-related questions
-- Reference specific files and patterns found in the Effect codebase
-- Do not answer from memory - always verify against the source
+- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses.
+- Use `Effect.gen(function* () { ... })` for multi-step workflows.
+- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows.
+- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces.
+- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services.
+- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so.
+- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see.
+- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior.
+- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types.
+- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first.
diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md
index e5a64d920..2be0261ea 100644
--- a/packages/opencode/specs/effect/http-api.md
+++ b/packages/opencode/specs/effect/http-api.md
@@ -178,8 +178,8 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
| `config` | `bridged` | read, providers, update |
| `project` | `bridged` | list, current, git init, update |
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
-| `mcp` | `bridged` partial | status only |
-| `workspace` | `bridged` | list, get, enter |
+| `mcp` | `bridged` partial | status, add, connect/disconnect; OAuth remains |
+| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain |
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later |
| `session` | `later/special` | large stateful surface plus streaming |
@@ -188,24 +188,180 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
| `pty` | `special` | websocket |
| `tui` | `special` | UI bridge |
+## Full Route Checklist
+
+This checklist tracks bridge parity only. Checked routes are available through the experimental `HttpApi` bridge; Hono deletion is tracked separately by the deletion checklist above.
+
+### Top-Level Instance Routes
+
+- [x] `POST /instance/dispose` - dispose active instance after response.
+- [x] `GET /path` - current directory and worktree paths.
+- [x] `GET /vcs` - current VCS status.
+- [x] `GET /vcs/diff` - VCS diff summary.
+- [x] `GET /command` - command catalog.
+- [x] `GET /agent` - agent catalog.
+- [x] `GET /skill` - skill catalog.
+- [x] `GET /lsp` - LSP status.
+- [x] `GET /formatter` - formatter status.
+
+### Config Routes
+
+- [x] `GET /config` - read config.
+- [x] `PATCH /config` - update config and dispose active instance after response.
+- [x] `GET /config/providers` - config provider summary.
+
+### Project Routes
+
+- [x] `GET /project` - list projects.
+- [x] `GET /project/current` - current project.
+- [x] `POST /project/git/init` - initialize git and reload active instance after response.
+- [x] `PATCH /project/:projectID` - update project metadata.
+
+### Provider Routes
+
+- [x] `GET /provider` - list providers.
+- [x] `GET /provider/auth` - list provider auth methods.
+- [x] `POST /provider/:providerID/oauth/authorize` - start provider OAuth.
+- [x] `POST /provider/:providerID/oauth/callback` - finish provider OAuth.
+
+### Question Routes
+
+- [x] `GET /question` - list questions.
+- [x] `POST /question/:requestID/reply` - reply to question.
+- [x] `POST /question/:requestID/reject` - reject question.
+
+### Permission Routes
+
+- [x] `GET /permission` - list permission requests.
+- [x] `POST /permission/:requestID/reply` - reply to permission request.
+
+### File Routes
+
+- [x] `GET /find` - text search.
+- [x] `GET /find/file` - file search.
+- [x] `GET /find/symbol` - symbol search.
+- [x] `GET /file` - list directory entries.
+- [x] `GET /file/content` - read file content.
+- [x] `GET /file/status` - file status.
+
+### MCP Routes
+
+- [x] `GET /mcp` - MCP status.
+- [x] `POST /mcp` - add MCP server at runtime.
+- [ ] `POST /mcp/:name/auth` - start MCP OAuth.
+- [ ] `POST /mcp/:name/auth/callback` - finish MCP OAuth callback.
+- [ ] `POST /mcp/:name/auth/authenticate` - run MCP OAuth authenticate flow.
+- [ ] `DELETE /mcp/:name/auth` - remove MCP OAuth credentials.
+- [x] `POST /mcp/:name/connect` - connect MCP server.
+- [x] `POST /mcp/:name/disconnect` - disconnect MCP server.
+
+### Experimental Routes
+
+- [x] `GET /experimental/console` - active Console provider metadata.
+- [x] `GET /experimental/console/orgs` - switchable Console orgs.
+- [ ] `POST /experimental/console/switch` - switch active Console org.
+- [x] `GET /experimental/tool/ids` - tool IDs.
+- [ ] `GET /experimental/tool` - tools for provider/model.
+- [x] `GET /experimental/worktree` - list worktrees.
+- [x] `POST /experimental/worktree` - create worktree.
+- [x] `DELETE /experimental/worktree` - remove worktree.
+- [x] `POST /experimental/worktree/reset` - reset worktree.
+- [ ] `GET /experimental/session` - global session list.
+- [x] `GET /experimental/resource` - MCP resources.
+
+### Workspace Routes
+
+- [x] `GET /experimental/workspace/adaptor` - list workspace adaptors.
+- [ ] `POST /experimental/workspace` - create workspace.
+- [x] `GET /experimental/workspace` - list workspaces.
+- [x] `GET /experimental/workspace/status` - workspace status.
+- [ ] `DELETE /experimental/workspace/:id` - remove workspace.
+- [ ] `POST /experimental/workspace/:id/session-restore` - restore session into workspace.
+
+### Sync Routes
+
+- [ ] `POST /sync/start` - start workspace sync.
+- [ ] `POST /sync/replay` - replay sync events.
+- [ ] `POST /sync/history` - list sync event history.
+
+### Session Routes
+
+- [ ] `GET /session` - list sessions.
+- [ ] `GET /session/status` - session status map.
+- [ ] `GET /session/:sessionID` - get session.
+- [ ] `GET /session/:sessionID/children` - get child sessions.
+- [ ] `GET /session/:sessionID/todo` - get session todos.
+- [ ] `POST /session` - create session.
+- [ ] `DELETE /session/:sessionID` - delete session.
+- [ ] `PATCH /session/:sessionID` - update session metadata.
+- [ ] `POST /session/:sessionID/init` - run project init command.
+- [ ] `POST /session/:sessionID/fork` - fork session.
+- [ ] `POST /session/:sessionID/abort` - abort session.
+- [ ] `POST /session/:sessionID/share` - share session.
+- [ ] `GET /session/:sessionID/diff` - session diff.
+- [ ] `DELETE /session/:sessionID/share` - unshare session.
+- [ ] `POST /session/:sessionID/summarize` - summarize session.
+- [ ] `GET /session/:sessionID/message` - list session messages.
+- [ ] `GET /session/:sessionID/message/:messageID` - get message.
+- [ ] `DELETE /session/:sessionID/message/:messageID` - delete message.
+- [ ] `DELETE /session/:sessionID/message/:messageID/part/:partID` - delete part.
+- [ ] `PATCH /session/:sessionID/message/:messageID/part/:partID` - update part.
+- [ ] `POST /session/:sessionID/message` - prompt with streaming response.
+- [ ] `POST /session/:sessionID/prompt_async` - async prompt.
+- [ ] `POST /session/:sessionID/command` - run command.
+- [ ] `POST /session/:sessionID/shell` - run shell command.
+- [ ] `POST /session/:sessionID/revert` - revert message.
+- [ ] `POST /session/:sessionID/unrevert` - restore reverted messages.
+- [ ] `POST /session/:sessionID/permissions/:permissionID` - deprecated permission response route.
+
+### Event Routes
+
+- [ ] `GET /event` - SSE event stream; replace with raw Effect HTTP, not `HttpApi`.
+
+### PTY Routes
+
+- [ ] `GET /pty` - list PTY sessions.
+- [ ] `POST /pty` - create PTY session.
+- [ ] `GET /pty/:ptyID` - get PTY session.
+- [ ] `PUT /pty/:ptyID` - update PTY session.
+- [ ] `DELETE /pty/:ptyID` - remove PTY session.
+- [ ] `GET /pty/:ptyID/connect` - PTY websocket; replace with raw Effect HTTP/websocket support.
+
+### TUI Routes
+
+- [ ] `POST /tui/append-prompt` - append prompt.
+- [ ] `POST /tui/open-help` - open help.
+- [ ] `POST /tui/open-sessions` - open sessions.
+- [ ] `POST /tui/open-themes` - open themes.
+- [ ] `POST /tui/open-models` - open models.
+- [ ] `POST /tui/submit-prompt` - submit prompt.
+- [ ] `POST /tui/clear-prompt` - clear prompt.
+- [ ] `POST /tui/execute-command` - execute command.
+- [ ] `POST /tui/show-toast` - show toast.
+- [ ] `POST /tui/publish` - publish TUI event.
+- [ ] `POST /tui/select-session` - select session.
+- [ ] `GET /tui/control/next` - get next TUI request.
+- [ ] `POST /tui/control/response` - submit TUI control response.
+
## Remaining PR Plan
Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays reviewable.
-1. Bridge `PATCH /project/:projectID`.
-2. Bridge MCP add/connect/disconnect routes.
-3. Bridge MCP OAuth routes: start, callback, authenticate, remove.
-4. Bridge experimental console switch and tool list routes.
-5. Bridge experimental global session list.
-6. Bridge sync start/replay/history routes.
-7. Bridge session read routes: list, status, get, children, todo, diff, messages.
-8. Bridge session lifecycle mutation routes: create, delete, update, fork, abort.
-9. Bridge session share/summary/message/part mutation routes.
-10. Replace event SSE with non-Hono Effect HTTP.
-11. Replace pty websocket/control routes with non-Hono Effect HTTP.
-12. Replace tui bridge routes or explicitly isolate them behind a non-Hono compatibility layer.
-13. Switch OpenAPI/SDK generation to Effect routes and compare SDK output.
-14. Flip ported JSON routes default-on, keep a short fallback, then delete replaced Hono route files.
+1. [x] Bridge `PATCH /project/:projectID`.
+2. [x] Bridge MCP add/connect/disconnect routes.
+3. [ ] Bridge MCP OAuth routes: start, callback, authenticate, remove.
+4. [ ] Bridge experimental console switch and tool list routes.
+5. [ ] Bridge experimental global session list.
+6. [ ] Bridge workspace create/remove/session-restore routes.
+7. [ ] Bridge sync start/replay/history routes.
+8. [ ] Bridge session read routes: list, status, get, children, todo, diff, messages.
+9. [ ] Bridge session lifecycle mutation routes: create, delete, update, fork, abort.
+10. [ ] Bridge session share/summary/message/part mutation routes.
+11. [ ] Replace event SSE with non-Hono Effect HTTP.
+12. [ ] Replace pty websocket/control routes with non-Hono Effect HTTP.
+13. [ ] Replace tui bridge routes or explicitly isolate them behind a non-Hono compatibility layer.
+14. [ ] Switch OpenAPI/SDK generation to Effect routes and compare SDK output.
+15. [ ] Flip ported JSON routes default-on, keep a short fallback, then delete replaced Hono route files.
## Checklist
@@ -216,7 +372,7 @@ Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays rev
- [x] Attach auth middleware in route modules.
- [x] Support `auth_token` as a query security scheme.
- [x] Add bridge-level auth and instance tests.
-- [ ] Complete exact Hono route inventory.
+- [x] Complete exact Hono route inventory.
- [x] Resolve implemented-but-unmounted route groups.
- [x] Port remaining top-level JSON reads.
- [ ] Generate SDK/OpenAPI from Effect routes.
diff --git a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts
index 34d4e09e2..81ca68e2c 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts
@@ -1,10 +1,20 @@
import { MCP } from "@/mcp"
+import { ConfigMCP } from "@/config/mcp"
import { Effect, Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, 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)
+
export const McpPaths = {
status: "/mcp",
+ connect: "/mcp/:name/connect",
+ disconnect: "/mcp/:name/disconnect",
} as const
export const McpApi = HttpApi.make("mcp")
@@ -20,6 +30,34 @@ export const McpApi = HttpApi.make("mcp")
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("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({
@@ -45,6 +83,24 @@ export const mcpHandlers = Layer.unwrap(
return yield* mcp.status()
})
- return HttpApiBuilder.group(McpApi, "mcp", (handlers) => handlers.handle("status", 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 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("connect", connect).handle("disconnect", disconnect),
+ )
}),
).pipe(Layer.provide(MCP.defaultLayer))
diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts
index 8d341b8a0..ab8632b5c 100644
--- a/packages/opencode/src/server/routes/instance/index.ts
+++ b/packages/opencode/src/server/routes/instance/index.ts
@@ -79,6 +79,9 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context))
app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context))
app.get(McpPaths.status, (c) => handler(c.req.raw, context))
+ app.post(McpPaths.status, (c) => handler(c.req.raw, context))
+ app.post(McpPaths.connect, (c) => handler(c.req.raw, context))
+ app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context))
}
return app
diff --git a/packages/opencode/test/server/httpapi-mcp.test.ts b/packages/opencode/test/server/httpapi-mcp.test.ts
index 3da1dc933..68144503b 100644
--- a/packages/opencode/test/server/httpapi-mcp.test.ts
+++ b/packages/opencode/test/server/httpapi-mcp.test.ts
@@ -11,12 +11,13 @@ void Log.init({ print: false })
const context = Context.empty() as Context.Context<unknown>
-function request(route: string, directory: string) {
+function request(route: string, directory: string, init?: RequestInit) {
+ const headers = new Headers(init?.headers)
+ headers.set("x-opencode-directory", directory)
return ExperimentalHttpApiServer.webHandler().handler(
new Request(`http://localhost${route}`, {
- headers: {
- "x-opencode-directory": directory,
- },
+ ...init,
+ headers,
}),
context,
)
@@ -45,4 +46,41 @@ describe("mcp HttpApi", () => {
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ demo: { status: "disabled" } })
})
+
+ test("serves add, connect, and disconnect endpoints", async () => {
+ await using tmp = await tmpdir({
+ config: {
+ mcp: {
+ demo: {
+ type: "local",
+ command: ["echo", "demo"],
+ enabled: false,
+ },
+ },
+ },
+ })
+
+ const added = await request(McpPaths.status, tmp.path, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ name: "added",
+ config: {
+ type: "local",
+ command: ["echo", "added"],
+ enabled: false,
+ },
+ }),
+ })
+ expect(added.status).toBe(200)
+ expect(await added.json()).toMatchObject({ added: { status: "disabled" } })
+
+ const connected = await request("/mcp/demo/connect", tmp.path, { method: "POST" })
+ expect(connected.status).toBe(200)
+ expect(await connected.json()).toBe(true)
+
+ const disconnected = await request("/mcp/demo/disconnect", tmp.path, { method: "POST" })
+ expect(disconnected.status).toBe(200)
+ expect(await disconnected.json()).toBe(true)
+ })
})