mcp-instana 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.0.dist-info}/METADATA +2 -1
- {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.0.dist-info}/RECORD +29 -29
- src/application/application_alert_config.py +45 -12
- src/application/application_analyze.py +28 -6
- src/application/application_catalog.py +11 -2
- src/application/application_global_alert_config.py +60 -21
- src/application/application_metrics.py +20 -4
- src/application/application_resources.py +20 -4
- src/application/application_settings.py +111 -35
- src/application/application_topology.py +22 -14
- src/automation/action_catalog.py +165 -188
- src/automation/action_history.py +21 -6
- src/core/server.py +7 -1
- src/core/utils.py +42 -5
- src/event/events_tools.py +30 -7
- src/infrastructure/infrastructure_analyze.py +18 -4
- src/infrastructure/infrastructure_catalog.py +72 -16
- src/infrastructure/infrastructure_metrics.py +5 -1
- src/infrastructure/infrastructure_resources.py +30 -11
- src/infrastructure/infrastructure_topology.py +10 -2
- src/log/log_alert_configuration.py +106 -31
- src/settings/custom_dashboard_tools.py +30 -7
- src/website/website_analyze.py +10 -2
- src/website/website_catalog.py +14 -3
- src/website/website_configuration.py +54 -13
- src/website/website_metrics.py +10 -2
- {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.0.dist-info}/WHEEL +0 -0
- {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.0.dist-info}/entry_points.txt +0 -0
- {mcp_instana-0.2.1.dist-info → mcp_instana-0.3.0.dist-info}/licenses/LICENSE.md +0 -0
src/automation/action_catalog.py
CHANGED
|
@@ -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
|
|
122
|
-
logger.debug("Calling
|
|
123
|
-
result = api_client.
|
|
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
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
160
|
-
"data": result_dict
|
|
147
|
+
"message": "Action matches retrieved successfully",
|
|
148
|
+
"data": result_dict,
|
|
149
|
+
"count": len(result_dict)
|
|
161
150
|
}
|
|
162
|
-
|
|
163
|
-
logger.
|
|
151
|
+
else:
|
|
152
|
+
logger.debug(f"Result from get_action_matches: {result_dict}")
|
|
164
153
|
return {
|
|
165
|
-
"success":
|
|
166
|
-
"message": "
|
|
167
|
-
"
|
|
168
|
-
"raw_data": str(result)
|
|
154
|
+
"success": True,
|
|
155
|
+
"message": "Action match retrieved successfully",
|
|
156
|
+
"data": result_dict
|
|
169
157
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
213
|
-
result = api_client.
|
|
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
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
#
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
263
|
-
result = api_client.
|
|
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
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
361
|
-
result = api_client.
|
|
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
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
#
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
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
|
|
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
|
|
345
|
+
Dict[str, Any]: The list of available action tags
|
|
394
346
|
"""
|
|
395
347
|
try:
|
|
396
|
-
logger.debug("
|
|
348
|
+
logger.debug("get_action_tags called")
|
|
397
349
|
|
|
398
|
-
# Call the
|
|
399
|
-
result = api_client.
|
|
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
|
-
#
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
#
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
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
|
|
416
|
-
return {"error": f"Failed to get action
|
|
392
|
+
logger.error(f"Error in get_action_tags: {e}")
|
|
393
|
+
return {"error": f"Failed to get action tags: {e!s}"}
|
src/automation/action_history.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
17
|
-
"""
|
|
18
|
-
MCP_TOOLS
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|