clo-mcp
by zz22394
README.md
# clo-mcp — Cloudera Lakehouse Optimizer MCP
An MCP server that wraps the **Cloudera Lakehouse Optimizer (CLO)** REST API — the
`DLM` (Data Lifecycle Manager) API — so you can ask an AI things like *"which tables in
`mydb` have an optimization policy, and what do their stats look like?"* instead of
clicking through Postman.
Reverse-engineered from the `Cloudera Lakehouse Optimizer Demo` Postman collection.
**10 read tools + 8 guarded write tools.** Works against two deployments via config:
public-cloud Knox (bearer JWT, path `dlm/api/v1`) and CDP-Base direct (trusted-proxy,
path `clo/api/v1`, reached over an SSH tunnel). What you can do with it: `USE-CASES.md`;
workshop walkthrough: `DEMO-CASES.md`.
## Read tools
| Tool | CLO endpoint | What it answers |
|------|--------------|-----------------|
| `health` | `GET /config/health` | Is CLO up? |
| `list_namespaces(fetch=false)` | `GET /namespaces` | All namespaces (fetch=true refreshes from catalog) |
| `list_active_namespaces` | `GET /namespaces/active` | Namespaces with optimization enabled |
| `list_namespace_policies(namespace)` | `GET /namespaces/{ns}/policies` | Policies applied in a namespace |
| `get_table_stats(table)` | `GET /tables/{ns.table}/stats` | Iceberg file counts/sizes, snapshots |
| `describe_table_policy(table, policy=ClouderaAdaptive)` | `GET /policies/{policy}/tables/{ns.table}/desc` | Effective policy on a table |
| `list_policy_tables(namespace, policy=ClouderaAdaptive)` | `GET /policies/{policy}/tables?namespace=` | Tables governed by a policy |
| `describe_policy_resource(uri)` | `GET /policies/resource?uri=` | A policy by its `dlm://` URI |
| `list_tasks` | `GET /tasks` | All optimization tasks + status |
| `get_task(task_id)` | `GET /tasks/id/{id}` | One task |
`table` args are `namespace.table`, e.g. `mydb.my_table`.
## Connection profiles
Set these in `.env` (see `.env.example`) or the MCP client's `env` block.
**A — bearer** (public-cloud Knox JWT):
```
CLO_BASE_URL=https://<host>/<cluster>/cdp-proxy-api
CLO_API_PREFIX=dlm/api/v1 # default
CLO_AUTH_MODE=bearer # default
CLO_TOKEN=<fresh Knox JWT> # or CLO_WORKLOAD_USER/PASSWORD to auto-mint
```
**B — trusted-proxy** (CDP-Base direct `:8085` via SSH tunnel to base-master):
```
# tunnel first:
# ssh -i <key> -N -L 8085:10.20.10.11:8085 \
# -o ProxyCommand="ssh -i <key> -W %h:%p rocky@<bastion>" rocky@<bastion>
CLO_BASE_URL=http://localhost:8085
CLO_API_PREFIX=clo/api/v1
CLO_AUTH_MODE=trusted-proxy
CLO_DOAS_USER=clodemo
CLO_ACTOR_GROUPS=dlm_admin
```
## Setup
```bash
cd /path/to/clo-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
```
### Auth — get a fresh Knox token
The token in the Postman export is **expired** (Knox JWTs are short-lived). Mint a new one:
```bash
export CLO_TOKEN="$(CLO_WORKLOAD_USER=<workload-user> CLO_WORKLOAD_PASSWORD='***' ./get_clo_token.sh)"
```
(or set `CLO_WORKLOAD_USER` / `CLO_WORKLOAD_PASSWORD` in the MCP env and let the server
auto-mint. If the derived token endpoint is wrong for your cluster, override
`CLO_KNOX_TOKEN_URL`.)
### Smoke test (without an MCP client)
```bash
CLO_TOKEN=... python3 -c "import server; print(server.health())"
```
## Register with Claude Code
```bash
claude mcp add clo -- python3 /path/to/clo-mcp/server.py
```
or add to your MCP config JSON:
```json
{
"mcpServers": {
"clo": {
"command": "/path/to/clo-mcp/.venv/bin/python",
"args": ["/path/to/clo-mcp/server.py"],
"env": {
"CLO_BASE_URL": "https://<clo-host>/<cluster>/cdp-proxy-api",
"CLO_TOKEN": "<fresh Knox JWT>"
}
}
}
}
```
Then ask: *"Use clo: is the optimizer healthy? List active namespaces, then show the
policy and stats for the tables in mydb."*
## Security
- **No token is stored in this repo.** Supply it at runtime via env; `.env` and `*.token`
are gitignored.
- Keep your token **and** base URL out of commits and screenshots.
## Write tools (guarded)
Mutating tools are present but **double-gated**: the server env must have
`CLO_ENABLE_WRITES=true` **and** the call must pass `confirm=true`. This stops an AI from
mutating the CLO env by accident. `dryrun_table_policy` is exempt — it only simulates.
| Tool | CLO endpoint | Notes |
|------|--------------|-------|
| `dryrun_table_policy(table, policy_uri, policy_text\|policy_file)` | `PUT /policies/{policy}/tables/{ns.table}/dryrun` | **Safe** — simulates, no gate |
| `add_namespace(namespace, confirm)` | `PUT /namespaces/{ns}` | onboard a DB |
| `reschedule_namespace(namespace, confirm)` | `PATCH /namespaces/{ns}` | apply policy to all tables (onboard step) |
| `evaluate_and_submit(table, policy_uri, policy_text\|policy_file, confirm)` | `POST …/tables/{ns.table}/evaluation` | **Runs Spark** — actual maintenance |
| `create_policy(policy_uri, policy_text\|policy_file, confirm)` | `PUT /policies/resource` | multipart, field name = policy_uri |
| `update_policy(policy_uri, policy_text\|policy_file, confirm)` | `PATCH /policies/resource` | |
| `associate_policy_to_table(table, policy_uri, …, confirm)` | `POST …/tables/{ns.table}/subs` | |
| `delete_policy(policy_uri, confirm)` | `DELETE /policies/resource?uri=` | **Destructive** |
Policy definitions go in via `policy_text` (an inline jexl/json string an AI can generate)
or `policy_file` (a path). `policy_uri` is the dlm:// resource id CLO expects as the
multipart field name, e.g. `dlm://tps/hive/<db>/<table>/ClouderaAdaptive`.
To enable: set `CLO_ENABLE_WRITES=true` in the server env, then call e.g.
`create_policy(policy_uri=..., policy_text=..., confirm=true)`.
> Write endpoint shapes are reverse-engineered from the Postman collection and may need a
> per-version tweak. Always `dryrun_table_policy` before `evaluate_and_submit`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues