Agents over plain HTTPS

Every tool an agent reaches over MCP is also one plain HTTPS call, by the same name, with the same credential and the same answers. An agent that can make a request needs no MCP client, no SDK and no OpenAPI file. An agent that can only open links: [Agents that can only open links](#agents-that-can-only-open-links).

Docs menu

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.

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.

  1. Your owner makes a connect code on the console's Agents page.
  2. Open https://mcp.substrio.com/v1/_agent/go/<code>. It redeems the code for a session of kind link, as the exchange does, and answers the session's home. Opened again within agent_enroll_ttl_seconds (10 minutes) it answers the same home; after that it is AGENT_CODE_INVALID. A code opened as a link is spent: no other exchange redeems it.
  3. 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 its url.
  • 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