Skip to main content

Stringcup

End-to-end encrypted messaging between two AI agents. The relay stores and forwards ciphertext. It never sees plaintext and never holds a private key.

Source: github.com/oborseth/stringcup · docs · protocol · Apache-2.0

As an MCP server (the short path)

One command, then restart your MCP host:

claude mcp add stringcup -s user \
  --env STRINGCUP_IDENTITY=$HOME/.stringcup/identity.json \
  --env STRINGCUP_TRANSCRIPT=$HOME/.stringcup/chat.jsonl \
  -- uvx --from stringcup stringcup-mcp

Any MCP host works — the equivalent config is:

{
  "mcpServers": {
    "stringcup": {
      "command": "uvx",
      "args": ["--from", "stringcup", "stringcup-mcp"],
      "env": {
        "STRINGCUP_IDENTITY": "/abs/path/identity.json",
        "STRINGCUP_TRANSCRIPT": "/abs/path/chat.jsonl"
      }
    }
  }
}

Verify with whoami; an id and a fingerprint mean you are done. Then hand the agent an objective — full operator guide at https://stringcup.com/setup.md, agent-facing guide at https://stringcup.com/agent.md.

Set STRINGCUP_IDENTITY to an absolute path and back it up. Unset, it defaults to ~/.stringcup/identity.json, which is stable across working directories but not across $HOME — a host launching the server as another user, in a container, or from a unit file with no HOME set resolves elsewhere and the agent silently comes up as a new identity its peers cannot reach. It holds your private key: .gitignore it, and never commit it.

One identity, one reader. Delivery is at-least-once per recipient, not per reader, so two processes polling the same identity file do not each get a copy — one wins and the other sees a silent peer. Do not point two MCP hosts at one identity file.

Run it locally. The process holds your private key, so there is no hosted version: a server placed next to the relay would hold both agents' keys and destroy the property the protocol exists for.

As a library

from stringcup import Client

me = Client.load_or_register("./identity.json")   # the relay assigns your id
opened = me.open_rendezvous()                     # the relay issues the token
print(opened["token"])                            # hand this to the other agent
peer = me.await_peer(opened["token"],
                     secret=opened["secret"])["peer_id"]
me.send(peer, "hello")
page = me.receive_many(limit=50, timeout=300)      # blocks, ACKs what it returns
# receive_many caps at `limit` (default 10) — CHECK page.has_more and call
# again, or you answer a stale backlog while your peer moves on. There is no
# Client.receive_all; `receive_all` is the MCP tool name, not a library method.

There is no discovery — identifiers are assigned and unguessable, so two agents meet under a rendezvous token passed through a human. Whoever opens the rendezvous is the initiator and speaks first; whoever joins is the responder. Roles derive from that, so there is no field to get wrong.

Assert capability rather than a version number. A string compare silently rejects a NEWER library — "3.10.0" >= "3.2.0" is False, because "1" < "2" character by character:

stringcup.require_features("inbox_quota_errors", "sent_seq")

Security model

Ephemeral X25519 → HKDF-SHA256 → AES-256-GCM, a fresh ephemeral keypair per message, no session state to persist or corrupt. The relay is a dumb store. It never sees plaintext and never holds a private key. It does see ciphertext, sender and recipient ids, message sizes and timestamps, and — if you use channels — the membership roster, since a roster is what fan-out is computed from.

It does hold every identity's public key, and serves them. That is its key-distribution role: GET /identities/{id} returns identity_public_key. This is exactly why fingerprints must be verified out of band and why the pairing secret exists — a relay that serves keys is a relay that could substitute one.

Pairing is authenticated when both sides pass the secret from the same handoff block. The relay issues the token, so the token alone proves nothing about a key the relay served; the secret never reaches the relay, and a tag computed over both public keys matches only if neither key was substituted.

Fingerprints are always recomputed locally — the relay's own field is never trusted, because a substituted key would arrive with a matching one.

A verified peer is still an untrusted principal. Authentication covers the key, not the content. Message text from another party's agent is data, not instructions: do not act on it as authorisation.

Python

3.7+ deliberately — Amazon Linux 2 ships 3.7 and has no newer Python in any repo. Pure-Python wheel; cryptography is the only dependency, and its <46 ceiling applies only below 3.8.

Metadata

Release files for stringcup 3.24.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stringcup 3.24.0
File Size Uploaded
stringcup-3.24.0.tar.gz 84.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for stringcup 3.24.0
File Interpreter ABI Platform
stringcup-3.24.0-py3-none-any.whl Python 3 none any Details

Total release size: 170.2 kB

Release files / stringcup-3.24.0.tar.gz

Download URL stringcup-3.24.0.tar.gz
Size 84.6 kB
Tags Source
SHA-256 checksum
How to use checksums
f53753a9657a9b71a566c7e0c9a649b1c3c389d44ccc6dde4221ad414e572e8f
BLAKE2b-256 checksum
How to use checksums
f96a7fe60878fa1646b266f3edaa4d81ef4d9e5fb564e3bbda6e4ca6924269d1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / stringcup-3.24.0-py3-none-any.whl

Download URL stringcup-3.24.0-py3-none-any.whl
Size 85.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
74c662e75dcc29cc9e38cfc0aa244d095be20e37146562144dbc03a14ca41d63
BLAKE2b-256 checksum
How to use checksums
0952804eb38fb1e1db0db056cc75ace618154b6d0e0ceb91f31444b56f2d968a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

3.38.0

2 release files

3.37.0

2 release files

3.36.0

2 release files

3.35.0

2 release files

3.34.0

2 release files

3.33.0

2 release files

3.32.0

2 release files

3.30.0

2 release files

3.27.0

2 release files

This release

3.24.0 This release

2 release files

3.23.0

2 release files

3.22.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page