- name
- deploy-assist
- description
- Place generated MCP server deployment manifests into a cluster repo, fill in placeholders by inferring values from existing cluster configuration, and explain remaining manual steps. Use when you have a generated MCP server project and a cluster repo where you want to deploy it.
- argument-hint
- <generated-project-dir> <cluster-repo-path>
# Deploy Assist Skill
This skill handles the last mile of the mcp-builder pipeline: placing configured deployment manifests into a cluster repo. The pipeline transforms an OpenAPI spec into a ToolHive-ready MCP server across four phases — AI Scoping (Phase 1) produces a validated `mcp-scope.yaml`, Human Review (Phase 2) refines it, Deterministic Code Generation (Phase 3) scaffolds the complete server project, and AI Validation & Polish (Phase 4) verifies the generated code. This skill sits after that pipeline: it takes the generated project's deployment manifests, reads a cluster repo to understand its structure and environment, copies the manifests into the right location with placeholders filled, and explains what the user still needs to do.
The container image is assumed to already be built and pushed to a registry. This skill does not guide through docker build/push or run kubectl commands.
## Pipeline Context
Before starting, read `{skill_base_dir}/../ai-scoping/assets/pipeline-context.md` for full pipeline context — it describes what mcp-builder is, the four phases, and what `mcp-scope.yaml` contains. This skill's focus is the `deploy/` directory of the generated project.
## Startup
Before beginning the workflow:
1. Determine the base directory for this skill (the absolute path to the directory containing this SKILL.md). The repo root is two levels up: `{skill_base_dir}/../..`. Do not share these internal paths with the user.
## Workflow
Given a generated project directory and a cluster repo path ($ARGUMENTS), execute the following steps:
1. Collect and verify inputs
2. Understand the cluster repo (emits a preflight artifact)
3. Confirm inferred values (user gate)
4. Copy and configure manifests
5. Post-apply assertions
6. Explain what's left
---
### Step 1: Collect and Verify Inputs
1. Parse $ARGUMENTS for two required paths:
- **Generated project directory** — the Phase 3 output (e.g., `~/code/google-drive-mcp/`)
- **Cluster repo path** — the repo where deployment manifests live (e.g., `~/code/poc-demo-deploy/`)
If fewer than 2 arguments are provided, ask the user for the missing paths.
2. Verify the generated project directory exists and contains a `deploy/` subdirectory.
3. Catalog which deployment manifests exist in `deploy/`:
- `mcpserver.yaml` — **must exist**. If missing, tell the user and exit.
- `ingress.yaml` — expected but not fatal if missing.
- `mcpexternalauthconfig.yaml` — present only if auth is configured.
- `secret.yaml` — present only for api_key auth.
4. Read `deploy/mcpserver.yaml` and extract the server name from `metadata.name`.
5. Verify the cluster repo path exists.
---
### Step 2: Understand the Cluster Repo
Explore the cluster repo to answer five questions (Q1–Q5 below), then emit the preflight artifact described at the end of this step. Do not assume any particular repo structure — every cluster repo is organized differently.
**Question 1: Where should the new manifests go?**
Explore the repo's directory layout. Look for how existing services, apps, or MCP servers are organized. There is no single pattern — it might be directories per app, per namespace, per environment, or something else entirely. Understand the organizational pattern and determine where a new MCP server's manifests should be placed to be consistent with the rest of the repo.
**Question 2: What values should fill the placeholders?**
The generated manifests contain these placeholders that need real values:
| Placeholder | Files it appears in |
|---|---|
| `REPLACE_ME_DOMAIN` | mcpserver.yaml, ingress.yaml, mcpexternalauthconfig.yaml |
| `REPLACE_ME_OTEL_ENDPOINT` | mcpserver.yaml |
| `{server_name}-mcp:latest` (image needs registry prefix) | mcpserver.yaml |
The cluster repo likely contains existing deployments, config files, or values files that reveal the correct domain, OTEL endpoint, container registry, and namespace for this environment. Find them however makes sense for this repo — they might be in Kubernetes manifests, Helm values, Terraform configs, Kustomize overlays, environment files, or documentation.
**Question 3: How does deployment work here?**
Determine the deployment mechanism: Flux, ArgoCD, Helm, plain `kubectl apply`, or something else. This shapes the final guidance about how the user should trigger deployment after the manifests are in place.
**Question 4: What shape do existing MCPServer resources in this repo use?**
The generator emits manifests against a known CRD version, but the target cluster may pin a different operator version or follow local conventions (shared-ALB ingress, telemetryConfigRef, oidcConfigRef patterns, etc.). Silent mismatches here are the single largest source of first-deploy failures.
**Find candidate siblings deterministically.** Do not rely on directory intuition:
```
find {cluster_repo} -type f \( -name '*.yaml' -o -name '*.yml' \) \
-exec grep -l 'kind: MCPServer' {} \;
```
- **If >0 results:** pick the file whose path has the smallest edit-distance to the proposed target directory from Question 1. That file is the "nearest sibling". Record its path (relative to the cluster repo root) as `sibling_used` in the preflight artifact below.
- **If 0 results:** record `sibling_used: ""`, note "first MCPServer in repo — no sibling-diff possible", and skip the field extraction and diff below. Proceed with the rest of Question 4 marked as N/A in the preflight artifact (empty lists / empty strings per the schema).
From the chosen sibling, extract:
- The set of top-level keys under `spec` (e.g. `image`, `transport`, `oidcConfigRef`, `telemetryConfigRef`, `externalAuthConfigRef`).
- The set of sibling CRD kinds referenced — e.g. does this repo use a shared `MCPTelemetryConfig`, standalone `MCPOIDCConfig`, inline blocks, or something else?
- Any ingress pattern differences (shared ALB group, URL-rewrite transforms, backend service naming — operator-managed `mcp-{name}-proxy` vs. plain `{name}`, whether a separate authinfo ingress exists, whether paths/audiences end with `/mcp`).
- If a sibling `MCPExternalAuthConfig` exists, the shape of its `spec.embeddedAuthServer.upstreamProviders[*]`: `type` (`oidc` vs. `oauth2`), which config block is populated (`oidcConfig` vs. `oauth2Config`), and field-mapping patterns under `userInfo`. Don't stop at the top-level `spec` — descend into this subtree.
Diff those sets against what's in the generated `deploy/` directory and populate the preflight artifact's `sibling_diff` block (see the "Preflight artifact" subsection at the end of Step 2). Each of these categories must be represented as an explicit (possibly empty) list — absence of a list is the signal that the check was skipped:
- **Keys present in generated but NOT in sibling** (`spec_keys_only_in_generated`) — likely to be rejected by the cluster's CRD. Most common class of failure.
- **Keys present in sibling but NOT in generated** (`spec_keys_only_in_sibling`) — the cluster expects something the generator didn't emit. Usually requires adding a ref or annotation.
- **Kinds referenced by sibling but NOT emitted by generator** (`kinds_only_in_sibling`) — e.g. sibling uses `telemetryConfigRef: shared-telemetry` and the generator emits an inline `telemetry:` block. Requires an adapter rewrite.
- **Ingress shape mismatches** (`sibling_diff.ingress.*`) — each of `sibling_service_name_pattern` vs `generated_service_name`, `sibling_has_authinfo_ingress`, and `sibling_path_has_mcp_suffix` is recorded explicitly.
- **Upstream provider `type` mismatch** (`sibling_diff.external_auth.*`) — e.g. generated uses `type: oidc` but all siblings use `type: oauth2` with explicit endpoints. Note this; Question 5 will confirm whether a rewrite is required.
**Question 5: Does the upstream IdP's OIDC discovery doc actually conform?**
Only applies when the generated `mcpexternalauthconfig.yaml` has `upstreamProviders[*].type: oidc` with a non-empty `issuerUrl`. The embedded auth server performs strict OIDC discovery validation at startup and crash-loops on non-compliant docs — this is the single most common silent first-deploy failure after the sibling-diff class.
Record the probe outcome in the preflight artifact's `oidc_discovery` block. There are exactly three outcomes:
1. **Unresolvable issuer URL** (contains template literal like `{companyDomain}`, DNS failure, or network error). Record `resolvable: false` with `failure` set to the reason. Add `replace_template_literal` to `rewrites_planned` and add the literal's name to `user_inputs_required`. Do not mark `oidc_to_oauth2` yet — the user must supply the tenant first.
2. **Resolvable but non-compliant** — fetch `{issuerUrl}/.well-known/openid-configuration` and confirm all of these fields are present:
- `response_types_supported`
- `id_token_signing_alg_values_supported`
- `subject_types_supported`
- `authorization_endpoint`
- `token_endpoint`
- `jwks_uri`
If any are missing, record `resolvable: true`, `failure: "non-compliant discovery doc: missing <fields>"`, and add `oidc_to_oauth2` to `rewrites_planned`. The rewrite will be performed in Step 4 by the `rewrite_upstream_oidc_to_oauth2` subroutine defined below.
3. **Resolvable and compliant** — record `resolvable: true`, empty `failure`, and leave `rewrites_planned` untouched. No rewrite needed.
Skip this check entirely when `type: oauth2` is already in use, when there is no `mcpexternalauthconfig.yaml`, or when auth type is `none`/`api_key`. In the skipped case, still emit the `oidc_discovery` block with `resolvable: false` and `failure: "skipped: <reason>"` so the preflight artifact's shape is uniform.
---
### Subroutine: `rewrite_upstream_oidc_to_oauth2`
Extracted from the former Step 2 Q5 / Step 4 prose into a named, independently verifiable routine. Called from Step 4 when `oidc_to_oauth2` is in `rewrites_planned`. Clear contract so the rewrite is testable in isolation, even once the upstream auth-schema-alignment work lands and most OIDC scopes stop needing it — this remains the escape hatch for `oidc`-declared IdPs whose discovery docs don't conform.
**Signature:**
```
rewrite_upstream_oidc_to_oauth2(
discovery_doc: dict | None,
sibling_fieldmapping: dict | None,
user_provided_tenant: str | None,
) -> patched_yaml
```
**Inputs — three explicit branches, do not collapse:**
- `discovery_doc`:
- **dict** — the parsed `.well-known/openid-configuration`. Use its `authorization_endpoint`, `token_endpoint`, and `userinfo_endpoint` verbatim.
- **None** — the issuer URL was unresolvable (template literal, DNS failure). The subroutine cannot invent endpoints; caller must either supply `user_provided_tenant` so the literal can be substituted and the doc re-fetched, or abort.
- `sibling_fieldmapping`:
- **dict** — the nearest sibling's `spec.embeddedAuthServer.upstreamProviders[*].oauth2Config.userInfo.fieldMapping`. Use verbatim.
- **None** — no sibling exists or none have `oauth2Config`. Default to `{subjectFields: [sub], nameFields: [name], emailFields: [email]}`.
- `user_provided_tenant`:
- **str** — the value the user confirmed at the Step 3 gate for a template literal in the issuer URL (e.g. `stacklok` for `{companyDomain}`). If set, substitute it across the issuer URL and across every matching literal in the generated project's source tree (grep the generated project for the same literal and replace everywhere). Note in Step 6 that the container image must be rebuilt and the `spec.image` digest updated after substitution.
- **None** — no template literal present, or literal already resolved.
**Output — `patched_yaml`:** the modified `mcpexternalauthconfig.yaml` contents, with:
- `upstreamProviders[*].type` changed from `oidc` to `oauth2`.
- The `oidcConfig:` block replaced by an `oauth2Config:` block.
- `authorizationEndpoint`, `tokenEndpoint`, and `userInfo.endpointUrl` populated from the discovery doc.
- `userInfo.fieldMapping` populated from `sibling_fieldmapping` or the default above.
- `clientId`, `clientSecretRef`, `redirectUri`, and `scopes` preserved verbatim from the original `oidcConfig` block.
**Verification contract:** the returned YAML parses cleanly, contains no `oidcConfig` key, and every endpoint URL is absolute (starts with `https://`).
---
### Preflight artifact
Before presenting findings to the user, the skill writes a structured preflight file to `{cluster_repo}/.deploy-assist-preflight.yaml`. The shape is pinned by `{skill_base_dir}/assets/preflight-schema.yaml` — every top-level key is required; leaf fields default to empty string, empty list, or `false`. An absent or empty required field is the signal to the user that the skill skipped work, which Step 3 then flags.
**Purpose:** force Q1–Q5 findings into a machine-checkable shape before the user gate. Prose summaries let LLMs skim past tedious cross-reference steps (wrong ingress service names, missing authinfo ingress, unresolved template literals, fabricated endpoints). A structured artifact makes absence of a check visible.
**Content:** populate each field from the answers to Q1–Q5. Concretely:
- `sibling_used` — path recorded in Q4 (or `""` if 0 candidates).
- `sibling_diff.spec_keys_only_in_generated`, `spec_keys_only_in_sibling`, `kinds_only_in_sibling` — from Q4.
- `sibling_diff.ingress.*` — from Q4's ingress comparison. Each subfield is explicit; do not omit.
- `sibling_diff.external_auth.*` — from Q4 when a sibling `MCPExternalAuthConfig` exists; otherwise both `sibling_upstream_type` and `generated_upstream_type` are `""`.
- `oidc_discovery.*` — from Q5, with the three outcomes (unresolvable / non-compliant / compliant / skipped) encoded as described there.
- `rewrites_planned` — ordered list of rewrite tokens the skill will apply in Step 4. Known tokens: `oidc_to_oauth2`, `replace_template_literal`, `ingress_service_rename`, `add_authinfo_ingress`, `add_mcp_suffix`.
- `user_inputs_required` — any values the user must supply before Step 4. Each entry is `"name: description"` (e.g. `"companyDomain: concrete value for {companyDomain}"`).
Do not proceed to Step 3 until the file is written and its content matches the schema. Treat this as the exit criterion of Step 2.
---
### Step 3: Confirm Inferred Values (USER GATE)
Present what you found to the user:
1. **Server name** (from the generated project)
2. **Target directory** in the cluster repo where manifests will be placed
3. **Inferred values:**
- Namespace
- Container registry prefix (for the image reference)
- Domain (for `REPLACE_ME_DOMAIN`)
- OTEL endpoint (for `REPLACE_ME_OTEL_ENDPOINT`)
4. **Auth type** detected from the generated manifests (oauth2 / oidc / api_key / none)
5. **Deployment mechanism** (Flux, ArgoCD, plain manifests, etc.)
6. **Files to copy** (list of manifests from `deploy/`)
7. **Preflight artifact**: show the path `{cluster_repo}/.deploy-assist-preflight.yaml` and either display its content inline or summarize each section (`sibling_used`, `sibling_diff`, `oidc_discovery`, `rewrites_planned`, `user_inputs_required`). For each planned rewrite, state what the skill will do and why:
- `oidc_to_oauth2` — rewrite required because the upstream IdP's discovery doc is non-compliant (list missing fields). This is a proposed rewrite, not an open question — the server won't start otherwise — but the user may override.
- `replace_template_literal` — the issuer URL contains an unresolved template (show the exact literal). Ask the user for the concrete value.
- `ingress_service_rename`, `add_authinfo_ingress`, `add_mcp_suffix` — show what the sibling expects vs what the generator emitted, and confirm the rewrite.
- Each sibling-diff divergence not already covered above should be labeled **keep as-generated**, **rewrite to match sibling**, or **ask user**. Default to rewriting when the sibling uses a cluster-wide convention (shared-ALB ingress, shared telemetry/OIDC refs); ask when it could be either a generator bug or a sibling convention.
If any entries in `user_inputs_required` were not resolved by inference, ask the user to provide them now.
**Do NOT proceed until the user confirms the values, the target location, and every rewrite in `rewrites_planned`.**
---
### Step 4: Copy and Configure Manifests
Create the target directory in the cluster repo if it does not exist.
For each manifest in the generated project's `deploy/` directory, read the file, perform the replacements below, and write it to the target directory.
**mcpserver.yaml:**
- Replace `REPLACE_ME_DOMAIN` with the confirmed domain
- Replace `REPLACE_ME_OTEL_ENDPOINT` with the confirmed OTEL endpoint (match the format expected by the template — typically `host:port` without protocol prefix)
- Update `spec.image` from `{server_name}-mcp:latest` to `{registry_prefix}{server_name}-mcp:latest`
- Update `metadata.namespace` if it differs from the confirmed namespace
**ingress.yaml:**
- Replace `REPLACE_ME_DOMAIN` with the confirmed domain
- Update `metadata.namespace` if needed
- Adjust annotations or `ingressClassName` if the cluster repo's existing Ingress resources use a different pattern
**mcpexternalauthconfig.yaml (if present):**
- Replace `REPLACE_ME_DOMAIN` with the confirmed domain
- Update `metadata.namespace` if needed
- **Leave `clientId: REPLACE_ME` as-is** — this is a secret the user must fill in
- **If `rewrites_planned` contains `oidc_to_oauth2`**, call the `rewrite_upstream_oidc_to_oauth2` subroutine defined after Step 2. Pass:
- `discovery_doc` — the dict fetched in Step 2 Q5, or `None` if the issuer URL is still unresolvable.
- `sibling_fieldmapping` — the nearest sibling's `oauth2Config.userInfo.fieldMapping`, or `None`.
- `user_provided_tenant` — the value the user confirmed at the Step 3 gate for any template literal, or `None`.
Apply the returned `patched_yaml` in place of the in-memory file contents before writing. If `user_provided_tenant` was used, also grep the generated project's source tree for the same literal and substitute it everywhere — note in Step 6 that the container image must be rebuilt and `spec.image` digest updated.
**secret.yaml (if present):**
- Update `metadata.namespace` if needed
- **Leave `token: REPLACE_ME` as-is** — this is a secret the user must fill in
If the cluster repo has other files that need updating to pick up the new manifests (e.g., a Kustomization file that lists resources, an ArgoCD Application, a Flux Kustomization), note this but do not modify them automatically — flag it for the user in Step 6.
---
### Step 5: Post-Apply Assertions
After every file in Step 4 has been written, re-read the files from disk (not from the in-memory pre-write state) and run each assertion below. Every assertion must pass before the skill can exit cleanly. An assertion failure blocks Step 6 — report the specific failure to the user and stop.
Ver en GitHub