--- name: migrate-mcp-between-projects description: Redeploy one Platform-managed MCP server from an explicit source AICP project to an explicit target project in the same organization, complete secure setup there, verify readiness, distribute it, and optionally retire the source with a safe cutover through the Speakeasy AI Control Plane Platform MCP. --- # Migrate an MCP between projects Use this workflow only through the authenticated Speakeasy AI Control Plane (AICP) Platform MCP connected as an external administrator. There is no move tool, so it follows the same guarded outcome a user would complete manually in the AICP dashboard: read the source registration, re-add the same reviewed source in an explicit target project, finish secure setup there, verify readiness, distribute to one exact target plugin, and only then optionally disable the source. The source stays enabled and untouched until the target is registered, connected, verified ready, and distributed. For a first-time addition with no source, use `add-mcp-from-catalog` or `add-mcp-from-remote-url` instead. It is not available to managed project assistants: an assistant acts only inside its own project and cannot name a second one, and the plugin inventory, access reads, forced readiness probes, and distribution this workflow depends on are external-client tools. Installing this package grants no organization or project access. ## Safety rules - Secrets never enter chat. Never ask the user to paste credentials, tokens, secret headers, or OAuth state into chat or tool arguments, and never claim that any secret, header value, or authorization travelled from the source. Secret entry and provider sign-in happen only in server-returned dashboard and authorization URLs. - Show non-secret setup, provider, and authorization URLs only when a Platform MCP tool returns them; never reconstruct or invent a link. - Keep the source project, source MCP, target project, and target plugin explicit. Never infer any of them from prior context, a display name, or the Default project, never silently substitute another target, and never let the source and target be the same project. - Migration re-creates rather than moves: the target gets a new registration, a new endpoint address, and its own identity-provider attachment. State per attribute whether registration copied it, it is re-created only after the user confirms it, the user must re-enter it, or it cannot be reproduced. Nothing is dropped silently. - Registration-scoped reads and lifecycle mutations on the source work only for the administrator who registered it. A not-found, invalid-request, or unsupported-target refusal on the source means the caller cannot probe or disable it; report that attribute as unreadable and hand off to the AICP dashboard rather than working around it. - Registration in the target is private until a plugin carries it. Distribute only after fresh readiness from the same connection, and never disable the source until the target's live state has been verified and the user confirms retirement. Treat server-returned evidence as bounded and report every gap. - A denial or conflict returns to fresh reads and user review. Never retry a mutation automatically or with another project, MCP, or plugin, and never disable the source to make the target succeed. - Use `send_platform_mcp_feedback` only after asking for consent, and never include identifiers, URLs, credentials, payloads, headers, logs, or attachments. ## Workflow 1. Call `list_projects` with `limit: 100` to verify that the Platform MCP is authenticated and obtain the eligible projects. If authenticated discovery is unavailable, stop and ask the user to complete or repair AICP OAuth. If `truncated: true`, report that project discovery is incomplete and hand off to the AICP dashboard. Present the projects and ask the user to choose one exact source project and one exact, different target project in this organization; keep each project's ID and slug paired internally. 2. Call `find_mcp` for only the source project with `limit: 100`, following every `next_cursor` using `cursor` with the same project and no `query` until the list is complete. Present only entries with `model: platform_managed`; a dashboard-managed, hosted, tunnelled, or legacy server cannot be migrated with these tools, so report that and stop. Ask the user to choose one exact source MCP. 3. Call `get_mcp` with the source project ID and that exact `mcp_id`. Continue only when it returns a registration ID, `registration.status: registered`, `registration.components_complete: true`, `visibility: private`, and `effective_enabled: true`. A `disabled` source was deliberately turned off, and a `public` source cannot be retired with these tools; in either case stop and report that state rather than re-creating it, because the target is always registered private and enabled regardless of the source. Retain its `name`, `slug`, `visibility`, `source.kind`, `source.provider`, `source.reference`, `distributions`, and registration ID internally as the source shape. 4. Call `get_mcp_access` for the source and retain its stored tool names and the roles that reach it for later comparison. 5. Call `get_mcp_client_admission` with the source project slug and registration ID and retain the returned `mode` and custom client ID metadata URLs. If it answers `invalid_request`, the admission setting cannot be read by this caller, most often because another administrator registered the source: mark admission as unreadable and skip every source-scoped probe and lifecycle step. 6. Call `list_plugins` for the source project ID, following every `next_cursor` until the list is complete, then `get_plugin` for each plugin, matching `servers[].mcp_slug` against the source `slug`, because `distributions` lists only Platform-flow memberships. Record each carrying plugin's name, policy, and assignment set; these memberships are project-scoped and are not copied. 7. Present the migration inventory before any write. Copied by registration: backend kind, catalogue entry or canonical remote URL, and display name; the target is always created private and enabled, whatever the source's state. Re-created only after the user's explicit confirmation in later steps: the client admission mode, plugin membership, and audience, each by name. Re-collected from the user: the declared non-secret configuration values, which no tool reads back from the source. Cannot be copied: secret configuration, the identity-provider attachment and every user's authorization, the endpoint address and custom domains, custom client ID metadata URLs, and access-role rules pinned to the source MCP. Ask the user to acknowledge this inventory explicitly. 8. Re-derive the candidate from the persisted source shape and branch by `source.kind`: - For `reviewed_catalogue`, call `inspect_mcp_candidate` with `provider_key` set to `source.provider` and `catalog_ref` set to `source.reference`. Present the declared configuration fields and collect only the non-secret values from the user; stored source values are not readable and must be supplied again. - For `user_supplied_url`, call `inspect_mcp_candidate` with `remote_url` set to `source.reference` and no catalogue selectors, and present the canonical URL and the source `name` for confirmation. If the entry no longer exists or inspection reports an error, stop without registering. Compare the returned tool names with the source access read and report differences as evidence rather than failure. 9. Present the bounded target change: the exact target project, the re-derived candidate, the non-secret configuration, and everything that must be redone. After explicit confirmation, for `reviewed_catalogue` call `register_catalog_mcp` with the exact target project slug, `provider_key`, `catalog_ref`, `non_secret_config`, and a fresh idempotency key; for `user_supplied_url` call `register_remote_mcp` with the exact target project slug, `remote_url`, the source `name` as `display_name`, and a fresh idempotency key. Registration is private and does not distribute the MCP. Read `next_action`: `ready` and `start_setup` continue to the readiness check; `continue_dashboard_setup` and `secure_dashboard_setup_required` return a `dashboard_setup_url` to present as-is in step 11. A `conflict` with reason `active_registration_cap` or an `ineligible_project` refusal means the target project cannot receive it; a `feature_unavailable` refusal means registration is unavailable right now; a plain `conflict` means the idempotency key was already used with different input. In every case report the refusal and stop; do not retry against another project or with altered input. 10. Call `find_mcp` for the target project and select only the entry whose `registration.id` equals the returned registration ID, then call `get_mcp` with the target project ID and that `mcp_id` to obtain its `version`, `source.provider`, and `source.reference`. If the target `name` differs from the source `name` and the user wants it kept, call `update_mcp_metadata` with the target project slug, registration ID, `mcp_id`, the source name, the immediately preceding `version` as `expected_version`, and a fresh idempotency key. 11. Call `get_mcp_readiness` with the target project slug and registration ID to inspect persisted readiness. Registration writes no readiness, so a freshly registered MCP reports `readiness_unavailable`; that is not a reason to stop. Call `get_mcp_readiness` once with `force: true` to obtain provider evidence, then route secure setup from the exact target evidence: - For `upstream_identity_provider_not_configured`, explain that AICP can attach the one identity provider discovered from the persisted source. Ask for explicit confirmation, then call `attach_platform_mcp_identity_provider` with `confirmed: true`, present its exact `authorization_url` as a clickable link, and wait for the user to use Connect or Authorize. - For `upstream_authorization_required`, call `attach_platform_mcp_identity_provider` again with `confirmed: true` to retrieve the current server-issued `authorization_url` and wait for Connect or Authorize. - For a `continue_dashboard_setup` or `secure_dashboard_setup_required` registration result or `required_header_missing` evidence, present the exact `dashboard_setup_url`. If none was returned, call `get_setup_handoff` with the target project slug, registration ID, and the target's `source.provider` as `provider_key` and `source.reference` as `catalog_ref` exactly as the step-10 `get_mcp` returned them (for a remote URL these are the platform's direct-remote provider key and the canonical URL), never values retyped from the source project or supplied by the user, and present only its exact `setup_url`. The user re-enters every secret there, never in chat; never request the resulting value. 12. After the user completes any secure handoff, call `get_mcp_readiness` with `force: true` again from the same connection. Do not rely on stale or inferred readiness. If the state is still not `ready`, return to the routing in step 11 with the new evidence and repeat until it is `ready` or the user stops. Forced probes are limited to three per minute for one registration, so never loop them to wait out a handoff. A `rate_limited` refusal means that budget is spent: call `get_mcp_readiness` without `force` to read the last stored check, which is already fresh from the prior probe, and force again only after a minute has passed. 13. When readiness is current and ready, report the server-returned evidence. Call `get_mcp_client_admission` with the target project slug and registration ID and present the target's current `mode` next to the source mode. If the source mode was readable, differs from the target's, and appears in the target's `allowed_modes`, explain what it admits and refuses, ask for explicit confirmation, then call `set_mcp_client_admission` with that exact mode and `confirmed: true`. A source mode absent from `allowed_modes` cannot be written: report it and leave the target's mode unchanged. Known clients (`presets`) refuses an unlisted client at authorization with no fallback, so confirm the user accepts that before selecting it. Custom client ID metadata URLs are re-added only in the AICP dashboard. 14. Call `list_plugins` for the target project ID, following every `next_cursor` until the list is complete, present its plugins alongside the source carrying plugins by name, and ask the user which one exact target plugin should carry this MCP. Do not choose for them and do not assume the default plugin. Call `get_plugin` for that plugin. If the user wants the source audience mirrored, call `list_plugin_assignments` for the target project, present the complete replacement set, state that it changes who receives every MCP server in that plugin, ask for explicit confirmation, then call `set_plugin_assignments` with the immediately preceding `assignment_version` as `expected_assignment_version`, a fresh idempotency key, and `confirmed: true`. 15. Confirm this exact distribution with the user. If more than a minute has passed since the fresh ready probe, call `get_mcp_readiness` with `force: true` again from the same connection, then call `distribute_mcp_to_plugin` with the target project slug and that exact plugin. Report `not_found`, `ambiguous_target`, `not_ready`, or `approval_required` as-is and ask again rather than retrying with a different plugin or project. 16. Access-role rules pinned to the source MCP do not cover the target. Report the roles the source access read listed and use the `manage-mcp-access` workflow in the target project to re-author them; do not claim access parity. 17. After distribution, call `get_plugin` for that plugin and `get_mcp` for the target again. Report the live attachment, `publication_state`, and readiness separately. Do not claim that users have the MCP unless the returned live state supports that conclusion. The source is still enabled at this point; do not claim the cutover is complete. 18. Retiring the source is a separate decision. Only when the user explicitly asks, call `get_mcp` for the source again and present its live state. Explain that disabling makes its endpoint unreachable for users and clears custom-domain roots while keeping the registration, plugins, and roles for rollback. After explicit confirmation, call `disable_mcp` with the source project slug, registration ID, `mcp_id`, the immediately preceding `expected_version`, and a fresh idempotency key. A `feature_unavailable` answer with reason `unsupported_lifecycle_target` on a Platform-managed source means another administrator registered it; do not repeat its message verbatim, and tell the user to retire it in the AICP dashboard, where its registration keeps its project slot until removed. A `conflict` means the source changed since the last read, for example it became public or was already disabled: call `get_mcp` again, present the live state, and renew confirmation; a public source is retired only in the AICP dashboard. Never delete anything. 19. After disabling, call `get_mcp` for the source and for the target again and report both live states. Rollback stays available: `enable_mcp` on the source with its fresh `expected_version` and a fresh idempotency key restores it, and `remove_mcp_from_plugin` with the target project slug and plugin detaches the target membership this workflow created. Provider authorization, secret re-entry, and dashboard-only attributes are the expected out-of-agent stops. Source and target selection, the migration inventory acknowledgement, non-secret configuration, and the registration, admission, distribution, and source retirement confirmations remain explicit conversation checkpoints. All selectors and mutation-control fields remain agent-operational.