mcp-instana 0.1.0__py3-none-any.whl → 0.1.1__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 (37) hide show
  1. mcp_instana-0.1.1.dist-info/METADATA +908 -0
  2. mcp_instana-0.1.1.dist-info/RECORD +30 -0
  3. {mcp_instana-0.1.0.dist-info → mcp_instana-0.1.1.dist-info}/WHEEL +1 -1
  4. mcp_instana-0.1.1.dist-info/entry_points.txt +4 -0
  5. mcp_instana-0.1.0.dist-info/LICENSE → mcp_instana-0.1.1.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 +415 -0
  9. src/application/application_catalog.py +153 -0
  10. src/{client/application_metrics_mcp_tools.py → application/application_metrics.py} +107 -129
  11. src/{client/application_resources_mcp_tools.py → application/application_resources.py} +128 -150
  12. src/application/application_settings.py +1135 -0
  13. src/application/application_topology.py +107 -0
  14. src/core/__init__.py +1 -0
  15. src/core/server.py +436 -0
  16. src/core/utils.py +213 -0
  17. src/event/__init__.py +1 -0
  18. src/{client/events_mcp_tools.py → event/events_tools.py} +128 -136
  19. src/infrastructure/__init__.py +1 -0
  20. src/{client/infrastructure_analyze_mcp_tools.py → infrastructure/infrastructure_analyze.py} +200 -203
  21. src/{client/infrastructure_catalog_mcp_tools.py → infrastructure/infrastructure_catalog.py} +194 -264
  22. src/infrastructure/infrastructure_metrics.py +167 -0
  23. src/{client/infrastructure_resources_mcp_tools.py → infrastructure/infrastructure_resources.py} +192 -223
  24. src/{client/infrastructure_topology_mcp_tools.py → infrastructure/infrastructure_topology.py} +105 -106
  25. src/log/__init__.py +1 -0
  26. src/log/log_alert_configuration.py +331 -0
  27. src/prompts/mcp_prompts.py +900 -0
  28. src/prompts/prompt_loader.py +29 -0
  29. src/prompts/prompt_registry.json +21 -0
  30. mcp_instana-0.1.0.dist-info/METADATA +0 -649
  31. mcp_instana-0.1.0.dist-info/RECORD +0 -19
  32. mcp_instana-0.1.0.dist-info/entry_points.txt +0 -3
  33. src/client/What is the sum of queue depth for all q +0 -55
  34. src/client/instana_client_base.py +0 -93
  35. src/client/log_alert_configuration_mcp_tools.py +0 -316
  36. src/client/show the top 5 services with the highest +0 -28
  37. src/mcp_server.py +0 -343
@@ -4,70 +4,50 @@ Application Resources MCP Tools Module
4
4
  This module provides application resources-specific MCP tools for Instana monitoring.
