The Platform MCP Server
Build and operate integrations with coding agents through the platform MCP server.
The previous pages showed your integrations serving MCP. The platform also is an MCP server: it exposes the whole authoring lifecycle — read the runtime schema, draft and validate YAML, save integrations, run flows, read logs — as MCP tools. Connect a coding agent like Claude Code to it and the agent can build, test, and debug integrations against your live platform, in a conversation.
How to connect
The platform serves MCP at /mcp on its public origin (streamable HTTP;
SSE is not supported). The endpoint is an OAuth 2.1 protected resource:
claude mcp add --transport http octo https://<your-platform-host>/mcpThe first request without a token gets a 401 whose WWW-Authenticate
header points at /.well-known/oauth-protected-resource/mcp (RFC 9728); the
client discovers the authorization server there, walks you through sign-in,
and retries with a bearer access token. This is exactly the handshake
described in Secure an MCP Server with OAuth — the platform
follows its own advice. An octo_… API key sent as the bearer is also
accepted.
Each MCP session gets its own namespace for runs, so concurrent clients drive independent dev runners without stepping on each other.
The standalone app exposes the same MCP handler at its local /mcp
route, unauthenticated — it is local-only, like the rest of the standalone
editor. Point a client at http://localhost:<port>/mcp on the standalone
origin and every tool below works the same way, backed by the local disk
store.
What the server exposes
Authoring tools
| Tool | What it does |
|---|---|
list_integrations | Every saved integration as { id, name }. |
open_integration | One integration by id: { id, name, definition } — the runtime YAML. |
list_flows | The flows in an integration as { name, source } (source is the trigger type, or null for a sourceless flow). |
validate_definition | Validate draft YAML against the runtime schema without saving; returns descriptive errors. |
create_integration | Create an integration from a name and a YAML definition. |
update_integration | Overwrite an integration's whole definition (and optionally rename it). |
Flow tools
An integration is one YAML file holding many flows. These edit one of them, leaving
every other byte of the file — the other flows, the connectors, the comments — exactly as
it was. Reach for them whenever you are changing a single flow: update_integration would
have you reproduce the whole file from memory, and silently lose whatever you got wrong.
Each mutating tool validates the spliced result before saving, so an edit that would leave the integration unloadable is refused rather than persisted.
| Tool | What it does |
|---|---|
get_flow | One flow's YAML, exactly as it appears in the file. Hand it back to update_flow to edit it. |
add_flow | Append a new flow. Errors when the name is already taken. |
update_flow | Replace a flow in place. Giving the replacement a different name renames it where it sits. |
delete_flow | Remove a flow by name. |
In each, definition is the YAML of that flow alone — a mapping starting with name:
— not the whole file and not a flows: list.
Run and debug tools
| Tool | What it does |
|---|---|
can_start_integration | Pre-flight: is a runner available, does the definition validate? |
run_integration | Start (or restart) the integration in the dev runner, optionally injecting env vars; returns a test URL for networked integrations. |
invoke_flow | Run one named flow once — from a saved id or an inline definition — with optional JSON data, vars and env, without starting sources. Returns { ok, timedOut, dropped, output, logs }, where output is the flow's result message ({event_id, variables, body}) as JSON text. Takes breakAt, spies and mocks — see below. |
evaluate_cel | Evaluate a single CEL expression against sample body/vars/env without running a flow. |
get_run_logs | The session's runner log buffer, as plain text. |
stop_integration | Stop the session's running integration. |
Debugging a flow with invoke_flow
Three features compose on one run, each addressing a block by the same path grammar —
<flow>.<block>, descending into a composite with a bracketed branch: orders.charge,
orders.checkHeader[else].api-call, orders[error].notify.
breakAt— run until this block, then stop. The result carriesbreakpoint: { reached, block, message, error }.reached: falsemeans the flow took another branch: a normal outcome, not an error.spies— record every message crossing these blocks, without changing what the flow does. The result carriesspies: [{ address, records }], where each record shows what the block received and what it produced — including the two outcomes that are not a message: it dropped, or it failed.seqorders records across all spies, so a multi-spy trace reads as one timeline.mocks— stand in for these blocks so the real one never runs. This is how a flow whose blocks call a payment API or an LLM gets exercised without one.
Two rules the runtime enforces, and both are easy to get wrong:
A mock replaces its target, so an unmatched message fails the block. It does not fall
through to the real block, because there is no real block left — keeping one wired would
let a "mocked" HTTP call still reach the network, which is the failure mocking exists to
prevent. Give a default, or a trailing case with when: "true", unless you mean an
unmatched message to be an error.
Each case does exactly one thing — body, error, or drop — because a block either
returns a message, fails, or filters it out. vars only goes alongside a body.
{
"id": "orders-api",
"flow": "checkout",
"data": "{\"amount\": 250}",
"spies": ["checkout.validate"],
"mocks": {
"checkout.charge": {
"cases": [{ "when": "body.amount > 100", "error": "card declined" }],
"default": { "body": { "charged": true } }
}
}
}You cannot breakAt or spy a block inside a mocked block: the mock deleted that
subtree, so nothing there can ever run, and the request is rejected rather than silently
reporting nothing.
Documentation tools
| Tool | What it does |
|---|---|
getSchema | No argument: a compact index of every block and connector type. With elementName (e.g. "log", "http"): that element's full spec, including its fields — and, for a composite, the addressBranches a breakpoint/spy/mock address needs to descend into it (handle-errors[process].<block>). |
getExamples | No argument: an index of worked example integrations. With a slug: that example's full runtime YAML. |
getCelFunctions | The CEL variables and custom functions in scope for message expressions; pass a functionName for one entry. |
These exist as tools (not only resources) because some MCP clients surface only tools; they let an agent fetch exactly one block spec or one example instead of wading through the whole schema.
Resource tools
| Tool | What it does |
|---|---|
list_env_keys | Every env var an integration expects, as { name, default, required, source } — declared vars plus keys found in env resource files. |
list_resources | An integration's stored resources as { id, kind, name } (kind is env or template). |
open_resource | Read one resource's content. |
create_resource | Create an env file or a template on an integration. |
update_resource | Update a resource's content, kind, or name. |
delete_resource | Delete a resource. |
MCP resources and prompts
For clients that consume them, the same catalogues are also published as
resources — octo://runtime/schema (the capability catalogue, generated from
the actual runner binary, so it reflects exactly what your deployment
supports), octo://examples (the example index), and
octo://examples/<slug> (each example's YAML). Two prompts,
create-integration and write-effective-integrations, give a connected
agent step-by-step authoring guidance and design best practices.
The authoring loop
Here is what a session with a coding agent actually looks like — ask Claude Code to "build me an integration that posts a daily summary to Slack" and watch it run this loop:
Orient. The agent calls list_integrations to see what exists, and
open_integration to read anything related.
Learn the vocabulary. It calls getSchema for the index of block and
connector types, getSchema("slack-send-message") (and friends) for exact
fields, and getExamples to find and adapt a worked example — instead of
guessing YAML syntax from training data.
Draft and validate. It writes a definition and calls
validate_definition until the errors are gone — no save, no side effects.
Save. create_integration (or update_integration on iteration)
persists the definition. list_env_keys reveals which env vars still need
values; the agent asks you for secrets and passes them per-run or stores
them with create_resource as an env file.
Test one flow fast. invoke_flow runs a single flow with sample data —
no sources started — and returns the output and logs in one call. This is
the tight inner loop: tweak, invoke, read, repeat. evaluate_cel checks a
gnarly expression in isolation.
Run for real. run_integration boots the integration in the dev runner
and returns a test URL when it serves HTTP. The agent exercises the
endpoints, reads get_run_logs for the runtime's own load errors and log
output, fixes, and re-runs. stop_integration tears it down.
Validation is best-effort by design: a clean validate_definition is not a
guarantee the runtime loads the definition, and the pre-flight can flag YAML
the runtime accepts. The runtime is the final judge — which is why the loop
ends at get_run_logs, not at the validator.
Related pages
- Expose an MCP Server — make your own integrations MCP
servers with
mcp-router. - Secure an MCP Server with OAuth — the same RFC 9728 handshake the platform endpoint uses, applied to your integrations.
- Capstone: A Production Slack Agent — the kind of integration a connected coding agent can build and debug through these tools.