MCP server

Connect Claude and Codex to it. Each gets your access, and nothing more.

attest_tag is an MCP server. Give Claude, Claude Code, Codex, Cursor or VS Code one address, sign in and press Allow, and your assistant can rewrite the documents the bot answers from, keep its memories, set up its routines, and read what it did and what that cost. Every call acts as you, with exactly the permissions your role holds.

https://app.attesttag.com/mcpstreamable HTTPOAuth or an API key26 tools

The deploy runbook still says we ship from main. We ship from release branches now. Fix the bot's copy.

attest-tagread_documentrunbooks/deploys.mdread-only

attest-tagwrite_documentrunbooks/deploys.mdwrite

saved · indexing in the background

Updated runbooks/deploys.md: deploys go from release/* now. The bot answers from the new text once the re-index finishes.

And every weekday at nine, post the failed payouts in #finance.

attest-taglist_scopesread-only

attest-tagcreate_routine#finance · 0 9 * * 1-5write

Routine 7 is set. It runs as the bot in #finance, and anything it tries to change waits for a Confirm there.

An MCP client with attest_tag added as a server. Each tool says whether it only reads. Illustrative.

Connect

One address. Sign in, press Allow, and it is connected.

On the hosted service the address is https://app.attesttag.com/mcp; on a deployment of your own it is /mcp on the console’s host. The console shows it under Developer → MCP, with the steps for each client.

The client registers itself and opens the console’s consent screen. If you are not signed in, you sign in first, and your organisation’s sign-in policy and two-factor rule apply as they always do. Press Allow and the client is sent back, connected as you, in the organisation you signed in to.

Scripts, CI and clients configured with a header use a developer key instead: make one under Developer → API keys and send it as a bearer token. The key is the person who made it, and revoking it cuts the client off.

Claude and Claude Desktop
Settings → Connectors → Add custom connector, and paste the address.
Claude Code
claude mcp add with the address, then /mcp, pick attest-tag and choose Authenticate.
Codex
codex mcp add with the address, then codex mcp login attest-tag, which opens a browser for you to sign in. The CLI and the IDE extension share the setting.
Cursor and VS Code
Add the address as an HTTP MCP server. Each opens a browser for you to sign in.
Any other client
One that speaks streamable HTTP and the MCP authorization spec finds its own way to the sign-in: an unauthenticated call is answered with where to go.

Claude Code

claude mcp add --transport http attest-tag \
  https://app.attesttag.com/mcp
# then, in Claude Code: /mcp → attest-tag → Authenticate

Codex

codex mcp add attest-tag --url https://app.attesttag.com/mcp
codex mcp login attest-tag     # opens a browser to sign in

With an API key instead

# Claude Code
claude mcp add --transport http attest-tag \
  https://app.attesttag.com/mcp \
  --header "Authorization: Bearer $ATTESTTAG_KEY"

# Codex reads the key from the variable you name
codex mcp add attest-tag --url https://app.attesttag.com/mcp \
  --bearer-token-env-var ATTESTTAG_KEY

JSON config (.mcp.json, Cursor's mcp.json)

{
  "mcpServers": {
    "attest-tag": {
      "type": "http",
      "url": "https://app.attesttag.com/mcp",
      "headers": {
        "Authorization": "Bearer <your API key>"
      }
    }
  }
}

Keep the key out of anything you commit. A config file checked into a repository is a key handed to everyone who can read it.

Tools

26 tools, each one route of the API.

A tool runs its /v1 route in-process, as the caller: the same permission check, the same boundary around your organisation, the same validation and the same audit row. It cannot say or allow anything the route does not.

Who and where
whoamilist_workspaceslist_scopes

The organisation, account and permissions the connection acts with; the Slack workspaces and Teams tenants connected; which channels the bot is in and how each is set up.

Documents
list_documentsread_documentwrite_documentdelete_document

What the bot searches, with the channel each document is limited to; one document's text; writing or removing one. A write re-indexes in the background, so the next answer comes from the new text.

Memory
list_memoriesadd_memoryupdate_memorydelete_memory

The facts it keeps for a workspace or a channel: added, reworded, forgotten.

Routines
list_routineslist_routine_runsrun_routinecreate_routineupdate_routinedelete_routine

Scheduled prompts and every run's output, cost and error, quiet runs included. Run one now, make one, change or pause one, delete one.

Artifacts
list_artifactsget_artifact

The files it made in its answers (reports, tables, drafts) with their contents.

Fix jobs
list_jobsget_job

What each job is changing, its status, branch and pull request, and what it cost.

Access requests
list_access_requests

Who asked the bot for access to what, and how each request was closed.

Activity and audit
list_activitylist_audit

Every turn with its model, tokens and cost; the audit log of sign-ins and where they came from, changes, approvals and exports. Both page forward from the last row seen, which is what a collector wants.

Spend
get_usageget_billing

This month's spend against the budget, the busiest channels, and the limit that paused the bot if one has; the plan, when it renews, and the credit left.

Amber tools change something, and the deletes among them say they are destructive: the hints an MCP client reads to decide what to ask you before it calls one. Reading workspaces, documents, memories, routines and spend needs only a connection. Changing documents, memories or routines needs the permission for each, which admin and editor both hold. Artifacts, fix jobs, access requests and activity each need a view permission every built-in role holds; the audit log needs audit.view, which among the built-in roles only admin holds. A client is offered only the tools its person’s role may use.

Two things are deliberately not tools. There is none that asks the bot a question: it answers in the channel, and what a client gets is what the bot knows, what it did and what that cost. And nothing reaches a stored credential. Connections are set up in the console, and their secrets leave through the proxy and nowhere else. Uploading a PDF is not a tool either; the API takes it.

Your access

It acts as you, and checks who you are on every call.

A connection acts as the person who approved it, and their role is read again on every request. Give them a smaller role and the next call is smaller; remove them and it stops. An admin sees all 26 tools; an editor sees all but the audit log.

Approving an app takes what making an API key takes: a role that may manage keys, which admin and editor both hold, and, where the deployment sends mail, a confirmed email address. The consent screen names the app, your account, the organisation and the address it will send you back to. Read the address: an app’s name is whatever it called itself.

An access token lasts an hour and opens /mcp and nothing else. The refresh token behind it lasts thirty days from its last use and changes every time it is used. If a spent one ever comes back, somebody else has a copy, so the connection ends there and you connect again.

Model Context Protocol

Connect Claude

Claude wants to use attest_tag as you.

Account
[email protected]
Organisation
Nexa Labs
Returns to
claude.ai

It can do what your role can do through the API: read documents, memories, activity and spend, and change documents and memories where your role may. It keeps that until you disconnect it under Developer → MCP, or leave the organisation.

Only allow this if you started it from Claude, and it is sending you back to claude.ai.

An app names itself. The address it returns to is the part to check.

Routines

A routine made from Claude still waits for a Confirm.

create_routine puts a scheduled prompt in a channel the bot is in, named by its id or as #name, at most every fifteen minutes. It runs as the bot, with that channel’s connections and budget, never as you, so it reaches nobody’s own accounts.

Anything it tries to change waits for a Confirm in the channel, the way a write in a thread does, until someone who manages routines switches that off in the console. Rewriting what a routine does takes it off the person it ran as, as the console’s editor does, and from then on it runs as the bot. How a write is held.

Audit

Every change says it came over MCP, and which connection brought it.

A write through a tool is recorded as the same write through the API would be, with who made it and from which address, marked as having come over MCP along with the key or connection it came through. Approving an app, disconnecting one, and a connection ended because a spent refresh token came back each get an audit row of their own.

Developer → MCP lists every connected app with the person it acts as. You disconnect your own; anyone who may manage API keys can disconnect any. Removing someone from the organisation, or a password reset or change, ends their connections as it revokes their keys. Calls are limited to 240 a minute per connection, as keys are.

The developer API

Or skip MCP: the same routes, over HTTP with a key.

Every tool is a route of /v1, and a script can call the routes directly: keep the runbooks the bot answers from in step with the repository they live in, or feed the activity and audit logs to a collector.

A key carries the access of whoever made it and not a scrap more, read again on every request, so an integration cannot reach further than its maker and stops when they leave. It is shown once and stored only as a hash. It skips the sign-in policy and two-factor, which belong to somebody at a keyboard, and is bounded instead by its expiry, if it was given one, and the revoke button.

A record outside your organisation answers 404, never 403: a key cannot tell a record it may not see from one that does not exist. Keys and connections are each limited to 240 requests a minute.

The developer API

export ATTESTTAG_KEY=atk1.…   # Developer → API keys

curl https://app.attesttag.com/v1/whoami \
  -H "Authorization: Bearer $ATTESTTAG_KEY"

# Keep what the bot reads in step with the repository.
curl https://app.attesttag.com/v1/documents/runbooks/deploys.md \
  -X PUT \
  -H "Authorization: Bearer $ATTESTTAG_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"content": "# Deploys\n\nShip from release/*.\n"}'

For client authors

The deployment is its own authorization server.

It follows the MCP authorization spec, so a client that implements the spec needs nothing from us but the address. The same endpoints are on every deployment, hosted or your own.

Discovery
/.well-known/oauth-protected-resource/mcp (RFC 9728), which a 401 from /mcp points at, and /.well-known/oauth-authorization-server (RFC 8414).
Registration
POST /oauth/register, open dynamic client registration (RFC 7591). Clients are public by default, with PKCE standing behind them; one that registers with client_secret_basic or client_secret_post is given a secret and must use it.
Authorization
GET /oauth/authorize, the authorization code grant. PKCE with S256 is required, resource (RFC 8707) must name this server if it is sent, and every redirect back carries iss (RFC 9207). A code lasts ten minutes and works once; used a second time, it ends what the first use made.
Tokens
POST /oauth/token for authorization_code and refresh_token; POST /oauth/revoke (RFC 7009). Access tokens open /mcp and nothing else.
Redirect addresses
https, http to localhost on any port (RFC 8252), or an app's own scheme.
Transport
Streamable HTTP, stateless. Each request is answered on its own, so a role changed a minute ago changes the very next tool list.

Connect it, and see what it knows.

Sign up and the address is under Developer → MCP in the console. Run it yourself and it is /mcp on your own host. The tools are the same either way.

The other direction: the bot calling your MCP servers