On this page

Agent interface

MCP tool reference

Exact schemas, defaults, return shapes, and failure behavior for all thirteen Shoal MCP tools.

Status
Current MCP facade contract
For
Agent authors and MCP client implementers
On this page
  1. Tool map
  2. Tool-result envelope
    1. Kernel errors are tool errors
  3. shoal_exec
    1. Position is semantic
    2. Run versus plan
    3. Background and timeout
    4. Result and references
    5. Elision budget
  4. shoal_plan
  5. shoal_apply
  6. shoal_get
    1. Path grammar
    2. Top-level slices
    3. Retrieval is not recomputation
  7. shoal_journal
  8. shoal_cancel
  9. shoal_cap_request
  10. PTY lifecycle
  11. shoal_pty_open
  12. shoal_pty_send
  13. shoal_pty_read
  14. shoal_pty_resize
  15. shoal_pty_close
  16. shoal_pty_list
  17. Choosing the right tool
  18. Client-side resilience checklist

The Shoal MCP facade exposes thirteen tools. Seven operate on structured Shoal execution, plans, values, journal entries, and tasks; six drive real terminal programs through PTYs.

This page documents the MCP layer, not every method accepted by the kernel socket. The facade validates each argument object against its advertised JSON Schema, supplies MCP-specific defaults, maps the call to a kernel method, and wraps the kernel response in an MCP tool result. For the lower-level API, see Kernel JSON-RPC protocol.

Tool map🔗

MCP toolKernel methodPurpose
shoal_execexecRun or plan Shoal source.
shoal_planexec with mode: "plan"Concise plan-only convenience tool.
shoal_applyplan.applyApply a stored, approved plan.
shoal_getvalue.getQuery a transcript value without rerunning source.
shoal_journaljournal.querySearch structured execution history.
shoal_canceltask.cancelRequest cancellation of a background task.
shoal_cap_requestcap.requestRequest approval for effects in a stored plan.
shoal_pty_openpty.openStart an interactive terminal program.
shoal_pty_sendpty.sendType text, keys, or bytes into a PTY.
shoal_pty_readpty.readRead the rendered terminal screen.
shoal_pty_resizepty.resizeChange the terminal grid.
shoal_pty_closepty.closeTerminate and reap the PTY process.
shoal_pty_listpty.listList open PTYs in the attached session.
flowchart TB
accTitle: Tool map
accDescr: Shows the components and relationships described in Tool map.
    C["MCP client"] --> E["Execution tools"]
    C --> T["PTY tools"]
    E --> X["exec / plan / apply"]
    E --> V["value.get"]
    E --> J["journal / tasks / approval"]
    T --> O["open"]
    O --> S["send ↔ read"]
    S --> Z["resize"]
    S --> Q["close"]

Tool-result envelope🔗

Every successful facade dispatch returns the normal MCP CallToolResult shape:

{
  "content": [
    { "type": "text", "text": "human-readable bounded rendering" },
    { "type": "resource_link", "uri": "shoal://out/17" }
  ],
  "structuredContent": {
    "ref": "out:17",
    "uri": "shoal://out/17",
    "value": { "$": "int", "v": 42 },
    "render": "42"
  },
  "isError": false
}

The resource_link member is included only when the response contains an addressable URI or reference. Use structuredContent for program logic and the resource link for later retrieval. The text is a display aid, not the canonical data model.

The facade places a hard 64 KiB limit on the textual content of one tool result. If a rendering is longer, Shoal keeps whole leading lines where possible and appends a marker such as:

