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.
- mcp_instana-0.2.0.dist-info/METADATA +1229 -0
- mcp_instana-0.2.0.dist-info/RECORD +59 -0
- {mcp_instana-0.1.0.dist-info → mcp_instana-0.2.0.dist-info}/WHEEL +1 -1
- mcp_instana-0.2.0.dist-info/entry_points.txt +4 -0
- mcp_instana-0.1.0.dist-info/LICENSE → mcp_instana-0.2.0.dist-info/licenses/LICENSE.md +3 -3
- src/application/__init__.py +1 -0
- src/{client/application_alert_config_mcp_tools.py → application/application_alert_config.py} +251 -273
- src/application/application_analyze.py +628 -0
- src/application/application_catalog.py +155 -0
- src/application/application_global_alert_config.py +653 -0
- src/{client/application_metrics_mcp_tools.py → application/application_metrics.py} +113 -131
- src/{client/application_resources_mcp_tools.py → application/application_resources.py} +131 -151
- src/application/application_settings.py +1731 -0
- src/application/application_topology.py +111 -0
- src/automation/action_catalog.py +416 -0
- src/automation/action_history.py +338 -0
- src/core/__init__.py +1 -0
- src/core/server.py +586 -0
- src/core/utils.py +213 -0
- src/event/__init__.py +1 -0
- src/event/events_tools.py +850 -0
- src/infrastructure/__init__.py +1 -0
- src/{client/infrastructure_analyze_mcp_tools.py → infrastructure/infrastructure_analyze.py} +207 -206
- src/{client/infrastructure_catalog_mcp_tools.py → infrastructure/infrastructure_catalog.py} +197 -265
- src/infrastructure/infrastructure_metrics.py +171 -0
- src/{client/infrastructure_resources_mcp_tools.py → infrastructure/infrastructure_resources.py} +198 -227
- src/{client/infrastructure_topology_mcp_tools.py → infrastructure/infrastructure_topology.py} +110 -109
- src/log/__init__.py +1 -0
- src/log/log_alert_configuration.py +331 -0
- src/prompts/__init__.py +16 -0
- src/prompts/application/__init__.py +1 -0
- src/prompts/application/application_alerts.py +54 -0
- src/prompts/application/application_catalog.py +26 -0
- src/prompts/application/application_metrics.py +57 -0
- src/prompts/application/application_resources.py +26 -0
- src/prompts/application/application_settings.py +75 -0
- src/prompts/application/application_topology.py +30 -0
- src/prompts/events/__init__.py +1 -0
- src/prompts/events/events_tools.py +161 -0
- src/prompts/infrastructure/infrastructure_analyze.py +72 -0
- src/prompts/infrastructure/infrastructure_catalog.py +53 -0
- src/prompts/infrastructure/infrastructure_metrics.py +45 -0
- src/prompts/infrastructure/infrastructure_resources.py +74 -0
- src/prompts/infrastructure/infrastructure_topology.py +38 -0
- src/prompts/settings/__init__.py +0 -0
- src/prompts/settings/custom_dashboard.py +157 -0
- src/prompts/website/__init__.py +1 -0
- src/prompts/website/website_analyze.py +35 -0
- src/prompts/website/website_catalog.py +40 -0
- src/prompts/website/website_configuration.py +105 -0
- src/prompts/website/website_metrics.py +34 -0
- src/settings/__init__.py +1 -0
- src/settings/custom_dashboard_tools.py +417 -0
- src/website/__init__.py +0 -0
- src/website/website_analyze.py +433 -0
- src/website/website_catalog.py +171 -0
- src/website/website_configuration.py +770 -0
- src/website/website_metrics.py +241 -0
- mcp_instana-0.1.0.dist-info/METADATA +0 -649
- mcp_instana-0.1.0.dist-info/RECORD +0 -19
- mcp_instana-0.1.0.dist-info/entry_points.txt +0 -3
- src/client/What is the sum of queue depth for all q +0 -55
- src/client/events_mcp_tools.py +0 -531
- src/client/instana_client_base.py +0 -93
- src/client/log_alert_configuration_mcp_tools.py +0 -316
- src/client/show the top 5 services with the highest +0 -28
- src/mcp_server.py +0 -343
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Website Analyze MCP Tools Module
|
|
3
|
+
|
|
4
|
+
This module provides website analyze-specific MCP tools for Instana monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def clean_nan_values(data: Any) -> Any:
|
|
13
|
+
"""
|
|
14
|
+
Recursively clean 'NaN' string values from data structures.
|
|
15
|
+
This is needed because the Instana API sometimes returns 'NaN' as strings
|
|
16
|
+
instead of proper null values, which causes Pydantic validation errors.
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(data, dict):
|
|
19
|
+
return {key: clean_nan_values(value) for key, value in data.items()}
|
|
20
|
+
elif isinstance(data, list):
|
|
21
|
+
return [clean_nan_values(item) for item in data]
|
|
22
|
+
elif isinstance(data, str) and data == 'NaN':
|
|
23
|
+
return None
|
|
24
|
+
else:
|
|
25
|
+
return data
|
|
26
|
+
|
|
27
|
+
# Import the necessary classes from the SDK
|
|
28
|
+
try:
|
|
29
|
+
from instana_client.api.website_analyze_api import WebsiteAnalyzeApi
|
|
30
|
+
from instana_client.models.get_website_beacon_groups import GetWebsiteBeaconGroups
|
|
31
|
+
except ImportError as e:
|
|
32
|
+
import logging
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
logger.error(f"Error importing Instana SDK: {e}", exc_info=True)
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
|
|
38
|
+
|
|
39
|
+
# Configure logger for this module
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WebsiteAnalyzeMCPTools(BaseInstanaClient):
|
|
44
|
+
"""Tools for website analyze in Instana MCP."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, read_token: str, base_url: str):
|
|
47
|
+
"""Initialize the Website Analyze MCP tools client."""
|
|
48
|
+
super().__init__(read_token=read_token, base_url=base_url)
|
|
49
|
+
|
|
50
|
+
@register_as_tool
|
|
51
|
+
@with_header_auth(WebsiteAnalyzeApi)
|
|
52
|
+
async def get_website_beacon_groups(self,
|
|
53
|
+
payload: Optional[Union[Dict[str, Any], str]] = None,
|
|
54
|
+
fill_time_series: Optional[bool] = True,
|
|
55
|
+
ctx=None, api_client=None) -> Dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
Get grouped website beacon metrics.
|
|
58
|
+
|
|
59
|
+
This API endpoint retrieves grouped website monitoring beacon metrics, allowing you to analyze
|
|
60
|
+
performance across different dimensions like page URLs, browsers, or geographic locations.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
payload: Complete request payload as a dictionary or JSON string
|
|
64
|
+
{
|
|
65
|
+
"metrics": [
|
|
66
|
+
{
|
|
67
|
+
"metric": "beaconCount",
|
|
68
|
+
"aggregation": "SUM",
|
|
69
|
+
"granularity": 60
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"group": {
|
|
73
|
+
"groupByTag": "beacon.page.name"
|
|
74
|
+
},
|
|
75
|
+
"tagFilterExpression": {
|
|
76
|
+
"type": "EXPRESSION",
|
|
77
|
+
"logicalOperator": "AND",
|
|
78
|
+
"elements": [
|
|
79
|
+
{
|
|
80
|
+
"type": "TAG_FILTER",
|
|
81
|
+
"name": "beacon.website.name",
|
|
82
|
+
"operator": "EQUALS",
|
|
83
|
+
"entity": "NOT_APPLICABLE",
|
|
84
|
+
"value": "robot-shop"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "TAG_FILTER",
|
|
88
|
+
"name": "beacon.location.path",
|
|
89
|
+
"operator": "EQUALS",
|
|
90
|
+
"entity": "NOT_APPLICABLE",
|
|
91
|
+
"value": "/checkout"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"timeFrame": {
|
|
96
|
+
"to": null,
|
|
97
|
+
"windowSize": 3600000
|
|
98
|
+
},
|
|
99
|
+
"type": "PAGELOAD"
|
|
100
|
+
}
|
|
101
|
+
fill_time_series: Whether to fill missing data points with timestamp and value 0
|
|
102
|
+
ctx: The MCP context (optional)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary containing grouped website metrics data or error information
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
logger.debug("get_website_beacon_groups called")
|
|
109
|
+
|
|
110
|
+
# Parse the payload
|
|
111
|
+
if isinstance(payload, str):
|
|
112
|
+
logger.debug("Payload is a string, attempting to parse")
|
|
113
|
+
try:
|
|
114
|
+
import json
|
|
115
|
+
try:
|
|
116
|
+
parsed_payload = json.loads(payload)
|
|
117
|
+
logger.debug("Successfully parsed payload as JSON")
|
|
118
|
+
request_body = parsed_payload
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
|
|
121
|
+
|
|
122
|
+
# Try replacing single quotes with double quotes
|
|
123
|
+
fixed_payload = payload.replace("'", "\"")
|
|
124
|
+
try:
|
|
125
|
+
parsed_payload = json.loads(fixed_payload)
|
|
126
|
+
logger.debug("Successfully parsed fixed JSON")
|
|
127
|
+
request_body = parsed_payload
|
|
128
|
+
except json.JSONDecodeError:
|
|
129
|
+
# Try as Python literal
|
|
130
|
+
import ast
|
|
131
|
+
try:
|
|
132
|
+
parsed_payload = ast.literal_eval(payload)
|
|
133
|
+
logger.debug("Successfully parsed payload as Python literal")
|
|
134
|
+
request_body = parsed_payload
|
|
135
|
+
except (SyntaxError, ValueError) as e2:
|
|
136
|
+
logger.debug(f"Failed to parse payload string: {e2}")
|
|
137
|
+
return {"error": f"Invalid payload format: {e2}", "payload": payload}
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.debug(f"Error parsing payload string: {e}")
|
|
140
|
+
return {"error": f"Failed to parse payload: {e}", "payload": payload}
|
|
141
|
+
else:
|
|
142
|
+
# If payload is already a dictionary, use it directly
|
|
143
|
+
logger.debug("Using provided payload dictionary")
|
|
144
|
+
request_body = payload
|
|
145
|
+
|
|
146
|
+
# Handle nested payload structure - if the payload contains a 'payload' key, extract it
|
|
147
|
+
if isinstance(request_body, dict) and "payload" in request_body and len(request_body) == 1:
|
|
148
|
+
logger.debug("Found nested payload structure, extracting inner payload")
|
|
149
|
+
request_body = request_body["payload"]
|
|
150
|
+
|
|
151
|
+
logger.debug(f"Final request_body structure: {request_body}")
|
|
152
|
+
logger.debug(f"Request body keys: {list(request_body.keys()) if isinstance(request_body, dict) else 'Not a dict'}")
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
from instana_client.models.get_website_beacon_groups import (
|
|
156
|
+
GetWebsiteBeaconGroups,
|
|
157
|
+
)
|
|
158
|
+
logger.debug("Successfully imported GetWebsiteBeaconGroups")
|
|
159
|
+
except ImportError as e:
|
|
160
|
+
logger.debug(f"Error importing GetWebsiteBeaconGroups: {e}")
|
|
161
|
+
return {"error": f"Failed to import GetWebsiteBeaconGroups: {e!s}"}
|
|
162
|
+
|
|
163
|
+
# Create an GetWebsiteBeaconGroups object from the request body
|
|
164
|
+
try:
|
|
165
|
+
query_params = {}
|
|
166
|
+
|
|
167
|
+
# Extract required fields from request_body
|
|
168
|
+
if request_body and "type" in request_body:
|
|
169
|
+
query_params["type"] = request_body["type"]
|
|
170
|
+
|
|
171
|
+
# Handle required 'group' field
|
|
172
|
+
if request_body and "group" in request_body:
|
|
173
|
+
group_data = request_body["group"]
|
|
174
|
+
# Map the group field names to match Pydantic model expectations
|
|
175
|
+
mapped_group = {}
|
|
176
|
+
if "groupByTag" in group_data:
|
|
177
|
+
mapped_group["groupbyTag"] = group_data["groupByTag"]
|
|
178
|
+
if "groupByTagEntity" in group_data:
|
|
179
|
+
mapped_group["groupbyTagEntity"] = group_data["groupByTagEntity"]
|
|
180
|
+
# Also handle the correct case if already present
|
|
181
|
+
if "groupbyTag" in group_data:
|
|
182
|
+
mapped_group["groupbyTag"] = group_data["groupbyTag"]
|
|
183
|
+
if "groupbyTagEntity" in group_data:
|
|
184
|
+
mapped_group["groupbyTagEntity"] = group_data["groupbyTagEntity"]
|
|
185
|
+
|
|
186
|
+
# Ensure both required fields are present - provide default for groupbyTagEntity if missing
|
|
187
|
+
if "groupbyTag" not in mapped_group:
|
|
188
|
+
return {"error": "Required field 'groupByTag' is missing from group payload"}
|
|
189
|
+
if "groupbyTagEntity" not in mapped_group:
|
|
190
|
+
# Provide default value for groupbyTagEntity when not specified
|
|
191
|
+
mapped_group["groupbyTagEntity"] = "NOT_APPLICABLE"
|
|
192
|
+
|
|
193
|
+
query_params["group"] = mapped_group
|
|
194
|
+
else:
|
|
195
|
+
return {"error": "Required field 'group' is missing from payload"}
|
|
196
|
+
|
|
197
|
+
# Handle required 'metrics' field
|
|
198
|
+
if request_body and "metrics" in request_body:
|
|
199
|
+
query_params["metrics"] = request_body["metrics"]
|
|
200
|
+
else:
|
|
201
|
+
return {"error": "Required field 'metrics' is missing from payload"}
|
|
202
|
+
|
|
203
|
+
# Handle optional fields
|
|
204
|
+
if request_body and "timeFrame" in request_body:
|
|
205
|
+
query_params["timeFrame"] = request_body["timeFrame"]
|
|
206
|
+
if request_body and "tagFilterExpression" in request_body:
|
|
207
|
+
query_params["tagFilterExpression"] = request_body["tagFilterExpression"]
|
|
208
|
+
if request_body and "tagFilters" in request_body:
|
|
209
|
+
query_params["tagFilters"] = request_body["tagFilters"]
|
|
210
|
+
if request_body and "order" in request_body:
|
|
211
|
+
query_params["order"] = request_body["order"]
|
|
212
|
+
if request_body and "pagination" in request_body:
|
|
213
|
+
query_params["pagination"] = request_body["pagination"]
|
|
214
|
+
|
|
215
|
+
logger.debug(f"Creating GetWebsiteBeaconGroups with params: {query_params}")
|
|
216
|
+
config_object = GetWebsiteBeaconGroups(**query_params)
|
|
217
|
+
logger.debug("Successfully created GetWebsiteBeaconGroups object")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.debug(f"Error creating GetWebsiteBeaconGroups: {e}")
|
|
220
|
+
return {"error": f"Failed to get website beacon groups: {e!s}"}
|
|
221
|
+
|
|
222
|
+
# Call the get_beacon_groups method from the SDK using without_preload_content to avoid NaN validation errors
|
|
223
|
+
logger.debug("Calling get_beacon_groups with config object")
|
|
224
|
+
try:
|
|
225
|
+
# Use without_preload_content to bypass Pydantic validation and handle NaN values manually
|
|
226
|
+
response = api_client.get_beacon_groups_without_preload_content(
|
|
227
|
+
get_website_beacon_groups=config_object,
|
|
228
|
+
fill_time_series=fill_time_series
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Check if the response was successful
|
|
232
|
+
if response.status != 200:
|
|
233
|
+
error_message = f"Failed to get website beacon groups: HTTP {response.status}"
|
|
234
|
+
logger.debug(error_message)
|
|
235
|
+
return {"error": error_message}
|
|
236
|
+
|
|
237
|
+
# Read the response content
|
|
238
|
+
response_text = response.data.decode('utf-8')
|
|
239
|
+
|
|
240
|
+
# Parse the response as JSON
|
|
241
|
+
import json
|
|
242
|
+
result_dict = json.loads(response_text)
|
|
243
|
+
|
|
244
|
+
logger.debug("Successfully parsed raw response")
|
|
245
|
+
|
|
246
|
+
logger.debug(f"Result from get_website_beacon_groups: {result_dict}")
|
|
247
|
+
return result_dict
|
|
248
|
+
except Exception as api_error:
|
|
249
|
+
# Handle validation errors from the SDK, particularly for customMetric 'NaN' values
|
|
250
|
+
error_msg = str(api_error)
|
|
251
|
+
if "customMetric" in error_msg and "NaN" in error_msg:
|
|
252
|
+
logger.warning(f"API returned 'NaN' values for customMetric field: {error_msg}")
|
|
253
|
+
return {
|
|
254
|
+
"error": "API returned invalid data (NaN values for customMetric field). This is a known issue with the Instana API response format.",
|
|
255
|
+
"details": error_msg,
|
|
256
|
+
"suggestion": "Try using a different time range or filtering criteria to avoid beacons with NaN customMetric values."
|
|
257
|
+
}
|
|
258
|
+
else:
|
|
259
|
+
# Re-raise other errors
|
|
260
|
+
raise api_error
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"Error in get_website_beacon_groups: {e}")
|
|
263
|
+
return {"error": f"Failed to get website beacon groups: {e!s}"}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@register_as_tool
|
|
267
|
+
@with_header_auth(WebsiteAnalyzeApi)
|
|
268
|
+
async def get_website_beacons(self,
|
|
269
|
+
payload: Optional[Union[Dict[str, Any], str]] = None,
|
|
270
|
+
ctx=None, api_client=None) -> Dict[str, Any]:
|
|
271
|
+
"""
|
|
272
|
+
Get all website beacon metrics.
|
|
273
|
+
|
|
274
|
+
This API endpoint retrieves all website monitoring beacon metrics with matching type.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
payload: Complete request payload as a dictionary or JSON string
|
|
278
|
+
{
|
|
279
|
+
"tagFilterExpression": {
|
|
280
|
+
"type": "TAG_FILTER",
|
|
281
|
+
"name": "beacon.website.name",
|
|
282
|
+
"operator": "EQUALS",
|
|
283
|
+
"entity": "NOT_APPLICABLE",
|
|
284
|
+
"value": "Ecommerce_Bob_Squad"
|
|
285
|
+
},
|
|
286
|
+
"timeFrame": {
|
|
287
|
+
"to": 1757333439394,
|
|
288
|
+
"windowSize": 3600000
|
|
289
|
+
},
|
|
290
|
+
"type": "ERROR"
|
|
291
|
+
}
|
|
292
|
+
ctx: The MCP context (optional)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Dictionary containing all website beacon data with matching type or error information
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
logger.debug("get_website_beacons called")
|
|
299
|
+
|
|
300
|
+
# Parse the payload
|
|
301
|
+
if isinstance(payload, str):
|
|
302
|
+
logger.debug("Payload is a string, attempting to parse")
|
|
303
|
+
try:
|
|
304
|
+
import json
|
|
305
|
+
try:
|
|
306
|
+
parsed_payload = json.loads(payload)
|
|
307
|
+
logger.debug("Successfully parsed payload as JSON")
|
|
308
|
+
request_body = parsed_payload
|
|
309
|
+
except json.JSONDecodeError as e:
|
|
310
|
+
logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
|
|
311
|
+
|
|
312
|
+
# Try replacing single quotes with double quotes
|
|
313
|
+
fixed_payload = payload.replace("'", "\"")
|
|
314
|
+
try:
|
|
315
|
+
parsed_payload = json.loads(fixed_payload)
|
|
316
|
+
logger.debug("Successfully parsed fixed JSON")
|
|
317
|
+
request_body = parsed_payload
|
|
318
|
+
except json.JSONDecodeError:
|
|
319
|
+
# Try as Python literal
|
|
320
|
+
import ast
|
|
321
|
+
try:
|
|
322
|
+
parsed_payload = ast.literal_eval(payload)
|
|
323
|
+
logger.debug("Successfully parsed payload as Python literal")
|
|
324
|
+
request_body = parsed_payload
|
|
325
|
+
except (SyntaxError, ValueError) as e2:
|
|
326
|
+
logger.debug(f"Failed to parse payload string: {e2}")
|
|
327
|
+
return {"error": f"Invalid payload format: {e2}", "payload": payload}
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.debug(f"Error parsing payload string: {e}")
|
|
330
|
+
return {"error": f"Failed to parse payload: {e}", "payload": payload}
|
|
331
|
+
else:
|
|
332
|
+
# If payload is already a dictionary, use it directly
|
|
333
|
+
logger.debug("Using provided payload dictionary")
|
|
334
|
+
request_body = payload
|
|
335
|
+
|
|
336
|
+
# Handle nested payload structure - if the payload contains a 'payload' key, extract it
|
|
337
|
+
if isinstance(request_body, dict) and "payload" in request_body and len(request_body) == 1:
|
|
338
|
+
logger.debug("Found nested payload structure, extracting inner payload")
|
|
339
|
+
request_body = request_body["payload"]
|
|
340
|
+
|
|
341
|
+
logger.debug(f"Final request_body structure: {request_body}")
|
|
342
|
+
logger.debug(f"Request body keys: {list(request_body.keys()) if isinstance(request_body, dict) else 'Not a dict'}")
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
from instana_client.models.get_website_beacons import (
|
|
346
|
+
GetWebsiteBeacons,
|
|
347
|
+
)
|
|
348
|
+
logger.debug("Successfully imported GetWebsiteBeacons")
|
|
349
|
+
except ImportError as e:
|
|
350
|
+
logger.debug(f"Error importing GetWebsiteBeacons: {e}")
|
|
351
|
+
return {"error": f"Failed to import GetWebsiteBeacons: {e!s}"}
|
|
352
|
+
|
|
353
|
+
# Create an GetWebsiteBeacons object from the request body
|
|
354
|
+
try:
|
|
355
|
+
query_params = {}
|
|
356
|
+
|
|
357
|
+
# Handle required 'type' field
|
|
358
|
+
if request_body and "type" in request_body:
|
|
359
|
+
query_params["type"] = request_body["type"]
|
|
360
|
+
else:
|
|
361
|
+
return {"error": "Required field 'type' is missing from payload"}
|
|
362
|
+
|
|
363
|
+
# Handle optional fields
|
|
364
|
+
if request_body and "timeFrame" in request_body:
|
|
365
|
+
time_frame = {}
|
|
366
|
+
if "to" in request_body["timeFrame"]:
|
|
367
|
+
time_frame["to"] = request_body["timeFrame"]["to"]
|
|
368
|
+
if "windowSize" in request_body["timeFrame"]:
|
|
369
|
+
time_frame["windowSize"] = request_body["timeFrame"]["windowSize"]
|
|
370
|
+
query_params["timeFrame"] = time_frame
|
|
371
|
+
|
|
372
|
+
if request_body and "tagFilters" in request_body:
|
|
373
|
+
query_params["tagFilters"] = request_body["tagFilters"]
|
|
374
|
+
|
|
375
|
+
if request_body and "pagination" in request_body:
|
|
376
|
+
query_params["pagination"] = request_body["pagination"]
|
|
377
|
+
|
|
378
|
+
logger.debug(f"Creating GetWebsiteBeacons with params: {query_params}")
|
|
379
|
+
config_object = GetWebsiteBeacons(**query_params)
|
|
380
|
+
logger.debug("Successfully created GetWebsiteBeacons object")
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.debug(f"Error creating GetWebsiteBeacons: {e}")
|
|
383
|
+
return {"error": f"Failed to get website beacons: {e!s}"}
|
|
384
|
+
|
|
385
|
+
# Call the get_beacons method from the SDK using without_preload_content to avoid NaN validation errors
|
|
386
|
+
logger.debug("Calling get_beacons with config object")
|
|
387
|
+
try:
|
|
388
|
+
# Use without_preload_content to bypass Pydantic validation and handle NaN values manually
|
|
389
|
+
result = api_client.get_beacons_without_preload_content(
|
|
390
|
+
get_website_beacons=config_object
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Read the response content
|
|
394
|
+
response_text = result.data.decode('utf-8')
|
|
395
|
+
|
|
396
|
+
# Parse the response as JSON
|
|
397
|
+
import json
|
|
398
|
+
result_dict = json.loads(response_text)
|
|
399
|
+
|
|
400
|
+
logger.debug("Successfully parsed raw response")
|
|
401
|
+
|
|
402
|
+
# Clean any NaN values from the response
|
|
403
|
+
result_dict = clean_nan_values(result_dict)
|
|
404
|
+
|
|
405
|
+
# Handle nested JSON response format
|
|
406
|
+
if isinstance(result_dict, dict) and "data" in result_dict and isinstance(result_dict["data"], list):
|
|
407
|
+
# Check if data contains JSON strings that need to be parsed
|
|
408
|
+
if len(result_dict["data"]) > 0 and isinstance(result_dict["data"][0], str):
|
|
409
|
+
try:
|
|
410
|
+
# Parse the JSON string from the data array
|
|
411
|
+
parsed_data = json.loads(result_dict["data"][0])
|
|
412
|
+
result_dict = parsed_data
|
|
413
|
+
logger.debug("Successfully parsed nested JSON response")
|
|
414
|
+
except (json.JSONDecodeError, IndexError) as e:
|
|
415
|
+
logger.debug(f"Failed to parse nested JSON: {e}")
|
|
416
|
+
# Fall back to original structure
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
# Ensure we always return a dictionary, not a list
|
|
420
|
+
if isinstance(result_dict, list):
|
|
421
|
+
result_dict = {"beacons": result_dict, "count": len(result_dict)}
|
|
422
|
+
elif not isinstance(result_dict, dict):
|
|
423
|
+
result_dict = {"data": result_dict}
|
|
424
|
+
logger.debug(f"Result from get_website_beacons: {result_dict}")
|
|
425
|
+
return result_dict
|
|
426
|
+
except Exception as api_error:
|
|
427
|
+
# Handle validation errors from the SDK, particularly for customMetric 'NaN' values
|
|
428
|
+
error_msg = str(api_error)
|
|
429
|
+
logger.debug(f"API error details: {error_msg}")
|
|
430
|
+
return {"error": error_msg}
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.error(f"Error in get_website_beacons: {e}")
|
|
433
|
+
return {"error": f"Failed to get website beacons: {e!s}"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Website Catalog MCP Tools Module
|
|
3
|
+
|
|
4
|
+
This module provides website catalog-specific MCP tools for Instana monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
# Import the necessary classes from the SDK
|
|
11
|
+
try:
|
|
12
|
+
from instana_client.api.website_catalog_api import WebsiteCatalogApi
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
import logging
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.error(f"Error importing Instana SDK: {e}", exc_info=True)
|
|
17
|
+
raise
|
|
18
|
+
|
|
19
|
+
from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
|
|
20
|
+
|
|
21
|
+
# Configure logger for this module
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
class WebsiteCatalogMCPTools(BaseInstanaClient):
|
|
25
|
+
"""Tools for website catalog in Instana MCP."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, read_token: str, base_url: str):
|
|
28
|
+
"""Initialize the Website Catalog MCP tools client."""
|
|
29
|
+
super().__init__(read_token=read_token, base_url=base_url)
|
|
30
|
+
|
|
31
|
+
@register_as_tool
|
|
32
|
+
@with_header_auth(WebsiteCatalogApi)
|
|
33
|
+
async def get_website_catalog_metrics(self, ctx=None, api_client=None) -> Dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Get website monitoring metrics catalog.
|
|
36
|
+
|
|
37
|
+
This API endpoint retrieves all available metric definitions for website monitoring.
|
|
38
|
+
Use this to discover what metrics are available for website monitoring.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
ctx: The MCP context (optional)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary containing available website metrics or error information
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
logger.debug("get_website_catalog_metrics called")
|
|
48
|
+
|
|
49
|
+
# Call the get_website_catalog_metrics method from the SDK
|
|
50
|
+
result = api_client.get_website_catalog_metrics()
|
|
51
|
+
|
|
52
|
+
# Convert the result to a list of dictionaries
|
|
53
|
+
if isinstance(result, list):
|
|
54
|
+
# If it's a list, convert each item to dict if possible
|
|
55
|
+
result_list = []
|
|
56
|
+
for item in result:
|
|
57
|
+
if hasattr(item, 'to_dict'):
|
|
58
|
+
result_list.append(item.to_dict())
|
|
59
|
+
else:
|
|
60
|
+
result_list.append(item)
|
|
61
|
+
elif hasattr(result, 'to_dict'):
|
|
62
|
+
result_list = [result.to_dict()]
|
|
63
|
+
else:
|
|
64
|
+
# If it's already a list or another format, use it as is
|
|
65
|
+
result_list = result
|
|
66
|
+
|
|
67
|
+
# Ensure we always return a dictionary, not a list
|
|
68
|
+
if isinstance(result_list, list):
|
|
69
|
+
result_dict = {"metrics": result_list, "count": len(result_list)}
|
|
70
|
+
else:
|
|
71
|
+
result_dict = {"data": result_list}
|
|
72
|
+
|
|
73
|
+
logger.debug(f"Result from get_website_catalog_metrics: {result_dict}")
|
|
74
|
+
return result_dict
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Error in get_website_catalog_metrics: {e}", exc_info=True)
|
|
77
|
+
return {"error": f"Failed to get website catalog metrics: {e!s}"}
|
|
78
|
+
|
|
79
|
+
@register_as_tool
|
|
80
|
+
@with_header_auth(WebsiteCatalogApi)
|
|
81
|
+
async def get_website_catalog_tags(self, ctx=None, api_client=None) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Get website monitoring tags catalog.
|
|
84
|
+
|
|
85
|
+
This API endpoint retrieves all available tags for website monitoring.
|
|
86
|
+
Use this to discover what tags are available for filtering website beacons.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
ctx: The MCP context (optional)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dictionary containing available website tags or error information
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
logger.debug("get_website_catalog_tags called")
|
|
96
|
+
|
|
97
|
+
# Call the get_website_catalog_tags method from the SDK
|
|
98
|
+
result = api_client.get_website_catalog_tags()
|
|
99
|
+
|
|
100
|
+
# Convert the result to a list of dictionaries
|
|
101
|
+
if isinstance(result, list):
|
|
102
|
+
# If it's a list, convert each item to dict if possible
|
|
103
|
+
result_list = []
|
|
104
|
+
for item in result:
|
|
105
|
+
if hasattr(item, 'to_dict'):
|
|
106
|
+
result_list.append(item.to_dict())
|
|
107
|
+
else:
|
|
108
|
+
result_list.append(item)
|
|
109
|
+
elif hasattr(result, 'to_dict'):
|
|
110
|
+
result_list = [result.to_dict()]
|
|
111
|
+
else:
|
|
112
|
+
# If it's already a list or another format, use it as is
|
|
113
|
+
result_list = result
|
|
114
|
+
|
|
115
|
+
# Ensure we always return a dictionary, not a list
|
|
116
|
+
if isinstance(result_list, list):
|
|
117
|
+
result_dict = {"tags": result_list, "count": len(result_list)}
|
|
118
|
+
else:
|
|
119
|
+
result_dict = {"data": result_list}
|
|
120
|
+
|
|
121
|
+
logger.debug(f"Result from get_website_catalog_tags: {result_dict}")
|
|
122
|
+
return result_dict
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Error in get_website_catalog_tags: {e}", exc_info=True)
|
|
125
|
+
return {"error": f"Failed to get website catalog tags: {e!s}"}
|
|
126
|
+
|
|
127
|
+
@register_as_tool
|
|
128
|
+
@with_header_auth(WebsiteCatalogApi)
|
|
129
|
+
async def get_website_tag_catalog(self,
|
|
130
|
+
beacon_type: str,
|
|
131
|
+
use_case: str,
|
|
132
|
+
ctx=None, api_client=None) -> Dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Get website monitoring tag catalog.
|
|
135
|
+
|
|
136
|
+
This API endpoint retrieves all available tags for website monitoring.
|
|
137
|
+
Use this to discover what tags are available for filtering website beacons.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
beacon_type: The beacon type (e.g., 'PAGELOAD')
|
|
141
|
+
use_case: The use case (e.g., 'GROUPING')
|
|
142
|
+
ctx: The MCP context (optional)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dictionary containing available website tags or error information
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
logger.debug("get_website_tag_catalog called")
|
|
149
|
+
if not beacon_type:
|
|
150
|
+
return {"error": "beacon_type parameter is required"}
|
|
151
|
+
if not use_case:
|
|
152
|
+
return {"error": "use_case parameter is required"}
|
|
153
|
+
|
|
154
|
+
# Call the get_website_tag_catalog method from the SDK
|
|
155
|
+
result = api_client.get_website_tag_catalog(
|
|
156
|
+
beacon_type=beacon_type,
|
|
157
|
+
use_case=use_case
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Convert the result to a dictionary
|
|
161
|
+
if hasattr(result, 'to_dict'):
|
|
162
|
+
result_dict = result.to_dict()
|
|
163
|
+
else:
|
|
164
|
+
# If it's already a dict or another format, use it as is
|
|
165
|
+
result_dict = result
|
|
166
|
+
|
|
167
|
+
logger.debug(f"Result from get_website_tag_catalog: {result_dict}")
|
|
168
|
+
return result_dict
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Error in get_website_tag_catalog: {e}", exc_info=True)
|
|
171
|
+
return {"error": f"Failed to get website tag catalog: {e!s}"}
|