MCP
OKE serves MCP on :6535 so agents can operate a running app, and apps consume external MCP servers as allowlisted tools on the same fx.call path.
Two directions, same execution model
MCP is a surface of the model, not the model itself.
Two genuinely distinct things share the protocol in OKE:
- Gate Element (MCP Provider role): Your app exposes Flows as MCP tools on port 6535 (or via
mcp.tool()+ OAuth 2.1 Authorization Server) so external AI agents (Claude, ChatGPT) can read the Manifest and call declared Flows. - AI Element (MCP Client role): Your app consumes external MCP tool servers via
ai.mcpServer(...), routing tool calls throughfx.callinside prompts and agents.
OKE does not create separate security models for users, operators, and agents. They enter the same execution model through different triggers and planes.
The server on 6535 speaks JSON-RPC over HTTP (MCP protocol 2024-11-05), requires a Bearer token even on localhost, and never forwards that token upstream — adapters receive structured operator ids instead.
The one rule
MCP inherits the operator's capability and can never exceed it. Server-level controls alone are exactly where the confused-deputy problem lives, so access descends to the tool, the operation, and each parameter.
Read tools
Default-safe — they return inert data envelopes, never live handles:
| Tool | Returns | Scope (any of) |
|---|---|---|
oke.manifest.get | The current Manifest catalogue | mcp:manifest:read · console:manifest:read |
oke.schema.get | In/out/error schemas for one flow (flowId) | mcp:schema:read · console:manifest:read |
oke.effects.get | Declared effects for one flow (flowId) | mcp:effects:read · console:manifest:read |
oke.traces.list | Recent runs (limit ≤ 200, optional flowId) | mcp:traces:read · console:runs:read |
oke.traces.get | One run/trace record (runId) | mcp:traces:read · console:runs:read |
Write tools — confirmed, every single call
Two actions are write-class, and each call needs a fresh, single-use confirmation — there is no session-level consent cache to leak:
| Tool | Does | Scope (any of) |
|---|---|---|
oke.action.invoke | Invoke a flow by id (sensitive) | mcp:action:invoke · console:flows:invoke |
oke.action.structural_propose | Propose a structural diff — reviewable, not applied | mcp:action:structural · console:structural:propose |
Request a confirmation token
oke.action.confirm with the target tool, the exact args, and a human reason — it returns a single-use token.
Call the write tool
Pass the token as confirmToken plus the phrase CONFIRM in confirmation. Token, phrase, args, and principal must all match what was confirmed.
The token dies
Consumed tokens cannot be replayed; the next write needs a new confirmation. A mismatch fails with write tool requires fresh human confirmation and the confirmVia hint.
Structural changes arrive as diffs
oke.action.structural_propose is how an agent suggests a file change: it takes title, relativePath, and contents, and produces a reviewable diff for a human — it is never applied to the tree. This is the boundary that lets an agent propose boldly while a human stays the one who merges.
The security model
| Layer | Enforcement |
|---|---|
| Authentication | Bearer session token required — even on 127.0.0.1 |
| Scope inheritance | console:* / mcp:* expand to every declared tool scope |
| Per-tool ACL | Each tool declares required scopes + read/write class |
| Per-parameter rules | maxLength, enum allow-lists, forbidden parameters |
| Token hygiene | Caller token never forwarded — structured operator ids only |
| Confirmation | Single-use, per call, phrase + token + args bound |
Docs MCP — a second, docs-only server
oke dev also boots okengine-docs-mcp on port 6536 — the same protocol, but exposing the documentation itself instead of a live Manifest. It is how an agent answers "how do I … in OKE?" from the real pages rather than its training data.
| Fact | Value |
|---|---|
| Port | 6536 (moves upward when busy) |
| Auth | None — public documentation, read-only |
| Endpoint | POST http://127.0.0.1:6536/mcp |
| Health | GET http://127.0.0.1:6536/health |
| Tools | oke.docs.search · oke.docs.get |
{
"mcpServers": {
"okengine-docs": { "url": "http://127.0.0.1:6536/mcp" }
}
}The docs content ships inside the okengine package, so the index your agent searches is exactly the version you have installed. If the surface cannot boot (missing content, busy port), oke dev prints Docs MCP skipped — … and continues — docs search never takes your dev session down.
Consume — ai.mcpServer
Your app can call other MCP servers. Those tools are not a second loop — they join fx.ask / ai.agent the same way a Flow tool does.
export const github = ai.mcpServer("github", {
url: "https://mcp.example/github",
auth: { bearer: githubToken },
tools: ["create_issue"], // required allowlist
});
await fx.ask(triage, input, { tools: [github.tool("create_issue")] });| Rule | Meaning |
|---|---|
| Allowlist | tools is required. Extra names from tools/list are dropped. |
| Capability | mcp:<server>/<tool> on effects.calls — undeclared throws OKE1007. |
| Transport | url (Streamable HTTP) or command + args (stdio). Not both. |
| Cancel | HTTP aborts the fetch / SSE stream. stdio sends notifications/cancelled then kills the process. |
Console draws each declared server as one AI node on the flow graph; Units chips read Call github → create_issue; traces label the effect MCP call. There is no separate MCP page or connect UI.
Learn more
- Agent contracts — what agents are taught about the system they operate
- Flow — the effects the MCP reads back