Tensile Docs
Guides

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:

SurfaceWhat it isUse it when
HTTP APIThe 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 serverA 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.

  1. Authenticate

    Log in as a seeded user (or sign up your own) to get an access token. The acme-corp fixture creates six users who all share the password password123 — dana@acme.test is 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.)

  2. 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's C… that doesn't change call signatures), and chat.postMessage takes 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"}'
  3. Point your agent at the base URL

    Hand http://127.0.0.1:3001/api to 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):

ToolDoes
slack_list_channelsList channels in the workspace.
slack_post_messagePost a message to a channel.
slack_reply_to_threadReply in a thread.
slack_add_reactionReact to a message.
slack_get_channel_historyRead a channel's recent messages.
slack_get_thread_repliesRead a thread's replies.
slack_get_usersList workspace users.
slack_get_user_profileLook 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.js

Register 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" --json

When the trial is done, hone env reset slack-a1b2 returns the clone to its seeded starting state for the next run.

Where to go next

On this page