An agent that can make an HTTPS request does not need an MCP client, an SDK, or an OpenAPI file. If your agent can only open links, see Agents that can only open links.
The routes
On the platform host, https://mcp.substrio.com, an app tool specifies its
app. The same routes are available on an app's host, where tools run for that
app without naming it.
| 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. |
Send an agent session, account key, or system key as Authorization: Bearer <key>,
as on /mcp. Without a credential, only POST /v1/_agent/tools/substrio_connect
with {"code": "..."} is available. It performs the exchange above by tool name.
An app tool specifies its app with "app": "<slug>" in the body. The call runs
for that app and returns the same result. substrio_route_call returns 200
with the route response 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
This call creates an app in account 7 with an agent session or account key.
curl -X POST https://mcp.substrio.com/v1/_agent/tools/substrio_account_app_create \
-H "Authorization: Bearer $SUBSTRIO_KEY" -H 'Content-Type: application/json' \
-d '{"id": 7, "slug": "my-app"}'
# 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 links with GET requests and cannot supply headers or a body. They can also access tools through these links. The session secret is in the path, so the link itself is the credential.
- Your owner makes a connect code (
POST /v1/_accounts/{id}/agent/enrollments; the console makes none). - 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 that requires your owner's approval returns APPROVAL_REQUIRED (403)
with an approve_url for the console page. Ask your owner to approve it there,
then open the same link again to run the call once.
A link has the same lifetime and idle limits as a bearer session
(agent_bearer_max_seconds and agent_bearer_idle_seconds). It is listed and
revoked with the account's agent sessions under kind link. An ended link
returns 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