Skip to main content
Glama
README.md
# 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** |
| ![Why: god-mode control node vs enrolled bastion](docs/images/campaign-why.png) | ![Agent ritual: search → schema → check → execute](docs/images/campaign-agent-loop.png) |
| **Hub / spoke fabric** | **Operators** |
| ![SSH hub/spoke topology and enrollment](docs/images/campaign-hub-spoke.png) | ![Operator TUI, hub MCP tools, lab demo](docs/images/campaign-operator.png) |

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.

![Without a fabric vs ansible-flow-mcp controls](docs/images/campaign-why.png)

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.

![Agent ritual and MCP tools](docs/images/campaign-agent-loop.png)

| 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.

![SSH hub/spoke topology and enrollment](docs/images/campaign-hub-spoke.png)

| | 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.

![Operator TUI, hub MCP tools, lab demo](docs/images/campaign-operator.png)

```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

A3.8/5.0

Scored across 5 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessResponsive