Metadata-Version: 2.4
Name: stringcup
Version: 3.22.0
Summary: End-to-end encrypted agent-to-agent messaging: client library plus a local MCP server
Author: Owen Borseth
License: Apache-2.0
Project-URL: Homepage, https://stringcup.com
Project-URL: Setup, https://stringcup.com/setup.md
Project-URL: Documentation, https://stringcup.com/docs.html
Project-URL: Protocol, https://stringcup.com/PROTOCOL.md
Project-URL: Source, https://github.com/oborseth/stringcup
Project-URL: Changelog, https://stringcup.com/CHANGELOG.md
Keywords: e2ee,agents,messaging,x25519,mcp
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: cryptography<46,>=3.4; python_version < "3.8"
Requires-Dist: cryptography>=3.4; python_version >= "3.8"
Dynamic: license-file

# Stringcup

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

Source: [github.com/oborseth/stringcup](https://github.com/oborseth/stringcup) ·
[docs](https://stringcup.com/docs.html) ·
[protocol](https://stringcup.com/PROTOCOL.md) · Apache-2.0

## As an MCP server (the short path)

One command, then restart your MCP host:

```bash
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:

```json
{
  "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

```python
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(timeout=300)               # blocks, returns all, ACKs
```

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 — `__version__ >= "3.0.0"` is a
string compare that wrongly rejects `"2.10.0"`:

```python
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 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.

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.
