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