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.
+-----------+ +----------------------+ +--------------+
| Dispatch | ---> | Observe | ---> | Resolve |
|spawn_task | |get_events / get_task | |resolve_task |
+-----------+ +----------------------+ +-------+------+
^ |
| Repeat: next task / new viewpoint |
+-------------------------------------------------+
Status
0.2.0, the latest release
(changelog), published on
PyPI as symm-mcp. The package is classified as Alpha.
How a Task Flows
spawn_task returns as soon as the agent process is running. ACP session setup and the prompt
continue after that; the caller can do other work and later read only events it has not seen yet.
Caller Symm server Agent
| spawn_task(...) | |
|----------------------------->| start process group |
|<-- task_id, status=running --| |
| | initialize, new or |
| | load session, settings|
| |---- prompt over ACP ->|
| |<- updates/permission -|
| wait_tasks([task_id]) | |
|----------------------------->| |
| |<-- turn ends ---------|
|<-- completed, terminal ------| |
| get_events(after_seq=0) | |
|----------------------------->| |
|<-- page, cursor, has_more ---| |
| get_events(after_seq=cursor) | ... until has_more |
|----------------------------->| is false |
| resolve_task(...) | |
|----------------------------->| |
|<-- task with resolution -----| |
The next task may swap the agent and caller roles.
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.
Execution status (owned by Symm)
created --> starting --> running --success--> succeeded
| | |-- failure --> failed
| | +-- cancel/shutdown --> cancelled*
| +-- start failure -----------------> failed
| +----------------------------------> cancelled
+----------------------------------------------> cancelled
* If process-group cleanup cannot be confirmed, the task ends as failed.
Resolution (recorded by callers, finished tasks only)
unresolved --resolve_task--> judged
judged --resolve_task--> judged
judged = accepted | rejected | needs_followup | superseded
For a clean completion, succeeded means the agent's prompt turn ended with stop reason
end_turn.
This does not show that the agent did useful work: check for agent_message or tool_call
events, or verify the work, before accepting it.
Symmetric by Design
There is no built-in implementer or reviewer role. The same eight tools cover every direction work can travel:
Implement, then verify yourself
You -- implement X --> Agent
You <-- result ------ Agent
Implement yourself, ask for review
You -- review workspace --> Agent
You <-- findings ------- Agent
Chain or fan out
Agent A -- implement --> Agent B -- find flaws --> Agent C
|-- review 1 --> Reviewer 1
+-- review 2 --> Reviewer 2
initiator_id and executor_id are descriptive labels only. They never grant ownership or
authority.
Tools
| Tool | What it does |
|---|---|
describe_agent |
Reads advertised model/effort offers as {current, values}; launch flags have a null current, and effort values describe the default model. Process startup and ACP setup share a 30-second deadline; ending the process group afterward is a separate, bounded supervisor cleanup attempt. Creates no task. |
spawn_task |
Dispatch a prompt to a registered agent. Returns as soon as the agent process is running, with the task's notify_path. |
get_task |
Current state: execution status, exit_code, error (the latest error message, usually why it failed; one recorded after the task ended does not replace it), session_id, resolution, timestamps. |
get_events |
One bounded page of events after after_seq; returns cursor, has_more, status. |
wait_tasks |
Wait up to 30 seconds for any given task to end; returns summaries and a reason. |
list_tasks |
This server's tasks, newest first, as summaries; recovers task ids a caller lost. |
resolve_task |
Record accepted, rejected, needs_followup, or superseded for a finished task. No side effects; may be revised. |
cancel_task |
Stop a running agent's process group; failed cleanup ends the task as failed. A finished task is unchanged. |
An agent may keep the session it created for a description.
Registered agents. Symm is the Agent Client Protocol (ACP) client for six agents, and drives
every agent over ACP. The Claude Code and Codex adapters launch the user-installed
claude-agent-acp and codex-acp executables. Copilot, Cline, Cursor, and opencode are launched
through their ACP commands.
The caller's model and effort choices reach each agent through the interface it accepts. Claude
Code uses ACP session settings model and effort. Codex and Copilot use model and map effort
to the session setting reasoning_effort. Symm checks session settings against the agent's ACP
offer before sending the prompt. Cursor and opencode accept only model as a session setting.
Cline takes model as a session setting and passes effort as the launch flag
--thinking=<effort>.
| Agent | Executable Symm launches | options |
|---|---|---|
claude_code |
claude-agent-acp |
allow, model, effort |
codex |
codex-acp |
allow, model, effort |
copilot |
copilot --acp |
allow, model, effort |
cline |
cline --acp |
allow, model, effort |
cursor |
cursor-agent acp |
allow, model |
opencode |
opencode acp |
allow, model |
Permissions
Symm never widens an agent's permissions on its own: it approves only request kinds in allow,
never chooses an "always" option, and pins the permission settings the caller cannot set. Actions
an agent takes without sending Symm a permission request are outside what Symm observes or
enforces. Broader enforcement, such as sandboxing, belongs to the caller or an external policy
layer.
- Symm answers agent-sent permission requests. The
allowoption lists ACP tool kinds approved for this task:read,edit,delete,move,search,execute,think,fetch, andother. For an allowed kind, Symm chooses a one-time allow option; otherwise it chooses a one-time reject option. It never chooses an option that applies always. Mode-switch requests are never granted. If the one-time option it needs is not offered, Symm cancels the request. A decision Symm cannot record is cancelled, and the task fails. Each prompt-turn decision is recorded as apermissionevent. A request outside the prompt turn is cancelled without an event. - Permission settings are pinned. Before the prompt, Symm sets Claude Code
mode=default, Codexmode=read-only, Copilot itsagentmode and thenallow_all=off, and Clineauto_approve=false. Copilot'sautopilotmode turns allow-all on and continues the turn on its own, and a resumed Copilot session keeps its saved mode. Cursor is pinned tomode=agentonly whenallowcontainsedit; otherwise its mode isplan. The opencode adapter is pinned tomode=buildand configured to ask before edits, shell commands, and web fetches. Callers cannot set these pinned settings. If an ACP agent does not offer a setting or value Symm must pin, the task fails before the prompt and records anerrorevent naming it. The opencode adapter's ask rules are supplied throughOPENCODE_CONFIG_CONTENT: Symm preserves the user's other inline configuration, overrides those permission entries, and rejects a value that is not a JSON object. An empty value, or one of only JSON whitespace, counts as unset. - Cursor has a mode-specific limitation. Symm pins Cursor to
planwhenallowlacksedit; inplanmode Cursor itself writes no files and runs no commands. Inagentmode Cursor creates and edits files without asking, so Symm selects it only whenallowincludesedit. Cursor labels file deletions asedit, so allowingeditalso allows deletion. - Launch flags are checked by the agent. Cline's
effortlaunch flag acceptsnone,low,medium,high, orxhigh; a value it rejects fails the task. Model and effort session settings are checked against the agent's offer before the prompt.
Events
Every created task records task_created; each status transition is a status_changed event.
When the agent process starts, Symm records process_started; after supervision finishes, it
records process_exited. A start failure records error without a process start or exit.
Depending on what happens, a task can also record task_cancelled and task_resolved.
The event types a task records today are:
task_created, process_started, stderr, status_changed, process_exited,
task_resolved, task_cancelled, error, session_started, agent_message,
agent_thought, tool_call, tool_call_update, plan, permission, and agent_update.
New event types may be added in any release. Consumers must ignore event types they do not know.
process_exited carries the agent's exit_code and the turn's stop_reason; with no requested
stop or cleanup failure, only end_turn counts as succeeded, regardless of process exit code.
A requested stop ends as cancelled even when the agent reports end_turn.
session_started records the agent session id, stores it in the task's session_id, and makes
it available to spawn_task for a later resume. All six adapters support resume. A resumed task starts a new agent process and loads the
existing session in the same workspace. It records only updates from the new prompt turn, not
history replayed while loading. It uses only the model and effort choices supplied to that
spawn_task call; Symm does not reapply choices from an earlier task. An agent that cannot load
sessions fails before the prompt. If it rejects or does not recognize an id, the error event
names the id and says it may not exist for that agent or workspace.
Each ACP request before the prompt (initialize, new or load session, and setting changes) has a
120-second timeout. If the agent does not answer, the task fails before the prompt and the
error event names the unanswered request, such as load_session.
An invalid or unavailable model/effort session setting fails before the prompt and
records an error naming the setting; it also names the value when the agent offers
that setting but not that value.
ACP message and thought chunks are coalesced and recorded at least once per second. tool_call
and tool_call_update omit content blocks; raw output whose JSON encoding exceeds 4,000
characters is truncated to a preview. An agent's stdout carries the protocol and is not recorded;
its stderr lines are recorded as stderr events.
Symm's own invalid-request and lookup or transition failures are MCP tool errors whose text has
the form <code>: <message>. Codes are unknown_agent, invalid_request, task_not_found, and
invalid_transition. The MCP SDK rejects arguments that do not fit a tool's input schema,
including wrong types, missing required arguments, and unsupported values such as a resolution
outside its allowed values, before Symm handles the call. These validation errors use
the SDK's error text and have no Symm error code. Failed
starts return a failed task with an error event. ACP session and supervision failures may also
record error; an agent that exits before ending its turn fails the task.
Use
Requires uv, plus the CLI of each agent you use on PATH
(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
(intent/agent-adapters.md).
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.2.0
Or in a client's JSON configuration:
{
"mcpServers": {
"symm": {
"command": "uvx",
"args": ["symm-mcp@0.2.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.
wait_tasks([task_id])until it returnsreasonterminalwith the task in itscompletedlist. Each call waits at most 30 seconds. Then read its events withget_events(task_id, after_seq=<cursor>), passing the returnedcursorback whilehas_moreis true;event_typesnarrows what is returned.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.
Waiting Without Polling
spawn_task returns notify_path. When a task ends while the server is running, Symm creates an
empty file at that path. If the server shuts down first, it removes the directory and no marker
appears; read the outcome with get_task. A caller whose host wakes it when a
background command ends, as Claude Code does, can run this in the background instead of calling
wait_tasks again and again:
until [ -e "$P" ] || [ ! -d "$(dirname "$P")" ]; do sleep 1; done
P is the notify_path. The loop checks before it waits, because a task can end before
spawn_task returns. The file only signals that the task ended. Read the outcome with get_task.
If the directory is gone, the server has usually exited. Call get_task anyway: if the server
still answers, its base directory was moved, and you can fall back to wait_tasks. A server
killed with SIGKILL, or one whose file system stalls during shutdown, can leave the directory
behind, so give the loop your own deadline if the wait must end.
Markers live in a directory of their own for each server process, inside a per-user base:
$XDG_RUNTIME_DIR/symm-mcp, or symm-mcp-<uid> in the system temporary directory, where <uid>
is the effective uid. Pass
--notify-dir DIR to choose another base, or --no-notify to turn markers off, after the
package in the registration above, for example symm-mcp --no-notify. If the base is a symbolic link, is not yours, or is writable by
others, Symm logs a warning and starts with markers off. notify_path is null when markers are
off, whether because of --no-notify or because Symm refused the base at startup
(intent/completion-markers.md).
Agents run in the given workspace with the server's environment plus values their adapter sets;
adapter values override inherited ones. The opencode adapter rewrites OPENCODE_CONFIG_CONTENT to
include its permission configuration. 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
MCP client
| stdio
v
+------------------------ symm-mcp process -------------------------+
| MCP server (eight tools, errors, signals) |
| | |
| v |
| TaskService (orchestration, per-task locks) |
| +--> Task store (JSON strings in memory) |
| +--> Agent registry (adapter option allowlists) |
| +--> TaskSupervisor (process groups, argv without a shell) |
| | |
| +-- launches ACP agents in shared workspace |
| +-- reads agent stderr, observes exits |
| +-- runs the ACP session client |
| (sessions, settings, permissions) |
| | ^ |
+-------------------------|---------------------|-------------------+
| ACP over stdio | session updates,
v | permission requests
ACP agent processes (own process groups)
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.
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.Agent A | stdio v Symm for A [ledger: task 1] | task 1 v Agent B | stdio v Symm for B [ledger: task 2] | task 2 v 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 later, even from a new client or server process, keep the task's
session_idand pass it tospawn_task.spawn_taskusually returns before an ACP session exists, so readsession_idwithget_taskor from thesession_startedevent. Resume with the same agent and workspace. The ledger and earlier task's events do not resume with it. -
No total output cap or retention policy. Stderr lines longer than 1 MiB are split into pieces no larger than 1 MiB. There is no cap on total output; output remains in the process-local ledger, so long, high-output tasks grow server memory.
-
Tools only. No MCP resources or subscriptions yet. Wait for completion with
wait_tasks, which returns within 30 seconds, or in the background on the task's completion marker. Read progress withget_eventsand its cursor. -
Bounded process cleanup. Before reporting completion, Symm tries to end every remaining member of the agent's process group. When canceling a running ACP task, it first asks the agent to cancel its prompt turn, then uses SIGTERM and, if needed, SIGKILL with bounded waits. If Symm cannot confirm the group is empty, the task ends as
failedand the supervisor stops managing it; a process can remain alive. On client disconnect or handled SIGTERM, SIGINT, or SIGHUP, the server applies the same bounded cleanup before it exits. A task requested after shutdown begins fails rather than starting. A server killed with SIGKILL or one that crashes cannot run its shutdown handling, so its agents may survive. -
Your environment, your permissions. Symm does not install agent software or provide an operating-system sandbox for agent actions. Dependencies, secrets, sandboxing, and workspace cleanliness are the caller's responsibility. Symm pins adapter-controlled permission settings and answers the ACP permission requests an agent sends according to
allow. -
POSIX only. Process groups and signals; Windows is not supported.
Rationale: PROJECT.md and the decisions under intent/. 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 records its decisions as plerosis
intent under intent/; 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
plerosis check --run
License
Licensed under either of Apache-2.0 or MIT, at your option.
Metadata
Release files for symm-mcp 0.2.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.2.0.tar.gz | 64.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| symm_mcp-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 135.6 kB
Release files / symm_mcp-0.2.0.tar.gz
| Download URL | symm_mcp-0.2.0.tar.gz |
|---|---|
| Size | 64.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0e98e7d246e9b61f197bed9f7fca31cd4db06e57fed9c01e0c4fb586f7bbeff7
|
|
BLAKE2b-256 checksum How to use checksums |
0ee369c5c95f675415348a5b6e8de451d35bf78a060ee453a1cbb1836185e537
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|
Release files / symm_mcp-0.2.0-py3-none-any.whl
| Download URL | symm_mcp-0.2.0-py3-none-any.whl |
|---|---|
| Size | 70.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ea6dd0d9e9c3b081eb9111d44f666b92020af660d1a33cd4786bd02be0e7c54e
|
|
BLAKE2b-256 checksum How to use checksums |
094d2e666d4ec838b76d8a6fa7d1ac1ca419dcc2aeb440f1f3dcc22351743e57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|