mcp-instana 0.6.2__py3-none-any.whl → 0.7.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.6.2.dist-info → mcp_instana-0.7.0.dist-info}/METADATA +179 -120
- {mcp_instana-0.6.2.dist-info → mcp_instana-0.7.0.dist-info}/RECORD +28 -21
- src/application/application_alert_config.py +397 -146
- src/application/application_analyze.py +597 -597
- src/application/application_call_group.py +528 -0
- src/application/application_catalog.py +0 -8
- src/application/application_global_alert_config.py +255 -38
- src/application/application_metrics.py +377 -237
- src/application/application_resources.py +414 -365
- src/application/application_settings.py +605 -1651
- src/application/application_topology.py +62 -62
- src/core/custom_dashboard_smart_router_tool.py +135 -0
- src/core/server.py +92 -119
- src/core/smart_router_tool.py +574 -0
- src/core/utils.py +17 -8
- src/custom_dashboard/custom_dashboard_tools.py +422 -0
- src/infrastructure/elicitation_handler.py +338 -0
- src/infrastructure/entity_registry.py +329 -0
- src/infrastructure/infrastructure_analyze_new.py +600 -0
- src/infrastructure/{infrastructure_analyze.py → infrastructure_analyze_old.py} +1 -16
- src/infrastructure/infrastructure_catalog.py +7 -28
- src/infrastructure/infrastructure_metrics.py +93 -17
- src/infrastructure/infrastructure_resources.py +5 -20
- src/infrastructure/infrastructure_topology.py +2 -8
- src/prompts/application/application_settings.py +58 -0
- {mcp_instana-0.6.2.dist-info → mcp_instana-0.7.0.dist-info}/WHEEL +0 -0
- {mcp_instana-0.6.2.dist-info → mcp_instana-0.7.0.dist-info}/entry_points.txt +0 -0
- {mcp_instana-0.6.2.dist-info → mcp_instana-0.7.0.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart Router Tool
|
|
3
|
+
|
|
4
|
+
This module provides a unified MCP tool that routes queries to the appropriate
|
|
5
|
+
application-specific tools for Instana monitoring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
|
|
11
|
+
from mcp.types import ToolAnnotations
|
|
12
|
+
|
|
13
|
+
from src.core.utils import BaseInstanaClient, register_as_tool
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SmartRouterMCPTool(BaseInstanaClient):
|
|
19
|
+
"""
|
|
20
|
+
Smart router that routes queries to Application Metrics, Alert Configuration, and Catalog tools.
|
|
21
|
+
The LLM agent determines the appropriate operation based on query understanding.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, read_token: str, base_url: str):
|
|
25
|
+
"""Initialize the Smart Router MCP tool."""
|
|
26
|
+
super().__init__(read_token=read_token, base_url=base_url)
|
|
27
|
+
|
|
28
|
+
# Initialize the application tool clients
|
|
29
|
+
from src.application.application_alert_config import ApplicationAlertMCPTools
|
|
30
|
+
from src.application.application_call_group import ApplicationCallGroupMCPTools
|
|
31
|
+
from src.application.application_catalog import ApplicationCatalogMCPTools
|
|
32
|
+
from src.application.application_global_alert_config import (
|
|
33
|
+
ApplicationGlobalAlertMCPTools,
|
|
34
|
+
)
|
|
35
|
+
from src.application.application_resources import ApplicationResourcesMCPTools
|
|
36
|
+
from src.application.application_settings import ApplicationSettingsMCPTools
|
|
37
|
+
|
|
38
|
+
self.app_call_group_client = ApplicationCallGroupMCPTools(read_token, base_url)
|
|
39
|
+
self.app_alert_config_client = ApplicationAlertMCPTools(read_token, base_url)
|
|
40
|
+
self.app_global_alert_config_client = ApplicationGlobalAlertMCPTools(read_token, base_url)
|
|
41
|
+
self.app_resources_client = ApplicationResourcesMCPTools(read_token, base_url)
|
|
42
|
+
self.app_settings_client = ApplicationSettingsMCPTools(read_token, base_url)
|
|
43
|
+
self.app_catalog_client = ApplicationCatalogMCPTools(read_token, base_url)
|
|
44
|
+
|
|
45
|
+
logger.info("Smart Router initialized with Application tools")
|
|
46
|
+
|
|
47
|
+
@register_as_tool(
|
|
48
|
+
title="Manage Instana Application Resources",
|
|
49
|
+
annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
|
|
50
|
+
)
|
|
51
|
+
async def manage_instana_resources(
|
|
52
|
+
self,
|
|
53
|
+
resource_type: str,
|
|
54
|
+
operation: str,
|
|
55
|
+
params: Optional[Dict[str, Any]] = None,
|
|
56
|
+
ctx=None
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Unified Instana application resource manager for metrics, alerts, configurations, and catalog.
|
|
60
|
+
|
|
61
|
+
Resource Types:
|
|
62
|
+
- "metrics": Query application metrics, services, and endpoints
|
|
63
|
+
- "alert_config": Manage application-specific alert configurations
|
|
64
|
+
- "global_alert_config": Manage global application alert configurations
|
|
65
|
+
- "settings": Manage application perspectives, endpoints, services, manual services
|
|
66
|
+
- "catalog": Access application tag and metric catalog information
|
|
67
|
+
|
|
68
|
+
METRICS (resource_type="metrics"):
|
|
69
|
+
operation: "application"
|
|
70
|
+
params: {query, time_frame, metrics, tag_filter_expression, group, order, pagination, include_internal, include_synthetic}
|
|
71
|
+
|
|
72
|
+
List services: group={"groupbyTag": "service.name", "groupbyTagEntity": "DESTINATION"}
|
|
73
|
+
List endpoints: group={"groupbyTag": "endpoint.name", "groupbyTagEntity": "DESTINATION"}
|
|
74
|
+
|
|
75
|
+
ALERT_CONFIG (resource_type="alert_config"):
|
|
76
|
+
operations: find_active, find_versions, find, create, update, delete, enable, disable, restore, update_baseline
|
|
77
|
+
params: {application_id OR application_name, id, alert_ids, valid_on, created, payload}
|
|
78
|
+
Note: Provide application_name (auto-resolved to ID) or application_id
|
|
79
|
+
|
|
80
|
+
GLOBAL_ALERT_CONFIG (resource_type="global_alert_config"):
|
|
81
|
+
operations: find_active, find_versions, find, create, update, delete, enable, disable, restore
|
|
82
|
+
params: {application_id OR application_name, id, alert_ids, valid_on, created, payload}
|
|
83
|
+
Note: Provide application_name (auto-resolved to ID) or application_id
|
|
84
|
+
|
|
85
|
+
SETTINGS (resource_type="settings"):
|
|
86
|
+
operations: get_all, get, create, update, delete, order, replace_all
|
|
87
|
+
params: {resource_subtype, id, application_name, payload, request_body}
|
|
88
|
+
|
|
89
|
+
resource_subtypes: "application", "endpoint", "service", "manual_service"
|
|
90
|
+
|
|
91
|
+
Creating application perspectives (resource_subtype="application", operation="create"):
|
|
92
|
+
- REQUIRED: label (application name)
|
|
93
|
+
- OPTIONAL: scope (default: INCLUDE_ALL_DOWNSTREAM), boundaryScope (default: ALL),
|
|
94
|
+
accessRules (default: READ_WRITE_GLOBAL), tagFilterExpression
|
|
95
|
+
|
|
96
|
+
Minimal example:
|
|
97
|
+
params={"resource_subtype": "application", "payload": {"label": "My App"}}
|
|
98
|
+
|
|
99
|
+
Full example:
|
|
100
|
+
params={
|
|
101
|
+
"resource_subtype": "application",
|
|
102
|
+
"payload": {
|
|
103
|
+
"label": "My App",
|
|
104
|
+
"scope": "INCLUDE_ALL_DOWNSTREAM",
|
|
105
|
+
"boundaryScope": "ALL",
|
|
106
|
+
"accessRules": [{"accessType": "READ_WRITE", "relationType": "GLOBAL"}],
|
|
107
|
+
"tagFilterExpression": {"type": "TAG_FILTER", "name": "service.name", "operator": "CONTAINS", "entity": "DESTINATION", "value": "my-service"}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
CATALOG (resource_type="catalog"):
|
|
112
|
+
operations: get_tag_catalog, get_metric_catalog
|
|
113
|
+
params: {use_case, data_source, var_from}
|
|
114
|
+
|
|
115
|
+
Get tag catalog: operation="get_tag_catalog", params={"use_case": "GROUPING", "data_source": "CALLS"}
|
|
116
|
+
Get metric catalog: operation="get_metric_catalog"
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
resource_type: "metrics", "alert_config", "global_alert_config", "settings", or "catalog"
|
|
120
|
+
operation: Specific operation for the resource type
|
|
121
|
+
params: Operation-specific parameters (optional)
|
|
122
|
+
ctx: MCP context (internal)
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dictionary with results from the appropriate tool
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
# List services
|
|
129
|
+
resource_type="metrics", operation="application", params={
|
|
130
|
+
"tag_filter_expression": {"type": "TAG_FILTER", "name": "application.name", "operator": "EQUALS", "entity": "DESTINATION", "value": "All Services"},
|
|
131
|
+
"group": {"groupbyTag": "service.name", "groupbyTagEntity": "DESTINATION"}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Find active alerts by name
|
|
135
|
+
resource_type="alert_config", operation="find_active", params={"application_name": "All Services"}
|
|
136
|
+
|
|
137
|
+
# Get application config by name
|
|
138
|
+
resource_type="settings", operation="get", params={"resource_subtype": "application", "application_name": "MCP_TEST_DEMO"}
|
|
139
|
+
|
|
140
|
+
# Create application perspective
|
|
141
|
+
resource_type="settings", operation="create", params={"resource_subtype": "application", "payload": {"label": "My App"}}
|
|
142
|
+
|
|
143
|
+
# Get application tag catalog
|
|
144
|
+
resource_type="catalog", operation="get_tag_catalog", params={"use_case": "GROUPING", "data_source": "CALLS"}
|
|
145
|
+
|
|
146
|
+
# Get application metric catalog
|
|
147
|
+
resource_type="catalog", operation="get_metric_catalog"
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
logger.info(f"Smart Router received: resource_type={resource_type}, operation={operation}")
|
|
151
|
+
|
|
152
|
+
# Initialize params if not provided
|
|
153
|
+
if params is None:
|
|
154
|
+
params = {}
|
|
155
|
+
|
|
156
|
+
# Validate resource_type
|
|
157
|
+
if resource_type not in ["metrics", "alert_config", "global_alert_config", "settings", "catalog"]:
|
|
158
|
+
return {
|
|
159
|
+
"error": f"Invalid resource_type '{resource_type}'. Must be 'metrics', 'alert_config', 'global_alert_config', 'settings', or 'catalog'",
|
|
160
|
+
"suggestion": "Choose 'metrics' for querying data, 'alert_config' for application-specific alerts, 'global_alert_config' for global alerts, 'settings' for application perspective configurations, or 'catalog' for tag and metric catalog information"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Route to the appropriate resource handler
|
|
164
|
+
if resource_type == "metrics":
|
|
165
|
+
return await self._handle_metrics(operation, params, ctx)
|
|
166
|
+
elif resource_type == "alert_config":
|
|
167
|
+
return await self._handle_alert_config(operation, params, ctx)
|
|
168
|
+
elif resource_type == "global_alert_config":
|
|
169
|
+
return await self._handle_global_alert_config(operation, params, ctx)
|
|
170
|
+
elif resource_type == "settings":
|
|
171
|
+
return await self._handle_settings(operation, params, ctx)
|
|
172
|
+
elif resource_type == "catalog":
|
|
173
|
+
return await self._handle_catalog(operation, params, ctx)
|
|
174
|
+
else:
|
|
175
|
+
return {
|
|
176
|
+
"error": f"Unsupported resource_type: {resource_type}",
|
|
177
|
+
"supported_types": ["metrics", "alert_config", "global_alert_config", "settings", "catalog"]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Error in smart router: {e}", exc_info=True)
|
|
182
|
+
return {
|
|
183
|
+
"error": f"Smart router error: {e!s}",
|
|
184
|
+
"resource_type": resource_type,
|
|
185
|
+
"operation": operation
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async def _handle_metrics(
|
|
189
|
+
self,
|
|
190
|
+
operation: str,
|
|
191
|
+
params: Dict[str, Any],
|
|
192
|
+
ctx
|
|
193
|
+
) -> Dict[str, Any]:
|
|
194
|
+
"""Handle application metrics queries."""
|
|
195
|
+
if operation != "application":
|
|
196
|
+
return {
|
|
197
|
+
"error": f"Invalid operation '{operation}' for metrics. Only 'application' is supported.",
|
|
198
|
+
"valid_operations": ["application"]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Extract parameters
|
|
202
|
+
query = params.get("query", "")
|
|
203
|
+
time_frame = params.get("time_frame")
|
|
204
|
+
metrics = params.get("metrics")
|
|
205
|
+
tag_filter_expression = params.get("tag_filter_expression")
|
|
206
|
+
group = params.get("group")
|
|
207
|
+
order = params.get("order")
|
|
208
|
+
pagination = params.get("pagination")
|
|
209
|
+
include_internal = params.get("include_internal")
|
|
210
|
+
include_synthetic = params.get("include_synthetic")
|
|
211
|
+
|
|
212
|
+
# Route to Application Call Group Metrics
|
|
213
|
+
logger.info("Routing to Application Call Group Metrics")
|
|
214
|
+
|
|
215
|
+
result = await self.app_call_group_client.get_grouped_calls_metrics(
|
|
216
|
+
metrics=metrics,
|
|
217
|
+
time_frame=time_frame,
|
|
218
|
+
group=group,
|
|
219
|
+
tag_filter_expression=tag_filter_expression,
|
|
220
|
+
include_internal=include_internal,
|
|
221
|
+
include_synthetic=include_synthetic,
|
|
222
|
+
order=order,
|
|
223
|
+
pagination=pagination,
|
|
224
|
+
ctx=ctx
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"resource_type": "metrics",
|
|
229
|
+
"technology": "application",
|
|
230
|
+
"query": query,
|
|
231
|
+
"results": result
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async def _handle_alert_config(
|
|
235
|
+
self,
|
|
236
|
+
operation: str,
|
|
237
|
+
params: Dict[str, Any],
|
|
238
|
+
ctx
|
|
239
|
+
) -> Dict[str, Any]:
|
|
240
|
+
"""Handle Application Alert Config operations."""
|
|
241
|
+
valid_operations = [
|
|
242
|
+
"find_active", "find_versions", "find", "create", "update",
|
|
243
|
+
"delete", "enable", "disable", "restore", "update_baseline"
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
if operation not in valid_operations:
|
|
247
|
+
return {
|
|
248
|
+
"error": f"Invalid operation '{operation}' for alert_config",
|
|
249
|
+
"valid_operations": valid_operations
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Extract parameters
|
|
253
|
+
application_id = params.get("application_id")
|
|
254
|
+
application_name = params.get("application_name")
|
|
255
|
+
id = params.get("id")
|
|
256
|
+
alert_ids = params.get("alert_ids")
|
|
257
|
+
valid_on = params.get("valid_on")
|
|
258
|
+
created = params.get("created")
|
|
259
|
+
payload = params.get("payload")
|
|
260
|
+
|
|
261
|
+
# If application_name is provided but not application_id, resolve it
|
|
262
|
+
if application_name and not application_id:
|
|
263
|
+
logger.info(f"Resolving application name '{application_name}' to application ID")
|
|
264
|
+
app_id_result = await self._get_application_id_by_name(application_name, ctx)
|
|
265
|
+
|
|
266
|
+
if "error" in app_id_result:
|
|
267
|
+
return {
|
|
268
|
+
"resource_type": "alert_config",
|
|
269
|
+
"operation": operation,
|
|
270
|
+
"error": f"Failed to resolve application name '{application_name}': {app_id_result['error']}"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
application_id = app_id_result.get("application_id")
|
|
274
|
+
logger.info(f"Resolved application '{application_name}' to ID: {application_id}")
|
|
275
|
+
|
|
276
|
+
# Route to the alert config client
|
|
277
|
+
result = await self.app_alert_config_client.execute_alert_config_operation(
|
|
278
|
+
operation=operation,
|
|
279
|
+
application_id=application_id,
|
|
280
|
+
id=id,
|
|
281
|
+
alert_ids=alert_ids,
|
|
282
|
+
valid_on=valid_on,
|
|
283
|
+
created=created,
|
|
284
|
+
payload=payload,
|
|
285
|
+
ctx=ctx
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
"resource_type": "alert_config",
|
|
290
|
+
"operation": operation,
|
|
291
|
+
"application_name": application_name,
|
|
292
|
+
"application_id": application_id,
|
|
293
|
+
"results": result
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async def _handle_global_alert_config(
|
|
297
|
+
self,
|
|
298
|
+
operation: str,
|
|
299
|
+
params: Dict[str, Any],
|
|
300
|
+
ctx
|
|
301
|
+
) -> Dict[str, Any]:
|
|
302
|
+
"""Handle Global Application Alert Config operations."""
|
|
303
|
+
valid_operations = [
|
|
304
|
+
"find_active", "find_versions", "find", "create", "update",
|
|
305
|
+
"delete", "enable", "disable", "restore"
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
if operation not in valid_operations:
|
|
309
|
+
return {
|
|
310
|
+
"error": f"Invalid operation '{operation}' for global_alert_config",
|
|
311
|
+
"valid_operations": valid_operations
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Extract parameters
|
|
315
|
+
application_id = params.get("application_id")
|
|
316
|
+
application_name = params.get("application_name")
|
|
317
|
+
id = params.get("id")
|
|
318
|
+
alert_ids = params.get("alert_ids")
|
|
319
|
+
valid_on = params.get("valid_on")
|
|
320
|
+
created = params.get("created")
|
|
321
|
+
payload = params.get("payload")
|
|
322
|
+
|
|
323
|
+
# If application_name is provided but not application_id, resolve it
|
|
324
|
+
if application_name and not application_id:
|
|
325
|
+
logger.info(f"Resolving application name '{application_name}' to application ID")
|
|
326
|
+
app_id_result = await self._get_application_id_by_name(application_name, ctx)
|
|
327
|
+
|
|
328
|
+
if "error" in app_id_result:
|
|
329
|
+
return {
|
|
330
|
+
"resource_type": "global_alert_config",
|
|
331
|
+
"operation": operation,
|
|
332
|
+
"error": f"Failed to resolve application name '{application_name}': {app_id_result['error']}"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
application_id = app_id_result.get("application_id")
|
|
336
|
+
logger.info(f"Resolved application '{application_name}' to ID: {application_id}")
|
|
337
|
+
|
|
338
|
+
# Route to the global alert config client
|
|
339
|
+
result = await self.app_global_alert_config_client.execute_alert_config_operation(
|
|
340
|
+
operation=operation,
|
|
341
|
+
application_id=application_id,
|
|
342
|
+
id=id,
|
|
343
|
+
alert_ids=alert_ids,
|
|
344
|
+
valid_on=valid_on,
|
|
345
|
+
created=created,
|
|
346
|
+
payload=payload,
|
|
347
|
+
ctx=ctx
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"resource_type": "global_alert_config",
|
|
352
|
+
"operation": operation,
|
|
353
|
+
"application_name": application_name,
|
|
354
|
+
"application_id": application_id,
|
|
355
|
+
"results": result
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async def _handle_settings(
|
|
359
|
+
self,
|
|
360
|
+
operation: str,
|
|
361
|
+
params: Dict[str, Any],
|
|
362
|
+
ctx
|
|
363
|
+
) -> Dict[str, Any]:
|
|
364
|
+
"""Handle Application Settings operations."""
|
|
365
|
+
valid_operations = [
|
|
366
|
+
"get_all", "get", "create", "update", "delete", "order", "replace_all"
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
if operation not in valid_operations:
|
|
370
|
+
return {
|
|
371
|
+
"error": f"Invalid operation '{operation}' for settings",
|
|
372
|
+
"valid_operations": valid_operations
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# Extract parameters
|
|
376
|
+
resource_subtype = params.get("resource_subtype")
|
|
377
|
+
id = params.get("id")
|
|
378
|
+
application_name = params.get("application_name")
|
|
379
|
+
payload = params.get("payload")
|
|
380
|
+
request_body = params.get("request_body")
|
|
381
|
+
|
|
382
|
+
# Validate resource_subtype
|
|
383
|
+
valid_subtypes = ["application", "endpoint", "service", "manual_service"]
|
|
384
|
+
if not resource_subtype or resource_subtype not in valid_subtypes:
|
|
385
|
+
return {
|
|
386
|
+
"error": f"Invalid or missing resource_subtype. Must be one of: {valid_subtypes}",
|
|
387
|
+
"resource_subtype": resource_subtype
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# If application_name is provided for application resource_subtype and operation is "get"
|
|
391
|
+
# resolve it to application ID
|
|
392
|
+
if resource_subtype == "application" and operation == "get" and application_name and not id:
|
|
393
|
+
logger.info(f"Resolving application name '{application_name}' to application config ID")
|
|
394
|
+
|
|
395
|
+
# First, get all application configs
|
|
396
|
+
all_configs_result = await self.app_settings_client.execute_settings_operation(
|
|
397
|
+
operation="get_all",
|
|
398
|
+
resource_subtype="application",
|
|
399
|
+
ctx=ctx
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Search for matching application name in configs
|
|
403
|
+
if isinstance(all_configs_result, list):
|
|
404
|
+
for config in all_configs_result:
|
|
405
|
+
if isinstance(config, dict):
|
|
406
|
+
config_label = config.get('label', '')
|
|
407
|
+
config_id = config.get('id', '')
|
|
408
|
+
|
|
409
|
+
# Case-insensitive match
|
|
410
|
+
if config_label.lower() == application_name.lower() and config_id:
|
|
411
|
+
logger.info(f"Found application config '{config_label}' with ID: {config_id}")
|
|
412
|
+
id = config_id
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
if not id:
|
|
416
|
+
return {
|
|
417
|
+
"resource_type": "settings",
|
|
418
|
+
"resource_subtype": resource_subtype,
|
|
419
|
+
"operation": operation,
|
|
420
|
+
"error": f"No application perspective found with name '{application_name}'"
|
|
421
|
+
}
|
|
422
|
+
else:
|
|
423
|
+
return {
|
|
424
|
+
"resource_type": "settings",
|
|
425
|
+
"resource_subtype": resource_subtype,
|
|
426
|
+
"operation": operation,
|
|
427
|
+
"error": "Failed to retrieve application perspectives for name resolution"
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Route to the settings client
|
|
431
|
+
result = await self.app_settings_client.execute_settings_operation(
|
|
432
|
+
operation=operation,
|
|
433
|
+
resource_subtype=resource_subtype,
|
|
434
|
+
id=id,
|
|
435
|
+
payload=payload,
|
|
436
|
+
request_body=request_body,
|
|
437
|
+
ctx=ctx
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
"resource_type": "settings",
|
|
442
|
+
"resource_subtype": resource_subtype,
|
|
443
|
+
"operation": operation,
|
|
444
|
+
"application_name": application_name if application_name else None,
|
|
445
|
+
"resolved_id": id if application_name else None,
|
|
446
|
+
"results": result
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async def _get_application_id_by_name(
|
|
450
|
+
self,
|
|
451
|
+
application_name: str,
|
|
452
|
+
ctx
|
|
453
|
+
) -> Dict[str, Any]:
|
|
454
|
+
"""
|
|
455
|
+
Get application ID by application name using the Application Resources API.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
application_name: Name of the application
|
|
459
|
+
ctx: MCP context
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Dictionary with application_id or error
|
|
463
|
+
"""
|
|
464
|
+
try:
|
|
465
|
+
from datetime import datetime
|
|
466
|
+
|
|
467
|
+
logger.info(f"Resolving application name '{application_name}' to application ID using Application Resources API")
|
|
468
|
+
|
|
469
|
+
# Set time range (last hour)
|
|
470
|
+
to_time = int(datetime.now().timestamp() * 1000)
|
|
471
|
+
window_size = 60 * 60 * 1000 # 1 hour
|
|
472
|
+
|
|
473
|
+
# Use the app_resources_client to get applications
|
|
474
|
+
result = await self.app_resources_client._get_applications_internal(
|
|
475
|
+
name_filter=application_name,
|
|
476
|
+
window_size=window_size,
|
|
477
|
+
to_time=to_time,
|
|
478
|
+
ctx=ctx
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
logger.debug(f"Application Resources API result: {result}")
|
|
482
|
+
|
|
483
|
+
# Extract items from the result
|
|
484
|
+
items = result.get('items', []) if isinstance(result, dict) else []
|
|
485
|
+
|
|
486
|
+
if not items:
|
|
487
|
+
logger.warning(f"No application found with name filter '{application_name}'")
|
|
488
|
+
return {"error": f"No application found with name '{application_name}'"}
|
|
489
|
+
|
|
490
|
+
# Find exact match (case-insensitive)
|
|
491
|
+
for item in items:
|
|
492
|
+
if isinstance(item, dict):
|
|
493
|
+
label = item.get('label', '')
|
|
494
|
+
app_id = item.get('id', '')
|
|
495
|
+
|
|
496
|
+
if label.lower() == application_name.lower() and app_id:
|
|
497
|
+
logger.info(f"Found application '{label}' with ID: {app_id}")
|
|
498
|
+
return {
|
|
499
|
+
"application_id": app_id,
|
|
500
|
+
"application_name": label
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# If no exact match, return the first result
|
|
504
|
+
first_item = items[0]
|
|
505
|
+
if isinstance(first_item, dict):
|
|
506
|
+
label = first_item.get('label', '')
|
|
507
|
+
app_id = first_item.get('id', '')
|
|
508
|
+
|
|
509
|
+
if app_id:
|
|
510
|
+
logger.info(f"Using closest match: '{label}' with ID: {app_id}")
|
|
511
|
+
return {
|
|
512
|
+
"application_id": app_id,
|
|
513
|
+
"application_name": label
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {"error": f"No application found with name '{application_name}'"}
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
logger.error(f"Error fetching application ID: {e}", exc_info=True)
|
|
520
|
+
return {"error": f"Failed to fetch application ID: {e!s}"}
|
|
521
|
+
|
|
522
|
+
async def _handle_catalog(
|
|
523
|
+
self,
|
|
524
|
+
operation: str,
|
|
525
|
+
params: Dict[str, Any],
|
|
526
|
+
ctx
|
|
527
|
+
) -> Dict[str, Any]:
|
|
528
|
+
"""Handle Application Catalog operations."""
|
|
529
|
+
valid_operations = ["get_tag_catalog", "get_metric_catalog"]
|
|
530
|
+
|
|
531
|
+
if operation not in valid_operations:
|
|
532
|
+
return {
|
|
533
|
+
"error": f"Invalid operation '{operation}' for catalog",
|
|
534
|
+
"valid_operations": valid_operations
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
# Extract parameters
|
|
538
|
+
use_case = params.get("use_case")
|
|
539
|
+
data_source = params.get("data_source")
|
|
540
|
+
var_from = params.get("var_from")
|
|
541
|
+
|
|
542
|
+
# Route to the appropriate catalog method
|
|
543
|
+
if operation == "get_tag_catalog":
|
|
544
|
+
logger.info("Routing to Application Tag Catalog")
|
|
545
|
+
result = await self.app_catalog_client.get_application_tag_catalog(
|
|
546
|
+
use_case=use_case,
|
|
547
|
+
data_source=data_source,
|
|
548
|
+
var_from=var_from,
|
|
549
|
+
ctx=ctx
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
"resource_type": "catalog",
|
|
554
|
+
"operation": operation,
|
|
555
|
+
"results": result
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
elif operation == "get_metric_catalog":
|
|
559
|
+
logger.info("Routing to Application Metric Catalog")
|
|
560
|
+
result = await self.app_catalog_client.get_application_metric_catalog(
|
|
561
|
+
ctx=ctx
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
"resource_type": "catalog",
|
|
566
|
+
"operation": operation,
|
|
567
|
+
"results": result
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
"error": f"Unsupported catalog operation: {operation}",
|
|
572
|
+
"valid_operations": valid_operations
|
|
573
|
+
}
|
|
574
|
+
|
src/core/utils.py
CHANGED
|
@@ -13,6 +13,14 @@ import requests
|
|
|
13
13
|
# Import MCP dependencies
|
|
14
14
|
from mcp.types import ToolAnnotations
|
|
15
15
|
|
|
16
|
+
# Import for getting package version from meta data rather than server.py
|
|
17
|
+
try:
|
|
18
|
+
from importlib.metadata import version
|
|
19
|
+
__version__ = version("mcp-instana")
|
|
20
|
+
except Exception:
|
|
21
|
+
# Fallback version if package metadata is not available
|
|
22
|
+
__version__ = "0.3.1"
|
|
23
|
+
|
|
16
24
|
# Registry to store all tools
|
|
17
25
|
MCP_TOOLS = {}
|
|
18
26
|
|
|
@@ -112,10 +120,6 @@ def with_header_auth(api_class, allow_mock=True):
|
|
|
112
120
|
error_msg = "Instana base URL must start with http:// or https://"
|
|
113
121
|
print(f" {error_msg}", file=sys.stderr)
|
|
114
122
|
return {"error": error_msg}
|
|
115
|
-
|
|
116
|
-
print(" Using header-based authentication (HTTP mode)", file=sys.stderr)
|
|
117
|
-
print(" instana_base_url: ", instana_base_url)
|
|
118
|
-
|
|
119
123
|
# Import SDK components
|
|
120
124
|
from instana_client.api_client import ApiClient
|
|
121
125
|
from instana_client.configuration import Configuration
|
|
@@ -125,9 +129,10 @@ def with_header_auth(api_class, allow_mock=True):
|
|
|
125
129
|
configuration.host = instana_base_url
|
|
126
130
|
configuration.api_key['ApiKeyAuth'] = instana_token
|
|
127
131
|
configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
|
|
128
|
-
configuration.default_headers = {"User-Agent": "MCP-server/0.1.0"}
|
|
129
132
|
|
|
130
133
|
api_client_instance = ApiClient(configuration=configuration)
|
|
134
|
+
user_agent_value = f"MCP-server/{__version__}"
|
|
135
|
+
api_client_instance.set_default_header("User-Agent", header_value=user_agent_value)
|
|
131
136
|
api_instance = api_class(api_client=api_client_instance)
|
|
132
137
|
|
|
133
138
|
# Add the API instance to kwargs so the decorated function can use it
|
|
@@ -178,9 +183,12 @@ def with_header_auth(api_class, allow_mock=True):
|
|
|
178
183
|
configuration.host = self.base_url
|
|
179
184
|
configuration.api_key['ApiKeyAuth'] = self.read_token
|
|
180
185
|
configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
|
|
181
|
-
configuration.default_headers = {"User-Agent": "MCP-server/0.1.0"}
|
|
182
|
-
|
|
183
186
|
api_client_instance = ApiClient(configuration=configuration)
|
|
187
|
+
# Set User-Agent header instead of User-Agent
|
|
188
|
+
user_agent_value = f"MCP-server/{__version__}"
|
|
189
|
+
# Add custom tracking headers
|
|
190
|
+
api_client_instance.set_default_header("User-Agent", user_agent_value)
|
|
191
|
+
print(f"✅ Set User-Agent header: {user_agent_value}", file=sys.stderr)
|
|
184
192
|
api_instance = api_class(api_client=api_client_instance)
|
|
185
193
|
|
|
186
194
|
kwargs['api_client'] = api_instance
|
|
@@ -212,7 +220,8 @@ class BaseInstanaClient:
|
|
|
212
220
|
return {
|
|
213
221
|
"Authorization": f"apiToken {self.read_token}",
|
|
214
222
|
"Content-Type": "application/json",
|
|
215
|
-
"Accept": "application/json"
|
|
223
|
+
"Accept": "application/json",
|
|
224
|
+
"User-Agent": f"MCP-server/{__version__}",
|
|
216
225
|
}
|
|
217
226
|
|
|
218
227
|
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]:
|