mcp-instana 0.2.1__py3-none-any.whl → 0.3.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 (29) hide show
  1. {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.1.dist-info}/METADATA +69 -40
  2. {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.1.dist-info}/RECORD +29 -29
  3. src/application/application_alert_config.py +45 -12
  4. src/application/application_analyze.py +28 -6
  5. src/application/application_catalog.py +11 -2
  6. src/application/application_global_alert_config.py +60 -21
  7. src/application/application_metrics.py +20 -4
  8. src/application/application_resources.py +20 -4
  9. src/application/application_settings.py +111 -35
  10. src/application/application_topology.py +22 -14
  11. src/automation/action_catalog.py +165 -188
  12. src/automation/action_history.py +21 -6
  13. src/core/server.py +7 -1
  14. src/core/utils.py +42 -5
  15. src/event/events_tools.py +30 -7
  16. src/infrastructure/infrastructure_analyze.py +18 -4
  17. src/infrastructure/infrastructure_catalog.py +72 -16
  18. src/infrastructure/infrastructure_metrics.py +5 -1
  19. src/infrastructure/infrastructure_resources.py +30 -11
  20. src/infrastructure/infrastructure_topology.py +10 -2
  21. src/log/log_alert_configuration.py +106 -31
  22. src/settings/custom_dashboard_tools.py +30 -7
  23. src/website/website_analyze.py +10 -2
  24. src/website/website_catalog.py +14 -3
  25. src/website/website_configuration.py +54 -13
  26. src/website/website_metrics.py +10 -2
  27. {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.1.dist-info}/WHEEL +0 -0
  28. {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.1.dist-info}/entry_points.txt +0 -0
  29. {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
@@ -18,6 +18,8 @@ except ImportError:
18
18
  logger.error("Failed to import application alert configuration API", exc_info=True)
19
19
  raise
20
20
 
21
+ from mcp.types import ToolAnnotations
22
+
21
23
  from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
22
24
 
23
25
  # Configure logger for this module
@@ -30,7 +32,10 @@ class ActionCatalogMCPTools(BaseInstanaClient):
30
32
  """Initialize the Application Alert MCP tools client."""
31
33
  super().__init__(read_token=read_token, base_url=base_url)
32
34
 
33
- @register_as_tool
35
+ @register_as_tool(
36
+ title="Get Action Matches",
37
+ annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
38
+ )
34
39
  @with_header_auth(ActionCatalogApi)
35
40
  async def get_action_matches(self,
36
41
  payload: Union[Dict[str, Any], str],
@@ -118,125 +123,106 @@ class ActionCatalogMCPTools(BaseInstanaClient):
118
123
  logger.debug(f"Error creating ActionSearchSpace: {e}")
119
124
  return {"error": f"Failed to create config object: {e!s}"}
120
125
 
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(
126
+ # Call the get_action_matches_without_preload_content method from the SDK to avoid Pydantic validation issues
127
+ logger.debug("Calling get_action_matches_without_preload_content with config object")
128
+ result = api_client.get_action_matches_without_preload_content(
124
129
  action_search_space=config_object,
125
130
  target_snapshot_id=target_snapshot_id,
126
131
  )
127
132
 
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()
133
+ # Parse the JSON response manually
134
+ import json
135
+ try:
136
+ # The result from get_action_matches_without_preload_content is a response object
137
+ # We need to read the response data and parse it as JSON
138
+ response_text = result.data.decode('utf-8')
139
+ result_dict = json.loads(response_text)
140
+ logger.debug("Successfully retrieved action matches data")
141
+
142
+ # Handle the parsed JSON data
143
+ if isinstance(result_dict, list):
156
144
  logger.debug(f"Result from get_action_matches: {result_dict}")
157
145
  return {
158
146
  "success": True,
159
- "message": "Action match retrieved successfully",
160
- "data": result_dict
147
+ "message": "Action matches retrieved successfully",
148
+ "data": result_dict,
149
+ "count": len(result_dict)
161
150
  }
162
- except Exception as e:
163
- logger.warning(f"Failed to convert result to dict: {e}")
151
+ else:
152
+ logger.debug(f"Result from get_action_matches: {result_dict}")
164
153
  return {
165
- "success": False,
166
- "message": "Failed to serialize result",
167
- "error": str(e),
168
- "raw_data": str(result)
154
+ "success": True,
155
+ "message": "Action match retrieved successfully",
156
+ "data": result_dict
169
157
  }
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
158
+ except (json.JSONDecodeError, AttributeError) as json_err:
159
+ error_message = f"Failed to parse JSON response: {json_err}"
160
+ logger.error(error_message)
161
+ return {"error": error_message}
178
162
  except Exception as e:
179
163
  logger.error(f"Error in get_action_matches: {e}")
180
164
  return {"error": f"Failed to get action matches: {e!s}"}
181
165
 
182
- @register_as_tool
166
+ @register_as_tool(
167
+ title="Get Actions",
168
+ annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
169
+ )
183
170
  @with_header_auth(ActionCatalogApi)
184
171
  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
172
  ctx=None,
192
- api_client=None) -> Dict[str, Any]:
173
+ api_client=None) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
193
174
  """
194
175
  Get a list of available automation actions from the action catalog.
195
176
 
177
+ Note: The SDK get_actions method does not support pagination or filtering parameters.
178
+
196
179
  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
180
  ctx: Optional[Dict[str, Any]]: The context for the action retrieval
204
181
  api_client: Optional[ActionCatalogApi]: The API client for action catalog
205
182
 
206
183
  Returns:
207
- Dict[str, Any]: The list of available automation actions
184
+ Union[List[Dict[str, Any]], Dict[str, Any]]: The list of available automation actions or error dict
208
185
  """
209
186
  try:
210
187
  logger.debug("get_actions called")
211
188
 
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
- )
189
+ # Call the get_actions_without_preload_content method from the SDK to avoid Pydantic validation issues
190
+ result = api_client.get_actions_without_preload_content()
221
191
 
