mcp-instana 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,653 @@
1
+ """
2
+ Infrastructure Resources MCP Tools Module
3
+
4
+ This module provides infrastructure resources-specific MCP tools for Instana monitoring.
5
+ """
6
+
7
+ import sys
8
+ import traceback
9
+ from typing import Dict, Any, Optional, List, Union
10
+ from datetime import datetime
11
+
12
+ # Import the necessary classes from the SDK
13
+ try:
14
+ from instana_client.api.infrastructure_resources_api import InfrastructureResourcesApi
15
+ from instana_client.api_client import ApiClient
16
+ from instana_client.configuration import Configuration
17
+ # Check if GetSnapshotsQuery exists, otherwise we'll handle it differently
18
+ try:
19
+ from instana_client.models.get_snapshots_query import GetSnapshotsQuery
20
+ has_get_snapshots_query = True
21
+ except ImportError:
22
+ has_get_snapshots_query = False
23
+ except ImportError as e:
24
+ traceback.print_exc(file=sys.stderr)
25
+ raise
26
+
27
+ from .instana_client_base import BaseInstanaClient, register_as_tool
28
+
29
+ # Helper function for debug printing
30
+ def debug_print(*args, **kwargs):
31
+ """Print debug information to stderr instead of stdout"""
32
+ print(*args, file=sys.stderr, **kwargs)
33
+
34
+ class InfrastructureResourcesMCPTools(BaseInstanaClient):
35
+ """Tools for infrastructure resources in Instana MCP."""
36
+
37
+ def __init__(self, read_token: str, base_url: str):
38
+ """Initialize the Infrastructure Resources MCP tools client."""
39
+ super().__init__(read_token=read_token, base_url=base_url)
40
+
41
+ try:
42
+
43
+ # Configure the API client with the correct base URL and authentication
44
+ configuration = Configuration()
45
+ configuration.host = base_url
46
+ configuration.api_key['ApiKeyAuth'] = read_token
47
+ configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
48
+
49
+ # Create an API client with this configuration
50
+ api_client = ApiClient(configuration=configuration)
51
+
52
+ # Initialize the Instana SDK's InfrastructureResourcesApi with our configured client
53
+ self.infra_api = InfrastructureResourcesApi(api_client=api_client)
54
+ except Exception as e:
55
+ debug_print(f"Error initializing InfrastructureResourcesApi: {e}")
56
+ traceback.print_exc(file=sys.stderr)
57
+ raise
58
+
59
+ @register_as_tool
60
+ async def get_monitoring_state(self, ctx=None) -> Dict[str, Any]:
61
+ """
62
+ Get the current monitoring state of the Instana system. This tool retrieves details about the number of monitored hosts and serverless entities in your environment.
63
+ Use this when you need an overview of your monitoring coverage, want to check how many hosts are being monitored, or need to verify the scale of your Instana deployment.
64
+ Use this tool when asked about 'monitoring status', 'how many hosts are monitored', 'monitoring coverage', or when someone wants to 'check the monitoring state'.
65
+
66
+ Args:
67
+ ctx: The MCP context (optional)
68
+
69
+ Returns:
70
+ Dictionary containing monitoring state information or error information
71
+ """
72
+ try:
73
+ print("get_monitoring_state called", file=sys.stderr)
74
+
75
+ # Call the get_monitoring_state method from the SDK
76
+ result = self.infra_api.get_monitoring_state()
77
+
78
+ print(f"Result from get_monitoring_state: {result}", file=sys.stderr)
79
+ return result
80
+ except Exception as e:
81
+ print(f"Error in get_monitoring_state: {e}", file=sys.stderr)
82
+ import traceback
83
+ traceback.print_exc(file=sys.stderr)
84
+ return {"error": f"Failed to get monitoring state: {str(e)}"}
85
+
86
+ @register_as_tool
87
+ async def get_plugin_payload(self,
88
+ snapshot_id: str,
89
+ payload_key: str,
90
+ to_time: Optional[int] = None,
91
+ window_size: Optional[int] = None,
92
+ ctx=None) -> Dict[str, Any]:
93
+ """
94
+ Get detailed payload data for a specific snapshot in Instana. This tool retrieves raw monitoring data for a particular entity snapshot using its ID and a specific payload key.
95
+ Use this when you need to access detailed, low-level monitoring information that isn't available through other APIs.
96
+ This is particularly useful for deep troubleshooting, accessing specific metrics or configuration details, or when you need the complete raw data for a monitored
97
+ entity. For example, use this tool when asked about 'detailed snapshot data', 'raw monitoring information', 'plugin payload details', or when someone wants to 'get the complete data for a specific entity'.
98
+
99
+ Args:
100
+ snapshot_id: The ID of the snapshot
101
+ payload_key: The key of the payload to retrieve
102
+ to_time: End timestamp in milliseconds (optional)
103
+ window_size: Window size in milliseconds (optional)
104
+ ctx: The MCP context (optional)
105
+
106
+ Returns:
107
+ Dictionary containing the payload data or error information
108
+ """
109
+ try:
110
+ print(f"get_plugin_payload called with snapshot_id={snapshot_id}, payload_key={payload_key}", file=sys.stderr)
111
+
112
+ # Call the get_plugin_payload method from the SDK
113
+ result = self.infra_api.get_plugin_payload(
114
+ snapshot_id=snapshot_id,
115
+ payload_key=payload_key,
116
+ to=to_time,
117
+ window_size=window_size
118
+ )
119
+
120
+ print(f"Result from get_plugin_payload: {result}", file=sys.stderr)
121
+ return result
122
+ except Exception as e:
123
+ print(f"Error in get_plugin_payload: {e}", file=sys.stderr)
124
+ import traceback
125
+ traceback.print_exc(file=sys.stderr)
126
+ return {"error": f"Failed to get plugin payload: {str(e)}"}
127
+
128
+ @register_as_tool
129
+ async def get_snapshot(self,
130
+ snapshot_id: str,
131
+ to_time: Optional[int] = None,
132
+ window_size: Optional[int] = None,
133
+ ctx=None) -> Dict[str, Any]:
134
+ """
135
+ Get detailed information about a specific snapshot in Instana using its ID.
136
+
137
+ This tool retrieves comprehensive data for a single, specific entity snapshot that you already have the ID for.
138
+ Use this when you need to examine one particular entity in depth, such as investigating a specific host, container,
139
+ or process that you've already identified. This is NOT for searching or discovering entities - use get_snapshots for that purpose.
140
+
141
+ For example, use this tool when:
142
+ - You already have a specific snapshot ID and need its details
143
+ - You want to examine one particular entity's configuration and metrics
144
+ - You need to troubleshoot a specific component that's already been identified
145
+ - Someone asks for "details about this specific entity" or "information about snapshot 12345"
146
+
147
+ Args:
148
+ snapshot_id: The ID of the snapshot to retrieve
149
+ to_time: End timestamp in milliseconds (optional)
150
+ window_size: Window size in milliseconds (optional)
151
+ ctx: The MCP context (optional)
152
+
153
+ Returns:
154
+ Dictionary containing snapshot details or error information
155
+ """
156
+ try:
157
+ print(f"get_snapshot called with snapshot_id={snapshot_id}", file=sys.stderr)
158
+
159
+ if not snapshot_id:
160
+ return {"error": "snapshot_id parameter is required"}
161
+
162
+ # Try using the standard SDK method
163
+ try:
164
+ # Call the get_snapshot method from the SDK
165
+ result = self.infra_api.get_snapshot(
166
+ id=snapshot_id,
167
+ to=to_time,
168
+ window_size=window_size
169
+ )
170
+
171
+ # Convert the result to a dictionary
172
+ if hasattr(result, 'to_dict'):
173
+ result_dict = result.to_dict()
174
+ elif isinstance(result, dict):
175
+ result_dict = result
176
+ else:
177
+ # For any other type, convert to string representation
178
+ result_dict = {"data": str(result), "snapshot_id": snapshot_id}
179
+
180
+ print(f"Result from get_snapshot: {result_dict}", file=sys.stderr)
181
+ return result_dict
182
+
183
+ except Exception as sdk_error:
184
+ print(f"SDK method failed: {sdk_error}, trying fallback", file=sys.stderr)
185
+
186
+ # Check if it's a "not found" error
187
+ error_str = str(sdk_error).lower()
188
+ if "not exist" in error_str or "not found" in error_str or "not available" in error_str:
189
+ return {
190
+ "error": f"Snapshot with ID '{snapshot_id}' does not exist or is not available.",
191
+ "details": str(sdk_error)
192
+ }
193
+
194
+ # Check if it's a validation error
195
+ if "validation error" in error_str:
196
+ # Try using the without_preload_content version to get the raw response
197
+ try:
198
+ response_data = self.infra_api.get_snapshot_without_preload_content(
199
+ id=snapshot_id,
200
+ to=to_time,
201
+ window_size=window_size
202
+ )
203
+
204
+ # Check if the response was successful
205
+ if response_data.status != 200:
206
+ error_message = f"Failed to get snapshot: HTTP {response_data.status}"
207
+ print(error_message, file=sys.stderr)
208
+ return {"error": error_message}
209
+
210
+ # Read the response content
211
+ response_text = response_data.data.decode('utf-8')
212
+
213
+ # Try to parse as JSON
214
+ import json
215
+ try:
216
+ result_dict = json.loads(response_text)
217
+ print(f"Result from fallback method: {result_dict}", file=sys.stderr)
218
+ return result_dict
219
+ except json.JSONDecodeError:
220
+ # If not valid JSON, return as string
221
+ print(f"Result from fallback method (string): {response_text}", file=sys.stderr)
222
+ return {"message": response_text, "snapshot_id": snapshot_id}
223
+
224
+ except Exception as fallback_error:
225
+ print(f"Fallback method failed: {fallback_error}", file=sys.stderr)
226
+ # Continue to the general error handling
227
+
228
+ # Re-raise if we couldn't handle it specifically
229
+ raise
230
+
231
+ except Exception as e:
232
+ print(f"Error in get_snapshot: {e}", file=sys.stderr)
233
+ import traceback
234
+ traceback.print_exc(file=sys.stderr)
235
+ return {"error": f"Failed to get snapshot: {str(e)}"}
236
+
237
+ @register_as_tool
238
+ async def get_snapshots(self,
239
+ query: Optional[str] = None,
240
+ from_time: Optional[int] = None,
241
+ to_time: Optional[int] = None,
242
+ size: Optional[int] = 100,
243
+ plugin: Optional[str] = None,
244
+ offline: Optional[bool] = False,
245
+ detailed: Optional[bool] = False,
246
+ ctx=None) -> Dict[str, Any]:
247
+ """
248
+ Search and discover snapshots based on search criteria.
249
+
250
+ This tool is for finding and retrieving MULTIPLE entities that match your search parameters.
251
+ Use this when you need to discover entities, search across your infrastructure, or find components
252
+ matching certain criteria. This is NOT for retrieving details about a specific entity you already know -
253
+ use get_snapshot (singular) for that purpose.
254
+
255
+ For example, use this tool when:
256
+ - Searching for all hosts with high CPU
257
+ - Finding all containers in a specific namespace
258
+ - Discovering entities matching a query pattern
259
+ - You need to list multiple components of a certain type
260
+
261
+ Args:
262
+ query: Query string to filter snapshots (optional)
263
+ from_time: Start timestamp in milliseconds (optional, defaults to 1 hour ago)
264
+ to_time: End timestamp in milliseconds (optional, defaults to now)
265
+ size: Maximum number of snapshots to return (optional, default 100)
266
+ plugin: Entity type to filter by (optional)
267
+ offline: Whether to include offline snapshots (optional, default False)
268
+ detailed: If True, returns full raw data. If False (default), returns summarized data
269
+ ctx: The MCP context (optional)
270
+
271
+ Returns:
272
+ Dictionary containing matching snapshots (summarized by default) or error information
273
+ """
274
+ try:
275
+ debug_print(f"get_snapshots called with query={query}, from_time={from_time}, to_time={to_time}, size={size}, detailed={detailed}")
276
+
277
+ # Set default time range if not provided
278
+ if not to_time:
279
+ to_time = int(datetime.now().timestamp() * 1000)
280
+
281
+ if not from_time:
282
+ from_time = to_time - (60 * 60 * 1000) # Default to 1 hour
283
+
284
+ # Call the get_snapshots method from the SDK
285
+ result = self.infra_api.get_snapshots(
286
+ query=query,
287
+ to=to_time,
288
+ window_size=to_time - from_time if from_time else None,
289
+ size=size,
290
+ plugin=plugin,
291
+ offline=offline
292
+ )
293
+
294
+ debug_print(f"SDK returned result type: {type(result)}")
295
+
296
+ # Convert result to dictionary if needed
297
+ if hasattr(result, 'to_dict'):
298
+ result_dict = result.to_dict()
299
+ elif isinstance(result, dict):
300
+ result_dict = result
301
+ else:
302
+ result_dict = {"data": str(result)}
303
+
304
+ debug_print(f"Result dict keys: {list(result_dict.keys()) if isinstance(result_dict, dict) else 'Not a dict'}")
305
+
306
+ # Return based on detailed parameter
307
+ if detailed:
308
+ debug_print("Returning detailed/raw response")
309
+ return result_dict
310
+ else:
311
+ debug_print("Returning summarized response")
312
+ return self._summarize_get_snapshots_response(result_dict)
313
+
314
+ except Exception as e:
315
+ debug_print(f"Error in get_snapshots: {e}")
316
+ import traceback
317
+ traceback.print_exc(file=sys.stderr)
318
+ return {"error": f"Failed to get snapshots: {str(e)}"}
319
+
320
+ def _summarize_get_snapshots_response(self, response_data: Dict[str, Any]) -> Dict[str, Any]:
321
+ """
322
+ Create a summarized version of the get_snapshots response.
323
+ """
324
+ try:
325
+ debug_print("Creating summarized get_snapshots response...")
326
+ items = response_data.get('items', [])
327
+
328
+ if len(items) == 0:
329
+ return {
330
+ "message": "No snapshots found matching your criteria.",
331
+ "total_found": 0,
332
+ "snapshots": []
333
+ }
334
+
335
+ # Create a readable summary
336
+ summary_lines = [f"Found {len(items)} snapshot(s) matching your criteria:\n"]
337
+
338
+ snapshots_list = []
339
+ for i, item in enumerate(items, 1):
340
+ snapshot_id = item.get('snapshotId', 'Unknown')
341
+ label = item.get('label', 'No label')
342
+ host = item.get('host', 'Unknown host')
343
+ plugin = item.get('plugin', 'Unknown plugin')
344
+
345
+ # Parse host information
346
+ host_info = "Unknown"
347
+ if 'arn:aws:ecs' in host:
348
+ parts = host.split(':')
349
+ if len(parts) >= 6:
350
+ cluster_info = parts[5].split('/') if len(parts) > 5 else []
351
+ region = parts[3] if len(parts) > 3 else "unknown"
352
+ cluster = cluster_info[1] if len(cluster_info) > 1 else "unknown"
353
+ task_id = cluster_info[2] if len(cluster_info) > 2 else "unknown"
354
+ host_info = f"AWS ECS Task in {region} (cluster: {cluster})"
355
+ else:
356
+ host_info = host
357
+
358
+ # Create readable entry
359
+ snapshot_entry = {
360
+ "number": i,
361
+ "snapshotId": snapshot_id,
362
+ "label": label,
363
+ "plugin": plugin,
364
+ "host_info": host_info,
365
+ "full_host": host
366
+ }
367
+
368
+ snapshots_list.append(snapshot_entry)
369
+
370
+ # Add to summary lines
371
+ summary_lines.append(f"{i}. Snapshot ID: {snapshot_id}")
372
+ summary_lines.append(f" Label: {label}")
373
+ summary_lines.append(f" Plugin: {plugin}")
374
+ summary_lines.append(f" Host: {host_info}")
375
+ summary_lines.append("") # Empty line for spacing
376
+
377
+ return {
378
+ "summary": "\n".join(summary_lines),
379
+ "total_found": len(items),
380
+ "snapshots": snapshots_list,
381
+ "message": f"Successfully found {len(items)} snapshot(s). See details above."
382
+ }
383
+
384
+ except Exception as e:
385
+ debug_print(f"Error summarizing get_snapshots response: {e}")
386
+ import traceback
387
+ traceback.print_exc(file=sys.stderr)
388
+ return {
389
+ "error": "Failed to summarize response",
390
+ "details": str(e)
391
+ }
392
+
393
+
394
+
395
+ @register_as_tool
396
+ async def post_snapshots(self,
397
+ snapshot_ids: Union[List[str], str],
398
+ to_time: Optional[int] = None,
399
+ window_size: Optional[int] = None,
400
+ detailed: Optional[bool] = False,
401
+ ctx=None) -> Dict[str, Any]:
402
+ """
403
+ Get details for multiple snapshots by their IDs using SDK.
404
+
405
+ Args:
406
+ snapshot_ids: List of snapshot IDs to retrieve, or a comma-separated string of IDs
407
+ to_time: End timestamp in milliseconds (optional)
408
+ window_size: Window size in milliseconds (optional)
409
+ detailed: If True, returns full raw data. If False (default), returns summarized data
410
+ ctx: The MCP context (optional)
411
+
412
+ Returns:
413
+ Dictionary containing snapshot details (summarized by default) or error information
414
+ """
415
+ try:
416
+ debug_print(f"post_snapshots called with snapshot_ids={snapshot_ids}, detailed={detailed}")
417
+
418
+ # Handle string input conversion
419
+ if isinstance(snapshot_ids, str):
420
+ if snapshot_ids.startswith('[') and snapshot_ids.endswith(']'):
421
+ import ast
422
+ snapshot_ids = ast.literal_eval(snapshot_ids)
423
+ else:
424
+ snapshot_ids = [id.strip() for id in snapshot_ids.split(',')]
425
+
426
+ if not snapshot_ids:
427
+ return {"error": "snapshot_ids parameter is required"}
428
+
429
+ # Use working timeframe
430
+ if not to_time:
431
+ to_time = 1745389956000
432
+ if not window_size:
433
+ window_size = 3600000
434
+
435
+ debug_print(f"Using to_time={to_time}, window_size={window_size}")
436
+
437
+ if has_get_snapshots_query:
438
+ from instana_client.models.get_snapshots_query import GetSnapshotsQuery
439
+
440
+ query_obj = GetSnapshotsQuery(
441
+ snapshot_ids=snapshot_ids,
442
+ time_frame={
443
+ "to": to_time,
444
+ "windowSize": window_size
445
+ }
446
+ )
447
+
448
+ debug_print(f"Making SDK request with without_preload_content...")
449
+
450
+ # Use the working SDK method that bypasses model validation
451
+ response = self.infra_api.post_snapshots_without_preload_content(
452
+ get_snapshots_query=query_obj
453
+ )
454
+
455
+ debug_print(f"SDK response status: {response.status}")
456
+
457
+ if response.status == 200:
458
+ # Parse the JSON response manually
459
+ import json
460
+ response_text = response.data.decode('utf-8')
461
+ result_dict = json.loads(response_text)
462
+
463
+ debug_print(f"Successfully parsed response with {len(result_dict.get('items', []))} items")
464
+
465
+ # Return based on detailed parameter
466
+ if detailed:
467
+ debug_print("Returning detailed/raw response")
468
+ return result_dict
469
+ else:
470
+ debug_print("Returning summarized response")
471
+ return self._summarize_snapshots_response(result_dict)
472
+ else:
473
+ return {
474
+ "error": f"SDK returned status {response.status}",
475
+ "details": response.data.decode('utf-8') if response.data else None
476
+ }
477
+ else:
478
+ return {"error": "GetSnapshotsQuery model not available"}
479
+
480
+ except Exception as e:
481
+ debug_print(f"Error in post_snapshots: {e}")
482
+ import traceback
483
+ traceback.print_exc(file=sys.stderr)
484
+ return {"error": f"Failed to post snapshots: {str(e)}"}
485
+
486
+ def _summarize_snapshots_response(self, response_data: Dict[str, Any]) -> Dict[str, Any]:
487
+ """
488
+ Create a summarized version of the snapshots response.
489
+ """
490
+ try:
491
+ debug_print("Creating summarized response...")
492
+ items = response_data.get('items', [])
493
+
494
+ summary = {
495
+ "total_snapshots": len(items),
496
+ "snapshots": []
497
+ }
498
+
499
+ for item in items:
500
+ debug_print(f"Processing snapshot: {item.get('snapshotId')} - {item.get('plugin')}")
501
+
502
+ snapshot_summary = {
503
+ "snapshotId": item.get('snapshotId'),
504
+ "plugin": item.get('plugin'),
505
+ "label": item.get('label'),
506
+ "entityId": item.get('entityId', {}),
507
+ "timeframe": {
508
+ "from": item.get('from'),
509
+ "to": item.get('to')
510
+ },
511
+ "tags": item.get('tags', [])
512
+ }
513
+
514
+ # Extract key information from data based on plugin type
515
+ data = item.get('data', {})
516
+
517
+ if item.get('plugin') == 'jvmRuntimePlatform':
518
+ debug_print("Processing JVM snapshot...")
519
+ snapshot_summary["key_info"] = {
520
+ "process_name": data.get('name'),
521
+ "pid": data.get('pid'),
522
+ "jvm_version": data.get('jvm.version'),
523
+ "jvm_vendor": data.get('jvm.vendor'),
524
+ "jvm_name": data.get('jvm.name'),
525
+ "jvm_build": data.get('jvm.build'),
526
+ "memory_max": data.get('memory.max'),
527
+ "jvm_pools_count": len(data.get('jvm.pools', {})),
528
+ "jvm_args_count": len(data.get('jvm.args', [])),
529
+ "jvm_collectors": data.get('jvm.collectors', [])
530
+ }
531
+ elif item.get('plugin') == 'nodeJsRuntimePlatform':
532
+ debug_print("Processing Node.js snapshot...")
533
+ versions = data.get('versions', {})
534
+ snapshot_summary["key_info"] = {
535
+ "app_name": data.get('name'),
536
+ "version": data.get('version'),
537
+ "description": data.get('description'),
538
+ "pid": data.get('pid'),
539
+ "node_version": versions.get('node'),
540
+ "v8_version": versions.get('v8'),
541
+ "uv_version": versions.get('uv'),
542
+ "sensor_version": data.get('sensorVersion'),
543
+ "dependencies_count": len(data.get('dependencies', {})),
544
+ "start_time": data.get('startTime'),
545
+ "http_endpoints": list(data.get('http', {}).keys()),
546
+ "gc_stats_supported": data.get('gc.statsSupported'),
547
+ "libuv_stats_supported": data.get('libuv.statsSupported')
548
+ }
549
+ else:
550
+ # Generic summary for other plugin types
551
+ debug_print(f"Processing generic snapshot for plugin: {item.get('plugin')}")
552
+ snapshot_summary["key_info"] = {
553
+ "data_keys": list(data.keys())[:10], # First 10 keys
554
+ "total_data_fields": len(data.keys())
555
+ }
556
+
557
+ summary["snapshots"].append(snapshot_summary)
558
+
559
+ debug_print(f"Created summary with {len(summary['snapshots'])} snapshots")
560
+ return summary
561
+
562
+ except Exception as e:
563
+ debug_print(f"Error summarizing response: {e}")
564
+ import traceback
565
+ traceback.print_exc(file=sys.stderr)
566
+ return {
567
+ "error": "Failed to summarize response",
568
+ "details": str(e),
569
+ "raw_data_sample": str(response_data)[:500] # First 500 chars for debugging
570
+ }
571
+
572
+
573
+
574
+
575
+ @register_as_tool
576
+ async def software_versions(self, ctx=None) -> Dict[str, Any]:
577
+ """
578
+ Get information about installed software versions across the monitored infrastructure.
579
+ Retrieve information about the software that are sensed by the agent remotely, natively, or both. This includes runtime and package manager information.
580
+ Args:
581
+ ctx: The MCP context (optional)
582
+
583
+ Returns:
584
+ Dictionary containing software version information or error information
585
+ """
586
+ try:
587
+ print("Calling software_versions API...", file=sys.stderr)
588
+
589
+ # Call the software_versions method from the SDK with no parameters
590
+ result = self.infra_api.software_versions()
591
+
592
+ print(f"API call successful. Result type: {type(result)}", file=sys.stderr)
593
+
594
+ # Handle different response formats
595
+ if hasattr(result, 'to_dict'):
596
+ print("Converting result using to_dict() method", file=sys.stderr)
597
+ result_dict = result.to_dict()
598
+ elif isinstance(result, dict):
599
+ print("Result is already a dictionary", file=sys.stderr)
600
+ result_dict = result
601
+ elif isinstance(result, list):
602
+ print("Result is a list", file=sys.stderr)
603
+ result_dict = {"items": result}
604
+ else:
605
+ print(f"Unexpected result format: {type(result)}", file=sys.stderr)
606
+ # Try to convert to a dictionary or string representation
607
+ try:
608
+ result_dict = {"data": str(result)}
609
+ except Exception as str_error:
610
+ return {"error": f"Unexpected result format: {type(result)}", "details": str(str_error)}
611
+
612
+ # Print a sample of the result for debugging
613
+ if isinstance(result_dict, dict):
614
+ keys = list(result_dict.keys())
615
+ print(f"Result keys: {keys}", file=sys.stderr)
616
+
617
+ # If the result is very large, return a summary
618
+ if 'items' in result_dict and isinstance(result_dict['items'], list):
619
+ items_count = len(result_dict['items'])
620
+ print(f"Found {items_count} items in the response", file=sys.stderr)
621
+
622
+ # Limit the number of items to return
623
+ if items_count > 10:
624
+ result_dict['summary'] = f"Showing 10 of {items_count} items"
625
+ result_dict['items'] = result_dict['items'][:10]
626
+
627
+ # If tagTree exists, extract tag names
628
+ if 'tagTree' in result_dict and isinstance(result_dict['tagTree'], list):
629
+ tag_names = []
630
+ for category in result_dict['tagTree']:
631
+ if isinstance(category, dict) and 'children' in category:
632
+ category_name = category.get('label', 'Unknown')
633
+ for tag in category['children']:
634
+ if isinstance(tag, dict) and 'tagName' in tag:
635
+ tag_names.append({
636
+ 'category': category_name,
637
+ 'tagName': tag['tagName'],
638
+ 'description': tag.get('description', '')
639
+ })
640
+
641
+ # Replace the large tagTree with the extracted tag names
642
+ result_dict['tagNames'] = tag_names
643
+ del result_dict['tagTree']
644
+
645
+ return result_dict
646
+ except Exception as e:
647
+ print(f"Error in software_versions: {e}", file=sys.stderr)
648
+ import traceback
649
+ traceback.print_exc(file=sys.stderr)
650
+ return {"error": f"Failed to get software versions: {str(e)}"}
651
+
652
+
653
+