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.

+-----------+      +----------------------+      +--------------+
| Dispatch  | ---> | Observe              | ---> | Resolve      |
|spawn_task |      |wait_tasks / get_task |      |resolve_task  |
+-----------+      +----------------------+      +-------+------+
       ^                                                 |
       |        Repeat: next task / new viewpoint        |
       +-------------------------------------------------+

Status

0.3.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 ------|                       |
  |    (status, final_message,   |                       |
  |     permissions_refused)     |                       |
  :                              :                       :
  : optional, when the outcome is not enough:            :
  | get_task(task_id)            |                       |
  |----------------------------->|                       |
  |<-- usage, session_id, ... ---|                       |
  | get_events(after_seq=cursor) |                       |
  |----------------------------->|                       |
  |<-- page, cursor, has_more ---| repeat until          |
  |                              | has_more is false     |
  :                              :                       :
  | resolve_task(...)            |                       |
  |----------------------------->|                       |
  |<-- task with resolution -----|                       |

wait_tasks and get_task both return the turn's final_message (its trailing agent_message text, capped at the last 4,000 characters, with final_message_truncated saying whether the cap cut it) and permissions_refused, so the usual path reads the outcome there. get_task alone also returns usage. get_events pages through the full record when the outcome is not enough, and can be read while the task runs.

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 thirteen 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 an agent's advertised model and effort offers, how each choice is delivered, and the settings Symm pins. Creates no task.
list_sessions Lists the sessions an agent can resume from a workspace, one page at a time, with a cursor for the next. Creates no task.
delete_session Deletes one of an agent's sessions, which cannot be undone, and returns the agent, the resolved workspace, the session id and deleted: true. Creates no task.
spawn_task Dispatches a prompt to a registered agent and returns the task summary plus the task's notify_path as soon as the agent process is running. With keep_idle, keep_turns or on_agent it also returns kept_agent (see Keeping an agent).
get_task Returns a task's current state: status, error, resolution and its outcome (final_message, permissions_refused, usage).
get_events Returns one bounded page of a task's events after after_seq, with cursor, has_more and status.
wait_tasks Waits for any of the given tasks to end and returns the finished ones with final_message and permissions_refused, and a reason.
list_tasks Lists this server's tasks, newest first, as summaries; recovers task ids a caller lost.
resolve_task Records accepted, rejected, needs_followup or superseded for a finished task and returns the task summary plus its resolution note, actor and time. It has no other effect and may be revised.
cancel_task Stops a running task's agent process group and returns the task summary. A finished task is unchanged.
forget_task Frees a finished task the caller has already resolved, with its event history and completion marker, and returns {task_id, forgotten: true}. Creates no task and cannot be undone.
extend_agent Renews a kept agent's lease and returns {handle, expires_at, turns_left}. Creates no task.
release_agent Ends a kept agent and returns {handle, agent, session_id, released: true} once its end is known. Creates no task.

Tool results and limits

