mcp-instana 0.1.0__py3-none-any.whl → 0.2.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.
Files changed (67) hide show
  1. mcp_instana-0.2.0.dist-info/METADATA +1229 -0
  2. mcp_instana-0.2.0.dist-info/RECORD +59 -0
  3. {mcp_instana-0.1.0.dist-info → mcp_instana-0.2.0.dist-info}/WHEEL +1 -1
  4. mcp_instana-0.2.0.dist-info/entry_points.txt +4 -0
  5. mcp_instana-0.1.0.dist-info/LICENSE → mcp_instana-0.2.0.dist-info/licenses/LICENSE.md +3 -3
  6. src/application/__init__.py +1 -0
  7. src/{client/application_alert_config_mcp_tools.py → application/application_alert_config.py} +251 -273
  8. src/application/application_analyze.py +628 -0
  9. src/application/application_catalog.py +155 -0
  10. src/application/application_global_alert_config.py +653 -0
  11. src/{client/application_metrics_mcp_tools.py → application/application_metrics.py} +113 -131
  12. src/{client/application_resources_mcp_tools.py → application/application_resources.py} +131 -151
  13. src/application/application_settings.py +1731 -0
  14. src/application/application_topology.py +111 -0
  15. src/automation/action_catalog.py +416 -0
  16. src/automation/action_history.py +338 -0
  17. src/core/__init__.py +1 -0
  18. src/core/server.py +586 -0
  19. src/core/utils.py +213 -0
  20. src/event/__init__.py +1 -0
  21. src/event/events_tools.py +850 -0
  22. src/infrastructure/__init__.py +1 -0
  23. src/{client/infrastructure_analyze_mcp_tools.py → infrastructure/infrastructure_analyze.py} +207 -206
  24. src/{client/infrastructure_catalog_mcp_tools.py → infrastructure/infrastructure_catalog.py} +197 -265
  25. src/infrastructure/infrastructure_metrics.py +171 -0
  26. src/{client/infrastructure_resources_mcp_tools.py → infrastructure/infrastructure_resources.py} +198 -227
  27. src/{client/infrastructure_topology_mcp_tools.py → infrastructure/infrastructure_topology.py} +110 -109
  28. src/log/__init__.py +1 -0
  29. src/log/log_alert_configuration.py +331 -0
  30. src/prompts/__init__.py +16 -0
  31. src/prompts/application/__init__.py +1 -0
  32. src/prompts/application/application_alerts.py +54 -0
  33. src/prompts/application/application_catalog.py +26 -0
  34. src/prompts/application/application_metrics.py +57 -0
  35. src/prompts/application/application_resources.py +26 -0
  36. src/prompts/application/application_settings.py +75 -0
  37. src/prompts/application/application_topology.py +30 -0
  38. src/prompts/events/__init__.py +1 -0
  39. src/prompts/events/events_tools.py +161 -0
  40. src/prompts/infrastructure/infrastructure_analyze.py +72 -0
  41. src/prompts/infrastructure/infrastructure_catalog.py +53 -0
  42. src/prompts/infrastructure/infrastructure_metrics.py +45 -0
  43. src/prompts/infrastructure/infrastructure_resources.py +74 -0
  44. src/prompts/infrastructure/infrastructure_topology.py +38 -0
  45. src/prompts/settings/__init__.py +0 -0
  46. src/prompts/settings/custom_dashboard.py +157 -0
  47. src/prompts/website/__init__.py +1 -0
  48. src/prompts/website/website_analyze.py +35 -0
  49. src/prompts/website/website_catalog.py +40 -0
  50. src/prompts/website/website_configuration.py +105 -0
  51. src/prompts/website/website_metrics.py +34 -0
  52. src/settings/__init__.py +1 -0
  53. src/settings/custom_dashboard_tools.py +417 -0
  54. src/website/__init__.py +0 -0
  55. src/website/website_analyze.py +433 -0
  56. src/website/website_catalog.py +171 -0
  57. src/website/website_configuration.py +770 -0
  58. src/website/website_metrics.py +241 -0
  59. mcp_instana-0.1.0.dist-info/METADATA +0 -649
  60. mcp_instana-0.1.0.dist-info/RECORD +0 -19
  61. mcp_instana-0.1.0.dist-info/entry_points.txt +0 -3
  62. src/client/What is the sum of queue depth for all q +0 -55
  63. src/client/events_mcp_tools.py +0 -531
  64. src/client/instana_client_base.py +0 -93
  65. src/client/log_alert_configuration_mcp_tools.py +0 -316
  66. src/client/show the top 5 services with the highest +0 -28
  67. src/mcp_server.py +0 -343