222
- # Convert the result to a dictionary
223
- if hasattr(result, 'to_dict'):
224
- result_dict = result.to_dict()
192
+ # Parse the JSON response manually
193
+ import json
194
+ try:
195
+ # The result from get_actions_without_preload_content is a response object
196
+ # We need to read the response data and parse it as JSON
197
+ response_text = result.data.decode('utf-8')
198
+ result_dict = json.loads(response_text)
199
+ logger.debug("Successfully retrieved actions data")
200
+ except (json.JSONDecodeError, AttributeError) as json_err:
201
+ error_message = f"Failed to parse JSON response: {json_err}"
202
+ logger.error(error_message)
203
+ return {"error": error_message}
204
+
205
+ # Handle the case where the API returns a list directly
206
+ if isinstance(result_dict, list):
207
+ # Return the list directly
208
+ logger.debug(f"Result from get_actions: {result_dict}")
209
+ return result_dict
210
+ elif isinstance(result_dict, dict) and "actions" in result_dict:
211
+ logger.debug(f"Result from get_actions: {result_dict['actions']}")
212
+ return result_dict["actions"]
225
213
  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
214
+ # Return as is if it's already a list or other format
215
+ logger.debug(f"Result from get_actions: {result_dict}")
216
+ return result_dict
234
217
 
235
218
  except Exception as e:
236
219
  logger.error(f"Error in get_actions: {e}")
237
220
  return {"error": f"Failed to get actions: {e!s}"}
238
221
 
239
- @register_as_tool
222
+ @register_as_tool(
223
+ title="Get Action Details",
224
+ annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
225
+ )
240
226
  @with_header_auth(ActionCatalogApi)
241
227
  async def get_action_details(self,
242
228
  action_id: str,
@@ -259,18 +245,21 @@ class ActionCatalogMCPTools(BaseInstanaClient):
259
245
 
260
246
  logger.debug(f"get_action_details called with action_id: {action_id}")
261
247
 
262
- # Call the get_action method from the SDK
263
- result = api_client.get_action(action_id=action_id)
248
+ # Call the get_action_by_id_without_preload_content method from the SDK to avoid Pydantic validation issues
249
+ result = api_client.get_action_by_id_without_preload_content(id=action_id)
264
250
 
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
- }
251
+ # Parse the JSON response manually
252
+ import json
253
+ try:
254
+ # The result from get_action_by_id_without_preload_content is a response object
255
+ # We need to read the response data and parse it as JSON
256
+ response_text = result.data.decode('utf-8')
257
+ result_dict = json.loads(response_text)
258
+ logger.debug("Successfully retrieved action details")
259
+ except (json.JSONDecodeError, AttributeError) as json_err:
260
+ error_message = f"Failed to parse JSON response: {json_err}"
261
+ logger.error(error_message)
262
+ return {"error": error_message}
274
263
 
