crosstalk-mcp
v1.2.0
Published
Local MCP server for shared AI agent collaboration sessions
Maintainers
Readme
Crosstalk
A local MCP server that lets multiple AI agents collaborate through shared sessions. Use it when a frontend project agent and a backend project agent need to coordinate on features that touch both codebases.
Sessions are stored in a local JSON file (~/.crosstalk/sessions.json by default).
Setup
From npm (recommended)
No clone required — use with Cursor via npx:
{
"mcpServers": {
"crosstalk": {
"command": "npx",
"args": ["-y", "crosstalk-mcp"],
"env": {
"CROSSTALK_AGENT_ID": "frontend"
}
}
}
}CLI commands also work globally after install, or via npx:
npx crosstalk-mcp wait --session-id <uuid> --agent-id frontend --agent-key <your-key>
npx crosstalk-mcp export <session-id> ./transcript.mdNote: The npm package name is
crosstalk-mcpbecausecrosstalkis taken by an unrelated legacy package. Usenpx crosstalk-mcp, notnpx crosstalk.
From source
git clone <repo-url>
cd crosstalk
npm install
npm run buildCursor configuration
Add to your Cursor MCP settings (.cursor/mcp.json in a project, or global Cursor settings). Use the same config in both the frontend and backend projects — they share the same session store.
Frontend project — set a distinct agent ID:
{
"mcpServers": {
"crosstalk": {
"command": "npx",
"args": ["-y", "crosstalk-mcp"],
"env": {
"CROSSTALK_AGENT_ID": "frontend"
}
}
}
}Backend project:
{
"mcpServers": {
"crosstalk": {
"command": "npx",
"args": ["-y", "crosstalk-mcp"],
"env": {
"CROSSTALK_AGENT_ID": "backend"
}
}
}
}For local development from a clone, use "command": "node" and "args": ["/absolute/path/to/crosstalk/dist/index.js"] instead.
Optional: set CROSSTALK_DATA_DIR to change where sessions are stored (both projects must use the same directory). Set CROSSTALK_DEFAULT_TTL_HOURS to override the default idle auto-close window (default 72).
Tools
| Tool | Description |
|------|-------------|
| create_session | Create a session with name, description; creator is auto-joined. Optional tags, ttl_hours. |
| list_sessions | List active sessions (newest first). Optional include_closed, include_archived. |
| search_sessions | Search by name, description, ID, or tags. Optional include_closed, include_archived. |
| get_session | Get session details (works on archived sessions). |
| join_session | Join a session as a participant. |
| send_message | Post a message. Optional kind (proposal/decision/blocker/question/contract), attachments ({path, description?}[]), reply_to. |
| read_messages | Read messages; use unread_only: true to poll for new ones. |
| mark_read | Mark messages as read. This is the ack — there is no separate ack/reaction tool. |
| set_tags | Add and/or remove tags on a session. |
| archive_session | Archive a session (closes it first if open). Hidden from list/search by default. |
| unarchive_session | Reverse archiving. Session stays closed if it was closed. |
| get_wait_command | Get the background wait command — exits on each message so Cursor resumes the agent. |
| get_watch_command | Deprecated alias for get_wait_command. |
| close_session | Close a session when done. |
MCP prompts
The server exposes reusable prompt templates (if your client supports MCP prompts):
| Prompt | When to use |
|--------|-------------|
| collaboration-guide | Starting cross-project collaboration |
| handle-message-alert | A CROSSTALK_NEW_MESSAGE notification arrived |
| join-session | Joining a session with a known ID |
Server instructions also include the full workflow and are injected into the agent context when Crosstalk MCP is connected.
Data model
Sessions are stored as JSON at ~/.crosstalk/sessions.json. The store is schema-versioned (currently 2); older files migrate automatically on first read. All new fields are optional, so existing session files keep working unchanged.
Concurrency
Writes are serialized with a cross-process lockfile (sessions.json.lock via proper-lockfile) and the JSON is written via temp-file + atomic rename, so concurrent agents can't clobber each other or produce a torn file. If the store is busy you'll get a clear "Session store is busy; retry" error.
Typed messages
send_message accepts an optional kind to signal intent:
| Kind | Use for |
|------|---------|
| message | Default freeform message |
| proposal | A proposed approach or contract |
| decision | A settled decision |
| blocker | Something blocking progress |
| question | A question for the other agent |
| contract | An API/data contract |
The kind appears in the wait alert JSON and the exported transcript.
Attachments
send_message accepts an optional attachments array of { path, description? }. These are filesystem references the receiving agent can read directly — useful for pointing at code instead of pasting large blocks into content.
Tags
create_session accepts optional tags, and set_tags adds/removes tags on an existing session. search_sessions matches tags in addition to name, description, and ID.
Archive
archive_session hides a finished session from list_sessions / search_sessions without deleting it (it closes the session first if still open). Archived sessions remain readable via get_session and export. Passing include_archived: true surfaces archived sessions in list/search regardless of closed state. unarchive_session reverses the archive AND reopens the session (clears both archivedAt and closedAt).
Idle auto-close
Sessions auto-close after their TTL of inactivity. The TTL is ttl_hours from create_session, falling back to CROSSTALK_DEFAULT_TTL_HOURS (default 72). The sweep is lazy: it runs on the next write to the store, so there's no daemon. When a session is auto-closed, Crosstalk stamps a system decision message (_Session auto-closed after Nh idle._) so the close is visible in the transcript.
mark_read is the acknowledgment mechanism — calling it tells the other agent you've seen the message.
Cursor skill
Install the skill in each collaborating project by copying or linking .cursor/skills/crosstalk/ from this repo. The skill auto-triggers when agents work with Crosstalk or receive message alerts.
Background message alerts
MCP cannot push notifications to agents. In Cursor, background tasks wake the agent when they complete — not when they print output mid-run.
Use the wait command (not watch). It blocks until the other agent sends a message, prints CROSSTALK_NEW_MESSAGE, then exits. Cursor resumes the agent; the agent handles the message and starts a new wait.
After joining a session, call get_wait_command (or use the wait field from create_session / join_session):
node /Users/dean.ward/projects/crosstalk/dist/index.js wait \
--session-id <uuid> \
--agent-id frontend \
--agent-key <your-key> \
--interval 2Agent loop:
- Run
waitin a background shell (block_until_ms: 0) - Continue local work
- When the other agent messages,
waitexits → Cursor resumes you - Read
CROSSTALK_NEW_MESSAGEfrom the terminal output - Handle via
read_messages/send_message/mark_read - Start a new background
wait— repeat from step 1
Do not use long-running watch for Cursor agents — it never exits, so Cursor will not wake you.
Sentinel lines:
| Line prefix | Meaning |
|-------------|---------|
| CROSSTALK_NEW_MESSAGE | New message from the other agent (wait exits after this) |
| CROSSTALK_SESSION_CLOSED | Session was closed (wait exits) |
| CROSSTALK_WAIT_TIMEOUT | Wait timed out — restart wait to keep listening |
Typical workflow
- Backend agent creates a session:
create_session(name: "User auth", description: "OAuth flow API contract", agent_id: "backend") - Frontend agent finds and joins:
search_sessions(query: "User auth")→join_session(session_id, agent_id: "frontend") - Each agent starts a background wait (
get_wait_command) — it exits when the other agent replies, waking Cursor - After handling each message, agents restart wait and use
send_message/read_messages/mark_readas needed - Either agent calls
close_sessionwhen the feature is done
Export transcript
Export a session to markdown:
node /Users/dean.ward/projects/crosstalk/dist/index.js export \
--session-id <uuid> \
--output ./transcript.mdOr positionally:
node /Users/dean.ward/projects/crosstalk/dist/index.js export <session-id> ./transcript.mdAgent IDs and keys
Pass agent_id on every tool call. Use stable, unique identifiers like "frontend" and "backend" so messages are attributed correctly. The CROSSTALK_AGENT_ID env var is a reminder in config — agents should still pass it explicitly in tool calls.
Agent keys prevent id collisions
The first time an agent_id is claimed in a session (via create_session or join_session), the server issues an agent_key — a secret token bound to that id in that session. To act as that id afterwards you must pass the matching key:
- Requires the key:
join_session,send_message,mark_read,get_wait_command/get_watch_command, and thewait/watchCLIs (via--agent-key). Thewaitcommand generated byget_wait_commandalready embeds it. - No key needed: read-only calls (
read_messages,get_session,list_sessions,search_sessions,export).
If another agent tries to use an id that's already claimed without its key, the call is rejected with a message telling it to pick a unique id. This makes id collisions impossible rather than silently merging two agents into one identity.
Keys are only ever returned to the claiming agent — they never appear in summaries, read_messages, or exported transcripts. They're a collision guard for cooperating local agents, not a security boundary (the wait command embeds the key in plain text).
Migration: participants in sessions created before keys existed have no key; the next agent to act as that id adopts it and is issued a key going forward.
