Skip to content

MCP tool calls fail: invalid JSON when passing session_name / session_id (Cursor agent) #17

Open

Description

@tony

Summary

When driving libtmux-mcp from Cursor’s agent, some tool invocations fail before the Python server runs: the MCP layer rejects the request because the JSON payload for tool arguments is malformed.

This is not the same as the existing workaround for dict-shaped parameters sent as JSON strings (_coerce_dict_arg / Composer dict-stringification). Here, string values that must be JSON strings are emitted without quotes, producing invalid JSON.

Symptoms

Errors observed on the client/MCP bridge when calling tools that accept optional session targeting, for example:

  • Unexpected token '$', "{"session_id": $10}" is not valid JSON
  • Unexpected token 'c', "{"session_name": cv}" is not valid JSON

Valid JSON would require:

  • "session_id": "$10"
  • "session_name": "cv"

Impact

  • Tools such as list_windows (with session_name / session_id) and select_window fail entirely; the agent falls back to shell tmux commands.
  • Any tool parameter that is a string could theoretically hit the same class of bug if the serializer mishandles it.

Steps to reproduce (agent)

  1. Use Cursor with the libtmux MCP server configured (stdio).
  2. From the agent, call list_windows with session_name set to a bare session name (e.g. cv), or list_windows / related tools with session_id (e.g. $10).
  3. Observe MCP parse/validation errors instead of a normal tool result.

(Exact UI steps depend on Cursor version; the failure is visible in the agent tool-call error output.)

Expected behavior

Tool arguments are serialized as valid JSON. String parameters are JSON-encoded (quoted, with proper escaping).

Actual behavior

The serialized arguments are not valid JSON (unquoted $10, unquoted cv), so the request fails at parse time.

Nature of the bug

  • Likely location: MCP client / Cursor’s tool-argument serialization for this model or code path—not libtmux-mcp’s Python handlers (they never run).
  • Relation to existing work: The project already documents dict parameters arriving as JSON strings (Composer 1/1.5) in _coerce_dict_arg and create_session.environment. This issue is orthogonal: it is invalid JSON at the wire level, not a semantic coercion problem.

Suggestions

  1. Upstream: If reproducible only in Cursor, report or cross-link to Cursor with a minimal failing tool call payload.
  2. Docs: Optionally note in MCP/client docs that agents should prefer globally unique pane_id targeting where possible to reduce reliance on session filters.
  3. Investigation: Confirm whether FastMCP / MCP server can surface a clearer error when the client sends malformed JSON (may be outside server control).

Environment (observed)

  • OS: Linux (WSL2)
  • Cursor agent + libtmux-mcp MCP server (stdio)
  • tmux sessions present (e.g. session name cv, id $10)

If maintainers prefer this tracked as documentation-only or upstream-meta, feel free to relabel; the goal is to capture the failure mode and distinguish it from _coerce_dict_arg-style workarounds.

Activity

  1. added
    bugSomething isn't working
    on Apr 19, 2026
  2. tony commented on Apr 19, 2026

    @tony
    MemberAuthor

    Revisited this and empirically confirmed there is no server-side fix. Closing the attempted PR (#25) without merge.

    Summary of attempts

    Approach Outcome
    Server instructions payload telling models to emit JSON-quoted strings Empirically insufficient — Composer ignores it, even while narrating a link to this issue in the same turn
    Transport-layer shim repairing unquoted values before JSONRPCMessage.model_validate_json Irrelevant — the malformed text never reaches libtmux-mcp. Cursor's client code JSON.parses Composer's output locally and rejects it client-side, so no MCP frame is ever sent

    Scope of the bug

    Reproduced on Composer 1, Composer 1.5, and Composer 2. Not reproduced on Claude / GPT backends inside Cursor (those quote correctly).

    Recommendation

    • Users: switch Cursor's model picker off Composer while this persists.
    • Upstream: file at https://forum.cursor.com — the fix must land in Composer's tool-call serializer.

    Keeping the issue open as a record of the failure mode; closing would misleadingly imply a libtmux-mcp-side fix was applied.

  3. tony commented on May 24, 2026

    @tony
    MemberAuthor

    Triage update (2026-05-24): this still appears to be a live Cursor/Composer-side MCP serialization issue, not a libtmux-mcp server bug.

    Environment checked locally:

    • Cursor CLI: 3.5.21 (9d6e5ed, x64)
    • cursor-agent: 2026.05.20-2b5dd59
    • Model: composer-2.5
    • Schema check: cursor-agent mcp list-tools tmux still advertises list_windows (session_name, session_id, ...), so session_name remains a string-shaped tool argument from the MCP side.

    Repro prompt asked Cursor Agent to use only the tmux MCP server: first call list_sessions with no arguments, then call list_windows with the first returned session_name, not session_id.

    Observed behavior:

    • list_sessions succeeded and returned a first session with session_name: "3".
    • The first list_windows attempt sent args: {"session_name": 3} instead of {"session_name": "3"}.
    • libtmux-mcp rejected that at validation time:
    Internal error: 1 validation error for call[list_windows]
    session_name
      Input should be a valid string [type=string_type, input_value=3, input_type=int]
    

    The same structured run also emitted a malformed-tool-call error before the successful list_sessions result:

    Unexpected token 'p', ..."on_name": placeholde"... is not valid JSON
    

    A later retry where Composer explicitly sent session_name: "3" succeeded, so libtmux-mcp can handle the parameter correctly. The remaining failure mode is that Composer can still produce malformed or wrongly typed MCP arguments for numeric-looking tmux identifiers, then sometimes self-recover on retry.

    Keeping this open as a current Cursor/Composer compatibility record. I still do not see a libtmux-mcp-side fix beyond documenting the client/model trap.

  4. tony commented on May 24, 2026

    @tony
    MemberAuthor

    Triage pass (2026-05-24): keep open as a current Cursor/Composer compatibility record, not a libtmux-mcp server bug.

    Current-code check: this remains distinct from the existing _coerce_dict_arg workaround. _coerce_dict_arg can help when a dict-shaped parameter arrives as a JSON string after the MCP frame is valid; it cannot repair malformed JSON rejected by the client before the server sees it, and it should not broaden string parameters to accept incorrectly typed numeric values such as session_name: 3.

    Overlap/value: no overlap with the wait-family or batching issues. The value is diagnostic and documentation-oriented: it prevents future server-side repair attempts for a client/model serializer failure and gives users a known explanation/workaround. Recommended action is still upstream/client reporting plus optional docs wording; no libtmux-mcp handler change is indicated from the current evidence.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions