MCP
Two directions — expose Flows as gated mcp.tool endpoints on :6535, and consume external servers with ai.mcpServer allowlists.
Model Context Protocol (MCP) connects OKE to Cursor, Claude Desktop, and other agents in two directions: your app exposes selected Flows as tools, and your Flows call tools on external MCP servers.
For developers wiring Cursor to a backend or letting an agent open a GitHub issue — opt-in gates inbound; allowlists outbound.
The one rule
Inbound: on(mcp.tool("name").gate(...), flow) — ungated tools are not exposed. Outbound:
ai.mcpServer with a required tools allowlist — the runtime never trusts raw tools/list.
Smallest Example — expose a Flow
Bind a gated MCP tool
import { on, flow, mcp } from "okengine";
import { z } from "zod";
import { member } from "@/core/gate";
export const create = on(
mcp
.tool("bookings.create", {
in: z.object({
guest: z.string().min(1),
night: z.string(),
}),
out: z.object({ id: z.string(), guest: z.string(), night: z.string() }),
})
.gate(member),
flow({
do: async (input, fx) => {
return { id: fx.id(), ...input };
},
}),
);Deny-by-default: on(mcp.tool("…"), flow) without .gate(...) fails gate posture at boot.
Connect a client
App MCP listens on port 6535, path /mcp (not the backend :6530 HTTP port). Bearer
auth is required.
{
"mcpServers": {
"notes": {
"url": "http://127.0.0.1:6535/mcp",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}GET /health on the same port probes liveness. Override with ports.mcp in config when needed.
Call the tool
The client lists bookings.create (and other gated tools). Invocations run the Flow under the
same gate chain as HTTP.
Smallest Example — consume a server
Declare an outbound server
import { ai, vault } from "okengine";
const githubToken = vault.secret("GITHUB_MCP_TOKEN");
export const github = ai.mcpServer("github", {
url: "https://mcp.example/github",
auth: { bearer: githubToken },
tools: ["create_issue", "list_repos"],
});
export const createIssue = github.tool("create_issue"); // mcp:github/create_issueExactly one of url (Streamable HTTP) or command (+ optional args for stdio). Bearer must
be a vault.secret handle or contract name — never a token literal in source.
Use in ask / agent tools
await fx.ask(triage, input, {
tools: [github.tool("create_issue")],
maxSteps: 2,
});Capability ref is mcp:github/create_issue. Model-facing names use server__tool
(github__create_issue).
Progressive Patterns
Chain policies like HTTP — first denial wins:
on(
mcp.tool("admin.wipe").gate(member, gate.scope("admin")),
flow({ do: async (_input, fx) => fx.json.empty() }),
);Inbound reference
| Piece | Detail |
|---|---|
| Trigger | mcp.tool(name, { in, out, errors? }) from okengine |
| Gates | .gate(...) required for exposure |
| Port | 6535 (ports.mcp) |
| Path | POST /mcp · GET /health |
| Auth | Bearer required (Bearer token required) |
Empty name throws TypeError: mcp.tool(name): name is required.
Outbound reference — ai.mcpServer
| Option | Type | Required | Meaning |
|---|---|---|---|
url | string | XOR command | Streamable HTTP endpoint |
command | string | XOR url | stdio executable (no shell string) |
args | string[] | no | Arguments for command |
auth.bearer | secret / name | no | Vault contract for Bearer |
tools | string[] | yes | Allowlist — never raw tools/list |
| Method | Returns | Meaning |
|---|---|---|
server.tool(name) | { name: "mcp:server/tool" } | Capability ref for ask / agent / fx.call |
Declare errors
| Message | Fix |
|---|---|
ai.mcpServer: name is required | Pass a non-empty server id |
name "…" must not contain "/" or "__" | Use a simple id (github, not org/repo) |
tools allowlist is required | Pass tools: ["…"] |
declare exactly one of url or command | Pick HTTP or stdio |
tool "…" is not in the allowlist | Add the name to tools before .tool(...) |
Ports
| Port | Surface |
|---|---|
| 6530 | Backend HTTP |
| 6535 | App MCP (Flows as tools) |
| 6536 | Docs MCP (read-only) |
Mnemonic: O·K·E = 6·5·3.
Troubleshooting
Clients must send Authorization: Bearer …. Missing or invalid Bearer is rejected before tool
dispatch.
The Flow is not bound with mcp.tool(…).gate(…), gates deny the identity, or the client points at
:6530 instead of :6535/mcp.
Deny-by-default posture: attach at least one gate. Public-style exposure still needs an explicit
gate policy you intend (there is no .public() on MCP triggers).
See the declare-error table above. Runtime ai.mcp: … covers unknown server, non-allowlisted
tool, and unsupported HITL prompts from the remote.
Learn more
- Agents — MCP refs in tool bags
- Gate — policies on
mcp.tool - Vault — bearer secret contracts
- AI — element overview
- Security — ports 6530 · 6533 · 6535 · 6536