MCP

Two gates decide what an agent can call, and one of two modes decides how.

An agent can call an MCP tool only when two separate declarations both allow it. The package says what the whole recipe may ever touch, and each agent says which part of that its own role needs.

Declared inAnswersWritten by
package.jsonWhat may this recipe touch at all?Whoever owns the package
agents/*.yamlWhat does this one role need?Whoever writes the agent

Both gates fail closed, so anything neither of them named is unavailable rather than merely unused. Widening an agent can only ever reach as far as the package already allows, which is what makes the package file the one place to review when you want to know a recipe's blast radius.

Declare the package boundary

package.json
{
  "pi": {
    "mcp": {
      "manifests": ["mcp.json"],
      "servers": [
        {
          "id": "contacts",
          "required": true,
          "tools": { "include": ["search_contacts", "get_contact"] }
        },
        {
          "id": "warehouse",
          "required": true,
          "tools": { "include": ["run_query"], "exclude": ["drop_table"] }
        }
      ]
    }
  }
}

Policy rules

  • Prefer exact tool names. "*" permits the package-visible set including tools the server adds later.
  • Patterns such as search_* are invalid. It is exact names or "*".
  • exclude removes exact names after inclusion and always wins.
  • An agent can only narrow. It can never add capability the package did not declare.
  • Omitting a server, or the whole agent mcp block, means no access to it.
  • "required": true that cannot be bound fails the session at launch rather than starting without the capability.
  • manifests is optional, and points at portable endpoint definitions the package ships with.

Narrow it per agent

agents/agent.yaml
tools:
  - bash                   # CLI mode runs the mcp command, so keep a shell tool
mcp:
  mode: cli
  servers:
    contacts:              # only this server, and only these two tools
      include:
        - search_contacts
        - get_contact

MCP tools are selected here, never in the agent's ordinary tools list. A declared mcp block replaces the inherited one whole, so a derived agent must restate its mode and servers. That is deliberate: it makes a capability change visible in the diff of the agent that made it.

CLI mode

mode: cli gives the agent one mcp command containing only its authorized tools. A large server costs one tool definition instead of forty, and the agent discovers narrowly, reads one schema, then calls.

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

Sessions expose only search, list, call and run. Interactive authentication, administrative commands, URL selectors, ad-hoc transports, config overrides and persistence flags are all rejected, so a session cannot reach past its materialized surface. Delegating to another agent does not give the parent that child's capabilities.

Tools mode

mode: tools registers each authorized tool with Pi directly, which is what you want when the model should see real tool schemas. Every resolved agent owns its mode independently, so a root and its subagents may differ. With no mode anywhere in a chain, a resolved agent gets CLI.

mcp:
  mode: tools
  servers:
    contacts:
      include: ["*"]       # authorize everything the package permits
      defer: ["*"]         # but start with all of it hidden
      eager:
        - search_contacts  # except this one, visible at startup

Deferred tools

defer hides authorized tools at session start; eager subtracts exceptions and wins when a tool matches both. Neither can authorize a tool that include and exclude did not, and both are invalid in CLI mode. Omit defer to expose everything immediately.

Deferred tools stay authorized and discoverable. When any exist, Recipes registers mcp_search, which searches only the authorized deferred catalog and adds matches to the active set for the next request. It never grants access beyond servers.

This is Pi's own deferred tool loading, applied to MCP. A loader tool stays active, the searchable tools start inactive, and activation is additive. On models with native support the prompt prefix survives and the new definitions load at the tool-result position; elsewhere Pi falls back to sending the updated active list. You get the token saving without implementing any of it.

Endpoints and credentials

The package declares which servers a recipe may use. Where those servers live, and what authenticates to them, is supplied where the recipe runs. See Running locally for .pi/mcp.local.json and local OAuth, and Deploying for host bindings and terminating auth at the edge.