Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .ansible-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
profile: production

exclude_paths:
- .github/
- tests/integration
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
# Byte-compiled / optimized / DLL files
tests/integration/inventory
tests/integration/integration_config.yml
__pycache__/
*.py[cod]
*$py.class
tests/integration/integration_config.yml

# C extensions
*.so
Expand Down
31 changes: 31 additions & 0 deletions plugins/action/tools_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-

# Copyright (c) 2025 Red Hat, Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from ansible.module_utils.connection import Connection
from ansible.plugins.action import ActionBase


class ActionModule(ActionBase):

def run(self, tmp=None, task_vars=None):
"""Perform the process of the action plugin"""
connection_name = self._play_context.connection.split(".")[-1]

result = super(ActionModule, self).run(task_vars=task_vars)

if connection_name != "mcp":
# It is supported only with mcp connection plugin
result["failed"] = True
result["msg"] = (
"connection type %s is not valid for tools_info module,"
" please use fully qualified name of mcp connection type"
% self._play_context.connection
)
return result

conn = Connection(self._connection.socket_path)
tools = conn.list_tools().get("tools", [])

return dict(changed=False, tools=tools)
44 changes: 44 additions & 0 deletions plugins/modules/tools_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-

# Copyright (c) 2025 Red Hat, Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)


DOCUMENTATION = """
module: tools_info
author: Aubin Bikouo (@abikouo)
short_description: Retrieve a list of supported tools from an MCP server
description:
- This module is used to discover available tools from an MCP server.
- The module sends a tools/list request to the server.
version_added: 1.0.0
options: {}
"""

EXAMPLES = """
- name: Retrieve list of supported tools from an MCP server.
ansible.mcp.tools_info:
"""

RETURN = """
tools:
description: List of supported tools.
returned: success
type: list
elements: dict
sample: {
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or zip code"
}
},
"required": ["location"]
}
}
"""
6 changes: 5 additions & 1 deletion plugins/plugin_utils/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,14 @@ def request(self, data: dict) -> dict:
try:
# Send request to the server
self._stdin_write(data)
except Exception as e:
raise AnsibleConnectionFailure(f"Error sending request to MCP server: {str(e)}")

try:
# Read response
return self._stdout_read()
except Exception as e:
raise AnsibleConnectionFailure(f"Error sending request to MCP server: {str(e)}")
raise AnsibleConnectionFailure(f"Error reading server response: {str(e)}")

def close(self) -> None:
"""Close the server connection."""
Expand Down
1 change: 1 addition & 0 deletions tests/integration/targets/prepare_github_mcp_server/alias
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
disabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
go_version: "1.25.4"
go_arch_translation:
amd64: amd64
x86_64: amd64
i386: 386
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
- name: Clean temporary directory
ansible.builtin.file:
state: absent
path: "{{ gh_mcp_server_tmp_dir }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
- name: Build Github MCP server
ansible.builtin.include_tasks: setup.yml
when: not (cleanup_tmp_dir | default('false') | bool)

- name: Clean temporary files
ansible.builtin.include_tasks: cleanup.yml
when: cleanup_tmp_dir | default('false') | bool
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
- name: Check if the go command exists
ansible.builtin.command: which go
register: check_go
ignore_errors: true

- name: Create temporary directory
ansible.builtin.tempfile:
state: directory
suffix: .github-mcp-server
register: tmp_dir

- name: Set temporary directory and Github MCP server executable path
ansible.builtin.set_fact:
gh_mcp_server_tmp_dir: "{{ tmp_dir.path }}"
gh_mcp_server_path: "{{ tmp_dir.path }}/github-mcp-server"

# install go
- name: Install Go executable
when: check_go.rc != 0
vars:
go_version_label: "{{ go_version }}.{{ ansible_system | lower }}-{{ go_arch_translation[ansible_architecture] }}"
block:
- name: Get archive checksum
uri:
url: "https://dl.google.com/go/go{{ go_version_label }}.tar.gz.sha256"
return_content: true
register: go_checksum

- name: Download go archive
get_url:
url: "https://dl.google.com/go/go{{ go_version_label }}.tar.gz"
dest: "{{ gh_mcp_server_tmp_dir }}/go{{ go_version_label }}.tar.gz"
checksum: "sha256:{{ go_checksum.content }}"

