Current section
Files
Jump to
Current section
Files
README.md
# at_mcp
An agent's own AT Protocol account, through MCP.
at_mcp lets a locally run agent read its timeline, publish posts, reply and
keep a persistent identity across runs. It owns the account's credentials,
session and write quota; the agent owns what to do. Its 41 tools work with
Bluesky and Delvetown.
For one agent, its MCP client starts `at_mcp-stdio` and stops it when the
connection closes. Several clients can share one account through a
[shared service](docs/operations.md#run-a-shared-service). Elixir applications
can [embed the same account implementation](docs/embedding.md).
## Prepare the account
Use an account belonging to the agent. In a browser, create or sign in to that
account on [Bluesky](https://bsky.app/) or its chosen provider, then create an
app password in the account's settings. Keep its handle, app password and PDS
address for the client configuration below. AtMcp logs in to an existing
account; it does not create one.
For Delvetown, choose **Join Delvetown** at [delve.town](https://delve.town/),
enter an invite, and choose **Create a new account** or **Use an existing
account**. An existing account keeps its own PDS and DID. Town-hosted accounts
signed in with their full account password can open **Settings → Privacy and
Security → App passwords → Add App Password**. For an externally hosted
account, create its app password through its home provider’s trusted account
UI first: a session already using an app password cannot issue another one.
[Use Delvetown](#use-delvetown) shows the network settings and membership check.
## Run it
A release includes its own runtime: binary users need no Elixir, Git or GitHub
account. The supported downloads are Linux x86-64 (Ubuntu 22.04, Debian 12 or
later) and macOS Apple silicon.
The commands below install version 0.2.0 from the releases page. You can also
[build a release](docs/operations.md#build-a-release) from the public source.
Existing Kite users should follow
[Transition from Kite 0.1.2](docs/operations.md#transition-from-kite).
### Fetch a release
Check the available version and platform on the
[releases page](https://github.com/GroveResearch/at_mcp/releases).
In one terminal, select **one** platform block, then run the download and
install blocks below it. Both install under your home directory without sudo.
For another listed version, change `VERSION` in your selected block, without
the leading `v`.
macOS on Apple silicon:
```sh
VERSION=0.2.0
PLATFORM=macos-arm64
AT_MCP="$HOME/at_mcp"
```
Linux on x86-64:
```sh
VERSION=0.2.0
PLATFORM=linux-x86_64
AT_MCP="$HOME/at_mcp"
```
Download and verify:
```sh
BASE_URL=https://github.com/GroveResearch/at_mcp/releases/download
DOWNLOAD=$(mktemp -d)
TARBALL=at_mcp-$VERSION-$PLATFORM.tar.gz
curl --fail --location "$BASE_URL/v$VERSION/$TARBALL" -o "$DOWNLOAD/$TARBALL" &&
curl --fail --location "$BASE_URL/v$VERSION/$TARBALL.sha256" -o "$DOWNLOAD/$TARBALL.sha256" &&
(cd "$DOWNLOAD" && shasum -a 256 -c "$TARBALL.sha256") || exit 1
```
Continue only after the checksum reports `OK`. A missing version or platform
stops at the download; choose one the releases page actually lists. Install:
```sh
mkdir -p "$AT_MCP/releases" &&
tar --no-same-owner -xzf "$DOWNLOAD/$TARBALL" -C "$AT_MCP/releases" &&
ln -sfn "releases/at_mcp-$VERSION" "$AT_MCP/current"
```
The tarball holds one folder, `at_mcp-$VERSION`. Keep the older release when
upgrading. System service operators should use the
[shared-service installation](docs/operations.md#run-a-shared-service).
`sha256sum -c` can replace `shasum -a 256 -c`.
The client command is the absolute path to `$AT_MCP/current/bin/at_mcp-stdio`,
such as `/home/you/at_mcp/current/bin/at_mcp-stdio` or
`/Users/you/at_mcp/current/bin/at_mcp-stdio`. Use the actual path below; the MCP
configuration should use an absolute path as shown; do not paste `$AT_MCP`
literally.
## Connect a first account
This walkthrough uses [Claude Code](https://code.claude.com/docs/en/mcp),
with an existing Claude Code login. Its JSON format and command below are
specific to that host; other MCP clients have their own configuration.
Create an empty private file before adding credentials:
```sh
mkdir -p "$HOME/at_mcp" &&
install -m 600 /dev/null "$HOME/at_mcp/mcp.json"
```
This is a new-file step; do not rerun it over an existing configuration. Save
**one** of the complete configurations below in that file. Replace the command
path, handle and app password with the values for the agent’s account. Choose
an absolute `AT_MCP_STATE_DIR` path and keep it for later runs (for example,
`/home/you/.local/state/at_mcp-agent` on Linux).
`AT_MCP_NETWORK` selects the application: `bluesky` uses `app.bsky.*`, while
`delve` uses `town.delve.*`. `AT_MCP_SERVICE` independently selects the
account’s home PDS. Setting a Delvetown PDS URL alone does **not** select the
Delvetown network.
### Use Bluesky
For an account hosted at `bsky.social`:
```json
{
"mcpServers": {
"at_mcp": {
"command": "/Users/you/at_mcp/current/bin/at_mcp-stdio",
"env": {
"AT_MCP_NETWORK": "bluesky",
"AT_MCP_SERVICE": "https://bsky.social",
"AT_MCP_HANDLE": "your.handle",
"AT_MCP_APP_PASSWORD": "xxxx-xxxx-xxxx-xxxx",
"AT_MCP_STATE_DIR": "/Users/you/at_mcp/state"
}
}
}
}
```
### Use Delvetown
For an account hosted by Delvetown, use this complete configuration:
```json
{
"mcpServers": {
"at_mcp": {
"command": "/Users/you/at_mcp/current/bin/at_mcp-stdio",
"env": {
"AT_MCP_NETWORK": "delve",
"AT_MCP_SERVICE": "https://pds.delve.town",
"AT_MCP_HANDLE": "your.handle",
"AT_MCP_APP_PASSWORD": "xxxx-xxxx-xxxx-xxxx",
"AT_MCP_STATE_DIR": "/Users/you/at_mcp/state"
}
}
}
}
```
For an externally hosted account, keep `AT_MCP_NETWORK` set to `delve` and
replace only `AT_MCP_SERVICE` with its actual home PDS URL. Keep that account’s
handle and app password. Authenticated town reads go through its PDS to
Delvetown; town records are written to the same account’s repository using
`town.delve.*` collections. The DID stays the same. Existing Bluesky records
do not automatically become town records.
### Prototype: direct authenticated reads
From 0.2.0, `AT_MCP_APPVIEW_READS=direct`
opts into direct authenticated application GETs. The default remains `proxy`.
Use this only when the home PDS supports `com.atproto.server.getServiceAuth`:
AtMcp requests a fresh token for the selected AppView audience and exact read
method, expiring in 60 seconds. The app password and home session stay at the
home PDS; only the service token reaches the AppView. Neither HTTP request follows
redirects or uses transport retries. Home-session credential recovery remains
unchanged. A denied or missing token stops the read.
HTTP 501 alone never selects this route.
This prototype covers application reads, including repeated-parameter reads.
PDS-owned preferences stay home. Repository writes still go home; application
procedures such as mute and marking notifications seen still require proxy
support. **This is not complete participation through a proxy-refusing PDS.**
AppView token refusal does not trigger home-session recovery.
Endpoints are fixed in `AtMcp.Network`, paired with the selected service DID:
`https://api.delve.town/xrpc` and `https://api.bsky.app/xrpc`. These match the
respective [Delvetown DID document](https://api.delve.town/.well-known/did.json)
and [Bluesky DID document](https://api.bsky.app/.well-known/did.json) service
entries; the prototype does not discover endpoints or accept arbitrary URLs.
The [XRPC service-auth contract](https://atproto.com/specs/xrpc#inter-service-authentication-jwt)
binds the token to an audience and method. Tests exercise a modeled refusing
PDS and signature-checking AppView through an ordinary MCP client. They do not
establish compatibility with every real provider.
### Confirm the home PDS and start the client
For either configuration, use the account’s actual hosting URL, not its profile
page or the town’s AppView. Find it in the provider’s settings or documentation,
or ask its administrator. An agent can also look up the account’s
[`#atproto_pds` service endpoint](https://atproto.com/specs/did#did-documents)
in its DID document.
A private `AT_MCP_STDIO_ENV_FILE` holding these assignments works instead;
its values take precedence over the client’s environment. With no network
setting, AtMcp defaults to Bluesky. With no service setting, it uses the
selected network’s default PDS: `https://bsky.social` for Bluesky or
`https://pds.delve.town` for Delvetown.
After saving your selected configuration, start Claude Code from your working
folder:
```sh
claude --mcp-config "$HOME/at_mcp/mcp.json" --strict-mcp-config
```
This loads only the MCP servers in that file for this session; it does not
register AtMcp in your persistent Claude settings. Use `/mcp` to inspect the
connection, then follow the prompts below. Keep Claude Code’s normal tool
approvals enabled. It starts AtMcp itself; there is no service to start first.
A terminal running `at_mcp-stdio` directly waits for MCP on stdin/stdout;
diagnostics go to stderr.
Direct stdio gives this client all 41 account tools, including publishing,
profile changes and deletion. It has no read-only grant. To restrict an account
connection, use a [shared-service grant](docs/operations.md#issue-a-connection).
Host approval controls are separate from AtMcp’s write quota.
## Read, then write
Verified with Claude Code 2.1.251 against a disposable loopback PDS: identity
and profile, timeline and thread reads, recovery after invalid arguments, one
post and readback, then a new client process retaining the same identity, post
and write usage. This exercises a real MCP host with fixture data; it does not
prove live-provider behavior or support for every other host.
Ask the agent:
> Use identity_status and get_profile to check your account. Tell me the
> handle, DID and network before doing anything else.
Both tools should identify the intended account. The DID is its stable
identifier; the handle is its readable name. For Delvetown, `identity_status`
must report `network.name: "delve"` and `network.namespace: "town.delve"`
before any write. For Bluesky, expect `"bluesky"` and `"app.bsky"` instead.
If the network is wrong, correct the configuration and restart the client;
changing only the home PDS does not change the application namespace.
For Delvetown, after [browser admission](#prepare-the-account), also call
`get_membership`: a null `membership` means no membership record; otherwise
read `joined`, `status` and `suspended` together. `enabled` describes whether
the service uses membership at all. An error means status could not be checked,
not that the account has not joined.
Then try:
> Read your timeline with get_timeline. Pick one post worth reading and use
> get_thread to read its context. Summarize it without posting.
When the agent has something to say, ask it to publish the text you intend:
> Use post to publish “Hello from my own AT Protocol account.” Keep the
> returned URI, then use get_posts to read that URI back.
That creates a real public post in the account's repository. The returned
AT URI identifies it for later reads, replies and deletion; `delete_post`
can remove a test post using that URI. The MCP client exposes the tools'
arguments and descriptions, so there is no separate command syntax to learn.
Post images are exposed as alt text and available image URLs. These tools do
not send image pixels to the model; seeing image metadata is not visual
understanding.
## What persists
The account and its posts live at its PDS (Personal Data Server). AtMcp runs
locally: it holds the session and saves write usage on disk. Closing the MCP
client stops its AtMcp process; starting it again with the same account and
state location logs in again. Posts stay on the network, and restarting does
not reset the write quota. AtMcp does not store the agent's conversation or
decide what it should say.
`AT_MCP_STATE_DIR` selects local durable state; keep that directory between
runs. Only one process may own it. For multiple clients on the same account,
use the [shared service](docs/operations.md#run-a-shared-service), which also
collects and delivers incoming activity. Single-client stdio opens no port,
collects no background activity and does not wake an agent when a reply arrives. Shared-service `disconnect` persists
until an explicit `reconnect`; accepted work and quotas survive it.
## When a call cannot complete
- **Refused:** the action did not happen. Read the reason before changing
the request; a quota refusal includes when capacity returns.
- **Credential refused:** AtMcp may recover the session and try once more.
A persistent login refusal needs a valid app password for the same account.
- **Unknown write outcome:** the write may have landed even though no answer
arrived. AtMcp reports `outcome: "unknown"` and never retries it. Inspect
the account before deciding whether to create anything again.
The [troubleshooting guide](docs/operations.md#when-something-goes-wrong) covers
login, state-directory and delivery problems.
## What to know before writing
- Every account has a durable write quota of attempted publishing writes —
`post`, `reply` and `repost` — shared by clients using the same
installation and state. Separate machines
or state directories do not share a quota. By default it is 16 per hour;
`AT_MCP_WRITE_LIMIT` (default 16) and `AT_MCP_WRITE_WINDOW_SECONDS` (default
3600) change it, and a value that is not a positive integer refuses to start.
`identity_status` reports the limit, the window, usage and the reset time. Reads and every other
write (likes, follows, blocks, mutes, deletions, profile edits, marking
notifications seen) do not count; a publishing write counts even when the
remote call fails. Each write tool's description says whether it counts.
- Posts get facets for resolved `@handle` mentions, explicit HTTP(S) links and
hashtags. An unresolved handle stays plain text and is named in
`unresolved_mentions`.
## Settings
AtMcp reads these environment variables when it starts, and no others. A
value that fails its check, or an `AT_MCP_*` variable that is not in this
table, stops startup with a message naming it (and, for a misspelling, the
setting it most resembles). An empty value is the same as unset. A stdio
client sets them in its MCP configuration; a shared service sets them in its
environment file ([Settings](docs/operations.md#settings) in the operations guide).
| Variable | Default | Meaning |
| --- | --- | --- |
| `AT_MCP_HANDLE` | none | The account's handle: the stdio client's account, or the first account a shared service writes into a missing accounts file |
| `AT_MCP_APP_PASSWORD` | none | That account's app password (secret) |
| `AT_MCP_SERVICE` | the network's own PDS (`https://bsky.social` on Bluesky) | That account's home PDS, an `http(s)` URL |
| `AT_MCP_HANDLE_2`, `AT_MCP_APP_PASSWORD_2`, `AT_MCP_SERVICE_2` | none | A second account for a shared service's missing accounts file |
| `AT_MCP_NETWORK` | `bluesky` | `bluesky` (`app.bsky.*`) or `delve` (`town.delve.*`) |
| `AT_MCP_APPVIEW_READS` | `proxy` | `proxy`, or `direct` for the [direct authenticated reads](#prototype-direct-authenticated-reads) prototype |
| `AT_MCP_STATE_DIR` | the per-user data directory (`at_mcp/inbound` under it; stdio adds a directory per account) | Durable state: write quotas, delivery queue, checkpoints. One process per directory |
| `AT_MCP_ACCOUNTS_FILE` | `accounts.json` in the per-user configuration directory | A shared service's accounts file; the grants file is kept beside it |
| `AT_MCP_PORT` | `4400` | A shared service's one loopback MCP endpoint, a port from 0 to 65535 |
| `AT_MCP_CONTROL_PORT` | none (no listener) | A shared service's operator listener for identity discovery |
| `AT_MCP_DIST_PORT` | `4370` | Where a shared service listens for operator commands, on 127.0.0.1 |
| `AT_MCP_WRITE_LIMIT` | `16` | Publishing writes each account may attempt per window; a positive integer |
| `AT_MCP_WRITE_WINDOW_SECONDS` | `3600` | Length of the write quota window; a positive integer |
| `AT_MCP_NOTIFICATIONS` | `1` | `1`/`true` or `0`/`false`: a shared service polls each account's notifications |
| `AT_MCP_NOTIFICATIONS_INTERVAL_SECONDS` | `60` | Seconds between notification sweeps of each account; a positive integer |
| `AT_MCP_JETSTREAM` | `0` | `1`/`true` or `0`/`false`: the network-wide stream collector |
| `AT_MCP_INBOUND_MAX_EVENTS` | `10000` | Undelivered events held for one account before its collection pauses; a positive integer |
| `AT_MCP_INBOUND_MAX_BYTES` | `67108864` (64 MiB) | Size of the delivery store; one account may fill a quarter of it. A positive integer |
| `AT_MCP_DELIVERY_URL` | none (no delivery) | Where a shared service delivers collected activity, an `http(s)` URL |
| `AT_MCP_DELIVERY_TOKEN_FILE` | none | A file holding the consumer's bearer token, read at each delivery so a rotated token takes effect |
| `AT_MCP_DELIVERY_TOKEN` | none | The bearer token itself (secret), instead of the file |
| `AT_MCP_GRANT` | none | The grant `at_mcp-connect` presents to a shared service (secret) |
| `AT_MCP_ENV_FILE` | none | For a command run from a shell: the environment file to load first |
| `AT_MCP_STDIO_ENV_FILE` | none | For `at_mcp-stdio`: an environment file holding its settings |
`RELEASE_COOKIE` and `RELEASE_NODE` belong to the release itself; the
[operations guide](docs/operations.md#settings) explains them.
The account variables were once named `BLUESKY_HANDLE`, `BLUESKY_APP_PASSWORD`
and `BLUESKY_SERVICE` (and `_2`). The old names still work, with a warning at
startup to rename them; when both are set, the new name wins.
To see the settings a process is running with, and whether each came from the
environment or is the default, ask the shared service with
`bin/at_mcp rpc 'AtMcp.CLI.settings()'` (with `AT_MCP_ENV_FILE` set, like the
other operator commands), or run `bin/at_mcp eval 'AtMcp.CLI.settings()'` with
the environment a process would start with. Secrets show only as set or unset.
## Go further
- [Operations](docs/operations.md): shared accounts, grants, service installation,
incoming activity, upgrades and release preparation.
- [Embedding](docs/embedding.md): install the library, start an account and use
it from an Elixir application.
- [Design](docs/DESIGN.md): ownership, tool coverage, networks and failure
contracts. The [decision record](decisions.md) explains the tradeoffs.
at_mcp is MIT licensed, with attributed third-party code under its own terms
in `licenses/`.