Skip to content

pi-dispatch v4.2.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 16:52
· 12 commits to main since this release

pi-dispatch 4.2.0 lets a job use MCP servers. You offer the servers in the overlay, and a trigger names the ones its jobs connect. A github or gitlab trigger can also give its jobs the forge's own MCP server, running as the token the job already holds.

A trigger that names no server runs as before. Three things change for everyone: a pi started inside a job gets --no-mcp, import-pi now also stages your MCP servers, and the triggers file refuses a misspelled run.mcp (see Changed).

New

  • MCP servers in the overlay. Put them in mcp.json in the overlay folder, in pi's own mcpServers shape. Streamable HTTP servers and stdio servers both work. The legacy SSE transport, auth, oauth and cwd are refused.

    • pi-dispatch import-pi stages the file from your ~/.pi/agent/mcp.json. It keeps only the servers a job can use, prints each one it staged, and names each one it left behind with the reason. It never copies mcp-auth.json. Use --no-mcp to stage none.
    • A header, an env value and a url's query value are always a reference, ${NAME}. The job fills it in from the trigger's run.secrets or from PI_FORWARD_ENV. A literal argument or a literal part of a url's path is copied as written, and every job can read the overlay, so put a credential in a reference.
    • A stdio command must be in the job image or the overlay (a bare name on PATH, or a path under /usr, /bin, /sbin, /opt or /app). A package runner such as npx or uvx needs "allowPackageRunner": true on the server.
    • pi-dispatch doctor reads the file, names each refused server and why, offers to delete an mcp-auth.json, and lists package runners. With the egress policy on, it names each HTTP server and each package registry the proxy would not let a job reach.
  • run.mcp in a trigger names the servers its jobs connect, for example "mcp": ["docs"], up to 8. You write it in triggers.json by hand: the panel's trigger tools refuse it. A chained child does not inherit it. Before anything is spent, the worker refuses a job when:

    • the overlay does not offer the name, turns it off, or refuses it (mcp-server-unknown), or its mcp.json is missing or cannot be read (mcp-config-invalid);
    • the server references a variable the trigger does not pass (mcp-secret-unmapped), or one the job keeps for itself, such as a provider key or the forge token (mcp-secret-reserved);
    • with the egress policy on, the proxy would not let the job reach the server (mcp-egress-unreachable) or the registry a package runner fetches from (mcp-package-runner);
    • the trigger's run.excludeTools leaves the server no way in (mcp-excluded);
    • the list the job carries is not one the loader writes (mcp-list-invalid).

    The issue gets a short comment that names the server and the rule, never a host or a variable. The worker's log has the detail.

  • pi's MCP works in a job as in the pi CLI. That covers the codemode, deferred (through tool_search), direct and hidden exposures, and resources. A job never signs in: an HTTP 401 ends it. There is no /mcp command. The job waits for every server before its prompt: 15 seconds, or a server's own timeout up to 120.

  • Codemode's models.classify() and models.generateImages() are on. Each call is metered with the job and held to its model list. Under a cost cap, every image call is refused, and so is a classifier with an output price, because neither cost can be bounded in advance. A classifier priced on input only is admitted. A refused call stops the whole job, as any refused model call does. Set "codemodeModels": false at the top of the overlay's mcp.json to turn them off for every job.

  • run.excludeTools on a trigger with run.mcp can also name codemode, tool_search, list_mcp_resources, list_mcp_resource_templates, read_mcp_resource, and one server's tools as a whole as mcp__<server>__* (with - in the name written as _). A single MCP tool cannot be named. A list that would leave a named server no way in is refused before spend (mcp-excluded).

  • The forge's own MCP server. A github or gitlab trigger may add forge to run.mcp. The job then gets GitHub's github-mcp-server 2.0.2 or the community @zereight/mcp-gitlab 2.1.67, both baked into the job image. Each runs as the forge token the job already holds for gh or glab, so there is no new credential and no entry in the overlay.

    • On GitHub the server runs its default toolsets (Actions stays off) in lockdown mode.
    • The servers' dedicated merge and approval tools are withheld, so the job is not handed a ready-made one. The two Copilot tools, and GitLab's discover_tools and execute_graphql, are withheld too.
    • With the egress policy on, a github job needs api.github.com in egress-allowlist.conf. A gitlab job needs the host of your GITLAB_URL there on port 443, or declared in model-endpoints.json.
    • A cron, Forgejo or Azure DevOps trigger that names forge is refused when the triggers file loads.
  • The panel shows an [mcp ...] badge on a trigger that names servers. Its detail view says, per server, how the overlay offers it now or why a job would be refused. It is read-only. dispatch_triggers lists run.mcp.

  • A stop inside the job is named. When the job's MCP stops it before its prompt (mcp-unavailable, mcp-config-invalid, mcp-config-mismatch, mcp-secret-missing, mcp-command-missing or mcp-shadowed), the run record and the panel carry that reason, the failure hook is called with it, and the issue gets a fixed sentence for it. A server that had not answered in time is mcp-unreachable, retried like any infrastructure failure.

