---
name: deploy-connect
description: "Connect an Introspection agent (Recipe) to the MCP servers and data it declares, project-wide before its runtime exists or per environment after, or run it against local fakes with `introspection dev`. The first step of deploy, after build-review: connect the required servers before the runtime exists, then again after it for anything that needs the runtime. deploy-create follows. Needs `introspection login`."
metadata:
  min-cli-version: "0.73.0"
---

# Deploy: connect

A required server is best connected before the runtime exists: connected project-wide, it serves the runtime's first version, which then needs no second try. With no runtime yet, connect project-wide: the owner adds it on the project's Bindings page (`/p/<project>/bindings`, a link you give them), or `introspection bindings mcp connect --mcp-server-id <id> --endpoint-url <url> --connector <id>` with no `--runtime`, which serves every runtime and environment in the project. The connector is the project's and needs no runtime; a key-based connection doesn't either, as the owner registers it themselves on the project's connectors page, or with `introspection connectors connections create --connector <id> @<file>` from a file they write, never from a command line. Only a chat connector (`requires_runtime: true`) needs the runtime first. Once a runtime exists, its page also connects, per environment. A version can be ready and still unable to do its job: every MCP server the Recipe declares (`package.json#pi.mcp.servers`) needs a connection in each environment that serves it. Find what's missing and get it connected, or test against fakes until it is.

## Before you start

