Perform exposes a studio’s data to AI clients through the Model Context Protocol. There are two transports and one tool set. All three execute a tool the same way: an HTTP call to the automation API (/v1/automation) with a studio API key. One key means one studio, so every tool is already scoped and cannot reach another studio’s data.

Why tools call HTTP on the same process

The hosted endpoint builds its client with baseUrl set to http://127.0.0.1:<PORT>/v1/automation. That loopback hop is deliberate:
  • The MCP surface cannot drift from the public API, because it is the public API.
  • Tool calls inherit the automation lane’s key check, validation and error wording.
  • The lane’s errors are written as sentences that say what to do next, which is exactly what a model needs to correct itself.

@perform/mcp-tools

tools

tools: ToolDefinition[] is the single list every transport registers.
A tool does not perform the request. call maps validated input to a method, a path under /v1/automation and an optional body. The transport makes the request. That keeps tools pure and testable.

Read tools

List tools share limit (1 to 200, default 50 on the server) and offset.

Write tools

create_trainee takes an externalId. Models retry, and with that set a repeat call returns the trainee that already exists. onDuplicatePhone can be return_existing or update.

Scope limits

The header comment in tools.ts states the rule. A model acts without a human checking each step, so anything that destroys data or reaches a real person is left out. Assigning a plan or a program can notify, so those tools force notify: false. WhatsApp invites are never sent from MCP. Those actions exist on /v1/automation for callers that decide to use them. They are not offered to a model. Every tool is registered with destructiveHint: false, openWorldHint: true and readOnlyHint from the definition.

createPerformClient

  • baseUrl defaults to the production automation URL.
  • request sends Authorization: Bearer <apiKey>, parses the automation envelope and returns data.
  • A non 2xx response or success: false throws PerformApiError with the lane’s message and the HTTP status. A network failure throws with status 0. A non JSON body throws with the first 200 characters.
Errors are passed through as text on purpose. Do not reshape them into codes.

The hosted endpoint

POST /mcp in mcp.routes.ts.
  1. Reads Authorization: Bearer. A missing key or one without the pf_live_ prefix answers 401 with WWW-Authenticate: Bearer realm="Perform", error="invalid_token" and a JSON-RPC error body. MCP clients read that header to work out how to authenticate.
  2. Builds a createPerformClient with the key and the loopback URL.
  3. Creates a new McpServer named perform and registers every tool.
  4. Creates a StreamableHTTPServerTransport with no session id generator, which puts it in stateless mode.
  5. Connects and calls transport.handleRequest(req, res, req.body).
  6. Closes the transport and the server when the response closes.
Stateless means every request builds its own server and transport. There is no session affinity and nothing to keep alive. GET /mcp answers 405 with This server is stateless; use POST. The prefix check in step 1 is only a shape check. The real validation happens when the first tool call reaches /v1/automation and requirePartnerKey looks the key up. A tool failure is returned to the model as a tool result with isError: true and the message as text. A failure in the transport itself is logged as mcp request failed and answered with JSON-RPC error -32603. /mcp is behind the /v1 rate limiter, keyed by IP.

Connecting a client

A coach creates an API key in the web app under Business settings, Integrations. The key is shown once.

The stdio binary

apps/mcp-server/src/index.ts is the same registration loop over StdioServerTransport. On startup it calls client.validate() and logs the studio it connected to on stderr. A bad key fails at startup instead of on the first tool call, so the model never has to diagnose a setup problem. stdout is reserved for the protocol.
The file reads process.env directly and disables the lint rule for it. It runs on a coach’s machine and must not pull in the server’s config schema, which demands a database.

Adding a tool

1

Make sure the automation route exists

A tool is a view of an automation API route. Add the route first if it is missing, with sentence style error messages.
2

Check it against the scope rules

If it deletes data, sends something to a trainee or ends a subscription, do not add it. If it can notify, force the silent option in call.
3

Add the definition

Append a ToolDefinition to tools in packages/mcp-tools/src/tools.ts. Write description for a model: say when to use it, what to call first to get ids, and what a retry does. Add .describe() to every input field, with units.
4

Rebuild

pnpm --filter @perform/mcp-tools build. The hosted endpoint, the binary and the WhatsApp agent pick it up from the shared list.

Testing

The mcp-server README describes two end to end suites that run against a live API and clean up after themselves (a temporary key and a temporary trainee, both deleted): one for the hosted endpoint over HTTPS and one that spawns the stdio binary and drives it over real MCP. Run them only against an environment where creating and deleting a trainee is acceptable.