See docs/mcp.md.

Changed

  • A pi started inside a job gets --no-mcp. Before, a pi CLI the job ran could connect the servers in its agent folder's mcp.json and, when it trusted the project, in the repo's .pi/mcp.json. Now such a child loads no built-in MCP. Code the job runs can still connect a server on its own, for example with pi mcp list or by loading pi's MCP extension by its path. docs/costs.md lists these. This needs the 4.2.0 job image.
  • import-pi also writes the overlay's mcp.json. Each import replaces it with the servers from your host, and removes it under --no-mcp or when your host offers none. A file you wrote into the overlay by hand is replaced too. A staged server does nothing until a trigger names it.
  • The triggers file refuses a near miss of run.mcp, such as run.mcpServers, run.Mcp or on.mcp. An older version dropped such a key silently.
  • Doctor and MCP endpoints. An endpoint in model-endpoints.json that only an overlay MCP server uses is reported as an "MCP server endpoint". It passes once anything answers through the proxy, instead of warning that it does not answer as a model server.
  • The job image is a little larger: it carries both forge servers. It still runs pi 1.1.0.

Before you upgrade

  1. Update the job image to 4.2.0 before a trigger names a server. It declares the mcp and forgeMcp capabilities. On an older image, a job that names a server is refused before it costs anything (job-image-mcp-unsupported, or job-image-forge-mcp-unsupported for forge).
  2. Upgrade every worker before any trigger names a server. A 4.1.0 worker drops run.mcp from the triggers file without a word. It runs such a job without its servers, and the record reads like a clean run. That holds for a job it takes from the shared queue and for the cron jobs it schedules. A fleet with older workers is safe only while no trigger names a server.
  3. Upgrade the receiver to 4.2.0 with the workers. A 4.1.0 receiver also drops run.mcp. A 4.2.0 worker then refuses each such job as trigger-skew instead of running it without its servers, but only when it can read the triggers file itself. A worker that cannot read it runs the job without its servers.
  4. Update the panel with pi install npm:@edgehero/pi-dispatch-admin to see the servers a trigger names. The panel's setup wizard now installs worker and receiver 4.2.0.
  5. Adding run.mcp to a trigger refuses its jobs that were already queued (trigger-skew). Re-run them.
  6. Run pi-dispatch import-pi again if you want your host's MCP servers in the overlay, then pi-dispatch doctor.
  7. An HTTP MCP server on a port other than 443, with the egress policy on, needs its exact host and port in model-endpoints.json, the same as a model server on your network. Then run pi-dispatch egress render and reload the proxy. A declared host and port is reached as declared, over http:// or https://. A host in egress-allowlist.conf is reached on port 443 over https:// only. With the egress policy off, the route is not checked.
  8. GitLab: give the token the Developer role, not Maintainer, and leave "Allowed to merge" on protected branches at Maintainers. A Maintainer token lets the job, and the forge's MCP server, unprotect a branch or change who may merge. Withholding the merge tools is not a boundary: a GitLab quick action in a note, or the job's own gh or glab, can still do what the token's role allows. See docs/gitlab.md.

Upgrading from 3.x? Read Before you upgrade in the v4.0.0 release first.

Full changelog: v4.1.0...v4.2.0