Skip to main content
The PIO MCP server lets a coding agent check its own work. After the agent writes the integration and runs the game, it calls check_integration. The response carries the same facts the dashboard shows: whether events arrived, which event types are missing, and the next step. The tools cover the loop from creating a project, through authoring its states, decisions, rewards and event types, to confirming that decisions flow in and events flow out. An agent can also create a model from a template, fork and archive models, and read notifications. The opt-in traffic scope adds the tools that change what live players get: going live, setting the split and automatic rollback, deciding challengers, resizing Baseline, moving users to a winner, retiring an option, resetting a broken measurement, and editing what a live model uses. The server speaks MCP over Streamable HTTP at one endpoint:
Every request is a POST with a JSON-RPC body. There is no session to keep alive.

Choose a credential

Two credential types work, with the same scopes. Pick by how the agent runs. Scopes are the same for both: A credential without mcp:write never sees the write tools in tools/list, and one without mcp:traffic never sees the traffic tools. Every credential is bound to one account, chosen when it is issued.

OAuth 2.1

The server publishes standard discovery documents, so a client that supports MCP authorisation needs only the endpoint URL:
  • /.well-known/oauth-protected-resource names the authorisation server and the scopes.
  • /.well-known/oauth-authorization-server names the authorise, token, and registration endpoints.
  • /oauth/register accepts dynamic client registration (RFC 7591). No client secret is issued; clients are public.
  • Authorisation code flow with PKCE (S256) is required. The token request must carry resource=https://app.pioneeroptimisation.com/mcp (RFC 8707); tokens are issued for that resource only.
On the consent page you pick the account the client may act in and confirm the scopes it asked for. Access tokens last one hour; the client renews them with a refresh token without asking you again.

API token

In the dashboard open your account menu, then API tokens, then New token. Set a name, pick the account, tick the scopes the agent needs, and set an expiry date. The token is shown once, starts with pio_, and is stored as a hash: if you lose it, create another. Treat the token as a secret. Keep it in an environment variable or the client’s secret store, never in a repository.

Connect a client

With OAuth, add the server and complete the login when prompted:
With an API token, pass it as a header:
Confirm the connection by asking the agent to call whoami. The response names the account, the user, the scopes, the credential type, and the projects in the account.

Tools

Tool arguments use numeric ids. whoami lists the project ids; create_project returns the id of the project it made. Every tool response is a JSON document. When a tool cannot do what was asked it returns error, message, and next_step, so the agent can act on the failure rather than guess:

check_integration

This is the tool the agent runs after each change. The rungs array is the same ladder as the dashboard’s integration-health view, and each rung carries its facts. An unsatisfied rung adds a next_step with a message, the dashboard path, and a link into these docs. The top-level next_step is the one thing to do now. coverage lists the event types the project expects, the ones seen, and any unregistered types the game is sending.
status is never until the first event, integrated while events keep arriving, and stale once they stop; streaming is true while events are arriving now. See Integration health for what each rung checks.

go_live

go_live is the go-live page for agents. It runs in two calls, so the agent acts on the same facts a person would see.
  1. Call it with project_id and model_id only. Nothing changes. The response carries the suggested share and why, the split across live models, the timeline, the safety checks, the rollback setting, the per-option caps, and expected_epoch_id. If the model is not ready, blocker says why.
  2. Call it again with confirm: true, the share you chose (5 to 50), and the expected_epoch_id from the first call. To set the project’s automatic rollback, pass rollback with rollback_was set to the preview’s rollback.now. A setting someone changed since the preview is kept. Omit both to keep the current setting.
The confirmed call runs the same checks as the page, and refuses with one of these errors:
  • not_first: the model has served users before. Change its share with set_traffic_split.
  • share: the share is outside 5 to 50.
  • locked: the model is larger than the free plan covers.
  • not_ready: the model fails a readiness or fallback check; the message names it.
  • stale: traffic changed since the preview. Preview again.
  • confirm_needs_preview: share or expected_epoch_id is missing, or rollback came without rollback_was.
Every go-live is written to the audit log with the credential that made it. See Going live for how the share is chosen.

Read results before moving traffic

shrink_baseline, shift_to_winner and set_traffic_split act on results. Read them first. get_experiment_evidence and get_guardrails return the same results the Experiments tab shows, and get_forecasts returns the Performance tab’s forecast accuracy panel.
  • measurement.superseded is true when the traffic changed and a fresh measurement has not reported yet. Those rows are the last results from the previous measurement.
  • Every model in these results, and in get_model_lifecycle and set_traffic_split, carries traffic_weight and serving_class. Nothing is left out for being at 0%:
    • serving: the model gets traffic now.
    • zero_with_evidence: it is at 0% now, but the results shown still include it. The pages show it muted with a “0% now” tag.
    • not_receiving: it is at 0% with nothing in these results. It may be a challenger waiting for go_live, a retired model, or one that never served. The pages leave it out of filters and lists of results.
  • srm.tripped means the traffic split did not match the configured weights, so no verdict can be trusted. reset_measurement starts a fresh one.
  • ebh on a comparison is passes or does_not_pass against the multiplicity bar that protects against a lucky pair among many. It is null when there are too few comparisons for the bar to apply, the same as the tab.
  • A guardrail row with decision: "harm" is a model making things worse on that check. Don’t move traffic toward it.
  • per_model_checks_dropped in get_guardrails lists the models that have no checks of their own. With many models live, only the checks across all models combined fit each update. When it isn’t empty, a pooled “all clear” says nothing about any one of those models, and automatic rollback can’t switch a single model off. shift_to_winner refuses those models. Archive models you’ve finished with to turn per-model checks back on.