275
264
  logger.debug(f"Result from get_action: {result_dict}")
276
265
  return result_dict
@@ -279,67 +268,10 @@ class ActionCatalogMCPTools(BaseInstanaClient):
279
268
  logger.error(f"Error in get_action_details: {e}")
280
269
  return {"error": f"Failed to get action details: {e!s}"}
281
270
 
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
271
+ @register_as_tool(
272
+ title="Get Action Types",
273
+ annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
274
+ )
343
275
  @with_header_auth(ActionCatalogApi)
344
276
  async def get_action_types(self,
345
277
  ctx=None,
@@ -357,18 +289,33 @@ class ActionCatalogMCPTools(BaseInstanaClient):
357
289
  try:
358
290
  logger.debug("get_action_types called")
359
291
 
360
- # Call the get_action_types method from the SDK
361
- result = api_client.get_action_types()
292
+ # Call the get_actions_without_preload_content method from the SDK to avoid Pydantic validation issues
293
+ result = api_client.get_actions_without_preload_content()
362
294
 
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"
295
+ # Parse the JSON response manually
296
+ import json
297
+ try:
298
+ # The result from get_actions_without_preload_content is a response object
299
+ # We need to read the response data and parse it as JSON
300
+ response_text = result.data.decode('utf-8')
301
+ actions_list = json.loads(response_text)
302
+ logger.debug("Successfully retrieved actions data")
303
+
304
+ # Extract unique types from actions
305
+ types = set()
306
+ if isinstance(actions_list, list):
307
+ for action in actions_list:
308
+ if isinstance(action, dict) and 'type' in action:
309
+ types.add(action['type'])
310
+
311
+ result_dict = {
312
+ "types": list(types),
313
+ "total_types": len(types)
371
314
  }
315
+ except (json.JSONDecodeError, AttributeError) as json_err:
316
+ error_message = f"Failed to parse JSON response: {json_err}"
317
+ logger.error(error_message)
318
+ return {"error": error_message}
372
319
 
373
320
  logger.debug(f"Result from get_action_types: {result_dict}")
374
321
  return result_dict
@@ -377,40 +324,70 @@ class ActionCatalogMCPTools(BaseInstanaClient):
377
324
  logger.error(f"Error in get_action_types: {e}")
378
325
  return {"error": f"Failed to get action types: {e!s}"}
379
326
 
380
- @register_as_tool
327
+ @register_as_tool(
328
+ title="Get Action Tags",
329
+ annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
330
+ )
381
331
  @with_header_auth(ActionCatalogApi)
382
- async def get_action_categories(self,
383
- ctx=None,
384
- api_client=None) -> Dict[str, Any]:
332
+ async def get_action_tags(self,
333
+ ctx=None,
334
+ api_client=None) -> Dict[str, Any]:
385
335
  """
386
- Get a list of available action categories in the action catalog.
336
+ Get a list of available action tags from the action catalog.
337
+
338
+ This method extracts unique 'tags' fields from all actions.
387
339
 
388
340
  Args:
389
- ctx: Optional[Dict[str, Any]]: The context for the action categories retrieval
341
+ ctx: Optional[Dict[str, Any]]: The context for the action tags retrieval
390
342
  api_client: Optional[ActionCatalogApi]: The API client for action catalog
391
343
 
392
344
  Returns:
393
- Dict[str, Any]: The list of available action categories
345
+ Dict[str, Any]: The list of available action tags
394
346
  """
395
347
  try:
396
- logger.debug("get_action_categories called")
348
+ logger.debug("get_action_tags called")
397
349
 
398
- # Call the get_action_categories method from the SDK
399
- result = api_client.get_action_categories()
350
+ # Call the get_actions_without_preload_content method from the SDK to avoid Pydantic validation issues
351
+ result = api_client.get_actions_without_preload_content()
400
352
 
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
- }
353
+ # Parse the JSON response manually
354
+ import json
355
+ try:
356
+ # The result from get_actions_without_preload_content is a response object
357
+ # We need to read the response data and parse it as JSON
358
+ response_text = result.data.decode('utf-8')
359
+ actions_list = json.loads(response_text)
360
+ logger.debug("Successfully retrieved actions data")
361
+
362
+ # Extract tags from the actions list
363
+ if isinstance(actions_list, list):
364
+ # Extract unique tags from actions
365
+ tags = set()
366
+ for action in actions_list:
367
+ if isinstance(action, dict):
368
+ # Extract tags field
369
+ if 'tags' in action and isinstance(action['tags'], list):
370
+ tags.update(action['tags'])
371
+
372
+ result_dict = {
373
+ "tags": list(tags),
374
+ "total_tags": len(tags)
375
+ }
376
+ else:
377
+ # If it's not a list, return as is
378
+ result_dict = {
379
+ "tags": [],
380
+ "total_tags": 0
381
+ }
382
+
383
+ except (json.JSONDecodeError, AttributeError) as json_err:
384
+ error_message = f"Failed to parse JSON response: {json_err}"
385
+ logger.error(error_message)
386
+ return {"error": error_message}
410
387
 
