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:
uvx claude-c2c --version
For local development:
uv tool install --editable .
Connect each Claude Code session
In each repo's .mcp.json (an example lives in examples/.mcp.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:
{
"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/onlineand 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_sessionwins; 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_byaudit 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:
messages.from_namedenormalized. The spec stores onlyfrom_session. I addedfrom_nameso messages display the sender even if that session has been superseded and renamed.pruneincluded in MVP. It's ~10 lines and you'll want it eventually.list_sessionsfilters out stale-renamed rows by default. Otherwise the list grows forever.SessionStarthook 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. (PureCLAUDE.mdinstructions work just as well.)- Schema is created once at startup, not lazily on every tool call (
executescriptissues an implicitCOMMITthat interferes with Python's transaction tracking insidewith conn:). CLAUDE_C2C_DBenv var to override the DB path (used by the test harness and useful for sandboxing).wait_for_messageskipped for now. V2 if you find you miss it.
Tests
Two smoke harnesses live in tests/:
# 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.
Metadata
Release files for claude-c2c 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 | |
|---|---|---|---|
| claude_c2c-0.1.0.tar.gz | 9.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| claude_c2c-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 16.7 kB
Release files / claude_c2c-0.1.0.tar.gz
| Download URL | claude_c2c-0.1.0.tar.gz |
|---|---|
| Size | 9.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1b366540031664086163da0f08b39100c3768ab3152a7cbe65badb6871bb6b5d
|
|
BLAKE2b-256 checksum How to use checksums |
17fa8a220297ca20fe8385e7f18012c6cb29970a9c8231c3798f7f014a41e930
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.1
|
Release files / claude_c2c-0.1.0-py3-none-any.whl
| Download URL | claude_c2c-0.1.0-py3-none-any.whl |
|---|---|
| Size | 7.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8f4f29293317c85bfc38f2e1d32bbd5da9140862701eab06160d399b5ae6ccee
|
|
BLAKE2b-256 checksum How to use checksums |
ae84829d72d04ee0dbb8e36bad38c7e39465fafbf284b09eb9ab2bdf3dbf5763
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.1
|