…(832 more lines, fetch via shoal://out/17)

This MCP text limit is separate from value elision. structuredContent contains the kernel’s already-elided result, including the reference needed to drill into it.

Kernel errors are tool errors🔗

If the kernel returns a JSON-RPC error, the facade turns that error object into a valid tool result with isError: true. It does not fail the MCP request transport:

{
  "content": [{ "type": "text", "text": "{ ...kernel error... }" }],
  "structuredContent": {
    "code": -32011,
    "message": "approval required",
    "data": { "plan_ref": "plan:..." }
  },
  "isError": true
}

Malformed MCP arguments, an unknown tool name, a broken kernel connection, or another bridge-level failure can instead become an MCP JSON-RPC error. Clients should therefore handle both a failed request and a successful tools/call whose result has isError: true.

shoal_exec🔗

Execute Shoal source in the attached named session.

{
  "src": "(ls .).where(.type == 'file')",
  "mode": "run",
  "position": "value",
  "background": false,
  "timeout_ms": 5000,
  "elide": {
    "max_bytes": 8192,
    "max_rows": 100,
    "max_items": 500
  }
}
ArgumentTypeRequiredDefaultMeaning
srcstringyesShoal source to parse and execute or plan.
mode"run" or "plan"no"run"Execute now or derive effects without spawning.
position"stmt" or "value"no"value"Decide whether a final failed outcome raises or remains inspectable.
backgroundbooleannofalseReturn a task immediately instead of waiting.
timeout_msinteger ≥ 1nono timeoutWait this long, then return the still-running work as a task.
elideobjectnoserver defaultsOverride max_bytes, max_rows, and/or max_items for this response.

Unknown properties are rejected by the advertised schema.

Position is semantic🔗

The MCP tool defaults to position: "value", unlike the raw kernel exec method, whose omitted-position default is statement position. In value position, the final command may return an outcome with ok: false:

{
  "src": "^false",
  "position": "value"
}

That is useful when failure is expected and the agent wants to inspect status, stderr, or the structured error. In statement position, the same final failed outcome raises cmd_failed.

Position applies to the final expression boundary. Earlier source statements still have statement semantics. This can surprise a caller who sends a multi-statement block:

^false
42

The first statement fails before 42 can become the value. Capture expected failures explicitly in language syntax:

let probe = (^false)
{ probe, answer: 42 }

See Outcomes and errors for the language rules.

Run versus plan🔗

mode: "plan" parses and analyzes the source, derives effects and reversibility, evaluates policy, stores a plan, and does not spawn the planned command. A plan response contains a plan_ref, concrete effect descriptors, a decision, and stored source/session/principal metadata used by shoal_apply.

Planning is meaningful for command-shaped work. Pure expressions can have no external effects. Analysis is conservative: opaque or dynamically computed effects may require a stricter policy decision than an equivalent fully concrete action.

Background and timeout🔗

With background: true, Shoal returns a task:N reference without waiting for completion. The task runs in the session’s evaluator worker and emits task.N events.

With timeout_ms, Shoal waits up to the given duration. If execution finishes, the ordinary transcript result is returned. If it is still running, Shoal returns the task rather than killing it. Timeout is therefore a wait budget, not an execution deadline. Follow with task resources/events or shoal_cancel.

Do not set both options merely to make a task: background: true already returns immediately, so a timeout does not add a useful deadline.

Result and references🔗

A completed execution normally returns fields including:

FieldMeaning
refShort session transcript reference such as out:17.
uriMCP resource URI such as shoal://out/17.
valueTagged wire value, possibly elided.
renderBounded human-oriented rendering.
elapsed_msExecution time when reported by the kernel.

The reference names the complete stored value, not merely the preview embedded in this result. Use shoal_get to select a small field or slice.

Elision budget🔗

elide may tighten or loosen three presentation thresholds:

{
  "src": "range(0, 100000).collect()",
  "elide": { "max_items": 20, "max_bytes": 4096 }
}

The kernel’s encoded result still has a 64 KiB hard ceiling in normal JSON/render modes. Large values receive an addressable reference and preview. Per-call item and row values are not currently schema-capped, so setting enormous limits is not a promise that the whole value will be inlined.

shoal_plan🔗

Convenience form for a value-position plan:

{
  "src": "cp ./report.csv ./archive/report.csv"
}

It is exactly mapped as:

{
  "method": "exec",
  "params": {
    "src": "cp ./report.csv ./archive/report.csv",
    "mode": "plan",
    "position": "value"
  }
}

Only src is accepted. Use shoal_exec with mode: "plan" if you need the general tool form, although the current facade does not add a plan-specific elision option either way.

A typical flow is:

sequenceDiagram
accTitle: shoal_plan
accDescr: Shows the components and relationships described in shoal_plan.
    participant A as Agent
    participant M as shoal-mcp
    participant K as Kernel
    A->>M: shoal_plan(src)
    M->>K: exec(mode=plan)
    K-->>A: plan_ref + effects + decision
    alt approval_pending
        A->>M: shoal_cap_request(plan_ref, effects)
        M->>K: cap.request
        K-->>A: approval result
    end
    A->>M: shoal_apply(plan_ref)
    M->>K: plan.apply
    K-->>A: transcript result

shoal_apply🔗

Apply a previously stored plan:

{
  "plan_ref": "plan:7b2fd854cb805ba1"
}

plan_ref is the only argument. Applying does not accept replacement source, altered arguments, or an alternate effect list. The kernel checks the selected stored plan’s session, principal, source, and policy state before application.

The reference itself is not a complete identity: it is currently a 16-hex-character truncation of a hash over effects, reversibility, and estimates, excluding source/session/principal. A same-shape plan can overwrite another entry in the kernel’s global plan map. The metadata check prevents a direct source substitution at apply time, but collisions can invalidate references or make them select another plan’s metadata. Until this is fixed, derive and apply promptly, inspect the selected plan resource, and do not use plan_ref as an authorization token or durable unique ID.

Plans are ephemeral kernel state. They do not survive daemon restart. An unknown or expired-in-memory reference produces UNKNOWN_PLAN (-32012). A plan that still needs approval produces APPROVAL_REQUIRED (-32011).

Plan application evaluates the stored source; it is not a transaction across arbitrary external systems. “Reversible” records the planner’s knowledge and supports journal/undo integrations where an inverse is available, but it is not a universal rollback guarantee.

shoal_get🔗

Fetch a transcript value, a nested path, or a top-level slice without re-executing source.

{
  "ref": "out:17",
  "path": ".rows[0].name",
  "elide": { "max_bytes": 2048 }
}
ArgumentTypeRequiredMeaning
refstringyesShort reference such as out:17, or another reference accepted by the kernel.
pathstringnoField/index/range selector applied to the referenced value.
slicetwo-integer arraynoTop-level half-open range [start, end].
elideobjectnoPer-call max_bytes, max_rows, and/or max_items.

The MCP schema does not expose a format argument. Use a resource URI such as shoal://out/17?format=raw, or the raw kernel method, when the representation format matters.

Path grammar🔗

Paths are intentionally smaller than the Shoal language:

.field
[0]
[2..8]
.users[0].name
.rows[10..20]
  • Dot segments select a record or outcome field.
  • [n] selects a nonnegative index.
  • [a..b] selects a half-open range.
  • Table field selection may name a column; .rows exposes row records.
  • Outcome fields are selectable, and selection can continue through out.

Paths do not evaluate arbitrary expressions, invoke methods, interpolate variables, accept negative indexes, or execute code. That limitation is deliberate: retrieval is deterministic and side-effect-free.

Top-level slices🔗

slice: [start, end] applies to the value after any path selection and supports:

  • lists, by element;
  • tables, by row;
  • strings, by Unicode scalar value rather than UTF-8 byte offset;
  • bytes, by byte.

The end is exclusive. Invalid bounds or slicing a scalar produces BAD_PATH (-32005). Prefer a path range when the slice is naturally part of a nested selector; prefer the separate slice field for pagination generated by a client.

Retrieval is not recomputation🔗

The transcript holds the value produced by the original execution. Fetching .rows[0] does not rerun ls, reread a file, or invoke a method. That gives agents a stable “execute once, inspect many times” pattern.

shoal_journal🔗

Query durable structured execution history:

{
  "since": 1200,
  "until": 1800,
  "principal": "agent:reviewer",
  "ok": false,
  "effects": ["proc.spawn"],
  "head": "git",
  "limit": 50
}

All arguments are optional.

ArgumentTypeMeaning
sinceintegerLower time/sequence boundary accepted by the journal handler.
untilintegerUpper boundary.
principalstringExact principal filter.
okbooleanOnly successful or failed entries.
effectsstring arrayRequire matching effect categories.
headstringFilter by command head.
limitinteger ≥ 1Maximum entries to return.

Journal entries describe executions and plans; they are not raw terminal transcripts. Treat field additions as forward-compatible. A client should select the fields it understands rather than requiring byte-for-byte object equality.

The kernel journal and the standalone shoal-history CLI use compatible storage code but can default to different XDG roots. See Companion CLI reference before diagnosing an apparently empty journal.

shoal_cancel🔗

Request cancellation of a task:

{
  "task": "task:9"
}

Cancellation is cooperative at the evaluator/task layer. The result reports the updated task state; clients should not assume the process vanished before observing a terminal state. Read shoal://task/9, subscribe to it, or poll the task resource until it is completed, failed, or cancelled.

Task references and visibility are scoped to the attached named session. A daemon restart loses the task registry. Unknown tasks produce UNKNOWN_TASK (-32021); an operation not available for the task’s current execution form produces TASK_CONTROL_UNAVAILABLE (-32020).

MCP exposes cancellation but not the kernel’s task.suspend or task.resume; those require the raw kernel protocol.

shoal_cap_request🔗

Request an approval/grant for a plan that is waiting on policy:

{
  "plan_ref": "plan:7b2fd854cb805ba1",
  "effects": ["fs.write", "proc.spawn"]
}
ArgumentTypeRequiredDefault
plan_refstringyes
effectsarrayno[]

The effect list scopes the requested grant; it does not rewrite the plan’s derived effects. Send only the capability categories the user or supervising system has actually approved. An empty list delegates the current implementation’s default request behavior and should not be used as a blanket “approve everything” convention.

Approval and enforcement are distinct. An approval changes the policy decision associated with the stored plan; caps_enforced on attachment tells you whether an OS sandbox is actually active. See Security and trust boundaries.

PTY lifecycle🔗

The PTY tools are for programs whose behavior depends on a terminal: editors, installers, REPLs, pagers, password-less prompts, and full-screen TUIs. Prefer shoal_exec for noninteractive commands because it returns typed outcomes and participates in normal adapters.

stateDiagram-v2
accTitle: PTY lifecycle
accDescr: Shows the components and relationships described in PTY lifecycle.
    [*] --> Open: shoal_pty_open
    Open --> Open: send / read / resize
    Open --> Exited: child exits
    Open --> Closed: shoal_pty_close
    Exited --> Closed: close / reap
    Closed --> [*]

PTY reads expose an emulator’s current screen, not the original byte stream. This removes ANSI control sequences and bounds the response to the terminal grid, but it also means scrollback and byte-perfect output are not an audit log.

shoal_pty_open🔗

Start a program on a real pseudoterminal:

{
  "cmd": "python3",
  "args": ["-q"],
  "cols": 100,
  "rows": 30,
  "env": { "TERM": "xterm-256color" }
}
ArgumentTypeRequiredDefaultMeaning
cmdstringyesExecutable name or path.
argsstring arrayno[]Arguments, without repeating cmd.
colsinteger 1–1000no80Terminal columns.
rowsinteger 1–1000no24Terminal rows.
envstring mapno{}Overrides applied over the session environment.

The process starts in the attached session’s current directory with its environment plus overrides. Spawn is Leash-gated and, when available, pinned to the selected OS sandbox policy. A denied spawn returns LEASH_DENIED (-32010); an approval-required spawn returns APPROVAL_REQUIRED (-32011); OS/PTY setup failures use PTY_SPAWN_FAILED (-32023).

The result includes pty_id, pid, command, dimensions, and liveness information. Store the pty_id; it is required by every other PTY tool.

The PTY tool itself does not provide a plan/apply handshake for a spawn that needs approval. Pre-authorize the relevant policy outside this call or use a noninteractive planned execution when the workflow permits it. This is a current surface limitation, not an instruction to bypass policy.

shoal_pty_send🔗

Send one input item or an array of items:

{
  "pty_id": "pty:3",
  "input": [
    "print(6 * 7)",
    { "key": "Enter" },
    { "key": "Ctrl-D" }
  ]
}

Accepted item forms:

"text typed verbatim"
{ "text": "text typed verbatim" }
{ "key": "Enter" }
{ "bytes": "base64-encoded-bytes" }

An array may mix all forms. Objects must select a supported member; raw bytes are base64 because JSON strings cannot preserve arbitrary octets.

Named keys include:

  • Enter, Tab, Escape, Backspace, Delete, and Space;
  • Up, Down, Left, and Right;
  • Home, End, PageUp, and PageDown;
  • F1 through F12;
  • Ctrl-A through Ctrl-Z.

Text is not shell-escaped or interpreted by Shoal; it is written to the terminal input exactly as text. Whether an Enter submits it, an editor inserts it, or a TUI treats it as a command is determined by the child program’s terminal mode.

Sending input and reading output are separate calls. After a send, read until the screen is stable enough for the workflow rather than assuming one response cycle is sufficient.

shoal_pty_read🔗

Read the current rendered screen:

{
  "pty_id": "pty:3"
}

Representative result:

{
  "pty_id": "pty:3",
  "screen": [
    ">>> print(6 * 7)",
    "42",
    ">>>"
  ],
  "cursor": { "row": 2, "col": 4, "hidden": false },
  "changed": true,
  "alive": true,
  "exit": null,
  "pid": 48120,
  "cmd": "python3"
}

screen is an array of rows bounded by the configured grid. Trailing empty cells are rendered as text rather than an ANSI stream. cursor uses grid coordinates. changed means the emulator screen changed since this PTY’s previous read; it is a hint for polling, not a durable event cursor.

When the child exits, alive becomes false and exit carries status information. Read once more after exit if the final output matters, then call shoal_pty_close to release the registry entry and reap resources.

PTY output is currently not exposed as a subscribable MCP resource. Poll shoal_pty_read with a reasonable client-side delay; avoid a tight loop.

shoal_pty_resize🔗

Resize both the operating-system PTY and the terminal emulator:

{
  "pty_id": "pty:3",
  "cols": 120,
  "rows": 40
}

All three fields are required by the MCP schema; dimensions must be in 1..=1000. Many TUIs repaint asynchronously after receiving the resize signal, so follow with shoal_pty_read and allow for more than one screen update.

shoal_pty_close🔗

Terminate and reap a PTY session:

{
  "pty_id": "pty:3"
}

Close is the explicit lifecycle boundary. Use it even after the child appears to have exited so the kernel can remove the PTY from the session registry. Closing a live PTY terminates the process; do not use it as a “detach and leave running” operation.

shoal_pty_list🔗

List open PTY registry entries in the attached session:

{}

The result is a stable, ascending list of summaries such as:

[
  {
    "pty_id": "pty:2",
    "cmd": "vim",
    "pid": 48091,
    "cols": 100,
    "rows": 30,
    "alive": true
  }
]

Summaries do not include screen contents. Call shoal_pty_read for one ID, or read shoal://pty for the same discovery view. PTYs in other named sessions are not listed.

Choosing the right tool🔗

NeedPreferWhy
Evaluate data or run a normal commandshoal_execTyped outcome, transcript ref, adapter support.
Inspect likely effects before actingshoal_planNo planned spawn; records effects and caller metadata.
Continue an approved planshoal_applyRechecks the selected stored plan’s caller/source metadata.
Read part of a prior resultshoal_getNo re-execution and small context cost.
Find prior activityshoal_journalDurable structured filters.
Stop background workshoal_cancelTask-aware control.
Drive an editor or TUIPTY toolsReal terminal semantics and rendered screen.
Run a program that merely prints textshoal_exec, not PTYEasier status handling and structured capture.

Client-side resilience checklist🔗

  1. Check both the MCP request status and isError in the tool result.
  2. Treat structuredContent as authoritative; text is bounded presentation.
  3. Save returned references and fetch narrow paths/slices instead of repeating work.
  4. Expect daemon restart to invalidate plans, tasks, PTYs, and transcript refs.
  5. Use event sequence numbers for subscriptions and task resources for final state.
  6. Treat timeout as conversion to background work, not termination.
  7. Close every PTY you open.
  8. Never infer OS sandbox enforcement solely from an allowed policy decision.
  9. Keep session names as trust-boundary choices, not cosmetic labels.
  10. Make unknown fields nonfatal so clients can survive additive protocol evolution.

Continue with Resources and events for addressable state and subscriptions, or Agent workflows for end-to-end patterns.

Type to search every guide navigate open esc close
Diagram