Skip to main content
Glama
aasthapit

ocp-triage-mcp

by aasthapit
README.md
# ocp-triage-mcp

An MCP server that triages OpenShift alerts by orchestrating an **upstream OCP
MCP server** (the one exposing `oc get nodes`, `get namespaces`, `describe
pods`, etc.). This server is both an MCP *server* (to whoever is triaging) and
an MCP *client* (of the OCP MCP) — the consuming team never touches the
upstream server directly.

```
 LLM / agent ──MCP──▶ ocp-triage-mcp ──MCP (Streamable HTTP)──▶ OCP MCP ──▶ cluster
                        │
                        └── runbooks/*.yaml   (one file per alert code)
```

Each alert code maps to a **runbook**: a YAML-defined sequence of upstream tool
calls. Triage is deterministic — no LLM inside this server — so evidence
collection is repeatable, auditable, and cheap. The LLM sitting above it
interprets the evidence bundle.

## Tools exposed

| Tool | Purpose |
|---|---|
| `list_runbooks` | Alert codes supported, required/optional inputs, steps |
| `triage_alert(alert_code, params)` | Run the full runbook, return the evidence bundle |
| `run_step(alert_code, step_id, params)` | Re-run one step of a runbook |
| `validate_runbooks` | Check all runbooks against the live upstream tool list |

The evidence bundle reports per-step status (`ok` / `error` / `skipped` /
`aborted`) so partial failures are visible, never silent.

### Passthrough discovery tools

Callers usually need to *find* the runbook inputs first — which clusters,
namespaces, and pods exist. Set `TRIAGE_PASSTHROUGH_TOOLS` to a
comma-separated allowlist of upstream tool names (fnmatch patterns allowed):

```
TRIAGE_PASSTHROUGH_TOOLS=get_clusters,get_namespaces,get_pods,list_*
```

Matching upstream tools are re-exposed on this server verbatim — same name,
same input schema, same description — and calls are forwarded to the OCP MCP.
Nothing is passed through by default; the surface stays curated. The tool list
is fetched from the upstream lazily and cached; `validate_runbooks` refreshes
it and reports which names currently match.

## Setup

> Full guide — install, verification, hosting for another team, container
> deployment, troubleshooting: **[docs/setup.md](docs/setup.md)**

Quick start:

```bash
pip install -e .
```

Configuration is via environment variables:

| Variable | Meaning | Default |
|---|---|---|
| `OCP_MCP_URL` | Upstream OCP MCP Streamable HTTP endpoint, e.g. `https://host/mcp` | *(required)* |
| `OCP_MCP_HEADERS` | Extra upstream headers, `;;`-separated: `Authorization: Bearer x;;X-Y: z` | none |
| `TRIAGE_PASSTHROUGH_TOOLS` | Upstream tools to re-expose here (comma-separated, fnmatch patterns) | none |
| `TRIAGE_RUNBOOKS_DIR` | Directory of runbook YAMLs | `./runbooks` |
| `TRIAGE_MCP_TRANSPORT` | This server's transport: `stdio`, `streamable-http`, `sse` | `stdio` |
| `TRIAGE_HTTP_HOST` / `TRIAGE_HTTP_PORT` | Listen address for the HTTP transports | `127.0.0.1` / `8000` |

Variables can also live in a `.env` file next to the server (copy
[.env.example](.env.example)); real environment variables override it.

Run it:

```bash
ocp-triage-mcp
```

Registering in Claude Code (stdio):

```json
{
  "mcpServers": {
    "ocp-triage": {
      "command": "ocp-triage-mcp",
      "env": {
        "OCP_MCP_URL": "https://ocp-mcp.example.com/mcp",
        "OCP_MCP_HEADERS": "Authorization: Bearer <token>",
        "TRIAGE_RUNBOOKS_DIR": "C:/GIT/mcp-runbook/runbooks"
      }
    }
  }
}
```

To serve it to another team over HTTP instead, set
`TRIAGE_MCP_TRANSPORT=streamable-http` and deploy it like any web service.

## Writing runbooks

One YAML file per alert code in `runbooks/`:

```yaml
alert: KubePodCrashLooping          # the alert code callers pass to triage_alert
description: What this runbook collects and why.

inputs:
  required: [namespace, pod]        # must be present in params
  optional: [cluster]

steps:
  - id: describe_pod                # unique id; defaults to the tool name
    tool: describe_pod              # tool name ON THE UPSTREAM OCP MCP
    args:
      namespace: "{{namespace}}"    # template from params...
      pod: "{{pod}}"

  - id: node_status
    tool: describe_node
    when: "{{describe_pod.spec.nodeName}}"   # skip unless resolvable & truthy
    continue_on_error: true                  # don't abort the runbook on failure
    args:
      node: "{{describe_pod.spec.nodeName}}" # ...or from earlier step results
```

Templating rules:

- `{{name}}` resolves from `params` first, then from earlier step results by
  step id.
- Dotted paths (`{{describe_pod.spec.nodeName}}`) walk into a step's result —
  this requires the upstream tool to return JSON (structured content or a JSON
  text block). Plain-text output is kept verbatim and can't be path-referenced.
- A string that is exactly one template keeps the referenced value's type
  (numbers, booleans, objects); mixed strings are substituted as text.
- Steps run sequentially. A step failure aborts the rest of the runbook unless
  the failing step has `continue_on_error: true`.

**The example runbooks use placeholder tool names.** After pointing
`OCP_MCP_URL` at your real server, call `validate_runbooks` — it lists the
upstream's actual tools and flags every runbook step that references a tool
the upstream doesn't expose.

## Design notes

- **Fresh upstream connection per call.** Each `triage_alert` opens its own
  Streamable HTTP session to the upstream and closes it when done. Remote
  sessions get dropped by idle timeouts/proxies; reconnecting per run makes
  every triage self-contained at negligible handshake cost.
- **Runbooks are re-read from disk on every call**, so editing a YAML takes
  effect without restarting the server. If load cost ever matters, add mtime
  caching in `server._load`.
- **No LLM inside.** If a runbook someday needs in-flight reasoning, first try
  extending `when:` conditions; embedding an agent is the last resort.