What the table leaves out, tool by tool. Error codes are listed under Events.

  • describe_agent. Each model or effort offer is {current, values}; launch flags have a null current, and effort values describe the default model. It also reports delivery (per choice, session_setting or launch_flag), pins (the settings Symm fixes) and pins_with_edit (the pins when allow includes edit). Process startup and ACP setup share a 30-second deadline; ending the process group afterward is a separate, bounded supervisor cleanup attempt. No task is created, though the agent may keep the session it created for the description in its history.
  • list_sessions. Returns {agent, workspace, sessions, next_cursor}; each session keeps session_id, cwd, title, updated_at and additional_directories only when the agent reports them, never _meta. It passes the resolved workspace as the session/list cwd filter and a cursor through; the page size is the agent's. It has the same 30-second deadline and bounded cleanup as describe_agent. An agent that did not advertise session list fails with unsupported:.
  • delete_session. Sends ACP session/delete for session_id, under the same 30-second deadline and bounded cleanup as describe_agent. An agent that did not advertise session delete fails with unsupported: before anything is sent; a session a live task or a kept agent of this server holds, or that another delete is deleting, fails with session_in_use:, as does a resume that arrives while the delete runs. describe_agent and list_sessions never delete anything.
  • spawn_task. keep_idle (1 to 600 seconds) and keep_turns (1 to 20) ask Symm to keep the agent alive after a successful turn (see Keeping an agent). A ninth keep request fails with agent_limit: and creates no task. on_agent takes a kept agent's handle and runs the task's turn on that agent instead of starting one; it is refused, creating no task, with agent_gone:, agent_busy: or invalid_request:. With on_agent, keep_idle and keep_turns renew the lease instead of asking to keep an agent.
  • get_task. Returns the task summary (execution status, exit_code, error, session_id, resolution, timestamps) plus started_at, resolution_note, resolved_by and resolved_at. error is the latest error message, usually why the task failed; one recorded after the task ended does not replace it. include_prompt returns the prompt as well. permissions_refused is a count of the task's recorded permission requests that were rejected or cancelled, never more than were recorded; it is informational and never changes status. usage is the turn's last context report and its token report, as the agent reported them, and null before a turn. final_message is the turn's trailing consecutive agent_message events, capped at the last 4,000 characters, and final_message_truncated says whether the cap cut it. A task that asked to keep its agent, or runs on a kept agent with on_agent, also carries kept_agent.
  • get_events. A page holds 50 events by default (up to 500 with limit) and a soft 8,000-character budget (max_chars of at least 1,000). agent_thought events and the rawInput/rawOutput previews of tool_call and tool_call_update payloads are excluded unless asked: include_thoughts returns thoughts, include_raw keeps the raw previews. event_types narrows what is returned.
  • wait_tasks. Takes up to 50 distinct task ids and waits at most 30 seconds. Each finished task is its summary plus permissions_refused, final_message and final_message_truncated as in get_task; usage is read with get_task. reason is terminal, timeout or shutdown.
  • list_tasks. Pages 20 tasks by default (up to 100 with limit) and returns next, which after takes for older tasks.
  • cancel_task. A stop that leaves the process group not confirmed empty ends the task as failed, not cancelled, and keeps both the cancellation and the error in its events.
  • forget_task. Afterwards get_task, get_events and a second forget_task fail with task_not_found:, list_tasks no longer lists the task, and an after cursor naming it still pages. A task that is still running, not resolved, or whose kept agent is still alive fails with invalid_transition:; an unknown id fails with task_not_found:. Nothing is created or changed by a refusal.
  • extend_agent. keep_idle (1 to 600 seconds) replaces the idle bound and keep_turns (1 to 20) replaces the turns left; one left out keeps its current value, and the idle clock starts over from the idle bound either way. A bound out of range fails with invalid_request:, a handle that names no kept agent with agent_gone:, and an agent running a turn with agent_busy:.
  • release_agent. Sends ACP session/close first when the agent advertised it, then ends the process group. A handle that names no kept agent fails with agent_gone:, one running a turn with agent_busy:, and a group that cannot be confirmed empty with invalid_request: after the handle is gone.

Keeping an agent

A task ends its agent when its turn ends. spawn_task with keep_idle or keep_turns asks Symm to keep the agent process alive instead, under a lease, when the turn ends succeeded with no unkept guarantee; the other bound takes its default of 300 seconds or 1 turn. The agent's handle (agent_ and 12 hexadecimal digits) comes back in kept_agent, which get_task shows live: its state, expires_at and turns_left. The turn records agent_kept in place of process_exited. Any other turn ends its agent as before, and kept_agent says why it was not kept; so does a cancel_task that arrives before the keep completes, which ends the agent and leaves a finished task succeeded. At most 8 agents are kept, and the kept agent holds its session, so a resume of that session is refused with session_in_use: naming the handle. The lease ends, and Symm ends the agent's process group, when release_agent is called, when keep_idle passes with no turn, when the agent exits, or when the server shuts down; a group that cannot be confirmed empty keeps the session held and makes the server exit 1.

