# MCP

Canonical URL: https://texttree.ai/en/docs/mcp/
Markdown URL: https://texttree.ai/en/docs/mcp.md
Page type: docs
Translation status: approved
Legal status: not_required

## Summary

Current MCP routes, scopes, execution envelopes, and runtime controls.

## Source content

# MCP

TextTree exposes a narrow MCP surface for controlled tool discovery and execution.

## Base routes

- `GET /mcp/health`
- `POST /mcp`
- `POST /mcp/accounts`
- `GET /mcp/tools`
- `POST /mcp/tools/:name`
- `GET /api/v1/mcp/oauth-clients`
- `DELETE /api/v1/mcp/oauth-clients/:client_id`
- `GET /.well-known/oauth-protected-resource/mcp`
- `GET /.well-known/oauth-authorization-server/mcp`
- `POST /oauth/register`
- `GET /oauth/authorize`
- `POST /oauth/authorize`
- `POST /oauth/token`
- `POST /oauth/revoke`

Local Phoenix development runs at `http://localhost:4001`.

`POST /mcp` is the native MCP JSON-RPC endpoint. The `/mcp/tools` routes are
REST compatibility routes for existing TextTree integrations and debugging.
TextTree currently runs stateless Streamable HTTP: JSON-RPC messages are sent
with `POST /mcp`. `GET /mcp` SSE stream probes and `DELETE /mcp` session
termination requests return `405 Method Not Allowed` because no server-side MCP
session is allocated.

## Discovery metadata

TextTree publishes MCP authorization discovery metadata for HTTP MCP clients:

```bash
curl https://api.texttree.ai/.well-known/oauth-protected-resource/mcp
curl https://api.texttree.ai/.well-known/oauth-authorization-server/mcp
```

Unauthenticated MCP requests return `401` with a `WWW-Authenticate` challenge
that points to the protected-resource metadata URL. The authorization metadata
advertises Client ID Metadata Document support, dynamic client registration,
authorization-code grants, PKCE S256, and public-client token exchange for HTTP
MCP clients. TextTree also continues to support first-party bearer-token
bootstrap through `POST /mcp/accounts`. The backward-compatible
`POST /api/v1/onboarding/api-key` route now issues the `txt_...` bearer access
tokens used by MCP and the developer API.

OAuth MCP client flow:

1. Prefer a Client ID Metadata Document: set `client_id` to an HTTPS URL that
   hosts the client metadata JSON. Use `POST /oauth/register` only as the
   dynamic-registration fallback for clients that cannot host metadata.
2. Send the user to `GET /oauth/authorize` with `response_type=code`,
   `client_id`, `redirect_uri`, `resource=https://api.texttree.ai/mcp`,
   `code_challenge`, and `code_challenge_method=S256`.
3. TextTree shows a browser consent screen with the MCP client, redirect URI,
   resource, and requested scopes. Approval submits `POST /oauth/authorize` and
   redirects back with `code`; denial redirects back with `error=access_denied`.
4. Exchange the returned code at `POST /oauth/token` with the matching
   `code_verifier` and the same `resource` value. JSON and
   `application/x-www-form-urlencoded` requests are accepted.
5. Use the returned TextTree bearer token on `POST /mcp`.
6. Revoke a held bearer token with `POST /oauth/revoke` when the MCP client
   disconnects or rotates credentials.

Client ID Metadata Document example:

```json
{
  "client_id": "https://agent.example.com/.well-known/oauth-client.json",
  "client_name": "Example Agent",
  "client_uri": "https://agent.example.com",
  "redirect_uris": ["http://localhost:8787/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "scope": "mcp:read mcp:execute onboarding:read",
  "token_endpoint_auth_method": "none"
}
```

When TextTree sees a URL-formatted `client_id` during authorization, it fetches
and validates that document before rendering consent. The document URL must use
HTTPS on the default port, must not include credentials or a fragment, and must
resolve only to public IP addresses. Localhost, private, link-local, multicast,
and unresolved hosts are rejected before fetch. The fetched JSON must contain a
`client_id` that exactly matches the URL, a `client_name`, at least one
supported scope, and the requested `redirect_uri`. If `grant_types` or
`response_types` are present, they must include `authorization_code` and `code`.
TextTree caches valid metadata according to `Cache-Control: max-age` or
`Expires`; `no-cache` and `no-store` force revalidation on the next
authorization request. Documents without cache headers use a short default
cache window, and TextTree caps metadata cache lifetime at one day.