5
5
  """
6
6
 
7
- import sys
8
- import traceback
9
- from typing import Dict, Any, Optional, List
7
+ import logging
10
8
  from datetime import datetime
9
+ from typing import Any, Dict, List, Optional
11
10
 
12
11
  # Import the necessary classes from the SDK
13
12
  try:
14
13
  from instana_client.api.application_resources_api import ApplicationResourcesApi
15
- from instana_client.api_client import ApiClient
16
- from instana_client.configuration import Configuration
14
+
17
15
  except ImportError as e:
18
- print(f"Error importing Instana SDK: {e}", file=sys.stderr)
19
- traceback.print_exc(file=sys.stderr)
16
+ import logging
17
+ logger = logging.getLogger(__name__)
18
+ logger.error(f"Error importing Instana SDK: {e}", exc_info=True)
20
19
  raise
21
20
 
22
- from .instana_client_base import BaseInstanaClient, register_as_tool
21
+ from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
23
22
 
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)
23
+ # Configure logger for this module
24
+ logger = logging.getLogger(__name__)
28
25
 
29
26
  class ApplicationResourcesMCPTools(BaseInstanaClient):
30
27
  """Tools for application resources in Instana MCP."""
31
-
28
+
32
29
  def __init__(self, read_token: str, base_url: str):
33
30
  """Initialize the Application Resources MCP tools client."""
34
31
  super().__init__(read_token=read_token, base_url=base_url)
35
-
36
- try:
37
-
38
- # Configure the API client with the correct base URL and authentication
39
- configuration = Configuration()
40
- configuration.host = base_url
41
- configuration.api_key['ApiKeyAuth'] = read_token
42
- configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
43
-
44
- # Create an API client with this configuration
45
- api_client = ApiClient(configuration=configuration)
46
-
47
- # Initialize the Instana SDK's ApplicationResourcesApi with our configured client
48
- self.app_api = ApplicationResourcesApi(api_client=api_client)
49
- except Exception as e:
50
- debug_print(f"Error initializing ApplicationResourcesApi: {e}")
51
- traceback.print_exc(file=sys.stderr)
52
- raise
53
-
32
+
54
33
  @register_as_tool
55
- async def get_application_endpoints(self,
56
- name_filter: Optional[str] = None,
57
- types: Optional[List[str]] = None,
58
- technologies: Optional[List[str]] = None,
59
- window_size: Optional[int] = None,
60
- to_time: Optional[int] = None,
61
- page: Optional[int] = None,
62
- page_size: Optional[int] = None,
63
- application_boundary_scope: Optional[str] = None,
64
- ctx=None) -> Dict[str, Any]:
34
+ @with_header_auth(ApplicationResourcesApi)
35
+ async def get_application_endpoints(self,
36
+ name_filter: Optional[str] = None,
37
+ types: Optional[List[str]] = None,
38
+ technologies: Optional[List[str]] = None,
39
+ window_size: Optional[int] = None,
40
+ to_time: Optional[int] = None,
41
+ page: Optional[int] = None,
42
+ page_size: Optional[int] = None,
43
+ application_boundary_scope: Optional[str] = None,
44
+ ctx=None, api_client=None) -> Dict[str, Any]:
65
45
  """
66
46
  Get endpoints for all services from Instana. Use this API endpoint if one wants to retrieve a list of Endpoints. A use case could be to view the endpoint id of an Endpoint.
67
- Retrieve a list of application endpoints from Instana. This tool is useful when you need to get information about endpoints across services in your application.
47
+ Retrieve a list of application endpoints from Instana. This tool is useful when you need to get information about endpoints across services in your application.
68
48
  You can filter by endpoint name, types, technologies, and other parameters. Use this when you want to see what endpoints exist in your application, understand their IDs, or analyze endpoint performance metrics.
69
49
  For example, use this tool when asked about 'application endpoints', 'service endpoints', 'API endpoints in my application','endpoint id of an Endpoint', or when someone wants to 'list all endpoints'.
70
-
50
+
71
51
  Args:
72
52
  name_filter: Name of service to filter by (optional)
73
53
  types: List of endpoint types to filter by (optional)
@@ -78,22 +58,22 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
78
58
  page_size: Number of items per page (optional)
79
59
  application_boundary_scope: Filter for application scope, e.g., 'INBOUND' or 'ALL' (optional)
80
60
  ctx: The MCP context (optional)
81
-
61
+
82
62
  Returns:
83
63
  Dictionary containing endpoints data or error information