411
- logger.debug(f"Result from get_action_categories: {result_dict}")
388
+ logger.debug(f"Result from get_action_tags: {result_dict}")
412
389
  return result_dict
413
390
 
414
391
  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}"}
392
+ logger.error(f"Error in get_action_tags: {e}")
393
+ return {"error": f"Failed to get action tags: {e!s}"}
@@ -7,6 +7,11 @@ This module provides automation action history tools for Instana Automation.
7
7
  import logging
8
8
  from typing import Any, Dict, List, Optional, Union
9
9
 
10
+ from mcp.types import ToolAnnotations
11
+
12
+ from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
13
+ from src.prompts import mcp
14
+
10
15
  # Import the necessary classes from the SDK
11
16
  try:
12
17
  from instana_client.api.action_history_api import (
@@ -19,8 +24,6 @@ except ImportError:
19
24
  raise
20
25
 
21
26
 
22
- from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
23
-
24
27
  # Configure logger for this module
25
28
  logger = logging.getLogger(__name__)
26
29
 
@@ -31,7 +34,10 @@ class ActionHistoryMCPTools(BaseInstanaClient):
31
34
  """Initialize the Action History MCP tools client."""
32
35
  super().__init__(read_token=read_token, base_url=base_url)
33
36
 
34
- @register_as_tool
37
+ @register_as_tool(
38
+ title="Submit Automation Action",
39
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
40
+ )
35
41
  @with_header_auth(ActionHistoryApi)
36
42
  async def submit_automation_action(self,
37
43
  payload: Union[Dict[str, Any], str],
@@ -164,7 +170,10 @@ class ActionHistoryMCPTools(BaseInstanaClient):
164
170
  logger.error(f"Error in submit_automation_action: {e}")
165
171
  return {"error": f"Failed to submit automation action: {e!s}"}
166
172
 
167
- @register_as_tool
173
+ @register_as_tool(
174
+ title="Get Action Instance Details",
175
+ annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
176
+ )
168
177
  @with_header_auth(ActionHistoryApi)
169
178
  async def get_action_instance_details(self,
170
179
  action_instance_id: str,
@@ -212,7 +221,10 @@ class ActionHistoryMCPTools(BaseInstanaClient):
212
221
  logger.error(f"Error in get_action_instance_details: {e}")
213
222
  return {"error": f"Failed to get action instance details: {e!s}"}
214
223
 
215
- @register_as_tool
224
+ @register_as_tool(
225
+ title="List Action Instances",
226
+ annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
227
+ )
216
228
  @with_header_auth(ActionHistoryApi)
217
229
  async def list_action_instances(self,
218
230
  window_size: Optional[int] = None,
@@ -285,7 +297,10 @@ class ActionHistoryMCPTools(BaseInstanaClient):
285
297
  logger.error(f"Error in list_action_instances: {e}")
286
298
  return {"error": f"Failed to list action instances: {e!s}"}
287
299
 
288
- @register_as_tool
300
+ @register_as_tool(
301
+ title="Delete Action Instance",
302
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True)
303
+ )
289
304
  @with_header_auth(ActionHistoryApi)
290
305
  async def delete_action_instance(self,
291
306
  action_instance_id: str,
src/core/server.py CHANGED
@@ -151,7 +151,13 @@ def create_app(token: str, base_url: str, port: int = int(os.getenv("PORT", "808
151
151
  client = getattr(clients_state, attr_name, None)
152
152
  if client and hasattr(client, tool_name):
153
153
  bound_method = getattr(client, tool_name)
154
- server.tool()(bound_method)
154
+
155
+ # Use the stored metadata (all tools now have metadata)
156
+ server.tool(
157
+ title=bound_method._mcp_title,
158
+ annotations=bound_method._mcp_annotations
159
+ )(bound_method)
160
+
155
161
  tools_registered += 1
156
162
  break
157
163
  except Exception as e:
src/core/utils.py CHANGED
@@ -10,13 +10,43 @@ from typing import Any, Callable, Dict, Union
10
10
 
11
11
  import requests
12
12
 
13
+ # Import MCP dependencies
14
+ from mcp.types import ToolAnnotations
15
+
13
16
  # Registry to store all tools
14
17
  MCP_TOOLS = {}
15
18
 
16
- def register_as_tool(func):
17
- """Decorator to register a method as an MCP tool."""
18
- MCP_TOOLS[func.__name__] = func
19
- return func
19
+ def register_as_tool(title=None, annotations=None):
20
+ """
21
+ Enhanced decorator that registers both in MCP_TOOLS and with @mcp.tool
22
+
23
+ Args:
24
+ title: Title for the MCP tool (optional, defaults to function name)
25
+ annotations: ToolAnnotations for the MCP tool (optional)
26
+ """
27
+ def decorator(func):
28
+ # Get function metadata
29
+ func_name = func.__name__
30
+
31
+ # Use provided title or generate from function name
32
+ tool_title = title or func_name.replace('_', ' ').title()
33
+
34
+ # Use provided annotations or default
35
+ tool_annotations = annotations or ToolAnnotations(
36
+ readOnlyHint=True,
37
+ destructiveHint=False
38
+ )
39
+
40
+ # Store the metadata for later use by the server
41
+ func._mcp_title = tool_title
42
+ func._mcp_annotations = tool_annotations
43
+
44
+ # Register in MCP_TOOLS (existing functionality)
45
+ MCP_TOOLS[func_name] = func
46
+
47
+ return func
48
+
49
+ return decorator
20
50
 
21
51
  def with_header_auth(api_class, allow_mock=True):
22
52
  """
@@ -160,7 +190,12 @@ def with_header_auth(api_class, allow_mock=True):
160
190
  print(f"Error in header auth decorator: {e}", file=sys.stderr)
161
191
  import traceback
162
192
  traceback.print_exc(file=sys.stderr)
163
- return {"error": f"Authentication error: {e!s}"}
193
+ # Handle the specific case where e might be a string
194
+ if isinstance(e, str):
195
+ error_msg = f"Authentication error: {e}"
196
+ else:
197
+ error_msg = f"Authentication error: {e!s}"
198
+ return {"error": error_msg}
164
199
 
165
200
  return wrapper
166
201
  return decorator
@@ -182,6 +217,8 @@ class BaseInstanaClient:
182
217
 
183
218
  async def make_request(self, endpoint: str, params: Union[Dict[str, Any], None] = None, method: str = "GET", json: Union[Dict[str, Any], None] = None) -> Dict[str, Any]:
184
219
  """Make a request to the Instana API."""
220
+ if endpoint is None:
221
+ return {"error": "Endpoint cannot be None"}
185
222
  url = f"{self.base_url}/{endpoint.lstrip('/')}"
186
223
  headers = self.get_headers()
187
224