---
title: "MCP"
description: "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."
icon: "Plug"
source: "docs/spec/unified-theory.md"
---

## 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:

1. **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.
2. **AI Element (MCP Client role):** Your app consumes _external_ MCP tool servers via `ai.mcpServer(...)`, routing tool calls through `fx.call` inside 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. It implements `initialize`, `ping`, `tools/list`, and `tools/call`, and advertises protocol `2024-11-05`. A client that sends another `protocolVersion` is rejected. An omitted version gets `2024-11-05`. It requires a Bearer token **even on localhost**, and never forwards that token upstream — adapters receive structured operator ids instead. The outbound client that calls external MCP servers probes `2026-07-28` (`_meta` on each request) and falls back to a `2024-11-05` `initialize`. That probe is not what this server advertises.

<Callout title="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.
</Callout>

## Read tools

Default-safe — they return inert data envelopes, never live handles. Approving a tool, resolving a review, and promoting a decision are not tools. Those stay with a person in the Console or the CLI.

| 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`       |
| `oke.ai.runs.list`      | Agent runs for the token's tenant (`limit` ≤ 200). `console:*` or `mcp:*` may omit `tenant` or name another one. PII and secrets are masked. | `mcp:ai:read` · `console:runs:read`           |
| `oke.ai.runs.get`       | One agent run plus its follow events (`runId`). Same tenant rule. Args, trail, and events are masked.                                        | `mcp:ai:read` · `console:runs:read`           |
| `oke.ai.approvals.list` | Pending tool approvals for the token's tenant. Same operator rule. Approval args are masked.                                                 | `mcp:ai:read` · `console:runs:read`           |
| `oke.decisions.list`    | Decision state, `by`, `why`, per-decider certificates, pending count, and drift. Read-only. Inputs are masked. Resolve stays in the Console. | `mcp:decisions: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` |

<Steps>

<Step>
### Ask for confirmation

The write returns an opaque `confirmationId`. It does not return a token. Confirm from a **different auth session** (`claims.sid`) than the session that will invoke. The same principal is allowed. A rejected same-session confirm leaves the pending confirmation in place. A confirmation issued on another process is unknown.

</Step>

<Step>
### Call the write tool

`oke.action.confirm(confirmationId, reason)` returns a single-use token. Pass it as `confirmToken` plus the phrase `CONFIRM`. The invoking session must be the one that asked, and it must not be the session that confirmed.

</Step>

<Step>
### 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.

</Step>

</Steps>

## 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`     |

```json
{
  "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.

```typescript
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](/docs/ai/skills) — what agents are taught about the system they operate
- [Flow](/docs/elements/flow) — the effects the MCP reads back

## Next

<Cards>
  <Card
    title="Skills"
    description="AGENTS.md and the shipped agent skills."
    href="/docs/ai/skills"
  />
  <Card
    title="llms.txt"
    description="llms.txt, llms.json, per-page markdown, /llms/agents."
    href="/docs/ai/llms-txt"
  />
  <Card
    title="AI element"
    description="Models inside your app — the other direction."
    href="/docs/elements/ai"
  />
</Cards>
