Skip to content

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

Closed as not planned

Description

@brad07

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 server — mcp-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.ts → InstanceStore.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.

Activity

  1. github-actions commented on Jul 23, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or closely related to existing issues. Please check:

    If your issue is specifically about the HTTP API / opencode serve path rather than subagent sessions, it may be a distinct (but related) code path and worth keeping open separately.

  2. brad07 commented on Jul 23, 2026

    @brad07
    Author

    Additional detail from a --log-level DEBUG run (ruling out the common 'add enabled:true / OAuth' advice):

    • Config already has "enabled": true; the server logs no config error, no OAuth URL, and no process-startup failure. The MCP server connects (GET /mcp -> connected).
    • For an API-driven session, the DEBUG log shows a per-request creating instance for the project directory, cleanly bootstrapping (loading config -> watcher -> booting location services), with MCP initialized lazily afterward - but the resulting turn still exposes zero MCP tools to the model.
    • By contrast, a TUI run against the identical config logs an evaluated permission=pingtest_ping_test ... action=allow line (the model actually invoking the tool). The API run logs no tool at all.

    So this isn't a config/enabled/OAuth issue - it's specifically that the per-request instance used for an API session doesn't surface the connected MCP server's tools, whereas the persistent TUI instance does.

  3. brad07 commented on Jul 23, 2026

    @brad07
    Author

    Update: opencode run does NOT have this problem - it exposes the MCP tools correctly. opencode run --attach http://127.0.0.1:PORT --dir <project> "call the mcp tool" drives a turn against the SAME serve that fails via raw POST /api/session, and there the model DOES get and call the MCP tool. So the gap is specifically the server's /api/session create path not loading MCP the way run (and the TUI) do - likely run performs additional instance setup that the bare session-create over HTTP skips.

  4. herjarsa commented on Aug 11, 2026

    @herjarsa

    PR with fix: #41728

    Root cause: the serve command had \instance: false, so the lazy Plugin service (InstanceState) is never materialised in HTTP serve mode — \Plugin.init()\ is only reached via the per-process InstanceBootstrap that \instance: true\ enables. The same plugin bundle works in TUI and \opencode run\ (logs \ actory_invoked), and shows only the module-scope import under \opencode serve.

    The PR flips serve to \instance: true\ with \directory: () => process.cwd(); per-request \x-opencode-directory\ entries are unaffected (different cache keys). Reproducer + source evidence in PROPOSAL-server-plugin-init.md.

  5. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions