Octov0.4.2
AI

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>/mcp

The 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

ToolWhat it does
list_integrationsEvery saved integration as { id, name }.
open_integrationOne integration by id: { id, name, definition } — the runtime YAML.
list_flowsThe flows in an integration as { name, source } (source is the trigger type, or null for a sourceless flow).
validate_definitionValidate draft YAML against the runtime schema without saving; returns descriptive errors.
create_integrationCreate an integration from a name and a YAML definition.
update_integrationOverwrite 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.

ToolWhat it does
get_flowOne flow's YAML, exactly as it appears in the file. Hand it back to update_flow to edit it.
add_flowAppend a new flow. Errors when the name is already taken.
update_flowReplace a flow in place. Giving the replacement a different name renames it where it sits.
delete_flowRemove 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

ToolWhat it does
can_start_integrationPre-flight: is a runner available, does the definition validate?
run_integrationStart (or restart) the integration in the dev runner, optionally injecting env vars; returns a test URL for networked integrations.
invoke_flowRun 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_celEvaluate a single CEL expression against sample body/vars/env without running a flow.
get_run_logsThe session's runner log buffer, as plain text.
stop_integrationStop 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 carries breakpoint: { reached, block, message, error }. reached: false means 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 carries spies: [{ 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. seq orders 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 thingbody, 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

ToolWhat it does
getSchemaNo 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>).
getExamplesNo argument: an index of worked example integrations. With a slug: that example's full runtime YAML.
getCelFunctionsThe 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

ToolWhat it does
list_env_keysEvery env var an integration expects, as { name, default, required, source } — declared vars plus keys found in env resource files.
list_resourcesAn integration's stored resources as { id, kind, name } (kind is env or template).
open_resourceRead one resource's content.
create_resourceCreate an env file or a template on an integration.
update_resourceUpdate a resource's content, kind, or name.
delete_resourceDelete 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.

On this page