Skip to content

Commit 18c6315

Browse files
authored
f #10: Enhance get_vm_status tool to support multiple VM IDs in a single call. (#13)
1 parent 6fa01b1 commit 18c6315

File tree

4 files changed

+117
-21
lines changed

4 files changed

+117
-21
lines changed

src/static/mcp_server_prompt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ The `execute_command` tool implements a mandatory safety analysis protocol:
7676
### Virtual Machine Management
7777
- `list_vms`: List all virtual machines with optional filtering
7878
- `get_vm_status`: Get detailed VM status, IP addresses, and metrics
79+
- When requesting the status of **multiple VMs**, you MUST make **one** call and
80+
pass all IDs as a single comma-separated string in the `vm_id` argument
81+
(e.g. `get_vm_status(vm_id="1,2,3")`). Calling the tool repeatedly for
82+
each ID is prohibited.
83+
- If the user provides multiple ranges or individual IDs in one request (e.g. "5 to 3 and 3 to 1"), **merge**
84+
everything into a single ascending list without duplicates (result: "1,2,3,4,5") and call the tool once.
7985
- `execute_command`: Execute shell commands with comprehensive safety analysis. It can be used to run commands inside the VMs and you need to pass the vm_id argument.
8086
## Best Practices
8187

src/tests/promptfoo/tests/tools/tools_call.yaml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,35 @@
148148
transform: file://transforms/extract_function_call.js
149149
assert:
150150
- type: javascript
151-
value: output.length === 1 && output[0].function === 'get_vm_status'
151+
value: output.length === 1 && output[0].function === 'get_vm_status'
152+
153+
# VM get_vm_status tool tests for multiple VM IDs
154+
- vars:
155+
input: "Get VM status for VMs 1,2,3"
156+
options:
157+
transform: file://transforms/extract_function_call.js
158+
assert:
159+
- type: javascript
160+
value: output.length === 1 && output[0].function === 'get_vm_status'
161+
- type: javascript
162+
value: output[0].arguments.vm_id === '1,2,3'
163+
164+
- vars:
165+
input: "Get VM status for VM 1 through 10"
166+
options:
167+
transform: file://transforms/extract_function_call.js
168+
assert:
169+
- type: javascript
170+
value: output.length === 1 && output[0].function === 'get_vm_status'
171+
- type: javascript
172+
value: output[0].arguments.vm_id === '1,2,3,4,5,6,7,8,9,10'
173+
174+
- vars:
175+
input: "Get VM status from 5 to 3 and from 3 to 1"
176+
options:
177+
transform: file://transforms/extract_function_call.js
178+
assert:
179+
- type: javascript
180+
value: output.length === 1 && output[0].function === 'get_vm_status'
181+
- type: javascript
182+
value: output[0].arguments.vm_id === '1,2,3,4,5'

src/tests/promptfoo/tests/tools/vm/vm_execute_command.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
options:
77
transform: file://transforms/extract_function_call.js
88
assert:
9-
# Expect get_vm_status followed by execute_command with non-interactive flags
109
- type: javascript
1110
value: output.length === 1
1211
- type: javascript

src/tools/vm/vm.py

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -107,37 +107,97 @@ def register_tools(mcp, allow_write):
107107