TextTree bearer tokens can be audience-bound to the MCP resource. New MCP
operator-issued tokens should use the canonical MCP resource URL as the
audience, for example `https://api.texttree.ai/mcp`. Audience-free tokens remain
accepted for compatibility, but an audience-bound token is rejected when used
against a different TextTree resource.

The OAuth `resource` value is restricted to the MCP resource URL. Unsupported
requested scopes fail with `invalid_scope` instead of being silently broadened
or narrowed.

Authorized MCP OAuth clients can be listed and revoked through the API:

```bash
curl https://api.texttree.ai/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$TEXTREE_ACCESS_TOKEN&token_type_hint=access_token"

curl https://api.texttree.ai/api/v1/mcp/oauth-clients \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN"

curl -X DELETE https://api.texttree.ai/api/v1/mcp/oauth-clients/mcp_client_... \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN"
```

`POST /oauth/revoke` follows the OAuth revocation convention and returns `200`
for unknown tokens so callers do not leak token existence.
Listing requires `mcp:read`. Revocation requires `mcp:execute` and revokes
active TextTree bearer tokens issued through that MCP OAuth client for the
current user.

`POST /oauth/register` and `POST /oauth/token` are rate-limited by client IP.
Rate-limited requests return `429` with a `Retry-After` header and either
`oauth_client_registration_rate_limited` or `oauth_token_exchange_rate_limited`.
The buckets are separate so registration spikes do not starve authorization-code
token exchange.

## Authentication and scopes

Authenticated MCP requests use TextTree-issued bearer tokens. Humans, wallet
users, and AI agents all present the same `Authorization: Bearer <token>`
header after authenticating through TextTree.

AI agents can bootstrap their own TextTree identity through `POST /mcp/accounts`
with a username and password. That route is unauthenticated, returns backup
codes plus a TextTree bearer token, and is throttled to one successful account
creation per IP address every five minutes.

The current route-level scopes are:

- `POST /mcp` accepts `initialize` and `ping` with any authenticated token
- `POST /mcp` `tools/list`, `resources/list`, `resources/templates/list`, `prompts/list`, and `completion/complete` require `mcp:read`
- `POST /mcp` `resources/read` requires the resource-specific scope
- `POST /mcp` `tools/call` requires `mcp:execute`
- `GET /mcp/tools` requires `mcp:read`
- `POST /mcp/tools/:name` requires `mcp:execute`
- `GET /api/v1/mcp/oauth-clients` requires `mcp:read`
- `DELETE /api/v1/mcp/oauth-clients/:client_id` requires `mcp:execute`

Tool entries can also declare a more specific required scope in the server registry. If that
happens, TextTree records a blocked execution audit and returns `403 insufficient_scope`.

## Native MCP JSON-RPC

`POST /mcp`

TextTree supports MCP-style JSON-RPC over HTTP for clients that expect standard
MCP methods. The endpoint requires a TextTree bearer token and returns the
`mcp-protocol-version` response header.
JSON-RPC request envelopes must include `jsonrpc: "2.0"`. Request IDs must be
strings or integers; explicit `null` IDs are rejected, and invalid ID shapes are
not echoed in error responses. Method names must be strings. Request envelopes
are strict: only `jsonrpc`, `id`, `method`, `params`, and the MCP-reserved
top-level `_meta` field are accepted. Any `_meta` field must be a JSON object.
When request params include `_meta.progressToken`, the token must be a string or
integer. TextTree currently treats progress tokens as advisory metadata and does
not emit progress notifications.

Supported protocol versions:

- `2025-11-25`
- `2025-06-18`
- `2025-03-26`

During `initialize`, TextTree negotiates the requested `params.protocolVersion`
when it is supported. If an initialize request asks for an unsupported string
version, TextTree responds with its latest supported version instead of failing
the handshake. For subsequent requests, clients should send the
`MCP-Protocol-Version` header with the negotiated version. If no header is
present, TextTree falls back to `2025-03-26` for compatibility. Unsupported
protocol-version headers return `400 Bad Request`, matching the MCP Streamable
HTTP transport requirement.
If supplied, `initialize.params.protocolVersion` must be a string,
`capabilities` must be an object, and `clientInfo` must be an object with a
string `name` and optional string `version`. Unknown initialize params are
rejected, except for the MCP-reserved `_meta` field.
`ping` accepts an empty params object or `_meta`; other ping params are rejected.

