Metadata-Version: 2.4
Name: claude-c2c
Version: 0.1.0
Summary: Local MCP server for code-to-code messaging between Claude Code instances
Author: C2C
Keywords: claude,claude-code,ipc,mcp
Requires-Python: >=3.10
Requires-Dist: mcp>=1.0.0
Description-Content-Type: text/markdown

# claude-c2c

Local MCP server for **code-to-code** messaging between Claude Code instances on the same machine. Two sessions register friendly names ("local", "online"), then exchange messages and shared state through a SQLite-backed inbox — no external services, no extra deps beyond the MCP SDK.

## Why

When you run two Claude Code sessions in parallel — one on a local data pipelines repo, another on a deployed branch — you usually copy/paste between them. This MCP cuts you out of the loop: each side reads its inbox at the start of a turn and writes to the other's inbox when it has something to hand off.

## Tools

| Tool | Purpose |
| --- | --- |
| `register_session(name, metadata?)` | Claim a friendly name. Supersedes any existing session with the same name. |
| `heartbeat()` | Refresh `last_seen_at`. Auto-called by every other tool. |
| `list_sessions(active_within_min=10)` | See who's online. |
| `send_message(to, body, tag?)` | Queue a message for another session. `to="*"` broadcasts. |
| `inbox(unread_only=True, limit=20, tag?)` | Read messages addressed to me (or to `*`). Doesn't mark read. |
| `mark_read(message_ids)` | Ack messages. |
| `set_state(key, value)` | Upsert a JSON value into shared k/v. |
| `get_state(key?, prefix?)` | Read shared state. |
| `prune(older_than_days=30)` | Delete read messages + stale-renamed sessions. |

## Install

Once published to PyPI:

```sh
uvx claude-c2c --version
```

For local development:

```sh
uv tool install --editable .
```

## Connect each Claude Code session

In each repo's `.mcp.json` (an example lives in `examples/.mcp.json`):

```json
{
  "mcpServers": {
    "c2c": {
      "command": "uvx",
      "args": ["claude-c2c"]
    }
  }
}
```

## Auto-register at session start

The cleanest path is a one-liner in your `CLAUDE.md`:

```
At session start, call c2c.register_session(name="local").
At the start of every turn, call c2c.inbox() and act on anything new
before continuing.
```

A `SessionStart` hook in `.claude/settings.json` can also remind Claude — note that hooks output text into context, they don't directly call MCP tools, so the hook just nudges Claude to register:

```json
{
  "hooks": {
    "SessionStart": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "echo 'Reminder: call c2c.register_session(name=\"local\") and check c2c.inbox() now.'"
      }]
    }]
  }
}
```

## Usage pattern

```
A: register_session(name="local")
A: send_message(to="online", body="ran the etl, last_run_id=abc123", tag="status")
A: set_state(key="last_run_id", value="abc123")

B: register_session(name="online")
B: inbox()                          → sees A's status message
B: mark_read([id])
B: get_state(key="last_run_id")     → "abc123"
B: send_message(to="local", body="redeploying", tag="handoff")
```

## Conventions

- **Names are commitments.** Pick `local` / `online` and stick. Routing is by name.
- **State for facts, messages for asks.** `set_state("last_run_id", ...)` for snapshots; `send_message("online", "redeploy now", tag="handoff")` for actions.
- **Read inbox first.** Run `inbox(unread_only=True)` at the top of each turn before deciding what to do.
- **Suggested tags:** `handoff`, `question`, `status`, `error`. Filters, not types.
- **Message size:** no cap. SQLite TEXT handles arbitrary lengths fine; if you blow up the DB with megabyte payloads it's on you.

## Storage

SQLite at `~/.claude/c2c.db` with WAL mode. Concurrent writes are safe; the file is durable across crashes. No automatic pruning — call `prune` when the DB grows.

Override the path with `CLAUDE_C2C_DB=/some/path.db` (handy for tests or sandboxed setups).

## Trust model

Single-user, single-machine, stdio transport. Each Claude Code session spawns its own server child; nothing is exposed over the network. The DB lives in `~/.claude/`, which is already a trusted directory (Claude Code stores its own state there). Anything that can read your home directory can read the inbox — same as any other file you own. There's no auth and no encryption, by design.

## Edge cases

- **Two sessions claim the same name:** Last `register_session` wins; the older row is renamed `<name>-stale-<ts>` so its history is preserved but it stops receiving messages.
- **Crash mid-write:** WAL replay guarantees consistency.
- **State conflict:** Last-writer-wins. `updated_at` / `updated_by` audit trail.
- **Recipient offline:** Messages queue indefinitely until somebody registers under that name and reads them.

## Changes from the original spec

A few small adjustments from the original spec worth flagging:

1. **`messages.from_name` denormalized.** The spec stores only `from_session`. I added `from_name` so messages display the sender even if that session has been superseded and renamed.
2. **`prune` included in MVP.** It's ~10 lines and you'll want it eventually.
3. **`list_sessions` filters out stale-renamed rows by default.** Otherwise the list grows forever.
4. **`SessionStart` hook reframed as a reminder.** The spec's hook example shells out a JSON tool-call payload, but Claude Code hooks emit text into the model's context — they don't directly invoke MCP tools. The hook above prints a reminder string; Claude reads it and calls the tool itself. (Pure `CLAUDE.md` instructions work just as well.)
5. **Schema is created once at startup**, not lazily on every tool call (`executescript` issues an implicit `COMMIT` that interferes with Python's transaction tracking inside `with conn:`).
6. **`CLAUDE_C2C_DB` env var** to override the DB path (used by the test harness and useful for sandboxing).
7. **`wait_for_message` skipped for now.** V2 if you find you miss it.

## Tests

Two smoke harnesses live in `tests/`:

```sh
# in-process: drives the tool functions directly against a tmp SQLite
.venv/Scripts/python.exe tests/smoke_test.py

# end-to-end: spawns the installed `claude-c2c` over stdio
# and exercises it via the official MCP client
.venv/Scripts/python.exe tests/mcp_handshake.py
```

Both use a temp DB (the handshake test passes `CLAUDE_C2C_DB`), so they don't touch your real `~/.claude/c2c.db`.