Caller                     Symm server                Agent process
  | spawn_task(keep_idle,        |                       |
  |   keep_turns=2)              |---- prompt, turn 1 -->|
  |<-- task_id, kept_agent ------|                       |
  |    (handle, state=pending)   |<-- turn ends ---------|
  |                              | succeeded: keep it,   |
  |                              | record agent_kept     |
  |                              | (turns_left=2)        |
  | wait_tasks([task_id])        |                       |
  |----------------------------->|                       |
  |<-- completed ----------------|                       |
  | get_task(task_id)            |                       |
  |----------------------------->|                       |
  |<-- kept_agent: state=kept ---| idle clock runs       |
  |    expires_at, turns_left    |                       |
  | spawn_task(on_agent=handle)  |                       |
  |----------------------------->|---- prompt, turn 2 -->|
  |<-- new task_id --------------|<-- turn ends ---------|
  |                              | same process and      |
  |                              | session, no           |
  |                              | process_started;      |
  |                              | succeeded: kept again |
  |                              | (turns_left=1)        |
  | extend_agent(handle)         |                       |
  |----------------------------->| restart idle clock;   |
  |<-- handle, expires_at, ------| given bounds replace  |
  |    turns_left                | the old ones          |
  | release_agent(handle)        |                       |
  |----------------------------->|---- session/close --->|
  |<-- released: true -----------| end process group     |
  :                              :                       :
  : or Symm ends the group itself when keep_idle passes
  : with no turn, the agent exits, the server shuts down,
  : or a turn leaves no turn to spend or does not succeed

extend_agent and release_agent are refused with agent_busy: while a turn runs, so they come between turns; release_agent sends session/close only when the agent advertised it.

A later task runs its turn on a kept agent with spawn_task(on_agent=<handle>). The turn uses the agent's live process and session, so the process is not started again and the task records no process_started, and the handle keeps holding the session. It is still one task per turn: the task carries the handle and get_task shows its kept_agent. A turn spends one of the agent's turns_left, and the idle bound pauses while the agent runs it; when the turn succeeds the agent is kept again with a fresh keep_idle, and its task records agent_kept. When no turn is left, or the turn does not succeed, or cancel_task stops it, the agent's group is ended after the turn and the task records process_exited. A turn's model and effort are sent before its prompt and stay with the session until a later turn changes them; its allow applies to that turn only. What Symm can only pass when it starts the process, such as Cline's effort, the workspace and the agent must be the kept agent's: a launch option left out keeps the process's value, and one with a different value is refused with invalid_request: naming it. So is a model or effort the agent does not offer, checked before the turn is claimed, so a typo costs neither a task nor a turn. An agent that is running a turn, or is still finishing the turn that kept it after a wait of at most the grace period, is agent_busy: and a task that waits for it should wait_tasks and retry; Symm does not queue. on_agent cannot be combined with session_id. keep_idle and keep_turns given with it renew the lease from the keep that follows the turn, as extend_agent does while the agent is idle: keep_idle is the idle bound, keep_turns the turns left after this turn, a bound left out keeps its value, and a turn that does not keep the agent renews nothing. A value out of range is refused with invalid_request: before the claim.

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 allow option lists ACP tool kinds approved for this task: read, edit, delete, move, search, execute, think, fetch, and other. 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 a permission event. A request outside the prompt turn is cancelled without an event.
  • Permission settings are pinned. Before the prompt, Symm sets Claude Code mode=default, Codex mode=read-only, Copilot its agent mode and then allow_all=off, and Cline auto_approve=false. Copilot's autopilot mode turns allow-all on and continues the turn on its own, and a resumed Copilot session keeps its saved mode. Cursor is pinned to mode=agent only when allow contains edit; otherwise its mode is plan. The opencode adapter is pinned to mode=build and 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 an error event naming it. The opencode adapter's ask rules are supplied through OPENCODE_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 plan when allow lacks edit; in plan mode Cursor itself writes no files and runs no commands. In agent mode Cursor creates and edits files without asking, so Symm selects it only when allow includes edit. Cursor labels file deletions as edit, so allowing edit also allows deletion.
  • Launch flags are checked against no offer. Cline's effort launch flag accepts none, low, medium, high, or xhigh; Symm refuses any other value with invalid_request: and creates no 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, or agent_kept when the agent is kept after its turn. 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, agent_kept, error, session_started, agent_message, agent_thought, tool_call, tool_call_update, plan, permission, agent_request, 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. list_sessions shows the sessions an agent can resume from a workspace, and delete_session deletes one. 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 request, lookup, transition, and session-use failures are MCP tool errors whose text has the form <code>: <message>. Codes are unknown_agent, unsupported (the agent did not advertise the capability the call needs), invalid_request, task_not_found, invalid_transition, session_in_use, agent_limit, agent_gone, and agent_busy. 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.3.0

Or in a client's JSON configuration:

