ansible-flow-mcp
# ansible-flow-mcp
**Give agents Ansible. Not the keys.**
This branch (`CORE`) is documentation only: why it exists, campaign screenshots, and the marketing site. The runnable MCP and hub live on [`DEVELOPMENT`](https://github.com/real-limitless/ansible-flow-mcp/tree/DEVELOPMENT). `main` stays a product alias.
| | |
|---|---|
| **License** | [Apache-2.0](LICENSE) |
| **Install / code** | [`DEVELOPMENT`](https://github.com/real-limitless/ansible-flow-mcp/tree/DEVELOPMENT) |
| **Marketing site** | [site/](site/) |
| **Hub ops** | [docs/HUB.md](docs/HUB.md) |
| **Branches** | [BRANCHES.md](BRANCHES.md) |
| **Species** | [SPECIES.md](SPECIES.md) |
| **Voice** | [VOICE.md](VOICE.md) |
## Visual tour
| | |
| :---: | :---: |
| **Why it exists** | **Agent loop** |
|  |  |
| **Hub / spoke fabric** | **Operators** |
|  |  |
Screenshots live in [`docs/images/campaign-*.png`](docs/images/). Re-shoot from [`docs/campaign/`](docs/campaign/) with `./capture.sh`.
---
## Why this exists
Agents on a god-mode control node invent inventory, reach for `shell`, and treat every worker as an entrypoint. That is not a security model.

Typical cases:
- You want Cursor / Claude / OpenCode to run Ansible **like an operator**, not freestyle root across the fleet
- Multi-host must mean **bastion ops you already understand** (SSH, inventory, enrollment): not a mesh hop plane
- Prompt injection will still *ask* for bad ops: **policy and topology must refuse**
---
## Two tracks
### 1. Agent loop: search → schema → check → execute
Curated module gallery. Slim argSpec before any run. Check mode default. Free-form modules denied. Playbooks path-jailed.

| Tool | Purpose |
| --- | --- |
| `search_modules` | Gallery search |
| `get_module_schema` | Slim argSpec for FQCN |
| `run_module` | Ad-hoc Ansible (`check_mode` default **true**) |
| `run_playbook` | `ansible-playbook` on a path-jailed `.yml` |
| `list_collections` | Collections in gallery |
**Ritual (modules):** `search_modules` → `get_module_schema` → `run_module(..., check_mode=true)` → apply only if appropriate.
**Ritual (playbooks):** confirm path under allowlisted roots → check → apply.
### 2. Hub/spoke: nothing is a target until enrolled
Secure multi-host mode: **agent attaches to the hub only**. Hub reaches spokes over **SSH only**. Spokes execute **localhost** and cannot lateral-move via this fabric.

| | Full mesh (withdrawn) | **Hub/spoke (shipped)** |
| --- | --- | --- |
| Worker compromise | Could MCP-hop fleet-wide | **No lateral MCP** |
| Inventory | Gossip / replicas | **Hub is source of truth** |
| Agent attach | Any node | **Hub only** |
| Ops model | Mesh OS | **Classic Ansible bastion** |
**Enrollment:** `hub init` → `issue-token` (TTL, one-time jti) → `spoke join` (token + SSH identity) → hub inventory. Runtime: ForceCommand MCP session: **no shell** on the hub→spoke path.
**Hub tools:** `list_nodes` / `hub_status`, `issue_token`, `revoke_node`, groups (`create_group`, `set_group_members`, …), `spoke_call`, plus catalog `run_*` against **enrolled hosts or groups only**. Client-supplied `-i` is rejected in hub mode.
Deep ops: **[docs/HUB.md](docs/HUB.md)**.
---
## Operators
Day-2 surface matches the agent: enroll, group, hand the hub to OpenCode.

```bash
ansible-flow-mcp hub init --name ctrl-01
ansible-flow-mcp hub issue-token --name web-03 --ttl 15m
ansible-flow-mcp spoke join --token "$TOKEN" --hub user@hub:22 --public-addr web-03.example.com
ansible-flow-mcp hub session # MCP stdio for the agent
ansible-flow-mcp tui # servers · groups · invite · OpenCode
ansible-flow-mcp hub spoke-call --node web-03 --tool list_collections
```
### Lab one-shot
```bash
cd lab && ./scripts/demo.sh
# then: ./scripts/tui.sh | ./scripts/opencode-host.sh
```
See [lab/README.md](lab/README.md).
---
## Quick start
**Full guide (all paths, verify, troubleshooting):** **[docs/QUICKSTART.md](docs/QUICKSTART.md)**
| Path | Guide section |
| --- | --- |
| Local MCP + Cursor/Claude | [Path A](docs/QUICKSTART.md#path-a--local-mcp-single-node) |
| Compose lab (hub + 3 spokes) | [Path B](docs/QUICKSTART.md#path-b--compose-lab-full-hubspoke) |
| Bare-metal hub/spoke | [Path C](docs/QUICKSTART.md#path-c--bare-metal-hubspoke) |
### Single-node / dev (short form)
Requirements: Python ≥ 3.11 · collection **`ansible.posix`** (JSON callback) · collections you will run.
`pip install` pulls **`ansible-core`** (provides `ansible` / `ansible-playbook`).
```bash
cd ansible-flow-mcp
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
ansible-galaxy collection install ansible.posix
pytest -q
ansible-flow-mcp
```
Or install via pypi.org
```bash
pip install ansible-flow-mcp
ansible-galaxy collection install ansible.posix
ansible-flow-mcp
```
Editor snippets: `examples/cursor-mcp.json`, `examples/claude-desktop.json`.
Hub + OpenCode: `examples/opencode-hub.jsonc` · `ansible-flow-mcp hub write-opencode-config`.
```json
{
"mcpServers": {
"ansible-flow": {
"command": "/path/to/ansible-flow-mcp/.venv/bin/ansible-flow-mcp"
}
}
}
```
---
## Security (honest)
| Control | Behavior |
| --- | --- |
| Collection allowlist | Only configured collections |
| Module deny list | `command` / `shell` / `raw` / `script` denied by default |
| Check mode | Default **true** on `run_module` |
| Playbook jail | Allowlisted roots · size limit · `.yml`/`.yaml` only |
| No shell interpolation | argv-only subprocess |
| Hub inventory | Enrolled hosts only · no client `-i` · host key checking on |
| Spoke path | SSH ForceCommand · localhost exec · no peer fabric |
**Residual:** hub compromise = fleet (same class as any Ansible control node). Harden the bastion: see [docs/SECURITY.md](docs/SECURITY.md) and [docs/HUB.md](docs/HUB.md).
---
## Catalog & OpenFlow
- `catalog/collections-allowlist.yml`: allowlist + deny free-form modules
- `catalog/gallery.json` + `catalog/schemas/`: searchable gallery
- Regenerate: `python scripts/generate_catalog.py`
- Galaxy factory TUI: [scripts/factory/README.md](scripts/factory/README.md)
OpenFlow reads this gallery for its Ansible canvas
([plan](https://github.com/real-limitless/ansible-flow-mcp/issues/1) · [umbrella](https://github.com/real-limitless/OpenFlow/issues/56)).
| OpenFlow | This MCP server |
| --- | --- |
| Palette Ansible gallery | `search_modules` |
| Form \| JSON module options | `get_module_schema` + `run_module` |
| Playbook resource | `run_playbook` |
| Control-node SSH / become | Inventory + Ansible config · hub→spoke SSH in hub mode |
---
## Env (common)
| Variable | Meaning |
| --- | --- |
| `ANSIBLE_FLOW_CATALOG_DIR` | Override catalog path |
| `ANSIBLE_FLOW_COLLECTIONS` | Comma-separated allowlist override |
| `ANSIBLE_FLOW_INVENTORY` | Default `-i` (non-hub / dev) |
| `ANSIBLE_FLOW_TIMEOUT` | Seconds (default 120 module / 300 playbook) |
| `ANSIBLE_FLOW_PLAYBOOK_ROOTS` | Extra playbook roots (`:`-separated) |
| `ANSIBLE_FLOW_HUB_DIR` | Hub state (default: `/var/lib/…` if writable, else `~/.local/share/ansible-flow/hub`) |
| `ANSIBLE_FLOW_SPOKE_DIR` | Spoke state (same pattern under `…/spoke`) |
---
## License & publish
Apache-2.0
```bash
pip install build twine && python -m build
# twine upload dist/*
```
```bash
uvx --from ansible-flow-mcp ansible-flow-mcp
```
## Family
Written standard: private TheFLOW.
- OpenFlow: reads this Ansible gallery
- mcp-flow: MCP gateway
- skill-flow: Agent Skills
- wiki-flow, roster-flow, CleanFlow, ProjectEverflow: siblings
TDQS
Scored across 5 tools
Each tool targets a distinct action: searching the module catalog, retrieving module schemas, running a module, running a playbook, and listing collections. There is no functional overlap between any of the tools, making selection unambiguous.
All tool names follow a consistent verb_noun pattern using snake_case: search_modules, get_module_schema, run_module, run_playbook, list_collections. The naming is predictable and uniform across the entire set.
With 5 tools, the set is well-scoped for an Ansible flow server. It covers the essential operations of discovering, inspecting, and executing Ansible content without unnecessary bloat or heaviness.
The toolset covers the core lifecycle: discover (search/list), inspect (get schema), and execute (run module/playbook). A minor gap is the lack of a direct way to list playbooks or manage inventory, but these are not essential for the stated flow-centric purpose.