anomalyco / anomalyco/opencode

MCP tools missing from sessions created via POST /api/session on `opencode serve` (present in the TUI)

Open
#38,470 4 comments 2 reactions 1 assignee View on GitHub

@kitlangton is already working on this.

Since Jul 23, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

MCP server tools are missing from sessions created via POST /api/session on opencode serve (present in the TUI)

Summary

On opencode serve (headless), a configured local (stdio) MCP server connects fine (GET /mcp shows status: "connected"), but sessions created and prompted through the HTTP API (POST /api/session + POST /api/session/{id}/prompt) receive none of that server's tools — the model reports only the built-ins and cannot call any MCP tool.

The exact same config, run as the interactive TUI, works: the model calls the MCP tool successfully. So the MCP server, its config, and its tools are all fine — the tools just don't reach sessions driven over the HTTP API.

This makes opencode serve unusable for programmatic/agent use cases that depend on MCP tools (drive opencode as a headless HTTP service).

Environment

  • opencode 1.18.3 (also reproduced reading 1.18.4 source)
  • macOS (arm64); model: Anthropic claude-sonnet-5

Minimal reproduction

1. A trivial stdio MCP servermcp-ping.js (one tool, ping_test, returns pong):

let buf = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (d) => {
  buf += d; let i;
  while ((i = buf.indexOf('\n')) >= 0) {
    const line = buf.slice(0, i); buf = buf.slice(i + 1);
    if (!line.trim()) continue;
    let m; try { m = JSON.parse(line); } catch { continue; } handle(m);
  }
});
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
function handle(m) {
  if (m.method === 'initialize')
    send({ jsonrpc: '2.0', id: m.id, result: { protocolVersion: (m.params && m.params.protocolVersion) || '2025-06-18', capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'ping-test', version: '1.0.0' } } });
  else if (m.method === 'tools/list')
    send({ jsonrpc: '2.0', id: m.id, result: { tools: [{ name: 'ping_test', description: 'Diagnostic ping tool', inputSchema: { type: 'object', properties: {}, additionalProperties: false } }] } });
  else if (m.method === 'tools/call')
    send({ jsonrpc: '2.0', id: m.id, result: { content: [{ type: 'text', text: 'pong' }] } });
  else if (m.id !== undefined && m.id !== null)
    send({ jsonrpc: '2.0', id: m.id, result: {} });
}

2. opencode.json in the same dir (add your Anthropic key):

{
  "$schema": "https://opencode.ai/config.json",
  "model": "anthropic/claude-sonnet-5",
  "provider": { "anthropic": { "options": { "apiKey": "sk-ant-..." } } },
  "mcp": { "pingtest": { "type": "local", "command": ["node", "./mcp-ping.js"], "enabled": true } }
}

3a. Works — TUI:

opencode
> call the ping_test tool and show me exactly what it returns

Result: the model calls pingtest_ping_test and reports pong. ✅

3b. Fails — headless serve via the HTTP API:

OPENCODE_SERVER_PASSWORD=pw opencode serve --hostname 127.0.0.1 --port 39901 &

curl -s -u opencode:pw http://127.0.0.1:39901/mcp
# -> {"pingtest":{"status":"connected"}}

SID=$(curl -s -u opencode:pw -X POST http://127.0.0.1:39901/api/session \
  -H 'content-type: application/json' -d '{}' | jq -r .data.id)

curl -s -u opencode:pw -X POST http://127.0.0.1:39901/api/session/$SID/prompt \
  -H 'content-type: application/json' \
  -d '{"prompt":{"text":"Call ping_test with no arguments and report its output verbatim."},"model":{"providerID":"anthropic","id":"claude-sonnet-5"},"delivery":"queue"}'

# wait, then read the reply:
curl -s -u opencode:pw http://127.0.0.1:39901/api/session/$SID/message

Result: the assistant replies "I don't have access to a tool called ping_test. The tools available to me are: apply_patch, bash, edit, glob, grep, question, skill, todowrite, webfetch, websearch, write."

Also tried: passing x-opencode-directory: <project dir> on both the create and prompt requests, and ?directory=<project dir> — no change (the serve cwd already is the project dir, so it resolves to the same directory anyway).

Expected

A session created via POST /api/session on a server whose MCP server is connected should expose that server's tools to the model — same as the TUI.

Suspected cause (from reading 1.18.4 source)

  • serve is instance: false and resolves a per-request instance (server/routes/instance/httpapi/middleware/instance-context.tsInstanceStore.load({ directory })), with per-request instance disposal (.../lifecycle.ts). The TUI holds one long-lived instance.
  • Tools reach the model via SessionTools.resolve iterating MCP.tools() (session/tools.ts), which yields tools only for servers that are connected and have populated defs.
  • mcp/index.ts create(): const listed = getServerCapabilities()?.tools ? McpCatalog.defs(client, timeout) : []; if (!listed) fail. Because an empty array is truthy, a server that ends up with an empty tools/list is cached as connected with defs: [] (no failure) — and MCP.tools() then contributes zero tools. Net observable state: GET /mcp says connected, model gets nothing.

So it looks like the per-request instance used for an API session ends up with the MCP server connected but with empty/unpopulated defs, whereas the persistent TUI instance has them fully loaded.

(Note: GET /experimental/tool/ids returns built-ins/plugins only by design and never lists MCP tools, so it can't be used to diagnose this — use an actual model turn.)

Impact

opencode serve can't be used as a headless MCP-enabled agent runtime — any integration that adds tools via MCP is silently unavailable to API-driven sessions, with no error surfaced.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.