`POST /mcp` requires an `Accept` header that includes both `application/json`
and `text/event-stream`, plus `Content-Type: application/json`. `GET /mcp`
stream probes require `text/event-stream`. Requests missing the advertised
response types return `406 Not Acceptable`; POST requests with a non-JSON
content type return `415 Unsupported Media Type`.

Browser-originated MCP transport requests must also pass Origin validation.
Requests without an `Origin` header are accepted for server-side and CLI MCP
clients. When an `Origin` header is present, it must match the request origin,
the configured TextTree public app or site origin, or a loopback local
development origin. Untrusted browser origins return `403 forbidden_origin`.

### Initialize

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "init-1",
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-11-25",
      "capabilities": {},
      "clientInfo": {
        "name": "agent-client",
        "version": "0.1.0"
      }
    }
  }'
```

Success returns server capabilities and identity:

```json
{
  "jsonrpc": "2.0",
  "id": "init-1",
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
      "prompts": {
        "listChanged": false
      },
      "completions": {},
      "resources": {
        "listChanged": false
      },
      "tools": {
        "listChanged": false
      }
    },
    "serverInfo": {
      "name": "texttree",
      "version": "0.1.0"
    },
    "instructions": "TextTree exposes audited SMS onboarding and messaging workflows..."
  }
}
```

Clients can place the returned `instructions` into model context. The
instructions summarize TextTree-specific workflow rules: prefer Dedicated
Number for stable sender identity, read resources before executing tools, use
`messages.send` only with recipient-approved SMS and a stable
`idempotency_key`, and rely on TextTree's suppression, spend, sender-readiness,
and delivery-worker gates for production sends.

### List MCP tools

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "tools-1",
    "method": "tools/list",
    "params": {}
  }'
```

`tools/list` requires `mcp:read`. Each tool includes a title, JSON Schema
2020-12 `inputSchema`, JSON Schema 2020-12 `outputSchema`, MCP safety
annotations, and TextTree metadata for the required scope and timeout.
`tools/list` supports MCP cursor pagination. Treat `nextCursor` as opaque and
pass it back as `params.cursor` only on the next `tools/list` request. List
requests accept only `cursor` and the MCP-reserved `_meta` field.

### List MCP resources

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "resources-1",
    "method": "resources/list",
    "params": {}
  }'
```

`resources/list` requires `mcp:read`. Resources expose read-only context with
scope metadata so agents can inspect state without executing a tool.
`resources/list` supports MCP cursor pagination. Treat `nextCursor` as opaque
and pass it back as `params.cursor` only on the next `resources/list` request.
List requests accept only `cursor` and the MCP-reserved `_meta` field.

Current resources:

- `texttree://mcp/tools` requires `mcp:read`
- `texttree://onboarding/status` requires `onboarding:read`
- `texttree://billing/status` requires `onboarding:read`
- `texttree://messages/recent` requires `messages:write`

### List MCP Resource Templates

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "resource-templates-1",
    "method": "resources/templates/list",
    "params": {}
  }'
```

`resources/templates/list` requires `mcp:read` and returns safe by-ID resource
templates for agent polling:

- `texttree://invoices/{id}` requires `onboarding:read`
- `texttree://messages/{id}` requires `messages:write`
- `texttree://numbers/{id}` requires `numbers:read`
- `texttree://onboarding/checklist` requires `onboarding:read`
- `texttree://billing/readiness` requires `onboarding:read`
- `texttree://elicitations/{correlation_id}` requires `onboarding:read`

It supports the same cursor pagination contract as the other list methods, and
accepts only `cursor` plus the MCP-reserved `_meta` field.

### Read an MCP resource

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "resource-read-1",
    "method": "resources/read",
    "params": {
      "uri": "texttree://onboarding/status"
    }
  }'
```

`resources/read` returns JSON content in the MCP `contents` array. Resource
reads enforce the resource-specific TextTree scope before returning data.
`texttree://messages/recent` and `texttree://messages/{id}` omit phone numbers
and message bodies; use them for delivery-state context, not private recipient
content. `texttree://numbers/{id}` redacts the full phone number and returns
status, usage, capabilities, and compliance state. All by-ID reads are scoped to
the current TextTree user or workspace. `texttree://elicitations/{correlation_id}`
returns the resumable status for a URL-mode funding, consent, or number setup
handoff.

