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
@@ -4,27 +4,26 @@ Infrastructure Catalog MCP Tools Module
4
4
  This module provides infrastructure catalog-specific MCP tools for Instana monitoring.
5
5
  """
6
6
 
7
- import sys
8
- import traceback
9
- from typing import Dict, Any, Optional, List, Union
10
- from datetime import datetime
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
11
9
 
12
10
  # Import the necessary classes from the SDK
13
11
  try:
14
- from instana_client.api.infrastructure_catalog_api import InfrastructureCatalogApi
12
+ from instana_client.api.infrastructure_catalog_api import (
13
+ InfrastructureCatalogApi,
14
+ )
15
15
  from instana_client.api_client import ApiClient
16
16
  from instana_client.configuration import Configuration
17
17
  except ImportError as e:
18
- print(f"Error importing Instana SDK: {e}", file=sys.stderr)
19
- traceback.print_exc(file=sys.stderr)
18
+ import logging
19
+ logger = logging.getLogger(__name__)
20
+ logger.error(f"Error importing Instana SDK: {e}", exc_info=True)
20
21
  raise
21
22
 
22
- from .instana_client_base import BaseInstanaClient, register_as_tool
23
+ from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
23
24
 
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)
25
+ # Configure logger for this module
26
+ logger = logging.getLogger(__name__)
28
27
 
29
28
  class InfrastructureCatalogMCPTools(BaseInstanaClient):
30
29
  """Tools for infrastructure catalog in Instana MCP."""
@@ -32,163 +31,148 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
32
31
  def __init__(self, read_token: str, base_url: str):
33
32
  """Initialize the Infrastructure Catalog MCP tools client."""
34
33
  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 InfrastructureCatalogApi with our configured client
48
- self.catalog_api = InfrastructureCatalogApi(api_client=api_client)
49
- except Exception as e:
50
- debug_print(f"Error initializing InfrastructureCatalogApi: {e}")
51
- traceback.print_exc(file=sys.stderr)
52
- raise
53
-
34
+
54
35
  @register_as_tool
55
- async def get_available_payload_keys_by_plugin_id(self,
56
- plugin_id: str,
57
- ctx=None) -> Dict[str, Any]:
58
- """
59
- Get available payload keys for a specific plugin in Instana. This tool retrieves the list of payload keys that can be used to access detailed monitoring data
60
- for a particular plugin type. Use this when you need to understand what data is available for a specific entity type, want to explore the monitoring capabilities
36
+ @with_header_auth(InfrastructureCatalogApi)
37
+ async def get_available_payload_keys_by_plugin_id(self,
38
+ plugin_id: str,
39
+ ctx=None, api_client=None) -> Dict[str, Any]:
40
+ """
41
+ Get available payload keys for a specific plugin in Instana. This tool retrieves the list of payload keys that can be used to access detailed monitoring data
42
+ for a particular plugin type. Use this when you need to understand what data is available for a specific entity type, want to explore the monitoring capabilities
61
43
  for a plugin, or need to find the correct payload key for accessing specific metrics or configuration data. This is particularly useful for preparing detailed
62
- queries, understanding available monitoring data structures, or when building custom dashboards or integrations. For example, use this tool when asked about
63
- 'what data is available for Java processes', 'payload keys for Kubernetes', 'what metrics can I access for MySQL', or when someone wants to
44
+ queries, understanding available monitoring data structures, or when building custom dashboards or integrations. For example, use this tool when asked about
45
+ 'what data is available for Java processes', 'payload keys for Kubernetes', 'what metrics can I access for MySQL', or when someone wants to
64
46
  'find out what monitoring data is collected for a specific technology'.
65
-
47
+
66
48
  Args:
67
49
  plugin_id: The ID of the plugin to get payload keys for
68
50
  ctx: The MCP context (optional)
69
-
51
+
70
52
  Returns:
71
53
  Dictionary containing payload keys or error information
72
54
  """
73
55
  try:
74
- debug_print(f"get_available_payload_keys_by_plugin_id called with plugin_id={plugin_id}")
75
-
56
+ logger.debug(f"get_available_payload_keys_by_plugin_id called with plugin_id={plugin_id}")
57
+
76
58
  if not plugin_id:
77
59
  return {"error": "plugin_id parameter is required"}
78
-
60
+
79
61
  # Try using the standard SDK method
80
62
  try:
81
63
  # Call the get_available_payload_keys_by_plugin_id method from the SDK
