Connect your agent
Point your agent at a running clone — over the Slack Web API or the MCP server.
A clone is only useful once your agent is acting against it. There are two surfaces to connect to, and you can use either or both:
| Surface | What it is | Use it when |
|---|---|---|
| HTTP API | The clone's HTTP API at the endpoint spin printed. For Slack this is the real Slack Web API — RPC methods, the {ok} envelope, opaque bearer tokens. | Your agent already speaks the product's API, or you want full control. The default, fully-wired path. |
| MCP server | A Model Context Protocol server that exposes the clone as a set of tools. | Your agent is an MCP client (Claude Desktop, an SDK agent) and you want tool-shaped access. |
This guide assumes you have a running, seeded clone from the
Quickstart. Everything below uses the Slack template, slack-a1b2,
and port 3001 — substitute your own clone id and endpoint.
Option A — the Slack Web API
spin hands you a base URL and two ready-to-use tokens:
✓ slack-a1b2 (api) http://127.0.0.1:3001/api ready in 38.2s
db clone_slack_a1b2 · env -
bot token xoxb-…
user token xoxp-…That base URL is your agent's target, and it's the real Slack Web API: RPC
method routes (POST /api/chat.postMessage), args in the JSON body, and the
{ "ok": true, … } envelope. The clone expects real auth — you have two ways in.
Fastest — the provisioned bot token
Every spin slack auto-provisions a default workspace, admin user, and app, then
prints a bot token (xoxb-…) and a user token (xoxp-…). No signup or
login; send one as a bearer token. Reprint them any time with
hone env tokens <id>.
B=http://127.0.0.1:3001/api
TOKEN=$(hone env tokens slack-a1b2 --json | jq -r .botToken)
# Confirm identity with auth.test (a real Slack method)
curl -s -X POST $B/auth.test -H "authorization: Bearer $TOKEN"
# → { "ok": true, "user_id": "5f3a8b2c-…", "team_id": "9d1e7a4f-…", "user": "admin" }
# (ids are internal UUIDs — a known cosmetic deviation from Slack's `U…`/`T…`)
# List conversations
curl -s -X POST $B/conversations.list -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"types":"public_channel"}'A bot token acts as the app's bot user (messages it posts show is_bot: true);
a user token acts as the admin. App-token scopes are enforced — a method
whose scope the token lacks returns { "ok": false, "error": "missing_scope" } at
HTTP 200, like every Slack-method error (the clone never returns 4xx/5xx on a
method route — the one non-200 is 429 ratelimited).
See Apps and tokens.
Per-user — JWT login
When you want to act as a specific seeded human (e.g. to test multi-user flows), log in for a JWT instead. JWT/web-UI identities are exempt from scope enforcement, and the access token is used exactly like the bearer token above.
- Authenticate
Log in as a seeded user (or sign up your own) to get an access token. The
acme-corpfixture creates six users who all share the passwordpassword123—dana@acme.testis one:TOKEN=$(curl -s -X POST http://127.0.0.1:3001/api/auth/login \ -H 'content-type: application/json' \ -d '{"email":"dana@acme.test","password":"password123"}' \ | jq -r .accessToken)(
/api/auth/*is a clone-operation, so it keeps a plain REST shape rather than the{ ok }envelope.) - Act
Now your agent drives the Web API like any client — list channels, then post a message. Take a channel id from
conversations.list(the clone uses internal UUID ids — a known cosmetic deviation from Slack'sC…that doesn't change call signatures), andchat.postMessagetakes its args in the body:B=http://127.0.0.1:3001/api curl -s -X POST $B/conversations.list -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' -d '{"types":"public_channel"}' curl -s -X POST $B/chat.postMessage -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"channel":"<channel-id from conversations.list>","text":"hello from my agent"}' - Point your agent at the base URL
Hand
http://127.0.0.1:3001/apito your agent as its API base, with the token in its auth header. Every action it takes lands in the clone's own database — which is exactly what you'll read back to score the run.
Option B — the MCP server
The Slack template ships an MCP server (apps/slack-clone/mcp-server) that
exposes the clone as the reference Slack MCP tools — handy for MCP-native agents
that prefer tools over raw HTTP. It speaks stdio, connects to the clone's Postgres
database, and is bound to a single app token so every tool acts strictly as
that identity, inside that workspace, limited to that token's scopes.
The tools it exposes (verbatim names from the reference Slack MCP server):
| Tool | Does |
|---|---|
slack_list_channels | List channels in the workspace. |
slack_post_message | Post a message to a channel. |
slack_reply_to_thread | Reply in a thread. |
slack_add_reaction | React to a message. |
slack_get_channel_history | Read a channel's recent messages. |
slack_get_thread_replies | Read a thread's replies. |
slack_get_users | List workspace users. |
slack_get_user_profile | Look up one user's profile. |
Plus a few clearly-labeled extensions: slack_auth_test, slack_search_messages,
slack_open_dm.
Wire it up
The MCP server needs two things: a DATABASE_URL pointing at the clone's
database, and a SLACK_MCP_TOKEN — the bot (xoxb-…) or user (xoxp-…) token
from spin that fixes its identity. The clone's database is named after its id
(slack-a1b2 → clone_slack_a1b2) inside the shared Postgres.
Build the server once, then run it with both env vars set on the shared network:
# Build the MCP server
cd apps/slack-clone/mcp-server && bun install && bun run build
# DATABASE_URL reaches the clone's db as host `postgres` on asym-shared;
# SLACK_MCP_TOKEN is a bot/user token from `hone env tokens slack-a1b2`.
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/clone_slack_a1b2 \
SLACK_MCP_TOKEN=xoxb-… \
node dist/index.jsRegister it with an MCP client
Point your MCP client at the built server over stdio. For a Claude Desktop-style config:
{
"mcpServers": {
"slack-clone": {
"command": "node",
"args": ["/abs/path/to/apps/slack-clone/mcp-server/dist/index.js"],
"env": {
"DATABASE_URL": "postgresql://postgres:postgres@postgres:5432/clone_slack_a1b2",
"SLACK_MCP_TOKEN": "xoxb-…"
}
}
}
}Your agent now sees slack_post_message, slack_list_channels, and the rest as
tools — each operating on the clone's real data, gated by the bound token's scopes.
Score the run
However your agent connected, its work is now rows in the clone's database. Read
them back to grade the run — see Inspect what happened
and the query reference:
hone env query slack-a1b2 "select count(*) from messages"
hone env query slack-a1b2 "select user_id, text from messages order by created_at" --jsonWhen the trial is done, hone env reset slack-a1b2 returns the clone to its seeded
starting state for the next run.