### List MCP prompts

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "prompts-1",
    "method": "prompts/list",
    "params": {}
  }'
```

`prompts/list` requires `mcp:read`. Prompts package common TextTree workflows
into reusable agent instructions.
`prompts/list` supports MCP cursor pagination. Treat `nextCursor` as opaque and
pass it back as `params.cursor` only on the next `prompts/list` request. List
requests accept only `cursor` and the MCP-reserved `_meta` field.

Current prompts:

- `texttree.onboard_agent` accepts optional `path` (`dedicated_number` or
  `fast_send`), `region`, `brand_name`, `website`, `funding_amount_cents`,
  `payment_method`, `recipient_phone_number`, `secret_storage`, and
  `execution_policy`
- `texttree.first_send` accepts optional `sender_path` (`dedicated_number` or
  `fast_send`) and optional string `recipient_context`

Prompt arguments are strict. Unknown argument names, wrong types, and invalid
sender-path enum values return `-32602 Invalid params`.

### Get an MCP prompt

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "prompt-1",
    "method": "prompts/get",
    "params": {
      "name": "texttree.onboard_agent",
      "arguments": {
        "path": "dedicated_number",
        "region": "US"
      }
    }
  }'
```

Prompt responses return MCP `messages` that a client can place into the model
context before calling resources or tools.

### Complete MCP prompt arguments

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "complete-1",
    "method": "completion/complete",
    "params": {
      "ref": {
        "type": "ref/prompt",
        "name": "texttree.onboard_agent"
      },
      "argument": {
        "name": "path",
        "value": "d"
      }
    }
  }'
```

`completion/complete` requires `mcp:read` and returns non-sensitive suggestions
for TextTree prompt arguments. Current completions cover `path`,
`sender_path`, and `region`. Free-form arguments return an empty completion
list. TextTree does not expose resource templates, so resource-template
completion requests return `-32602 Invalid params`.

### Call an MCP tool

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "call-1",
    "method": "tools/call",
    "params": {
      "name": "onboarding.status",
      "arguments": {}
    }
  }'
```

`tools/call` requires `mcp:execute` plus the tool-specific scope declared in
the registry. Results include MCP `content`, `structuredContent`, `isError`,
MCP `_meta`, and the TextTree execution audit envelope. The
`structuredContent` payload matches each tool's advertised `outputSchema`.
Tool-call result `_meta` contains safe correlation fields:
`texttree/execution_id`, `texttree/tool`, `texttree/outcome`, and
`texttree/is_error`. The same metadata is included under
`structuredContent._meta` so the advertised `outputSchema` matches the
machine-readable result payload. It does not include phone numbers or message
bodies.

TextTree validates known tool arguments against each tool's advertised
`inputSchema` before execution. Missing required fields, unknown fields, wrong
types, invalid enum values, and minimum violations return `-32602 Invalid
params` without running the tool.
JSON-RPC method params are also strict: `prompts/get`, `resources/read`, and
`tools/call` reject unknown top-level params while accepting the MCP-reserved
`_meta` field. Tool-call `_meta` is passed to the executor context as side
channel MCP metadata and is not merged into tool `arguments`.
If `_meta.progressToken` is present, it must be a string or integer.
Message tool `arguments.metadata` is separate caller-owned metadata and may
contain arbitrary JSON object fields for correlation.

`messages.send` is a real MCP tool. It queues through the same TextTree
messaging path as `POST /api/v1/messages`, including suppression, spend, and
delivery-worker gates. Its MCP annotations mark it as destructive and
open-world because it can enqueue an outbound SMS and consume account balance.

JSON-RPC errors use standard MCP-style response envelopes and preserve
TextTree safety details in `error.data`, including `required_scope`,
`quota_exceeded`, `tool_not_allowed`, timeout, and execution audit data when
available.

### Batches and notifications

JSON-RPC batches are accepted only when the effective protocol version is the
`2025-03-26` compatibility version. Later MCP revisions removed batch messages
from the protocol schema, so clients that negotiate `2025-06-18` or
`2025-11-25` should send one JSON-RPC message per `POST /mcp`; batch arrays on
those versions return `-32600 Invalid Request` with `error.data.error` set to
`batch_not_supported`.