84
64
  """
85
65
  try:
86
- debug_print(f"get_application_endpoints called with name_filter={name_filter}")
87
-
66
+ logger.debug(f"get_application_endpoints called with name_filter={name_filter}")
67
+
88
68
  # Set default time range if not provided
89
69
  if not to_time:
90
70
  to_time = int(datetime.now().timestamp() * 1000)
91
-
71
+
92
72
  if not window_size:
93
73
  window_size = 60 * 60 * 1000 # Default to 1 hour
94
-
74
+
95
75
  # Call the get_application_endpoints method from the SDK
96
- result = self.app_api.get_application_endpoints(
76
+ result = api_client.get_application_endpoints(
97
77
  name_filter=name_filter,
98
78
  types=types,
99
79
  technologies=technologies,
@@ -103,38 +83,37 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
103
83
  page_size=page_size,
104
84
  application_boundary_scope=application_boundary_scope
105
85
  )
106
-
86
+
107
87
  # Convert the result to a dictionary
108
88
  if hasattr(result, 'to_dict'):
109
89
  result_dict = result.to_dict()
110
90
  else:
111
91
  # If it's already a dict or another format, use it as is
112
92
  result_dict = result
113
-
114
- debug_print(f"Result from get_application_endpoints: {result_dict}")
93
+
94
+ logger.debug(f"Result from get_application_endpoints: {result_dict}")
115
95
  return result_dict
116
96
  except Exception as e:
117
- debug_print(f"Error in get_application_endpoints: {e}")
118
- traceback.print_exc(file=sys.stderr)
119
- return {"error": f"Failed to get application endpoints: {str(e)}"}
120
-
97
+ logger.error(f"Error in get_application_endpoints: {e}", exc_info=True)
98
+ return {"error": f"Failed to get application endpoints: {e!s}"}
99
+
121
100
  @register_as_tool
122
- async def get_application_services(self,
123
- name_filter: Optional[str] = None,
124
- window_size: Optional[int] = None,
125
- to_time: Optional[int] = None,
126
- page: Optional[int] = None,
127
- page_size: Optional[int] = None,
128
- application_boundary_scope: Optional[str] = None,
129
- include_snapshot_ids: Optional[bool] = None,
130
- ctx=None) -> Dict[str, Any]:
101
+ @with_header_auth(ApplicationResourcesApi)
102
+ async def get_application_services(self,
103
+ name_filter: Optional[str] = None,
104
+ window_size: Optional[int] = None,
105
+ to_time: Optional[int] = None,
106
+ page: Optional[int] = None,
107
+ page_size: Optional[int] = None,
108
+ application_boundary_scope: Optional[str] = None,
109
+ include_snapshot_ids: Optional[bool] = None,
110
+ ctx=None, api_client=None) -> Dict[str, Any]:
131
111
  """
132
- Retrieve a list of services within application perspectives from Instana. This tool is useful when you need to get information about all services in your monitored applications.
133
- You can filter by service name and other parameters to narrow down results. Use this when you want to see what services exist in your application,
134
- understand their IDs, or analyze service-level metrics. This is particularly helpful when you need to retrieve all service IDs present in an Application Perspective for further analysis or monitoring.
135
- For example, use this tool when asked about 'application services', 'microservices in my application', 'list all services', or when someone wants to 'get service information'. A use case could be to retrieve all service ids present in an Application Perspective.
112
+ Retrieve a list of services within application perspectives from Instana. This tool is useful when you need to get information about all services in your monitored applications.
113
+ You can filter by service name and other parameters to narrow down results. Use this when you want to see what services exist in your application,
114
+ understand their IDs, or analyze service-level metrics. This is particularly helpful when you need to retrieve all service IDs present in an Application Perspective for further analysis or monitoring.
115
+ For example, use this tool when asked about 'application services', 'microservices in my application', 'list all services', or when someone wants to 'get service information'. A use case could be to retrieve all service ids present in an Application Perspective.
136
116
 
137
-
138
117
  Args:
139
118
  name_filter: Name of application/service to filter by (optional)
140
119
  window_size: Size of time window in milliseconds (optional)
@@ -144,22 +123,22 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
144
123
  application_boundary_scope: Filter for application scope, e.g., 'INBOUND' or 'ALL' (optional)
145
124
  include_snapshot_ids: Whether to include snapshot IDs in the results (optional)
146
125
  ctx: The MCP context (optional)
147
-
126
+
148
127
  Returns:
149
128
  Dictionary containing service labels with their IDs and summary information
