This page is the companion to MCP, Permissions & Sandbox. That page covers the safety story (who can call what, why). This page covers the catalog (what exists, how it’s wired, where parity holds and where it breaks).
Every tool an Orion agent can call falls into one of five buckets:
| # | Category | Where defined | Available on Claude SDK | Available on Pi |
|---|
| 1 | SDK built-ins | Inside @anthropic-ai/claude-agent-sdk | Native (Read, Edit, Write, Bash, Grep, Glob, WebSearch, WebFetch, Task, AskUserQuestion, TodoWrite) | Re-implemented via pi-sandbox wrappers + a few direct Pi tools |
| 2 | Internal MCP servers | handler.mjs per query | 4 servers (orion, orion-tools, canvas, terminal) | 3 servers bridged (terminal is Claude-only) |
| 3 | External MCP | User’s .mcp.json | Via loadMcpConfig() + OAuth singleton | Via the same mcpManager, brokered through proxy on strict routes |
| 4 | Pi-direct tools | engine/pi-tools.mjs | n/a | 8 hardcoded tools (knowledge_search, spawn_subagent, agents_list, agent_info, ask_user_question, TodoWrite, switch_model, mcp proxy) |
| 5 | CCW delegate | ccw/orion-tools-mcp-server.mjs | As mcp__orion-tools__delegate_to_cli | Same name (lenient routes) or via proxy (strict routes) |
The dual-engine framing: most tools work on both runtimes, but the exposure mechanism differs by route. Read on.
The four internal MCP servers (Claude side)
Created per query inside handler.mjs, wrapped via trackingCreateServer() which adds each server name to internalServers: Set<string> — that set is what canUseTool consults to auto-trust the server’s tools (no user prompt).
Bridging Claude MCP → Pi
When a Pi agent runs, the four (well, three — terminal is skipped) internal MCP servers must become Pi ToolDefinitions. The flow lives in engine/mcp-bridge.mjs:
Schema conversion uses z.toJSONSchema(zodSchema, { target: "draft-2020-12" }) at mcp-bridge.mjs:82 — openapi-3.0 is explicitly avoided because it emits legacy boolean exclusiveMinimum: true which OpenAI’s function-calling validator rejects (comment at :78-82).
terminal and codexlens are NOT extracted. extractMcpToolDefs only instantiates the three factories createMemoryMcpServer / createCanvasMcpServer / createOrionToolsMcpServer (mcp-bridge.mjs:220-272). So Pi sessions never see those tool surfaces.
The collision — knowledge_search
There is exactly one tool name that collides between the direct Pi surface and the bridged MCP surface: knowledge_search.
- Direct Pi tool:
pi-tools.mjs:166-227 — createKnowledgeSearchTool registers knowledge_search (bare name)
- Bridged MCP tool: would have name
mcp__orion__knowledge_search
The dedup loop at pi-tools.mjs:125-141 keeps a Set<baseName> of already-registered direct tools, strips the mcp__server__ prefix from each bridged tool, and skips any bridged tool whose base name is already taken. In practice this fires for exactly one tool — knowledge_search. The bridged variant is silently dropped.
Author rule (2026-05-17): In config.yaml, command frontmatter, or skill references, always use the bare name knowledge_search — never mcp__orion__knowledge_search. The prefixed form resolves on Claude but vanishes on Pi. A CI lint at scripts/lint-no-prefixed-orion-tools.mjs blocks reintroduction across prompts/{agents,commands,skills}/.
There is also a feature gap behind this name. The Claude-side mcp__orion__knowledge_search exposes 7 parameters: query, maxResults, source, entityId, minImportance, topics, includeConsolidations, includeRelated. The bare Pi knowledge_search exposes only query and maxResults (pi-tools.mjs:178-220). Advanced filtering (importance, topics, edge traversal) is Claude-only today.
The Phase 5b proxy gate
The most consequential dual-engine divergence. Lives at engine/model-router.mjs:96-123:
const OPENAI_RESPONSES_API_PROVIDERS = new Set([
'openai',
'openai-codex',
'azure-openai-responses',
]);
export function isOpenAiResponsesModel(route) {
return route.engine === ENGINE_PI
&& OPENAI_RESPONSES_API_PROVIDERS.has(route.provider);
}
In mcp-bridge.mjs:370-377, when the caller omits proxyInternalMcp, the default is derived from this function:
if (typeof proxyInternalMcp !== "boolean") {
const { isOpenAiResponsesModel } = await import("./model-router.mjs");
proxyInternalMcp = isOpenAiResponsesModel(route);
}
Result:
| Route | proxyInternalMcp default | Tool surface seen by Pi agent |
|---|
Pi openai/* (Responses API) | true | Internal MCP hidden behind a single mcp proxy tool — search → describe → call |
Pi openai-codex/* | true | Same |
Pi azure-openai-responses/* | true | Same |
Pi google/* (Gemini) | false | Full 37 mcp__server__tool names exposed upfront |
Pi openrouter/* | false | Same |
| Pi anthropic-via-Pi, deepseek, groq, mistral, etc. | false | Same |
| Claude SDK | n/a | buildPiCustomTools is Pi-only |
Why the split exists. OpenAI’s strict Responses-API validator rejects context_cache’s discriminatedUnion schema. Phase 5 (PEX-extend-mcp-proxy-internal-2026-04-28) defaulted the proxy ON unconditionally, but that forced lenient providers (Gemini, OpenRouter) to pay an extra search/describe/call round-trip for no validator benefit. Phase 5b (2026-04-29) made the default route-conditional. Escape hatch: explicit proxyInternalMcp: <bool> overrides.
Parity matrix
| Category | Claude SDK | Pi lenient (Gemini, OpenRouter, etc.) | Pi strict (OpenAI Responses) | Divergence |
|---|
Read/Write/Edit/Grep/Glob/Bash/NotebookEdit | Native SDK built-ins | pi-sandbox wrappers — bash, read, write, edit, grep, find, ls (pi-session.mjs:419-422) | Same | Pi has no NotebookEdit, no Glob (uses find/ls) |
Delegation (Task / Agent) | Native SDK | spawn_subagent direct Pi tool — modes single / chain / parallel (pi-tools.mjs:319-368) | Same | Pi gains chain + parallel modes natively |
AskUserQuestion | Native SDK | ask_user_question direct Pi tool — reuses same questionRequest IPC | Same | Functional parity |
TodoWrite | Native SDK | TodoWrite direct Pi tool — exact name match for ActivityStream detection | Same | Parity by intentional naming |
WebSearch/WebFetch | Native SDK; gated by web_search_enabled | Not in Pi sandbox toolset; reachable only via Exa MCP | Same | Asymmetric — Pi lacks SDK primitive |
mcp__orion__knowledge_search | Auto-trusted (7 params) | Dropped by dedup; bare knowledge_search available (2 params) | Hidden behind mcp proxy; bare knowledge_search still direct | Feature gap on Pi |
mcp__orion__* (other ~19) | Auto-trusted | Bridged as prefixed names | Behind mcp proxy | Tool-name surface differs by route |
mcp__canvas__* (3) | Auto-trusted | Bridged | Behind proxy | Same pattern |
mcp__orion-tools__* (13, incl. delegate_to_cli) | Auto-trusted | Bridged | Behind proxy | Same pattern |
run_in_terminal | Per-command user prompt | Not registered | Not registered | Claude-only |
mcp__codexlens__* | Optional, project-conditional | Not extracted by bridge | Not extracted | Claude-only (unverified whether reachable via external mcpManager) |
switch_model | Not present | Direct Pi tool (pi-tools.mjs:1062), gated on capability | Same | Pi-exclusive |
agents_list / agent_info | Not present (SDK auto-loads via folder) | Direct Pi tools (pi-tools.mjs:1361, :1408) | Same | Pi-exclusive — replaces ~120K-token agent catalog injection |
Three permission layers
Quick reference — full treatment lives on the MCP, Permissions & Sandbox page.
| Layer | Claude SDK | Pi |
|---|
| L1 — OS sandbox | SDK’s additionalDirectories allowlist, sandbox-exec on macOS | pi-sandbox/ module: assertSandboxPath + path-policy + bash-wrapper via @carderne/sandbox-runtime (same primitive Claude uses internally) |
| L2 — Permission middleware | createCanUseTool(ctx) closure factory (permissions/canUseTool.mjs:117) — internalServers Set auto-trusts internal MCP | Same closure passed in; cron mode (sessionType === 'cron' OR !canUseTool) sets noUserAvailable: true → all sandbox prompts auto-deny |
| L3 — Capability prefs | applyCapabilityPreferences() adds tools to disallowedTools | pi-session.mjs:425-438 filters the tools array before createAgentSession |
L3 toggles that apply on both engines:
| Preference | Effect on Claude | Effect on Pi |
|---|
web_search_enabled === 'disabled' | Adds WebSearch/WebFetch to disallowedTools | Pi has no native WebSearch — gating relies on external MCP visibility |
file_edit_mode === 'read_only' | Adds Write/Edit/NotebookEdit to disallowedTools | Filters write, edit from tool array (pi-session.mjs:427, :432) |
file_edit_mode === 'auto_accept' | Sets permissionMode: 'acceptEdits' | n/a |
code_execution_mode === 'disabled' | Adds Bash to disallowedTools | Filters bash (pi-session.mjs:426, :431) |
model_switch_mode === 'disabled' | n/a | Drops switch_model tool (pi-tools.mjs:119) |
Delegation: delegate_to_cli
Lives at ccw/orion-tools-mcp-server.mjs:725-750. Available to agents on both engines (as mcp__orion-tools__delegate_to_cli on Claude and lenient Pi; via the mcp proxy on strict Pi).
Schema:
{
tool: 'gemini' | 'codex' | 'claude' | 'qwen' | 'opencode',
prompt: string,
mode: 'analysis' | 'write', // default 'analysis'
model?: string,
dir?: string // defaults to session cwd
}
Behavior: fires startDelegation(input, requestId) and returns immediately with a delegationId. The user gets a toast and an inline result when the delegated CLI completes. Sister tools execute_cli_inprocess (synchronous), cancel_delegation, and check_delegations round out the lifecycle.
The delegated CLIs are NOT sandboxed. Bash runs under sandbox-exec / sandbox-runtime. But delegate_to_cli calling out to Codex / Gemini / Claude CLI / Qwen / opencode spawns those child processes directly — they inherit sidecar permissions, not sandbox restrictions. Whether this is a deliberate trade-off (so delegated CLIs can do real work) or a hardening gap depends on your threat model. Filed as the architectural sibling of GitHub issue #78. See MCP, Permissions & Sandbox for the full discussion.
Honest gaps
| # | Gap | Severity | Where |
|---|
| T1 | run_in_terminal is Claude-only. Pi sessions cannot use the embedded PTY. | Medium | mcp-bridge.mjs:220-272 (terminal not extracted) |
| T2 | mcp__codexlens__* listed as optional but not bridged to Pi. Reachable via external mcpManager? Unverified. | Low | Absence in extractMcpToolDefs |
| T3 | knowledge_search feature gap — Pi version takes 2 params, Claude version takes 7. Advanced filters (importance, topics, edges) Claude-only. | Medium | pi-tools.mjs:178-220 vs memory/tools.mjs |
| T4 | delegate_to_cli spawns external CLIs unsandboxed. | Medium | ccw/orion-tools-mcp-server.mjs:725-750, related to #78 |
| T5 | Pi’s noPromptTemplates: true blocks the Pi SDK’s own /compact and other native slash commands. Orion routes /command via handler.mjs BEFORE the engine split. | Accepted | pi-session.mjs:500, documented in cron-engine-routing.md Known Constraints §1 |
| T6 | Layer-1 sandbox asymmetry on Windows — both engines fall through to per-command canUseTool prompts. | Accepted | Platform constraint |
| T7 | mcp_tool_permissions DB lookup runs only on Claude’s canUseTool — Pi’s permission middleware classifies all mcp__* as safe (issue #77). | Medium-High | GitHub issue #77; permission middleware gap |
See also
- MCP, Permissions & Sandbox — the three layers in depth, OAuth singleton, sandbox runtime
- Two engines — engine routing,
routeModel(), model preferences
- Agents — how
agents_list / agent_info (Pi) and the agents map (Claude) work
- Memory — the
knowledge_search indexing pipeline