@@ -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