@@ -0,0 +1,111 @@
1
+ """
2
+ Application Topology MCP Tools Module
3
+
4
+ This module provides application topology-specific MCP tools for Instana monitoring.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import Any, Dict, Optional
10
+
11
+ from src.core.utils import BaseInstanaClient, register_as_tool
12
+
13
+ try:
14
+ from instana_client.api.application_topology_api import (
15
+ ApplicationTopologyApi,
16
+ )
17
+ from instana_client.api_client import ApiClient
18
+ from instana_client.configuration import Configuration
19
+ except ImportError as e:
20
+ import logging
21
+ import traceback
22
+ logger = logging.getLogger(__name__)
23
+ logger.error(f"Error importing Instana SDK: {e}")
24
+ traceback.print_exc()
25
+ raise
26
+
27
+ # Configure logger for this module
28
+ logger = logging.getLogger(__name__)
29
+
30
+ class ApplicationTopologyMCPTools(BaseInstanaClient):
31
+ """Tools for application topology in Instana MCP."""
32
+
33
+ def __init__(self, read_token: str, base_url: str):
34
+ """Initialize the Application Topology MCP tools client."""
35
+ super().__init__(read_token=read_token, base_url=base_url)
36
+
37
+ try:
38
+
39
+ # Configure the API client with the correct base URL and authentication
40
+ configuration = Configuration()
41
+ configuration.host = base_url
42
+ configuration.api_key['ApiKeyAuth'] = read_token
43
+ configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
44
+
45
+ # Create an API client with this configuration
46
+ api_client = ApiClient(configuration=configuration)
47
+
48
+ # Initialize the Instana SDK's ApplicationTopologyMCPTools with our configured client
49
+ self.topology_api = ApplicationTopologyApi(api_client=api_client)
50
+
51
+ except Exception as e:
52
+ logger.error(f"Error initializing ApplicationTopologyMCPTools: {e}", exc_info=True)
53
+ raise
54
+
55
+ @register_as_tool
56
+ async def get_application_topology(self,
57
+ window_size: Optional[int] = None,
58
+ to_timestamp: Optional[int] = None,
59
+ application_id: Optional[str] = None,
60
+ application_boundary_scope: Optional[str] = None,
61
+ ctx = None) -> Dict[str, Any]:
62
+ """
63
+ Get the service topology from Instana Server.
64
+ This tool retrieves services and connections (call paths) between them for calls in the scope given by the parameters.
65
+
66
+ Args:
67
+ window_size: Size of time window in milliseconds
68
+ to_timestamp: Timestamp since Unix Epoch in milliseconds of the end of the time window
69
+ application_id: Filter by application ID
70
+ application_boundary_scope: Filter by application scope, i.e., INBOUND or ALL. The default value is INBOUND.
71
+ ctx: Context information
72
+
73
+ Returns:
74
+ A dictionary containing the service topology data
75
+ """
76
+
77
+ try:
78
+ logger.debug("Fetching service topology data")
79
+
80
+ # Set default values if not provided
81
+ if not to_timestamp:
82
+ to_timestamp = int(datetime.now().timestamp() * 1000)
83
+
84
+ if not window_size:
85
+ window_size = 3600000 # Default to 1 hour in milliseconds
86
+
87
+ # Call the API
88
+ # Note: The SDK expects parameters in camelCase, but we use snake_case in Python
89
+ # The SDK will handle the conversion
90
+ result = self.topology_api.get_services_map(
91
+ window_size=window_size,
92
+ to=to_timestamp,
93
+ application_id=application_id,
94
+ application_boundary_scope=application_boundary_scope
95
+ )
96
+
97
+ # Ensure we always return a dictionary
98
+ if hasattr(result, "to_dict"):
99
+ result_dict = result.to_dict()
100
+ elif isinstance(result, dict):
101
+ result_dict = result
102
+ else:
103
+ # Convert to dictionary using __dict__ or as a fallback, create a new dict with string representation
104
+ result_dict = getattr(result, "__dict__", {"data": str(result)})
105
+
106
+ logger.debug("Successfully retrieved service topology data")
107
+ return result_dict
108
+
109
+ except Exception as e:
110
+ logger.error(f"Error in get_application_topology: {e}", exc_info=True)
111
+ return {"error": f"Failed to get application topology: {e!s}"}
@@ -0,0 +1,416 @@
1
+ """
2
+ Automation Action CAtalog MCP Tools Module
3
+
4
+ This module provides automation action catalog tools for Instana Automation.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ # Import the necessary classes from the SDK
11
+ try:
12
+ from instana_client.api.action_catalog_api import (
13
+ ActionCatalogApi,
14
+ )
15
+ except ImportError:
16
+ import logging
17
+ logger = logging.getLogger(__name__)
18
+ logger.error("Failed to import application alert configuration API", exc_info=True)
19
+ raise
20
+
21
+ from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
22
+
23
+ # Configure logger for this module
24
+ logger = logging.getLogger(__name__)
25
+
26
+ class ActionCatalogMCPTools(BaseInstanaClient):
27
+ """Tools for application alerts in Instana MCP."""
28
+
29
+ def __init__(self, read_token: str, base_url: str):
30
+ """Initialize the Application Alert MCP tools client."""
31
+ super().__init__(read_token=read_token, base_url=base_url)
32
+
33
+ @register_as_tool
34
+ @with_header_auth(ActionCatalogApi)
35
+ async def get_action_matches(self,
36
+ payload: Union[Dict[str, Any], str],
37
+ target_snapshot_id: Optional[str] = None,
38
+ ctx=None,
39
+ api_client=None) -> Dict[str, Any]:
40
+ """
41
+ Get action matches for a given action search space and target snapshot ID.
42
+ Args:
43
+ Sample payload:
44
+ {
45
+ "name": "CPU spends significant time waiting for input/output",
46
+ "description": "Checks whether the system spends significant time waiting for input/output."
47
+ }
48
+ target_snapshot_id: Optional[str]: The target snapshot ID to get action matches for.
49
+ ctx: Optional[Dict[str, Any]]: The context to get action matches for.
50
+ api_client: Optional[ActionCatalogApi]: The API client to get action matches for.
51
+ Returns:
52
+ Dict[str, Any]: The action matches for the given payload and target snapshot ID.
53
+ """
54
+ try:
55
+
56
+ if not payload:
57
+ return {"error": "payload is required"}
58
+
59
+ # Parse the payload if it's a string
60
+ if isinstance(payload, str):
61
+ logger.debug("Payload is a string, attempting to parse")
62
+ try:
63
+ import json
64
+ try:
65
+ parsed_payload = json.loads(payload)
66
+ logger.debug("Successfully parsed payload as JSON")
67
+ request_body = parsed_payload
68
+ except json.JSONDecodeError as e:
69
+ logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
70
+
71
+ # Try replacing single quotes with double quotes
72
+ fixed_payload = payload.replace("'", "\"")
73
+ try:
74
+ parsed_payload = json.loads(fixed_payload)
75
+ logger.debug("Successfully parsed fixed JSON")
76
+ request_body = parsed_payload
77
+ except json.JSONDecodeError:
78
+ # Try as Python literal
79
+ import ast
80
+ try:
81
+ parsed_payload = ast.literal_eval(payload)
82
+ logger.debug("Successfully parsed payload as Python literal")
83
+ request_body = parsed_payload
84
+ except (SyntaxError, ValueError) as e2:
85
+ logger.debug(f"Failed to parse payload string: {e2}")
86
+ return {"error": f"Invalid payload format: {e2}", "payload": payload}
87
+ except Exception as e:
88
+ logger.debug(f"Error parsing payload string: {e}")
89
+ return {"error": f"Failed to parse payload: {e}", "payload": payload}
90
+ else:
91
+ # If payload is already a dictionary, use it directly
92
+ logger.debug("Using provided payload dictionary")
93
+ request_body = payload
94
+
95
+ # Validate required fields in the payload
96
+ required_fields = ["name"]
97
+ for field in required_fields:
98
+ if field not in request_body:
99
+ logger.warning(f"Missing required field: {field}")
100
+ return {"error": f"Missing required field: {field}"}
101
+
102
+ # Import the ActionSearchSpace class
103
+ try:
104
+ from instana_client.models.action_search_space import (
105
+ ActionSearchSpace,
106
+ )
107
+ logger.debug("Successfully imported ActionSearchSpace")
108
+ except ImportError as e:
109
+ logger.debug(f"Error importing ActionSearchSpace: {e}")
110
+ return {"error": f"Failed to import ActionSearchSpace: {e!s}"}
111
+
112
+ # Create an ActionSearchSpace object from the request body
113
+ try:
114
+ logger.debug(f"Creating ActionSearchSpace with params: {request_body}")
115
+ config_object = ActionSearchSpace(**request_body)
116
+ logger.debug("Successfully created config object")
117
+ except Exception as e:
118
+ logger.debug(f"Error creating ActionSearchSpace: {e}")
119
+ return {"error": f"Failed to create config object: {e!s}"}
120
+
121
+ # Call the get_action_matches method from the SDK
122
+ logger.debug("Calling get_action_matches with config object")
123
+ result = api_client.get_action_matches(
124
+ action_search_space=config_object,
125
+ target_snapshot_id=target_snapshot_id,
126
+ )
127
+
128
+ # Convert the result to a dictionary
129
+ if isinstance(result, list):
130
+ # Convert list of ActionMatch objects to list of dictionaries
131
+ result_dict = []
132
+ for action_match in result:
133
+ try:
134
+ if hasattr(action_match, 'to_dict'):
135
+ result_dict.append(action_match.to_dict())
136
+ else:
137
+ result_dict.append(action_match)
138
+ except Exception as e:
139
+ logger.warning(f"Failed to convert action match to dict: {e}")
140
+ # Add a fallback representation
141
+ result_dict.append({
142
+ "error": f"Failed to serialize action match: {e}",
143
+ "raw_data": str(action_match)
144
+ })
145
+
146
+ logger.debug(f"Result from get_action_matches: {result_dict}")
147
+ return {
148
+ "success": True,
149
+ "message": "Action matches retrieved successfully",
150
+ "data": result_dict,
151
+ "count": len(result_dict)
152
+ }
153
+ elif hasattr(result, 'to_dict'):
154
+ try:
155
+ result_dict = result.to_dict()
156
+ logger.debug(f"Result from get_action_matches: {result_dict}")
157
+ return {
158
+ "success": True,
159
+ "message": "Action match retrieved successfully",
160
+ "data": result_dict
161
+ }
162
+ except Exception as e:
163
+ logger.warning(f"Failed to convert result to dict: {e}")
164
+ return {
165
+ "success": False,
166
+ "message": "Failed to serialize result",
167
+ "error": str(e),
168
+ "raw_data": str(result)
169
+ }
170
+ else:
171
+ # If it's already a dict or another format, use it as is
172
+ result_dict = result or {
173
+ "success": True,
174
+ "message": "Get action matches"
175
+ }
176
+ logger.debug(f"Result from get_action_matches: {result_dict}")
177
+ return result_dict
178
+ except Exception as e:
179
+ logger.error(f"Error in get_action_matches: {e}")
180
+ return {"error": f"Failed to get action matches: {e!s}"}
181
+
182
+ @register_as_tool
183
+ @with_header_auth(ActionCatalogApi)
184
+ async def get_actions(self,
185
+ page: Optional[int] = None,
186
+ page_size: Optional[int] = None,
187
+ search: Optional[str] = None,
188
+ types: Optional[List[str]] = None,
189
+ order_by: Optional[str] = None,
190
+ order_direction: Optional[str] = None,
191
+ ctx=None,
192
+ api_client=None) -> Dict[str, Any]:
193
+ """
194
+ Get a list of available automation actions from the action catalog.
195
+
196
+ Args:
197
+ page: Page number for pagination (optional)
198
+ page_size: Number of actions per page (optional)
199
+ search: Search term to filter actions by name or description (optional)
200
+ types: List of action types to filter by (optional)
201
+ order_by: Field to order results by (optional)
202
+ order_direction: Sort direction ('asc' or 'desc') (optional)
203
+ ctx: Optional[Dict[str, Any]]: The context for the action retrieval
204
+ api_client: Optional[ActionCatalogApi]: The API client for action catalog
205
+
206
+ Returns:
207
+ Dict[str, Any]: The list of available automation actions
208
+ """
209
+ try:
210
+ logger.debug("get_actions called")
211
+
212
+ # Call the get_actions method from the SDK
213
+ result = api_client.get_actions(
214
+ page=page,
215
+ page_size=page_size,
216
+ search=search,
217
+ types=types,
218
+ order_by=order_by,
219
+ order_direction=order_direction
220
+ )
221
+
222
+ # Convert the result to a dictionary
223
+ if hasattr(result, 'to_dict'):
224
+ result_dict = result.to_dict()
225
+ else:
226
+ # If it's already a dict or another format, use it as is
227
+ result_dict = result or {
228
+ "success": True,
229
+ "message": "Actions retrieved successfully"
230
+ }
231
+
232
+ logger.debug(f"Result from get_actions: {result_dict}")
233
+ return result_dict
234
+
235
+ except Exception as e:
236
+ logger.error(f"Error in get_actions: {e}")
237
+ return {"error": f"Failed to get actions: {e!s}"}
238
+
239
+ @register_as_tool
240
+ @with_header_auth(ActionCatalogApi)
241
+ async def get_action_details(self,
242
+ action_id: str,
243
+ ctx=None,
244
+ api_client=None) -> Dict[str, Any]:
245
+ """
246
+ Get detailed information about a specific automation action by ID.
247
+
248
+ Args:
249
+ action_id: The unique identifier of the action (required)
250
+ ctx: Optional[Dict[str, Any]]: The context for the action details retrieval
251
+ api_client: Optional[ActionCatalogApi]: The API client for action catalog
252
+
253
+ Returns:
254
+ Dict[str, Any]: The detailed information about the automation action
255
+ """
256
+ try:
257
+ if not action_id:
258
+ return {"error": "action_id is required"}
259
+
260
+ logger.debug(f"get_action_details called with action_id: {action_id}")
261
+
262
+ # Call the get_action method from the SDK
263
+ result = api_client.get_action(action_id=action_id)
264
+
265
+ # Convert the result to a dictionary
266
+ if hasattr(result, 'to_dict'):
267
+ result_dict = result.to_dict()
268
+ else:
269
+ # If it's already a dict or another format, use it as is
270
+ result_dict = result or {
271
+ "success": True,
272
+ "message": "Action details retrieved successfully"
273
+ }
274
+
275
+ logger.debug(f"Result from get_action: {result_dict}")
276
+ return result_dict
277
+
278
+ except Exception as e:
279
+ logger.error(f"Error in get_action_details: {e}")
280
+ return {"error": f"Failed to get action details: {e!s}"}
281
+
282
+ @register_as_tool
283
+ @with_header_auth(ActionCatalogApi)
284
+ async def search_actions(self,
285
+ search: str,
286
+ page: Optional[int] = None,
287
+ page_size: Optional[int] = None,
288
+ types: Optional[List[str]] = None,
289
+ order_by: Optional[str] = None,
290
+ order_direction: Optional[str] = None,
291
+ ctx=None,
292
+ api_client=None) -> Dict[str, Any]:
293
+ """
294
+ Search for automation actions in the action catalog.
295
+
296
+ Args:
297
+ search: Search term to find actions by name, description, or other attributes (required)
298
+ page: Page number for pagination (optional)
299
+ page_size: Number of actions per page (optional)
300
+ types: List of action types to filter by (optional)
301
+ order_by: Field to order results by (optional)
302
+ order_direction: Sort direction ('asc' or 'desc') (optional)
303
+ ctx: Optional[Dict[str, Any]]: The context for the action search
304
+ api_client: Optional[ActionCatalogApi]: The API client for action catalog
305
+
306
+ Returns:
307
+ Dict[str, Any]: The search results for automation actions
308
+ """
309
+ try:
310
+ if not search:
311
+ return {"error": "search parameter is required"}
312
+
313
+ logger.debug(f"search_actions called with search: {search}")
314
+
315
+ # Call the search_actions method from the SDK
316
+ result = api_client.search_actions(
317
+ search=search,
318
+ page=page,
319
+ page_size=page_size,
320
+ types=types,
321
+ order_by=order_by,
322
+ order_direction=order_direction
323
+ )
324
+
325
+ # Convert the result to a dictionary
326
+ if hasattr(result, 'to_dict'):
327
+ result_dict = result.to_dict()
328
+ else:
329
+ # If it's already a dict or another format, use it as is
330
+ result_dict = result or {
331
+ "success": True,
332
+ "message": "Action search completed successfully"
333
+ }
334
+
335
+ logger.debug(f"Result from search_actions: {result_dict}")
336
+ return result_dict
337
+
338
+ except Exception as e:
339
+ logger.error(f"Error in search_actions: {e}")
340
+ return {"error": f"Failed to search actions: {e!s}"}
341
+
342
+ @register_as_tool
343
+ @with_header_auth(ActionCatalogApi)
344
+ async def get_action_types(self,
345
+ ctx=None,
346
+ api_client=None) -> Dict[str, Any]:
347
+ """
348
+ Get a list of available action types in the action catalog.
349
+
350
+ Args:
351
+ ctx: Optional[Dict[str, Any]]: The context for the action types retrieval
352
+ api_client: Optional[ActionCatalogApi]: The API client for action catalog
353
+
354
+ Returns:
355
+ Dict[str, Any]: The list of available action types
356
+ """
357
+ try:
358
+ logger.debug("get_action_types called")
359
+
360
+ # Call the get_action_types method from the SDK
361
+ result = api_client.get_action_types()
362
+
363
+ # Convert the result to a dictionary
364
+ if hasattr(result, 'to_dict'):
365
+ result_dict = result.to_dict()
366
+ else:
367
+ # If it's already a dict or another format, use it as is
368
+ result_dict = result or {
369
+ "success": True,
370
+ "message": "Action types retrieved successfully"
371
+ }
372
+
373
+ logger.debug(f"Result from get_action_types: {result_dict}")
374
+ return result_dict
375
+
376
+ except Exception as e:
377
+ logger.error(f"Error in get_action_types: {e}")
378
+ return {"error": f"Failed to get action types: {e!s}"}
379
+
380
+ @register_as_tool
381
+ @with_header_auth(ActionCatalogApi)
382
+ async def get_action_categories(self,
383
+ ctx=None,
384
+ api_client=None) -> Dict[str, Any]:
385
+ """
386
+ Get a list of available action categories in the action catalog.
387
+
388
+ Args:
389
+ ctx: Optional[Dict[str, Any]]: The context for the action categories retrieval
390
+ api_client: Optional[ActionCatalogApi]: The API client for action catalog
391
+
392
+ Returns:
393
+ Dict[str, Any]: The list of available action categories
394
+ """
395
+ try:
396
+ logger.debug("get_action_categories called")
397
+
398
+ # Call the get_action_categories method from the SDK
399
+ result = api_client.get_action_categories()
400
+
401
+ # Convert the result to a dictionary
402
+ if hasattr(result, 'to_dict'):
403
+ result_dict = result.to_dict()
404
+ else:
405
+ # If it's already a dict or another format, use it as is
406
+ result_dict = result or {
407
+ "success": True,
408
+ "message": "Action categories retrieved successfully"
409
+ }
410
+
411
+ logger.debug(f"Result from get_action_categories: {result_dict}")
412
+ return result_dict
413
+
414
+ except Exception as e:
415
+ logger.error(f"Error in get_action_categories: {e}")
416
+ return {"error": f"Failed to get action categories: {e!s}"}