--- # Deploy a custom Ansible MCP server on Watchtower. # # Usage: # cd /home/chester/homelab/ansible # ansible-playbook -i inventory/hosts.ini playbooks/ai/deploy_ansible_mcp_watchtower.yml # # Validate only: # ansible-playbook -i inventory/hosts.ini playbooks/ai/deploy_ansible_mcp_watchtower.yml --check - name: Deploy Ansible MCP server on Watchtower hosts: watchtower become: true gather_facts: true vars: mcp_service_name: ansible-mcp mcp_install_dir: /opt/ansible-mcp mcp_state_dir: /var/lib/ansible-mcp mcp_user: chester mcp_group: chester mcp_transport: streamable-http mcp_host: 0.0.0.0 mcp_port: 8449 mcp_repo_root: /home/chester/homelab/ansible mcp_inventory: inventory/hosts.ini mcp_allowed_playbook_dirs: playbooks mcp_allowed_playbooks: "" mcp_api_token: "{{ lookup('env', 'ANSIBLE_MCP_API_TOKEN') | default('', true) }}" mcp_max_extra_vars_bytes: 16384 mcp_blocked_extra_vars_keys: "ansible_password,ansible_become_password,vault_password" # Full-write mode is enabled by default here to match requested behavior. # Keep confirm enforcement enabled in server guardrails. mcp_allow_write: true mcp_require_confirm_for_write: true mcp_default_timeout: 900 mcp_max_timeout: 3600 mcp_python_packages: - ansible-core>=2.16,<2.19 - mcp>=1.0.0 tasks: - name: Assert API token is configured for HTTP transport ansible.builtin.assert: that: - mcp_transport == "stdio" or (mcp_api_token | length) > 0 fail_msg: >- HTTP transport requires ANSIBLE_MCP_API_TOKEN to be set in the control shell environment before running this playbook. success_msg: "Transport/auth configuration validated." - name: Assert service account exists ansible.builtin.getent: database: passwd key: "{{ mcp_user }}" - name: Ensure installation and state directories exist ansible.builtin.file: path: "{{ item.path }}" state: directory owner: "{{ item.owner }}" group: "{{ item.group }}" mode: "{{ item.mode }}" loop: - { path: "{{ mcp_install_dir }}", owner: "{{ mcp_user }}", group: "{{ mcp_group }}", mode: "0755" } - { path: "{{ mcp_state_dir }}", owner: "{{ mcp_user }}", group: "{{ mcp_group }}", mode: "0750" } - name: Copy MCP server script ansible.builtin.copy: src: ../../scripts/ansible_mcp_server.py dest: "{{ mcp_install_dir }}/ansible_mcp_server.py" owner: "{{ mcp_user }}" group: "{{ mcp_group }}" mode: "0755" notify: Restart ansible mcp service - name: Ensure Python venv exists ansible.builtin.command: "python3 -m venv {{ mcp_install_dir }}/.venv" args: creates: "{{ mcp_install_dir }}/.venv/bin/python" changed_when: false - name: Install MCP server dependencies in venv ansible.builtin.pip: name: "{{ mcp_python_packages }}" virtualenv: "{{ mcp_install_dir }}/.venv" state: present notify: Restart ansible mcp service - name: Install systemd unit for ansible mcp service ansible.builtin.copy: dest: "/etc/systemd/system/{{ mcp_service_name }}.service" owner: root group: root mode: "0644" content: | [Unit] Description=Ansible MCP Server Wants=network-online.target After=network-online.target [Service] Type=simple User={{ mcp_user }} Group={{ mcp_group }} WorkingDirectory={{ mcp_repo_root }} Environment=ANSIBLE_MCP_REPO_ROOT={{ mcp_repo_root }} Environment=ANSIBLE_MCP_INVENTORY={{ mcp_inventory }} Environment=ANSIBLE_MCP_ALLOWED_PLAYBOOK_DIRS={{ mcp_allowed_playbook_dirs }} Environment=ANSIBLE_MCP_ALLOWED_PLAYBOOKS={{ mcp_allowed_playbooks }} Environment=ANSIBLE_MCP_API_TOKEN={{ mcp_api_token }} Environment=ANSIBLE_MCP_ALLOW_WRITE={{ mcp_allow_write | ternary('true', 'false') }} Environment=ANSIBLE_MCP_REQUIRE_CONFIRM={{ mcp_require_confirm_for_write | ternary('true', 'false') }} Environment=ANSIBLE_MCP_DEFAULT_TIMEOUT={{ mcp_default_timeout }} Environment=ANSIBLE_MCP_MAX_TIMEOUT={{ mcp_max_timeout }} Environment=ANSIBLE_MCP_MAX_EXTRA_VARS_BYTES={{ mcp_max_extra_vars_bytes }} Environment=ANSIBLE_MCP_BLOCKED_EXTRA_VARS_KEYS={{ mcp_blocked_extra_vars_keys }} Environment=ANSIBLE_MCP_STATE_DIR={{ mcp_state_dir }} Environment=ANSIBLE_MCP_TRANSPORT={{ mcp_transport }} Environment=ANSIBLE_MCP_HOST={{ mcp_host }} Environment=ANSIBLE_MCP_PORT={{ mcp_port }} ExecStart={{ mcp_install_dir }}/.venv/bin/python {{ mcp_install_dir }}/ansible_mcp_server.py --transport {{ mcp_transport }} --host {{ mcp_host }} --port {{ mcp_port }} Restart=on-failure RestartSec=3 [Install] WantedBy=multi-user.target notify: - Reload systemd - Restart ansible mcp service - name: Ensure ansible mcp service is enabled and running ansible.builtin.systemd: name: "{{ mcp_service_name }}" enabled: true state: started - name: Verify MCP health endpoint ansible.builtin.uri: url: "http://127.0.0.1:{{ mcp_port }}" method: GET return_content: true status_code: 200 changed_when: false register: _mcp_http_probe failed_when: false - name: Show deployment summary ansible.builtin.debug: msg: - "Ansible MCP deployed to watchtower" - "Service: {{ mcp_service_name }}" - "Transport: {{ mcp_transport }}" - "Endpoint: {{ mcp_host }}:{{ mcp_port }}" - "Repo root: {{ mcp_repo_root }}" - "Allow write: {{ mcp_allow_write }}" - "Auth enabled: {{ (mcp_api_token | length) > 0 }}" - "Require confirm for write: {{ mcp_require_confirm_for_write }}" - "Explicit playbook allowlist set: {{ (mcp_allowed_playbooks | length) > 0 }}" - "HTTP probe status: {{ _mcp_http_probe.status | default('n/a') }}" handlers: - name: Reload systemd ansible.builtin.systemd: daemon_reload: true - name: Restart ansible mcp service ansible.builtin.systemd: name: "{{ mcp_service_name }}" state: restarted