nifi-mcp
# nifi-mcp
An MCP server and a CLI for the Apache NiFi REST API, sharing one async client.
- **`nifi-mcp`** — exposes NiFi to an LLM agent over stdio (59 tools).
- **`nifi`** — the same capabilities from a terminal, with tables by default and `--json` for scripting.
Built and verified against **NiFi 1.15.3**. See [SPEC.md](SPEC.md) for the design decisions.
## Install
```bash
uv sync
source .venv/bin/activate
nifi doctor # verify connectivity, auth, host header, and version
```
## Configuration
Settings resolve in this order: environment → `.env` → defaults.
Copy `.env.example` to `.env` to start. The important ones:
| Variable | Default | Meaning |
|---|---|---|
| `NIFI_BASE_URL` | `https://172.21.80.1:8443` | The NiFi instance |
| `NIFI_HOST_HEADER` | `localhost:8443` | Host header override — see below |
| `NIFI_ALLOW_WRITE` | `false` | Gates every mutating **MCP** tool |
| `NIFI_VERIFY_SSL` | `false` | TLS verification (dev server is self-signed) |
### Credentials
Credentials come from the environment, and only from the environment:
| Variable | Meaning |
|---|---|
| `NIFI_USERNAME` | Login identity |
| `NIFI_PASSWORD` | Login password |
Set them however your environment does it — exported in the shell, in the MCP server's
`env` block, or in a `.env` file next to `pyproject.toml` (gitignored). The process is
handed its secret; it never goes looking for one on disk, so what it authenticates with
does not depend on the directory it was launched from.
```bash
export NIFI_USERNAME='7090b632-6bb7-4a11-b6bc-c308a331e559'
export NIFI_PASSWORD='...'
nifi doctor
```
NiFi in single-user mode generates the pair on its first run and logs it once, to
`logs/nifi-app.log` on the NiFi host:
```bash
grep 'Generated \(Username\|Password\)' logs/nifi-app.log
```
```
... o.a.n.a.s.SingleUserCredentialsGenerator Generated Username [<uuid>]
... o.a.n.a.s.SingleUserCredentialsGenerator Generated Password [<password>]
```
Copy those two values into the variables above. If the line has already been rotated out
of the log, set a pair of your own instead:
```bash
./bin/nifi.sh set-single-user-credentials <username> <password> # on the NiFi host
```
The values are exchanged for a **JWT** at `POST /access/token`; the token is cached, its
`exp` is decoded, and it is refreshed about 60s before expiry (and once more on any
`401`). The password itself is sent only at login, is held in a `SecretStr`, and never
appears in logs or `repr()`.
### The host header
NiFi validates the `Host` header against `nifi.web.proxy.host`. Reaching the instance by
an address that is not in that allowlist — for example from WSL at `172.21.80.1` when NiFi
runs on the Windows host — gets this:
```
The request contained an invalid host header [172.21.80.1:8443]
```
`NIFI_HOST_HEADER` rewrites the header without changing where the connection goes. Set it
to an empty value if your NiFi accepts the real host. `nifi doctor` diagnoses this case.
## CLI
```bash
nifi doctor # connectivity + auth + version diagnostics
nifi about # version and identity
nifi status # flow health at a glance
nifi bulletins # recent warnings and errors
nifi pg tree # the flow, as a tree
nifi pg list|get|create|delete|start|stop
nifi processor list|get|create|update|start|stop|run-once|terminate|delete
nifi connection list|get|create|delete
nifi queue list|peek|empty
nifi cs list|get|create|enable|disable|delete
nifi param list|get|create|set|delete
nifi types search|show
nifi provenance query|lineage
nifi version status|registries|commit
nifi system diagnostics|counters|cluster
```
Global flags: `--json` (raw JSON), `--yes` (skip destructive prompts), `--dry-run`
(show what would happen), `--verbose` (log HTTP to stderr).
Exit codes: `0` ok, `1` NiFi error, `2` usage error, `3` connection or auth failure.
### Example: build and run a flow
```bash
GID=$(nifi --json pg create demo | jq -r .id)
GEN=$(nifi --json processor create GenerateFlowFile --pg $GID --name gen \
--period "1 sec" --prop "File Size=64B" | jq -r .id)
LOG=$(nifi --json processor create LogAttribute --pg $GID --name log \
--y 300 --auto-terminate success | jq -r .id)
nifi connection create $GEN $LOG -r success --pg $GID
nifi pg start $GID
nifi connection list $GID # watch the queue
nifi pg stop $GID
nifi --yes pg delete $GID
```
Component types accept short names (`GenerateFlowFile`); they are resolved against the
instance's catalogue. `nifi types search kafka` finds what is installed.
## MCP server
Register with Claude Code:
```bash
claude mcp add nifi -- /home/alexsaez/projects/nifi-mcp/.venv/bin/nifi-mcp
```
Or add to your MCP client config directly:
```json
{
"mcpServers": {
"nifi": {
"command": "/home/alexsaez/projects/nifi-mcp/.venv/bin/nifi-mcp",
"env": {
"NIFI_BASE_URL": "https://172.21.80.1:8443",
"NIFI_HOST_HEADER": "localhost:8443",
"NIFI_USERNAME": "7090b632-6bb7-4a11-b6bc-c308a331e559",
"NIFI_PASSWORD": "...",
"NIFI_ALLOW_WRITE": "false"
}
}
}
}
```
Note that env values in this config file are stored in plain text, so it should be
readable only by you. To keep the password out of the file entirely, export
`NIFI_USERNAME` / `NIFI_PASSWORD` in the shell that launches the client and reference
them as `"${NIFI_PASSWORD}"` if your client expands variables, or omit them here and let
the server inherit them.
If the configuration is wrong the server still starts and each tool call returns a
structured error naming the problem. It does not exit, because a process that dies
before the transport is established shows up in the client as nothing more than
"connection closed". `nifi doctor` diagnoses the same things from a terminal.
### Write safety
Two layers, because the caller is a model:
1. **`NIFI_ALLOW_WRITE`** — when false (the default), mutating tools are *not registered*.
The model sees 28 read tools and cannot attempt a write at all. Set it to `true` to
register all 59.
2. **`confirm`** — the 9 destructive tools (deletes, `nifi_empty_queue`,
`nifi_terminate_processor`, `nifi_stop_version_control`) take `confirm`. Called without
it, they report exactly what would be affected and change nothing.
Tools also carry MCP annotations (`read_only_hint`, `destructive_hint`) so clients can
apply their own policy.
The CLI is deliberately *not* gated by `NIFI_ALLOW_WRITE` — a human at a terminal is
already the confirmation. Destructive commands prompt instead; `--yes` skips the prompt.
## Development
```bash
pytest # 49 unit tests, no server needed
NIFI_INTEGRATION=1 pytest # + 5 live tests against NIFI_BASE_URL
ruff check src tests && mypy src/nifi_mcp
```
Integration tests build a real flow inside a scratch process group and tear it down
afterwards; they never touch anything outside it.
## Layout
```
src/nifi_mcp/
├── config.py settings from environment and .env
├── errors.py typed exceptions with actionable hints
├── client.py async client: auth, host header, revisions, retries
├── compat.py version detection, 1.x/2.x differences
├── summaries.py trim NiFi entities to the useful fields
├── api/ one module per resource family
├── tools/ MCP tool definitions (read_tools / write_tools)
├── server.py MCP entry point
└── cli.py Typer CLI
```
### Notes on the NiFi API
Three things the client handles so callers don't have to:
- **Revisions.** Every mutating call must echo the revision it read (optimistic locking).
`update_with_revision` fetches, merges, and retries once on a `409`.
- **Async request endpoints.** Queue listings, drop requests, provenance queries, and
parameter updates are submitted, polled, then deleted. Some of these are `POST`s that
only *read*; those bypass the write gate explicitly.
- **Display strings.** NiFi returns `queuedCount` as a formatted string (`"1,234"`). The
summaries expose numeric `count`/`bytes` alongside the `_display` variants.
## Known gaps
- NiFi 2.x auth (OIDC/SAML) — the compat layer detects the version, but 2.x is untested.
- Version-control writes are implemented but unexercised: no registry is configured on
the dev server.
- Cluster management, tenants/policies, and templates are out of scope (SPEC.md §11).
TDQS
Scored across 28 tools
Most tools target distinct NiFi resources and the descriptions explicitly differentiate overlapping process-group views (tree vs contents vs status). However several status/diagnostic tools (flow_status, process_group_status, get_process_group) and provenance/lineage tools share enough conceptual ground that misselection is possible for less careful agents.
All tools use a consistent nifi_ prefix and snake_case. The dominant pattern is verb_noun (get_, list_, search_, find_, query_), with a few noun-only deviations (about, counters, flow_status, flow_tree, system_diagnostics) that are still readable.
28 tools exceed the 25-tool threshold for 'too many' per the rubric, especially given the apparent read-only scope. While they cover many subresources, the set could be consolidated or split into focus modes.
The surface is almost entirely read-only: no create/update/delete/start/stop tools for processors, connections, controller services, or process groups. Descriptions even reference nifi_create_processor and setting processor properties, which are absent, indicating significant gaps for flow management.