/mcp, outside /v1. Router mcpRouter in src/modules/mcp/mcp.routes.ts, mounted in src/app.ts behind the same rate limiter instance as /v1. Tool definitions live in the package @perform/mcp-tools (backend/packages/mcp-tools/src/tools.ts), which a stdio build of the server shares.
Production URL: https://perform-api.otherwise.co.il/mcp
How it works
The endpoint is a stateless MCP server over Streamable HTTP, built with@modelcontextprotocol/sdk.
- Each
POST /mcprequest builds a newMcpServernamedperform(version1.0.0) and a newStreamableHTTPServerTransportwith no session id generator. There is no session, no stream to resume and no affinity between requests. - Every tool from
@perform/mcp-toolsis registered with its Zod input schema and annotations (readOnlyHintfrom the tool,destructiveHint: false,openWorldHint: true). - When a tool is called, the handler does not touch the database. It builds an HTTP request and sends it to
/v1/automationon the same process, athttp://127.0.0.1:<PORT>/v1/automation, with the caller’s own API key. - The automation lane’s
datais returned to the model as pretty-printed JSON text. An error from the lane is returned as a tool result withisError: trueand the lane’s message as text.
Authentication
Send a studio API key as a bearer token. It is the same key used for the automation and partner lanes, created by anOWNER or HEAD_COACH. See Partner API for key management.
pf_live_. The key is really verified when a tool makes its call to /v1/automation. So a well-formed but invalid key can connect and list tools, and every tool call then fails with invalid partner api key.
A typical client configuration:
Endpoints
POST /mcp
Handles one MCP JSON-RPC message: initialize, tools/list, tools/call and the rest of the protocol the SDK implements.
Auth: studio API key.
The body is a JSON-RPC 2.0 message. The response is JSON-RPC, as JSON or as a server-sent event stream, depending on what the transport negotiates with the client’s Accept header.
Listing tools:
data as text:
GET /mcp
Not supported. The server is stateless, so there is no stream to open.
Response: 405.
/mcp.
Tools
The tool set is limited on purpose. A model acts without a person approving each step, so anything that destroys data or reaches a trainee’s phone is left out. The comment at the top oftools.ts lists what is excluded: deleting a trainee, sending a message, freezing a plan and cancelling a plan. Webhooks are not exposed either.
Read tools
limit is 1 to 200 and offset is 0 or more on every tool that takes them.
Write tools
Tool inputs are validated twice: by the tool’s own Zod schema in the MCP layer, then by the automation lane’s schema. Tool schemas use real JSON types (numbers, booleans, arrays), not the string coercion Make needs.
send_form with notify: true is the one tool that can push a notification to a trainee’s device straight away. A PDF_SIGNATURE form sends nothing and returns a signing link for the coach to deliver.
Limits and operations
- Rate limit.
/mcpuses the same limiter instance as/v1: 120 requests per 60 seconds per IP by default (RATE_LIMIT_MAX,RATE_LIMIT_WINDOW_MS). - Internal calls are also limited. A tool call makes a second request to
/v1/automationfrom127.0.0.1. That request passes through the same limiter keyed by its own source address, so tool calls from every MCP user share the loopback address’s bucket. Under heavy combined use, tool calls can be throttled even when each client is under its own limit. - Body size. The global 24 MB JSON limit applies.
- Audit. Writes are recorded in
AuditLogby the automation lane with the API key id as the actor. Nothing marks an entry as having come through MCP. - Revoking access. Revoke the API key. The next tool call fails.
- Logging. A failure inside the handler is logged as
mcp request failed.
Adding a tool
- Add the endpoint to the automation lane first, with its schema, service function and readable errors.
- Add a
ToolDefinitiontobackend/packages/mcp-tools/src/tools.ts:name,title,description,readOnly, a ZodinputSchemaand acallfunction that returns the method, path and body. - Decide what the tool must force. Anything that notifies a trainee should default to silent.
mcp.routes.ts. It registers every tool in the exported list.