mcp-instana 0.1.1__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 (55) hide show
  1. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/METADATA +459 -138
  2. mcp_instana-0.2.0.dist-info/RECORD +59 -0
  3. src/application/application_analyze.py +373 -160
  4. src/application/application_catalog.py +3 -1
  5. src/application/application_global_alert_config.py +653 -0
  6. src/application/application_metrics.py +6 -2
  7. src/application/application_resources.py +3 -1
  8. src/application/application_settings.py +966 -370
  9. src/application/application_topology.py +6 -2
  10. src/automation/action_catalog.py +416 -0
  11. src/automation/action_history.py +338 -0
  12. src/core/server.py +159 -9
  13. src/core/utils.py +2 -2
  14. src/event/events_tools.py +602 -275
  15. src/infrastructure/infrastructure_analyze.py +7 -3
  16. src/infrastructure/infrastructure_catalog.py +3 -1
  17. src/infrastructure/infrastructure_metrics.py +6 -2
  18. src/infrastructure/infrastructure_resources.py +7 -5
  19. src/infrastructure/infrastructure_topology.py +5 -3
  20. src/prompts/__init__.py +16 -0
  21. src/prompts/application/__init__.py +1 -0
  22. src/prompts/application/application_alerts.py +54 -0
  23. src/prompts/application/application_catalog.py +26 -0
  24. src/prompts/application/application_metrics.py +57 -0
  25. src/prompts/application/application_resources.py +26 -0
  26. src/prompts/application/application_settings.py +75 -0
  27. src/prompts/application/application_topology.py +30 -0
  28. src/prompts/events/__init__.py +1 -0
  29. src/prompts/events/events_tools.py +161 -0
  30. src/prompts/infrastructure/infrastructure_analyze.py +72 -0
  31. src/prompts/infrastructure/infrastructure_catalog.py +53 -0
  32. src/prompts/infrastructure/infrastructure_metrics.py +45 -0
  33. src/prompts/infrastructure/infrastructure_resources.py +74 -0
  34. src/prompts/infrastructure/infrastructure_topology.py +38 -0
  35. src/prompts/settings/__init__.py +0 -0
  36. src/prompts/settings/custom_dashboard.py +157 -0
  37. src/prompts/website/__init__.py +1 -0
  38. src/prompts/website/website_analyze.py +35 -0
  39. src/prompts/website/website_catalog.py +40 -0
  40. src/prompts/website/website_configuration.py +105 -0
  41. src/prompts/website/website_metrics.py +34 -0
  42. src/settings/__init__.py +1 -0
  43. src/settings/custom_dashboard_tools.py +417 -0
  44. src/website/__init__.py +0 -0
  45. src/website/website_analyze.py +433 -0
  46. src/website/website_catalog.py +171 -0
  47. src/website/website_configuration.py +770 -0
  48. src/website/website_metrics.py +241 -0
  49. mcp_instana-0.1.1.dist-info/RECORD +0 -30
  50. src/prompts/mcp_prompts.py +0 -900
  51. src/prompts/prompt_loader.py +0 -29
  52. src/prompts/prompt_registry.json +0 -21
  53. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/WHEEL +0 -0
  54. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/entry_points.txt +0 -0
  55. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,338 @@
