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 withbaseUrl 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.
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 intools.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
baseUrldefaults to the production automation URL.requestsendsAuthorization: Bearer <apiKey>, parses the automation envelope and returnsdata.- A non 2xx response or
success: falsethrowsPerformApiErrorwith the lane’smessageand the HTTP status. A network failure throws with status0. A non JSON body throws with the first 200 characters.
The hosted endpoint
POST /mcp in mcp.routes.ts.
- Reads
Authorization: Bearer. A missing key or one without thepf_live_prefix answers401withWWW-Authenticate: Bearer realm="Perform", error="invalid_token"and a JSON-RPC error body. MCP clients read that header to work out how to authenticate. - Builds a
createPerformClientwith the key and the loopback URL. - Creates a new
McpServernamedperformand registers every tool. - Creates a
StreamableHTTPServerTransportwith no session id generator, which puts it in stateless mode. - Connects and calls
transport.handleRequest(req, res, req.body). - Closes the transport and the server when the response closes.
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.
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.