150
129
  """
151
- try:
152
- debug_print(f"get_application_services called with name_filter={name_filter}")
153
-
130
+ try:
131
+ logger.debug(f"get_application_services called with name_filter={name_filter}")
132
+
154
133
  # Set default time range if not provided
155
134
  if not to_time:
156
135
  to_time = int(datetime.now().timestamp() * 1000)
157
-
136
+
158
137
  if not window_size:
159
138
  window_size = 60 * 60 * 1000 # Default to 1 hour
160
-
139
+
161
140
  # Call the get_application_services method from the SDK
162
- result = self.app_api.get_application_services(
141
+ result = api_client.get_application_services(
163
142
  name_filter=name_filter,
164
143
  window_size=window_size,
165
144
  to=to_time,
@@ -168,27 +147,27 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
168
147
  application_boundary_scope=application_boundary_scope,
169
148
  include_snapshot_ids=include_snapshot_ids
170
149
  )
171
-
150
+
172
151
  # Convert the result to a dictionary
173
152
  if hasattr(result, 'to_dict'):
174
153
  result_dict = result.to_dict()
175
154
  else:
176
155
  # If it's already a dict or another format, use it as is
177
156
  result_dict = result
178
-
179
- debug_print(f"Result from get_application_services: {result_dict}")
180
-
157
+
158
+ logger.debug(f"Result from get_application_services: {result_dict}")
159
+
181
160
  # Extract service labels and IDs from the items
182
161
  services = []
183
162
  service_labels = []
184
163
  items = result_dict.get('items', [])
185
-
164
+
186
165
  for item in items:
187
166
  if isinstance(item, dict):
188
167
  service_id = item.get('id', '')
189
168
  label = item.get('label', '')
190
169
  technologies = item.get('technologies', [])
191
-
170
+
192
171
  if label and service_id:
193
172
  service_labels.append(label)
194
173
  services.append({
@@ -203,12 +182,12 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
203
182
  'label': item.label,
204
183
  'technologies': getattr(item, 'technologies', [])
205
184
  })
206
-
185
+
207
186
  # Sort services by label alphabetically and limit to first 15
208
187
  services.sort(key=lambda x: x['label'])
209
188
  limited_services = services[:15]
210
189
  service_labels = [service['label'] for service in limited_services]
211
-
190
+
212
191
  return {
213
192
  "message": f"Found {len(services)} services in application perspectives. Showing first {len(limited_services)}:",
214
193
  "service_labels": service_labels,
@@ -216,29 +195,29 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
216
195
  "total_available": len(services),
217
196
  "showing": len(limited_services)
218
197
  }
219
-
198
+
220
199
  except Exception as e:
221
- debug_print(f"Error in get_application_services: {e}")
222
- traceback.print_exc(file=sys.stderr)
223
- return {"error": f"Failed to get application services: {str(e)}"}
200
+ logger.error(f"Error in get_application_services: {e}", exc_info=True)
201
+ return {"error": f"Failed to get application services: {e!s}"}
224
202
 
225
203
 
226
204
  @register_as_tool
227
- async def get_applications(self,
228
- name_filter: Optional[str] = None,
229
- window_size: Optional[int] = None,
230
- to_time: Optional[int] = None,
231
- page: Optional[int] = None,
232
- page_size: Optional[int] = None,
233
- application_boundary_scope: Optional[str] = None,
234
- ctx=None) -> List[str]:
205
+ @with_header_auth(ApplicationResourcesApi)
206
+ async def get_applications(self,
207
+ name_filter: Optional[str] = None,
208
+ window_size: Optional[int] = None,
209
+ to_time: Optional[int] = None,
210
+ page: Optional[int] = None,
211
+ page_size: Optional[int] = None,
212
+ application_boundary_scope: Optional[str] = None,
213
+ ctx=None, api_client=None) -> List[str]:
235
214
  """
236
- Retrieve a list of Application Perspectives from Instana. This tool is useful when you need to get information about any one application perspective in Instana.
237
- You can filter by application name and other parameters to narrow down results. Use this tool when you want to see what application perspectives exist, understand their IDs,
238
- or get an overview of your monitored applications. This is particularly helpful when you need to retrieve application IDs for use with other Instana APIs or when setting up monitoring dashboards.
239
- For example, use this tool when asked about 'application perspectives', 'list all applications in Instana', 'what applications are being monitored', or when someone wants to 'get application IDs'
240
- or 'get details about an application'.
241
-
215
+ Retrieve a list of Application Perspectives from Instana. This tool is useful when you need to get information about any one application perspective in Instana.
216
+ You can filter by application name and other parameters to narrow down results. Use this tool when you want to see what application perspectives exist, understand their IDs,
217
+ or get an overview of your monitored applications. This is particularly helpful when you need to retrieve application IDs for use with other Instana APIs or when setting up monitoring dashboards.
218
+ For example, use this tool when asked about 'application perspectives', 'list all applications in Instana', 'what applications are being monitored', or when someone wants to 'get application IDs'
219
+ or 'get details about an application'.
220
+
242
221
  Args:
