--- name: add-existing-mcp-servers description: Use when migrating MCP servers already configured in a local agent client (Claude Code, Codex, Cursor, Gemini CLI, VS Code or another client) to an explicit Speakeasy AI Control Plane project as remote MCPs, including discovering which servers are missing and optionally restricting them to the organization's Tailscale private network. --- # Add existing MCP servers Add selected supported remote servers from the agent client you are running in to a Speakeasy AI Control Plane (AICP) project without editing local client configuration, then optionally restrict them to the organization's Tailscale private network. Installation grants no access; live authentication, entitlement, membership and administrator checks remain authoritative. All management mutations use authenticated Platform MCP tools, never direct backend APIs. ## 1. Verify access and request discovery permission Before any local discovery (including asking for manual inventory), successfully call `list_projects` with `limit: 100` through your OWN Speakeasy connection in this client session. Dashboard state, install intent, another client's authentication, or a prior session's result is not proof of access. It accepts only `limit` (capped at 100), not cursor or search. If unavailable or denied, stop and offer the normal Speakeasy sign-in/setup flow. If `truncated: true`, project discovery is incomplete: stop and hand off project selection to the AICP dashboard rather than inventing pagination or guessing a destination. If the user says they want the migrated servers kept on their Tailscale private network, call `get_network_ingress` once now and tell them what it reports (step 8 explains how to read it), so any dashboard setup can start while the import runs. A private network that is not ready never blocks the import; it only decides whether step 8 can finish in this session. Name the client you are running in, and discover only that client's own MCP servers, never another client's. Use a listing command only when you know it exists: | Client | Listing command | Known effects | | ------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Claude Code | `claude mcp list` | health-checks approved/enabled servers, as described below | | Any other client (for example Codex, Cursor, Gemini CLI or VS Code) | only a listing subcommand documented by that client's own `--help` | unknown until checked: unless the client's help states that listing never starts or connects to servers, describe and treat it as having the same effects as `claude mcp list` | Never invent a subcommand or flag. If the client has no documented listing command, or you cannot tell which client you are in, use the user-sanitized manual inventory. Never open a client's MCP configuration files yourself: they commonly hold headers, tokens and environment values. Before running `claude mcp list` or another client's listing command locally, obtain explicit permission. Require explicit informed consent for the following effects, not merely permission to view a list. Explain that `claude mcp list` health-checks approved/enabled servers, can launch stdio processes and contact local/private-network endpoints BEFORE filtering out unsupported entries. Those processes may write files or contact services; do not promise zero process side effects or passive/read-only discovery. Approval of a server in the client is not consent to run this discovery now. Explain the scope: current CLI user, working directory and configuration scope, not every client, account or workspace. Connection status does not establish remote importability. Offer a user-sanitized manual inventory instead (non-secret alias, safe endpoint, transport only), without running discovery, if the user declines those effects or local execution is unavailable. Never demand raw output. Do not inspect credential files, expand environment values, or forward raw output to any tool, service or report. Do not invent a no-connect flag. Never edit local configuration, approval settings or credentials to enable discovery. The no-edit rule constrains the agent; it cannot guarantee that the CLI or launched servers have no side effects. If discovery succeeds but returns no entries, report that no servers were found in the inspected scope. Offer a user-sanitized manual inventory or the normal catalogue path (`add-mcp-from-catalog`) as explicit next choices; do not switch workflows without the user's choice. Do not claim import completion for an empty inventory. ## 2. Sanitize and classify locally Retain only non-secret alias, exact safe endpoint, declared transport and provenance needed for selection. Never copy local credentials, including headers, tokens, passwords, environment secrets or OAuth state. Do not request secrets in chat. Sort every entry into exactly one group: - **Candidates:** public HTTPS Streamable HTTP endpoints. Only these can be added. - **Local servers, left unchanged:** stdio commands and localhost/loopback, private-network or link-local endpoints. They cannot become AICP remote MCPs through this workflow. Report each one to the user by alias and transport only; for stdio, say the command is not shown, because commands, arguments and environment can carry secrets. Do not migrate, wrap, tunnel, disable or remove them, and do not offer to. They stay exactly as configured in the client. - **Blocked:** unsupported transports (including legacy SSE), fragments, embedded credentials, credential-like query parameters and uncertain URLs. Safe non-secret endpoint query parameters can be meaningful; preserve them. Never manufacture a safe URL by silently stripping credentials or changing its path. Block the entire uncertain item and ask for secure/manual resolution; do not echo sensitive URL components. Server validation remains authoritative, including network safety checks. Exclude the connected Speakeasy management endpoint itself to prevent recursive import. Establish its identity from trusted connection endpoint metadata or other exact endpoint evidence, not display name alone. If that evidence is unavailable or ambiguous, flag the possible self-reference for manual resolution rather than importing it. ## 3. Confirm selection and check live inventory Present sanitized candidates, local servers left unchanged, and blocked items. Confirm candidate selection and destination from the eligible projects returned above; keep that project's ID and slug paired. Never infer the destination from a local alias or the Default project. Use `find_mcp` with the selected `project_id` and `limit: 100`, no query or readiness filter. Follow every `next_cursor` using `cursor` with the same project until exhausted. Never combine `query` and `cursor`; a name search is not an exhaustive inventory. If listing fails or pagination cannot finish, do not conclude a candidate is missing. Use `get_mcp` with `project_id` and returned `mcp_id` to inspect possible matches. Compare exact upstream URL and server-issued source/registration evidence. Deduplicate aliases only by proven identity, not hostname or display name: different paths or meaningful query values may represent different servers. Preserve the alias-to-item mapping for the final report. If identity cannot be proven, block for manual resolution rather than guessing or creating a duplicate. An identity match alone is not an already-present success: apply the model-specific completion evidence in step 6 before classifying it. A matching pending or incomplete registration must not be reported as already present or complete, and must not trigger a duplicate registration. ## 4. Inspect missing candidates and confirm the exact batch Prefer a suitable reviewed catalogue entry, even when its endpoint differs from the local server, but never silently substitute based on a name. Keep the original sanitized alias/endpoint and the effective confirmed target as separate identities. 1. Search exact endpoint identity FIRST: call `search_mcp_catalog` with the safe full endpoint as `query`. This is text search, not an exact URL lookup: its only inputs are optional `query`, `provider_key` and `cursor`; there is no URL lookup flag or `remote_url` search input. Follow `next_cursor` with the same `query` and `provider_key` (unlike `find_mcp` pagination). Results contain `provider_key`, `catalog_ref`, name, description, version, tool count and setup intent, not guaranteed endpoint evidence. Inspect plausible results with `inspect_mcp_candidate` using the returned `provider_key` and `catalog_ref`, without `remote_url`. Claim exact endpoint identity only if returned evidence proves it; an absent `canonical_url` is unknown, not a match. 2. If no suitable exact match is established, search local non-secret alias/provider/name SECONDARY using `query`. Do not invent a `provider_key` from a product name: it is a server-issued source filter. Deduplicate results by returned `provider_key` plus `catalog_ref`. A name hit is only a proposed alternative, never proof of equivalence. A URL search miss does not rule out a reviewed alternative. 3. Inspect plausible reviewed alternatives using those exact returned identities. Present candidate name, description, version, source/reference, transport, tool names/count, declared configuration and setup needs. Compare the original endpoint with the candidate's returned endpoint where available; explain region, product and tools differences, and explicitly mark unknown differences. Do not infer regional or functional equivalence from a shared name. If ambiguous, present the alternatives and ask for one exact candidate; do not choose automatically. 4. Catalogue substitutions require separate confirmation of the exact candidate, differences/unknowns, declared non-secret configuration and destination project. Only after explicit acceptance set the effective target to the returned `provider_key`/`catalog_ref` plus confirmed configuration and any returned endpoint. Recheck existing registrations after substitution: repeat the selected project's complete `find_mcp` inventory and inspect matches with `get_mcp`, comparing server-issued source/registration evidence for this confirmed catalogue target, not only the original URL. Deduplicate confirmed catalogue targets across aliases too, only when configuration identity is proven. Two configurations of one catalogue reference are not the same target. Inventory does not expose selected configuration values or a configuration fingerprint; source/reference alone cannot prove configuration equivalence. For any registration without server-backed proof of its exact effective configuration, keep configuration identity unverified and request manual resolution; do not claim already present or create a duplicate. If already present, apply step 6's completion checks and do not register again; pending/incomplete or uncertain identity blocks duplicate creation. 5. If the user declines, no suitable match exists, or catalogue search is unavailable, offer the original safe direct remote path; disclose search failure rather than claiming no match. Unresolved ambiguity must not trigger a catalogue write: ask the user to choose or explicitly continue direct remote. For a direct fallback, call `inspect_mcp_candidate` with the original `remote_url`, without `provider_key` or `catalog_ref`. Present its `canonical_url`, `transport`, `tool_names`/`tool_count`, `trust`, `authentication`, `oauth_discovery`, `automatic_client_registration`, `requires_dashboard_setup`, `setup_category` and `actions` where returned. After declining a catalogue candidate, require explicit confirmation of the inspected direct target, destination project and exact direct batch before `register_remote_mcp`; declining the candidate is not consent to the fallback. Never manufacture a direct URL from a catalogue name. Unsupported, denied or inconclusive candidates remain blocked pending returned guidance. Distinguish observed tools from missing evidence; an empty tool list or authentication requirement is not proof of readiness. Obtain explicit confirmation of the exact inspected batch and project before any write: show each effective target, original alias/endpoint, chosen catalogue or direct path, evidence and outstanding setup needs, plus already-present and blocked items. If inspection changes the endpoint, disclose it and reconfirm. Changed project, candidate or configuration requires fresh inspection as applicable and renewed confirmation. Use only the catalogue inspection/registration portion here, not the entire `add-mcp-from-catalog` workflow: import does not require readiness or plugin distribution. ## 5. Add confirmed supported items independently For each confirmed missing catalogue target, call `register_catalog_mcp` with `project_slug`, the exact returned `provider_key` and `catalog_ref`, only declared `non_secret_config`, and a caller-generated `idempotency_key`. Never create a custom direct-remote entry for a confirmed catalogue replacement. Correlate the exact submitted confirmed configuration, project and catalogue identity with the returned `registration_id` in the non-secret operation receipt; retain this mapping for final verification. This correlates a request with a returned registration ID only; it does not prove creation or persisted configuration. `register_catalog_mcp` can reuse an existing registration for the same source/reference with different configuration unchanged. Neither a new receipt nor `replayed: false` proves that the submitted configuration took effect. For each confirmed missing direct remote item, call `register_remote_mcp` with `project_slug`, the confirmed `remote_url`, optional `display_name`, and a caller-generated `idempotency_key`. Keep one key per logical operation. Preserve all logical-operation inputs and the same idempotency key on retries, including after timeouts or uncertain write outcomes. Do not generate a new key merely because the result was lost. A changed operation requires renewed confirmation and a fresh key. Apply returned repair/retry guidance and rate limits; do not retry permanent denial unchanged or bypass validation. Continue independent items after failure. Retain non-secret receipts, `registration_id`, `canonical_url`, `next_action` and any `dashboard_setup_url` for verification and handoff. A successful response alone is not completion. ## 6. Verify every selected item live Re-read the selected project's complete inventory with `find_mcp` pagination and `get_mcp` for matching entries. Verify all selected items, including already-present entries and uncertain write outcomes, against the effective confirmed target identity and project. For catalogue replacements, match fresh inventory `registration.id` to the receipt's returned `registration_id` where available, and verify the same project and returned `source.provider`/`source.reference` against the accepted `provider_key`/`catalog_ref`, plus any returned endpoint. Receipt-ID correlation alone is not persisted configuration proof. Require server-backed evidence of the registration's exact effective confirmed configuration before reporting added, already present or complete. `register_catalog_mcp`, `find_mcp` and `get_mcp` do not expose persisted configuration values or a persisted configuration fingerprint: do not invent fields or claim current configuration was read back. With this contract, configuration equivalence remains unverified and requires manual resolution, not automatic reuse. This applies even when the returned ID matches the receipt and live status is registered with complete components. A concurrent registration after preflight, an unknown existing registration or an uncertain write outcome must not be treated as newly created or correctly configured from the receipt; keep it unverified, do not create a duplicate, and offer manual dashboard resolution. Distinguish a configless candidate from an empty submitted `non_secret_config`: omitted values can use declared defaults, and a field may be optional or secret. Inspection's absent/empty `configuration` only describes the current candidate, not persisted settings of a reused registration; it is not a configless-success exemption. The current inspection does not return a catalogue `canonical_url`, and inventory does not bind persisted configuration to that inspected candidate version. If exact effective configuration cannot be proven server-side, even an apparently configless candidate remains unverified/manual resolution; never claim success from an empty request or absence of declared fields. In every case, do not verify against the original URL when the confirmed replacement differs. Preserve the original alias-to-confirmed-target mapping in the report. Do not use cached preflight results as final evidence. Reconcile uncertain writes before retrying with their original inputs/key; if still unverified, report uncertainty rather than success. For `model: platform_managed`, require a returned registration ID, `registration.status: registered` and `registration.components_complete: true` in addition to the exact project and effective target identity match (endpoint for direct remote; confirmed source identity and server-backed exact effective configuration proof for catalogue; receipt-ID correlation is insufficient) before reporting added, already present or complete. A missing registration, pending status or incomplete components means blocked/unverified; use returned repair guidance or a dashboard handoff, not duplicate creation. For an exact matching remote entry with `model: dashboard_managed` and no `registration`, report already present (dashboard-managed), not newly registered by this workflow. Do not require or invent a registration record for dashboard-managed entries; their `readiness.state: unsupported` is not evidence of a failed import or of working authentication. Other models or inconclusive identity/evidence remain blocked for manual resolution. Report added / already present / blocked / failed per item, with alias mappings and evidence or a reason. List local servers left unchanged separately so the user knows they were seen and deliberately not touched. Every selected supported server must be confirmed present in the selected project's live inventory to claim completion. Zero selections is not success. If any item remains blocked, failed or unverified, report partial completion and next steps, not unqualified success. ## 7. Keep authentication separate Report registration and authentication/readiness separately: “added to the project” does not mean connected, authorized, working or distributed. Offer exact server-returned Speakeasy setup/authorization links as clickable links, including `dashboard_setup_url`; never reconstruct or invent them. If no link is returned, say so and offer a manual dashboard handoff. Skip provider attachment for anonymous servers. Offer attachment only when inspection reports an authentication requirement and advertises a supported identity provider through its authentication/OAuth discovery evidence, and a registration ID is available. For direct remote inspection, require `authentication: authentication_required` and `oauth_discovery: available_dcr` or `oauth_discovery: available_cimd` before offering attachment; both are automatic client registration paths (`automatic_client_registration` names which), so describe them as automatic sign-in setup rather than manual setup. `available` alone or `incomplete` does not establish support for this automatic-registration flow. These are prerequisites, not a guarantee: the attachment tool still validates the supported provider and may return repair guidance. Authentication required with no supported provider evidence means a secure/manual setup handoff, not a speculative attachment call. Retain separate explicit consent for provider attachment. Only after those evidence checks and that consent, `attach_platform_mcp_identity_provider` takes `project_slug`, returned `registration_id` and `confirmed: true`; present its exact returned `provider_url` and `authorization_url`. Secret entry and provider sign-in belong in that secure browser flow, never in chat or tool arguments. Do not force readiness, provider attachment or distribution to finish import. Leave local config unchanged; do not migrate credentials or remove local entries. ## 8. Optionally restrict migrated servers to the Tailscale private network Offer this once, after step 6, or run it when the user asked for private access in step 1. It is optional: declining leaves the import complete. Explain what it does before asking: private access limits who can reach each server's AICP endpoint to devices on the organization's tailnet. It does not move or hide the upstream MCP server, which stays wherever its provider hosts it. It never changes local client configuration. 1. Eligible servers are only those this run proved present in step 6 (added or already present) with an exact MCP ID. Exclude anything blocked, failed, pending, incomplete or unverified, and any entry whose `get_mcp` result has `backend_kind: unproxied`: those support only `public_only`. 2. Call `get_network_ingress`. It requires organization administration; if it is denied, stop this step and say an organization administrator must run it. If `ready_for_private_access` is false, report its `next_action` in plain words and present its exact `setup_url` as a clickable link. `request_private_networking` means private networking is not switched on for the organization yet and must be requested from Speakeasy; the other actions happen in the AICP dashboard. Tailscale OAuth client credentials are entered only in that dashboard, never in chat or tool arguments. Do not attempt any network change while it is not ready. After the user reports finishing setup, call `get_network_ingress` again rather than assuming it is ready. `ready_for_private_access` is a precondition, not proof that any device can connect. 3. For each eligible server, call `get_mcp_network_traffic` with the project ID, `target_kind: mcp` and the exact MCP ID as `target_id`, and `window: "7d"`. Report its observed public and private request counts and last-seen times before proposing any transition. Counts cover only observed requests while telemetry was enabled: zero does not prove a route is unused or that every client has migrated. If traffic reporting is unavailable, say so and require an independent client inventory from the user before proposing `private_only`, which cuts off the public route. 4. Present the choice per server, and do not choose for the user. `dual` adds tailnet access and keeps public access, so existing clients keep working. `private_only` refuses every client that is not on the tailnet, including already installed plugins that use a public address and possibly this client. If the user has not yet tested tailnet connectivity, say `dual` is the lower-risk first step. 5. For each chosen server, call `get_mcp_connection_settings` with the project ID, `target_kind: mcp_server` and the exact MCP ID as `target_id`. Report its current `network_mode`, endpoints and plugin memberships. A server already in the requested mode needs no change; report it as unchanged. 6. Show the exact batch of transitions (server, current mode, requested mode) and obtain explicit confirmation of that batch and project. Immediately before each change, re-read `get_mcp_connection_settings` for that server, then call `set_mcp_network_access` with the same project and target, the confirmed `mode`, the fresh `version` as `expected_version`, a caller-generated `idempotency_key` kept stable for that exact request, and `confirmed: true`. On a conflict or refusal, re-read and ask again; never silently retry with another server or mode. If a `private_only` change is refused because an address must use the private network namespace, hand off to the `configure-private-mcp-access` workflow instead of changing addresses here. Continue independent servers after a failure. 7. Verify each change with a fresh `get_mcp_connection_settings` read; the mutation receipt records a request, not publication. For servers with plugin memberships, check each affected plugin with `get_plugin` as described in `configure-private-mcp-access`, and do not claim installed clients have refreshed. 8. Report per server: previous and current network mode, whether the fresh read confirmed it, publication evidence for affected plugins, and that the user should test from a device on the tailnet. Never edit local client configuration to point at a new address. ## 9. Record diagnostics only when the user asks This workflow is new, so a Speakeasy field engineer may give a user an exact phrase to run it with diagnostics. The trigger is the literal string `with diagnostics` in the user's own message for this run. Nothing else switches this section on: not "debug this", not "tell Speakeasy what happened", not a field engineer's instruction relayed second-hand, not a previous run that used it, and never your own initiative. If the user's wording is close but not that string, ask them to repeat the request with `with diagnostics` in it rather than deciding for them. Skipping this section is the normal outcome. When that string is present, first show them a **Run diagnostics** section covering the servers the user selected plus the entries sanitization excluded in step 2, including local servers left unchanged (as `blocked`, with the reason `local server left unchanged`). It is not a restatement of step 6: those excluded entries appear here and nowhere else. Give one row per server with its alias, transport, endpoint (or `—` when it has none), outcome, and the reason for anything that is not a plain `added` — the table and the payload carry the same rows and the same reasons. A candidate the user saw and chose not to import is not part of this report. Then tell the user that same report goes to Speakeasy rather than into their AI Control Plane project, and that they cannot read it back through the product. Proceed only after they agree. If they decline, the import is already complete; report it normally. Call `record_workflow_run` once with `skill` set to `add-existing-mcp-servers`, a caller-generated `run_id`, the confirmed destination `project_slug`, the `client` the inventory came from (the client you named in step 1, for example `claude_code`, `codex` or `cursor`), and one `items` entry per row of the section you just showed. Set `name` to the non-secret alias, `kind` to the declared transport, `endpoint` to the safe endpoint (omit it unless it is an `https` URL — a plain-http or loopback address goes in `reason` instead, written as scheme, host and path only), `outcome` to one of the five words below, and `reason` to the explanation for anything that is not a plain `added`. | Outcome | Use it for | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | `added` | step 6 proved the server is present with the configuration you submitted | | `added_unverified` | the add completed but step 6 could not prove its effective configuration, so it is neither confirmed nor failed | | `already_present` | step 6 found it already registered, and this run did not create it | | `blocked` | the run refused it: sanitization excluded it in step 2, or a check would not let it proceed | | `failed` | the run attempted it and the attempt errored | Never report a completed add as `blocked`. `blocked` means the run declined to act; an add that happened but could not be verified is `added_unverified`. A run that considered nothing still reports: send an empty `items` list rather than skipping the call. Never send raw discovery output, credentials, headers, environment values, or local commands. An address written into `reason` carries scheme, host and path only. Drop the query string and the fragment before writing it, every time, including for an entry step 2 excluded because its URL carried a credential — that entry is exactly the one whose query string must stay on this machine. `reason` is free text and reaches Speakeasy as written, so it is the one field where nothing is stripped for you. Diagnostics never gate the import. If the tool reports that diagnostics are not switched on, or returns `recorded: false`, tell the user the report was not recorded and that their servers were still imported; do not retry, and do not treat it as an import failure.