- **Read only what you need.** Every `introspection` command takes `--query` (JMESPath): `bindings mcp list --runtime <slug> --environment staging --query "[].[mcp_server_id, connected, endpoint_id]"`, `connectors list --query "[].[slug, status]"`, `connectors connections list --connector <id> --query "[].[id, status]"`. A single string prints quoted: strip it with `| tr -d '"'` when you capture it.
- **Check the stage:** deploy starts here, before there's a runtime: connect the Recipe's required servers project-wide, then hand over to deploy-create. After deploy-create, come back only for what needs the runtime (BYOK's LLM binding, a chat connector, a per-environment binding), then hand over to deploy-test.
- **List what's needed and what's there:** the Recipe's servers and whether each is `required`, the project's connectors (`introspection connectors list -o table`), and the version's bindings per environment (`introspection bindings mcp list --runtime-id <version-id> --environment staging -o table`, then `production`).
- <!-- rule: R-bindings-env-scoped --> **Each environment is its own.** A server bound in staging says nothing about production, and a binding is never copied from one environment to another without checking where it points and what credential it uses.

If every required server is bound in staging, say so in one line and hand over to deploy-test.

## What reaches what

Three kinds of run reach a server three different ways, so a server can be missing for one and fine for another:

| Run                             | Reaches a server through                                                                                                                               |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| A task in staging or production | that environment's binding, and nothing else                                                                                                           |
| A task in development           | the development binding, or a fake `introspection dev --mcp` serves while it stays attached                                                            |
| A Harbor trial                  | the servers its task declares (`[[environment.mcp_servers]]` in `task.toml`), which win over any binding; a task that declares none uses the Runtime's |

`dev` doesn't create a binding or configure staging or production: a fake it serves never reaches a staging task or a Harbor trial. Say this before the owner chooses, whenever the test they want is in staging.

## Steps

**Find the connector before creating one.** For each server, look in this order: the project's connectors (`introspection connectors list -o table`, a `provider` or `slug` matching the server id); then what the platform supports, which the project's connectors page lists (built-in providers, MCP servers it already knows, apps through Pipedream's catalog); and only then a new custom MCP connector, added by the server's URL, from which the platform discovers how it signs in. Then check its type, `introspection connectors get <slug> --query auth_mode`: an `oauth_*` connector exists first and the owner then signs in (its **Connect** button, or `connectors authorize`); a `static` one takes a key the owner pastes in the dashboard. Never assume which from the provider's name. Use what the connector provides: the Recipe's `pi.mcp` server id is the connector's slug and its URL the connector's, and nothing goes under `runtime.connectors`. When the Recipe names the server differently, change the Recipe (a Conventional Commit, `introspection check`, and the owner's yes before pushing) before binding.

For each required server that isn't bound, ask the owner with the question tool how to connect it, one server per question, offering in this order. Ask before acting on their answer: creating a connector, binding it and authorizing it change the project, so each needs their yes, as the runtime did.

1. **Connect it in the dashboard.** With the `introspection_open` tool, call it with `{kind: "binding", runtime: <slug>, name: <server id>, environment: <environment>}` and `wait: true`, on its own in a turn: the owner gets a Connect card (in the app's chat, the terminal or Slack), connects with their own identity, or, for a `static` connector that takes an API key rather than a sign-in, by pasting the key into its connect dialog, and the result says `connected`, `failed: <reason>` or that they declined. Without the tool (Claude Code, Codex), `introspection open --runtime <slug> --wait` opens the Runtime's page, which shows **Connect <id>** for each environment missing a connector, its connection or its binding; the owner connects there, clicks "Return to agent", and the command returns. A key goes only into that dialog: never ask for it in the conversation or put it in a command. This is the way to connect a key-based server. With no browser on this machine, `--print` gives them the link instead; then wait for them to say they're done. Give only a link `introspection open` printed, never one you put together: the dashboard addresses a project by its slug, and a guessed path can lead nowhere. Either way, `connected` is what the owner did, not proof: list the bindings again before recording it.
2. **Connect it from here.** <!-- rule: R-authorize-url-clickable-pause --> When the connector exists, `introspection connectors authorize <connector> --runtime <slug>` opens the provider's sign-in and waits for the connection; with no browser here, `introspection connectors authorize-url <connector> --runtime <slug>` prints the link: give it to the owner as a link, wait until they say they've signed in, then list the bindings again. When the connector doesn't exist, agree its endpoint with the owner, then, with their yes:
   - create it: `introspection connectors create --name <id> --provider <id> --auth-mode <mode> --issuer <url> --api-host <host>` (`oauth-stored` for a server that signs the owner in);
   - bind it: `introspection bindings mcp connect --runtime <slug> --mcp-server-id <id> --endpoint-url <the server's MCP URL> --connector <id> --name <id>`, once per environment with `--environment staging` and `--environment production`, or once with no `--environment` to serve every environment. A CLI whose `bindings mcp connect --help` shows `--mcp-server-id` as optional takes the server id and URL from the connector: `bindings mcp connect --connector <id> --runtime <slug>`;
   - authorize it as above;
   - check it: `introspection connectors connections list --connector <id>` shows the connection active, `introspection connectors test <id> --connection <connection-id>` reaches the server with it, and `bindings mcp list` shows the server connected in each environment. The Runtime page's connect dialog shows its last check, which can predate the sign-in: tell the owner to use Re-check rather than trust an older failure. A repository that ships a setup script for this (`scripts/setup-introspection.sh`, say) shows the exact commands: read it, and run its steps one at a time with the owner's yes rather than the script whole. Never put a token or secret in a command; the owner enters it where the platform asks.
3. **Fake it from this machine, for development tasks only,** when the real service isn't ready or the owner doesn't want real data touched yet. It tests the agent in development, not in staging: say so in the question. Then:
   - Start the fake yourself, in the background: the Recipe's `fakes/` as `eval-simulate` says, or a server the repository ships (its README or `.pi/mcp.local.example.json` says where it listens). If the owner says one is already running, use theirs. Check it answers before going on.
   - <!-- rule: R-dev-mcp-name-must-match --> Attach it from the Recipe's repository, on the commit the version was built from, in the background with its output kept in a log: `introspection dev --runtime <slug> --mcp <id>=<local url>`. The id is exactly as `package.json` declares it. It routes that server to the local process only while `dev` stays attached, and no local secret is uploaded.
   - <!-- rule: R-dev-target-not-credential --> Read the development target from `dev`'s output (the `INTROSPECTION_DEV_TARGET` line it prints). deploy-test sets it on the development tasks it starts, so they route to this machine. It selects an attached session, never a credential or an environment, and is never set for staging or production.
   - <!-- rule: R-no-dev-handback-in-sandbox --> In a cloud Operator session, `dev` can't serve the owner's machine: don't offer this option there. Record the server as missing in each environment and what the owner must connect.
   - Stop `dev` and the fake when the tests are done. A pass here proves the agent against the fake, not the real service.
4. **Not now.** Record the server as not connected; deploy-test runs only the cases that don't need it.

<!-- rule: R-new-binding-needs-new-task -->

A task's connections are fixed when it starts, so a server connected while a conversation is open is used only by tasks started afterwards. Check the bindings again after each change, and record what each environment has in the deploy record (Part B and `docs/PULL_REQUEST.md`, as deploy-create wrote it): connected, faked through `dev` for development, or missing.

## Guardrails

- **The owner connects:** they sign in, authorize and enter secrets. You find what's missing and say where.
- **Never run a task in an environment missing a required connection,** unless `dev` supplies it for that development task.
- **A fake proves the fake:** say which servers were faked, and in which environment, whenever you report a result.

## Done when

- Every required server is connected (project-wide before the runtime exists, or in staging), faked through `dev` for development tests, or recorded as missing with the owner's decision.
- Hand over to `deploy-create` when there's no runtime yet, otherwise to `deploy-test`.