243
222
  name_filter: Name of application to filter by (optional)
244
223
  window_size: Size of time window in milliseconds (optional)
@@ -247,22 +226,22 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
247
226
  page_size: Number of items per page (optional)
248
227
  application_boundary_scope: Filter for application scope, e.g., 'INBOUND' or 'ALL' (optional)
249
228
  ctx: The MCP context (optional)
250
-
229
+
251
230
  Returns:
252
231
  List of application names
253
232
  """
254
233
  try:
255
- debug_print(f"get_applications called with name_filter={name_filter}")
256
-
234
+ logger.debug(f"get_applications called with name_filter={name_filter}")
235
+
257
236
  # Set default time range if not provided
258
237
  if not to_time:
259
238
  to_time = int(datetime.now().timestamp() * 1000)
260
-
239
+
261
240
  if not window_size:
262
241
  window_size = 60 * 60 * 1000 # Default to 1 hour
263
-
242
+
264
243
  # Call the get_applications method from the SDK
265
- result = self.app_api.get_applications(
244
+ result = api_client.get_applications(
266
245
  name_filter=name_filter,
267
246
  window_size=window_size,
268
247
  to=to_time,
@@ -270,20 +249,20 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
270
249
  page_size=page_size,
271
250
  application_boundary_scope=application_boundary_scope
272
251
  )
273
-
252
+
274
253
  # Convert the result to a dictionary
275
254
  if hasattr(result, 'to_dict'):
276
255
  result_dict = result.to_dict()
277
256
  else:
278
257
  # If it's already a dict or another format, use it as is
279
258
  result_dict = result
280
-
281
- debug_print(f"Result from get_applications: {result_dict}")
282
-
259
+
260
+ logger.debug(f"Result from get_applications: {result_dict}")
261
+
283
262
  # Extract labels from the items
284
263
  labels = []
285
264
  items = result_dict.get('items', [])
286
-
265
+
287
266
  for item in items:
288
267
  if isinstance(item, dict):
289
268
  label = item.get('label', '')
@@ -291,34 +270,34 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
291
270
  labels.append(label)
292
271
  elif hasattr(item, 'label'):
293
272
  labels.append(item.label)
294
-
273
+
295
274
  # Sort labels alphabetically and limit to first 15
296
275
  labels.sort()
297
276
  return labels[:15]
298
-
277
+
299
278
  except Exception as e:
300
- debug_print(f"Error in get_applications: {e}")
301
- traceback.print_exc(file=sys.stderr)
302
- return [f"Error: Failed to get applications: {str(e)}"]
279
+ logger.error(f"Error in get_applications: {e}", exc_info=True)
280
+ return [f"Error: Failed to get applications: {e!s}"]
303
281
 
304
282
 
305
283
  @register_as_tool
306
- async def get_services(self,
307
- name_filter: Optional[str] = None,
308
- window_size: Optional[int] = None,
309
- to_time: Optional[int] = None,
310
- page: Optional[int] = None,
311
- page_size: Optional[int] = None,
312
- include_snapshot_ids: Optional[bool] = None,
313
- ctx=None) -> str:
284
+ @with_header_auth(ApplicationResourcesApi)
285
+ async def get_services(self,
286
+ name_filter: Optional[str] = None,
287
+ window_size: Optional[int] = None,
288
+ to_time: Optional[int] = None,
289
+ page: Optional[int] = None,
290
+ page_size: Optional[int] = None,
291
+ include_snapshot_ids: Optional[bool] = None,
292
+ ctx=None, api_client=None) -> str:
314
293
  """