For `2025-03-26` compatibility batches, TextTree returns one response object per
request and omits responses for notifications and JSON-RPC response messages.
Notification-only and response-only batches return `202` with an empty body.
TextTree currently does not initiate server-to-client requests, so client
response messages are accepted as no-op transport inputs when they include a
valid string or integer `id`. Result response messages must include an object
`result`; error response messages must include an object `error` with integer
`code` and string `message`. Send `initialize` as a standalone request; if it
appears inside a batch, TextTree returns `-32600 Invalid Request` for that batch
item. Compatibility batches are capped at 100 items; larger batches return a
single `-32600 Invalid Request` response with `error.data.error` set to
`batch_too_large`.
Methods in the `notifications/` namespace are treated as fire-and-forget
notifications and never receive JSON-RPC responses. TextTree currently acts on
`notifications/initialized` and accepts cancellation notifications as advisory;
unknown notification names are ignored. Ordinary request methods such as
`ping`, `tools/list`, and `tools/call` must include an `id`.

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "jsonrpc": "2.0",
      "id": "ping-1",
      "method": "ping",
      "params": {}
    },
    {
      "jsonrpc": "2.0",
      "method": "notifications/initialized",
      "params": {}
    },
    {
      "jsonrpc": "2.0",
      "id": "tools-1",
      "method": "tools/list",
      "params": {}
    }
  ]'
```

### Compatibility smoke test

For a running Phoenix server, TextTree includes a compatibility smoke task that
drives the native MCP endpoint through initialize, initialized notification,
tools, resources, prompts, and an onboarding status tool call.

```bash
cd apps/web
TEXTREE_ACCESS_TOKEN=txt_... mix textree.mcp.smoke --url http://localhost:4001/mcp
mix textree.mcp.smoke --url http://localhost:4001/mcp --bootstrap-local-token
```

The token should include `mcp:read`, `mcp:execute`, and `onboarding:read`.
Pass `--skip-execute` to skip the onboarding status tool call when validating
a read-only token. `--bootstrap-local-token` creates a one-hour token in the
current local database and is refused for non-localhost MCP URLs.

For operator-issued bearer tokens, bind the token to the MCP resource:

```bash
mix textree.auth.issue_token agent@example.com \
  --scopes mcp:read,mcp:execute,onboarding:read,messages:write \
  --audience https://api.texttree.ai/mcp
```

## List tools

`GET /mcp/tools`

```bash
curl https://api.texttree.ai/mcp/tools \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN"
```

Success returns a registry-backed tool list:

```json
{
  "tools": [
    {
      "name": "messages.send",
      "description": "Trigger a message send through the MCP surface.",
      "required_scope": "messages:write",
      "timeout_ms": 1000
    },
    {
      "name": "onboarding.status",
      "description": "Inspect token, balance, invoice, and SMS onboarding state.",
      "required_scope": "onboarding:read",
      "timeout_ms": 1000
    },
    {
      "name": "onboarding.create_invoice",
      "description": "Create or reuse a launch funding invoice.",
      "required_scope": "onboarding:write",
      "timeout_ms": 1000
    },
    {
      "name": "onboarding.invoice_status",
      "description": "Check funding invoice status for programmatic onboarding.",
      "required_scope": "onboarding:read",
      "timeout_ms": 1000
    },
    {
      "name": "onboarding.test_sms",
      "description": "Send the fixed Fast Send onboarding test SMS.",
      "required_scope": "onboarding:write",
      "timeout_ms": 1000
    },
    {
      "name": "onboarding.request_dedicated_number",
      "description": "Submit business details for the recommended Dedicated Number path.",
      "required_scope": "onboarding:write",
      "timeout_ms": 1000
    }
  ]
}
```

The current alpha registry is configured server-side. There is no public admin surface to mutate
the registry at runtime.

## Create a headless account

`POST /mcp/accounts`

Use this route when an AI client needs an account and bearer token without
email, Google OAuth, or wallet auth.

```bash
curl https://api.texttree.ai/mcp/accounts \
  -H "Content-Type: application/json" \
  -d '{
    "account": {
      "username": "agent-demo",
      "password": "correct horse battery staple"
    }
  }'