82
- result = self.catalog_api.get_available_payload_keys_by_plugin_id(
64
+ result = api_client.get_available_payload_keys_by_plugin_id(
83
65
  plugin_id=plugin_id
84
66
  )
85
-
67
+
86
68
  # Convert the result to a dictionary
87
69
  if hasattr(result, 'to_dict'):
88
70
  result_dict = result.to_dict()
89
71
  elif isinstance(result, dict):
90
72
  result_dict = result
91
73
  elif isinstance(result, list):
92
- result_dict = [item.to_dict() if hasattr(item, 'to_dict') else item for item in result]
74
+ # Wrap list in a dictionary to match return type
75
+ items = [item.to_dict() if hasattr(item, 'to_dict') else item for item in result]
76
+ result_dict = {"payload_keys": items, "plugin_id": plugin_id}
93
77
  elif isinstance(result, str):
94
78
  # Handle string response (special case for some plugins like db2Database)
95
- debug_print(f"Received string response for plugin_id={plugin_id}: {result}")
79
+ logger.debug(f"Received string response for plugin_id={plugin_id}: {result}")
96
80
  result_dict = {"message": result, "plugin_id": plugin_id}
97
81
  else:
98
82
  # For any other type, convert to string representation
99
83
  result_dict = {"data": str(result), "plugin_id": plugin_id}
100
-
101
- debug_print(f"Result from get_available_payload_keys_by_plugin_id: {result_dict}")
84
+
85
+ logger.debug(f"Result from get_available_payload_keys_by_plugin_id: {result_dict}")
102
86
  return result_dict
103
-
87
+
104
88
  except Exception as sdk_error:
105
- debug_print(f"SDK method failed: {sdk_error}, trying fallback")
106
-
89
+ logger.error(f"SDK method failed: {sdk_error}, trying fallback")
90
+
107
91
  # Use the without_preload_content version to get the raw response
108
92
  try:
109
- response_data = self.catalog_api.get_available_payload_keys_by_plugin_id_without_preload_content(
93
+ response_data = api_client.get_available_payload_keys_by_plugin_id_without_preload_content(
110
94
  plugin_id=plugin_id
111
95
  )
112
-
96
+
113
97
  # Check if the response was successful
114
98
  if response_data.status != 200:
115
99
  error_message = f"Failed to get payload keys: HTTP {response_data.status}"
116
- debug_print(error_message)
100
+ logger.debug(error_message)
117
101
  return {"error": error_message}
118
-
102
+
119
103
  # Read the response content
120
104
  response_text = response_data.data.decode('utf-8')
121
-
105
+
122
106
  # Try to parse as JSON first
123
107
  import json
124
108
  try:
125
109
  result_dict = json.loads(response_text)
126
- debug_print(f"Result from fallback method (JSON): {result_dict}")
110
+ logger.debug(f"Result from fallback method (JSON): {result_dict}")
127
111
  return result_dict
128
112
  except json.JSONDecodeError:
129
113
  # If not valid JSON, return as string
130
- debug_print(f"Result from fallback method (string): {response_text}")
114
+ logger.debug(f"Result from fallback method (string): {response_text}")
131
115
  return {"message": response_text, "plugin_id": plugin_id}
132
-
116
+
133
117
  except Exception as fallback_error:
134
- debug_print(f"Fallback method failed: {fallback_error}")
118
+ logger.warning(f"Fallback method failed: {fallback_error}")
135
119
  raise
136
-
120
+
137
121
  except Exception as e:
138
- debug_print(f"Error in get_available_payload_keys_by_plugin_id: {e}")
139
- traceback.print_exc(file=sys.stderr)
140
- return {"error": f"Failed to get payload keys: {str(e)}", "plugin_id": plugin_id}
122
+ logger.error(f"Error in get_available_payload_keys_by_plugin_id: {e}", exc_info=True)
123
+ return {"error": f"Failed to get payload keys: {e!s}", "plugin_id": plugin_id}
141
124
 
142
125
 
143
126
  @register_as_tool
144
- async def get_infrastructure_catalog_metrics(self,
145
- plugin: str,
146
- filter: Optional[str] = None,
147
- ctx=None) -> List[str]:
127
+ @with_header_auth(InfrastructureCatalogApi)
128
+ async def get_infrastructure_catalog_metrics(self,
129
+ plugin: str,
130
+ filter: Optional[str] = None,
131
+ ctx=None, api_client=None) -> List[str]:
148
132
  """
149
- Get metric catalog for a specific plugin in Instana. This tool retrieves all available metric definitions for a requested plugin type.
133
+ Get metric catalog for a specific plugin in Instana. This tool retrieves all available metric definitions for a requested plugin type.
150
134
  Use this when you need to understand what metrics are available for a specific technology, want to explore the monitoring capabilities for a plugin,
151
135
  or need to find the correct metric names for queries or dashboards. This is particularly useful for building custom dashboards, setting up alerts based on specific metrics,
152
136
  or understanding the monitoring depth for a particular technology. For example, use this tool when asked about 'what metrics are available for hosts',
153
137
  'JVM metrics catalog', 'available metrics for Kubernetes', or when someone wants to 'see all metrics for a database'.
154
-
138
+
155
139
  Returns the first 50 metrics to keep the response manageable.
156
-
140
+
157
141
  Args:
158
142
  plugin: The plugin ID to get metrics for
159
143
  filter: Filter to restrict returned metric definitions ('custom' or 'builtin')
160
144
  ctx: The MCP context (optional)
161
-
145
+
162
146
  Returns:
163
147
  List of metric names (strings) - limited to first 50 metrics
164
148
  """
165
149
  try:
166
- debug_print(f"get_infrastructure_catalog_metrics called with plugin={plugin}, filter={filter}")
167
-
150
+ logger.debug(f"get_infrastructure_catalog_metrics called with plugin={plugin}, filter={filter}")
151
+
168
152
  if not plugin:
169
153
  return ["Error: plugin parameter is required"]
170
-
154
+
171
155
  # Call the get_infrastructure_catalog_metrics method from the SDK
172
- result = self.catalog_api.get_infrastructure_catalog_metrics(
156
+ result = api_client.get_infrastructure_catalog_metrics(
173
157
  plugin=plugin,
174
158
  filter=filter # Pass the filter parameter to the SDK
175
159
  )
176
-
160
+
177
161
  # Handle different response types
178
162
  if isinstance(result, list):
179
163
  # If it's a list of metric names, limit to first 50
180
164
  limited_metrics = result[:50]
181
- debug_print(f"Received {len(result)} metrics for plugin {plugin}, returning first {len(limited_metrics)}")
165
+ logger.debug(f"Received {len(result)} metrics for plugin {plugin}, returning first {len(limited_metrics)}")
182
166
  return limited_metrics
183
-
167
+
184
168
  elif hasattr(result, 'to_dict'):
185
169
  # If it's an SDK object with to_dict method
186
170
  result_dict = result.to_dict()
187
-
171
+
188
172
  # Check if the dict contains a list of metrics
189
173
  if isinstance(result_dict, list):
190
174
  limited_metrics = result_dict[:50]
191
- debug_print(f"Received {len(result_dict)} metrics for plugin {plugin}, returning first {len(limited_metrics)}")
175
+ logger.debug(f"Received {len(result_dict)} metrics for plugin {plugin}, returning first {len(limited_metrics)}")
192
176
  return limited_metrics
193
177
  elif isinstance(result_dict, dict):
194
178
  # Try to extract metric names from dict structure
@@ -201,38 +185,38 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
201
185
  return [f"Unable to parse metrics for plugin {plugin}"]
202
186
  else:
203
187
  # For any other format
204
- debug_print(f"Unexpected result type for plugin {plugin}: {type(result)}")
188
+ logger.debug(f"Unexpected result type for plugin {plugin}: {type(result)}")
205
189
  return [f"Unexpected response format for plugin {plugin}"]
206
-
190
+
207
191
  except Exception as e:
208
- debug_print(f"Error in get_infrastructure_catalog_metrics: {e}")
209
- traceback.print_exc(file=sys.stderr)
210
- return [f"Error: Failed to get metric catalog for plugin '{plugin}': {str(e)}"]
192
+ logger.error(f"Error in get_infrastructure_catalog_metrics: {e}", exc_info=True)
193
+ return [f"Error: Failed to get metric catalog for plugin '{plugin}': {e!s}"]
194
+
211
195
 
212
-
213
196
  @register_as_tool
214
- async def get_infrastructure_catalog_plugins(self, ctx=None) -> Dict[str, Any]:
197
+ @with_header_auth(InfrastructureCatalogApi)
198
+ async def get_infrastructure_catalog_plugins(self, ctx=None, api_client=None) -> Dict[str, Any]:
215
199
  """
216
200
  Get plugin catalog from Instana. This tool retrieves all available plugin IDs for your monitored system, showing what types of entities Instana is monitoring in your environment.
217
201
  Use this when you need to understand what technologies are being monitored, want to explore the monitoring capabilities of your Instana installation,
218
- or need to find the correct plugin ID for other API calls. This is particularly useful for discovering what entity types are available for querying,
219
- understanding your monitoring coverage, or preparing for more detailed data retrieval. For example, use this tool when asked about
202
+ or need to find the correct plugin ID for other API calls. This is particularly useful for discovering what entity types are available for querying,
203
+ understanding your monitoring coverage, or preparing for more detailed data retrieval. For example, use this tool when asked about
220
204
  'what technologies are monitored', 'available plugins in Instana', 'list of monitored entity types', or when someone wants to 'see what kinds of systems Instana is tracking'.
221
-
205
+
222
206
  Returns the first 50 plugins to keep the response manageable.
223
-
207
+
224
208
  Args:
225
209
  ctx: The MCP context (optional)
226
-
210
+
227
211
  Returns:
228
212
  Dictionary with plugin list and metadata
229
213
  """
230
214
  try:
231
- debug_print("get_infrastructure_catalog_plugins called")
232
-
215
+ logger.debug("get_infrastructure_catalog_plugins called")
216
+
233
217
  # Call the get_infrastructure_catalog_plugins method from the SDK
234
- result = self.catalog_api.get_infrastructure_catalog_plugins()
235
-
218
+ result = api_client.get_infrastructure_catalog_plugins()
219
+
236
220
  # Handle the result based on its type
237
221
  if isinstance(result, list):
238
222
  # Extract just the plugin IDs from the list of dictionaries
@@ -242,9 +226,9 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
242
226
  plugin_ids.append(item['plugin'])
243
227
  elif hasattr(item, 'plugin'):
244
228
  plugin_ids.append(item.plugin)
245
-
246
- debug_print(f"Extracted {len(plugin_ids)} plugin IDs from response (limited to top 50)")
247
-
229
+
230
+ logger.debug(f"Extracted {len(plugin_ids)} plugin IDs from response (limited to top 50)")
231
+
248
232
  # Return structured response that encourages listing
249
233
  return {
250
234
  "message": f"Found {len(result)} total plugins. Showing first {len(plugin_ids)} plugins:",
@@ -253,7 +237,7 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
253
237
  "showing": len(plugin_ids),
254
238
  "note": "These are the plugin IDs for different technologies monitored by Instana"
255
239
  }
256
-
240
+
257
241
  elif hasattr(result, 'to_dict'):
258
242
  # If it's an SDK object with to_dict method
259
243
  result_dict = result.to_dict()
@@ -272,190 +256,141 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
272
256
  else:
273
257
  # For any other format
274
258
  return {"error": "Unable to parse response"}
275
-
259
+
276
260
  except Exception as e:
277
- debug_print(f"Error in get_infrastructure_catalog_plugins: {e}")
278
- traceback.print_exc(file=sys.stderr)
279
- return {"error": f"Failed to get plugin catalog: {str(e)}"}
261
+ logger.error(f"Error in get_infrastructure_catalog_plugins: {e}", exc_info=True)
262
+ return {"error": f"Failed to get plugin catalog: {e!s}"}
280
263
 
281
264
 
282
265
 
283
266
  @register_as_tool
284
- async def get_infrastructure_catalog_plugins_with_custom_metrics(self, ctx=None) -> Dict[str, Any]:
267
+ @with_header_auth(InfrastructureCatalogApi)
268
+ async def get_infrastructure_catalog_plugins_with_custom_metrics(self, ctx=None, api_client=None) -> Dict[str, Any] | List[Dict[str, Any]]:
285
269
  """
286
270
  Get all plugins with custom metrics catalog from Instana. This tool retrieves information about which entity types (plugins) in your environment have custom metrics configured.
287
- Use this when you need to identify which technologies have custom monitoring metrics defined, want to explore custom monitoring capabilities,
288
- or need to find entities with extended metrics beyond the standard set. This is particularly useful for understanding your custom monitoring setup,
271
+ Use this when you need to identify which technologies have custom monitoring metrics defined, want to explore custom monitoring capabilities,
272
+ or need to find entities with extended metrics beyond the standard set. This is particularly useful for understanding your custom monitoring setup,
289
273
  identifying opportunities for additional custom metrics, or troubleshooting issues with custom metric collection. For example, use this tool when asked about 'which systems have custom metrics',
290
274
  'custom monitoring configuration', 'plugins with extended metrics', or when someone wants to 'find out where custom metrics are being collected'.
291
-
275
+
292
276
  Args:
293
277
  ctx: The MCP context (optional)
294
-
278
+
295
279
  Returns:
296
280
  Dictionary containing plugins with custom metrics or error information
297
281
  """
298
282
  try:
299
- debug_print("get_infrastructure_catalog_plugins_with_custom_metrics called")
300
-
283
+ logger.debug("get_infrastructure_catalog_plugins_with_custom_metrics called")
284
+
301
285
  # Call the get_infrastructure_catalog_plugins_with_custom_metrics method from the SDK
302
- result = self.catalog_api.get_infrastructure_catalog_plugins_with_custom_metrics()
303
-
286
+ result = api_client.get_infrastructure_catalog_plugins_with_custom_metrics()
287
+
304
288
  # Convert the result to a dictionary
305
289
  if hasattr(result, 'to_dict'):
306
290
  result_dict = result.to_dict()
291
+ elif isinstance(result, list):
292
+ # Wrap list in a dictionary to match return type
293
+ items = [item.to_dict() if hasattr(item, 'to_dict') else item for item in result]
294
+ result_dict = {"plugins_with_custom_metrics": items}
307
295
  else:
308
- # If it's already a list or another format, convert it appropriately
309
- if isinstance(result, list):
310
- result_dict = [item.to_dict() if hasattr(item, 'to_dict') else item for item in result]
311
- else:
312
- result_dict = result
313
-
314
- debug_print(f"Result from get_infrastructure_catalog_plugins_with_custom_metrics: {result_dict}")
296
+ # Ensure we always return a dictionary
297
+ result_dict = result if isinstance(result, dict) else {"data": result}
298
+
299
+ logger.debug(f"Result from get_infrastructure_catalog_plugins_with_custom_metrics: {result_dict}")
315
300
  return result_dict
316
301
  except Exception as e:
317
- debug_print(f"Error in get_infrastructure_catalog_plugins_with_custom_metrics: {e}")
318
- traceback.print_exc(file=sys.stderr)
319
- return {"error": f"Failed to get plugins with custom metrics: {str(e)}"}
320
-
321
- @register_as_tool
322
- async def get_infrastructure_catalog_search_fields(self, ctx=None) -> Dict[str, Any]:
323
- """
324
- Get search field catalog from Instana. This tool retrieves all available search keywords and fields that can be used in dynamic focus queries for infrastructure monitoring.
325
- Use this when you need to understand what search criteria are available, want to build complex queries to filter entities, or need to find the correct search syntax for specific entity properties.
326
- This is particularly useful for constructing advanced search queries, understanding available filtering options, or discovering how to target specific entities in your environment.
327
- For example, use this tool when asked about 'what search fields are available', 'how to filter hosts by property', 'search syntax for Kubernetes pods', or when someone wants to 'learn how to build complex entity queries'.
328
-
329
- This endpoint retrieves all available search keywords for dynamic focus queries.
330
-
331
- Args:
332
- ctx: The MCP context (optional)
333
-
334
- Returns:
335
- Dictionary containing search field keywords or error information
336
- """
337
- try:
338
- debug_print("get_infrastructure_catalog_search_fields called")
339
-
340
- # Call the get_infrastructure_catalog_search_fields method from the SDK
341
- result = self.catalog_api.get_infrastructure_catalog_search_fields()
342
-
343
- # Convert the result to a dictionary
344
- if hasattr(result, 'to_dict'):
345
- full_result = result.to_dict()
346
- else:
347
- # If it's already a list or another format, convert it appropriately
348
- if isinstance(result, list):
349
- full_result = [item.to_dict() if hasattr(item, 'to_dict') else item for item in result]
350
- else:
351
- full_result = result
352
-
353
- debug_print(f"Full result from get_infrastructure_catalog_search_fields: {full_result}")
354
-
355
- # Extract only the keywords from the search fields
356
- keywords = self._extract_search_field_keywords(full_result)
357
-
358
- return {
359
- "keywords": keywords,
360
- "count": len(keywords),
361
- "summary": "List of available search keywords for dynamic focus queries"
362
- }
363
- except Exception as e:
364
- debug_print(f"Error in get_infrastructure_catalog_search_fields: {e}")
365
- traceback.print_exc(file=sys.stderr)
366
- return {"error": f"Failed to get search field catalog: {str(e)}"}
302
+ logger.error(f"Error in get_infrastructure_catalog_plugins_with_custom_metrics: {e}", exc_info=True)
303
+ return {"error": f"Failed to get plugins with custom metrics: {e!s}"}
304
+
367
305
 
368
-
369
306
  @register_as_tool
370
- async def get_tag_catalog(self, plugin: str, ctx=None) -> Dict[str, Any]:
307
+ @with_header_auth(InfrastructureCatalogApi)
308
+ async def get_tag_catalog(self, plugin: str, ctx=None, api_client=None) -> Dict[str, Any]:
371
309
  """
372
310
  Get available tags for a particular plugin. This tool retrieves the tag catalog filtered by plugin.
373
-
311
+
374
312
  Args:
375
313
  plugin: The plugin name (e.g., 'host', 'jvm', 'openTelemetry')
376
314
  ctx: The MCP context (optional)
377
-
315
+
378
316
  Returns:
379
317
  Dictionary containing available tags for the plugin or error information
380
318
  """
381
319
  try:
382
- debug_print(f"get_tag_catalog called with plugin={plugin}")
383
-
320
+ logger.debug(f"get_tag_catalog called with plugin={plugin}")
321
+
384
322
  if not plugin:
385
323
  return {"error": "plugin parameter is required"}
386
-
324
+
387
325
  # Try calling the SDK method first
388
326
  try:
389
327
  # Call the get_tag_catalog method from the SDK
390
- result = self.catalog_api.get_tag_catalog(
328
+ result = api_client.get_tag_catalog(
391
329
  plugin=plugin
392
330
  )
393
-
331
+
394
332
  # Convert the result to a dictionary
395
333
  if hasattr(result, 'to_dict'):
396
334
  result_dict = result.to_dict()
397
335
  else:
398
336
  # If it's already a dict or another format, use it as is
399
337
  result_dict = result
400
-
401
- debug_print(f"Result from get_tag_catalog: {result_dict}")
338
+
339
+ logger.debug(f"Result from get_tag_catalog: {result_dict}")
402
340
  return result_dict
403
-
341
+
404
342
  except Exception as sdk_error:
405
- debug_print(f"SDK method failed: {sdk_error}, trying with custom headers")
406
-
343
+ logger.error(f"SDK method failed: {sdk_error}, trying with custom headers")
344
+
407
345
  # Check if it's a 406 error
408
346
  is_406_error = False
409
- if hasattr(sdk_error, 'status') and sdk_error.status == 406:
347
+ if hasattr(sdk_error, 'status') and sdk_error.status == 406 or "406" in str(sdk_error) and "Not Acceptable" in str(sdk_error):
410
348
  is_406_error = True
411
- elif "406" in str(sdk_error) and "Not Acceptable" in str(sdk_error):
412
- is_406_error = True
413
-
349
+
414
350
  if is_406_error:
415
351
  # Try using the SDK's method with custom headers
416
352
  # The SDK should have a method that allows setting custom headers
417
353
  custom_headers = {
418
354
  "Accept": "*/*" # More permissive Accept header
419
355
  }
420
-
356
+
421
357
  # Use the without_preload_content version to get the raw response
422
- response_data = self.catalog_api.get_tag_catalog_without_preload_content(
358
+ response_data = api_client.get_tag_catalog_without_preload_content(
423
359
  plugin=plugin,
424
360
  _headers=custom_headers # Pass custom headers to the SDK method
425
361
  )
426
-
362
+
427
363
  # Check if the response was successful
428
364
  if response_data.status != 200:
429
365
  error_message = f"Failed to get tag catalog: HTTP {response_data.status}"
430
- debug_print(error_message)
366
+ logger.error(error_message)
431
367
  return {"error": error_message}
432
-
368
+
433
369
  # Read the response content
434
370
  response_text = response_data.data.decode('utf-8')
435
-
371
+
436
372
  # Parse the JSON manually
437
373
  import json
438
374
  try:
439
375
  result_dict = json.loads(response_text)
440
- debug_print(f"Result from SDK with custom headers: {result_dict}")
376
+ logger.debug(f"Result from SDK with custom headers: {result_dict}")
441
377
  return result_dict
442
378
  except json.JSONDecodeError as json_err:
443
379
  error_message = f"Failed to parse JSON response: {json_err}"
444
- debug_print(error_message)
380
+ logger.error(error_message)
445
381
  return {"error": error_message}
446
382
  else:
447
383
  # Re-raise if it's not a 406 error
448
384
  raise
449
-
385
+
450
386
  except Exception as e:
451
- debug_print(f"Error in get_tag_catalog: {e}")
452
- import traceback
453
- traceback.print_exc(file=sys.stderr)
454
- return {"error": f"Failed to get tag catalog: {str(e)}"}
387
+ logger.error(f"Error in get_tag_catalog: {e}", exc_info=True)
388
+ return {"error": f"Failed to get tag catalog: {e!s}"}
389
+
455
390
 
456
-
457
391
  @register_as_tool
458
- async def get_tag_catalog_all(self, ctx=None) -> Dict[str, Any]:
392
+ @with_header_auth(InfrastructureCatalogApi)
393
+ async def get_tag_catalog_all(self, ctx=None, api_client=None) -> Dict[str, Any]:
459
394
  """
460
395
  Get all available tags. This tool retrieves the complete list of all tags available in your Instana-monitored environment. It returns every tag across all plugins, services, and technologies, allowing users to explore the full tagging taxonomy.
461
396
 
@@ -464,81 +399,78 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
464
399
  "Show me all possible tags I can use for filtering or grouping"
465
400
  "What tags exist across all services or technologies?"
466
401
  "Give me the complete tag catalog from Instana"
467
-
402
+
468
403
  Args:
469
404
  ctx: The MCP context (optional)
470
-
405
+
471
406
  Returns:
472
407
  Dictionary containing a summarized view of available tags or error information
473
408
  """
474
409
  try:
475
- debug_print("get_tag_catalog_all called")
476
-
410
+ logger.debug("get_tag_catalog_all called")
411
+
477
412
  # Try using the standard SDK method first
478
413
  try:
479
- result = self.catalog_api.get_tag_catalog_all()
480
-
414
+ result = api_client.get_tag_catalog_all()
415
+
481
416
  # Convert the result to a dictionary
482
417
  if hasattr(result, 'to_dict'):
483
418
  full_result = result.to_dict()
484
419
  else:
485
420
  # If it's already a dict or another format, use it as is
486
421
  full_result = result
487
-
488
- debug_print(f"Full result from get_tag_catalog_all (standard method): {full_result}")
489
-
422
+
423
+ logger.debug(f"Full result from get_tag_catalog_all (standard method): {full_result}")
424
+
490
425
  # Create a summarized version of the response
491
426
  summarized_result = self._summarize_tag_catalog(full_result)
492
427
  return summarized_result
493
-
428
+
494
429
  except Exception as sdk_error:
495
- debug_print(f"Standard SDK method failed: {sdk_error}, trying fallback")
496
-
430
+ logger.error(f"Standard SDK method failed: {sdk_error}, trying fallback")
431
+
497
432
  # Fallback to using the without_preload_content method
498
- response_data = self.catalog_api.get_tag_catalog_all_without_preload_content()
499
-
433
+ response_data = api_client.get_tag_catalog_all_without_preload_content()
434
+
500
435
  # Check if the response was successful
501
436
  if response_data.status != 200:
502
437
  error_message = f"Failed to get tag catalog: HTTP {response_data.status}"
503
- debug_print(error_message)
504
-
505
- if response_data.status == 401 or response_data.status == 403:
438
+ logger.debug(error_message)
439
+
440
+ if response_data.status in (401, 403):
506
441
  return {"error": "Authentication failed. Please check your API token and permissions."}
507
442
  else:
508
443
  return {"error": error_message}
509
-
444
+
510
445
  # Read the response content
511
446
  response_text = response_data.data.decode('utf-8')
512
-
447
+
513
448
  # Parse the JSON manually
514
449
  import json
515
450
  try:
516
451
  full_result = json.loads(response_text)
517
- debug_print(f"Full result from get_tag_catalog_all (fallback method): {full_result}")
518
-
452
+ logger.debug(f"Full result from get_tag_catalog_all (fallback method): {full_result}")
453
+
519
454
  # Create a summarized version of the response
520
455
  summarized_result = self._summarize_tag_catalog(full_result)
521
456
  return summarized_result
522
-
457
+
523
458
  except json.JSONDecodeError as json_err:
524
459
  error_message = f"Failed to parse JSON response: {json_err}"
525
- debug_print(error_message)
526
- debug_print(f"Response text: {response_text}")
460
+ logger.error(f"Response text: {response_text}")
527
461
  return {"error": error_message}
528
-
462
+
529
463
  except Exception as e:
530
- debug_print(f"Error in get_tag_catalog_all: {e}")
531
- import traceback
532
- traceback.print_exc(file=sys.stderr)
533
- return {"error": f"Failed to get tag catalog: {str(e)}"}
464
+ logger.error(f"Error in get_tag_catalog_all: {e}", exc_info=True)
465
+ return {"error": f"Failed to get tag catalog: {e!s}"}
534
466
 
535
467
  def _summarize_tag_catalog(self, full_catalog: Dict[str, Any]) -> Dict[str, Any]:
536
468
  """
537
469
  Create a summarized version of the tag catalog response that includes tag labels.
538
-
470
+
539
471
  Args:
540
472
  full_catalog: The complete tag catalog response
541
-
473
+
542
474
  Returns:
543
475
  A simplified version of the tag catalog with tag labels
544
476
  """
@@ -547,15 +479,15 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
547
479
  "categories": {},
548
480
  "allLabels": []
549
481
  }
550
-
482
+
551
483
  # Extract tag tree if available
552
484
  tag_tree = full_catalog.get("tagTree", [])
553
-
485
+
554
486
  # Process each category in the tag tree
555
487
  for category in tag_tree:
556
488
  category_label = category.get("label", "Uncategorized")
557
489
  category_tags = []
558
-
490
+
559
491
  # Process children (actual tags)
560
492
  if "children" in category and isinstance(category["children"], list):
561
493
  for tag in category["children"]:
@@ -563,44 +495,45 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
563
495
  if tag_label:
564
496
  category_tags.append(tag_label)
565
497
  summary["allLabels"].append(tag_label)
566
-
498
+
567
499
  # Add category to summary if it has tags
568
500
  if category_tags:
569
501
  summary["categories"][category_label] = sorted(category_tags)
570
-
502
+
571
503
  # Remove duplicates and sort the all labels list
572
- summary["allLabels"] = sorted(list(set(summary["allLabels"])))
504
+ summary["allLabels"] = sorted(set(summary["allLabels"]))
573
505
  summary["count"] = len(summary["allLabels"])
574
-
506
+
575
507
  return summary
576
508
 
577
509
 
578
510
  @register_as_tool
579
- async def get_infrastructure_catalog_search_fields(self, ctx=None) -> Dict[str, Any]:
511
+ @with_header_auth(InfrastructureCatalogApi)
512
+ async def get_infrastructure_catalog_search_fields(self, ctx=None, api_client=None) -> List[str] | Dict[str, Any]:
580
513
  """
581
- Get search field catalog from Instana. This tool retrieves all available search keywords and fields that can be used in dynamic focus queries for infrastructure monitoring.
582
- Use this when you need to understand what search criteria are available, want to build complex queries to filter entities, or need to find the correct search syntax for specific entity properties.
583
- This is particularly useful for constructing advanced search queries, understanding available filtering options, or discovering how to target specific entities in your environment.
514
+ Get search field catalog from Instana. This tool retrieves all available search keywords and fields that can be used in dynamic focus queries for infrastructure monitoring.
515
+ Use this when you need to understand what search criteria are available, want to build complex queries to filter entities, or need to find the correct search syntax for specific entity properties.
516
+ This is particularly useful for constructing advanced search queries, understanding available filtering options, or discovering how to target specific entities in your environment.
584
517
  For example, use this tool when asked about 'what search fields are available', 'how to filter hosts by property', 'search syntax for Kubernetes pods', or when someone wants to 'learn how to build complex entity queries'.
585
-
518
+
586
519
  This endpoint retrieves all available search keywords for dynamic focus queries.
587
-
520
+
588
521
  Args:
589
522
  ctx: The MCP context (optional)
590
-
523
+
591
524
  Returns:
592
525
  Dictionary containing search field keywords or error information
593
526
  """
594
527
  try:
595
- debug_print("get_infrastructure_catalog_search_fields called")
596
-
528
+ logger.debug("get_infrastructure_catalog_search_fields called")
529
+
597
530
  # Call the get_infrastructure_catalog_search_fields method from the SDK
598
- result = self.catalog_api.get_infrastructure_catalog_search_fields()
599
- debug_print(f"API call successful, got {len(result)} search fields")
600
-
531
+ result = api_client.get_infrastructure_catalog_search_fields()
532
+ logger.debug(f"API call successful, got {len(result)} search fields")
533
+
601
534
  # Extract just 10 keywords to keep it very small
602
535
  keywords = []
603
-
536
+
604
537
  for field_obj in result[:10]:
605
538
  try:
606
539
  if hasattr(field_obj, 'to_dict'):
@@ -608,17 +541,16 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
608
541
  keyword = field_dict.get("keyword", "")
609
542
  else:
610
543
  keyword = getattr(field_obj, 'keyword', "")
611
-
544
+
612
545
  if keyword:
613
546
  keywords.append(keyword)
614
-
547
+
615
548
  except Exception:
616
549
  continue
617
-
618
- # Return just the raw keywords list - nothing else
619
- return keywords
620
-
550
+
551
+ # Wrap the keywords list in a dictionary to match return type
552
+ return {"search_fields": keywords, "count": len(keywords)}
553
+
621
554
  except Exception as e:
622
- debug_print(f"Error: {e}")
555
+ logger.error(f"Error: {e}")
623
556
  return {"error": str(e)}
624
-