315
294
  Retrieve a list of services from Instana. A use case could be to view the service id, or details,or information of a Service.
316
295
  This tool is useful when you need to get information about all services across your monitored environment,regardless of which application perspective they belong to.
317
- You can filter by service name and other parameters to narrow down results.Use this when you want to see what services exist in your system, understand their IDs .
318
- This is particularly helpful when you need to retrieve service IDs for further analysis or monitoring. For example, use this tool when asked about 'all services',
296
+ You can filter by service name and other parameters to narrow down results.Use this when you want to see what services exist in your system, understand their IDs .
297
+ This is particularly helpful when you need to retrieve service IDs for further analysis or monitoring. For example, use this tool when asked about 'all services',
319
298
  'list services across applications', or when someone wants to 'get service information without application context'. A use case could be to view the service ID of a specific Service.
320
299
 
321
-
300
+
322
301
  Args:
323
302
  name_filter: Name of service to filter by (optional)
324
303
  window_size: Size of time window in milliseconds (optional)
@@ -327,22 +306,22 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
327
306
  page_size: Number of items per page (optional)
328
307
  include_snapshot_ids: Whether to include snapshot IDs in the results (optional)
329
308
  ctx: The MCP context (optional)
330
-
309
+
331
310
  Returns:
332
311
  String containing service names
333
312
  """
334
313
  try:
335
- debug_print(f"get_services called with name_filter={name_filter}")
336
-
314
+ logger.debug(f"get_services called with name_filter={name_filter}")
315
+
337
316
  # Set default time range if not provided
338
317
  if not to_time:
339
318
  to_time = int(datetime.now().timestamp() * 1000)
340
-
319
+
341
320
  if not window_size:
342
321
  window_size = 60 * 60 * 1000 # Default to 1 hour
343
-
322
+
344
323
  # Call the get_services method from the SDK
345
- result = self.app_api.get_services(
324
+ result = api_client.get_services(
346
325
  name_filter=name_filter,
347
326
  window_size=window_size,
348
327
  to=to_time,
@@ -350,20 +329,20 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
350
329
  page_size=page_size,
351
330
  include_snapshot_ids=include_snapshot_ids
352
331
  )
353
-
332
+
354
333
  # Convert the result to a dictionary
355
334
  if hasattr(result, 'to_dict'):
356
335
  result_dict = result.to_dict()
357
336
  else:
358
337
  # If it's already a dict or another format, use it as is
359
338
  result_dict = result
360
-
361
- debug_print(f"Result from get_services: {result_dict}")
362
-
339
+
340
+ logger.debug(f"Result from get_services: {result_dict}")
341
+
363
342
  # Extract labels from the items
364
343
  labels = []
365
344
  items = result_dict.get('items', [])
366
-
345
+
367
346
  for item in items:
368
347
  if isinstance(item, dict):
369
348
  label = item.get('label', '')
@@ -371,21 +350,20 @@ class ApplicationResourcesMCPTools(BaseInstanaClient):
371
350
  labels.append(label)
372
351
  elif hasattr(item, 'label'):
373
352
  labels.append(item.label)
374
-
353
+
375
354
  # Sort labels alphabetically and limit to first 10
376
355
  labels.sort()
377
356
  limited_labels = labels[:10]
378
-
357
+
379
358
  # Return as a formatted string that forces display
380
359
  services_text = "Services found in your environment:\n"
381
360
  for i, label in enumerate(limited_labels, 1):
382
361
  services_text += f"{i}. {label}\n"
383
-
362
+
384
363
  services_text += f"\nShowing {len(limited_labels)} out of {len(labels)} total services."
385
-
364
+
386
365
  return services_text
387
-
366
+
388
367
  except Exception as e:
389
- debug_print(f"Error in get_services: {e}")
390
- traceback.print_exc(file=sys.stderr)
391
- return f"Error: Failed to get services: {str(e)}"
368
+ logger.error(f"Error in get_services: {e}", exc_info=True)
369
+ return f"Error: Failed to get services: {e!s}"