mcp-instana 0.3.1__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.
Files changed (30) hide show
  1. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/METADATA +186 -311
  2. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/RECORD +30 -22
  3. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/WHEEL +1 -1
  4. src/application/application_alert_config.py +393 -136
  5. src/application/application_analyze.py +597 -594
  6. src/application/application_call_group.py +528 -0
  7. src/application/application_catalog.py +0 -8
  8. src/application/application_global_alert_config.py +275 -57
  9. src/application/application_metrics.py +377 -237
  10. src/application/application_resources.py +414 -325
  11. src/application/application_settings.py +608 -1530
  12. src/application/application_topology.py +62 -62
  13. src/core/custom_dashboard_smart_router_tool.py +135 -0
  14. src/core/server.py +95 -119
  15. src/core/smart_router_tool.py +574 -0
  16. src/core/utils.py +17 -8
  17. src/custom_dashboard/custom_dashboard_tools.py +422 -0
  18. src/event/events_tools.py +57 -9
  19. src/infrastructure/elicitation_handler.py +338 -0
  20. src/infrastructure/entity_registry.py +329 -0
  21. src/infrastructure/infrastructure_analyze_new.py +600 -0
  22. src/infrastructure/{infrastructure_analyze.py → infrastructure_analyze_old.py} +1 -16
  23. src/infrastructure/infrastructure_catalog.py +37 -32
  24. src/infrastructure/infrastructure_metrics.py +93 -16
  25. src/infrastructure/infrastructure_resources.py +6 -24
  26. src/infrastructure/infrastructure_topology.py +29 -23
  27. src/observability.py +29 -0
  28. src/prompts/application/application_settings.py +58 -0
  29. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/entry_points.txt +0 -0
  30. {mcp_instana-0.3.1.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]: