Skip to main content

symm-mcp

Symm is a lightweight asynchronous MCP channel for handing work between independent agents and re-entering it with fresh attention.

The work persists. The viewpoint changes.

Any MCP client can dispatch a task to another agent, keep working, and later observe the result and record a resolution. Symm does not decide who implements and who reviews; that is expressed in the prompt.

Symm sits between two protocols: toward its caller it is an MCP server, and toward the agents it launches it is an Agent Client Protocol (ACP) client.

flowchart LR
    D["<b>Dispatch</b><br/>spawn_task"] --> O["<b>Observe</b><br/>get_events · get_task"]
    O --> R["<b>Resolve</b><br/>resolve_task"]
    R --> P(("<b>Repeat</b><br/>next task,<br/>new viewpoint"))
    P --> D

Status

0.1.0, the first release (changelog), published on PyPI as symm-mcp. It is not listed in the MCP Registry.

How a Task Flows

spawn_task returns as soon as the agent process is running. The caller is free to do other work and comes back whenever it wants, reading only the events it has not seen yet.

sequenceDiagram
    autonumber
    participant C as Caller (any MCP client)
    participant S as Symm server
    participant A as Agent process
    C->>S: spawn_task(agent, prompt, workspace)
    S->>A: launch in its own process group, prompt over ACP
    S-->>C: task_id, status = running
    Note over C: works on something else
    A-->>S: session updates, permission requests
    C->>S: get_events(task_id, after_seq = 0)
    S-->>C: events 1..N, cursor = N
    A-->>S: more updates, then the turn ends
    C->>S: get_events(task_id, after_seq = N)
    S-->>C: only the new events, status = succeeded
    C->>S: resolve_task(task_id, needs_followup, note)
    S-->>C: task with resolution recorded
    Note over C: dispatch the next task, possibly with roles swapped

Two Independent Axes

Execution status says what happened to the process. Resolution says what a caller concluded about the result. They never change each other: succeeded does not mean accepted, and a resolution can be revised later.

stateDiagram-v2
    direction LR
    state "Execution status (owned by Symm)" as status {
        [*] --> created
        created --> starting
        starting --> running
        starting --> failed: could not start
        running --> succeeded: exit 0
        running --> failed: exit ≠ 0
        created --> cancelled
        starting --> cancelled
        running --> cancelled: cancel_task / shutdown
    }
    state "Resolution (recorded by callers, finished tasks only)" as resolution {
        [*] --> unresolved
        unresolved --> judged: resolve_task
        judged --> judged: resolve_task again
        judged: accepted · rejected · needs_followup · superseded
    }

Symmetric by Design

There is no built-in implementer or reviewer role. The same five tools cover every direction work can travel:

flowchart TB
    subgraph one ["Implement, then verify yourself"]
        direction LR
        U1["You"] -- "implement X" --> W1["Agent"] -. "result" .-> U1
    end
    subgraph two ["Implement yourself, ask for review"]
        direction LR
        U2["You"] -- "review this workspace" --> W2["Agent"] -. "findings" .-> U2
    end
    subgraph three ["Chain or fan out"]
        direction LR
        A3["Agent A"] -- "implement" --> B3["Agent B"] -- "find flaws" --> C3["Agent C"]
        A3 -- "review 1" --> R1["Reviewer"]
        A3 -- "review 2" --> R2["Reviewer"]
    end
    one ~~~ two ~~~ three

initiator_id and executor_id are descriptive labels only. They never grant ownership or authority.

Tools

Tool What it does
spawn_task Dispatch a prompt to a registered agent. Returns as soon as the agent process is running.
get_task Current state: execution status, exit_code, resolution, timestamps.
get_events Events after after_seq plus a cursor to pass next time, and the current status.
resolve_task Record accepted, rejected, needs_followup, or superseded for a finished task. No side effects; may be revised.
cancel_task Terminate a running agent's whole process group. A finished task is returned unchanged.

Registered agents. Symm is the ACP client; six of the executables below are ACP agents it speaks to over their stdin and stdout. claude-agent-acp and codex-acp are third-party ACP agents that run Claude Code and Codex. Antigravity has no ACP mode and runs as a CLI. Each was verified by running a real task through symm-mcp.

