The routes
On the platform host, https://mcp.substrio.com, where an app tool names its
app. The same routes run on an app's own host, where every tool runs at that
app and names none.
| Route | What it does |
|---|---|
POST /v1/_agent/exchange |
Redeems a connect code ({"code": "substrio_ae_..."}) for a session. No credential. The answer's key is shown once, and next says how to use it (agent sessions). |
GET /v1/_agent/tools |
The everyday tools this credential reaches (the account tools, then the build loop's), as {"tools": [{"name", "title"}]}, filtered by its grant exactly as MCP tools/list is. ?area=all lists every tool it reaches. On an app's host, the app's own tools follow whole, as app_tools. |
GET /v1/_agent/tools/<name> |
Any one tool it reaches, listed or not: name, title, description and inputSchema (an app tool's has the app argument). |
POST /v1/_agent/tools/<name> |
Runs the tool. The body is its arguments object. The answer is the operation's own HTTP status and body. When the tool has a next step, the body ends with next: [{"tool", "why"}], plus pass and as when a field of this answer is the next tool's argument. A refusal is the standard error body, with fix when it has one. |
The credential is Authorization: Bearer <key>: an agent session, an account
key or the system key, as on /mcp. With no credential, only
POST /v1/_agent/tools/substrio_connect {"code": "..."} runs; it is the
exchange above by its tool name.
An app tool names its app with "app": "<slug>" in the body. The call runs at
that app and answers the same way. substrio_route_call answers 200 with the
route's own answer inside: {"status", "headers", "body"}.
Refusals
| Code | Status | When |
|---|---|---|
AGENT_CODE_INVALID |
401 | the connect code is unknown, already used or past its life |
AGENT_TOOL_UNKNOWN |
404 | no tool of that name reaches this credential |
INVALID_ARGUMENT |
400 | an argument is wrong; field names it (app for an app tool with none) |
AGENT_ALREADY_CONNECTED |
409 | substrio_connect with a credential that already works: keep using it; the code was not used |
APP_NOT_NAMED |
400 | a /v1 call that runs at an app, with no X-Substrio-Tenant and on a host no app serves |
CREDENTIAL_REQUIRED |
401 | a call other than substrio_connect with no credential, or with one that is not valid; a key refused at an app answers its own code there (KEY_EXPIRED, PERMISSION_DENIED, ...) |
REQUEST_TOO_LARGE |
413 | the body is larger than mcp_request_max_bytes |
Example
# 1. Redeem the code your owner gave you.
curl -X POST https://mcp.substrio.com/v1/_agent/exchange \
-H 'Content-Type: application/json' -d '{"code":"substrio_ae_..."}'
# {"id":"...","kind":"bearer",...,"key":"substrio_ab_...","next":"Keep key, shown only once. ..."}
# 2. Make an app, then commit a function with one route.
curl -X POST https://mcp.substrio.com/v1/_agent/tools/substrio_account_app_create \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"id": 7, "slug": "my-app"}'
curl -X POST https://mcp.substrio.com/v1/_agent/tools/substrio_version_commit \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"app":"my-app","message":"hello","units":[
{"path":"functions/hello.js","text":"module.exports = async () => new Response(\"hi\");"},
{"path":"functions/hello.json","text":"{\"routes\":[\"GET /hello\"]}"}]}'
# {"number":1,...,"next":[{"tool":"substrio_sandbox_create","why":"..."},...]}
# 3. Run it in a sandbox, check it, make it live.
curl -X POST .../v1/_agent/tools/substrio_sandbox_create -d '{"app":"my-app","name":"dev"}' ...
curl -X POST .../v1/_agent/tools/substrio_live_set -d '{"app":"my-app","version":1,"sandbox":"dev"}' ...
curl -X POST .../v1/_agent/tools/substrio_route_call -d '{"app":"my-app","method":"GET","path":"/hello","sandbox":"dev"}' ...
curl -X POST .../v1/_agent/tools/substrio_live_set -d '{"app":"my-app","version":1}' ...
curl https://my-app.substrio.com/hello
The same loop over MCP is in agents.md; the tools themselves are in MCP.
Agents that can only open links
Some agents can only open a link: a GET with no header of their own and no body (ChatGPT on the web is one). They reach every tool too. The session's secret is in the link's path, so the link itself is the credential.
- Your owner makes a connect code on the console's Agents page.
- Open
https://mcp.substrio.com/v1/_agent/go/<code>. It redeems the code for a session of kindlink, as the exchange does, and answers the session's home. Opened again withinagent_enroll_ttl_seconds(10 minutes) it answers the same home; after that it isAGENT_CODE_INVALID. A code opened as a link is spent: no other exchange redeems it. - Every URL the answers give starts with the link,
https://mcp.substrio.com/v1/_agent/link/<link>. Open only those.
| Link | What it does |
|---|---|
GET /v1/_agent/go/<code> |
Redeems the code and answers the home (below). |
GET /v1/_agent/link/<link> |
The home: account (id, name), approvals_waiting (your owner's calls a person must approve: id, tool, summary, expires_at), tools ([{"name", "title", "url", "about"}], the everyday tools this session reaches), head_max_bytes, and every answer's members (below). |
GET /v1/_agent/link/<link>/<tool>/about |
One tool's definition, as GET /v1/_agent/tools/<name>: its inputSchema names the arguments. |
GET /v1/_agent/link/<link>/<tool>?<arguments> |
Runs the tool, as POST /v1/_agent/tools/<name>. Each query argument is the tool's argument of that name: a number where the schema says integer, true or false where it says boolean, else a string. What nests (units, an object, a list) goes whole in args, one JSON object, URL-encoded: args={"units":[...]}. The answer is the operation's own status and body. |
Every answer, a refusal too, adds:
owner: your owner's line (account, plan, apps, storage, the limits it meets), as an MCP result ends with it.tell_your_owner: before each call, tell your owner in one line what you are about to do; after it, what happened.next: on a success, the tools that come next, each with itsurl.home: the link's home, where every tool is listed.
A body that is not a JSON object comes as result, a string. Every link
answer is sent with Cache-Control: no-store, X-Robots-Tag: noindex, nofollow and Referrer-Policy: no-referrer.
A call your owner must approve answers APPROVAL_REQUIRED (403) with
approve_url, the console page where they approve it. Ask your owner to
approve it there, then open the same link again: it runs once.
A link lives as a bearer session does (agent_bearer_max_seconds, idle
agent_bearer_idle_seconds) and is listed, and revoked, with the account's
agent sessions, as kind link. An ended link answers CREDENTIAL_REQUIRED
(401): ask your owner for a new code.
The request's head (its line and headers) is at most head_max_bytes (4096).
A link past it is refused 431 before it reaches a tool. Write a larger file
one unit a call (substrio_version_commit), or edit it in parts
(substrio_source_edit).
| Code | Status | When |
|---|---|---|
AGENT_CODE_INVALID |
401 | the code is unknown, spent by another exchange, or past its life |
SECRET_KEY_IN_URL |
400 | a key that belongs in a header is in the URL (an account key, the system key, an agent's substrio_ab_ key), in the link's place or as access_token |
CREDENTIAL_REQUIRED |
401 | the path holds no link, or the link has ended |
AGENT_TOOL_UNKNOWN |
404 | no tool of that name reaches this link; home lists the ones that do |
INVALID_ARGUMENT |
400 | an argument is wrong (field names it), or args is not one JSON object |
APPROVAL_REQUIRED |
403 | the call needs your owner's approval: approve_url |
# The code your owner gave you, opened:
GET https://mcp.substrio.com/v1/_agent/go/substrio_ae_...
# {"account":{"id":7,"name":"Ada"},"approvals_waiting":[],
# "tools":[{"name":"substrio_account_app_create","title":"...",
# "url":"https://mcp.substrio.com/v1/_agent/link/substrio_al_.../substrio_account_app_create",
# "about":".../substrio_account_app_create/about"},...],
# "head_max_bytes":4096,"next":[...],"owner":{"type":"text","text":"Account 7, Free plan: ..."},
# "tell_your_owner":"Before each call, ...","home":"https://mcp.substrio.com/v1/_agent/link/substrio_al_..."}
# Make an app, commit a function, run it, make it live:
GET .../substrio_account_app_create?id=7&slug=my-app
GET .../substrio_version_commit?app=my-app&message=hello&args=%7B%22units%22%3A%5B...%5D%7D
GET .../substrio_sandbox_create?app=my-app&name=dev
GET .../substrio_live_set?app=my-app&version=1&sandbox=dev
GET .../substrio_route_call?app=my-app&method=GET&path=%2Fhello&sandbox=dev
GET .../substrio_live_set?app=my-app&version=1