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,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Infrastructure Topology MCP Tools Module
|
|
3
|
+
|
|
4
|
+
This module provides infrastructure topology-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_topology_api import InfrastructureTopologyApi
|
|
15
|
+
from instana_client.api_client import ApiClient
|
|
16
|
+
from instana_client.configuration import Configuration
|
|
17
|
+
from instana_client.models.topology import Topology
|
|
18
|
+
except ImportError as e:
|
|
19
|
+
traceback.print_exc(file=sys.stderr)
|
|
20
|
+
raise
|
|
21
|
+
|
|
22
|
+
from .instana_client_base import BaseInstanaClient, register_as_tool
|
|
23
|
+
|
|
24
|
+
# Helper function for debug printing
|
|
25
|
+
def debug_print(*args, **kwargs):
|
|
26
|
+
"""Print debug information to stderr instead of stdout"""
|
|
27
|
+
print(*args, file=sys.stderr, **kwargs)
|
|
28
|
+
|
|
29
|
+
class InfrastructureTopologyMCPTools(BaseInstanaClient):
|
|
30
|
+
"""Tools for infrastructure topology in Instana MCP."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, read_token: str, base_url: str):
|
|
33
|
+
"""Initialize the Infrastructure Topology MCP tools client."""
|
|
34
|
+
super().__init__(read_token=read_token, base_url=base_url)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
|
|
38
|
+
# Configure the API client with the correct base URL and authentication
|
|
39
|
+
configuration = Configuration()
|
|
40
|
+
configuration.host = base_url
|
|
41
|
+
configuration.api_key['ApiKeyAuth'] = read_token
|
|
42
|
+
configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
|
|
43
|
+
|
|
44
|
+
# Create an API client with this configuration
|
|
45
|
+
api_client = ApiClient(configuration=configuration)
|
|
46
|
+
|
|
47
|
+
# Initialize the Instana SDK's InfrastructureTopologyApi with our configured client
|
|
48
|
+
self.topo_api = InfrastructureTopologyApi(api_client=api_client)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
debug_print(f"Error initializing InfrastructureTopologyApi: {e}")
|
|
51
|
+
traceback.print_exc(file=sys.stderr)
|
|
52
|
+
raise
|
|
53
|
+
|
|
54
|
+
@register_as_tool
|
|
55
|
+
async def get_related_hosts(self,
|
|
56
|
+
snapshot_id: str,
|
|
57
|
+
to_time: Optional[int] = None,
|
|
58
|
+
window_size: Optional[int] = None,
|
|
59
|
+
ctx=None) -> Dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Get hosts related to a specific snapshot.
|
|
62
|
+
|
|
63
|
+
This tool retrieves a list of host IDs that are related to the specified snapshot. Use this when you need to
|
|
64
|
+
understand the relationships between infrastructure components, particularly which hosts are connected to
|
|
65
|
+
a specific entity.
|
|
66
|
+
|
|
67
|
+
For example, use this tool when:
|
|
68
|
+
- You need to find all hosts connected to a specific container, process, or service
|
|
69
|
+
- You want to understand the infrastructure dependencies of an application component
|
|
70
|
+
- You're investigating an issue and need to see which hosts might be affected
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
snapshot_id: The ID of the snapshot to find related hosts for (required)
|
|
74
|
+
to_time: End timestamp in milliseconds (optional)
|
|
75
|
+
window_size: Window size in milliseconds (optional)
|
|
76
|
+
ctx: The MCP context (optional)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary containing related hosts information or error information
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
debug_print(f"get_related_hosts called with snapshot_id={snapshot_id}")
|
|
83
|
+
|
|
84
|
+
if not snapshot_id:
|
|
85
|
+
return {"error": "snapshot_id parameter is required"}
|
|
86
|
+
|
|
87
|
+
# Call the get_related_hosts method from the SDK
|
|
88
|
+
result = self.topo_api.get_related_hosts(
|
|
89
|
+
snapshot_id=snapshot_id,
|
|
90
|
+
to=to_time,
|
|
91
|
+
window_size=window_size
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Convert the result to a dictionary
|
|
95
|
+
if isinstance(result, list):
|
|
96
|
+
result_dict = {
|
|
97
|
+
"relatedHosts": result,
|
|
98
|
+
"count": len(result),
|
|
99
|
+
"snapshotId": snapshot_id
|
|
100
|
+
}
|
|
101
|
+
else:
|
|
102
|
+
# For any other type, convert to string representation
|
|
103
|
+
result_dict = {"data": str(result), "snapshotId": snapshot_id}
|
|
104
|
+
|
|
105
|
+
debug_print(f"Result from get_related_hosts: {result_dict}")
|
|
106
|
+
return result_dict
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
debug_print(f"Error in get_related_hosts: {e}")
|
|
110
|
+
import traceback
|
|
111
|
+
traceback.print_exc(file=sys.stderr)
|
|
112
|
+
return {"error": f"Failed to get related hosts: {str(e)}"}
|
|
113
|
+
|
|
114
|
+
@register_as_tool
|
|
115
|
+
async def get_topology(self,
|
|
116
|
+
include_data: Optional[bool] = False,
|
|
117
|
+
ctx=None) -> Dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Get the infrastructure topology information.
|
|
120
|
+
|
|
121
|
+
This tool retrieves the complete infrastructure topology from Instana, showing how all monitored entities
|
|
122
|
+
are connected. Use this when you need a comprehensive view of your infrastructure's relationships and dependencies.
|
|
123
|
+
|
|
124
|
+
The topology includes nodes (representing entities like hosts, processes, containers) and edges (representing
|
|
125
|
+
connections between entities). This is useful for understanding the overall structure of your environment.
|
|
126
|
+
|
|
127
|
+
For example, use this tool when:
|
|
128
|
+
- You need a complete map of your infrastructure
|
|
129
|
+
- You want to understand how components are connected
|
|
130
|
+
- You're analyzing dependencies between systems
|
|
131
|
+
- You need to visualize your infrastructure's architecture
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
include_data: Whether to include detailed snapshot data in nodes (optional, default: False)
|
|
135
|
+
ctx: The MCP context (optional)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dictionary containing infrastructure topology information with detailed summary or error information
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
debug_print(f"get_topology called - using include_data=False to avoid validation issues")
|
|
142
|
+
|
|
143
|
+
# Try to call the SDK method and handle validation errors
|
|
144
|
+
try:
|
|
145
|
+
result = self.topo_api.get_topology(include_data=False)
|
|
146
|
+
debug_print(f"SDK call successful, processing result")
|
|
147
|
+
except Exception as sdk_error:
|
|
148
|
+
debug_print(f"SDK validation error: {sdk_error}")
|
|
149
|
+
|
|
150
|
+
# If it's a validation error, try to extract useful information from the error
|
|
151
|
+
if "validation error" in str(sdk_error).lower():
|
|
152
|
+
return {
|
|
153
|
+
"error": "SDK validation error occurred",
|
|
154
|
+
"details": str(sdk_error),
|
|
155
|
+
"suggestion": "The API response format may not match the expected SDK model structure. This often happens with complex Kubernetes or cloud infrastructure data.",
|
|
156
|
+
"workaround": "Consider using other topology tools like get_related_hosts with specific snapshot IDs, or check if the include_data parameter affects the response format."
|
|
157
|
+
}
|
|
158
|
+
else:
|
|
159
|
+
# Re-raise if it's not a validation error
|
|
160
|
+
raise sdk_error
|
|
161
|
+
|
|
162
|
+
# Convert the result to a dictionary
|
|
163
|
+
result_dict = None
|
|
164
|
+
|
|
165
|
+
# Try different ways to convert the result
|
|
166
|
+
if hasattr(result, 'to_dict'):
|
|
167
|
+
try:
|
|
168
|
+
result_dict = result.to_dict()
|
|
169
|
+
debug_print("Successfully converted result using to_dict()")
|
|
170
|
+
except Exception as e:
|
|
171
|
+
debug_print(f"to_dict() failed: {e}")
|
|
172
|
+
|
|
173
|
+
if result_dict is None and isinstance(result, dict):
|
|
174
|
+
result_dict = result
|
|
175
|
+
debug_print("Result is already a dictionary")
|
|
176
|
+
|
|
177
|
+
if result_dict is None:
|
|
178
|
+
# Try to extract data from the result object manually
|
|
179
|
+
try:
|
|
180
|
+
if hasattr(result, '__dict__'):
|
|
181
|
+
result_dict = result.__dict__
|
|
182
|
+
debug_print("Extracted data using __dict__")
|
|
183
|
+
else:
|
|
184
|
+
result_dict = {"data": str(result)}
|
|
185
|
+
debug_print("Converted result to string representation")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
debug_print(f"Manual extraction failed: {e}")
|
|
188
|
+
result_dict = {"data": str(result)}
|
|
189
|
+
|
|
190
|
+
# Process the result if we have valid data
|
|
191
|
+
if isinstance(result_dict, dict) and ('nodes' in result_dict or 'data' in result_dict):
|
|
192
|
+
nodes = result_dict.get('nodes', [])
|
|
193
|
+
edges = result_dict.get('edges', [])
|
|
194
|
+
|
|
195
|
+
debug_print(f"Processing {len(nodes)} nodes and {len(edges)} edges")
|
|
196
|
+
|
|
197
|
+
# If we have no nodes but have data, try to extract from data field
|
|
198
|
+
if not nodes and 'data' in result_dict:
|
|
199
|
+
debug_print("No nodes found, checking data field")
|
|
200
|
+
return {
|
|
201
|
+
"summary": {
|
|
202
|
+
"status": "Data retrieved but in unexpected format",
|
|
203
|
+
"dataType": type(result_dict.get('data')).__name__,
|
|
204
|
+
"dataPreview": str(result_dict.get('data'))[:200] + "..." if len(str(result_dict.get('data'))) > 200 else str(result_dict.get('data'))
|
|
205
|
+
},
|
|
206
|
+
"rawDataAvailable": True,
|
|
207
|
+
"note": "Topology data was retrieved but not in the expected nodes/edges format"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Take only first 30 nodes for analysis to avoid token limits
|
|
211
|
+
sample_nodes = nodes[:30] if len(nodes) > 30 else nodes
|
|
212
|
+
sample_edges = edges[:30] if len(edges) > 30 else edges
|
|
213
|
+
|
|
214
|
+
# Count nodes by plugin type from sample
|
|
215
|
+
plugin_counts = {}
|
|
216
|
+
host_info = {}
|
|
217
|
+
kubernetes_resources = {}
|
|
218
|
+
sample_nodes_details = []
|
|
219
|
+
|
|
220
|
+
for node in sample_nodes:
|
|
221
|
+
if not isinstance(node, dict):
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
plugin = node.get('plugin', 'unknown')
|
|
225
|
+
plugin_counts[plugin] = plugin_counts.get(plugin, 0) + 1
|
|
226
|
+
|
|
227
|
+
# Keep minimal node info for sample
|
|
228
|
+
node_label = str(node.get('label', 'unknown'))
|
|
229
|
+
if len(node_label) > 40:
|
|
230
|
+
node_label = node_label[:37] + "..."
|
|
231
|
+
|
|
232
|
+
node_id = str(node.get('id', ''))
|
|
233
|
+
if len(node_id) > 15:
|
|
234
|
+
node_id = node_id[:12] + "..."
|
|
235
|
+
|
|
236
|
+
sample_nodes_details.append({
|
|
237
|
+
'plugin': plugin,
|
|
238
|
+
'label': node_label,
|
|
239
|
+
'id': node_id
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
# Extract host information
|
|
243
|
+
if plugin == 'host':
|
|
244
|
+
label = str(node.get('label', 'unknown'))
|
|
245
|
+
host_info[label] = str(node.get('id', ''))
|
|
246
|
+
|
|
247
|
+
# Group Kubernetes resources
|
|
248
|
+
if plugin.startswith('kubernetes'):
|
|
249
|
+
k8s_type = plugin.replace('kubernetes', '').lower()
|
|
250
|
+
if k8s_type not in kubernetes_resources:
|
|
251
|
+
kubernetes_resources[k8s_type] = 0
|
|
252
|
+
kubernetes_resources[k8s_type] += 1
|
|
253
|
+
|
|
254
|
+
# Estimate total counts based on sample
|
|
255
|
+
sample_size = len(sample_nodes)
|
|
256
|
+
total_size = len(nodes)
|
|
257
|
+
scaling_factor = total_size / sample_size if sample_size > 0 else 1
|
|
258
|
+
|
|
259
|
+
estimated_plugin_counts = {}
|
|
260
|
+
for plugin, count in plugin_counts.items():
|
|
261
|
+
estimated_plugin_counts[plugin] = int(count * scaling_factor)
|
|
262
|
+
|
|
263
|
+
# Create comprehensive summary
|
|
264
|
+
summary = {
|
|
265
|
+
'totalNodes': len(nodes),
|
|
266
|
+
'totalEdges': len(edges),
|
|
267
|
+
'sampleAnalysis': {
|
|
268
|
+
'sampleSize': sample_size,
|
|
269
|
+
'scalingFactor': round(scaling_factor, 2),
|
|
270
|
+
'note': f'Analysis based on first {sample_size} nodes out of {total_size} total'
|
|
271
|
+
},
|
|
272
|
+
'topPluginTypes': dict(list(sorted(estimated_plugin_counts.items(), key=lambda x: x[1], reverse=True))[:10]),
|
|
273
|
+
'infrastructureOverview': {
|
|
274
|
+
'estimatedHosts': int(len(host_info) * scaling_factor),
|
|
275
|
+
'sampleHosts': list(host_info.keys())[:3], # Show first 3 hosts
|
|
276
|
+
'kubernetesTypes': kubernetes_resources,
|
|
277
|
+
'estimatedContainers': int((plugin_counts.get('crio', 0) + plugin_counts.get('containerd', 0) + plugin_counts.get('docker', 0)) * scaling_factor),
|
|
278
|
+
'estimatedProcesses': int(plugin_counts.get('process', 0) * scaling_factor)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# Add edge analysis from sample if available
|
|
283
|
+
if sample_edges:
|
|
284
|
+
edge_types = {}
|
|
285
|
+
for edge in sample_edges:
|
|
286
|
+
if isinstance(edge, dict):
|
|
287
|
+
edge_type = edge.get('type', 'unknown')
|
|
288
|
+
edge_types[edge_type] = edge_types.get(edge_type, 0) + 1
|
|
289
|
+
|
|
290
|
+
if edge_types:
|
|
291
|
+
summary['connectionAnalysis'] = {
|
|
292
|
+
'sampleEdgeTypes': edge_types,
|
|
293
|
+
'sampleEdgesAnalyzed': len(sample_edges)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Return compact summary
|
|
297
|
+
return {
|
|
298
|
+
'summary': summary,
|
|
299
|
+
'sampleNodes': sample_nodes_details[:8], # Just 8 example nodes
|
|
300
|
+
'status': 'success',
|
|
301
|
+
'note': 'Topology data processed successfully with sampling to manage size'
|
|
302
|
+
}
|
|
303
|
+
else:
|
|
304
|
+
return {
|
|
305
|
+
"error": "Unexpected data format",
|
|
306
|
+
"dataType": type(result_dict).__name__,
|
|
307
|
+
"availableKeys": list(result_dict.keys()) if isinstance(result_dict, dict) else "Not a dictionary",
|
|
308
|
+
"suggestion": "The topology data may be in a different format than expected"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
debug_print(f"Error in get_topology: {e}")
|
|
313
|
+
import traceback
|
|
314
|
+
traceback.print_exc(file=sys.stderr)
|
|
315
|
+
return {
|
|
316
|
+
"error": f"Failed to get topology: {str(e)}",
|
|
317
|
+
"errorType": type(e).__name__,
|
|
318
|
+
"suggestion": "This may be due to API response format changes or network issues"
|
|
319
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Instana API Client Module
|
|
3
|
+
|
|
4
|
+
This module provides the base client for interacting with the Instana API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Dict, Any, AsyncIterator
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
|
|
12
|
+
# Registry to store all tools
|
|
13
|
+
MCP_TOOLS = {}
|
|
14
|
+
|
|
15
|
+
def register_as_tool(func):
|
|
16
|
+
"""Decorator to register a method as an MCP tool."""
|
|
17
|
+
MCP_TOOLS[func.__name__] = func
|
|
18
|
+
return func
|
|
19
|
+
|
|
20
|
+
class BaseInstanaClient:
|
|
21
|
+
"""Base client for Instana API with common functionality."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, read_token: str, base_url: str):
|
|
24
|
+
self.read_token = read_token
|
|
25
|
+
self.base_url = base_url
|
|
26
|
+
|
|
27
|
+
def get_headers(self):
|
|
28
|
+
"""Get standard headers for Instana API requests."""
|
|
29
|
+
return {
|
|
30
|
+
"Authorization": f"apiToken {self.read_token}",
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"Accept": "application/json"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async def make_request(self, endpoint: str, params: Dict[str, Any] = None, method: str = "GET", json: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
36
|
+
"""Make a request to the Instana API."""
|
|
37
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
38
|
+
headers = self.get_headers()
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
if method.upper() == "GET":
|
|
42
|
+
response = requests.get(url, headers=headers, params=params, verify=False)
|
|
43
|
+
elif method.upper() == "POST":
|
|
44
|
+
# Use the json parameter if provided, otherwise use params
|
|
45
|
+
data_to_send = json if json is not None else params
|
|
46
|
+
response = requests.post(url, headers=headers, json=data_to_send, verify=False)
|
|
47
|
+
elif method.upper() == "PUT":
|
|
48
|
+
data_to_send = json if json is not None else params
|
|
49
|
+
response = requests.put(url, headers=headers, json=data_to_send, verify=False)
|
|
50
|
+
elif method.upper() == "DELETE":
|
|
51
|
+
response = requests.delete(url, headers=headers, params=params, verify=False)
|
|
52
|
+
else:
|
|
53
|
+
return {"error": f"Unsupported HTTP method: {method}"}
|
|
54
|
+
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
return response.json()
|
|
57
|
+
except requests.exceptions.HTTPError as err:
|
|
58
|
+
print(f"HTTP Error: {err}", file=sys.stderr)
|
|
59
|
+
return {"error": f"HTTP Error: {err}"}
|
|
60
|
+
except requests.exceptions.RequestException as err:
|
|
61
|
+
print(f"Error: {err}", file=sys.stderr)
|
|
62
|
+
return {"error": f"Error: {err}"}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"Unexpected error: {str(e)}", file=sys.stderr)
|
|
65
|
+
return {"error": f"Unexpected error: {str(e)}"}
|
|
66
|
+
|
|
67
|
+
@asynccontextmanager
|
|
68
|
+
async def instana_api_token(read_token: str, base_url: str) -> AsyncIterator[Dict[str, BaseInstanaClient]]:
|
|
69
|
+
"""
|
|
70
|
+
Context manager for creating and managing Instana API clients.
|
|
71
|
+
Returns a dictionary of client instances for different API groups.
|
|
72
|
+
"""
|
|
73
|
+
# Import here to avoid circular imports
|
|
74
|
+
from .infrastructure_mcp_tools import InfrastructureMCPTools
|
|
75
|
+
from .application_mcp_tools import ApplicationClient
|
|
76
|
+
|
|
77
|
+
# Create the standard clients
|
|
78
|
+
infra_client = InfrastructureMCPTools(read_token=read_token, base_url=base_url)
|
|
79
|
+
app_client = ApplicationClient(read_token=read_token, base_url=base_url)
|
|
80
|
+
|
|
81
|
+
# Initialize clients dictionary
|
|
82
|
+
clients = {
|
|
83
|
+
"infrastructure": infra_client,
|
|
84
|
+
"application": app_client,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
yield clients
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(f"Error in Instana API client: {e}", file=sys.stderr)
|
|
91
|
+
finally:
|
|
92
|
+
# Clean up resources if needed
|
|
93
|
+
pass
|