```

Success returns the public username, one-time backup codes, and a bearer token:

```json
{
  "account": {
    "id": "9d7d9df7-58a0-4716-b82e-7ad5e73f7b36",
    "username": "agent-demo"
  },
  "backup_codes": ["AAAA-BBBB-CCCC-DDDD"],
  "token": {
    "type": "Bearer",
    "access_token": "txt_...",
    "scopes": [
      "messages:write",
      "mcp:read",
      "mcp:execute",
      "campaigns:read",
      "campaigns:write",
      "numbers:read",
      "numbers:write",
      "onboarding:read",
      "onboarding:write"
    ],
    "expires_at": "2026-06-03T15:30:00Z"
  }
}
```

Save the token and backup codes immediately. TextTree does not return the
internal synthetic email, token hash, raw IP, or IP hash. Validation failures
return `422 validation_failed`; repeated successful account creation from the
same IP within five minutes returns `429 account_creation_rate_limited`.

Headless account tokens include an `expires_at` value. Durable agent
integrations should either use an operator-issued token with an intentional
lifetime and audience, or include a token re-issuance and rotation plan. Do not
store raw bearer tokens or backup codes in logs, shell history, or committed
files.

## Safe MCP onboarding prompt

Use a prompt with explicit real-world targets and approval boundaries before an
agent starts setup:

```txt
Set up TextTree.ai SMS for my AI agent using https://texttree.ai/docs/mcp/.
Auto-generate account credentials; register the dedicated number under brand
"AlphaGrowth" / https://alphagrowth.io/ (US). Store the token and backup codes
in a gitignored .env. Create a $100 USDC funding invoice and give me the
deposit details to pay; do not attempt payment yourself. Once funded, send a
test SMS to +1XXXXXXXXXX and show me how to poll for inbound replies; I will
text the number myself to test inbound. Then save the assigned number, API base
URL, token location, and MCP connect steps to a README, and write live and
mocked bash/curl test scripts for send and receive. Pause for my OK before
anything that spends money, provisions or changes a number, or sends an SMS.
```

If any of the phone number, brand, website, funding method, secret-storage
target, or approval boundary is missing, collect it before executing tools.
Agents must not invent recipient numbers, make payments, or simulate inbound
SMS that requires a human-owned phone.

## MCP onboarding runbook

Agent-executable steps:

1. Create or choose a bearer token and store it in the requested secret target.
2. Read `texttree://onboarding/status` and `texttree://billing/status`.
3. After explicit approval, call `onboarding.request_dedicated_number` only
   when brand name, HTTPS website, and region are explicit.
4. After explicit approval, call `onboarding.create_invoice` with an explicit
   amount and supported method, then give the human the returned payment
   details.
5. Poll `onboarding.invoice_status` or `texttree://invoices/{id}` until payment
   state changes.
6. After approval, call `onboarding.test_sms` or `messages.send` with the
   explicit recipient phone number and a stable `idempotency_key`.
7. Poll `/api/v1/messages/$MESSAGE_ID` or `texttree://messages/{id}` for
   delivery state.

Human-payment steps:

1. Review the invoice amount, chain, token, wallet, and expiry.
2. Pay the invoice outside the agent.
3. Tell the agent to resume polling after payment is submitted.

Human-inbound-test steps:

1. Wait until the assigned number is connected.
2. Text the assigned number from a real phone.
3. Ask the agent for receive-polling commands or webhook inspection steps.

Steps requiring explicit approval:

- Creating a funding handoff or invoice intended for real payment.
- Requesting, provisioning, or changing a dedicated number.
- Sending any SMS to a real phone number.
- Storing or rotating bearer tokens and backup codes.

## Execute a tool

`POST /mcp/tools/:name`

### Request body

```json
{
  "params": {}
}
```

### curl

```bash
curl https://api.texttree.ai/mcp/tools/onboarding.status \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"params": {}}'
```

### JavaScript

```js
const response = await fetch("https://api.texttree.ai/mcp/tools/onboarding.status", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TEXTREE_ACCESS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ params: {} }),
});

const { execution } = await response.json();
```

### Success response

```json
{
  "execution": {
    "id": "513f6a41-2c33-4c15-b4e2-10da14ab806f",
    "tool": "onboarding.status",
    "outcome": "succeeded",
    "params_summary": {},
    "result_summary": {
      "complete": false,
      "product_mode": "instant_send"
    },
    "error_message": null,
    "duration_ms": 3,
    "inserted_at": "2026-04-26T21:02:15Z"
  }
}
```

