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.
allowlists 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 apermissionevent. - 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, Codexmode=read-only, Copilotallow_all=off, Clineauto_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
agentmode, so Symm selects that mode only whenallowcontainsedit, and otherwise keeps Cursor in read-onlyplan. Cursor labels file deletions asedit, so allowingeditalso approves them; and withoutedit, anexecutegrant cannot be used, becauseplanruns 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"}ordangerously_skip_permissions. Its prompt is one--print=<prompt>argument, limited to 100,000 bytes, and it stops after 5 minutes unless you setprint_timeout. modelandeffortmust be values the agent offers; otherwise the task fails before the prompt with anerrorevent naming them. Boolean options accept only JSONtrueorfalse.
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:
spawn_task(agent="claude_code", prompt="Review the authentication changes in this workspace and list concrete flaws.", workspace="/path/to/repo")returns atask_idwhile the reviewer starts working.- Continue with other work.
get_events(task_id, after_seq=<cursor>)to follow progress;get_task(task_id)oncestatusis terminal.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_idand pass it tospawn_task.spawn_taskusually returns before the agent's session exists, so readsession_idwithget_taskoncesession_startedis 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_eventswith 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)
| File | Size | Uploaded | |
|---|---|---|---|
| symm_mcp-0.1.0.tar.gz | 42.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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