mcp-instana 0.1.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.
@@ -0,0 +1,316 @@
1
+ """
2
+ Log Alert Configuration MCP Tools Module
3
+
4
+ This module provides log alert configuration-specific MCP tools for Instana monitoring.
5
+ """
6
+
7
+ import sys
8
+ import traceback
9
+ from typing import Dict, Any, List, Optional
10
+ from datetime import datetime
11
+
12
+ # Import the necessary classes from the SDK
13
+ try:
14
+ from instana_client.api.log_alert_configuration_api import LogAlertConfigurationApi
15
+ from instana_client.api_client import ApiClient
16
+ from instana_client.configuration import Configuration
17
+ from instana_client.models.log_alert_config import LogAlertConfig
18
+ except ImportError as e:
19
+ traceback.print_exc(file=sys.stderr)
20
+ raise
21
+
22
+ from .instana_client_base import BaseInstanaClient, register_as_tool
23
+
24
+ # Helper function for debug printing
25
+ def debug_print(*args, **kwargs):
26
+ """Print debug information to stderr instead of stdout"""
27
+ print(*args, file=sys.stderr, **kwargs)
28
+
29
+ class LogAlertConfigurationMCPTools(BaseInstanaClient):
30
+ """Tools for log alert configuration in Instana MCP."""
31
+
32
+ def __init__(self, read_token: str, base_url: str):
33
+ """Initialize the Log Alert Configuration MCP tools client."""
34
+ super().__init__(read_token=read_token, base_url=base_url)
35
+
36
+ try:
37
+ debug_print(f"Initializing LogAlertConfigurationMCPTools with base_url={base_url}")
38
+
39
+ # Configure the API client with the correct base URL and authentication
40
+ configuration = Configuration()
41
+ configuration.host = base_url
42
+ configuration.api_key['ApiKeyAuth'] = read_token
43
+ configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
44
+
45
+ # Create an API client with this configuration
46
+ api_client = ApiClient(configuration=configuration)
47
+
48
+ # Initialize the Instana SDK's LogAlertConfigurationApi with our configured client
49
+ self.log_alert_api = LogAlertConfigurationApi(api_client=api_client)
50
+ debug_print(f"Initialized LogAlertConfigurationApi with host: {configuration.host}")
51
+ except Exception as e:
52
+ debug_print(f"Error initializing LogAlertConfigurationApi: {e}")
53
+ traceback.print_exc(file=sys.stderr)
54
+ raise
55
+
56
+ @register_as_tool
57
+ async def create_log_alert_config(self, config: Dict[str, Any], ctx=None) -> Dict[str, Any]:
58
+ """
59
+ Create a new log alert configuration.
60
+
61
+ Args:
62
+ config: Dictionary containing the log alert configuration
63
+ Required fields:
64
+ - name: Name of the alert
65
+ - query: Log query string
66
+ - threshold: Threshold value for the alert
67
+ - timeThreshold: Time threshold in milliseconds
68
+ - rule: Rule configuration
69
+ ctx: The MCP context (optional)
70
+
71
+ Returns:
72
+ Dictionary containing the created log alert configuration or error information
73
+ """
74
+ try:
75
+ debug_print(f"create_log_alert_config called with config={config}")
76
+
77
+ # Convert dictionary to LogAlertConfig model
78
+ log_alert_config = LogAlertConfig(**config)
79
+
80
+ # Call the API
81
+ result = self.log_alert_api.create_log_alert_config(log_alert_config=log_alert_config)
82
+
83
+ debug_print(f"Result from create_log_alert_config: {result}")
84
+ return self._convert_to_dict(result)
85
+ except Exception as e:
86
+ debug_print(f"Error in create_log_alert_config: {e}")
87
+ import traceback
88
+ traceback.print_exc(file=sys.stderr)
89
+ return {"error": f"Failed to create log alert configuration: {str(e)}"}
90
+
91
+ @register_as_tool
92
+ async def delete_log_alert_config(self, id: str, ctx=None) -> Dict[str, Any]:
93
+ """
94
+ Delete a log alert configuration.
95
+
96
+ Args:
97
+ id: ID of the log alert configuration to delete
98
+ ctx: The MCP context (optional)
99
+
100
+ Returns:
101
+ Dictionary containing the result of the deletion operation or error information
102
+ """
103
+ try:
104
+ debug_print(f"delete_log_alert_config called with id={id}")
105
+
106
+ self.log_alert_api.delete_log_alert_config(id=id)
107
+
108
+ debug_print(f"Successfully deleted log alert configuration with ID {id}")
109
+ return {"success": True, "message": f"Log alert configuration with ID {id} deleted successfully"}
110
+ except Exception as e:
111
+ debug_print(f"Error in delete_log_alert_config: {e}")
112
+ import traceback
113
+ traceback.print_exc(file=sys.stderr)
114
+ return {"error": f"Failed to delete log alert configuration: {str(e)}"}
115
+
116
+ @register_as_tool
117
+ async def disable_log_alert_config(self, id: str, ctx=None) -> Dict[str, Any]:
118
+ """
119
+ Disable a log alert configuration.
120
+
121
+ Args:
122
+ id: ID of the log alert configuration to disable
123
+ ctx: The MCP context (optional)
124
+
125
+ Returns:
126
+ Dictionary containing the result of the disable operation or error information
127
+ """
128
+ try:
129
+ debug_print(f"disable_log_alert_config called with id={id}")
130
+
131
+ self.log_alert_api.disable_log_alert_config(id=id)
132
+
133
+ debug_print(f"Successfully disabled log alert configuration with ID {id}")
134
+ return {"success": True, "message": f"Log alert configuration with ID {id} disabled successfully"}
135
+ except Exception as e:
136
+ debug_print(f"Error in disable_log_alert_config: {e}")
137
+ import traceback
138
+ traceback.print_exc(file=sys.stderr)
139
+ return {"error": f"Failed to disable log alert configuration: {str(e)}"}
140
+
141
+ @register_as_tool
142
+ async def enable_log_alert_config(self, id: str, ctx=None) -> Dict[str, Any]:
143
+ """
144
+ Enable a log alert configuration.
145
+
146
+ Args:
147
+ id: ID of the log alert configuration to enable
148
+ ctx: The MCP context (optional)
149
+
150
+ Returns:
151
+ Dictionary containing the result of the enable operation or error information
152
+ """
153
+ try:
154
+ debug_print(f"enable_log_alert_config called with id={id}")
155
+
156
+ self.log_alert_api.enable_log_alert_config(id=id)
157
+
158
+ debug_print(f"Successfully enabled log alert configuration with ID {id}")
159
+ return {"success": True, "message": f"Log alert configuration with ID {id} enabled successfully"}
160
+ except Exception as e:
161
+ debug_print(f"Error in enable_log_alert_config: {e}")
162
+ import traceback
163
+ traceback.print_exc(file=sys.stderr)
164
+ return {"error": f"Failed to enable log alert configuration: {str(e)}"}
165
+
166
+ @register_as_tool
167
+ async def find_active_log_alert_configs(self, alert_ids: Optional[List[str]] = None, ctx=None) -> Dict[str, Any]:
168
+ """
169
+ Get all active log alert configurations.
170
+
171
+ Args:
172
+ alert_ids: Optional list of alert IDs to filter by
173
+ ctx: The MCP context (optional)
174
+
175
+ Returns:
176
+ Dictionary containing active log alert configurations or error information
177
+ """
178
+ try:
179
+ debug_print(f"find_active_log_alert_configs called with alert_ids={alert_ids}")
180
+
181
+ result = self.log_alert_api.find_active_log_alert_configs(alert_ids=alert_ids)
182
+
183
+ debug_print(f"Result from find_active_log_alert_configs: {result}")
184
+ return {"configs": [self._convert_to_dict(config) for config in result]}
185
+ except Exception as e:
186
+ debug_print(f"Error in find_active_log_alert_configs: {e}")
187
+ import traceback
188
+ traceback.print_exc(file=sys.stderr)
189
+ return {"error": f"Failed to find active log alert configurations: {str(e)}"}
190
+
191
+ @register_as_tool
192
+ async def find_log_alert_config(self, id: str, valid_on: Optional[int] = None, ctx=None) -> Dict[str, Any]:
193
+ """
194
+ Get a specific log alert configuration by ID.
195
+
196
+ Args:
197
+ id: ID of the log alert configuration to retrieve
198
+ valid_on: Optional timestamp to get the configuration valid at that time
199
+ ctx: The MCP context (optional)
200
+
201
+ Returns:
202
+ Dictionary containing the log alert configuration or error information
203
+ """
204
+ try:
205
+ debug_print(f"find_log_alert_config called with id={id}, valid_on={valid_on}")
206
+
207
+ result = self.log_alert_api.find_log_alert_config(id=id, valid_on=valid_on)
208
+
209
+ debug_print(f"Result from find_log_alert_config: {result}")
210
+ return self._convert_to_dict(result)
211
+ except Exception as e:
212
+ debug_print(f"Error in find_log_alert_config: {e}")
213
+ import traceback
214
+ traceback.print_exc(file=sys.stderr)
215
+ return {"error": f"Failed to find log alert configuration: {str(e)}"}
216
+
217
+ @register_as_tool
218
+ async def find_log_alert_config_versions(self, id: str, ctx=None) -> Dict[str, Any]:
219
+ """
220
+ Get all versions of a log alert configuration.
221
+
222
+ Args:
223
+ id: ID of the log alert configuration to get versions for
224
+ ctx: The MCP context (optional)
225
+
226
+ Returns:
227
+ Dictionary containing versions of the log alert configuration or error information
228
+ """
229
+ try:
230
+ debug_print(f"find_log_alert_config_versions called with id={id}")
231
+
232
+ result = self.log_alert_api.find_log_alert_config_versions(id=id)
233
+
234
+ debug_print(f"Result from find_log_alert_config_versions: {result}")
235
+ return {"versions": [self._convert_to_dict(version) for version in result]}
236
+ except Exception as e:
237
+ debug_print(f"Error in find_log_alert_config_versions: {e}")
238
+ import traceback
239
+ traceback.print_exc(file=sys.stderr)
240
+ return {"error": f"Failed to find log alert configuration versions: {str(e)}"}
241
+
242
+ @register_as_tool
243
+ async def restore_log_alert_config(self, id: str, created: int, ctx=None) -> Dict[str, Any]:
244
+ """
245
+ Restore a log alert configuration to a previous version.
246
+
247
+ Args:
248
+ id: ID of the log alert configuration to restore
249
+ created: Timestamp of the version to restore
250
+ ctx: The MCP context (optional)
251
+
252
+ Returns:
253
+ Dictionary containing the result of the restore operation or error information
254
+ """
255
+ try:
256
+ debug_print(f"restore_log_alert_config called with id={id}, created={created}")
257
+
258
+ self.log_alert_api.restore_log_alert_config(id=id, created=created)
259
+
260
+ debug_print(f"Successfully restored log alert configuration with ID {id}")
261
+ return {
262
+ "success": True,
263
+ "message": f"Log alert configuration with ID {id} restored to version from {datetime.fromtimestamp(created/1000).isoformat()}"
264
+ }
265
+ except Exception as e:
266
+ debug_print(f"Error in restore_log_alert_config: {e}")
267
+ import traceback
268
+ traceback.print_exc(file=sys.stderr)
269
+ return {"error": f"Failed to restore log alert configuration: {str(e)}"}
270
+
271
+ @register_as_tool
272
+ async def update_log_alert_config(self, id: str, config: Dict[str, Any], ctx=None) -> Dict[str, Any]:
273
+ """
274
+ Update a log alert configuration.
275
+
276
+ Args:
277
+ id: ID of the log alert configuration to update
278
+ config: Dictionary containing the updated log alert configuration
279
+ ctx: The MCP context (optional)
280
+
281
+ Returns:
282
+ Dictionary containing the updated log alert configuration or error information
283
+ """
284
+ try:
285
+ debug_print(f"update_log_alert_config called with id={id}, config={config}")
286
+
287
+ # Convert dictionary to LogAlertConfig model
288
+ log_alert_config = LogAlertConfig(**config)
289
+
290
+ # Call the API
291
+ result = self.log_alert_api.update_log_alert_config(id=id, log_alert_config=log_alert_config)
292
+
293
+ debug_print(f"Result from update_log_alert_config: {result}")
294
+ return self._convert_to_dict(result)
295
+ except Exception as e:
296
+ debug_print(f"Error in update_log_alert_config: {e}")
297
+ import traceback
298
+ traceback.print_exc(file=sys.stderr)
299
+ return {"error": f"Failed to update log alert configuration: {str(e)}"}
300
+
301
+ def _convert_to_dict(self, obj: Any) -> Dict[str, Any]:
302
+ """
303
+ Convert an object to a dictionary.
304
+
305
+ Args:
306
+ obj: Object to convert
307
+
308
+ Returns:
309
+ Dictionary representation of the object
310
+ """
311
+ if hasattr(obj, "to_dict"):
312
+ return obj.to_dict()
313
+ elif hasattr(obj, "__dict__"):
314
+ return obj.__dict__
315
+ else:
316
+ return obj
@@ -0,0 +1,28 @@
1
+ show the top 5 services with the highest number of calls to cassandra cluster ABC_CLUST
2
+ What is the total latency in milliseconds for HTTP calls from the service named MyService to the host named myhost in the last hour?
3
+ What is the sum of latency in the last 1 hour for database calls from the service named MyService to the host named myhost sorted by latency in descending order?
4
+ Show me the average response time of HTTP requests to the APPXYZ application in the last 10 minutes, group result by call name, and order result by average response time in ascending order.
5
+ show the average latency of all HTTP calls from service XYZ
6
+ show the average latency of HTTP calls from service XYZ to service ABC, grouped by the call name
7
+ What is the rate of calls in the last 5 minutes for HTTP calls from the service named MyService to the host named myhost grouped by destination host?
8
+ show the average latency of HTTP calls to the service named 'my_service'
9
+ show the average latency for all HTTP calls from service XYZ to service ABC
10
+ show the number of calls and errors from service XYZ to service ABC, grouped by the destination server
11
+ What is the number of calls in the last 2 hours for HTTP calls from the service named MyService to the host named myhost grouped by the call type and sorted by the number of calls in descending order?
12
+ Show me the total number of calls, errors, and the response time for HTTP calls from the APPXYZ application in the last 2 hours, group result by call name, and order result by response time in ascending order.
13
+ show the mean latency and number of calls from service XYZ to service ABC, grouped by the HTTP method
14
+ What is the rate of errors per minute for database calls from the service named MyService to the host named myhost in the last hour?
15
+ show the number of errors from service XYZ to service ABC, grouped by the HTTP method and ordered by errors
16
+ What is the max latency in the last 1 hour for database calls to the host named myhost of the service named MyService?
17
+ What are the sum of errors for HTTP calls from the service named MyService to the service named MyOtherService in the last 5 minutes?
18
+ What is the average latency in the last 1 hour for database calls to the host named myhost of the service named MyService, ordered by the latency in descending order?
19
+ Show me the number of failed HTTP requests between applications appXYZ and appABC in the last hour, group result by error messages, and order result by number of errors in descending order.
20
+ How many HTTP calls were made by the service named MyService to the host named myhost in the last 2 hours?
21
+ What is the average response time of PCFindInventoryIVSyncService calls handled by apiserver service of oms-pcity-prod-1 application?
22
+ Show top erroneous calls handled by 'Robot Shop - EP' application since yesterday
23
+ Show performance overview of all http calls in the past 2 hours group by call name
24
+ What is the average response time of promo HTTP calls handled by Kubernetes cluster demo-us-cluster?
25
+ What is the average response time of readiness calls, handled by service app of application zone in last 1 day?
26
+ Show count of erroneous HTTP calls by call.tag.Errorcode handled by AdService application.
27
+ Show top erroneous calls handled by AdService service since last 10 minutes.
28
+ Show performance overview of outbound http calls from kubernetes cluster kub8-names to AC-test1 application order by calls
src/mcp_server.py ADDED
@@ -0,0 +1,343 @@
1
+ """
2
+ Standalone MCP Server for Instana Events and Infrastructure Resources
3
+
4
+ This module provides a dedicated MCP server that exposes Instana tools.
5
+ Supports stdio and Streamable HTTP transports.
6
+ """
7
+
8
+ import argparse
9
+ import os
10
+ import sys
11
+ import warnings
12
+ import traceback
13
+ import json
14
+ from collections.abc import AsyncIterator
15
+ from contextlib import asynccontextmanager
16
+ from dataclasses import dataclass, fields
17
+ from dotenv import load_dotenv
18
+ load_dotenv()
19
+
20
+ # Add the project root to the Python path
21
+ current_path = os.path.abspath(__file__)
22
+ project_root = os.path.dirname(os.path.dirname(current_path))
23
+ if project_root not in sys.path:
24
+ sys.path.insert(0, project_root)
25
+
26
+ # Import the necessary modules
27
+ try:
28
+ from src.client.events_mcp_tools import AgentMonitoringEventsMCPTools
29
+ from src.client.infrastructure_resources_mcp_tools import InfrastructureResourcesMCPTools
30
+ from src.client.infrastructure_catalog_mcp_tools import InfrastructureCatalogMCPTools
31
+ from src.client.application_resources_mcp_tools import ApplicationResourcesMCPTools
32
+ from src.client.application_metrics_mcp_tools import ApplicationMetricsMCPTools
33
+ from src.client.infrastructure_topology_mcp_tools import InfrastructureTopologyMCPTools
34
+ from src.client.infrastructure_analyze_mcp_tools import InfrastructureAnalyzeMCPTools
35
+ from src.client.application_alert_config_mcp_tools import ApplicationAlertMCPTools
36
+
37
+ from src.client.instana_client_base import register_as_tool, MCP_TOOLS
38
+ except ImportError as e:
39
+ traceback.print_exc(file=sys.stderr)
40
+ sys.exit(1)
41
+
42
+ from mcp.server.fastmcp import FastMCP
43
+
44
+ @dataclass
45
+ class MCPState:
46
+ """State for the MCP server."""
47
+ events_client: AgentMonitoringEventsMCPTools = None
48
+ infra_client: InfrastructureResourcesMCPTools = None
49
+ app_resource_client: ApplicationResourcesMCPTools = None
50
+ app_metrics_client: ApplicationMetricsMCPTools = None
51
+ app_alert_client: ApplicationAlertMCPTools = None
52
+ infra_catalog_client: InfrastructureCatalogMCPTools = None
53
+ infra_topo_client: InfrastructureTopologyMCPTools = None
54
+ infra_analyze_client: InfrastructureAnalyzeMCPTools = None
55
+
56
+ # Global variables to store credentials for lifespan
57
+ _global_token = ""
58
+ _global_base_url = ""
59
+
60
+ def get_client_configs():
61
+ """Get client configurations dynamically from MCPState dataclass"""
62
+ # Map field names to their corresponding client classes
63
+ client_class_mapping = {
64
+ 'events_client': AgentMonitoringEventsMCPTools,
65
+ 'infra_client': InfrastructureResourcesMCPTools,
66
+ 'infra_catalog_client': InfrastructureCatalogMCPTools,
67
+ 'infra_topo_client': InfrastructureTopologyMCPTools,
68
+ 'infra_analyze_client': InfrastructureAnalyzeMCPTools,
69
+ 'app_resource_client': ApplicationResourcesMCPTools,
70
+ 'app_metrics_client': ApplicationMetricsMCPTools,
71
+ 'app_alert_client': ApplicationAlertMCPTools,
72
+ }
73
+
74
+ # Get all field names from MCPState dataclass
75
+ state_fields = [field.name for field in fields(MCPState)]
76
+
77
+ # Return configurations for fields that have corresponding client classes
78
+ configs = []
79
+ for field_name in state_fields:
80
+ if field_name in client_class_mapping:
81
+ configs.append((field_name, client_class_mapping[field_name]))
82
+ else:
83
+ print(f"Warning: No client class mapping found for field '{field_name}'", file=sys.stderr)
84
+
85
+ return configs
86
+
87
+ def create_clients(token: str, base_url: str, enabled_categories: str = "all") -> MCPState:
88
+ """Create only the enabled Instana clients"""
89
+ state = MCPState()
90
+
91
+ # Get enabled client configurations
92
+ enabled_client_configs = get_enabled_client_configs(enabled_categories)
93
+
94
+ for attr_name, client_class in enabled_client_configs:
95
+ try:
96
+ client = client_class(read_token=token, base_url=base_url)
97
+ setattr(state, attr_name, client)
98
+ except Exception as e:
99
+ print(f"Failed to create {attr_name}: {e}", file=sys.stderr)
100
+ traceback.print_exc(file=sys.stderr)
101
+ setattr(state, attr_name, None)
102
+
103
+ return state
104
+
105
+
106
+ @asynccontextmanager
107
+ async def lifespan(server: FastMCP) -> AsyncIterator[MCPState]:
108
+ """Set up and tear down the Instana clients."""
109
+ # Use global credentials set by create_app
110
+ token = _global_token or os.getenv("INSTANA_API_TOKEN", "")
111
+ base_url = _global_base_url or os.getenv("INSTANA_BASE_URL", "")
112
+ enabled_categories = os.getenv("INSTANA_ENABLED_TOOLS", "all")
113
+
114
+ try:
115
+ state = create_clients(token, base_url, enabled_categories)
116
+
117
+ yield state
118
+ except Exception as e:
119
+ traceback.print_exc(file=sys.stderr)
120
+
121
+ # Yield empty state if client creation failed
122
+ yield MCPState()
123
+
124
+ def create_app(instana_api_token_value: str, instana_base_url: str) -> FastMCP:
125
+ global _global_token, _global_base_url
126
+
127
+ try:
128
+ _global_token = instana_api_token_value
129
+ _global_base_url = instana_base_url
130
+
131
+ server = FastMCP("Instana Tools", lifespan=lifespan)
132
+
133
+ # Use the enabled categories from the environment
134
+ enabled_categories = os.getenv("INSTANA_ENABLED_TOOLS", "all")
135
+
136
+ # Only create and register enabled clients/tools
137
+ clients_state = create_clients(instana_api_token_value, instana_base_url, enabled_categories)
138
+
139
+ tools_registered = 0
140
+ for tool_name, tool_func in MCP_TOOLS.items():
141
+ try:
142
+ client_found = False
143
+ client_attr_names = [field.name for field in fields(MCPState)]
144
+ for attr_name in client_attr_names:
145
+ client = getattr(clients_state, attr_name, None)
146
+ if client and hasattr(client, tool_name):
147
+ bound_method = getattr(client, tool_name)
148
+ server.tool()(bound_method)
149
+ tools_registered += 1
150
+ client_found = True
151
+ break
152
+ except Exception as e:
153
+ print(f"Failed to register tool {tool_name}: {e}", file=sys.stderr)
154
+ traceback.print_exc(file=sys.stderr)
155
+
156
+ return server, tools_registered
157
+
158
+ except Exception as e:
159
+ traceback.print_exc(file=sys.stderr)
160
+ fallback_server = FastMCP("Instana Tools")
161
+ return fallback_server
162
+
163
+ async def execute_tool(tool_name: str, arguments: dict, clients_state) -> str:
164
+ """Execute a tool and return result"""
165
+ try:
166
+ # Get all field names from MCPState dataclass
167
+ client_attr_names = [field.name for field in fields(MCPState)]
168
+
169
+ for attr_name in client_attr_names:
170
+ client = getattr(clients_state, attr_name, None)
171
+ if client and hasattr(client, tool_name):
172
+ method = getattr(client, tool_name)
173
+ result = await method(**arguments)
174
+ return str(result)
175
+
176
+ return f"Tool {tool_name} not found"
177
+ except Exception as e:
178
+ return f"Error executing tool {tool_name}: {str(e)}"
179
+
180
+
181
+ client_categories = {
182
+ "infra": [
183
+ ('infra_client', InfrastructureResourcesMCPTools),
184
+ ('infra_catalog_client', InfrastructureCatalogMCPTools),
185
+ ('infra_topo_client', InfrastructureTopologyMCPTools),
186
+ ('infra_analyze_client', InfrastructureAnalyzeMCPTools),
187
+ ],
188
+ "app": [
189
+ ('app_resource_client', ApplicationResourcesMCPTools),
190
+ ('app_metrics_client', ApplicationMetricsMCPTools),
191
+ ('app_alert_client', ApplicationAlertMCPTools),
192
+ ],
193
+ "events": [
194
+ ('events_client', AgentMonitoringEventsMCPTools),
195
+ ]
196
+ }
197
+
198
+ def get_enabled_client_configs(enabled_categories: str):
199
+ """Get client configurations based on enabled categories"""
200
+ # Use the global client_categories mapping
201
+ if enabled_categories.lower() == "all":
202
+ all_configs = []
203
+ for category_clients in client_categories.values():
204
+ all_configs.extend(category_clients)
205
+ return all_configs
206
+ categories = [cat.strip() for cat in enabled_categories.split(",")]
207
+ enabled_configs = []
208
+ for category in categories:
209
+ if category in client_categories:
210
+ enabled_configs.extend(client_categories[category])
211
+ else:
212
+ print(f"Warning: Unknown category '{category}'", file=sys.stderr)
213
+ return enabled_configs
214
+
215
+
216
+ def main():
217
+ """Main entry point for the MCP server."""
218
+ try:
219
+ # Get token from environment
220
+ instana_api_token_value = os.getenv("INSTANA_API_TOKEN")
221
+ if not instana_api_token_value:
222
+ warnings.warn(
223
+ "Instana API token not provided. Some functionality will be limited. "
224
+ "Provide token via --api-token argument or INSTANA_API_TOKEN environment variable."
225
+ )
226
+
227
+ # Get base URL from environment
228
+ instana_base_url = os.getenv("INSTANA_BASE_URL")
229
+
230
+ # Create and configure the MCP server
231
+ parser = argparse.ArgumentParser(description="Instana MCP Server", add_help=False)
232
+ parser.add_argument(
233
+ "-h", "--help",
234
+ action="store_true",
235
+ dest="help",
236
+ help="show this help message and exit"
237
+ )
238
+ parser.add_argument(
239
+ "--transport",
240
+ type=str,
241
+ choices=["streamable-http","stdio"],
242
+ metavar='<mode>',
243
+ help="Transport mode. Choose from: streamable-http, stdio."
244
+ )
245
+ parser.add_argument(
246
+ "--debug",
247
+ action="store_true",
248
+ help="Enable debug mode with additional logging"
249
+ )
250
+ parser.add_argument(
251
+ "--disable",
252
+ type=str,
253
+ metavar='<categories>',
254
+ help="Disable categories: Choose from (infra (infrastructure), app (application), events (event-based))."
255
+ )
256
+ # Check for help arguments before parsing
257
+ if len(sys.argv) > 1 and any(arg in ['-h','--h','--help','-help'] for arg in sys.argv[1:]):
258
+ # Check if help is combined with other arguments
259
+ help_args = ['-h','--h','--help','-help']
260
+ other_args = [arg for arg in sys.argv[1:] if arg not in help_args]
261
+
262
+ if other_args:
263
+ print("error: argument -h/--h/--help/-help: not allowed with other arguments", file=sys.stderr)
264
+ sys.exit(2)
265
+
266
+ # Show help and exit
267
+ print("options:")
268
+ for action in parser._actions:
269
+ # Only print options that start with '--' and have a help string
270
+ if any(opt.startswith('--') for opt in action.option_strings) and action.help:
271
+ # Find the first long option
272
+ long_opt = next((opt for opt in action.option_strings if opt.startswith('--')), None)
273
+ metavar = action.metavar or ''
274
+ opt_str = f"{long_opt} {metavar}".strip()
275
+ print(f"{opt_str:<24} {action.help}")
276
+ sys.exit(0)
277
+
278
+ args = parser.parse_args()
279
+
280
+ all_categories = {"infra", "app", "events"}
281
+
282
+ # By default, enable all categories
283
+ enabled = set(all_categories)
284
+ invalid = set()
285
+ # Remove disabled categories if specified
286
+ if args.disable:
287
+ disabled = set(cat.strip() for cat in args.disable.split(","))
288
+ invalid = disabled - all_categories
289
+ enabled = enabled - disabled
290
+
291
+ if invalid:
292
+ print(f"Error: Unknown category/categories: {', '.join(invalid)}")
293
+ print(f"Available categories: infra, app, events")
294
+ sys.exit(2)
295
+
296
+ if args.disable:
297
+ disabled_tool_classes = []
298
+ for category in disabled:
299
+ if category in client_categories:
300
+ disabled_tool_classes.extend(
301
+ [cls.__name__ for _, cls in client_categories[category]]
302
+ )
303
+ if disabled_tool_classes:
304
+ print(
305
+ f"The following tools are disabled: {', '.join(disabled_tool_classes)}"
306
+ )
307
+
308
+ os.environ["INSTANA_ENABLED_TOOLS"] = ",".join(enabled)
309
+
310
+ # Create and configure the MCP server
311
+ app, registered_tool_count = create_app(instana_api_token_value or "", instana_base_url or "")
312
+
313
+ # Run the server with the appropriate transport
314
+ if args.transport == "streamable-http":
315
+ if args.debug:
316
+ print(f"FastMCP instance: {app}", file=sys.stderr)
317
+ print(f"Registered tools: {registered_tool_count}", file=sys.stderr)
318
+ try:
319
+ app.run(transport="streamable-http")
320
+ except Exception as e:
321
+ print(f"Failed to start HTTP server: {e}", file=sys.stderr)
322
+ if args.debug:
323
+ traceback.print_exc(file=sys.stderr)
324
+ sys.exit(1)
325
+ else:
326
+ print("Starting stdio transport", file=sys.stderr)
327
+ app.run(transport="stdio")
328
+
329
+ except KeyboardInterrupt:
330
+ print("Server stopped by user", file=sys.stderr)
331
+ sys.exit(0)
332
+ except Exception as e:
333
+ print(f"Server error: {e}", file=sys.stderr)
334
+ traceback.print_exc(file=sys.stderr)
335
+ sys.exit(1)
336
+
337
+ if __name__ == "__main__":
338
+ try:
339
+ main()
340
+ except Exception as e:
341
+ traceback.print_exc(file=sys.stderr)
342
+ sys.exit(1)
343
+