## Error responses

- `403` with `{"error":"tool_not_allowed"}` when the tool is outside the configured allowlist
- `403` with `{"error":"insufficient_scope","required_scope":"..."}` when the token lacks the
  route-level or tool-level scope
- `429` with `{"error":"quota_exceeded"}` when the per-identity quota window is exhausted
- `504` with `{"error":"execution_timed_out"}` when the execution exceeds the configured timeout
- `422` with `{"error":"execution_failed"}` when the executor returns an error

All of these execution paths return an `execution` envelope when TextTree was able to create an
audit record.

MCP `403 insufficient_scope` responses also include a `WWW-Authenticate`
challenge with `error="insufficient_scope"`, the required `scope`, and the
protected-resource metadata URL. MCP clients can use that header to trigger a
step-up authorization flow and request the missing scope without guessing.

## Runtime controls

The current alpha MCP runtime is intentionally strict:

- execution is gated by TextTree bearer-token scopes
- tools must exist in the server registry
- tools must also be present in the allowlist boundary
- executions are persisted with actor, token identity, tool, params summary, outcome, and timestamp
- per-identity quotas are enforced over a rolling window
- per-tool or global timeouts stop long-running work
- OAuth client registration and token exchange are separately IP rate-limited

OAuth rate-limit defaults and production environment variables:

- client registration: 20 requests per IP, refill every 60 seconds with
  `TEXTREE_OAUTH_CLIENT_REGISTRATION_RATE_LIMIT_CAPACITY` and
  `TEXTREE_OAUTH_CLIENT_REGISTRATION_RATE_LIMIT_REFILL_MS`
- token exchange: 60 requests per IP, refill every 60 seconds with
  `TEXTREE_OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_CAPACITY` and
  `TEXTREE_OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_REFILL_MS`

## Programmatic onboarding pattern

Use the onboarding API and MCP tools to complete setup without guessing which
step is next. Dedicated Number is the recommended path when an agent needs a
persistent sender that people can save and reply to. Fast Send is available
when the goal is the quickest first outbound test through pooled instant
senders.

### 1. Create or choose a token

Create a headless account through `POST /mcp/accounts`, or create a TextTree
bearer token in the authenticated app with the scopes needed for your path:

- `onboarding:read` to inspect onboarding status and invoices
- `onboarding:write` to create invoices, test sends, and dedicated number requests
- `messages:write` to send the first production SMS through `/api/v1/messages`
- `mcp:read` and `mcp:execute` to list and execute MCP tools

Do not use a legacy `txk_...` API key for this flow. `POST
/api/v1/onboarding/api-key` returns a `txt_...` bearer token for callers that
already have an onboarding-scoped token.

Operator-issued bearer tokens should be created with the exact scopes needed by
the client. For MCP onboarding plus first send, use:

```bash
cd apps/web && mix textree.auth.issue_token agent@example.com \
  --scopes onboarding:read,onboarding:write,mcp:read,mcp:execute,messages:write \
  --audience https://api.texttree.ai/mcp
```

Save returned tokens immediately. Raw tokens are shown once.

### 2. Check onboarding status

Use the API or the MCP tool to inspect token, balance, invoice, number, and
first-SMS progress.

```bash
curl https://api.texttree.ai/api/v1/onboarding \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN"
```

```bash
curl https://api.texttree.ai/mcp/tools/onboarding.status \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"params": {}}'
```

### 3. Choose a number path

Recommended: Dedicated Number. Use this when the agent needs a stable sender,
inbound replies, customer recognition, or a number operators can manage. The
native MCP tool saves the business details and returns URL-mode elicitation
metadata pointing at the hosted TextTree setup flow.

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "dedicated-number-1",
    "method": "tools/call",
    "params": {
      "name": "onboarding.request_dedicated_number",
      "arguments": {
        "brand_name": "Acme Support",
        "website": "https://acme.example",
        "region": "US"
      }
    }
  }'
```

Fast Send. Use this for the quickest first outbound test when a dedicated
number is not needed yet. The native MCP tool sends the fixed onboarding test
copy through the same Fast Send test path as the API.

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "test-sms-1",
    "method": "tools/call",
    "params": {
      "name": "onboarding.test_sms",
      "arguments": {
        "phone_number": "+15551234567"
      }
    }
  }'
```

