ansible-mcp
A minimal Ansible controller with an MCP interface, for AI agents — and a REST API for the humans who work alongside them.
Give it a playbook and an inventory; it runs them and keeps the history, the logs and the artifacts of every run. One process, one SQLite file, one container. No database server, no message broker, no worker fleet.
Русская версия: README.ru.md
Why
Running Ansible against a handful of hosts is easy. Running it programmatically is not, and the moment you want that the options get heavy: AWX and Ansible Automation Platform need PostgreSQL, Redis, RabbitMQ, Receptor and in practice Kubernetes. For fifty hosts that control plane costs more than the problem it solves.
And an AI agent driving infrastructure needs a machine-facing interface. A REST API designed for a human clicking through a UI means deep object graphs and a dozen calls to launch one job; an agent wants a handful of well-named verbs.
That is what this is: a dumb executor with an agent-shaped interface. It does not write playbooks and does not decide what to run — the calling agent does both. See docs/concept.md.
Quick start
The three-line version is below; the user guide is the same path with every step explained, plus what to do when it does not work.
The fastest path is stdio, where the client launches the process. The package
is ansible-mcp-controller on PyPI, and ansible-core arrives with it:
uv tool install ansible-mcp-controller
ansible-mcp # speaks MCP on stdin/stdout
In Claude Code, or any MCP client:
{
"mcpServers": {
"ansible-mcp": {
"command": "uvx",
"args": ["--from", "ansible-mcp-controller", "ansible-mcp"],
"env": { "ANSIBLE_MCP_DATA_DIR": "/tmp/ansible-mcp" }
}
}
}
As a container, serving over HTTP:
docker build -f Containerfile -t ansible-mcp .
mkdir -p data && sudo chown 1000:1000 data # the image runs as uid 1000
docker run -p 8080:8080 -v ./data:/data \
-e ANSIBLE_MCP_API_KEY="$(openssl rand -hex 32)" ansible-mcp
The key is not optional there: the container listens beyond loopback, and the server refuses to serve an unauthenticated endpoint that executes playbooks. For a host installation, see packaging.
The tools
| Tool | What it does |
|---|---|
run_playbook |
Starts a run and returns a task id; the run continues in the background. check=true is a dry run: it contacts the hosts and changes nothing |
syntax_check_playbook |
Parses a playbook and says whether it is valid, contacting nothing |
get_task_status |
pending, running, success, failed or cancelled, with timestamps and exit code |
get_task_logs |
What Ansible printed, the tail by default, secrets removed; after_line pages forward without re-reading |
cancel_task |
Stops a queued or running playbook; needs confirm=true |
list_tasks |
Recent runs, newest first, filterable by status |
save_playbook / list_playbooks / get_playbook / delete_playbook |
Playbooks by name, so a run need not carry its text |
add_provider / list_providers / get_inventory / delete_provider |
Where an inventory comes from |
A provider is a small plugin that produces an inventory: static ships built in,
others register through the ansible_mcp.providers entry point group. Cloud
providers are the next ones planned.
The same thing with curl
When it serves HTTP, every one of those operations is also at /api/v1, on the
same port as /mcp and behind the same token:
curl -si -X POST http://127.0.0.1:8080/api/v1/runs \
-H "Authorization: Bearer $ANSIBLE_MCP_API_KEY" -H 'Content-Type: application/json' \
-d '{"playbook_name":"site","provider":"lab"}'
HTTP/1.1 202 Accepted
location: /api/v1/runs/8aeac1aeb682464c81b55b0a338e45ec
Then GET /api/v1/runs/{id} follows it and .../logs reads what it printed.
MCP stays the primary interface and REST is the second door onto the same
operations (ADR-0002, ADR-0015); the schema is at /api/v1/openapi.json, and
the user guide has
the whole loop.
What it deliberately does not do
- No RBAC, no multi-tenancy. One key gates the instance.
- No horizontal scaling. One node, concurrency bounded by a semaphore.
- No web UI.
- No playbook generation. That is the calling agent's job, and the LLM layer the prototype had was removed rather than ported.
Each of these is a decision with its reasoning and its rejected alternatives in the decision log. Outgrowing them is the signal to move to a full platform, and that boundary is what keeps this project small.
Security: the API key is shell access
Read this before exposing the endpoint to anything.
Whoever holds the API key can run arbitrary code on the controller host, as
the user the service runs as. Not through a bug — through the feature. A playbook
with hosts: localhost, or an inventory line carrying
ansible_connection=local, executes on the controller itself, and the controller
is built to run whatever playbook it is handed: it does not inspect, judge or
restrict them (ADR-0004).
So treat the key exactly as you would an SSH login to that host. One key covers the whole instance, there is no per-caller identity, and the audit log records what was done rather than by whom.
What reduces the blast radius today:
- Run it as an unprivileged user. The container image and the Debian package both do; a manual install should too.
- Give that user only the SSH credentials it needs, and no sudo on the controller.
- Keep the endpoint off the network unless it has to be on it. The stdio transport has no port at all, and a loopback bind needs no key because reaching it already means being on the machine.
What actually fixes it is running each playbook inside a container rather than on the host, and that now exists. It is off by default, because it needs a container runtime and this project promises none:
ANSIBLE_MCP_ISOLATION=true
ANSIBLE_MCP_CONTAINER_RUNTIME=podman
ANSIBLE_MCP_EXECUTION_IMAGE=my-execution-environment:latest
With it on, a playbook that says hosts: localhost runs inside a container and
the controller's filesystem is not there to touch. The operator turns it on for
the whole installation and a run may pick its image but cannot decline the
sandbox, which is what stops a caller stepping out of it
(ADR-0008,
ADR-0016). The
packaging notes have the
full setup; it is for the Debian package, not the container image, which already
isolates the whole service from its host.
What it does not change is the fleet: whoever holds the key can still run any playbook against every host the controller's credentials reach. Isolation bounds the damage on this machine, not on the machines it manages.
Working on it
poetry install
poetry run task tests # 250 tests
poetry run task lint # ruff + ruff format + mypy
make integration # playbooks against real hosts over SSH
poetry run task eval # can a local model actually drive this?
The eval harness is not decoration: it reads the tool schemas from the running server and measures whether a model picks the right tool. Several tool descriptions and the whole argument-coercion layer exist because it failed first. See evals/README.md.
Contributors, human or otherwise, start at AGENTS.md.
Documentation
| User guide | Install, connect, and everything it can do |
| Concept | The problem, the consumers, the trade-offs |
| Architecture | What the code does now |
| Configuration | Every setting |
| Reaching hosts | Keys, passwords and host keys, per install method |
| Decision log | Why the boundaries are where they are |
| Packaging | Container and Debian package |
| Evals | Measuring whether an agent can drive it |
| Integration | Connecting a client, and a ready-made skill |
| Roadmap | What is done and what is next (in Russian) |
Status
The core works: runs, cancellation, timeouts, recovery after a crash, playbook storage, providers, redaction, an audit trail, a checked token and both surfaces — MCP and REST. Not yet: cloud providers — see the roadmap.
License
Apache-2.0 — see LICENSE.
Metadata
Release files for ansible-mcp-controller 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ansible_mcp_controller-0.1.0.tar.gz | 64.1 kB | With provenance | Details for ansible_mcp_controller-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ansible_mcp_controller-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for ansible_mcp_controller-0.1.0-py3-none-any.whl |
Total release size: 147.1 kB
Release files / ansible_mcp_controller-0.1.0.tar.gz
| Download URL | ansible_mcp_controller-0.1.0.tar.gz |
|---|---|
| Size | 64.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7c611f69f6ce1a5fb237bafc95e4fde885d1b0f96ca3b9f117fc3ed19d37afe4
|
|
BLAKE2b-256 checksum How to use checksums |
1f30d7ee923cf313092ad6be575403c29d38f70957d30be7585eae881baa214a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / ansible_mcp_controller-0.1.0-py3-none-any.whl
| Download URL | ansible_mcp_controller-0.1.0-py3-none-any.whl |
|---|---|
| Size | 83.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
46efa2c77608e9632bd55ceebc252810e58b65e23e752ae1ca69b5edd027f7bb
|
|
BLAKE2b-256 checksum How to use checksums |
98a34c31932f68c3a46a4a37316bc7de248a5ac907372886bfd72fc7634c4f10
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logSecurity for ansible-mcp-controller 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ansible_mcp_controller-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.