Skip to content

Port to the OpenCode 2.x server API (4.0.0) - #35

Open
norandom wants to merge 5 commits into
AlaeddineMessadi:mainfrom
norandom:opencode-2x-api-port
Open

norandom wants to merge 5 commits into
AlaeddineMessadi:mainfrom
norandom:opencode-2x-api-port

Conversation

@norandom

Copy link
Copy Markdown

Summary

Ports the MCP server from the OpenCode 1.x HTTP API to the 2.x /api/* surface and bumps to 4.0.0. Tested against opencode 2.1.2.

  • Client: uses @opencode-ai/sdk/v2/client (^1.19.3), unwraps {data, location, cursor} envelopes, derives SSE event names from the JSON type on the single /api/event stream.
  • Server management: probes GET /api/info with Basic auth, rejects 1.x servers with an upgrade message, reports 401s explicitly, and for auto-serve spawns opencode serve with a generated private password.
  • Tools/jobs: new session, prompt-inbox, forms, interrupt and model routes; completion is read from the idle marker that closes the turn.
  • Removed (no 2.x equivalent), 87 → 67 tools: all opencode_tui_*, session_share/unshare/todo/init, lsp_status, formatter_status, log, tool_ids/list, find_symbol, instance_dispose. Prompt-level structured output (format) is gone.
  • Fixes found while porting: idle marker of the current turn in opencode_message_send; finished older turns still reported as completed; form option labels mapped to values; OAuth authorize defaults methodID; startup line split across stdout reads; text always sent for slash commands; job-store scope no longer includes the generated auto-serve password; empty OPENCODE_SERVER_USERNAME falls back to opencode.
  • Tests, docs, generated docs/tools.md, 4.0.0 changelog and the smoke script are updated.

Test plan

  • npm test: 585 passed, 2 skipped (17 files)
  • npm run docs:check
  • node scripts/mcp-smoke-test.mjs against a live opencode 2.1.2 server (no model calls)
  • Live checks against 2.1.2: session listing, /api/event stream, 401 handling, auto-serve startup
  • Not run: --inference smoke test and real prompts through the job path
  • Not tested: other 2.x versions (e.g. 2.0.x). The 2.1.2 spec calls the API experimental and some tools use /api/experimental/* routes.

Notes for reviewers

  • 2.1.2 only accepted the username opencode, so a custom OPENCODE_SERVER_USERNAME will get 401s.
  • The changelog says to stay on 3.x for OpenCode 1.x; that is inferred from the code, not tested.
  • A prompt still queued in the session inbox reads as unknown when observed, and cancelling it does not remove the queued item (reported during test porting, not verified live).

🤖 Generated with Claude Code

Move the MCP server from the OpenCode 1.x HTTP API to the 2.x `/api/*`
surface (tested against opencode 2.1.2).

- client: use @opencode-ai/sdk/v2/client, unwrap {data, location, cursor}
  envelopes, derive SSE event names from the JSON `type`
- server-manager: probe GET /api/info with Basic auth, detect 1.x servers
  and 401s, spawn `opencode serve` with a generated password for auto-serve
- tools/jobs: new session, prompt-inbox, forms, interrupt and model routes;
  completion is read from the idle marker closing the turn
- remove tools without a 2.x equivalent (TUI, share/unshare, todo, init,
  lsp/formatter/log, tool_*, find_symbol, instance_dispose): 87 -> 67 tools
- drop prompt-level structured output (`format`)
- fix: idle marker of the current turn, form option values, OAuth default
  methodID, split startup lines, required command `text`, stable job scope
- port tests and docs to v2; add 4.0.0 changelog; fix smoke script

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@AlaeddineMessadi AlaeddineMessadi left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @norandom! This is the direction the project needs, and it fixes #31. I'd like it to land first as the base for 4.0.0, with my #29 following on top.

Tests, docs check and a smoke run against OpenCode 2.2.0 all pass. A few things to fix before merging:

  1. agent is dropped: 2.x prompts don't accept it, so use POST /api/session/{id}/agent instead (jobs.ts:392, workflow.ts:81, message.ts:124,203).
  2. A job never finishes once its turn passes 100 messages. Page through with cursor (jobs.ts:148, workflow.ts:101).
  3. Send delivery: "queue". The default steer merges a second prompt into the running turn.
  4. Cut the turn at the idle marker, not at the next user message (jobs.ts:179).
  5. opencode_ask/opencode_reply return partial answers on a /wait timeout and swallow errors (workflow.ts:86-97).
  6. Read pending inputs from /api/session/{id}/form and /permission, not the location-scoped lists (jobs.ts:123).

Optional, and I'm happy to take these as follow-ups: directory scoping for session lists and events_poll, form answer types, the Windows spawn (shell on win32), and recording the server process right after spawn().

I'm resolving the conflicts with main on your branch now with a merge commit, so no force-push.

Brings in 3.1.0 (OpenChamber sign-in, OPENCODE_SERVER_TOKEN, sign-in
cooldown, dependency floors) on top of the OpenCode 2.x port.

Auth integration:
- The v2 SDK client, the /api/event stream and the /api/info probe all
  go through AuthProvider (OpenChamber > bearer token > Basic).
- isServerRunning/ensureServer take an explicit Authorization header.
- Auto-serve: startup and reconnect call adoptServer(url, password),
  which switches to the spawned server's generated Basic password.
- Job-store scope hashes the configured credentials (plus token and
  OpenChamber inputs), never the generated password.
- OpenChamber tests now model OpenChamber 2.x passthrough at the origin.
- startServer takes the username, pins OPENCODE_SERVER_USERNAME in the
  child env and probes /api/info with it, so a custom username no longer
  fails the health check (ensureServer passes opts.username).
- Move the startup ensureServer + adoptServer step into
  OpenCodeClient.connect() so it is covered by tests; index.ts calls it
  and keeps the configured URL in the job scope for existing servers.
- Tests: username routing in the fake opencode, connect() auth/adoption.
@AlaeddineMessadi

Copy link
Copy Markdown
Owner

Resolved the conflicts with main (3.1.0) in a merge commit on your branch, no force-push. Pull before you push again.

611 tests pass. The review items above are still yours, and I'm happy to help with any of them.

…rns, wait timeouts, per-session inputs

- prompts: select the agent at session creation or via POST /api/session/{id}/agent
  (2.x prompts silently drop an agent field) and send delivery:"queue" so a prompt
  dispatched while a turn runs executes as its own turn instead of steering it
- observation: page session messages with the cursor envelope (getWithCursor), so a
  job still finishes when its turn spans more than one page; cut the observed turn
  at its idle marker instead of the next user message, so a steered follow-up no
  longer truncates it and the merged turn's marker completes the original job
- opencode_ask/opencode_reply: a wait-endpoint deadline expiry is now reported as a
  flagged timedOut partial answer (TIMEOUT note plus resumable session) and wait
  failures surface instead of being swallowed as partial state
- pending inputs: read GET /api/session/{id}/form and /api/session/{id}/permission
  instead of the location-scoped lists, which miss sessions in other directories
- request shapes verified against a live opencode 2.2.5 server (its /openapi.json
  and the model-free smoke run); tests updated and extended, 618 passing
@norandom

norandom commented Nov 5, 2026

Copy link
Copy Markdown
Author

All six review items are addressed in 6bb8c73 (pushed on top of your merge — no force-push). While implementing them I stood up a local opencode 2.2.5 server and verified every request shape against its live endpoints and /openapi.json before coding; the model-free smoke run passes against it (23/23 checks).

  1. Agent: new sessions take agent at creation; existing sessions switch via POST /api/session/{id}/agent before the prompt. No prompt body carries an agent any more.
  2. Pagination: OpenCodeClient.getWithCursor() now exposes the {data, cursor} envelope, and a shared readSessionMessages() pages newest→older via cursor.next until the page containing the target message (or the newest user turn, when no message ID is known) — jobs finish past 100 messages. Used by observeSession, cancel, ask/reply and message_send.
  3. All prompt submissions now send delivery: "queue".
  4. Turns are cut at the first idle marker after the target message, not at the next user message. A steered follow-up no longer truncates the turn, and the merged turn's idle marker completes the original job (there's a jobs.ts test documenting exactly that history).
  5. promptAndWait only treats the wait's own deadline as a timeout now: the result carries timedOut, opencode_ask/opencode_reply return a TIMEOUT-marked partial answer with structuredContent.timedOut and the session to resume, and every other wait failure (401/404/503/network) is raised instead of swallowed.
  6. Pending inputs are read from GET /api/session/{id}/form and GET /api/session/{id}/permission (plus a defensive sessionID match), replacing the location-scoped lists.

Two notes from the live-server digging, no code change needed:

  • The @opencode-ai/sdk 1.19.x v2 types are stale relative to 2.2.5: the server has no /question routes (they're /form), no /api/session/{id}/wait (it's /api/experimental/session/{id}/wait), the prompt body is flat {id?, text, delivery?, resume?} rather than a nested prompt.text, and the permission reply field is decision, not reply. The branch was already correct on all of these, so I left them as they were.
  • Two small Windows-only flakes I deliberately did not touch (out of scope): scripts/mcp-smoke-test.mjs silently exits 0 without running when the argv[1]/import.meta.url guard mismatches (e.g. a junctioned cwd), and its fixture cleanup can EBUSY-race the child shutdown. Happy to fix as a follow-up if useful.

Tests: 618 passing (up from 611). Fixtures moved to the per-session input routes, cursor envelopes and delivery bodies; new coverage for the steer-cut turn boundary, >100-message pagination, the agent switch route on existing sessions, and the ask timeout/error paths. npm run docs:check passes and CHANGELOG has an Unreleased section listing the fixes.

The optional items (directory scoping for session lists and events_poll, form answer types, the Windows spawn shell, recording the server pid right after spawn()) remain open as you suggested — thanks for offering to take them.

…nup crash

- The direct-run guard compared import.meta.url against pathToFileURL(resolve(argv[1]));
  on Windows the two spellings diverge through drive junctions/substituted drives
  (ESM resolves module URLs to the real path), so the script exited 0 without running
  any checks. Compare real filesystem paths instead, case-insensitively on win32.
  Verified: `node scripts/mcp-smoke-test.mjs` now runs end-to-end against a live
  opencode 2.2.5 server (23/23 model-free checks).
- Disposable-fixture cleanup crashed a passing run with an unhandled EBUSY. Root cause
  on Windows: the OpenCode server keeps a project-watcher handle on every directory
  the run touched, which blocks recursive deletion until that process exits (POSIX
  unlink is unaffected). Deletion now retries through transient handle lag
  (fs.rm maxRetries), and a persistent holder produces a warning naming the path
  instead of a crash; the exit code stays tied to the smoke results. Verified the
  fixture deletes cleanly the moment the server exits.
- Tests: fixture lifecycle coverage in smoke-runner.test.ts; suite 619 passing.
@norandom

norandom commented Nov 5, 2026

Copy link
Copy Markdown
Author

Follow-up in f1741c4: the two Windows smoke-script flakes I flagged earlier are fixed after all.

  • Silent no-op: the direct-run guard compared import.meta.url with pathToFileURL(resolve(argv[1])), which diverges on Windows when the entry is reached through a drive junction or differently-spelled path (ESM resolves module URLs to the real path) — the script then exited 0 without running a single check, which reads as a green smoke run. It now compares real filesystem paths (case-insensitive on win32). Verified: node scripts/mcp-smoke-test.mjs runs end-to-end against the local 2.2.5 server, 23/23 model-free checks pass.
  • EBUSY cleanup crash: root cause is that the OpenCode server keeps a project-watcher handle on every directory the run touches; on Windows an open handle blocks recursive deletion until that process exits (POSIX unlink is unaffected — why it never showed on the CI matrix). I confirmed the fixture deletes cleanly the moment the server exits. Cleanup now retries through transient handle lag via fs.rm retries, and a persistent holder (a long-lived shared server) produces a warning naming the path instead of an unhandled crash; the exit code stays tied to the smoke results.

Suite is at 619 passing (added fixture-lifecycle coverage); docs:check still green.

@norandom

norandom commented Nov 5, 2026

Copy link
Copy Markdown
Author

I think this should help, let me know if it's not sufficient please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants