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.
Files changed (67) hide show
  1. mcp_instana-0.2.0.dist-info/METADATA +1229 -0
  2. mcp_instana-0.2.0.dist-info/RECORD +59 -0
  3. {mcp_instana-0.1.0.dist-info → mcp_instana-0.2.0.dist-info}/WHEEL +1 -1
  4. mcp_instana-0.2.0.dist-info/entry_points.txt +4 -0
  5. mcp_instana-0.1.0.dist-info/LICENSE → mcp_instana-0.2.0.dist-info/licenses/LICENSE.md +3 -3
  6. src/application/__init__.py +1 -0
  7. src/{client/application_alert_config_mcp_tools.py → application/application_alert_config.py} +251 -273
  8. src/application/application_analyze.py +628 -0
  9. src/application/application_catalog.py +155 -0
  10. src/application/application_global_alert_config.py +653 -0
  11. src/{client/application_metrics_mcp_tools.py → application/application_metrics.py} +113 -131
  12. src/{client/application_resources_mcp_tools.py → application/application_resources.py} +131 -151
  13. src/application/application_settings.py +1731 -0
  14. src/application/application_topology.py +111 -0
  15. src/automation/action_catalog.py +416 -0
  16. src/automation/action_history.py +338 -0
  17. src/core/__init__.py +1 -0
  18. src/core/server.py +586 -0
  19. src/core/utils.py +213 -0
  20. src/event/__init__.py +1 -0
  21. src/event/events_tools.py +850 -0
  22. src/infrastructure/__init__.py +1 -0
  23. src/{client/infrastructure_analyze_mcp_tools.py → infrastructure/infrastructure_analyze.py} +207 -206
  24. src/{client/infrastructure_catalog_mcp_tools.py → infrastructure/infrastructure_catalog.py} +197 -265
  25. src/infrastructure/infrastructure_metrics.py +171 -0
  26. src/{client/infrastructure_resources_mcp_tools.py → infrastructure/infrastructure_resources.py} +198 -227
  27. src/{client/infrastructure_topology_mcp_tools.py → infrastructure/infrastructure_topology.py} +110 -109
  28. src/log/__init__.py +1 -0
  29. src/log/log_alert_configuration.py +331 -0
  30. src/prompts/__init__.py +16 -0
  31. src/prompts/application/__init__.py +1 -0
  32. src/prompts/application/application_alerts.py +54 -0
  33. src/prompts/application/application_catalog.py +26 -0
  34. src/prompts/application/application_metrics.py +57 -0
  35. src/prompts/application/application_resources.py +26 -0
  36. src/prompts/application/application_settings.py +75 -0
  37. src/prompts/application/application_topology.py +30 -0
  38. src/prompts/events/__init__.py +1 -0
  39. src/prompts/events/events_tools.py +161 -0
  40. src/prompts/infrastructure/infrastructure_analyze.py +72 -0
  41. src/prompts/infrastructure/infrastructure_catalog.py +53 -0
  42. src/prompts/infrastructure/infrastructure_metrics.py +45 -0
  43. src/prompts/infrastructure/infrastructure_resources.py +74 -0
  44. src/prompts/infrastructure/infrastructure_topology.py +38 -0
  45. src/prompts/settings/__init__.py +0 -0
  46. src/prompts/settings/custom_dashboard.py +157 -0
  47. src/prompts/website/__init__.py +1 -0
  48. src/prompts/website/website_analyze.py +35 -0
  49. src/prompts/website/website_catalog.py +40 -0
  50. src/prompts/website/website_configuration.py +105 -0
  51. src/prompts/website/website_metrics.py +34 -0
  52. src/settings/__init__.py +1 -0
  53. src/settings/custom_dashboard_tools.py +417 -0
  54. src/website/__init__.py +0 -0
  55. src/website/website_analyze.py +433 -0
  56. src/website/website_catalog.py +171 -0
  57. src/website/website_configuration.py +770 -0
  58. src/website/website_metrics.py +241 -0
  59. mcp_instana-0.1.0.dist-info/METADATA +0 -649
  60. mcp_instana-0.1.0.dist-info/RECORD +0 -19
  61. mcp_instana-0.1.0.dist-info/entry_points.txt +0 -3
  62. src/client/What is the sum of queue depth for all q +0 -55
  63. src/client/events_mcp_tools.py +0 -531
  64. src/client/instana_client_base.py +0 -93
  65. src/client/log_alert_configuration_mcp_tools.py +0 -316
  66. src/client/show the top 5 services with the highest +0 -28
  67. src/mcp_server.py +0 -343
src/core/server.py ADDED
@@ -0,0 +1,586 @@
1
+ """
2
+ Standalone MCP Server for Instana Events and Infrastructure Resources
3
+
4
+ This module provides a dedicated MCP server that exposes Instana MCP Server.
5
+ Supports stdio and Streamable HTTP transports.
6
+ """
7
+
8
+ import argparse
9
+ import logging
10
+ import os
11
+ import sys
12
+ from collections.abc import AsyncIterator
13
+ from contextlib import asynccontextmanager
14
+ from dataclasses import dataclass, fields
15
+ from typing import Any
16
+
17
+ from dotenv import load_dotenv
18
+
19
+ from src.prompts import PROMPT_REGISTRY
20
+
21
+ load_dotenv()
22
+
23
+ # Configure logging
24
+ logging.basicConfig(
25
+ level=logging.INFO, # Default level, can be overridden
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ handlers=[
28
+ logging.StreamHandler(sys.stderr)
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ def set_log_level(level_name):
34
+ """Set the logging level based on the provided level name"""
35
+ level_map = {
36
+ "DEBUG": logging.DEBUG,
37
+ "INFO": logging.INFO,
38
+ "WARNING": logging.WARNING,
39
+ "ERROR": logging.ERROR,
40
+ "CRITICAL": logging.CRITICAL
41
+ }
42
+
43
+ level = level_map.get(level_name.upper(), logging.INFO)
44
+ logger.setLevel(level)
45
+ logging.getLogger().setLevel(level)
46
+ logger.info(f"Log level set to {level_name.upper()}")
47
+
48
+ # Add the project root to the Python path
49
+ current_path = os.path.abspath(__file__)
50
+ project_root = os.path.dirname(os.path.dirname(current_path))
51
+ if project_root not in sys.path:
52
+ sys.path.insert(0, project_root)
53
+
54
+ # Import the necessary modules
55
+ try:
56
+ from src.core.utils import MCP_TOOLS, register_as_tool
57
+ except ImportError:
58
+ logger.error("Failed to import required modules", exc_info=True)
59
+ sys.exit(1)
60
+
61
+ from fastmcp import FastMCP
62
+
63
+
64
+ @dataclass
65
+ class MCPState:
66
+ """State for the MCP server."""
67
+ 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
81
+ website_metrics_client: Any = None
82
+ website_catalog_client: Any = None
83
+ website_analyze_client: Any = None
84
+ website_configuration_client: Any = None
85
+
86
+ # Global variables to store credentials for lifespan
87
+ _global_token = None
88
+ _global_base_url = None
89
+
90
+ def get_instana_credentials():
91
+ """Get Instana credentials from environment variables for stdio mode."""
92
+ # For stdio mode, use INSTANA_API_TOKEN and INSTANA_BASE_URL
93
+ token = (os.getenv("INSTANA_API_TOKEN") or "")
94
+ base_url = (os.getenv("INSTANA_BASE_URL") or "")
95
+
96
+ return token, base_url
97
+
98
+ def validate_credentials(token: str, base_url: str) -> bool:
99
+ """Validate that Instana credentials are provided for stdio mode."""
100
+ # For stdio mode, validate INSTANA_API_TOKEN and INSTANA_BASE_URL
101
+ return not (not token or not base_url)
102
+
103
+ def create_clients(token: str, base_url: str, enabled_categories: str = "all") -> MCPState:
104
+ """Create only the enabled Instana clients"""
105
+ state = MCPState()
106
+
107
+ # Get enabled client configurations
108
+ enabled_client_configs = get_enabled_client_configs(enabled_categories)
109
+
110
+ for attr_name, client_class in enabled_client_configs:
111
+ try:
112
+ client = client_class(read_token=token, base_url=base_url)
113
+ setattr(state, attr_name, client)
114
+ except Exception as e:
115
+ logger.error(f"Failed to create {attr_name}: {e}", exc_info=True)
116
+ setattr(state, attr_name, None)
117
+
118
+ return state
119
+
120
+
121
+ @asynccontextmanager
122
+ async def lifespan(server: FastMCP) -> AsyncIterator[MCPState]:
123
+ """Set up and tear down the Instana clients."""
124
+ # Get credentials from environment variables
125
+ token, base_url = get_instana_credentials()
126
+
127
+ try:
128
+ # For lifespan, we'll create all clients since we don't have access to command line args here
129
+ state = create_clients(token, base_url, "all")
130
+
131
+ yield state
132
+ except Exception:
133
+ logger.error("Error during lifespan", exc_info=True)
134
+
135
+ # Yield empty state if client creation failed
136
+ yield MCPState()
137
+
138
+ def create_app(token: str, base_url: str, port: int = int(os.getenv("PORT", "8080")), enabled_categories: str = "all") -> tuple[FastMCP, int]:
139
+ """Create and configure the MCP server with the given credentials."""
140
+ try:
141
+ server = FastMCP(name="Instana MCP Server", host="0.0.0.0", port=port)
142
+
143
+ # Only create and register enabled clients/tools
144
+ clients_state = create_clients(token, base_url, enabled_categories)
145
+
146
+ tools_registered = 0
147
+ for tool_name, _tool_func in MCP_TOOLS.items():
148
+ try:
149
+ client_attr_names = [field.name for field in fields(MCPState)]
150
+ for attr_name in client_attr_names:
151
+ client = getattr(clients_state, attr_name, None)
152
+ if client and hasattr(client, tool_name):
153
+ bound_method = getattr(client, tool_name)
154
+ server.tool()(bound_method)
155
+ tools_registered += 1
156
+ break
157
+ except Exception as e:
158
+ logger.error(f"Failed to register tool {tool_name}: {e}", exc_info=True)
159
+
160
+ # Register prompts from the prompt registry
161
+ # Get enabled prompt categories - use the same categories as tools
162
+ prompt_categories = get_prompt_categories()
163
+
164
+ # Use the same categories for prompts as for tools
165
+ enabled_prompt_categories = []
166
+ if enabled_categories.lower() == "all" or not enabled_categories:
167
+ enabled_prompt_categories = list(prompt_categories.keys())
168
+ logger.info("Enabling all prompt categories")
169
+ else:
170
+ enabled_prompt_categories = [cat.strip() for cat in enabled_categories.split(",") if cat.strip() in prompt_categories]
171
+ logger.info(f"Enabling prompt categories: {', '.join(enabled_prompt_categories)}")
172
+
173
+ # Register prompts to the server
174
+ logger.info("Registering prompts by category:")
175
+ registered_prompts = set()
176
+
177
+ for category, prompt_groups in prompt_categories.items():
178
+ if category in enabled_prompt_categories:
179
+ logger.info(f" - {category}: {len(prompt_groups)} prompt groups")
180
+
181
+ for group_name, prompts in prompt_groups:
182
+ prompt_count = len(prompts)
183
+ logger.info(f" - {group_name}: {prompt_count} prompts")
184
+
185
+ for prompt_name, prompt_func in prompts:
186
+ server.add_prompt(prompt_func)
187
+ registered_prompts.add(prompt_name)
188
+ logger.debug(f" * Registered prompt: {prompt_name}")
189
+ else:
190
+ logger.info(f" - {category}: DISABLED")
191
+
192
+ # Register any remaining prompts that might not be in categories
193
+ uncategorized_count = 0
194
+
195
+ # Just log the count of remaining prompts
196
+ remaining_prompts = len(PROMPT_REGISTRY) - len(registered_prompts)
197
+ if remaining_prompts > 0:
198
+ logger.info(f" - uncategorized: {remaining_prompts} prompts (not registered)")
199
+
200
+ if uncategorized_count > 0:
201
+ logger.info(f" - uncategorized: {uncategorized_count} prompts")
202
+
203
+
204
+ return server, tools_registered
205
+
206
+ except Exception:
207
+ logger.error("Error creating app", exc_info=True)
208
+ fallback_server = FastMCP("Instana Tools")
209
+ return fallback_server, 0 # Return a tuple with 0 tools registered
210
+
211
+ async def execute_tool(tool_name: str, arguments: dict, clients_state) -> str:
212
+ """Execute a tool and return result"""
213
+ try:
214
+ # Get all field names from MCPState dataclass
215
+ client_attr_names = [field.name for field in fields(MCPState)]
216
+
217
+ for attr_name in client_attr_names:
218
+ client = getattr(clients_state, attr_name, None)
219
+ if client and hasattr(client, tool_name):
220
+ method = getattr(client, tool_name)
221
+ result = await method(**arguments)
222
+ return str(result)
223
+
224
+ return f"Tool {tool_name} not found"
225
+ except Exception as e:
226
+ return f"Error executing tool {tool_name}: {e!s}"
227
+
228
+ def get_client_categories():
229
+ """Get client categories with lazy imports to avoid circular dependencies"""
230
+ try:
231
+ from src.application.application_alert_config import ApplicationAlertMCPTools
232
+ from src.application.application_analyze import ApplicationAnalyzeMCPTools
233
+ from src.application.application_catalog import ApplicationCatalogMCPTools
234
+ from src.application.application_global_alert_config import (
235
+ ApplicationGlobalAlertMCPTools,
236
+ )
237
+ from src.application.application_metrics import ApplicationMetricsMCPTools
238
+ from src.application.application_resources import ApplicationResourcesMCPTools
239
+ from src.application.application_settings import ApplicationSettingsMCPTools
240
+ from src.application.application_topology import ApplicationTopologyMCPTools
241
+ from src.automation.action_catalog import ActionCatalogMCPTools
242
+ from src.automation.action_history import ActionHistoryMCPTools
243
+ from src.event.events_tools import AgentMonitoringEventsMCPTools
244
+ from src.infrastructure.infrastructure_analyze import (
245
+ InfrastructureAnalyzeMCPTools,
246
+ )
247
+ from src.infrastructure.infrastructure_catalog import (
248
+ InfrastructureCatalogMCPTools,
249
+ )
250
+ from src.infrastructure.infrastructure_metrics import (
251
+ InfrastructureMetricsMCPTools,
252
+ )
253
+ from src.infrastructure.infrastructure_resources import (
254
+ InfrastructureResourcesMCPTools,
255
+ )
256
+ from src.infrastructure.infrastructure_topology import (
257
+ InfrastructureTopologyMCPTools,
258
+ )
259
+ from src.settings.custom_dashboard_tools import CustomDashboardMCPTools
260
+ from src.website.website_analyze import WebsiteAnalyzeMCPTools
261
+ from src.website.website_catalog import WebsiteCatalogMCPTools
262
+ from src.website.website_configuration import WebsiteConfigurationMCPTools
263
+ from src.website.website_metrics import WebsiteMetricsMCPTools
264
+ except ImportError as e:
265
+ logger.warning(f"Could not import client classes: {e}")
266
+ return {}
267
+
268
+ return {
269
+ "infra": [
270
+ ('infra_client', InfrastructureResourcesMCPTools),
271
+ ('infra_catalog_client', InfrastructureCatalogMCPTools),
272
+ ('infra_topo_client', InfrastructureTopologyMCPTools),
273
+ ('infra_analyze_client', InfrastructureAnalyzeMCPTools),
274
+ ('infra_metrics_client', InfrastructureMetricsMCPTools),
275
+ ],
276
+ "app": [
277
+ ('app_resource_client', ApplicationResourcesMCPTools),
278
+ ('app_metrics_client', ApplicationMetricsMCPTools),
279
+ ('app_alert_client', ApplicationAlertMCPTools),
280
+ ('app_catalog_client', ApplicationCatalogMCPTools),
281
+ ('app_topology_client', ApplicationTopologyMCPTools),
282
+ ('app_analyze_client', ApplicationAnalyzeMCPTools),
283
+ ('app_settings_client', ApplicationSettingsMCPTools),
284
+ ('app_global_alert_client', ApplicationGlobalAlertMCPTools),
285
+ ],
286
+ "events": [
287
+ ('events_client', AgentMonitoringEventsMCPTools),
288
+ ],
289
+ "automation": [
290
+ ('action_catalog_client', ActionCatalogMCPTools),
291
+ ('action_history_client', ActionHistoryMCPTools),
292
+ ],
293
+ "website": [
294
+ ('website_metrics_client', WebsiteMetricsMCPTools),
295
+ ('website_catalog_client', WebsiteCatalogMCPTools),
296
+ ('website_analyze_client', WebsiteAnalyzeMCPTools),
297
+ ('website_configuration_client', WebsiteConfigurationMCPTools),
298
+ ],
299
+ "settings": [
300
+ ('custom_dashboard_client', CustomDashboardMCPTools),
301
+ ]
302
+ }
303
+
304
+ def get_prompt_categories():
305
+ """Get prompt categories organized by functionality"""
306
+ # Import the class-based prompts
307
+ from src.prompts.application.application_alerts import ApplicationAlertsPrompts
308
+ from src.prompts.application.application_catalog import ApplicationCatalogPrompts
309
+ from src.prompts.application.application_metrics import ApplicationMetricsPrompts
310
+ from src.prompts.application.application_resources import (
311
+ ApplicationResourcesPrompts,
312
+ )
313
+ from src.prompts.application.application_settings import ApplicationSettingsPrompts
314
+ from src.prompts.application.application_topology import ApplicationTopologyPrompts
315
+ from src.prompts.infrastructure.infrastructure_analyze import (
316
+ InfrastructureAnalyzePrompts,
317
+ )
318
+ from src.prompts.infrastructure.infrastructure_catalog import (
319
+ InfrastructureCatalogPrompts,
320
+ )
321
+ from src.prompts.infrastructure.infrastructure_metrics import (
322
+ InfrastructureMetricsPrompts,
323
+ )
324
+ from src.prompts.infrastructure.infrastructure_resources import (
325
+ InfrastructureResourcesPrompts,
326
+ )
327
+ from src.prompts.infrastructure.infrastructure_topology import (
328
+ InfrastructureTopologyPrompts,
329
+ )
330
+ from src.prompts.settings.custom_dashboard import CustomDashboardPrompts
331
+ from src.prompts.website.website_analyze import WebsiteAnalyzePrompts
332
+ from src.prompts.website.website_catalog import WebsiteCatalogPrompts
333
+ from src.prompts.website.website_configuration import WebsiteConfigurationPrompts
334
+ from src.prompts.website.website_metrics import WebsiteMetricsPrompts
335
+
336
+ # Use the get_prompts method to get all prompts from the classes
337
+ infra_analyze_prompts = InfrastructureAnalyzePrompts.get_prompts()
338
+ infra_metrics_prompts = InfrastructureMetricsPrompts.get_prompts()
339
+ infra_catalog_prompts = InfrastructureCatalogPrompts.get_prompts()
340
+ infra_topology_prompts = InfrastructureTopologyPrompts.get_prompts()
341
+ infra_resources_prompts = InfrastructureResourcesPrompts.get_prompts()
342
+ app_resources_prompts = ApplicationResourcesPrompts.get_prompts()
343
+ app_metrics_prompts = ApplicationMetricsPrompts.get_prompts()
344
+ app_catalog_prompts = ApplicationCatalogPrompts.get_prompts()
345
+ app_settings_prompts = ApplicationSettingsPrompts.get_prompts()
346
+ app_topology_prompts = ApplicationTopologyPrompts.get_prompts()
347
+ app_alert_prompts = ApplicationAlertsPrompts.get_prompts()
348
+ website_metrics_prompts = WebsiteMetricsPrompts.get_prompts()
349
+ website_catalog_prompts = WebsiteCatalogPrompts.get_prompts()
350
+ website_analyze_prompts = WebsiteAnalyzePrompts.get_prompts()
351
+ website_configuration_prompts = WebsiteConfigurationPrompts.get_prompts()
352
+ custom_dashboard_prompts = CustomDashboardPrompts.get_prompts()
353
+
354
+ # Return the categories with their prompt groups
355
+ return {
356
+ "infra": [
357
+ ('infra_resources_prompts', infra_resources_prompts),
358
+ ('infra_catalog_prompts', infra_catalog_prompts),
359
+ ('infra_topology_prompts', infra_topology_prompts),
360
+ ('infra_analyze_prompts', infra_analyze_prompts),
361
+ ('infra_metrics_prompts', infra_metrics_prompts),
362
+ ],
363
+ "app": [
364
+ ('app_resources_prompts', app_resources_prompts),
365
+ ('app_metrics_prompts', app_metrics_prompts),
366
+ ('app_catalog_prompts', app_catalog_prompts),
367
+ ('app_settings_prompts', app_settings_prompts),
368
+ ('app_topology_prompts', app_topology_prompts),
369
+ ('app_alert_prompts', app_alert_prompts),
370
+ ],
371
+ "website": [
372
+ ('website_metrics_prompts', website_metrics_prompts),
373
+ ('website_catalog_prompts', website_catalog_prompts),
374
+ ('website_analyze_prompts', website_analyze_prompts),
375
+ ('website_configuration_prompts', website_configuration_prompts),
376
+ ],
377
+ "settings": [
378
+ ('custom_dashboard_prompts', custom_dashboard_prompts),
379
+ ],
380
+ }
381
+
382
+ def get_enabled_client_configs(enabled_categories: str):
383
+ """Get client configurations based on enabled categories"""
384
+ # Get client categories with lazy imports
385
+ client_categories = get_client_categories()
386
+
387
+ if not enabled_categories or enabled_categories.lower() == "all":
388
+ all_configs = []
389
+ for category_clients in client_categories.values():
390
+ all_configs.extend(category_clients)
391
+ return all_configs
392
+ categories = [cat.strip() for cat in enabled_categories.split(",")]
393
+ enabled_configs = []
394
+ for category in categories:
395
+ if category in client_categories:
396
+ enabled_configs.extend(client_categories[category])
397
+ else:
398
+ logger.warning(f"Unknown category '{category}'")
399
+ return enabled_configs
400
+
401
+ def main():
402
+ """Main entry point for the MCP server."""
403
+ try:
404
+ # Create and configure the MCP server
405
+ parser = argparse.ArgumentParser(description="Instana MCP Server", add_help=False)
406
+ parser.add_argument(
407
+ "-h", "--help",
408
+ action="store_true",
409
+ dest="help",
410
+ help="show this help message and exit"
411
+ )
412
+ parser.add_argument(
413
+ "--transport",
414
+ type=str,
415
+ choices=["streamable-http","stdio"],
416
+ metavar='<mode>',
417
+ help="Transport mode. Choose from: streamable-http, stdio."
418
+ )
419
+ parser.add_argument(
420
+ "--log-level",
421
+ type=str,
422
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
423
+ default="INFO",
424
+ help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
425
+ )
426
+ parser.add_argument(
427
+ "--debug",
428
+ action="store_true",
429
+ help="Enable debug mode with additional logging (shortcut for --log-level DEBUG)"
430
+ )
431
+ parser.add_argument(
432
+ "--tools",
433
+ type=str,
434
+ metavar='<categories>',
435
+ 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."
436
+ )
437
+ parser.add_argument(
438
+ "--list-tools",
439
+ action="store_true",
440
+ help="List all available tool categories and exit."
441
+ )
442
+ parser.add_argument(
443
+ "--port",
444
+ type=int,
445
+ default=int(os.getenv("PORT", "8080")),
446
+ help="Port to listen on (default: 8080, can be overridden with PORT env var)"
447
+ )
448
+ # Check for help arguments before parsing
449
+ if len(sys.argv) > 1 and any(arg in ['-h','--h','--help','-help'] for arg in sys.argv[1:]):
450
+ # Check if help is combined with other arguments
451
+ help_args = ['-h','--h','--help','-help']
452
+ other_args = [arg for arg in sys.argv[1:] if arg not in help_args]
453
+
454
+ if other_args:
455
+ logger.error("Argument -h/--h/--help/-help: not allowed with other arguments")
456
+ sys.exit(2)
457
+
458
+ # Show help and exit
459
+ try:
460
+ logger.info("Available options:")
461
+ for action in parser._actions:
462
+ # Only print options that start with '--' and have a help string
463
+ if any(opt.startswith('--') for opt in action.option_strings) and action.help:
464
+ # Find the first long option
465
+ long_opt = next((opt for opt in action.option_strings if opt.startswith('--')), None)
466
+ metavar = action.metavar or ''
467
+ opt_str = f"{long_opt} {metavar}".strip()
468
+ logger.info(f"{opt_str:<24} {action.help}")
469
+ sys.exit(0)
470
+ except Exception as e:
471
+ logger.error(f"Error displaying help: {e}")
472
+ sys.exit(0) # Still exit with 0 for help
473
+
474
+ args = parser.parse_args()
475
+
476
+ # Set log level based on command line arguments
477
+ if args.debug:
478
+ set_log_level("DEBUG")
479
+ else:
480
+ set_log_level(args.log_level)
481
+
482
+ all_categories = {"infra", "app", "events", "automation", "website", "settings"}
483
+
484
+ # Handle --list-tools option
485
+ if args.list_tools:
486
+ logger.info("Available tool categories:")
487
+ client_categories = get_client_categories()
488
+ for category, tools in client_categories.items():
489
+ tool_names = [cls.__name__ for _, cls in tools]
490
+ logger.info(f" {category}: {len(tool_names)} tools")
491
+ for tool_name in tool_names:
492
+ logger.info(f" - {tool_name}")
493
+ sys.exit(0)
494
+
495
+ # By default, enable all categories
496
+ enabled = set(all_categories)
497
+ invalid = set()
498
+
499
+ # Enable only specified categories if --tools is provided
500
+ if args.tools:
501
+ specified_tools = {cat.strip() for cat in args.tools.split(",")}
502
+ invalid = specified_tools - all_categories
503
+ enabled = specified_tools & all_categories
504
+
505
+ # If no valid tools specified, default to all
506
+ if not enabled:
507
+ enabled = set(all_categories)
508
+
509
+ if invalid:
510
+ logger.error(f"Error: Unknown category/categories: {', '.join(invalid)}. Available categories: infra, app, events, automation, website, settings")
511
+ sys.exit(2)
512
+
513
+ # Print enabled tools for user information
514
+ enabled_tool_classes = []
515
+ client_categories = get_client_categories()
516
+
517
+ # Log enabled categories and tools
518
+ logger.info(f"Enabled tool categories: {', '.join(enabled)}")
519
+
520
+ for category in enabled:
521
+ if category in client_categories:
522
+ category_tools = [cls.__name__ for _, cls in client_categories[category]]
523
+ enabled_tool_classes.extend(category_tools)
524
+ logger.info(f" - {category}: {len(category_tools)} tools")
525
+ for tool_name in category_tools:
526
+ logger.info(f" * {tool_name}")
527
+
528
+ if enabled_tool_classes:
529
+ logger.info(
530
+ f"Total enabled tools: {len(enabled_tool_classes)}"
531
+ )
532
+
533
+ # Get credentials from environment variables for stdio mode
534
+ INSTANA_API_TOKEN, INSTANA_BASE_URL = get_instana_credentials()
535
+
536
+ if args.transport == "stdio" or args.transport is None:
537
+ if not validate_credentials(INSTANA_API_TOKEN, INSTANA_BASE_URL):
538
+ logger.error("Error: Instana credentials are required for stdio mode but not provided. Please set INSTANA_API_TOKEN and INSTANA_BASE_URL environment variables.")
539
+ sys.exit(1)
540
+
541
+ # Create and configure the MCP server
542
+ try:
543
+ enabled_categories = ",".join(enabled)
544
+ # Ensure create_app is always called, even if credentials are missing
545
+ # This is needed for test_main_function_missing_token
546
+ app, registered_tool_count = create_app(INSTANA_API_TOKEN, INSTANA_BASE_URL, args.port, enabled_categories)
547
+ except Exception as e:
548
+ print(f"Failed to create MCP server: {e}", file=sys.stderr)
549
+ sys.exit(1)
550
+
551
+ # Run the server with the appropriate transport
552
+ if args.transport == "streamable-http":
553
+ if args.debug:
554
+ logger.info(f"FastMCP instance: {app}")
555
+ logger.info(f"Registered tools: {registered_tool_count}")
556
+ try:
557
+ app.run(transport="streamable-http")
558
+ except Exception as e:
559
+ logger.error(f"Failed to start HTTP server: {e}")
560
+ if args.debug:
561
+ logger.error("HTTP server error details", exc_info=True)
562
+ sys.exit(1)
563
+ else:
564
+ logger.info("Starting stdio transport")
565
+ try:
566
+ app.run(transport="stdio")
567
+ except AttributeError as e:
568
+ # Handle the case where sys.stdout is a StringIO object (in tests)
569
+ if "'_io.StringIO' object has no attribute 'buffer'" in str(e):
570
+ logger.info("Running in test mode, skipping stdio server")
571
+ else:
572
+ raise
573
+
574
+ except KeyboardInterrupt:
575
+ logger.info("Server stopped by user")
576
+ sys.exit(0)
577
+ except Exception as e:
578
+ logger.error(f"Server error: {e}", exc_info=True)
579
+ sys.exit(1)
580
+
581
+ if __name__ == "__main__":
582
+ try:
583
+ main()
584
+ except Exception:
585
+ logger.error("Unhandled exception in main", exc_info=True)
586
+ sys.exit(1)