Change models

set_traffic_split, set_auto_rollback, promote_model, and abandon_model run the Models page’s own actions with its checks, in two calls like go_live:
  1. Call without confirm. Nothing changes. The response shows the state now and after, and the values the second call needs: expected_epoch_id, or for set_auto_rollback, enabled_now.
  2. Call again with confirm: true and those values: expected_epoch_id, or enabled_was set to the preview’s enabled_now. If traffic or the setting changed since the preview, the call refuses with stale and changes nothing.
A model’s first users always come through go_live: set_traffic_split refuses with first_traffic for a model that has never served, and its rows mark such a model requires_go_live: true. get_model_lifecycle lists each model’s traffic_weight, forked_from_id, and challenger_state, which is what promote_model and abandon_model act on. fork_model and archive_model never move traffic, so they need only mcp:write and run in one call. To archive a model that still gets users, set its weight to 0 with set_traffic_split first. Every change, from an agent or from the Models page, is written to the audit log in the same transaction as the change.

Edit inputs a live model uses

Most authoring applies in one call. Three edits change what live players get, so they follow the same two calls as go_live:
  • update_reward, when an active model trains on the reward and the edit changes its rules, horizon or discount.
  • update_action_definition, for any operation except rename_choice, when an active model uses the decision.
  • retire_choice, always.
  1. Call without confirm. Nothing changes. The response shows the current values, the proposed change, the live models it reaches, and expected_revision.
  2. Call again with the same arguments, confirm: true, and that expected_revision. If the input, the requested change, or the set of live models differs from the preview, the call refuses with stale and changes nothing.
Without mcp:traffic, these edits refuse with traffic_scope_required. A rename that only changes a reward’s name, or an option’s label, applies at once: options keep their stable ids, so the live model serves the same arms. A confirmed change to a live champion’s reward does not rewrite what it serves. It creates a challenger at 0% that trains on the edited reward, for you to compare and promote. A reward used only by Baseline or a promoted challenger changes in place. update_action_definition takes one operation per call: reset_measurement follows the same pattern: the first call reports the tripped measurement’s epoch_id, and the second call passes it as expected_epoch_id.

The integrate-and-verify loop

  1. whoami to confirm the account and find or choose a project. With mcp:write, create_project makes a new one.
  2. get_api_key for the ApiKey, AccountId, and ProjectId that go into PioSettings. Keep the key out of source control.
  3. create_action_definition for each key the game registers, create_state_definition for each state it reports, and register_event_type for each event type it tracks. get_project_inputs lists what exists.
  4. Integrate the SDK following the quickstart and run the game.
  5. check_integration. If status is still never, the next_step says why. Fix, run, repeat.
  6. get_project_status to see which onboarding steps remain. list_model_templates and create_model_from_template make a draft model; get_model_lifecycle shows what it still needs before go_live.

Projects created by an agent

A project made through create_project starts under a not-child-directed default: its API key is published at once and events are accepted. A member of your studio must still confirm the child-directed answer in the project’s settings within 14 days; agents never answer it. While the confirmation is outstanding:
  • create_project and get_api_key return attestation_pending: true with a confirm_by date; once a studio member answers, the field reads null.
  • check_integration and get_project_status keep the confirmation as the next step, with the settings path.
  • Past confirm_by the key is unpublished and events stop until someone answers. The dashboard shows the same warning on every page of the project.
The person who confirms is recorded with the attestation, exactly as for a project created in the dashboard. See Consent and privacy for what the answer changes.

Limits and audit

  • get_api_key is limited to account admins. A non-admin credential gets a forbidden error naming who can help.
  • Each key reveal, project or input change, go-live, model change, and traffic change is written to the account’s audit log with the credential, the user, and the request’s IP address.
  • Unauthenticated requests to /mcp are limited to 20 per minute per IP; client registration to 5 per minute; token requests to 30 per minute.
  • Tool calls are recorded (tool name, duration, outcome, never arguments) so the tool set can improve with use.
  • Nothing in this tool set deletes or rotates anything. archive_model archives and retire_choice retires; both can be undone from the dashboard. Deleting a project stays in the dashboard. Rotating an API key, when it ships, will be a separate, explicit tool.