Local MCP server for code-to-code messaging between Claude Code instances
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_c2c-0.1.0.tar.gz.
File metadata
- Download URL: claude_c2c-0.1.0.tar.gz
- Upload date:
- Size: 9.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b366540031664086163da0f08b39100c3768ab3152a7cbe65badb6871bb6b5d
|
|
| MD5 |
25203dbabce54d69b284b2d05b90b162
|
|
| BLAKE2b-256 |
17fa8a220297ca20fe8385e7f18012c6cb29970a9c8231c3798f7f014a41e930
|
File details
Details for the file claude_c2c-0.1.0-py3-none-any.whl.
File metadata
- Download URL: claude_c2c-0.1.0-py3-none-any.whl
- Upload date:
- Size: 7.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f4f29293317c85bfc38f2e1d32bbd5da9140862701eab06160d399b5ae6ccee
|
|
| MD5 |
5aea705ebbb17aa3e3d3962bedbaa190
|
|
| BLAKE2b-256 |
ae84829d72d04ee0dbb8e36bad38c7e39465fafbf284b09eb9ab2bdf3dbf5763
|