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.

SourceWhat Rocannon readsTool 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.Signature matching the module's documented parameters
  • Adds a target parameter (the inventory host or group pattern)
  • Injects check and diff parameters for modules that declare support in their ansible-doc attributes
  • Attaches safety hints: read-only for _info/_facts modules; destructive + open-world for command/shell/raw
  • Tags the tool by collection and namespace for client-side filtering
Parameter name collisions. Some module parameters collide with Python keywords (from, type) or with reserved slots (target). The registration layer mangles those (e.g. typeparam_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:

LayerWhat it does
AuditWrites a per-call JSON record (tool, target, latency, status, redacted args) to the rocannon.audit logger. Feeds the in-memory run history.
ConcurrencyTwo semaphores: a global cap and a per-host cap. Prevents fan-out floods on both the server and individual targets.
PingHTTP transport only. Keepalive for long-lived sessions.
Structured loggingOptional JSON logging (gated by ROCANNON_LOG_FORMAT=json).
Response limitCaps 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.
RetryRe-runs on transient transport-level exceptions only. Module-level Ansible failures return a structured dict and are not retried.
Error handlingOutermost 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 steps
  • commit_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 scopeWhy
Bundled LLMRocannon is an MCP server. Pick your own client.
Opinionated model listLiteLLM handles backend selection; Rocannon stays neutral.
Inventory management UIAnsible inventories are YAML/INI files, same as always.
Policy engineAuthorisation is the MCP client's responsibility.
Terraform / Helm / SaltThose tools have dedicated MCP servers and different shapes. Rocannon stays narrow: every Ansible module, nothing else.
OpenAPI-to-MCPFastMCP 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