From 6881c0bcda519837474723c3b1414a6da6edbacc Mon Sep 17 00:00:00 2001 From: kent paik Date: Tue, 31 Mar 2026 15:01:56 +0900 Subject: [PATCH 1/4] feat: add get_instances_by_security_group prompt Add a prompt that guides LLM to find compute instances associated with a specific security group using existing tools. Closes #101 --- src/openstack_mcp_server/prompts/__init__.py | 26 ++++++++++++++++++ src/openstack_mcp_server/server.py | 3 +- tests/prompts/test_prompts.py | 29 ++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/prompts/test_prompts.py diff --git a/src/openstack_mcp_server/prompts/__init__.py b/src/openstack_mcp_server/prompts/__init__.py index e69de29..0f12d64 100644 --- a/src/openstack_mcp_server/prompts/__init__.py +++ b/src/openstack_mcp_server/prompts/__init__.py @@ -0,0 +1,26 @@ +from fastmcp import FastMCP + + +def register_prompt(mcp: FastMCP): + """ + Register Openstack MCP prompts. + """ + + @mcp.prompt() + def get_instances_by_security_group(security_group_name: str) -> str: + """ + Get instances associated with a specific security group. + + :param security_group_name: The name of the security group to filter instances by. + """ + return ( + f"Find all compute instances that have the security group " + f"'{security_group_name}' attached.\n\n" + f"Steps:\n" + f"1. Call get_servers to list all servers.\n" + f"2. Check each server's security_groups field.\n" + f"3. Return only the servers where security_groups contains " + f"an entry with name '{security_group_name}'.\n" + f"4. For each matching server, show the server name, ID, " + f"status, and the full list of its security groups." + ) diff --git a/src/openstack_mcp_server/server.py b/src/openstack_mcp_server/server.py index f71e788..a8be598 100644 --- a/src/openstack_mcp_server/server.py +++ b/src/openstack_mcp_server/server.py @@ -2,6 +2,7 @@ from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware from fastmcp.server.middleware.logging import LoggingMiddleware +from openstack_mcp_server.prompts import register_prompt from openstack_mcp_server.tools import register_tool @@ -13,7 +14,7 @@ def serve(transport: str, **kwargs): register_tool(mcp) # resister_resources(mcp) - # register_prompt(mcp) + register_prompt(mcp) # Add middlewares mcp.add_middleware(ErrorHandlingMiddleware()) diff --git a/tests/prompts/test_prompts.py b/tests/prompts/test_prompts.py new file mode 100644 index 0000000..63630f2 --- /dev/null +++ b/tests/prompts/test_prompts.py @@ -0,0 +1,29 @@ +from openstack_mcp_server.prompts import register_prompt + + +def test_get_instances_by_security_group_prompt_registered(): + """Test that the prompt is registered with the MCP instance.""" + from unittest.mock import MagicMock + + mcp = MagicMock() + register_prompt(mcp) + mcp.prompt.assert_called() + + +def test_get_instances_by_security_group_prompt_content(): + """Test that the prompt returns expected content.""" + from fastmcp import FastMCP + + mcp = FastMCP("test") + register_prompt(mcp) + + prompts = mcp._prompt_manager._prompts + assert "get_instances_by_security_group" in prompts + + prompt_obj = prompts["get_instances_by_security_group"] + assert prompt_obj.fn is not None + + result = prompt_obj.fn(security_group_name="my-sg") + assert "my-sg" in result + assert "get_servers" in result + assert "security_groups" in result From b5d7f915e9cd154c5457786cb706e52cb47c6707 Mon Sep 17 00:00:00 2001 From: kent paik Date: Thu, 2 Apr 2026 11:35:11 +0900 Subject: [PATCH 2/4] refactor: rename prompt from get_instances to get_servers Align naming with compute_tools convention (server over instance) as suggested in code review. --- src/openstack_mcp_server/prompts/__init__.py | 8 ++++---- tests/prompts/test_prompts.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/openstack_mcp_server/prompts/__init__.py b/src/openstack_mcp_server/prompts/__init__.py index 0f12d64..e27b30e 100644 --- a/src/openstack_mcp_server/prompts/__init__.py +++ b/src/openstack_mcp_server/prompts/__init__.py @@ -7,14 +7,14 @@ def register_prompt(mcp: FastMCP): """ @mcp.prompt() - def get_instances_by_security_group(security_group_name: str) -> str: + def get_servers_by_security_group(security_group_name: str) -> str: """ - Get instances associated with a specific security group. + Get servers associated with a specific security group. - :param security_group_name: The name of the security group to filter instances by. + :param security_group_name: The name of the security group to filter servers by. """ return ( - f"Find all compute instances that have the security group " + f"Find all compute servers that have the security group " f"'{security_group_name}' attached.\n\n" f"Steps:\n" f"1. Call get_servers to list all servers.\n" diff --git a/tests/prompts/test_prompts.py b/tests/prompts/test_prompts.py index 63630f2..559ba4c 100644 --- a/tests/prompts/test_prompts.py +++ b/tests/prompts/test_prompts.py @@ -1,7 +1,7 @@ from openstack_mcp_server.prompts import register_prompt -def test_get_instances_by_security_group_prompt_registered(): +def test_get_servers_by_security_group_prompt_registered(): """Test that the prompt is registered with the MCP instance.""" from unittest.mock import MagicMock @@ -10,7 +10,7 @@ def test_get_instances_by_security_group_prompt_registered(): mcp.prompt.assert_called() -def test_get_instances_by_security_group_prompt_content(): +def test_get_servers_by_security_group_prompt_content(): """Test that the prompt returns expected content.""" from fastmcp import FastMCP @@ -18,9 +18,9 @@ def test_get_instances_by_security_group_prompt_content(): register_prompt(mcp) prompts = mcp._prompt_manager._prompts - assert "get_instances_by_security_group" in prompts + assert "get_servers_by_security_group" in prompts - prompt_obj = prompts["get_instances_by_security_group"] + prompt_obj = prompts["get_servers_by_security_group"] assert prompt_obj.fn is not None result = prompt_obj.fn(security_group_name="my-sg") From 0c29e3c511f48c3c97e6a4233f95b359136b25c5 Mon Sep 17 00:00:00 2001 From: kent paik Date: Thu, 2 Apr 2026 21:49:28 +0900 Subject: [PATCH 3/4] style: align prompt test structure with tools test convention Wrap test functions in TestPrompts class and move imports to module level, matching the pattern used in tests/tools/. --- tests/prompts/test_prompts.py | 42 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/tests/prompts/test_prompts.py b/tests/prompts/test_prompts.py index 559ba4c..a18fc09 100644 --- a/tests/prompts/test_prompts.py +++ b/tests/prompts/test_prompts.py @@ -1,29 +1,31 @@ -from openstack_mcp_server.prompts import register_prompt +from unittest.mock import MagicMock +from fastmcp import FastMCP -def test_get_servers_by_security_group_prompt_registered(): - """Test that the prompt is registered with the MCP instance.""" - from unittest.mock import MagicMock +from openstack_mcp_server.prompts import register_prompt - mcp = MagicMock() - register_prompt(mcp) - mcp.prompt.assert_called() +class TestPrompts: + """Test cases for MCP prompts.""" -def test_get_servers_by_security_group_prompt_content(): - """Test that the prompt returns expected content.""" - from fastmcp import FastMCP + def test_get_servers_by_security_group_prompt_registered(self): + """Test that the prompt is registered with the MCP instance.""" + mcp = MagicMock() + register_prompt(mcp) + mcp.prompt.assert_called() - mcp = FastMCP("test") - register_prompt(mcp) + def test_get_servers_by_security_group_prompt_content(self): + """Test that the prompt returns expected content.""" + mcp = FastMCP("test") + register_prompt(mcp) - prompts = mcp._prompt_manager._prompts - assert "get_servers_by_security_group" in prompts + prompts = mcp._prompt_manager._prompts + assert "get_servers_by_security_group" in prompts - prompt_obj = prompts["get_servers_by_security_group"] - assert prompt_obj.fn is not None + prompt_obj = prompts["get_servers_by_security_group"] + assert prompt_obj.fn is not None - result = prompt_obj.fn(security_group_name="my-sg") - assert "my-sg" in result - assert "get_servers" in result - assert "security_groups" in result + result = prompt_obj.fn(security_group_name="my-sg") + assert "my-sg" in result + assert "get_servers" in result + assert "security_groups" in result From defc1ab2ccaaa7d7f06c21a3a5c7e185b29302a0 Mon Sep 17 00:00:00 2001 From: kent paik Date: Fri, 3 Apr 2026 14:26:38 +0900 Subject: [PATCH 4/4] refactor: rename test file to test_network_prompts.py Align test file naming with tools convention (e.g. test_network_tools.py) since security group is a network domain feature. --- tests/prompts/{test_prompts.py => test_network_prompts.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/prompts/{test_prompts.py => test_network_prompts.py} (100%) diff --git a/tests/prompts/test_prompts.py b/tests/prompts/test_network_prompts.py similarity index 100% rename from tests/prompts/test_prompts.py rename to tests/prompts/test_network_prompts.py