A dependency-free MCP server for AI-context group messaging
Project description
Crosstalk MCP Server
A dependency-free Model Context Protocol (MCP) server for exchanging messages between separate AI contexts.
Crosstalk stores each group in its own SQLite database, making it useful for coordinating agents, sessions, or tools that share a filesystem.
Trust boundary: Crosstalk is for trusted collaborators only. Its SQLite data is plaintext, and
context_idvalues are caller-provided rather than authenticated. See Security before using it with sensitive or untrusted workloads.
Highlights
- Dependency-free Python 3.9+ server using MCP JSON-RPC over standard input/output
- Persistent, shared group message history backed by SQLite
- Per-context unread tracking, message search, cursors, and full-history retrieval
- Targeted wakeup hints through explicit recipients,
@context_id,@name, or@allmentions - MCP resource subscriptions for group changes and targeted wakeup notifications
Quick start
Install from PyPI:
python3 -m pip install --user --upgrade crosstalk-mcp-server
To install from a checkout instead:
python3 -m pip install --user --upgrade .
Confirm that the command is available:
command -v crosstalk-mcp
Then add the server to your MCP client. For Codex, put this in ~/.codex/config.toml:
[mcp_servers.crosstalk]
command = "crosstalk-mcp"
[mcp_servers.crosstalk.env]
# Omit this table to use ~/.cache/crosstalk.
CROSSTALK_GROUPS_DIR = "/absolute/path/to/shared/crosstalk-groups"
# Optional; defaults to 0.5 seconds.
CROSSTALK_POLL_INTERVAL_SECONDS = "0.5"
Restart Codex after changing its configuration. To enable Crosstalk for one trusted repository only, place the same configuration in that repository’s .codex/config.toml.
For JSON-based MCP clients, use examples/mcp.example.json. A standalone Codex configuration is available at examples/mcp.example.toml.
Use crosstalk-mcp --help to see all commands and observer flags, and crosstalk-mcp --version to confirm the installed version. The observer’s detailed option reference is available with crosstalk-mcp observe --help.
Installation
macOS and Linux
The portable installation command is:
python3 -m pip install --user --upgrade .
Alternatively, make install installs the package and, for zsh, bash, or fish, adds Python’s user-script directory to the relevant shell startup file when necessary. It requires rsync. Restart your shell and MCP client after installing.
If crosstalk-mcp is not on PATH, use absolute paths to run the checkout directly:
[mcp_servers.crosstalk]
command = "/absolute/path/to/python3"
args = ["/absolute/path/to/crosstalk/src/main.py"]
Windows PowerShell
py -m pip install --user --upgrade .
Get-Command crosstalk-mcp
If PowerShell cannot find the command after opening a new window, add Python’s user scripts directory to your user Path:
$scriptsDir = "$(py -m site --user-base)\Scripts"
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if (($userPath -split ';') -notcontains $scriptsDir) {
[Environment]::SetEnvironmentVariable("Path", "$userPath;$scriptsDir", "User")
}
To run directly from a checkout instead, configure absolute Windows paths:
[mcp_servers.crosstalk]
command = 'C:\absolute\path\to\python.exe'
args = ['C:\absolute\path\to\crosstalk\src\main.py']
Configuration
| Variable | Default | Purpose |
|---|---|---|
CROSSTALK_GROUPS_DIR |
~/.cache/crosstalk |
Shared directory that holds one <group_id>.sqlite3 database per group. |
CROSSTALK_POLL_INTERVAL_SECONDS |
0.5 |
Subscription polling interval. Use a larger value such as 2 to reduce database activity. Invalid, non-positive, and non-finite values use the default. |
CROSSTALK_OBSERVABILITY_RETENTION_DAYS |
Unset (auditing disabled) | Enable audit history with inf for no automatic expiry or a positive number of days for bounded retention. |
All participants must use the same accessible CROSSTALK_GROUPS_DIR. The default works for server processes under the same user account; configure a shared writable directory for containers, separate users, or machines.
The server creates its default directory automatically. Each database records its schema version for inspection; automatic migration of old layouts is intentionally not supported.
Observer dashboard
Start the local, read-only dashboard separately from the MCP server:
crosstalk-mcp observe [--silent] [--port PORT] [--poll-interval SECONDS] [--groups-dir PATH]
The dashboard binds only to 127.0.0.1; it is not a network service or an access-control boundary. Anyone who can access the same local account, loopback endpoint, or groups directory is already within this trusted-local boundary. Do not expose the observer through a proxy or bind it to another interface.
By default it opens the displayed URL in your browser. Use --silent to suppress only that browser launch; the URL still prints. In headless environments or if the browser cannot be opened, leave the process running and visit the printed URL from the same machine.
Without --port, Crosstalk first tries 127.0.0.1:8787; if it is busy, it falls back to an OS-selected loopback port and prints the actual URL. --port PORT requests that exact port and fails if it is unavailable. --poll-interval controls observer refresh timing only and defaults to 0.5 seconds.
The observer resolves the groups directory in this order:
--groups-dir PATHCROSSTALK_GROUPS_DIR~/.cache/crosstalk
Unlike the MCP server, the observer does not create a missing directory. It starts empty and begins showing groups if the directory appears later.
UI guide
The observer is read-only for group data. The sidebar provides four views; screenshots use sample local data.
Overview
The operational cockpit combines current group health, a cross-group live-activity stream, and MCP reliability. Live activity shows the 500 newest combined events and distinguishes message content, pending wakeups, successful tool calls, and tool errors. Reliability metrics use all audit rows and list the 100 newest failures. Use View groups or View analytics to move from a summary to the relevant detail.
Chats
Select a group to browse its chronological conversation. The right-hand panel lists members and their unread counts, plus wakeup creation, acknowledgement, and notification state. Viewing this dashboard does not mark messages as read or acknowledge wakeups.
Tool analytics
When audit history is enabled, filter calls by time range, tool, outcome, caller, or group. The view summarizes total calls, latency, and error rate, then shows tool/outcome distributions, activity over time, caller volume, and the matching event log.
Storage
The Storage view reports the audit database size, row count, retention setting, activation time, and reclaimable space. Reclaim free space performs bounded maintenance without deleting retained audit rows. Delete audit history permanently removes audit rows only, after confirmation; it never changes group chat data.
Overview group health
The Overview page labels each group according to its current message and wakeup state: Needs attention means it has one or more unacknowledged wakeups; Unread means it has unread messages but no pending wakeup; and Healthy means neither condition applies. A wakeup is acknowledged when its target retrieves the associated unread message (for example, through get_unread_messages).
Audit history and privacy
Auditing is opt-in. Set one of these variables in the environment that starts crosstalk-mcp:
# Keep audit history without an automatic retention cleanup.
CROSSTALK_OBSERVABILITY_RETENTION_DAYS=inf
# Keep audit history for a bounded number of days.
CROSSTALK_OBSERVABILITY_RETENTION_DAYS=30
When unset, auditing is disabled and no observability database is created. When enabled, audit history is stored at <groups-directory>/observability.sqlite3, separate from the grp_*.sqlite3 chat databases. inf is the unlimited-retention option; a positive integer enables a once-per-day, bounded cleanup of expired rows. The MCP writer records its effective retention setting in audit metadata, so the Storage view reports the writer’s setting even when the dashboard was started from a different environment. Invalid values fail MCP startup rather than changing retention silently.
Analytics begin when auditing is enabled; earlier MCP calls cannot be reconstructed. The observer can show chats while auditing is disabled, but Tool Analytics and audit-storage information remain unavailable until new audit rows exist.
Audit rows contain operational facts only: time, tool, outcome, duration, applicable group/context identity, and a small allowlisted result summary. They never store message bodies, search queries, metadata values, arbitrary inputs, recipient IDs, raw errors, or stack traces. The summary is capped at 2 KiB; it may include counts, a created-group ID, changed metadata field names, a completion flag, or sent-message ID/priority/routing/wakeup-target count.
Failure categories
For failed tool calls, Crosstalk stores a safe category rather than the raw error text:
- Validation Error: invalid or incomplete tool input.
- Not Found Error: the requested group or item does not exist.
- Authorization Error: an operation reserved for the group creator was attempted by another context.
- Database Busy Error: SQLite reported a lock or busy condition.
- Internal Error: an unexpected failure that does not fit another category.
Retention cleanup makes deleted pages reusable but does not compact the file. The Storage view’s Reclaim free space action performs bounded incremental vacuum work on observability.sqlite3 only; it does not delete retained audit history or touch any group database. Delete audit history is a separate, explicitly confirmed permanent action: it removes audit rows only, leaves group data and audit metadata intact, and does not reclaim free pages automatically.
Browser assets and audit schema
The dashboard loads pinned HTMX and Alpine.js URLs from their CDNs with Subresource Integrity hashes. They are not bundled with Crosstalk, and the dashboard uses no service worker. Normal browser caching applies; an uncached offline browser may not load those libraries. The ordinary first asset request reveals normal browser/network metadata to the selected CDN, but no chats, audit rows, or analytics data are sent there.
observability.sqlite3 is a public local read contract for analytics tooling. The v1 schema is versioned with SQLite user_version; future changes are additive where possible and never automatically alter group databases. Older observers should ignore unknown optional fields and show an unavailable metric when a required field is absent, rather than attempting a destructive migration.
How it works
Create a group, then have every participating context join before it sends or retrieves messages:
Every MCP tools/call request must include a non-empty caller_name. It is recorded as the audit caller identity and is separate from a group's metadata name or a message sender's name.
create_group(context_id="developer-1", name="Cache redesign", description="Coordinate the cache work")
→ {"group_id": "grp_<id>", "name": "Cache redesign", ...}
join_group(group_id="grp_<id>", context_id="developer-1", name="developer")
join_group(group_id="grp_<id>", context_id="architect-1", name="architect")
send_message(
group_id="grp_<id>",
context_id="developer-1",
name="developer",
message="@architect Please review the cache invalidation design.",
priority="high"
)
→ {"message_id": 1, "routing_reason": "mentioned", "wakeup_targets": ["architect-1"], ...}
get_unread_messages(group_id="grp_<id>", context_id="architect-1", name="architect")
context_id is the stable, unique identity for an AI or session; name is its human-readable role or label. For Codex, use the thread ID exposed in CODEX_THREAD_ID as context_id, so the identity remains stable when the thread resumes. Do not use a display label such as architect as the ID.
Creating a group does not join its creator. join_group is idempotent and should be called whenever a context connects or reconnects to a server process. Leaving a group removes the membership, unread state, and pending wakeups for that context.
On a context’s first join, every message already in the group is initially unread for that context. Retrieve them with get_unread_messages to acknowledge that history and any matching wakeups. Rejoining an existing membership preserves its unread state while updating the display name.
Tools
| Tool | Description |
|---|---|
create_group(context_id, name?, description?) |
Create a group. The creator owns metadata updates and deletion. |
list_groups / get_group_metadata(group_id) |
Discover groups and read their metadata. |
update_group_metadata(group_id, context_id, name?, description?) |
Update metadata as the creator. |
get_users(group_id) |
List current members. |
join_group(group_id, context_id, name) / leave_group(...) |
Manage membership and wakeup registration. |
send_message(group_id, context_id, name, message, priority?, wake_context_ids?) |
Send a message and optionally route a wakeup hint. |
get_unread_messages(...) |
Retrieve messages not previously retrieved by the caller. |
get_all_messages(...) / get_latest_messages(...) |
Retrieve full history or messages after the caller’s latest sent message. |
get_messages_after(..., after_message_id) |
Retrieve messages after a known cursor. |
search_messages(..., query) |
Case-insensitive text search; % and _ use SQL LIKE wildcard semantics. |
delete_group(group_id, context_id) |
Permanently delete a group and its history as the creator. |
All message retrieval tools mark the messages they return as read for the caller. A caller’s own sent messages are never unread for that caller.
Routing and subscriptions
When a message is sent, Crosstalk selects wakeup recipients in this order:
- Valid
wake_context_ids @context_id,@name, or@allmentions- Every other joined context for
urgentmessages or when no target is clear
Mentions must end at whitespace, punctuation (., ,, !, ?, ;, :), or the end of the message. Unknown explicit recipient IDs are ignored; if none are valid, normal routing continues. Sent messages record their mentions, selected wakeup targets, and routing reason.
Every group is available as crosstalk://groups/<group_id>/messages. MCP clients can subscribe to this resource and, after a change notification, call get_unread_messages to determine whether anything is relevant.
Joining also registers a targeted wakeup resource for the current MCP session. The join_group response includes its URI:
crosstalk://groups/<group_id>/wakeups/<context_id>
Messages create one persistent wake event per recipient. A returned message acknowledges its matching wake event; unacknowledged events are reminded after five minutes, and acknowledged events are retained for seven days. Unsubscribing a targeted wakeup resource leaves that context from the group.
Notifications are delivery hints, not guaranteed model wake-ups. Crosstalk can notify an active, subscribed client, but it cannot revive an ended thread. Keep a target thread active when it needs to receive wakeups.
Reliability and data lifecycle
When SQLite is briefly busy, a tool call retries for up to three seconds before returning a retryable error. To remove a group’s persisted history, delete its <group_id>.sqlite3 file and any SQLite -wal or -shm sidecar files—or use delete_group as the group creator. Deletion is irreversible.
Security
Crosstalk is not an access-control system or a multi-tenant service. Group data is stored in plaintext SQLite files, and caller-provided context_id values determine creator-only metadata updates and deletion. Only use a shared directory that every participant is authorized to read and write, and do not use Crosstalk to exchange secrets with untrusted parties.
Troubleshooting
- The client cannot find
crosstalk-mcp. Open a new shell and runcommand -v crosstalk-mcp(orGet-Command crosstalk-mcpin PowerShell). If it resolves but the client still cannot start it, setcommandto its absolute path. - Participants cannot see each other’s messages. Check that every server process has the same
CROSSTALK_GROUPS_DIRand that the directory is readable and writable by each participant. - Wakeups do not start an AI turn. The MCP client or external agent runner controls whether an incoming notification starts work. Crosstalk only emits the notification.
Development
Run the regression suite:
python3 -m unittest tests/test_main.py tests/test_mcp.py tests/test_observe.py
Releases
Releases are managed from main with Release Please. Use Conventional Commits: fix: creates a patch release, feat: creates a minor release, and feat!: or a BREAKING CHANGE: footer creates a major release. Other prefixes, such as docs:, chore:, and test:, do not create a release by themselves.
Release Please collects eligible commits into a release pull request. Merging that pull request updates VERSION, generates release notes, creates the matching vX.Y.Z tag, and publishes the package to PyPI after the release checks pass.
License
Distributed under the MIT License.
Contributing
See CONTRIBUTING.md for local setup, testing, pull-request, and release conventions.
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 crosstalk_mcp_server-1.1.10.tar.gz.
File metadata
- Download URL: crosstalk_mcp_server-1.1.10.tar.gz
- Upload date:
- Size: 93.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7cb4407d564f0b7f8a4b0b9b854cbf925cc0c5832d57b302d04d2772f348a63
|
|
| MD5 |
afcce9bbf56fb845a0904b00adfc3a8d
|
|
| BLAKE2b-256 |
862728079fb9761e03bd0ad19f0c439ab07d215c92b808df3d21aa8acf381fd5
|
Provenance
The following attestation bundles were made for crosstalk_mcp_server-1.1.10.tar.gz:
Publisher:
pypi-publish.yml on CantC0unt/crosstalk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
crosstalk_mcp_server-1.1.10.tar.gz -
Subject digest:
e7cb4407d564f0b7f8a4b0b9b854cbf925cc0c5832d57b302d04d2772f348a63 - Sigstore transparency entry: 2256818189
- Sigstore integration time:
-
Permalink:
CantC0unt/crosstalk@7836780fbb56f53d4f72bf051b0f7b3d0dafca93 -
Branch / Tag:
refs/tags/v1.1.10 - Owner: https://github.com/CantC0unt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@7836780fbb56f53d4f72bf051b0f7b3d0dafca93 -
Trigger Event:
push
-
Statement type:
File details
Details for the file crosstalk_mcp_server-1.1.10-py3-none-any.whl.
File metadata
- Download URL: crosstalk_mcp_server-1.1.10-py3-none-any.whl
- Upload date:
- Size: 72.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a615b529b8c159aef36b1d084e6f87e5607757471ac50bb854acb9330fe19a09
|
|
| MD5 |
775e075d426d5789d661ae8d75a58264
|
|
| BLAKE2b-256 |
7e1deaec5c2262880ebee014133e40e5057d38d119195bfd36d943182612cd20
|
Provenance
The following attestation bundles were made for crosstalk_mcp_server-1.1.10-py3-none-any.whl:
Publisher:
pypi-publish.yml on CantC0unt/crosstalk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
crosstalk_mcp_server-1.1.10-py3-none-any.whl -
Subject digest:
a615b529b8c159aef36b1d084e6f87e5607757471ac50bb854acb9330fe19a09 - Sigstore transparency entry: 2256818191
- Sigstore integration time:
-
Permalink:
CantC0unt/crosstalk@7836780fbb56f53d4f72bf051b0f7b3d0dafca93 -
Branch / Tag:
refs/tags/v1.1.10 - Owner: https://github.com/CantC0unt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@7836780fbb56f53d4f72bf051b0f7b3d0dafca93 -
Trigger Event:
push
-
Statement type: