anomalyco / anomalyco/opencode

[DecimalError] OpenCode mangles number-typed MCP tool parameters (model emits a number; OpenCode forwards a string)

Open
#39,334 2 comments 0 reactions 1 assignee View on GitHub

@rekram1-node is already working on this.

Since Jul 28, 2026.

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

Description

Description

TL;DR

OpenCode mangles a primitive number emitted by the model into a string (or runs it through decimal.js) before forwarding it to an MCP server, then every conformant MCP server rejects it with a type-mismatch error. This is not the model emitting a string — the model emits id: 3, and OpenCode is the one that converts it. The same tool calls succeed in Cursor and Claude Desktop.

This is a recurrence / generalization of the DecimalError family (#6010, #6161), which were closed as "fixed by upgrading" but are still happening on 0.15.23.

Why this is not a duplicate of the closed "not a bug" issues

The most similar prior reports were closed with the rationale that the model was emitting strings:

  • #3593 — closed: "this isn't a bug, it is just a tool call validation error... the model failing to call a tool"
  • #7512 — closed as NOT_PLANNED: "LLMs sometimes serialize typed parameters as strings"
  • #26870 — auto-closed stale

Our case is different and verifiable at the wire level: the model emits a number, and OpenCode turns it into a string before it reaches the server. See the wire-level proof below — feeding the same model output as a raw JSON number vs. a string to any conformant MCP server shows only the number form is accepted, yet OpenCode sends the string form.

Error observed

echo_id %!s(float64=3) Error: [DecimalError] Invalid argument: [object Object]

The %!s(float64=3) fragment is Go fmt verb-mismatch output (a %s applied to a float64) embedded into the error message text — i.e. the value was a number (float64) at some point in OpenCode's pipeline. The [DecimalError] Invalid argument: [object Object] is a decimal.js error thrown inside OpenCode's JS/TS client when new Decimal(...) is handed a boxed/object value instead of a primitive.

Expected vs actual

  • Expected: tools/call carries {"name":"echo_id","arguments":{"id":3}} over stdio, with id as a JSON number primitive. Server receives typeof arg === "number".
  • Actual: OpenCode either (a) runs the number through Decimal and aborts with [DecimalError] Invalid argument: [object Object], or (b) serializes it as a JSON string ({"id":"3"}), which MCP servers reject with a type mismatch, e.g. cannot unmarshal string into Go struct field .id of type int64.

Wire-level proof that OpenCode (not the model, not the server) is at fault

The bug is entirely in OpenCode's argument-serialization layer. Feeding the two candidate payloads directly to any conformant MCP server over stdio shows that only the number form is accepted — so when OpenCode sends the string form, OpenCode is the source of the breakage:

# number form — what the MODEL emits and what Cursor/Claude Desktop forward:
{"name":"echo_id","arguments":{"id":3}}   # => server: ok, typeof id === "number"

# string form — what OpenCode ends up sending:
{"name":"echo_id","arguments":{"id":"3"}}  # => server: error, cannot unmarshal string into numeric field

The model is not in this loop; the divergence happens inside OpenCode between receiving the model's tool call and writing to the MCP stdio transport.

Plugins

Any local stdio MCP server exposing a tool with a "type": "number" parameter (see Steps to reproduce for a minimal 20-line example).

OpenCode version

0.15.23

Steps to reproduce
  1. Save this minimal MCP server (Node.js, stdio, NDJSON) as server.js. It asserts on the server side that id arrives as a JSON number, not a string:
const readline = require("readline");
const rl = readline.createInterface({ input: process.stdin });
const send = (o) => process.stdout.write(JSON.stringify(o) + "\n");
rl.on("line", (line) => {
  const msg = JSON.parse(line);
  if (msg.method === "initialize") {
    send({ jsonrpc: "2.0", id: msg.id, result: {
      protocolVersion: "2024-11-05",
      capabilities: { tools: {} },
      serverInfo: { name: "repro", version: "1.0.0" },
    }});
  } else if (msg.method === "tools/list") {
    send({ jsonrpc: "2.0", id: msg.id, result: { tools: [{
      name: "echo_id",
      description: "Echoes back the numeric id it receives",
      inputSchema: {
        type: "object",
        properties: { id: { type: "number", description: "A numeric id" } },
        required: ["id"],
      },
    }]});
  } else if (msg.method === "tools/call") {
    const arg = msg.params.arguments.id;
    const ok = typeof arg === "number";
    send({ jsonrpc: "2.0", id: msg.id, result: { content: [
      { type: "text", text: `received id=${arg} (typeof=${typeof arg}) ok=${ok}` },
    ]}});
  }
});
  1. Register it in opencode.json:
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "repro": { "type": "local", "command": ["node", "server.js"], "enabled": true }
  }
}
  1. Start OpenCode and ask the agent to call the tool with a number, e.g. "call echo_id with id 3".

  2. Observe the failure: echo_id %!s(float64=3) Error: [DecimalError] Invalid argument: [object Object]. If you instead hit the string-coercion path, the server's response will read typeof=string ok=false.

  3. (Control) Run the same echo_id tool from Cursor or Claude Desktop with the same model — it succeeds, confirming the model emits a number and only OpenCode mangles it.

Screenshot and/or share link

n/a — the wire-level proof in Description and the minimal server in Steps to reproduce fully demonstrate the bug without a session.

Operating System

Linux 6.1.0-51-amd64 (the wire-level behavior is OS-independent)

Terminal

xterm-256color

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.