`test-sms` uses fixed onboarding copy, can run before funding when the account
has an available Fast Send Sandbox allowance, and is rate-limited. A repeated
request can return `429 test_sms_rate_limited` with `next_allowed_at`; after the
included sandbox SMS is used, continue by adding balance.

### 4. Fund the workspace

Create or reuse a launch funding invoice with an amount that satisfies the
current onboarding minimum. Prefer the MCP invoice tool during agent
onboarding. The result includes URL-mode elicitation metadata with a hosted
TextTree action URL for completing payment.

```bash
curl https://api.texttree.ai/mcp/tools/onboarding.create_invoice \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "params": {
      "amount_cents": 10000,
      "payment_method": "direct_usdc"
    }
  }'
```

Poll the invoice until it becomes `paid`, `underpaid`, `expired`, or
`review_required`.

```bash
curl https://api.texttree.ai/api/v1/onboarding/funding-invoices/$INVOICE_ID \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN"
```

```bash
curl https://api.texttree.ai/mcp/tools/onboarding.invoice_status \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "params": {
      "id": "'"$INVOICE_ID"'"
    }
  }'
```

For `onboarding.invoice_status`, provide exactly one of `id` or `invoice_id`.
The by-ID resource template can also be used for polling:

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "invoice-resource-1",
    "method": "resources/read",
    "params": {
      "uri": "texttree://invoices/'"$INVOICE_ID"'"
    }
  }'
```

URL-mode elicitation payloads include `mode: "url"`, a `kind`, a
`correlation_id`, a `status_uri`, a hosted `url`, a human-readable `prompt`,
`expires_at`, and `status: "pending"`. Treat the URL as a user-action handoff;
do not treat model output alone as funding, consent, or number-provisioning
approval. Agents can resume by polling `status_uri`:

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "elicitation-resource-1",
    "method": "resources/read",
    "params": {
      "uri": "texttree://elicitations/'"$CORRELATION_ID"'"
    }
  }'
```

The elicitation resource is scoped to the current TextTree user. Unknown or
cross-user correlation IDs return `resource_not_found`; pending handoffs move
to `expired` after their `expires_at` timestamp.

### 5. Send the first production SMS

After the workspace is funded and, for the recommended path, the dedicated
number is connected, send through either `tools/call` with `messages.send` or
the normal V1 message endpoint. Both paths use the same suppression, spend,
idempotency, sender-readiness, and delivery-worker gates.

MCP tool call:

```bash
curl https://api.texttree.ai/mcp \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "send-1",
    "method": "tools/call",
    "params": {
      "name": "messages.send",
      "arguments": {
        "phone_number": "+15551234567",
        "body": "Thanks for connecting with Acme Support. Reply here any time.",
        "idempotency_key": "first-send-2026-06-07"
      }
    }
  }'
```

V1 API compatibility route:

```bash
curl https://api.texttree.ai/api/v1/messages \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15551234567",
    "body": "Thanks for connecting with Acme Support. Reply here any time.",
    "idempotency_key": "first-send-2026-06-07"
  }'
```

Poll the message for delivery state and inspect dashboard logs for provider
state.

```bash
curl https://api.texttree.ai/api/v1/messages/$MESSAGE_ID \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN"
```

## Agent integration pattern

Use MCP tools for bounded context and audited sends. An AI agent should:

1. read onboarding and billing resources before execution
2. collect explicit recipient, funding, brand, website, storage, and approval details
3. hand off to your application or human operator for final send approval when required
4. call `messages.send` or send through `/api/v1/messages` so suppression, spend, sender-readiness, and provider gates run

This keeps agent workflows behind the same production controls as human and API sends.

## Backup-code recovery

Backup codes are returned during headless account bootstrap or generated in account settings.
They are shown once, stored hashed, and consumed on first use. Generating a new
set invalidates unused existing codes after explicit confirmation. Recovery
attempts are rate-limited and logged without storing raw codes.

## Current alpha constraints

- MCP tooling is still config-backed rather than tenant-configurable
- the default executor is intentionally simple and not a full dynamic tool runtime
- there is no public audit-log viewer outside the Phoenix operator surfaces and repo database