{
  "mcpServers": {
    "symm": {
      "command": "uvx",
      "args": ["symm-mcp@0.3.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. wait_tasks([task_id]) until it returns reason terminal with the task in its completed list. Each call waits at most 30 seconds. Read the outcome there: status, final_message (check final_message_truncated) and permissions_refused, the count of the task's permission requests that were rejected or cancelled.
  4. Only when that is not enough, read the record with get_events(task_id, after_seq=<cursor>), passing the returned cursor back while has_more is true; event_types narrows what is returned. get_task adds usage.
  5. 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.
  6. When the result is no longer needed, forget_task(task_id) frees the task and its events. Do it after anything waiting on its notify_path has woken, because forgetting removes the marker.

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. Every agent also receives SYMM_MCP_DEPTH, set to the server's depth plus one, and a server started with SYMM_MCP_DEPTH of 1 or more, or a malformed value, or below a Linux ancestor that carries the nesting marker, lists no tools and says why in its instructions: an agent Symm launched cannot dispatch further agents through Symm. 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 (thirteen tools, errors, signals)                      |
|    |                                                              |
|    v                                                              |
| TaskService (orchestration) -> ledger (per-task locks, writes)    |
|    +--> Task store (JSON strings in memory)                       |
|    +--> Agent registry (adapter option allowlists)                |
|    +--> ProcessPool (process groups, argv without a shell)        |
|              |                                                    |
|              +-- launches ACP agents in shared workspace          |
|              +-- keeps agents alive after a turn, under leases    |
|              |     (an idle clock per lease, 8 kept at most)      |
|              +-- serves describe_agent, list_sessions and         |
|              |     delete_session on short-lived probe processes  |
|              |     (no task, not through the ledger)              |
|              +-- 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 process pool. 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_id and pass it to spawn_task. spawn_task usually returns before an ACP session exists, so read session_id with get_task or from the session_started event. Resume with the same agent and workspace. The ledger and earlier task's events do not resume with it. If a live task or a kept agent of this server already holds that (agent, session_id), spawn_task returns session_in_use: naming that task id or the handle without creating a task. This limit is process-local: another Symm server or the agent's CLI can still use the session because the agent owns its session store.

  • No cap on memory, output or retention. Task records, their event histories and every kept-agent handle ever reserved (one small entry each, also one never kept) live in the server's memory for as long as the server runs, with no limit on their number, their size or the total output, and are lost when the server exits; stderr lines longer than 1 MiB are split into pieces no larger than 1 MiB. A server that is never asked to forget grows with every task, and long, high-output tasks grow it faster. A caller that has read a finished task's result and resolved it can free it with forget_task; Symm never frees a task on its own, because that would change what get_task and get_events return for old tasks. A forgotten task still costs its id and its place in creation order, so that cursors keep working and the id is never reused. Forgetting removes the task's completion marker, so forget a task only after anything waiting on its notify_path has woken.

  • 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 with get_events and its cursor.

  • Bounded process cleanup. Before reporting completion, Symm tries to end every remaining member of the agent's process group, except for an agent the caller asked to keep after a turn that succeeded: its group ends with its lease, by the same bounded cleanup. 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 failed and the pool 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, and exits with status 1 on every one of those paths, including a signal, which would otherwise end the process by that signal, if any agent's process group could not be confirmed empty, so a host can tell that agents may remain. A shutdown that itself fails after end of input also exits with status 1; one that fails during a signal still ends the process by that signal unless a group is unconfirmed. When every group is confirmed empty and shutdown itself did not fail, end of input exits 0 and a signal ends the process by itself. 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. It declines every elicitation request an agent sends during a turn, and cancels one that arrives outside a turn.

  • 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.3.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.3.0
File Size Uploaded
symm_mcp-0.3.0.tar.gz 111.9 kB Details

Built distribution (wheel)

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

Total release size: 228.8 kB

Release files / symm_mcp-0.3.0.tar.gz

Download URL symm_mcp-0.3.0.tar.gz
Size 111.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c4cfeb77566629d508002d06ace2eb39d744453893edf138b6e002ee11b848ca
BLAKE2b-256 checksum
How to use checksums
f8b042edd7d832b7e88726067fee2369d3b7dc24f4eb735db9deade2f3f2f98b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.15

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

Download URL symm_mcp-0.3.0-py3-none-any.whl
Size 116.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f7c1f33d24669b9b3b622e4f58c970b4ad63083b3d8b0f1890573114b15ed4ab
BLAKE2b-256 checksum
How to use checksums
c2ded78e864d34a4f249154d7ffbe8ef325a8c304f86cf19f2c20574cdceae37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.15

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

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