Metadata-Version: 2.4
Name: symm-mcp
Version: 0.1.0
Summary: A lightweight, symmetric, asynchronous MCP channel for handing work between independent agents.
Keywords: mcp,model-context-protocol,agents,claude-code,async,handoff
Author: tacticaldoll
License-Expression: Apache-2.0 OR MIT
License-File: LICENSE-APACHE
License-File: LICENSE-MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development
Requires-Dist: agent-client-protocol>=0.12.1,<0.13
Requires-Dist: mcp>=2.3,<3
Requires-Dist: pydantic>=2.12,<3
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/tacticaldoll/symm-mcp
Project-URL: Repository, https://github.com/tacticaldoll/symm-mcp
Project-URL: Issues, https://github.com/tacticaldoll/symm-mcp/issues
Project-URL: Changelog, https://github.com/tacticaldoll/symm-mcp/blob/main/CHANGELOG.md
Project-URL: Security, https://github.com/tacticaldoll/symm-mcp/blob/main/SECURITY.md
Description-Content-Type: text/markdown

# 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](https://agentclientprotocol.com) (ACP) client**.

```mermaid
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](CHANGELOG.md)), published on PyPI as
[`symm-mcp`](https://pypi.org/project/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.

```mermaid
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.

```mermaid
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:

```mermaid
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](https://docs.astral.sh/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](docs/adr/0005-agents-are-installed-by-the-user.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:

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

Or in a client's JSON configuration:

```json
{
  "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

```mermaid
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](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`.

  ```mermaid
  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](docs/adr/0003-v0-1-runtime-boundaries.md) and
[ADR 0007](docs/adr/0007-the-ledger-is-not-persisted.md). Security assumptions and how to
report a vulnerability: [SECURITY.md](SECURITY.md).

## Run Locally

From a checkout:

```bash
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](AGENTS.md) before changing anything. The
Definition of Done:

```bash
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](LICENSE-APACHE) or [MIT](LICENSE-MIT), at your option.