1
+ """
2
+ Automation Action History MCP Tools Module
3
+
4
+ This module provides automation action history tools for Instana Automation.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ # Import the necessary classes from the SDK
11
+ try:
12
+ from instana_client.api.action_history_api import (
13
+ ActionHistoryApi,
14
+ )
15
+ except ImportError:
16
+ import logging
17
+ logger = logging.getLogger(__name__)
18
+ logger.error("Failed to import action history API", exc_info=True)
19
+ raise
20
+
21
+
22
+ from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
23
+
24
+ # Configure logger for this module
25
+ logger = logging.getLogger(__name__)
26
+
27
+ class ActionHistoryMCPTools(BaseInstanaClient):
28
+ """Tools for automation action history in Instana MCP."""
29
+
30
+ def __init__(self, read_token: str, base_url: str):
31
+ """Initialize the Action History MCP tools client."""
32
+ super().__init__(read_token=read_token, base_url=base_url)
33
+
34
+ @register_as_tool
35
+ @with_header_auth(ActionHistoryApi)
36
+ async def submit_automation_action(self,
37
+ payload: Union[Dict[str, Any], str],
38
+ ctx=None,
39
+ api_client=None) -> Dict[str, Any]:
40
+ """
41
+ Submit an automation action for execution on an agent.
42
+ The automation action to execute and the agent on which to execute the action must be specified as actionId and hostId. For more details on the request payload see the request sample.
43
+
44
+ Args:
45
+ Sample payload:
46
+ {
47
+ "hostId": "aHostId",
48
+ "actionId": "d473c1b0-0740-4d08-95fe-31e5d0a9faff",
49
+ "policyId": "2nIOVtEW-iPbsEIi89-yDqJabc",
50
+ "inputParameters": [
51
+ {
52
+ "name": "name",
53
+ "type": "type",
54
+ "value": "value"
55
+ }
56
+ ],
57
+ "eventId": "M3wuBxuaSDyecZJ7ICioiw",
58
+ "async": "true",
59
+ "timeout": "600"
60
+ }
61
+
62
+ Required fields:
63
+ - actionId: Action identifier of the action to run
64
+ - hostId: Agent host identifier on which to run the action
65
+
66
+ Optional fields:
67
+ - async: "true" if the action should be run in asynchronous mode, "false" otherwise. Default is "true"
68
+ - eventId: Event identifier (incident or issue) associated with the policy
69
+ - policyId: Policy identifier that associates the action trigger to the action to run
70
+ - timeout: Action run time out. Default is 30 seconds
71
+ - inputParameters: Array of action run input parameters
72
+
73
+ ctx: Optional[Dict[str, Any]]: The context for the action execution
74
+ api_client: Optional[ActionHistoryApi]: The API client for action execution
75
+
76
+ Returns:
77
+ Dict[str, Any]: The result of the automation action submission
78
+ """
79
+ try:
80
+ if not payload:
81
+ return {"error": "payload is required"}
82
+
83
+ # Parse the payload if it's a string
84
+ if isinstance(payload, str):
85
+ logger.debug("Payload is a string, attempting to parse")
86
+ try:
87
+ import json
88
+ try:
89
+ parsed_payload = json.loads(payload)
90
+ logger.debug("Successfully parsed payload as JSON")
91
+ request_body = parsed_payload
92
+ except json.JSONDecodeError as e:
93
+ logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
94
+
95
+ # Try replacing single quotes with double quotes
96
+ fixed_payload = payload.replace("'", "\"")
97
+ try:
98
+ parsed_payload = json.loads(fixed_payload)
99
+ logger.debug("Successfully parsed fixed JSON")
100
+ request_body = parsed_payload
101
+ except json.JSONDecodeError:
102
+ # Try as Python literal
103
+ import ast
104
+ try:
105
+ parsed_payload = ast.literal_eval(payload)
106
+ logger.debug("Successfully parsed payload as Python literal")
107
+ request_body = parsed_payload
108
+ except (SyntaxError, ValueError) as e2:
109
+ logger.debug(f"Failed to parse payload string: {e2}")
110
+ return {"error": f"Invalid payload format: {e2}", "payload": payload}
111
+ except Exception as e:
112
+ logger.debug(f"Error parsing payload string: {e}")
113
+ return {"error": f"Failed to parse payload: {e}", "payload": payload}
114
+ else:
115
+ # If payload is already a dictionary, use it directly
116
+ logger.debug("Using provided payload dictionary")
117
+ request_body = payload
118
+
119
+ # Validate required fields in the payload
120
+ required_fields = ["actionId", "hostId"]
121
+ for field in required_fields:
122
+ if field not in request_body:
123
+ logger.warning(f"Missing required field: {field}")
124
+ return {"error": f"Missing required field: {field}"}
125
+
126
+ # Import the ActionInstanceRequest class
127
+ try:
128
+ from instana_client.models.action_instance_request import (
129
+ ActionInstanceRequest,
130
+ )
131
+ logger.debug("Successfully imported ActionInstanceRequest")
132
+ except ImportError as e:
133
+ logger.debug(f"Error importing ActionInstanceRequest: {e}")
134
+ return {"error": f"Failed to import ActionInstanceRequest: {e!s}"}
135
+
136
+ # Create an ActionInstanceRequest object from the request body
137
+ try:
138
+ logger.debug(f"Creating ActionInstanceRequest with params: {request_body}")
139
+ config_object = ActionInstanceRequest(**request_body)
140
+ logger.debug("Successfully created config object")
141
+ except Exception as e:
142
+ logger.debug(f"Error creating ActionInstanceRequest: {e}")
143
+ return {"error": f"Failed to create config object: {e!s}"}
144
+
145
+ # Call the add_action_instance method from the SDK
146
+ logger.debug("Calling add_action_instance with config object")
147
+ result = api_client.add_action_instance(
148
+ action_instance_request=config_object,
149
+ )
150
+
151
+ # Convert the result to a dictionary
152
+ if hasattr(result, 'to_dict'):
153
+ result_dict = result.to_dict()
154
+ else:
155
+ # If it's already a dict or another format, use it as is
156
+ result_dict = result or {
157
+ "success": True,
158
+ "message": "Automation action submitted successfully"
159
+ }
160
+
161
+ logger.debug(f"Result from add_action_instance: {result_dict}")
162
+ return result_dict
163
+ except Exception as e:
164
+ logger.error(f"Error in submit_automation_action: {e}")
165
+ return {"error": f"Failed to submit automation action: {e!s}"}
166
+
167
+ @register_as_tool
168
+ @with_header_auth(ActionHistoryApi)
169
+ async def get_action_instance_details(self,
170
+ action_instance_id: str,
171
+ window_size: Optional[int] = None,
172
+ to: Optional[int] = None,
173
+ ctx=None,
174
+ api_client=None) -> Dict[str, Any]:
175
+ """
176
+ Get the details of an automation action run result by ID from action run history.
177
+
178
+ Args:
179
+ action_instance_id: Action run result ID to get action run result details (required)
180
+ window_size: Window size in milliseconds. This value is used to compute the from date (from = to - windowSize) to get the action run result details. The default windowSize is set to 10 minutes if this value is not provided.
181
+ to: To date filter in milliseconds (13-digit) to get the action run result details. The default to date is set to System.currentTimeMillis() if this value is not provided.
182
+ ctx: Optional[Dict[str, Any]]: The context for the action instance retrieval
183
+ api_client: Optional[ActionHistoryApi]: The API client for action history
184
+
185
+ Returns:
186
+ Dict[str, Any]: The details of the automation action run result
187
+ """
188
+ try:
189
+ if not action_instance_id:
190
+ return {"error": "action_instance_id is required"}
191
+
192
+ logger.debug(f"Getting action instance details for ID: {action_instance_id}")
193
+ result = api_client.get_action_instance(
194
+ action_instance_id=action_instance_id,
195
+ window_size=window_size,
196
+ to=to,
197
+ )
198
+
199
+ # Convert the result to a dictionary
200
+ if hasattr(result, 'to_dict'):
201
+ result_dict = result.to_dict()
202
+ else:
203
+ # If it's already a dict or another format, use it as is
204
+ result_dict = result or {
205
+ "success": True,
206
+ "message": "Action instance details retrieved successfully"
207
+ }
208
+
209
+ logger.debug(f"Result from get_action_instance: {result_dict}")
210
+ return result_dict
211
+ except Exception as e:
212
+ logger.error(f"Error in get_action_instance_details: {e}")
213
+ return {"error": f"Failed to get action instance details: {e!s}"}
214
+
215
+ @register_as_tool
216
+ @with_header_auth(ActionHistoryApi)
217
+ async def list_action_instances(self,
218
+ window_size: Optional[int] = None,
219
+ to: Optional[int] = None,
220
+ page: Optional[int] = None,
221
+ page_size: Optional[int] = None,
222
+ target_snapshot_id: Optional[str] = None,
223
+ event_id: Optional[str] = None,
224
+ event_specification_id: Optional[str] = None,
225
+ search: Optional[str] = None,
226
+ types: Optional[List[str]] = None,
227
+ action_statuses: Optional[List[str]] = None,
228
+ order_by: Optional[str] = None,
229
+ order_direction: Optional[str] = None,
230
+ ctx=None,
231
+ api_client=None) -> Dict[str, Any]:
232
+ """
233
+ Get the details of automation action run results from action run history.
234
+
235
+ Args:
236
+ window_size: Window size filter in milliseconds (to compute the from date) to get the action run result details
237
+ to: To date filter in milliseconds (13-digit) to get the action run result details
238
+ page: Page to fetch -- used for paging the action run result records
239
+ page_size: Number of records to return in each page -- used for paging the action run result records
240
+ target_snapshot_id: Target snapshot ID filter to get the action run result details
241
+ event_id: Event ID filter to get the action run result details
242
+ event_specification_id: Event specification ID filter to get the action run result details
243
+ search: Text in action run result name, description and event name filter to get the action run result details
244
+ types: Action type filter to get the action run result details
245
+ action_statuses: Action status filter to get the action run result details
246
+ order_by: Action run result column to order the result set
247
+ order_direction: Sort order direction
248
+
249
+ ctx: Optional[Dict[str, Any]]: The context for the action instances retrieval
250
+ api_client: Optional[ActionHistoryApi]: The API client for action history
251
+
252
+ Returns:
253
+ Dict[str, Any]: The paginated list of automation action run results
254
+ """
255
+ try:
256
+ logger.debug("Getting action instances with parameters")
257
+ result = api_client.get_action_instances(
258
+ window_size=window_size,
259
+ to=to,
260
+ page=page,
261
+ page_size=page_size,
262
+ target_snapshot_id=target_snapshot_id,
263
+ event_id=event_id,
264
+ event_specification_id=event_specification_id,
265
+ search=search,
266
+ types=types,
267
+ action_statuses=action_statuses,
268
+ order_by=order_by,
269
+ order_direction=order_direction
270
+ )
271
+
272
+ # Convert the result to a dictionary
273
+ if hasattr(result, 'to_dict'):
274
+ result_dict = result.to_dict()
275
+ else:
276
+ # If it's already a dict or another format, use it as is
277
+ result_dict = result or {
278
+ "success": True,
279
+ "message": "Action instances retrieved successfully"
280
+ }
281
+
282
+ logger.debug(f"Result from get_action_instances: {result_dict}")
283
+ return result_dict
284
+ except Exception as e:
285
+ logger.error(f"Error in list_action_instances: {e}")
286
+ return {"error": f"Failed to list action instances: {e!s}"}
287
+
288
+ @register_as_tool
289
+ @with_header_auth(ActionHistoryApi)
290
+ async def delete_action_instance(self,
291
+ action_instance_id: str,
292
+ from_time: int,
293
+ to_time: int,
294
+ ctx=None,
295
+ api_client=None) -> Dict[str, Any]:
296
+ """
297
+ Delete an automation action run result from the action run history by ID.
298
+
299
+ Args:
300
+ action_instance_id: Automation action run result ID to delete (required)
301
+ from_time: From date filter in milliseconds (13-digit) to look up the action run result ID (required)
302
+ to_time: To date filter in milliseconds (13-digit) to look up the action run result ID (required)
303
+ ctx: Optional[Dict[str, Any]]: The context for the action instance deletion
304
+ api_client: Optional[ActionHistoryApi]: The API client for action history
305
+
306
+ Returns:
307
+ Dict[str, Any]: The result of the action instance deletion
308
+ """
309
+ try:
310
+ if not action_instance_id:
311
+ return {"error": "action_instance_id is required"}
312
+ if not from_time:
313
+ return {"error": "from_time is required"}
314
+ if not to_time:
315
+ return {"error": "to_time is required"}
316
+
317
+ logger.debug(f"Deleting action instance with ID: {action_instance_id}")
318
+ result = api_client.delete_action_instance(
319
+ action_instance_id=action_instance_id,
320
+ var_from=from_time,
321
+ to=to_time,
322
+ )
323
+
324
+ # Convert the result to a dictionary
325
+ if hasattr(result, 'to_dict'):
326
+ result_dict = result.to_dict()
327
+ else:
328
+ # If it's already a dict or another format, use it as is
329
+ result_dict = result or {
330
+ "success": True,
331
+ "message": "Action instance deleted successfully"
332
+ }
333
+
334
+ logger.debug(f"Result from delete_action_instance: {result_dict}")
335
+ return result_dict
336
+ except Exception as e:
337
+ logger.error(f"Error in delete_action_instance: {e}")
338
+ return {"error": f"Failed to delete action instance: {e!s}"}
src/core/server.py CHANGED
@@ -16,7 +16,7 @@ from typing import Any
16
16
 
17
17
  from dotenv import load_dotenv
18
18
 
19
- from src.prompts.prompt_loader import register_prompts
19
+ from src.prompts import PROMPT_REGISTRY
20
20
 
21
21
  load_dotenv()
22
22
 
@@ -77,6 +77,11 @@ class MCPState:
77
77
  app_topology_client: Any = None
78
78
  app_analyze_client: Any = None
79
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
80
85
 
81
86
  # Global variables to store credentials for lifespan
82
87
  _global_token = None
@@ -130,7 +135,7 @@ async def lifespan(server: FastMCP) -> AsyncIterator[MCPState]:
130
135
  # Yield empty state if client creation failed
131
136
  yield MCPState()
132
137
 
133
- def create_app(token: str, base_url: str, port: int = 8000, enabled_categories: str = "all") -> tuple[FastMCP, int]:
138
+ def create_app(token: str, base_url: str, port: int = int(os.getenv("PORT", "8080")), enabled_categories: str = "all") -> tuple[FastMCP, int]:
134
139
  """Create and configure the MCP server with the given credentials."""
135
140
  try:
136
141
  server = FastMCP(name="Instana MCP Server", host="0.0.0.0", port=port)
@@ -153,7 +158,48 @@ def create_app(token: str, base_url: str, port: int = 8000, enabled_categories:
153
158
  logger.error(f"Failed to register tool {tool_name}: {e}", exc_info=True)
154
159
 
155
160
  # Register prompts from the prompt registry
156
- register_prompts(server)
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
+
157
203
 
158
204
  return server, tools_registered
159
205
 
@@ -185,10 +231,15 @@ def get_client_categories():
185
231
  from src.application.application_alert_config import ApplicationAlertMCPTools
186
232
  from src.application.application_analyze import ApplicationAnalyzeMCPTools
187
233
  from src.application.application_catalog import ApplicationCatalogMCPTools
234
+ from src.application.application_global_alert_config import (
235
+ ApplicationGlobalAlertMCPTools,
236
+ )
188
237
  from src.application.application_metrics import ApplicationMetricsMCPTools
189
238
  from src.application.application_resources import ApplicationResourcesMCPTools
190
239
  from src.application.application_settings import ApplicationSettingsMCPTools
191
240
  from src.application.application_topology import ApplicationTopologyMCPTools
241
+ from src.automation.action_catalog import ActionCatalogMCPTools
242
+ from src.automation.action_history import ActionHistoryMCPTools
192
243
  from src.event.events_tools import AgentMonitoringEventsMCPTools
193
244
  from src.infrastructure.infrastructure_analyze import (
194
245
  InfrastructureAnalyzeMCPTools,
@@ -205,6 +256,11 @@ def get_client_categories():
205
256
  from src.infrastructure.infrastructure_topology import (
206
257
  InfrastructureTopologyMCPTools,
207
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
208
264
  except ImportError as e:
209
265
  logger.warning(f"Could not import client classes: {e}")
210
266
  return {}
@@ -225,18 +281,110 @@ def get_client_categories():
225
281
  ('app_topology_client', ApplicationTopologyMCPTools),
226
282
  ('app_analyze_client', ApplicationAnalyzeMCPTools),
227
283
  ('app_settings_client', ApplicationSettingsMCPTools),
284
+ ('app_global_alert_client', ApplicationGlobalAlertMCPTools),
228
285
  ],
229
286
  "events": [
230
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),
231
301
  ]
232
302
  }
233
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
+
234
382
  def get_enabled_client_configs(enabled_categories: str):
235
383
  """Get client configurations based on enabled categories"""
236
384
  # Get client categories with lazy imports
237
385
  client_categories = get_client_categories()
238
386
 
239
- if enabled_categories.lower() == "all":
387
+ if not enabled_categories or enabled_categories.lower() == "all":
240
388
  all_configs = []
241
389
  for category_clients in client_categories.values():
242
390
  all_configs.extend(category_clients)
@@ -284,7 +432,7 @@ def main():
284
432
  "--tools",
285
433
  type=str,
286
434
  metavar='<categories>',
287
- help="Comma-separated list of tool categories to enable (--tools infra,app,events). If not provided, all tools are enabled."
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."
288
436
  )
289
437
  parser.add_argument(
290
438
  "--list-tools",
@@ -294,8 +442,8 @@ def main():
294
442
  parser.add_argument(
295
443
  "--port",
296
444
  type=int,
297
- default=8000,
298
- help="Port to listen on (default: 8000)"
445
+ default=int(os.getenv("PORT", "8080")),
446
+ help="Port to listen on (default: 8080, can be overridden with PORT env var)"
299
447
  )
300
448
  # Check for help arguments before parsing
301
449
  if len(sys.argv) > 1 and any(arg in ['-h','--h','--help','-help'] for arg in sys.argv[1:]):
@@ -331,7 +479,7 @@ def main():
331
479
  else:
332
480
  set_log_level(args.log_level)
333
481
 
334
- all_categories = {"infra", "app", "events"}
482
+ all_categories = {"infra", "app", "events", "automation", "website", "settings"}
335
483
 
336
484
  # Handle --list-tools option
337
485
  if args.list_tools:
@@ -359,7 +507,7 @@ def main():
359
507
  enabled = set(all_categories)
360
508
 
361
509
  if invalid:
362
- logger.error(f"Error: Unknown category/categories: {', '.join(invalid)}. Available categories: infra, app, events")
510
+ logger.error(f"Error: Unknown category/categories: {', '.join(invalid)}. Available categories: infra, app, events, automation, website, settings")
363
511
  sys.exit(2)
364
512
 
365
513
  # Print enabled tools for user information
@@ -393,6 +541,8 @@ def main():
393
541
  # Create and configure the MCP server
394
542
  try:
395
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
396
546
  app, registered_tool_count = create_app(INSTANA_API_TOKEN, INSTANA_BASE_URL, args.port, enabled_categories)
397
547
  except Exception as e:
398
548
  print(f"Failed to create MCP server: {e}", file=sys.stderr)
src/core/utils.py CHANGED
@@ -18,7 +18,7 @@ def register_as_tool(func):
18
18
  MCP_TOOLS[func.__name__] = func
19
19
  return func
20
20
 
21
- def with_header_auth(api_class, allow_mock=False):
21
+ def with_header_auth(api_class, allow_mock=True):
22
22
  """
23
23
  Universal decorator for Instana MCP tools that provides flexible authentication.
24
24
 
@@ -34,7 +34,7 @@ def with_header_auth(api_class, allow_mock=False):
34
34
  Args:
35
35
  api_class: The Instana API class to instantiate (e.g., InfrastructureTopologyApi,
36
36
  ApplicationMetricsApi, InfrastructureCatalogApi, etc.)
37
- allow_mock: If True, allows mock clients to be passed directly (for testing)
37
+ allow_mock: If True, allows mock clients to be passed directly (for testing). Defaults to True.
38
38
 
39
39
  Usage:
40
40
  @with_header_auth(YourApiClass)