Skip to content
Back to skills

Sales Demos Mcp

CSecurity

Connect Claude Code to this repo's OpenShift clusters, AAP instances, RHDH portal, Automation Orchestrator, and Grafana Cloud over MCP — up to ten servers, one skill. Generates per-environment kubeconfigs for OpenShift, auto-creates bearer tokens for AAP, extracts portal MCP tokens, registers the AO and Grafana Cloud MCP servers, then verifies every server answers. TRIGGER when: the user asks to set up, connect, refresh or fix the MCP servers, says an openshift-sandbox, openshift-demo, opensh...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 6, 2026
devopspythonrustgoshellbashnodekubernetesgitapibackend

Works with

  • claude code
  • cli
  • api
  • mcp

Security analysis

C67/100
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
  • criticalSends environment variables or credentials to an external URL
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 6, 2026

npx -y skills add ericcames/sales.demos --skill sales-demos-mcp --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sales Demos Mcp?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Sales Demos Mcp
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/ericcames-sales-demos-mcp/badge)](https://www.skillsdirectory.com/skills/ericcames-sales-demos-mcp)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: sales-demos-mcp
description: "Connect Claude Code to this repo's OpenShift clusters, AAP instances, RHDH portal, Automation Orchestrator, and Grafana Cloud over MCP — up to ten servers, one skill. Generates per-environment kubeconfigs for OpenShift, auto-creates bearer tokens for AAP, extracts portal MCP tokens, registers the AO and Grafana Cloud MCP servers, then verifies every server answers. TRIGGER when: the user asks to set up, connect, refresh or fix the MCP servers, says an openshift-sandbox, openshift-demo, openshift-edge, aap-sandbox, aap-demo, portal-sandbox, portal-demo, ao-sandbox, ao-demo, or grafana MCP server is failing or shows no tools, or has just repointed an environment or rotated a token. SKIP: if the user wants to install OpenShift Virtualization or apply AAP configuration — that is sales-demos-setup — or wants to deploy the AAP MCP server into a cluster, which is playbooks/mcp_server.yml run by sales-demos-setup."
---

# sales-demos-mcp

Makes the clusters, AAP instances, RHDH portal, and Grafana Cloud directly
queryable from Claude Code. Before this, every question about an environment
cost a `curl`, a vault read and a JSON parse; after it, `namespaces_list`,
`job_templates_list`, or a portal catalog query is a tool call.

**No playbook, by design.** This touches the laptop — it writes kubeconfigs,
creates tokens, and configures your MCP client. It must never run from AAP,
which is the same reasoning that keeps `sales-demos-collections-sync`,
`sales-demos-first-time` and `sales-demos-ee-build` playbook-free.

## What it sets up

**Up to ten servers — nine per-environment, one global/external:**

| Server | Auth | Access | Source |
|---|---|---|---|
| `openshift-sandbox` | kubeconfig | read-write | `.mcp.json` (committed) |
| `openshift-demo` | kubeconfig | read-only | `.mcp.json` (committed) |
| `openshift-edge` | kubeconfig | read-write | `.mcp.json` (committed) |
| `aap-sandbox` | bearer token | read-write (server-side, `group_vars/sandbox/mcp.yml`) | `.mcp.json` (committed) |
| `aap-demo` | bearer token | **read-only** (server-side, `group_vars/demo/mcp.yml`) | `.mcp.json` (committed) |
| `portal-sandbox` | static token | read-only | `.mcp.json` (committed) |
| `portal-demo` | static token | read-only | `.mcp.json` (committed) |
| `ao-sandbox` | JWT (auto-refreshed) | read-only | `claude mcp add --scope local` |
| `ao-demo` | JWT (auto-refreshed) | read-only | `claude mcp add --scope local` |
| `grafana` | service account token | read-only | `claude mcp add --scope local` |

**There is no `aap-edge`, and that is not an oversight to fix in passing.**
`edge` runs AAP, so the server is plausible, but adding it is a posture
decision, not a usage-line fix — the same call #405 made about that script.

**There is no `ao-edge` either, for a different reason: AO is not installed on
`edge`.** Measured 2026-09-11 — `openshift-edge` has no Route in the
`automation-orchestrator` namespace. Posture is not the blocker here: the AO
server is read-only everywhere, and `make-ao-mcp.sh` accepts any environment
name. `.claude/settings.json` already allowlists `mcp__ao-edge__*` (added with
the server in #465), so once `/sales-demos-orchestrator` has run against `edge`,
`bash utilities/make-ao-mcp.sh edge` is the whole job. Until then the script
stops at the Route lookup, which is the correct failure.

**One server per environment, named after it, is the whole design.** #16 is the
precedent: when two environments were not kept distinct, `--limit demo`
silently resolved to sandbox's hostname and sandbox's token with no warning at
all. A single server whose target changed underneath you would reintroduce
exactly that, so the environment is in the server's *name* and you pick it by
picking the tool.

`demo` is read-only on both servers, because it is the environment customers
watch, and **the two guards live in different places and must move together**:

- **OpenShift** — `--read-only` on `openshift-demo` in `.mcp.json`, enforced by
  the client. You can see it in the tracked file.
- **AAP** — `aap_mcp_allow_write_operations` in `inventory/group_vars/<env>/mcp.yml`
  (`true` for sandbox, `false` for demo), enforced by the server. It is *not*
  visible in `.mcp.json`, which is why this table names the file. Nothing
  overrides it during setup: `setup.yml` deploys demo read-only from the start.

Changing the AAP value means editing `mcp.yml` and re-running `mcp_server.yml`
(or the **AAP Ecosystem - Install MCP Server** template), which deletes and
recreates the server so the new permission actually takes effect. The token and
URL do not change, so no Claude restart is needed. See #102.

### Grafana Cloud server

`grafana` is a **single server**, not per-environment — Grafana Cloud is an
external SaaS instance that survives RHDP rebuilds. That is the whole point of
choosing it over self-hosted (#260). The service account token has the Viewer
role (read-only, matching the governance thesis). See
[`grafana-plan`](https://ericcames.github.io/sales.demos-docs/plan/grafana-plan/).

### OpenShift servers

`.mcp.json` is committed and defines the three OpenShift servers. `demo` has
`--read-only`, which removes the nine mutating tools and keeps every
investigative one — including `vm_guest_info` and `vm_troubleshoot`. `sandbox`
and `edge` carry all 25.

The `kubevirt` toolset is enabled because this repo is an OpenShift
Virtualization demo. It supplies `vm_create`, `vm_clone`, `vm_lifecycle`,
`vm_guest_info` and `vm_troubleshoot`.

### AAP servers

The AAP MCP servers **run in the cluster** (deployed by `playbooks/mcp_server.yml`,
which `setup.yml` calls). The client side needs a bearer token, and tokens must
not go in tracked files.

`.mcp.json` defines `aap-sandbox` and `aap-demo` as stdio servers (#515). Each
entry calls `utilities/aap-mcp-stdio.sh <env>`, which reads the token and route
URL from gitignored files in `.aap/` and bridges stdio to the remote server via
`npx supergateway`. The credential stays out of the tracked file — same pattern
as the kubeconfig paths for OpenShift servers.

**A local-scope entry of the same name outranks `.mcp.json`** (precedence is
local > project > user). Before #515 these servers were registered with
`claude mcp add --scope local` as HTTP entries with the URL inline, and one left
behind keeps the server on its old cluster through every regeneration and
restart. `make-aap-mcp.sh` removes it and then asserts, through
`claude mcp get`, that the project entry is the one that wins; it fails rather
than trust the removal (#603).

`utilities/make-aap-mcp.sh` automates the full flow: resolve credentials from
the vault, create a personal access token via the gateway API, find the MCP
route, and write `.aap/<env>.token` and `.aap/<env>.url` — the full `/mcp`
endpoint, since the route root answers `404` (#603). Token scope is always
`write` — server-side enforcement (`aap_mcp_allow_write_operations`) is the
real guard, not the token scope.

**These tokens do not clean themselves up.** They are the documented exception
in CLAUDE.md — an MCP client needs a durable credential, so the `always:` block
rule does not apply. The script prints cleanup instructions; retiring them is
manual.

## Preflight Check

Every one must pass.

```bash
ENV=${ENV:-sandbox}

test -s "$HOME/secrets/.vault_pass_sales_demos" \
  && echo "✅ vault password file" \
  || echo "❌ ~/secrets/.vault_pass_sales_demos missing — see /sales-demos-first-time step 2"

head -c 15 playbooks/group_vars/all/secrets.yml 2>/dev/null | grep -q '^\$ANSIBLE_VAULT' \
  && echo "✅ secrets.yml is vault-encrypted" \
  || echo "❌ secrets.yml is NOT encrypted — stop, do not commit"

command -v npx >/dev/null \
  && echo "✅ npx ($(node --version)) — needed to launch kubernetes-mcp-server" \
  || echo "❌ npx not found — install Node.js, or fetch the pinned binary from https://github.com/containers/kubernetes-mcp-server/releases"

command -v uvx >/dev/null \
  && echo "✅ uvx ($(uvx --version 2>/dev/null || echo 'unknown')) — needed to launch mcp-grafana" \
  || echo "❌ uvx not found — install uv (https://docs.astral.sh/uv/getting-started/installation/)"

test -f .mcp.json \
  && echo "✅ .mcp.json present" \
  || echo "❌ .mcp.json missing — it is committed; you may be outside the repo root"

# All MCP credentials (kubeconfig, AAP URL, AO registration) still point AT
# this environment. Generating them once is not enough: repointing an
# environment (via connection.yml, local.yml, or vault edits) does NOT
# regenerate these files (#161, #533). The failure is otherwise a DNS error
# naming a dead cluster, which says nothing about credentials.
bash utilities/check-mcp-staleness.sh "$ENV" 2>&1 || true

test -d "inventory/group_vars/$ENV" \
  && echo "✅ environment '$ENV' exists" \
  || echo "❌ no such environment '$ENV'"
```

If any fails, stop and tell the user which one and the fix beside it.

## Collect inputs

| Variable | Default | Meaning |
|---|---|---|
| `ENV` | `sandbox` | Which environment to (re)generate servers for |

Never prompt for a token and never pass one on the command line — that puts it
in shell history. Everything is read from the vault.

## Run

### Step 1 — OpenShift kubeconfig

```bash
bash utilities/make-kubeconfig.sh sandbox
```

Generate `demo` or `edge` too only if the user is actually working against
them. `demo` is the environment customers watch, and a stale kubeconfig for it
is harmless whereas a confidently wrong one is not. `edge` is the bare-metal
SNO — it is persistent rather than ephemeral, so its kubeconfig goes stale far
less often than an RHDP one.

The file lands at `.kube/<env>.kubeconfig`, mode `0600`. `.gitignore` covers
`.kube/`, and the script writes the token only after locking the file down, so
the credential is never briefly world-readable.

### Step 2 — AAP MCP server

Run **after** the kubeconfig exists — the script needs it to find the MCP route.

```bash
bash utilities/make-aap-mcp.sh sandbox
```

For `demo`:

```bash
bash utilities/make-aap-mcp.sh demo
```

The script creates a personal access token, finds the `aap-mcp` route via the
kubeconfig, and writes `.aap/<env>.token` and `.aap/<env>.url`. `.mcp.json`
references these through the `aap-mcp-stdio.sh` wrapper, so the server comes
online on the next Claude Code restart — no `claude mcp add` needed.

### Portal (RHDH) servers

The RHDH self-service portal exposes an MCP endpoint natively (#555) via the
`backstage-plugin-mcp-actions-backend` plugin, with `software-catalog-mcp-tool`
and `techdocs-mcp-tool` as action sources. `portal.yml` enables the plugins,
generates a static bearer token, and stores it in a `portal-mcp-token` Secret.

`.mcp.json` defines `portal-sandbox` and `portal-demo` as stdio servers. Each
entry calls `utilities/portal-mcp-stdio.sh <env>`, which reads the token and
route URL from gitignored files in `.portal/` and bridges stdio to the remote
endpoint via `npx supergateway`. Same pattern as the AAP servers.

`utilities/make-portal-mcp.sh` reads the portal Route and the `portal-mcp-token`
Secret from the cluster via the kubeconfig, and writes `.portal/<env>.token` and
`.portal/<env>.url`. Simpler than the AAP flow — the token already exists in the
Secret, so no API call creates one.

The token dies with the portal deployment (which dies with the RHDP env). On a
fresh bootstrap, `portal.yml` generates a new token and creates a new Secret.
`make-portal-mcp.sh` picks up the new one. No vault entry, no rotation — same
ephemeral-environment reasoning as the AAP MCP PAT.

### Step 3 — Portal MCP server

Run **after** the kubeconfig exists — the script needs it to find the portal
Route and Secret.

```bash
bash utilities/make-portal-mcp.sh sandbox
```

For `demo`:

```bash
bash utilities/make-portal-mcp.sh demo
```

The script reads the portal Route and `portal-mcp-token` Secret, then writes
`.portal/<env>.token` and `.portal/<env>.url`. `.mcp.json` references these
through the `portal-mcp-stdio.sh` wrapper, so the server comes online on the
next Claude Code restart.

### Step 4 — Automation Orchestrator MCP server

Run **after** the kubeconfig exists — the script needs it to find the AO Route.

```bash
bash utilities/make-ao-mcp.sh sandbox
```

For `demo`:

```bash
bash utilities/make-ao-mcp.sh demo
```

The script resolves the AO admin password from the vault (it is the AAP admin
password, #143), finds the AO Route via the kubeconfig, verifies login works,
and registers the server with `claude mcp add --scope local`. The server
manages JWT refresh transparently — no token to retire.

**Prerequisite:** `pip install mcp` — the server is a Python MCP server using
the official SDK.

### Step 5 — Grafana Cloud MCP server

Independent of the kubeconfig and AAP steps — Grafana Cloud is an external
service, not tied to any RHDP environment.

```bash
bash utilities/make-grafana-mcp.sh
```

The script reads `grafana_cloud_url` and `grafana_cloud_sa_token` from the
vault and registers the server with `claude mcp add --scope local`.

**Prerequisite:** a Grafana Cloud account with a service account token
(Viewer role). See [`grafana-plan`](https://ericcames.github.io/sales.demos-docs/plan/grafana-plan/)
for the manual browser setup steps.

**Restart Claude Code after running for the first time.** MCP servers are
launched at startup; a server that was not registered then stays absent until
the client is relaunched.

## Verify — ask the server, not the config

### OpenShift

**A generated kubeconfig is not proof.** It proves a file was written, not that
the cluster accepts it. Ask the target:

```bash
KUBECONFIG=$PWD/.kube/sandbox.kubeconfig oc whoami
KUBECONFIG=$PWD/.kube/sandbox.kubeconfig oc get nodes -o name
```

Then prove the MCP server itself starts and answers, independent of the client:

```bash
python3 - <<'PY'
import json, subprocess, os, sys
kc = os.path.abspath(".kube/sandbox.kubeconfig")
p = subprocess.Popen(
    ["npx","-y","kubernetes-mcp-server@0.0.66","--kubeconfig",kc,
     "--toolsets","core,config,kubevirt","--disable-multi-cluster"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
send = lambda o: (p.stdin.write(json.dumps(o)+"\n"), p.stdin.flush())
send({"jsonrpc":"2.0","id":1,"method":"initialize","params":{
    "protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"verify","version":"0"}}})
info = json.loads(p.stdout.readline())["result"]["serverInfo"]
send({"jsonrpc":"2.0","method":"notifications/initialized"})
send({"jsonrpc":"2.0","id":2,"method":"tools/list"})
tools = json.loads(p.stdout.readline())["result"]["tools"]
send({"jsonrpc":"2.0","id":3,"method":"tools/call",
      "params":{"name":"namespaces_list","arguments":{}}})
got = json.loads(p.stdout.readline())
p.terminate()
ok = info.get("name") == "kubernetes-mcp-server" and len(tools) >= 20 and "result" in got
print(f"server        : {info.get('name')} {info.get('version')}")
print(f"tools exposed : {len(tools)}")
print(f"live call     : {'namespaces_list returned data' if 'result' in got else got}")
print("\nMCP VERIFIED" if ok else "\nVERIFICATION FAILED — do not report success")
sys.exit(0 if ok else 1)
PY
```

Expect 25 tools on `sandbox`. If it returns 16, the `--read-only` flag is being
applied to the wrong server — check which entry in `.mcp.json` was launched.

### AAP

Verify the AAP MCP server by hitting its endpoint directly, with the same token
and URL files the stdio bridge reads:

```bash
ENV=${ENV:-sandbox}
AAP_MCP_URL=$(cat .aap/$ENV.url)      # already ends in /mcp
AAP_MCP_TOKEN=$(cat .aap/$ENV.token)

curl -sk -o /dev/null -w '%{http_code}\n' -X POST "$AAP_MCP_URL" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $AAP_MCP_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"verify","version":"0"}}}'
```

`200` and a body naming `"serverInfo":{"name":"aap"}` is the pass. **The endpoint
requires the bearer token** (#515), so a `401` from a request without the
`Authorization` header proves nothing about the server (#594). **A `503` means
the Route is admitted but the pod is not serving yet** — wait and retry rather
than assuming a misconfiguration. Measured on a working sandbox: **140 tools**,
including `job_templates_launch_create`, `workflow_job_templates_launch_create`
and `jobs_stdout_retrieve`.

### Portal (RHDH)

Verify the portal MCP endpoint is reachable:

```bash
ENV=${ENV:-sandbox}
PORTAL_URL=$(cat .portal/$ENV.url)
PORTAL_TOKEN=$(cat .portal/$ENV.token)

curl -sk -o /dev/null -w '%{http_code}\n' -X POST "$PORTAL_URL" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $PORTAL_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"verify","version":"0"}}}'
```

`200` is the pass, with `"serverInfo":{"name":"backstage"}` in the body. The
portal is read-only — it exposes the software catalog and TechDocs, not mutating
operations.

**The `Accept` header is not optional**, and it is the line this block used to
be missing (#789). The endpoint is Streamable HTTP and rejects a request that
does not name *both* content types, so without it a perfectly healthy server
answers `406 Not Acceptable` and the documented check cannot pass. A `401
Illegal token` is the failure this check is actually for — the token in
`.portal/<env>.token` no longer matches the `portal-mcp-token` Secret, which is
what happens when the portal is redeployed. Both are "not 200" and only one is
a real problem, so read the body, not just the number.

### Automation Orchestrator

`claude mcp add` registers the server, but its tools only load when Claude Code
starts — **restart first**, then ask the server rather than re-reading the
config. Two calls, in this order:

1. **`mcp__ao-<env>__version`** — proves the server started, logged in and
   reached the API. The pass is `"api_version": "v1"`. Measured on sandbox
   2026-09-11: `info_version` `1.1.0`.
2. **`mcp__ao-<env>__proxies_aap_job_templates`** — proves AO's AAP integration
   works, which is what makes AO useful. The pass is `count` greater than 0;
   measured on sandbox: **33**. A `count` of 0 or an error with `version`
   passing means the MCP server is fine and AO is not connected to AAP — that
   is `/sales-demos-orchestrator-config`, not this skill.

Measured on a working sandbox: **32 tools**, all read-only. A tool whose name is
missing after a restart is the server failing to start. The usual cause is the
SDK: `python3 -c 'import mcp'` fails, and `pip install mcp` fixes it. Otherwise
re-run `make-ao-mcp.sh <env>`, which tests the AO login before it registers
anything.

### Grafana Cloud

Verify the Grafana MCP server starts and can reach the Grafana Cloud instance:

```bash
python3 - <<'PY'
import json, subprocess, sys, os

vault_pass = os.path.expanduser("~/secrets/.vault_pass_sales_demos")
vault_id = f"sales.demos@{vault_pass}"

import yaml
raw = subprocess.check_output(
    ["ansible-vault", "view", "playbooks/group_vars/all/secrets.yml",
     "--vault-id", vault_id], text=True)
secrets = yaml.safe_load(raw)
url = secrets["grafana_cloud_url"]
token = secrets["grafana_cloud_sa_token"]

p = subprocess.Popen(
    ["uvx", "mcp-grafana"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
    text=True, env={**os.environ, "GRAFANA_URL": url,
                    "GRAFANA_SERVICE_ACCOUNT_TOKEN": token})
send = lambda o: (p.stdin.write(json.dumps(o)+"\n"), p.stdin.flush())
send({"jsonrpc":"2.0","id":1,"method":"initialize","params":{
    "protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"verify","version":"0"}}})
info = json.loads(p.stdout.readline())["result"]["serverInfo"]
send({"jsonrpc":"2.0","method":"notifications/initialized"})
send({"jsonrpc":"2.0","id":2,"method":"tools/list"})
tools = json.loads(p.stdout.readline())["result"]["tools"]
send({"jsonrpc":"2.0","id":3,"method":"tools/call",
      "params":{"name":"list_datasources","arguments":{}}})
got = json.loads(p.stdout.readline())
p.terminate()
ok = len(tools) >= 10 and "result" in got
print(f"server        : {info.get('name')} {info.get('version')}")
print(f"tools exposed : {len(tools)}")
print(f"live call     : {'list_datasources returned data' if 'result' in got else got}")
print("\nGRAFANA MCP VERIFIED" if ok else "\nVERIFICATION FAILED — do not report success")
sys.exit(0 if ok else 1)
PY
```

Finally, confirm the client sees all servers:

```bash
claude mcp list
```

## If it fails

| Symptom | Cause | Fix |
|---|---|---|
| `✔ Connected` but calls fail or return no data | stdio servers (OpenShift, Grafana, AO) start locally; the check mark proves the process launched, not that the cluster is reachable | Verify with `oc whoami` or a live tool call (`namespaces_list`, `nodes_top`); if the cluster is dead, the kubeconfig is stale |
| MCP server shows as failed at startup | Kubeconfig does not exist yet | Run the generator, then restart Claude Code |
| `401 Unauthorized` on a tool call | Token in the vault is stale or the environment expired | Update `env_secrets.<env>.openshift_api_token`, re-run the generator |
| `could not resolve <env> token` | Vault password wrong, or `env_secrets.<env>` missing | `/sales-demos-first-time` step 2 |
| `dial tcp: no such host` | The RHDP environment has expired | Check `connection.yml` points at a live cluster — both had expired once before (#101) |
| Tools present but every call fails | Kubeconfig points at a different cluster than you think | `oc whoami --show-server` with `KUBECONFIG` set |
| AAP MCP returns `503` | Route admitted, pod not serving yet | Wait — `oc get deploy aap-mcp -n aap`; this is normal for ~60s after deploy |
| `aap-<env>` still dials the previous cluster after a restart, though `.aap/<env>.url` is current | A pre-#515 local-scope registration outranks `.mcp.json` (#603) | `claude mcp remove aap-<env> -s local` from the main checkout, restart. `check-mcp-staleness.sh <env>` names every such shadow |
| AAP MCP returns `401` to a request with **no** `Authorization` header | The command is wrong, not the server: the endpoint requires the bearer token (#594) | Use the Verify → AAP block as written. Do **not** mint a new token for this |
| AAP MCP returns `401` **with** `Authorization: Bearer $(cat .aap/<env>.token)` | Token expired or deleted | Re-create it: `bash utilities/make-aap-mcp.sh <env>`, then restart Claude Code. Retire the old token by hand; these do not clean themselves up |
| AAP MCP write tools missing | `aap_mcp_allow_write_operations` is false for this environment | Intentional on `demo`. Changing it needs a delete-and-recreate — re-run `mcp_server.yml`, which handles that |
| `npx: command not found` | Node not installed | See preflight; a standalone binary is the alternative |
| `no aap-mcp route` from make-aap-mcp.sh | MCP server not deployed | Run `/sales-demos-setup` or `playbooks/mcp_server.yml` first |
| Portal MCP returns `401 Illegal token` | Stored token no longer matches the `portal-mcp-token` Secret — usual cause is the portal being redeployed | `bash utilities/make-portal-mcp.sh <env>` re-reads the Secret; only re-run `portal.yml` if the Secret itself is missing |
| Portal MCP returns `406 Not Acceptable` | The request omitted `Accept: application/json, text/event-stream` — the server is fine (#789) | Fix the command, not the server. Use the Verify → Portal block as written |
| `no portal route` from make-portal-mcp.sh | Portal not deployed | Run `/sales-demos-setup` or `playbooks/portal.yml` first |
| Portal MCP `no such host` | RHDP environment expired | Same as OpenShift — repoint and re-bootstrap |
| Grafana `grafana_cloud_url not set` | Vault keys missing or still CHANGEME | `ansible-vault edit` and add real values — see the [grafana plan](https://ericcames.github.io/sales.demos-docs/plan/grafana-plan/) |
| Grafana MCP tools present but calls fail | Token expired or revoked | Create a new SA token in the Grafana Cloud UI, update the vault |
| `uvx: command not found` | uv not installed | See preflight; install from https://docs.astral.sh/uv/ |

Never paste a live token into a commit message, issue, or PR. This repo is
public — see `CLAUDE.md`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…