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
@@ -55,65 +55,65 @@ class ApplicationTopologyMCPTools(BaseInstanaClient):
55
55
  logger.error(f"Error initializing ApplicationTopologyMCPTools: {e}", exc_info=True)
56
56
  raise
57
57
 
58
- @register_as_tool(
59
- title="Get Application Topology",
60
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
61
- )
62
- async def get_application_topology(self,
63
- window_size: Optional[int] = None,
64
- to_timestamp: Optional[int] = None,
65
- application_id: Optional[str] = None,
66
- application_boundary_scope: Optional[str] = None,
67
- ctx = None) -> Dict[str, Any]:
68
- """
69
- Get the service topology from Instana Server.
70
- This tool retrieves services and connections (call paths) between them for calls in the scope given by the parameters.
71
-
72
- Args:
73
- window_size: Size of time window in milliseconds
74
- to_timestamp: Timestamp since Unix Epoch in milliseconds of the end of the time window
75
- application_id: Filter by application ID
76
- application_boundary_scope: Filter by application scope, i.e., INBOUND or ALL. The default value is INBOUND.
77
- ctx: Context information
78
-
79
- Returns:
80
- A dictionary containing the service topology data
81
- """
82
-
83
- try:
84
- logger.debug("Fetching service topology data")
85
-
86
- # Set default values if not provided
87
- if not to_timestamp:
88
- to_timestamp = int(datetime.now().timestamp() * 1000)
89
-
90
- if not window_size:
91
- window_size = 3600000 # Default to 1 hour in milliseconds
92
-
93
- # Call the API with raw JSON response to avoid Pydantic validation issues
94
- # Note: The SDK expects parameters in camelCase, but we use snake_case in Python
95
- # The SDK will handle the conversion
96
- result = self.topology_api.get_services_map_without_preload_content(
97
- window_size=window_size,
98
- to=to_timestamp,
99
- application_id=application_id,
100
- application_boundary_scope=application_boundary_scope
101
- )
102
-
103
- # Parse the JSON response manually
104
- import json
105
- try:
106
- # The result from get_services_map_without_preload_content is a response object
107
- # We need to read the response data and parse it as JSON
108
- response_text = result.data.decode('utf-8')
109
- result_dict = json.loads(response_text)
110
- logger.debug("Successfully retrieved service topology data")
111
- return result_dict
112
- except (json.JSONDecodeError, AttributeError) as json_err:
113
- error_message = f"Failed to parse JSON response: {json_err}"
114
- logger.error(error_message)
115
- return {"error": error_message}
116
-
117
- except Exception as e:
118
- logger.error(f"Error in get_application_topology: {e}", exc_info=True)
119
- return {"error": f"Failed to get application topology: {e!s}"}
58
+ # @register_as_tool(
59
+ # title="Get Application Topology",
60
+ # annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
61
+ # )
62
+ # async def get_application_topology(self,
63
+ # window_size: Optional[int] = None,
64
+ # to_timestamp: Optional[int] = None,
65
+ # application_id: Optional[str] = None,
66
+ # application_boundary_scope: Optional[str] = None,
67
+ # ctx = None) -> Dict[str, Any]:
68
+ # """
69
+ # Get the service topology from Instana Server.
70
+ # This tool retrieves services and connections (call paths) between them for calls in the scope given by the parameters.
71
+
72
+ # Args:
73
+ # window_size: Size of time window in milliseconds
74
+ # to_timestamp: Timestamp since Unix Epoch in milliseconds of the end of the time window
75
+ # application_id: Filter by application ID
76
+ # application_boundary_scope: Filter by application scope, i.e., INBOUND or ALL. The default value is INBOUND.
77
+ # ctx: Context information
78
+
79
+ # Returns:
80
+ # A dictionary containing the service topology data
81
+ # """
82
+
83
+ # try:
84
+ # logger.debug("Fetching service topology data")
85
+
86
+ # # Set default values if not provided
87
+ # if not to_timestamp:
88
+ # to_timestamp = int(datetime.now().timestamp() * 1000)
89
+
90
+ # if not window_size:
91
+ # window_size = 3600000 # Default to 1 hour in milliseconds
92
+
93
+ # # Call the API with raw JSON response to avoid Pydantic validation issues
94
+ # # Note: The SDK expects parameters in camelCase, but we use snake_case in Python
95
+ # # The SDK will handle the conversion
96
+ # result = self.topology_api.get_services_map_without_preload_content(
97
+ # window_size=window_size,
98
+ # to=to_timestamp,
99
+ # application_id=application_id,
100
+ # application_boundary_scope=application_boundary_scope
101
+ # )
102
+
103
+ # # Parse the JSON response manually
104
+ # import json
105
+ # try:
106
+ # # The result from get_services_map_without_preload_content is a response object
107
+ # # We need to read the response data and parse it as JSON
108
+ # response_text = result.data.decode('utf-8')
109
+ # result_dict = json.loads(response_text)
110
+ # logger.debug("Successfully retrieved service topology data")
111
+ # return result_dict
112
+ # except (json.JSONDecodeError, AttributeError) as json_err:
113
+ # error_message = f"Failed to parse JSON response: {json_err}"
114
+ # logger.error(error_message)
115
+ # return {"error": error_message}
116
+
117
+ # except Exception as e:
118
+ # logger.error(f"Error in get_application_topology: {e}", exc_info=True)
119
+ # return {"error": f"Failed to get application topology: {e!s}"}
@@ -0,0 +1,135 @@
1
+ """
2
+ Custom Dashboard Smart Router Tool
3
+
4
+ This module provides a unified MCP tool that routes queries to the appropriate
5
+ custom dashboard-specific tools for Instana monitoring.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, Optional
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 CustomDashboardSmartRouterMCPTool(BaseInstanaClient):
19
+ """
20
+ Smart router that routes queries to Custom Dashboard 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 Custom Dashboard Smart Router MCP tool."""
26
+ super().__init__(read_token=read_token, base_url=base_url)
27
+
28
+ # Initialize the custom dashboard tool client
29
+ from src.custom_dashboard.custom_dashboard_tools import CustomDashboardMCPTools
30
+
31
+ self.dashboard_client = CustomDashboardMCPTools(read_token, base_url)
32
+
33
+ logger.info("Custom Dashboard Smart Router initialized")
34
+
35
+ @register_as_tool(
36
+ title="Manage Instana Custom Dashboards",
37
+ annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
38
+ )
39
+ async def manage_custom_dashboards(
40
+ self,
41
+ operation: str,
42
+ params: Optional[Dict[str, Any]] = None,
43
+ ctx=None
44
+ ) -> Dict[str, Any]:
45
+ """
46
+ Unified Instana custom dashboard manager for CRUD operations.
47
+
48
+ Operations:
49
+ - "get_all": Get all custom dashboards
50
+ - "get": Get a specific dashboard by ID
51
+ - "create": Create a new custom dashboard
52
+ - "update": Update an existing custom dashboard
53
+ - "delete": Delete a custom dashboard
54
+ - "get_shareable_users": Get shareable users for a dashboard
55
+ - "get_shareable_api_tokens": Get shareable API tokens for a dashboard
56
+
57
+ Parameters (params dict):
58
+ - dashboard_id: Dashboard ID (required for get, update, delete, get_shareable_users, get_shareable_api_tokens)
59
+ - custom_dashboard: Dashboard configuration payload (required for create, update)
60
+
61
+ Args:
62
+ operation: Operation to perform
63
+ params: Operation-specific parameters (optional)
64
+ ctx: MCP context (internal)
65
+
66
+ Returns:
67
+ Dictionary with results from the appropriate tool
68
+
69
+ Examples:
70
+ # Get all dashboards
71
+ operation="get_all"
72
+
73
+ # Get specific dashboard
74
+ operation="get", params={"dashboard_id": "abc123"}
75
+
76
+ # Create dashboard
77
+ operation="create", params={"custom_dashboard": {"title": "My Dashboard", "widgets": []}}
78
+
79
+ # Update dashboard
80
+ operation="update", params={"dashboard_id": "abc123", "custom_dashboard": {"title": "Updated Dashboard"}}
81
+
82
+ # Delete dashboard
83
+ operation="delete", params={"dashboard_id": "abc123"}
84
+
85
+ # Get shareable users
86
+ operation="get_shareable_users", params={"dashboard_id": "abc123"}
87
+
88
+ # Get shareable API tokens
89
+ operation="get_shareable_api_tokens", params={"dashboard_id": "abc123"}
90
+ """
91
+ try:
92
+ logger.info(f"Custom Dashboard Smart Router received: operation={operation}")
93
+
94
+ # Initialize params if not provided
95
+ if params is None:
96
+ params = {}
97
+
98
+ # Validate operation
99
+ valid_operations = [
100
+ "get_all", "get", "create", "update", "delete",
101
+ "get_shareable_users", "get_shareable_api_tokens"
102
+ ]
103
+
104
+ if operation not in valid_operations:
105
+ return {
106
+ "error": f"Invalid operation '{operation}'",
107
+ "valid_operations": valid_operations
108
+ }
109
+
110
+ # Extract parameters
111
+ dashboard_id = params.get("dashboard_id")
112
+ custom_dashboard = params.get("custom_dashboard")
113
+
114
+ # Route to the dashboard client
115
+ logger.info(f"Routing to Custom Dashboard client for operation: {operation}")
116
+
117
+ result = await self.dashboard_client.execute_dashboard_operation(
118
+ operation=operation,
119
+ dashboard_id=dashboard_id,
120
+ custom_dashboard=custom_dashboard,
121
+ ctx=ctx
122
+ )
123
+
124
+ return {
125
+ "operation": operation,
126
+ "dashboard_id": dashboard_id if dashboard_id else None,
127
+ "results": result
128
+ }
129
+
130
+ except Exception as e:
131
+ logger.error(f"Error in custom dashboard smart router: {e}", exc_info=True)
132
+ return {
133
+ "error": f"Custom dashboard smart router error: {e!s}",
134
+ "operation": operation
135
+ }
src/core/server.py CHANGED
@@ -20,6 +20,8 @@ from src.prompts import PROMPT_REGISTRY
20
20
 
21
21
  load_dotenv()
22
22
 
23
+ from src.observability import task, workflow
24
+
23
25
  # Configure logging
24
26
  logging.basicConfig(
25
27
  level=logging.INFO, # Default level, can be overridden
@@ -63,26 +65,30 @@ from fastmcp import FastMCP
63
65
 
64
66
  @dataclass
65
67
  class MCPState:
66
- """State for the MCP server."""
68
+ """State for the MCP server with all tool categories."""
69
+ # Router tools
70
+ smart_router_client: Any = None
71
+ custom_dashboard_smart_router_client: Any = None
72
+
73
+ # Infrastructure - Only the new two-pass elicitation tool
74
+ infra_analyze_new_client: Any = None
75
+
76
+ # Events tools
67
77
  events_client: Any = None
68
- infra_client: Any = None
69
- app_resource_client: Any = None
70
- app_metrics_client: Any = None
71
- app_alert_client: Any = None
72
- infra_catalog_client: Any = None
73
- infra_topo_client: Any = None
74
- infra_analyze_client: Any = None
75
- infra_metrics_client: Any = None
76
- app_catalog_client: Any = None
77
- app_topology_client: Any = None
78
- app_analyze_client: Any = None
79
- app_settings_client: Any = None
80
- app_global_alert_client: Any = None
78
+
79
+ # Automation tools
80
+ action_catalog_client: Any = None
81
+ action_history_client: Any = None
82
+
83
+ # Website tools
81
84
  website_metrics_client: Any = None
82
85
  website_catalog_client: Any = None
83
86
  website_analyze_client: Any = None
84
87
  website_configuration_client: Any = None
85
88
 
89
+ # Settings tools
90
+ custom_dashboard_client: Any = None
91
+
86
92
  # Global variables to store credentials for lifespan
87
93
  _global_token = None
88
94
  _global_base_url = None
@@ -234,33 +240,15 @@ async def execute_tool(tool_name: str, arguments: dict, clients_state) -> str:
234
240
  def get_client_categories():
235
241
  """Get client categories with lazy imports to avoid circular dependencies"""
236
242
  try:
237
- from src.application.application_alert_config import ApplicationAlertMCPTools
238
- from src.application.application_analyze import ApplicationAnalyzeMCPTools
239
- from src.application.application_catalog import ApplicationCatalogMCPTools
240
- from src.application.application_global_alert_config import (
241
- ApplicationGlobalAlertMCPTools,
242
- )
243
- from src.application.application_metrics import ApplicationMetricsMCPTools
244
- from src.application.application_resources import ApplicationResourcesMCPTools
245
- from src.application.application_settings import ApplicationSettingsMCPTools
246
- from src.application.application_topology import ApplicationTopologyMCPTools
247
243
  from src.automation.action_catalog import ActionCatalogMCPTools
248
244
  from src.automation.action_history import ActionHistoryMCPTools
249
- from src.event.events_tools import AgentMonitoringEventsMCPTools
250
- from src.infrastructure.infrastructure_analyze import (
251
- InfrastructureAnalyzeMCPTools,
252
- )
253
- from src.infrastructure.infrastructure_catalog import (
254
- InfrastructureCatalogMCPTools,
255
- )
256
- from src.infrastructure.infrastructure_metrics import (
257
- InfrastructureMetricsMCPTools,
258
- )
259
- from src.infrastructure.infrastructure_resources import (
260
- InfrastructureResourcesMCPTools,
245
+ from src.core.custom_dashboard_smart_router_tool import (
246
+ CustomDashboardSmartRouterMCPTool,
261
247
  )
262
- from src.infrastructure.infrastructure_topology import (
263
- InfrastructureTopologyMCPTools,
248
+ from src.core.smart_router_tool import SmartRouterMCPTool
249
+ from src.event.events_tools import AgentMonitoringEventsMCPTools
250
+ from src.infrastructure.infrastructure_analyze_new import (
251
+ InfrastructureAnalyzeOption2,
264
252
  )
265
253
  from src.settings.custom_dashboard_tools import CustomDashboardMCPTools
266
254
  from src.website.website_analyze import WebsiteAnalyzeMCPTools
@@ -272,22 +260,12 @@ def get_client_categories():
272
260
  return {}
273
261
 
274
262
  return {
275
- "infra": [
276
- ('infra_client', InfrastructureResourcesMCPTools),
277
- ('infra_catalog_client', InfrastructureCatalogMCPTools),
278
- ('infra_topo_client', InfrastructureTopologyMCPTools),
279
- ('infra_analyze_client', InfrastructureAnalyzeMCPTools),
280
- ('infra_metrics_client', InfrastructureMetricsMCPTools),
263
+ "router": [
264
+ ('smart_router_client', SmartRouterMCPTool),
265
+ ('custom_dashboard_smart_router_client', CustomDashboardSmartRouterMCPTool),
281
266
  ],
282
- "app": [
283
- ('app_resource_client', ApplicationResourcesMCPTools),
284
- ('app_metrics_client', ApplicationMetricsMCPTools),
285
- ('app_alert_client', ApplicationAlertMCPTools),
286
- ('app_catalog_client', ApplicationCatalogMCPTools),
287
- ('app_topology_client', ApplicationTopologyMCPTools),
288
- ('app_analyze_client', ApplicationAnalyzeMCPTools),
289
- ('app_settings_client', ApplicationSettingsMCPTools),
290
- ('app_global_alert_client', ApplicationGlobalAlertMCPTools),
267
+ "infra": [
268
+ ('infra_analyze_new_client', InfrastructureAnalyzeOption2),
291
269
  ],
292
270
  "events": [
293
271
  ('events_client', AgentMonitoringEventsMCPTools),
@@ -310,79 +288,65 @@ def get_client_categories():
310
288
  def get_prompt_categories():
311
289
  """Get prompt categories organized by functionality"""
312
290
  # Import the class-based prompts
313
- from src.prompts.application.application_alerts import ApplicationAlertsPrompts
314
- from src.prompts.application.application_catalog import ApplicationCatalogPrompts
315
- from src.prompts.application.application_metrics import ApplicationMetricsPrompts
316
- from src.prompts.application.application_resources import (
317
- ApplicationResourcesPrompts,
318
- )
319
- from src.prompts.application.application_settings import ApplicationSettingsPrompts
320
- from src.prompts.application.application_topology import ApplicationTopologyPrompts
321
- from src.prompts.infrastructure.infrastructure_analyze import (
322
- InfrastructureAnalyzePrompts,
323
- )
324
- from src.prompts.infrastructure.infrastructure_catalog import (
325
- InfrastructureCatalogPrompts,
326
- )
327
- from src.prompts.infrastructure.infrastructure_metrics import (
328
- InfrastructureMetricsPrompts,
329
- )
330
- from src.prompts.infrastructure.infrastructure_resources import (
331
- InfrastructureResourcesPrompts,
332
- )
333
- from src.prompts.infrastructure.infrastructure_topology import (
334
- InfrastructureTopologyPrompts,
335
- )
336
- from src.prompts.settings.custom_dashboard import CustomDashboardPrompts
337
- from src.prompts.website.website_analyze import WebsiteAnalyzePrompts
338
- from src.prompts.website.website_catalog import WebsiteCatalogPrompts
339
- from src.prompts.website.website_configuration import WebsiteConfigurationPrompts
340
- from src.prompts.website.website_metrics import WebsiteMetricsPrompts
341
-
342
- # Use the get_prompts method to get all prompts from the classes
343
- infra_analyze_prompts = InfrastructureAnalyzePrompts.get_prompts()
344
- infra_metrics_prompts = InfrastructureMetricsPrompts.get_prompts()
345
- infra_catalog_prompts = InfrastructureCatalogPrompts.get_prompts()
346
- infra_topology_prompts = InfrastructureTopologyPrompts.get_prompts()
347
- infra_resources_prompts = InfrastructureResourcesPrompts.get_prompts()
348
- app_resources_prompts = ApplicationResourcesPrompts.get_prompts()
291
+ try:
292
+ from src.prompts.application.application_alerts import ApplicationAlertsPrompts
293
+ from src.prompts.application.application_metrics import (
294
+ ApplicationMetricsPrompts,
295
+ )
296
+ from src.prompts.application.application_resources import (
297
+ ApplicationResourcesPrompts,
298
+ )
299
+ from src.prompts.application.application_settings import (
300
+ ApplicationSettingsPrompts,
301
+ )
302
+ from src.prompts.application.application_topology import (
303
+ ApplicationTopologyPrompts,
304
+ )
305
+ from src.prompts.events.events_tools import EventsPrompts
306
+ from src.prompts.settings.custom_dashboard import CustomDashboardPrompts
307
+ from src.prompts.website.website_analyze import WebsiteAnalyzePrompts
308
+ from src.prompts.website.website_catalog import WebsiteCatalogPrompts
309
+ from src.prompts.website.website_configuration import (
310
+ WebsiteConfigurationPrompts,
311
+ )
312
+ from src.prompts.website.website_metrics import WebsiteMetricsPrompts
313
+ except ImportError as e:
314
+ logger.warning(f"Could not import prompt classes: {e}")
315
+ return {}
316
+
317
+ # Get prompts from each class
318
+ app_alerts_prompts = ApplicationAlertsPrompts.get_prompts()
349
319
  app_metrics_prompts = ApplicationMetricsPrompts.get_prompts()
350
- app_catalog_prompts = ApplicationCatalogPrompts.get_prompts()
320
+ app_resources_prompts = ApplicationResourcesPrompts.get_prompts()
351
321
  app_settings_prompts = ApplicationSettingsPrompts.get_prompts()
352
322
  app_topology_prompts = ApplicationTopologyPrompts.get_prompts()
353
- app_alert_prompts = ApplicationAlertsPrompts.get_prompts()
354
- website_metrics_prompts = WebsiteMetricsPrompts.get_prompts()
355
- website_catalog_prompts = WebsiteCatalogPrompts.get_prompts()
323
+ events_prompts = EventsPrompts.get_prompts()
324
+ custom_dashboard_prompts = CustomDashboardPrompts.get_prompts()
356
325
  website_analyze_prompts = WebsiteAnalyzePrompts.get_prompts()
326
+ website_catalog_prompts = WebsiteCatalogPrompts.get_prompts()
357
327
  website_configuration_prompts = WebsiteConfigurationPrompts.get_prompts()
358
- custom_dashboard_prompts = CustomDashboardPrompts.get_prompts()
328
+ website_metrics_prompts = WebsiteMetricsPrompts.get_prompts()
359
329
 
360
- # Return the categories with their prompt groups
361
330
  return {
362
- "infra": [
363
- ('infra_resources_prompts', infra_resources_prompts),
364
- ('infra_catalog_prompts', infra_catalog_prompts),
365
- ('infra_topology_prompts', infra_topology_prompts),
366
- ('infra_analyze_prompts', infra_analyze_prompts),
367
- ('infra_metrics_prompts', infra_metrics_prompts),
368
- ],
369
331
  "app": [
370
- ('app_resources_prompts', app_resources_prompts),
371
- ('app_metrics_prompts', app_metrics_prompts),
372
- ('app_catalog_prompts', app_catalog_prompts),
373
- ('app_settings_prompts', app_settings_prompts),
374
- ('app_topology_prompts', app_topology_prompts),
375
- ('app_alert_prompts', app_alert_prompts),
332
+ ("Application Alerts", app_alerts_prompts),
333
+ ("Application Metrics", app_metrics_prompts),
334
+ ("Application Resources", app_resources_prompts),
335
+ ("Application Settings", app_settings_prompts),
336
+ ("Application Topology", app_topology_prompts),
337
+ ],
338
+ "events": [
339
+ ("Events Tools", events_prompts),
376
340
  ],
377
341
  "website": [
378
- ('website_metrics_prompts', website_metrics_prompts),
379
- ('website_catalog_prompts', website_catalog_prompts),
380
- ('website_analyze_prompts', website_analyze_prompts),
381
- ('website_configuration_prompts', website_configuration_prompts),
342
+ ("Website Analyze", website_analyze_prompts),
343
+ ("Website Catalog", website_catalog_prompts),
344
+ ("Website Configuration", website_configuration_prompts),
345
+ ("Website Metrics", website_metrics_prompts),
382
346
  ],
383
347
  "settings": [
384
- ('custom_dashboard_prompts', custom_dashboard_prompts),
385
- ],
348
+ ("Custom Dashboard", custom_dashboard_prompts),
349
+ ]
386
350
  }
387
351
 
388
352
  def get_enabled_client_configs(enabled_categories: str):
@@ -404,6 +368,7 @@ def get_enabled_client_configs(enabled_categories: str):
404
368
  logger.warning(f"Unknown category '{category}'")
405
369
  return enabled_configs
406
370
 
371
+ @workflow(name="instana_mcp_workflow")
407
372
  def main():
408
373
  """Main entry point for the MCP server."""
409
374
  try:
@@ -438,7 +403,7 @@ def main():
438
403
  "--tools",
439
404
  type=str,
440
405
  metavar='<categories>',
441
- help="Comma-separated list of tool categories to enable (--tools infra,app,events,automation,website, settings). Also controls which prompts are enabled. If not provided, all tools and prompts are enabled."
406
+ help="Comma-separated list of tool categories to enable (--tools router,infra,app,events,automation,website,settings). Also controls which prompts are enabled. If not provided, all tools and prompts are enabled. Use 'router' for smart routing across app and infra metrics."
442
407
  )
443
408
  parser.add_argument(
444
409
  "--list-tools",
@@ -451,6 +416,16 @@ def main():
451
416
  default=int(os.getenv("PORT", "8080")),
452
417
  help="Port to listen on (default: 8080, can be overridden with PORT env var)"
453
418
  )
419
+ parser.add_argument(
420
+ "--api-token",
421
+ type=str,
422
+ help="Instana API token (overrides INSTANA_API_TOKEN env var)"
423
+ )
424
+ parser.add_argument(
425
+ "--base-url",
426
+ type=str,
427
+ help="Instana base URL (overrides INSTANA_BASE_URL env var)"
428
+ )
454
429
  # Check for help arguments before parsing
455
430
  if len(sys.argv) > 1 and any(arg in ['-h','--h','--help','-help'] for arg in sys.argv[1:]):
456
431
  # Check if help is combined with other arguments
@@ -485,7 +460,7 @@ def main():
485
460
  else:
486
461
  set_log_level(args.log_level)
487
462
 
488
- all_categories = {"infra", "app", "events", "automation", "website", "settings"}
463
+ all_categories = {"router", "infra", "app", "events", "automation", "website", "settings"}
489
464
 
490
465
  # Handle --list-tools option
491
466
  if args.list_tools:
@@ -513,7 +488,7 @@ def main():
513
488
  enabled = set(all_categories)
514
489
 
515
490
  if invalid:
516
- logger.error(f"Error: Unknown category/categories: {', '.join(invalid)}. Available categories: infra, app, events, automation, website, settings")
491
+ logger.error(f"Error: Unknown category/categories: {', '.join(invalid)}. Available categories: router, infra, app, events, automation, website, settings")
517
492
  sys.exit(2)
518
493
 
519
494
  # Print enabled tools for user information
@@ -536,8 +511,9 @@ def main():
536
511
  f"Total enabled tools: {len(enabled_tool_classes)}"
537
512
  )
538
513
 
539
- # Get credentials from environment variables for stdio mode
540
- INSTANA_API_TOKEN, INSTANA_BASE_URL = get_instana_credentials()
514
+ # Get credentials from command line args or environment variables
515
+ INSTANA_API_TOKEN = args.api_token if args.api_token else os.getenv("INSTANA_API_TOKEN", "")
516
+ INSTANA_BASE_URL = args.base_url if args.base_url else os.getenv("INSTANA_BASE_URL", "")
541
517
 
542
518
  if args.transport == "stdio" or args.transport is None:
543
519
  if not validate_credentials(INSTANA_API_TOKEN, INSTANA_BASE_URL):