- name: Install go
unarchive:
src: "{{ gh_mcp_server_tmp_dir }}/go{{ go_version_label }}.tar.gz"
dest: "{{ gh_mcp_server_tmp_dir }}"
remote_src: true

- ansible.builtin.set_fact:
go_exec_path: "{{ gh_mcp_server_tmp_dir }}/go/bin/go"

- name: Clone repository for Github MCP server
ansible.builtin.command:
cmd: git clone https://github.com/github/github-mcp-server src
chdir: "{{ gh_mcp_server_tmp_dir }}"

- name: Build the Github MCP server executable
ansible.builtin.command:
cmd: "{{ go_exec_path | default('go') }} build -o {{ gh_mcp_server_path }}"
chdir: "{{ gh_mcp_server_tmp_dir }}/src/cmd/github-mcp-server"
3 changes: 3 additions & 0 deletions tests/integration/targets/tools_info/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
inventory.yml
vars.yml
manifest.json
10 changes: 10 additions & 0 deletions tests/integration/targets/tools_info/inventory.yml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
all:
children:
mcp_servers:
hosts:
github_server:
ansible_connection: ansible.mcp.mcp
ansible_mcp_server_name: github
ansible_mcp_server_args: []
ansible_mcp_server_env: {"GITHUB_PERSONAL_ACCESS_TOKEN": "{{ github_mcp_pat }}"}
ansible_mcp_manifest_path: {{ playbook_dir }}/manifest.json
8 changes: 8 additions & 0 deletions tests/integration/targets/tools_info/manifest.json.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"github": {
"type": "stdio",
"command": "{{ gh_mcp_server_path }}",
"args": ["stdio"],
"description": "GitHub MCP Server - Access GitHub repositories, issues, and pull requests"
}
}
22 changes: 22 additions & 0 deletions tests/integration/targets/tools_info/runme.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

set -eux

function cleanup() {
ansible-playbook teardown.yml -e "@./vars.yml" "$@"
exit 1
}

ANSIBLE_ROLES_PATH="../"
export ANSIBLE_ROLES_PATH

trap 'cleanup "${@}"' ERR

# Configure test environment
ansible-playbook setup.yml "$@"

# Run tests
ansible-playbook test.yml -i inventory.yml "$@"

# cleanup environment
ansible-playbook teardown.yml -e "@./vars.yml" "$@"
23 changes: 23 additions & 0 deletions tests/integration/targets/tools_info/setup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
- hosts: localhost
gather_facts: true

vars_files:
- vars/main.yml

roles:
- role: prepare_github_mcp_server

tasks:
- name: Remove file from previous tests
ansible.builtin.file:
state: absent
dest: "{{ item | replace('.j2', '') }}"
loop: "{{ template_files }}"
ignore_errors: true

- name: Generate required files from template
ansible.builtin.template:
src: "{{ item }}"
dest: "{{ item | replace('.j2', '') }}"
loop: "{{ template_files }}"
18 changes: 18 additions & 0 deletions tests/integration/targets/tools_info/teardown.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
- hosts: localhost
gather_facts: false

vars_files:
- vars/main.yml

roles:
- role: prepare_github_mcp_server
cleanup_tmp_dir: true

tasks:
- name: Remove temporary files
ansible.builtin.file:
state: absent
path: "{{ item | replace('.j2', '') }}"
loop: "{{ template_files }}"
ignore_errors: true
16 changes: 16 additions & 0 deletions tests/integration/targets/tools_info/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
- name: Run ansible.mcp.tools_info tests
hosts: mcp_servers
gather_facts: false

tasks:
- name: List tools from MCP servers
ansible.mcp.tools_info:
register: result

- name: Validate that the module response is as expected
ansible.builtin.assert:
that:
- '"tools" in result'
- result.tools | length > 0
- result.tools | selectattr('name', 'equalto', 'pull_request_read') | list | length == 1
2 changes: 2 additions & 0 deletions tests/integration/targets/tools_info/vars.yml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
gh_mcp_server_tmp_dir: {{ gh_mcp_server_tmp_dir }}
5 changes: 5 additions & 0 deletions tests/integration/targets/tools_info/vars/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
template_files:
- inventory.yml.j2
- vars.yml.j2
- manifest.json.j2
Loading