How it Works
The 30-second version
Rocannon is a Python program that, at startup, walks the Ansible module catalog, reads each module's schema via ansible-doc -j, and turns every module into a typed Python function. It hands those functions to FastMCP, which exposes them as MCP tools over stdio or HTTP.
| Source | What Rocannon reads | Tool name |
|---|---|---|
| Ansible collection | ansible-doc -j <module> |
ansible.builtin.copy, community.docker.docker_container, … |
No bundled LLM. No opinionated provider matrix. No inventory manager. Rocannon's job is the glue between "what ansible-doc ships" and "what an MCP client can call."
End-to-end: one tool call
Concrete example: the agent calls ansible.builtin.command(target="webhosts", cmd="systemctl restart nginx").
MCP client (Claude Code, Cursor, mcphost)
│ stdio JSON-RPC tools/call
▼
FastMCP server (src/rocannon/server.py)
│ middleware stack:
│ 1. correlation ID assigned
│ 2. structured log emitted
│ 3. audit record opened + response-size limit applied
│ 4. concurrency semaphores acquired (global + per-host)
│ 5. tool handler runs
│ 6. audit record completed (latency, status, redacted args)
▼
Typed tool function (built at startup from ansible-doc schema)
│ subprocess
▼
ansible-runner → ansible-playbook → SSH → target host
The result bubbles back up: ansible-runner parses the JSON event stream, the executor returns a structured dict, middleware records the audit entry, FastMCP serialises to MCP's tool-result format, the client gets structured JSON.
Module reflection
At startup, register_ansible_modules runs ansible-doc -j <names...> in batches of 256 modules (to amortise the 200ms Python startup cost), then for each module:
- Parses parameter names, types, required flags, choices, and descriptions
- Builds a typed Python function with a dynamic
inspect.Signaturematching the module's documented parameters - Adds a
targetparameter (the inventory host or group pattern) - Injects
checkanddiffparameters for modules that declare support in their ansible-doc attributes - Attaches safety hints: read-only for
_info/_factsmodules; destructive + open-world forcommand/shell/raw - Tags the tool by collection and namespace for client-side filtering
from, type) or with reserved slots (target). The registration layer mangles those (e.g. type → param_type) and de-mangles on the way out. The mangled name is what the MCP schema exposes.Middleware stack
The server composes seven middleware layers, innermost to outermost:
| Layer | What it does |
|---|---|
| Audit | Writes a per-call JSON record (tool, target, latency, status, redacted args) to the rocannon.audit logger. Feeds the in-memory run history. |
| Concurrency | Two semaphores: a global cap and a per-host cap. Prevents fan-out floods on both the server and individual targets. |
| Ping | HTTP transport only. Keepalive for long-lived sessions. |
| Structured logging | Optional JSON logging (gated by ROCANNON_LOG_FORMAT=json). |
| Response limit | Caps tool response size before it reaches the MCP client. Ansible command/shell stdout can be enormous; an LLM context blown by a single dump is a real failure mode. |
| Retry | Re-runs on transient transport-level exceptions only. Module-level Ansible failures return a structured dict and are not retried. |
| Error handling | Outermost layer. Catches and formats exceptions consistently so the client always receives a well-formed response. |
OpenTelemetry tracing is optional. If opentelemetry-api is importable, the server emits a tools/call <name> span per call with module name, target, and latency. If it isn't, the import fails silently. No runtime cost when disabled.
Playbook save and replay
A saved session is written as a standard Ansible playbook: a list of plays, one per recorded step, with the play's hosts: set to the step's target. The file runs directly under ansible-playbook with no Rocannon in the loop.
# Rocannon session: restart-stack
# Restart the web tier and verify
- name: ansible.builtin.command on webhosts
hosts: webhosts
gather_facts: false
tasks:
- name: ansible.builtin.command
ansible.builtin.command:
cmd: systemctl restart nginx
- name: ansible.builtin.wait_for on webhosts
hosts: webhosts
gather_facts: false
tasks:
- name: ansible.builtin.wait_for
ansible.builtin.wait_for:
host: 127.0.0.1
port: 80
Two server tools handle recording:
save_playbook(name, description, steps): writes a named playbook from explicit stepscommit_session(name, description, since): materialises the current session's successful calls from the in-memory ring buffer
On the next server start, saved playbooks are parsed back and registered as MCP prompts named playbook_<name>. Hand-edited Ansible playbooks load the same way; any task whose module key is a registered tool becomes a step.
The REPL
The REPL constructs the same FastMCP server as mcp serve and drives it in-process via fastmcp.Client without a JSON-RPC transport. The middleware stack, audit log, and run history all apply the same way.
This means: whatever tools an operator can call from the REPL are exactly the tools an LLM sees through MCP. There is no REPL-only surface.
.ai mode uses LiteLLM, so the backend is operator-chosen via ROCANNON_AI_MODEL (Ollama, OpenAI, Anthropic, watsonx, vLLM). There is no opinionated default.
What is deliberately out of scope
| Not in scope | Why |
|---|---|
| Bundled LLM | Rocannon is an MCP server. Pick your own client. |
| Opinionated model list | LiteLLM handles backend selection; Rocannon stays neutral. |
| Inventory management UI | Ansible inventories are YAML/INI files, same as always. |
| Policy engine | Authorisation is the MCP client's responsibility. |
| Terraform / Helm / Salt | Those tools have dedicated MCP servers and different shapes. Rocannon stays narrow: every Ansible module, nothing else. |
| OpenAPI-to-MCP | FastMCP already does this. Rocannon's contribution is the typed-tools-from-Ansible-catalog path. |
Code map
src/rocannon/
├── cli.py Typer entrypoint + FQCN dispatch
├── config.py Pydantic Config model + YAML profile loader
├── profiles.py Profile discovery, registry, RuntimeContext
├── ansible.py Module reflection + typed tool registration
├── server.py FastMCP server + middleware wiring
├── schema.py ansible-doc parsing, module spec expansion
├── executor.py ansible-runner Python-API wrapper
├── playbook.py Session → Ansible playbook save/load
├── repl.py Operator REPL + optional .ai mode
├── inventory.py ansible-inventory subprocess wrapper
├── history.py In-memory ring buffer (feeds save/replay)
├── correlation.py Per-call correlation IDs
└── redaction.py Secrets redaction in audit records