Agent Executable Symm launches options
claude_code claude-agent-acp (ACP adapter for Claude Code) allow, model, effort
codex codex-acp (ACP adapter for Codex) allow, model, effort
copilot copilot --acp allow
cline cline --acp allow, model
cursor cursor-agent acp allow, model
opencode opencode acp allow, model
antigravity agy --print=<prompt> model, mode (accept-edits, plan), sandbox, dangerously_skip_permissions, print_timeout

Permissions

Symm never widens an agent's permissions on its own.

  • ACP agents ask Symm, and Symm answers. allow lists the ACP tool kinds you approve for the task: read, edit, delete, move, search, execute, think, fetch, other. Every other permission request is rejected. Symm only ever grants or rejects once; it never chooses an "always" option, which the agent would store in your own configuration. Each decision is recorded as a permission event.
  • Permission settings are pinned. Before the prompt, Symm sets each agent's permission settings to a value under which it asks: Claude Code mode=default, Codex mode=read-only, Copilot allow_all=off, Cline auto_approve=false, opencode configured to ask before edits, shell commands, and web fetches. For agents that advertise these settings over ACP, a setting Symm must pin but cannot find fails the task before the prompt rather than running with unknown permissions; opencode's ask configuration is passed through its environment and cannot be checked that way. Callers cannot set these settings, and Symm never approves a request to switch an agent's mode.
  • Cursor creates and edits files without asking in its agent mode, so Symm selects that mode only when allow contains edit, and otherwise keeps Cursor in read-only plan. Cursor labels file deletions as edit, so allowing edit also approves them; and without edit, an execute grant cannot be used, because plan runs no command.
  • Antigravity has no ACP mode, so Symm cannot pin its settings; it adds no permission flag, and Antigravity's own headless default applies (unapproved actions are denied). Grant access with {"mode": "accept-edits"} or dangerously_skip_permissions. Its prompt is one --print=<prompt> argument, limited to 100,000 bytes, and it stops after 5 minutes unless you set print_timeout.
  • model and effort must be values the agent offers; otherwise the task fails before the prompt with an error event naming them. Boolean options accept only JSON true or false.

Events

For ACP agents the ledger holds structured events: session_started (the agent's session id, also stored as the task's session_id and accepted by spawn_task to resume), agent_message and agent_thought (text, streamed chunks coalesced and recorded at least once per second), tool_call and tool_call_update (without content blocks; large raw output truncated), plan, permission, and agent_update. Antigravity's output is recorded as stdout and stderr lines.

Errors are tool errors whose text contains <code>: <message>, with codes unknown_agent, invalid_request, task_not_found, and invalid_transition.

Use

Requires uv, plus the CLI of each agent you use on PATH (agy, claude-agent-acp, cline, codex-acp, copilot, cursor-agent, opencode). Symm does not install, update, or document the installation of agents: install and authenticate each one following its vendor's instructions (ADR 0005). Symm runs the first match for each executable name in the absolute entries of the server's own PATH; which copy and version that is, is up to you.

Register Symm with an MCP client, for example Claude Code:

claude mcp add symm -- uvx symm-mcp@0.1.0

Or in a client's JSON configuration:

{
  "mcpServers": {
    "symm": {
      "command": "uvx",
      "args": ["symm-mcp@0.1.0"]
    }
  }
}

A typical loop, as the calling agent sees it:

  1. spawn_task(agent="claude_code", prompt="Review the authentication changes in this workspace and list concrete flaws.", workspace="/path/to/repo") returns a task_id while the reviewer starts working.
  2. Continue with other work.
  3. get_events(task_id, after_seq=<cursor>) to follow progress; get_task(task_id) once status is terminal.
  4. resolve_task(task_id, resolution="needs_followup", note="two findings to fix"), then dispatch the next task, possibly with the reviewer and implementer roles swapped.

Agents run in the given workspace with the server's environment. Pass workspace explicitly: the default is the server's working directory, which your MCP client chose and may be your home directory. Symm does not copy, isolate, or clean workspaces.

Architecture