108108
@mcp.tool(
109109
name="get_vm_status",
110-
description=f"""Retrieve current state, IP address, and runtime metrics of a VM.
111-
It is possible to pass the vm_id as a string representing a non-negative integer otherwise you cannot use the tool
112-
Before calling this tool, ALWAYS perform these checks in the same turn:
113-
- vm_id validation
114-
- Accept only strings that represent non-negative integers (e.g. "0", "42").
115-
- If the user supplies anything else (letters, symbols, negative numbers), DO NOT call manage_vm.
116-
- Instead, respond with a short apology and explain the input must be a non-negative integer.
110+
description=f"""Retrieve current state, IP address, and runtime metrics of one or more VMs.
111+
112+
vm_id MUST be provided as **a single string** that contains one or more **comma-separated** non-negative
113+
integers, forming a **list** of IDs. Always supply a list – even when you
114+
need the status of a single VM. For example:
115+
116+
• "42" → list with one VM ID (42)
117+
• "1,2,3" → list with three VM IDs (1, 2 and 3)
118+
119+
IMPORTANT: Ranges (e.g. "5..10") or mixed separators (spaces, semicolons, etc.) are **NOT** supported – keep the
120+
approach simple and only use commas. If any element of the list is not a non-negative integer, the call must be
121+
rejected with an explanatory error.
122+
123+
Before calling this tool you MUST validate **every** ID in the list:
124+
- Each part after splitting on "," must be composed solely of digits and represent a non-negative integer.
125+
- If a user expresses a **range** in natural language (e.g. "5 to 3" or "1-10"), you MUST **expand** that
126+
range yourself before calling this tool. Produce an **ascending** comma-separated list with
127+
no duplicates ("5 to 3" → "3,4,5", "1-4" → "1,2,3,4"). Only after expansion should you invoke
128+
`get_vm_status`.
129+
- If multiple ranges or individual IDs are mentioned in the same request, **merge** them into a single
130+
set, remove duplicates, sort ascending, and pass as one comma-separated string (e.g. "5 to 3 and 3 to 1"
131+
→ "1,2,3,4,5"). Only ONE call to `get_vm_status` is allowed per user request.
132+
- After expansion, if any element is not a non-negative integer, respond with an error and DO NOT perform the OpenNebula call.
133+
134+
For multiple IDs the tool will return an <VMS> root element containing the XML description of each VM exactly as
135+
returned by `onevm show <id> --xml`.
117136
{VM_STATES_DESCRIPTION}
118137
{VM_TEMPLATE_DESCRIPTION}
138+
139+
**CALLING RULES (MANDATORY)**:
140+
• When a user requests the status of several VMs you MUST issue **one and only one** call to this tool.
141+
Consolidate all requested IDs into the `vm_id` argument as a comma-separated string (e.g. `"1,2,3"`).
142+
• Do **NOT** loop or invoke `get_vm_status` repeatedly for each ID – that violates protocol and
143+
wastes resources. Always batch the IDs into a single call.
119144
""",
120145
)
121146
def get_vm_status(vm_id: str) -> str:
122-
"""Retrieve full details for a specific VM.
147+
"""Retrieve full details for one or more VMs.
148+
123149
Args:
124-
vm_id: The integer ID of the virtual machine.
150+
vm_id: A **comma-separated** string of VM IDs (e.g. "1" or "1,2,3").
151+
125152
Returns:
126-
str: XML string conforming to the VM XSD Schema.
153+
str: XML string. For a single VM, the raw `<VM>` element returned by OpenNebula. For multiple VMs, a root
154+
`<VMS>` element containing each individual `<VM>` child.
127155
"""
128-
logger.debug(f"Getting VM status for VM ID: {vm_id}")
129156

130-
if not vm_id.isdigit() and int(vm_id) <= 0:
131-
logger.error(f"Invalid VM ID provided: {vm_id} (must be positive integer)")
132-
return "<error><message>vm_id must be a positive integer</message></error>"
157+
logger.debug(f"Getting VM status for VM ID(s): {vm_id}")
158+
159+
# Split by comma and validate each part
160+
id_parts = [part.strip() for part in vm_id.split(',') if part.strip()]
161+
162+
if not id_parts:
163+
logger.error("vm_id parameter is empty after parsing")
164+
return "<error><message>vm_id must contain at least one non-negative integer</message></error>"
165+
166+
for part in id_parts:
167+
if not part.isdigit():
168+
logger.error(f"Invalid VM ID provided: {part} (must be non-negative integer)")
169+
return (
170+
"<error><message>All vm_id values must be non-negative integers separated by commas</message></error>"
171+
)
133172

134173
try:
135-
result = execute_one_command(["onevm", "show", vm_id, "--xml"])
136-
logger.debug(f"Successfully retrieved VM status for VM {vm_id}")
137-
return result
174+
if len(id_parts) == 1:
175+
# Single VM – return raw XML as-is
176+
single_id = id_parts[0]
177+
result = execute_one_command(["onevm", "show", single_id, "--xml"])
178+
logger.debug(f"Successfully retrieved VM status for VM {single_id}")
179+
return result
180+
181+
# Multiple VMs – aggregate under <VMS>
182+
root = ET.Element("VMS")
183+
for vmid in id_parts:
184+
try:
185+
vm_xml = execute_one_command(["onevm", "show", vmid, "--xml"])
186+
vm_element = ET.fromstring(vm_xml)
187+
root.append(vm_element)
188+
logger.debug(f"Added VM {vmid} status to aggregate output")
189+
except Exception as e:
190+
logger.error(f"Failed to get VM status for VM {vmid}: {e}")
191+
# Include an <error> element for this VM instead of failing entire request
192+
err_el = ET.SubElement(root, "error")
193+
ET.SubElement(err_el, "vm_id").text = vmid
194+
ET.SubElement(err_el, "message").text = str(e)
195+
196+
return ET.tostring(root, encoding="unicode")
197+
138198
except Exception as e:
139-
logger.error(f"Failed to get VM status for VM {vm_id}: {e}")
140-
raise
199+
logger.error(f"Unexpected error while retrieving VM status: {e}")
200+
return f"<error><message>{e}</message></error>"
141201

142202
@mcp.tool(
143203
name="execute_command",

0 commit comments

Comments
 (0)