MCP
Long Tail is an MCP server and an MCP client. As a server, it gives any agent its queues, workflows and diagnostics at /mcp. As a client, it turns the tools of any registered MCP server into durable activities your workflows call. This page covers both directions.
Connect Claude Code
Section titled “Connect Claude Code”One command, then sign in once in the browser:
claude mcp add --transport http long-tail https://lt.acme.dev/mcpRun /mcp in Claude Code and pick Authenticate. The consent page shows the app, your account and the return address, and offers two grants:
| Grant | The client may |
|---|---|
| Allow read-only | Read what you can read and run workflows marked read-safe |
| Allow as me | Do anything you can do: claim, resolve, invoke and administer wherever your roles allow |
Allow as me appears only when it grants more than read-only. Either way the client acts as you: results are limited to your roles and every action is recorded under your name.
Then ask in plain language:
How many escalations are pending, by role?Find the escalation where orderId is ord-123 and show its status.Claim escalation <id> and resolve it as approved.Diagnose workflow <id>: is it waiting normally or stuck?The endpoint
Section titled “The endpoint”| URL | POST https://<host>/mcp |
| Transport | Streamable HTTP, stateless: one request, one response per call |
| Auth | Authorization: Bearer <token>: an OAuth access token, a service-account key or a signed user token |
Access tokens last 5 minutes and refresh silently. The refresh token rotates on each use and lasts 30 days. Every request reads the grant, so a disconnected app, a revoked token or a deactivated account stops on the next call. Disconnect a client under Connected apps in the user menu.
Enable the OAuth server
Section titled “Enable the OAuth server”Turn it on with auth.oauthServer in start(), or LT_OAUTH_ISSUER in the environment:
await start({ database: { connectionString: process.env.DATABASE_URL }, auth: { oauthServer: { issuer: 'https://lt.acme.dev', allowedRedirectUris: ['https://claude.ai/api/mcp/auth_callback'], }, },});Clients register themselves (RFC 7591). Loopback redirects on any port are always allowed, which covers Claude Code and Claude Desktop. An https redirect must equal an entry in allowedRedirectUris. Authorization responses carry iss (RFC 9207).
Machines and other environments
Section titled “Machines and other environments”A service account connects without a browser. Create one under Accounts > Service Accounts, give it the roles it should act under, and generate a key with a scope. The raw key is shown once. The same is available as tools: create_bot_account, then create_bot_api_key.
List one entry per environment in .mcp.json, with keys read from your shell:
{ "mcpServers": { "longtail-local": { "type": "http", "url": "http://localhost:3000/mcp", "headers": { "Authorization": "Bearer ${LONGTAIL_LOCAL_KEY}" } }, "longtail-staging": { "type": "http", "url": "https://staging.acme.dev/mcp", "headers": { "Authorization": "Bearer ${LONGTAIL_STAGING_KEY}" } }, "longtail-prod": { "type": "http", "url": "https://lt.acme.dev/mcp", "headers": { "Authorization": "Bearer ${LONGTAIL_PROD_KEY}" } } }}Each server keeps its tools under its own name, so you choose an environment by naming it: “on production, find orphaned signals”, “reproduce it locally”. Every listed server is live at once, so give production a read key by default.
Scope times role
Section titled “Scope times role”Two checks decide what a connected agent can do. Both must pass.
Scope decides read or write. A key or grant is mcp:read (list, search, diagnose, export) or mcp:full (also create, claim, resolve, invoke, configure). mcp.exposure.readOnly in start() limits every caller to read-safe tools, and allowServers and denyServers hide whole servers.
Role decides which tools and which records. Each tool declares a gate, the same gate its REST twin uses, and appears only to accounts that hold it:
| Gate | Held by | Covers |
|---|---|---|
| Caller | Any account | Escalations, tasks, workflow invocation, scan codes, docs, reads of users, roles and configuration |
| Admin | Admin or superadmin | Workflow configuration, diagnostics, pruning, role assignment |
| Builder | Superadmin or the engineer role |
Users, bot accounts, knowledge, YAML workflows, agents, topics, MCP servers, file and HTTP tools |
| Role manager | Admin, superadmin or engineer |
Roles, personas, scan rules, announcements |
| Superadmin | Superadmin | The human-queue tools workflows use, other users’ OAuth connections |
An account with mcp:full and only the reviewer role resolves reviewer escalations. It does not route work to finance or manage users.
Read-safe workflows
Section titled “Read-safe workflows”A workflow marked read-safe is invocable by read-scoped callers through invoke_workflow_read_safe. The flag defaults off and fails closed:
workers: [ { taskQueue: 'lookups', workflow: priceLookup, config: { readSafe: true } },],A workflow without the flag fails with a clear error when called this way.
Humans as tools
Section titled “Humans as tools”The long-tail-human-queue server exposes the escalation queue as MCP tools. An agent routes work to a person the same way it calls any other tool:
| Tool | Does |
|---|---|
escalate_and_wait |
Creates an escalation and pauses until it is resolved |
escalate_to_human |
Creates an escalation and returns its id |
check_resolution |
Reads whether an escalation is resolved, and its payload |
get_available_work |
Lists pending, unclaimed escalations for a role |
claim_and_resolve |
Claims and resolves in one atomic call |
resolve_escalation |
Resolves an escalation that is already claimed |
resolve_batch_item, accumulate_item, remove_item |
Fill a batch item, add to or remove from an open accumulator |
escalate_and_wait takes role, message, and optionally data, form_schema, assigned_to, type, subtype, priority (1 highest, 4 lowest) and schema_version. Setting assigned_to pre-claims the row for one person, which is how a one-time user gets a single-item surface.
claim_and_resolve takes escalation_id, resolver_id, payload (resumes the workflow) and optional metadata, which merges outcome facets into the row where they stay queryable.
These tools act with full authority, so /mcp lists them for superadmins. Workflows and agents inside Long Tail call them directly. An external agent with another role works the queue through the role-checked tools: find_escalations, claim_escalation, admin_resolve_escalation.
Tools as durable activities
Section titled “Tools as durable activities”Register an MCP server and its tools become proxy activities. Each call is checkpointed: if the process crashes after the tool returns, the replay reads the stored result and the server is not called again.
import { Durable } from '@hotmeshio/hotmesh';import { McpClient, type LTEnvelope } from '@hotmeshio/long-tail';
const tools = await McpClient.toolActivities(serverId);const { mcp_doc_analyzer_classify } = Durable.workflow.proxyActivities<typeof tools>({ activities: tools, retry: { maximumAttempts: 3 },});
export async function classifyDocument(envelope: LTEnvelope) { const result = await mcp_doc_analyzer_classify({ content: envelope.data.content }); if (result.confidence >= 0.85) return { type: 'return', data: result }; return { type: 'escalation', data: result, message: 'Low confidence', role: 'reviewer' };}Activity names are mcp_{serverName}_{toolName}, with non-alphanumeric characters replaced by underscores.
Register a server
Section titled “Register a server”Write the server in your app and pass a factory. It connects on first tool call over an in-memory transport and can use your database pool and services.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import { z } from 'zod';import { registerMcpTool } from '@hotmeshio/long-tail';
export function createImageToolsServer(): McpServer { const server = new McpServer({ name: 'image-tools', version: '1.0.0' }); registerMcpTool(server, 'resize_image', 'Resize an image.', { path: z.string().describe('Path to the image'), width: z.number().optional(), }, async (args: any) => ({ content: [{ type: 'text', text: JSON.stringify(await resize(args)) }], })); return server;}await start({ database: { connectionString: process.env.DATABASE_URL }, mcp: { serverFactories: { 'image-tools': createImageToolsServer } },});registerMcpTool wraps the SDK’s tool() so TypeScript skips its deep Zod inference. Inputs are still validated at call time. Pass { factory, config } instead of a bare factory to seed the server’s tags and tool manifest into the dashboard at startup.
Spawn a process. An npm package needs no wrapper:
curl -X POST https://lt.acme.dev/api/mcp/servers \ -H "Authorization: Bearer $LT_TOKEN" -H 'Content-Type: application/json' \ -d '{ "name": "filesystem", "transport_type": "stdio", "transport_config": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"] }, "tags": ["files", "storage"], "auto_connect": true }'For production, install the package and point command at node with the path under node_modules, so the lockfile pins it.
Point at a URL with sse or streamable-http. headers are sent with every request:
{ "name": "pricing", "transport_type": "streamable-http", "transport_config": { "url": "https://pricing.acme.dev/mcp", "headers": { "Authorization": "Bearer <token>" } }, "tags": ["pricing"]}The dashboard wizard (Servers & Tools > Register Server) collects the same fields: name, transport_type, transport_config, auto_connect, tags, compile_hints and credential_providers. POST /api/mcp/servers/test-connection lists a server’s tools without saving. POST /api/mcp/servers/:id/connect connects and caches the tool list. Registering, testing and connecting take builder access.
| Field | Use it for |
|---|---|
tags |
Tag-based tool discovery. Use lowercase, hyphenated tags. |
compile_hints |
Ordering, timeout or output guidance for the workflow compiler. |
credential_providers |
Providers a tool needs per user. A missing credential raises MissingCredentialError naming the provider. See OAuth and delegation. |
auto_connect |
Connect at every boot. |
Connect two Long Tail instances
Section titled “Connect two Long Tail instances”One instance calls another’s tools by registering the other’s /mcp as a remote server.
-
On the remote, create a service account with only the roles the connection needs, and a key with
mcp:readormcp:full. -
On the caller, register
https://<remote-host>/mcpwithtransport_type: "streamable-http"and the key intransport_config.headers. -
Test, then connect. With
auto_connect: trueit reconnects at every start.
The remote decides what the connection can do: it lists only the tools that account’s roles allow, applies the key’s scope and its own mcp.exposure, and records every action as that account. The caller decides who may use it: calling an external server’s tools directly is a builder capability. Everyone else reaches the remote through a deployed workflow with its own invocation roles.
Rotate a key by generating a new one on the remote, updating the server’s headers (PUT /api/mcp/servers/:id), then revoking the old key.
Built-in servers
Section titled “Built-in servers”Long Tail ships its API as MCP servers. The main ones:
| Server | Covers |
|---|---|
long-tail-admin |
Escalations, tasks, workflows, agents, users, roles, diagnostics |
long-tail-human-queue |
Create, wait on and resolve escalations from workflows |
long-tail-knowledge |
Persistent JSONB knowledge store |
long-tail-events |
Publish events and browse the topic catalog |
long-tail-file-storage |
Managed file storage |
long-tail-http-fetch, long-tail-schema-exchange |
Outbound HTTP, schema-validated exchange with credential resolution |
long-tail-oauth |
OAuth token management |
long-tail-docs |
Documentation search |
long-tail-vision, long-tail-translation, long-tail-claude-code |
AI tools (need an LLM API key) |
Every tool, its gate and its read-safe flag are in the MCP reference.