flowchart TB
    client["MCP client"] <-- "stdio" --> server
    subgraph symm ["symm-mcp process"]
        server["MCP server<br/>five tools, error codes, signals"]
        service["TaskService<br/>orchestration, per-task locks"]
        store[("Task store<br/>tasks and events as JSON<br/>(in memory)")]
        registry["Agent registry<br/>option allowlists"]
        supervisor["Supervisor and ACP client<br/>process groups, sessions,<br/>permission decisions"]
        server --> service
        service --> store
        service --> registry
        service --> supervisor
    end
    supervisor -- "ACP over stdio<br/>(argv without a shell)" --> agents["ACP agents and the agy CLI<br/>(own process groups,<br/>shared workspace)"]
    agents -- "session updates,<br/>permission requests, exit" --> supervisor

Logical state (tasks, events, resolutions) lives in the store and is always serializable. Runtime state (processes, pipes, process groups) lives only in the supervisor. The MCP layer stays a thin transport. These boundaries are part of the project contract in PROJECT.md.

v0.1 Scope and Limitations

  • stdio only, process-local task ledger. Each MCP client launches its own Symm server, and each server sees only the tasks it dispatched. stdio + in-memory = process-local task ledger.

    flowchart LR
        A["Agent A"] -- stdio --> SA["Symm for A<br/>ledger: task 1"]
        SA -- "task 1" --> B["Agent B"]
        B -- stdio --> SB["Symm for B<br/>ledger: task 2"]
        SB -- "task 2" --> C["Agent C"]
    

    Agent A sees task 1 but not task 2, which B dispatched through its own server. A shared ledger needs an HTTP gateway with a shared store, which is deferred.

  • In memory only, by design. Task state is lost when the server process exits, and Symm does not persist it. The agent keeps its own session: to continue an ACP agent's work later, even from a new client session, keep the task's session_id and pass it to spawn_task. spawn_task usually returns before the agent's session exists, so read session_id with get_task once session_started is recorded. Resume in the same workspace, and with the same agent.

  • No output caps or retention policy yet. Every output line is kept as an event; long, high-output tasks grow server memory.

  • Tools only. No MCP resources or subscriptions yet; observe by polling get_events with its cursor.

  • No orphans. When a task completes, Symm ends everything left in its agent's process group, so background processes an agent starts do not outlive the task. When the client disconnects or the server receives SIGTERM, SIGINT, or SIGHUP, every running agent's process group is terminated before the server exits, and a task spawned while it shuts down fails instead of starting. A server killed with SIGKILL, or one that crashes, cannot do this; agents run in their own process groups and survive it.

  • Your environment, your permissions. Symm never escalates an agent's permissions; dependencies, secrets, sandboxing, and workspace cleanliness are the caller's responsibility.

  • POSIX only. Process groups and signals; Windows is not supported.

Rationale: ADR 0003 and ADR 0007. Security assumptions and how to report a vulnerability: SECURITY.md.

Run Locally

From a checkout:

uv run symm-mcp

The command speaks MCP over stdio, so it is meant to be launched by an MCP client rather than used interactively.

Development

This project uses OpenSpec; read AGENTS.md before changing anything. The Definition of Done:

uv sync
uv run python -m compileall -q src tests
uv run python -m pytest
uv run ruff check .
uv run ruff format --check .
./scripts/changelog-guard.sh

License

Licensed under either of Apache-2.0 or MIT, at your option.

Metadata

Release files for symm-mcp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for symm-mcp 0.1.0
File Size Uploaded
symm_mcp-0.1.0.tar.gz 42.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for symm-mcp 0.1.0
File Interpreter ABI Platform
symm_mcp-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 88.8 kB

Release files / symm_mcp-0.1.0.tar.gz

Download URL symm_mcp-0.1.0.tar.gz
Size 42.1 kB
Tags Source
SHA-256 checksum
How to use checksums
e9c420a57a41041f12af8b77c27fddd578aafb305212bec94271a0d167bfbf9c
BLAKE2b-256 checksum
How to use checksums
d5d6223ac66f2dca72a49930734a293c59a3b021608f4aafe434358525f612fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / symm_mcp-0.1.0-py3-none-any.whl

Download URL symm_mcp-0.1.0-py3-none-any.whl
Size 46.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
46581e4709502bc71f340405fb7991dfd92ca4a9eede6372a8b494080bd9ae24
BLAKE2b-256 checksum
How to use checksums
2ae8b86b8c6051c86351c60aebab1c28798a5af332f2baf175457ef257ff1d58
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page