Skip to Content
Platform
Agent RecipesMCP tool declarations

MCP tool declarations

A recipe declares the external MCP capability its agents are allowed to use; three gates decide what a session actually gets.

The three gates

Access to MCP tools passes through three independent gates, and the effective tool set of a session is their intersection: a tool is available only when the package permits it, the agent selects it, and a binding makes its server reachable.

  1. The package grant. package.json declares, under pi.mcp, the MCP server ids the recipe may use and the maximum tools permitted from each server. Nothing outside this grant can ever be reached.
  2. The agent selection. Each agent’s mcp: block in its YAML picks a mode and narrows the package grant to the subset that role needs. An agent cannot add capability the package did not declare.
  3. The endpoint binding. A package MCP manifest, <recipe-root>/.pi/mcp.local.json, or a host/platform binding maps a declared server id to its endpoint and credentials. A binding never expands authorization — it only makes an already-authorized server reachable.

Every gate is fail-closed: a server with no include, a server left out of an agent’s servers, and an omitted mcp: block all mean no access, so a recipe can never grant more than it wrote down.

The package grant

package.json
{ "pi": { "mcp": { "manifests": ["mcp.json"], "servers": [ { "id": "contacts", "required": true, "tools": { "include": ["*"], "exclude": ["delete_contact"] } } ] } } }

Each entry in servers takes exactly three fields:

  • id — the server’s name as agents and bindings will refer to it.
  • required — optional, default false. A server marked "required": true must resolve to a bound endpoint when the session is materialized, or the session fails closed rather than starting without the capability. A non-required server with no binding simply contributes no tools.
  • tools — the maximum tool set, as include and optional exclude lists.

include names what may be reached: ["*"] means the server’s whole tool set (including tools the server adds later), and a list of exact names means just those. "*" is a whole-toolset sentinel, not a glob — a pattern such as search_* is invalid. exclude subtracts exact names after inclusion and always wins. Omitting include allows no tools. Prefer exact tool names in the package grant; reserve "*" for servers the recipe genuinely trusts wholesale.

manifests optionally lists recipe-relative paths or globs of portable MCP manifest files that ship endpoint configuration with the package. It is always an array; singular manifest and string shorthand are invalid. Like every other binding source, a packaged manifest supplies connections, not additional tools.

Example: resolve one tool for an agent

Assume a contacts MCP server exposes search_contacts, get_contact, and delete_contact. This recipe permits the first two tools, while its agent selects only search_contacts.

package.json
{ "name": "contacts-agent", "version": "0.1.0", "type": "module", "pi": { "agents": ["agents/*.yaml"], "mcp": { "servers": [ { "id": "contacts", "required": true, "tools": { "include": ["search_contacts", "get_contact"] } } ] } } }

The three gates resolve like this:

GateResult
Package grantcontacts.search_contacts and contacts.get_contact are permitted. delete_contact is outside the recipe boundary.
Agent selectionThe agent narrows the package set to contacts.search_contacts.
Endpoint bindingThe contacts id gains a reachable endpoint and credential reference, but no additional tools.
Effective sessionOnly contacts.search_contacts is available.

For an ordinary local run, <recipe-root> is the directory containing the recipe’s package.json. Run introspection local from that directory so the binding and recipe resolve together. A host may supply the same binding shape explicitly instead.

If a required user-owned binding is missing, explain that exact recipe-relative path and let the user create it before retrying the real recipe. Do not move a machine-specific URL or credential into a package MCP manifest merely to make a local proof pass. Validate the binding through the runner without printing or repeating credential values.

Keep .pi/mcp.local.json untracked and ignored by Git when it contains local endpoints or credential references. .recipeignore does not prevent Git from tracking the file and does not exclude it from source validation. Managed runtimes receive their endpoint and credential configuration through bindings, not this local file.

Two modes

Every resolved agent owns its mode independently; with no mode anywhere in a from: chain, an agent gets CLI.

  • mode: cli gives the agent one mcp command containing only its authorized tools. A large server costs one tool definition instead of forty: the agent discovers narrowly (mcp search), reads one schema (mcp list --schema), then calls. Sessions expose only search, list, call, and run — authentication, admin commands, and config overrides are rejected. Keep a shell tool such as bash in tools, since the command runs there.
  • mode: tools registers each authorized tool with Pi directly, for when the model should see real tool schemas. defer: ["*"] hides authorized tools at session start and eager: lists exceptions, which buys the token saving of deferred loading without implementing it; neither can authorize anything include/exclude did not.

In CLI mode, the session’s mcp command supports exactly four subcommands:

CommandWhat it does
mcp search "what you need" [--limit N] [--regex]Search only the tools authorized in this session; results give the exact tool ref, description, and required fields.
mcp list [server | server.tool]Compact inventory of the session’s tools. --schema shows one exact tool’s input/output contract; --all-parameters, --verbose, --status, and --timeout <ms> adjust the view.
mcp call <server>.<tool> key=value …Call one exact tool with schema-aware argument coercion. key=@path passes a file’s contents as an exact UTF-8 string (use @@ for a literal @); --json <json | -> supplies a structured object directly or from stdin; --output text|markdown|json|raw and --timeout <ms> control the result.
mcp run [--var KEY=value] [--json-errors] [file]Run a short JavaScript workflow — from a file or a quoted stdin heredoc — that batches or composes calls through tools["server"]["tool"](args), with dynamic input passed via --var and read as vars.KEY.

mcp run is resource-bounded, not a separate security boundary — the script runs with the same OS privileges as the agent’s shell sandbox. By default a workflow is killed after 120 seconds, each tool call is capped at 60 seconds, and a workflow may make at most 100 tool calls with at most 16 running concurrently; extra calls queue FIFO and inherit the remaining workflow deadline, and a synchronous busy-loop is force-killed at the deadline. Every tool-call chain must be awaited or returned before the script exits — a detached .then/.catch that is still pending fails the run. The defaults are overridable through PI_RECIPES_MCP_RUN_TIMEOUT_MS, PI_RECIPES_MCP_RUN_CALL_TIMEOUT_MS, PI_RECIPES_MCP_RUN_MAX_CALLS, and PI_RECIPES_MCP_RUN_MAX_CONCURRENCY.

A declared agent mcp block replaces the inherited one whole, so a derived agent must restate its mode and servers — a capability change is always visible in the diff of the agent that made it.

Validation

An invalid MCP policy fails closed: a task will not launch the agent with guessed access. Validate at author time with introspection check, which reports an agent naming a server the package never declared, an unknown mode, and a selector that is neither an exact tool name nor "*". Note what it does not flag, because these are policy rather than mistakes: a server with no include and a selection that resolves to no tools both validate, and both mean no access at run time.

Run it

After replacing the example URL with your server and exporting CONTACTS_MCP_TOKEN, run the recipe. local validates it before Pi starts:

introspection local

Bare introspection local auto-selects the runtime when the workspace has exactly one manifest (or one whose recipe contains the current directory); with several manifests, name one with --runtime <slug>.

Inside that Pi session, the resolved tool is exposed via the mcp CLI:

mcp list contacts mcp list contacts.search_contacts --schema mcp call contacts.search_contacts query="Ada Lovelace"

See MCP and federation for the managed side — bindings, connector-supplied servers, credentials, and identity on the Introspection platform — and the Recipes documentation  for the complete format contract.

Last updated on