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.
- mcp_instana-0.1.0.dist-info/LICENSE +201 -0
- mcp_instana-0.1.0.dist-info/METADATA +649 -0
- mcp_instana-0.1.0.dist-info/RECORD +19 -0
- mcp_instana-0.1.0.dist-info/WHEEL +4 -0
- mcp_instana-0.1.0.dist-info/entry_points.txt +3 -0
- src/__init__.py +0 -0
- src/client/What is the sum of queue depth for all q +55 -0
- src/client/application_alert_config_mcp_tools.py +680 -0
- src/client/application_metrics_mcp_tools.py +377 -0
- src/client/application_resources_mcp_tools.py +391 -0
- src/client/events_mcp_tools.py +531 -0
- src/client/infrastructure_analyze_mcp_tools.py +634 -0
- src/client/infrastructure_catalog_mcp_tools.py +624 -0
- src/client/infrastructure_resources_mcp_tools.py +653 -0
- src/client/infrastructure_topology_mcp_tools.py +319 -0
- src/client/instana_client_base.py +93 -0
- src/client/log_alert_configuration_mcp_tools.py +316 -0
- src/client/show the top 5 services with the highest +28 -0
- src/mcp_server.py +343 -0
|
@@ -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
|
+
|