mcp-instana 0.3.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/METADATA +186 -311
  2. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/RECORD +30 -22
  3. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/WHEEL +1 -1
  4. src/application/application_alert_config.py +393 -136
  5. src/application/application_analyze.py +597 -594
  6. src/application/application_call_group.py +528 -0
  7. src/application/application_catalog.py +0 -8
  8. src/application/application_global_alert_config.py +275 -57
  9. src/application/application_metrics.py +377 -237
  10. src/application/application_resources.py +414 -325
  11. src/application/application_settings.py +608 -1530
  12. src/application/application_topology.py +62 -62
  13. src/core/custom_dashboard_smart_router_tool.py +135 -0
  14. src/core/server.py +95 -119
  15. src/core/smart_router_tool.py +574 -0
  16. src/core/utils.py +17 -8
  17. src/custom_dashboard/custom_dashboard_tools.py +422 -0
  18. src/event/events_tools.py +57 -9
  19. src/infrastructure/elicitation_handler.py +338 -0
  20. src/infrastructure/entity_registry.py +329 -0
  21. src/infrastructure/infrastructure_analyze_new.py +600 -0
  22. src/infrastructure/{infrastructure_analyze.py → infrastructure_analyze_old.py} +1 -16
  23. src/infrastructure/infrastructure_catalog.py +37 -32
  24. src/infrastructure/infrastructure_metrics.py +93 -16
  25. src/infrastructure/infrastructure_resources.py +6 -24
  26. src/infrastructure/infrastructure_topology.py +29 -23
  27. src/observability.py +29 -0
  28. src/prompts/application/application_settings.py +58 -0
  29. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/entry_points.txt +0 -0
  30. {mcp_instana-0.3.1.dist-info → mcp_instana-0.7.0.dist-info}/licenses/LICENSE.md +0 -0
@@ -34,6 +34,8 @@ try:
34
34
  NewApplicationConfig, #type: ignore
35
35
  NewManualServiceConfig, #type: ignore
36
36
  ServiceConfig, #type: ignore
37
+ TagFilter, #type: ignore
38
+ TagFilterExpression, #type: ignore
37
39
  )
38
40
  except ImportError as e:
39
41
  print(f"Error importing Instana SDK: {e}", file=sys.stderr)
@@ -71,14 +73,101 @@ class ApplicationSettingsMCPTools(BaseInstanaClient):
71
73
  traceback.print_exc(file=sys.stderr)
72
74
  raise
73
75
 
74
- @register_as_tool(
75
- title="Get All Applications Configs",
76
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
77
- )
76
+ # CRUD Operations Dispatcher - called by smart_router_tool.py
77
+ async def execute_settings_operation(
78
+ self,
79
+ operation: str,
80
+ resource_subtype: str,
81
+ id: Optional[str] = None,
82
+ payload: Optional[Union[Dict[str, Any], str]] = None,
83
+ request_body: Optional[List[str]] = None,
84
+ ctx=None
85
+ ) -> Dict[str, Any]:
86
+ """
87
+ Execute Application Settings CRUD operations.
88
+ Called by the smart router tool.
89
+
90
+ Args:
91
+ operation: Operation to perform (get_all, get, create, update, delete, order, replace_all)
92
+ resource_subtype: Type of settings resource (application, endpoint, service, manual_service)
93
+ id: Resource ID (for get, update, delete operations)
94
+ payload: Configuration payload (for create, update operations)
95
+ request_body: List of IDs (for order, replace_all operations)
96
+ ctx: MCP context
97
+
98
+ Returns:
99
+ Operation result dictionary
100
+ """
101
+ try:
102
+ # Route based on resource_subtype and operation
103
+ if resource_subtype == "application":
104
+ if operation == "get_all":
105
+ return await self._get_all_applications_configs(ctx)
106
+ elif operation == "get":
107
+ return await self._get_application_config(id, ctx)
108
+ elif operation == "create":
109
+ return await self._add_application_config(payload, ctx)
110
+ elif operation == "update":
111
+ return await self._update_application_config(id, payload, ctx)
112
+ elif operation == "delete":
113
+ return await self._delete_application_config(id, ctx)
114
+
115
+ elif resource_subtype == "endpoint":
116
+ if operation == "get_all":
117
+ return await self._get_all_endpoint_configs(ctx)
118
+ elif operation == "get":
119
+ return await self._get_endpoint_config(id, ctx)
120
+ elif operation == "create":
121
+ return await self._create_endpoint_config(payload, ctx)
122
+ elif operation == "update":
123
+ return await self._update_endpoint_config(id, payload, ctx)
124
+ elif operation == "delete":
125
+ return await self._delete_endpoint_config(id, ctx)
126
+
127
+ elif resource_subtype == "service":
128
+ if operation == "get_all":
129
+ return await self._get_all_service_configs(ctx)
130
+ elif operation == "get":
131
+ return await self._get_service_config(id, ctx)
132
+ elif operation == "create":
133
+ return await self._add_service_config(payload, ctx)
134
+ elif operation == "update":
135
+ return await self._update_service_config(id, payload, ctx)
136
+ elif operation == "delete":
137
+ return await self._delete_service_config(id, ctx)
138
+ elif operation == "order":
139
+ return await self._order_service_config(request_body, ctx)
140
+ elif operation == "replace_all":
141
+ return await self._replace_all_service_configs(payload, ctx)
142
+
143
+ elif resource_subtype == "manual_service":
144
+ if operation == "get_all":
145
+ return await self._get_all_manual_service_configs(ctx)
146
+ elif operation == "create":
147
+ return await self._add_manual_service_config(payload, ctx)
148
+ elif operation == "update":
149
+ return await self._update_manual_service_config(id, payload, ctx)
150
+ elif operation == "delete":
151
+ return await self._delete_manual_service_config(id, ctx)
152
+ elif operation == "replace_all":
153
+ return await self._replace_all_manual_service_config(payload, ctx)
154
+
155
+ return {"error": f"Operation '{operation}' not supported for resource_subtype '{resource_subtype}'"}
156
+
157
+ except Exception as e:
158
+ logger.error(f"Error executing {operation} on {resource_subtype}: {e}", exc_info=True)
159
+ return {"error": f"Error executing {operation} on {resource_subtype}: {e!s}"}
160
+
161
+ # Individual operation functions
162
+
163
+ # @register_as_tool(
164
+ # title="Get All Applications Configs",
165
+ # annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
166
+ # )
78
167
  @with_header_auth(ApplicationSettingsApi)
79
- async def get_all_applications_configs(self,
80
- ctx=None,
81
- api_client=None) -> List[Dict[str, Any]]:
168
+ async def _get_all_applications_configs(self,
169
+ ctx=None,
170
+ api_client=None) -> List[Dict[str, Any]]:
82
171
  """
83
172
  All Application Perspectives Configuration
84
173
  Get a list of all Application Perspectives with their configuration settings.
@@ -115,1693 +204,682 @@ class ApplicationSettingsMCPTools(BaseInstanaClient):
115
204
  traceback.print_exc(file=sys.stderr)
116
205
  return [{"error": f"Failed to get all applications: {e!s}"}]
117
206
 
118
- @register_as_tool(
119
- title="Add Application Config",
120
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
121
- )
122
- @with_header_auth(ApplicationSettingsApi)
123
- async def add_application_config(self,
124
- payload: Union[Dict[str, Any], str],
125
- ctx=None,
126
- api_client=None) -> Dict[str, Any]:
207
+ def _validate_and_prepare_application_payload(self, payload: Union[Dict[str, Any], str]) -> Dict[str, Any]:
127
208
  """
128
- Add a new Application Perspective configuration.
129
- This tool allows you to create a new Application Perspective with specified settings.
130
- Sample Payload: {
131
- "accessRules": [
132
- {
133
- "accessType": "READ_WRITE",
134
- "relationType": "GLOBAL",
135
- "relatedId": null
136
- }
137
- ],
138
- "boundaryScope": "INBOUND",
139
- "label": "Discount Build 6987",
140
- "scope": "INCLUDE_IMMEDIATE_DOWNSTREAM_DATABASE_AND_MESSAGING",
141
- "tagFilterExpression": {
142
- "type": "EXPRESSION",
143
- "logicalOperator": "AND",
144
- "elements": [
145
- {
146
- "type": "TAG_FILTER",
147
- "name": "kubernetes.label",
148
- "stringValue": "stage=canary",
149
- "numberValue": null,
150
- "booleanValue": null,
151
- "key": "stage",
152
- "value": "canary",
153
- "operator": "EQUALS",
154
- "entity": "DESTINATION"
155
- },
156
- {
157
- "type": "TAG_FILTER",
158
- "name": "kubernetes.label",
159
- "stringValue": "build=6987",
160
- "numberValue": null,
161
- "booleanValue": null,
162
- "key": "build",
163
- "value": "6987",
164
- "operator": "EQUALS",
165
- "entity": "DESTINATION"
166
- },
167
- {
168
- "type": "TAG_FILTER",
169
- "name": "kubernetes.label",
170
- "stringValue": "app=discount",
171
- "numberValue": null,
172
- "booleanValue": null,
173
- "key": "app",
174
- "value": "discount",
175
- "operator": "EQUALS",
176
- "entity": "DESTINATION"
177
- }
178
- ]
179
- }
180
- }
209
+ Validate and prepare application configuration payload with proper defaults.
210
+
181
211
  Returns:
182
- Dictionary containing the created application perspective configuration or error information
212
+ Dict with either 'payload' (validated) or 'error' and 'missing_fields'
183
213
  """
184
- try:
185
-
186
- if not payload:
187
- return {"error": "payload is required"}
188
-
189
- # Parse the payload if it's a string
190
- if isinstance(payload, str):
191
- logger.debug("Payload is a string, attempting to parse")
192
- try:
193
- import json
194
- try:
195
- parsed_payload = json.loads(payload)
196
- logger.debug("Successfully parsed payload as JSON")
197
- request_body = parsed_payload
198
- except json.JSONDecodeError as e:
199
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
200
-
201
- # Try replacing single quotes with double quotes
202
- fixed_payload = payload.replace("'", "\"")
203
- try:
204
- parsed_payload = json.loads(fixed_payload)
205
- logger.debug("Successfully parsed fixed JSON")
206
- request_body = parsed_payload
207
- except json.JSONDecodeError:
208
- # Try as Python literal
209
- import ast
210
- try:
211
- parsed_payload = ast.literal_eval(payload)
212
- logger.debug("Successfully parsed payload as Python literal")
213
- request_body = parsed_payload
214
- except (SyntaxError, ValueError) as e2:
215
- logger.debug(f"Failed to parse payload string: {e2}")
216
- return {"error": f"Invalid payload format: {e2}", "payload": payload}
217
- except Exception as e:
218
- logger.debug(f"Error parsing payload string: {e}")
219
- return {"error": f"Failed to parse payload: {e}", "payload": payload}
220
- else:
221
- # If payload is already a dictionary, use it directly
222
- logger.debug("Using provided payload dictionary")
223
- request_body = payload
224
-
225
- # Import the NewApplicationConfig class
226
- try:
227
- from instana_client.models.new_application_config import (
228
- NewApplicationConfig,
229
- )
230
- logger.debug("Successfully imported NewApplicationConfig")
231
- except ImportError as e:
232
- logger.debug(f"Error importing NewApplicationConfig: {e}")
233
- return {"error": f"Failed to import NewApplicationConfig: {e!s}"}
234
-
235
- # Create an NewApplicationConfig object from the request body
214
+ # Parse the payload if it's a string
215
+ if isinstance(payload, str):
216
+ import json
236
217
  try:
237
- logger.debug(f"Creating NewApplicationConfig with params: {request_body}")
238
- config_object = NewApplicationConfig(**request_body)
239
- logger.debug("Successfully created config object")
240
- except Exception as e:
241
- logger.debug(f"Error creating NewApplicationConfig: {e}")
242
- return {"error": f"Failed to create config object: {e!s}"}
243
-
244
- # Call the add_application_config method from the SDK
245
- logger.debug("Calling add_application_config with config object")
246
- result = api_client.add_application_config(
247
- new_application_config=config_object
248
- )
249
-
250
- # Convert the result to a dictionary
251
- if hasattr(result, 'to_dict'):
252
- result_dict = result.to_dict()
253
- else:
254
- # If it's already a dict or another format, use it as is
255
- result_dict = result or {
256
- "success": True,
257
- "message": "Create new application config"
218
+ request_body = json.loads(payload)
219
+ except json.JSONDecodeError:
220
+ import ast
221
+ try:
222
+ request_body = ast.literal_eval(payload)
223
+ except (SyntaxError, ValueError) as e:
224
+ return {"error": f"Invalid payload format: {e}"}
225
+ else:
226
+ request_body = payload.copy() if payload else {}
227
+
228
+ # Define required fields
229
+ required_fields = ['label']
230
+ missing_fields = []
231
+
232
+ # Check for required fields
233
+ for field in required_fields:
234
+ if field not in request_body or not request_body[field]:
235
+ missing_fields.append(field)
236
+
237
+ if missing_fields:
238
+ return {
239
+ "error": "Missing required fields for application configuration",
240
+ "missing_fields": missing_fields,
241
+ "required_fields": {
242
+ "label": "Application perspective name (string, required)",
243
+ },
244
+ "optional_fields": {
245
+ "tagFilterExpression": "Tag filter to match services (dict, optional - defaults to empty EXPRESSION)",
246
+ "scope": "Monitoring scope (string, optional - defaults to 'INCLUDE_ALL_DOWNSTREAM')",
247
+ "boundaryScope": "Boundary scope (string, optional - defaults to 'ALL')",
248
+ "accessRules": "Access control rules (list, optional - defaults to READ_WRITE GLOBAL access)"
249
+ },
250
+ "scope_options": ["INCLUDE_ALL_DOWNSTREAM", "INCLUDE_IMMEDIATE_DOWNSTREAM_DATABASE_AND_MESSAGING", "INCLUDE_NO_DOWNSTREAM"],
251
+ "boundary_scope_options": ["ALL", "INBOUND", "DEFAULT"],
252
+ "access_rules_options": ["READ_WRITE_GLOBAL", "READ_ONLY_GLOBAL", "CUSTOM"],
253
+ "elicitation_prompt": "Please provide the following configuration options:\n1. Scope (INCLUDE_ALL_DOWNSTREAM/INCLUDE_IMMEDIATE_DOWNSTREAM_DATABASE_AND_MESSAGING/INCLUDE_NO_DOWNSTREAM)\n2. Boundary Scope (ALL/INBOUND/DEFAULT)\n3. Access Rules (READ_WRITE_GLOBAL/READ_ONLY_GLOBAL/CUSTOM)\n4. Tag Filter Expression (optional)",
254
+ "example_minimal": {
255
+ "label": "My Application"
256
+ },
257
+ "example_with_options": {
258
+ "label": "My Application",
259
+ "scope": "INCLUDE_ALL_DOWNSTREAM",
260
+ "boundaryScope": "ALL",
261
+ "accessRules": [{"accessType": "READ_WRITE", "relationType": "GLOBAL"}],
262
+ "tagFilterExpression": {
263
+ "type": "TAG_FILTER",
264
+ "name": "service.name",
265
+ "operator": "CONTAINS",
266
+ "entity": "DESTINATION",
267
+ "value": "my-service"
268
+ }
258
269
  }
270
+ }
259
271
 
260
- logger.debug(f"Result from add_application_config: {result_dict}")
261
- return result_dict
262
- except Exception as e:
263
- logger.error(f"Error in add_application_config: {e}")
264
- return {"error": f"Failed to add new application config: {e!s}"}
265
-
266
- @register_as_tool(
267
- title="Delete Application Config",
268
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True)
269
- )
270
- @with_header_auth(ApplicationSettingsApi)
271
- async def delete_application_config(self,
272
- id: str,
273
- ctx=None,
274
- api_client=None) -> Dict[str, Any]:
275
- """
276
- Delete an Application Perspective configuration.
277
- This tool allows you to delete an existing Application Perspective by its ID.
278
-
279
- Args:
280
- application_id: The ID of the application perspective to delete
281
- ctx: The MCP context (optional)
282
-
283
- Returns:
284
- Dictionary containing the result of the deletion or error information
285
- """
286
- try:
287
- if not id:
288
- return {"error": "Application perspective ID is required for deletion"}
289
-
272
+ # Apply defaults for optional fields only if not provided
273
+ if 'scope' not in request_body:
274
+ request_body['scope'] = 'INCLUDE_ALL_DOWNSTREAM'
275
+ debug_print("Applied default scope: INCLUDE_ALL_DOWNSTREAM")
290
276
 
291
- debug_print(f"Deleting application perspective with ID: {id}")
292
- # Call the delete_application_config method from the SDK
293
- api_client.delete_application_config(id=id)
277
+ if 'boundaryScope' not in request_body:
278
+ request_body['boundaryScope'] = 'ALL'
279
+ debug_print("Applied default boundaryScope: ALL")
294
280
 
295
- result_dict = {
296
- "success": True,
297
- "message": f"Application Confiuguration '{id}' has been successfully deleted"
281
+ if 'accessRules' not in request_body:
282
+ request_body['accessRules'] = [
283
+ {
284
+ "accessType": "READ_WRITE",
285
+ "relationType": "GLOBAL"
286
+ }
287
+ ]
288
+ debug_print("Applied default accessRules: READ_WRITE GLOBAL")
289
+
290
+ # If no tagFilterExpression provided, use empty EXPRESSION
291
+ if 'tagFilterExpression' not in request_body:
292
+ request_body['tagFilterExpression'] = {
293
+ "type": "EXPRESSION",
294
+ "logicalOperator": "AND",
295
+ "elements": []
298
296
  }
297
+ debug_print("Applied default tagFilterExpression: empty EXPRESSION")
298
+
299
+ # Convert nested tagFilterExpression to model objects if present
300
+ if 'tagFilterExpression' in request_body and isinstance(request_body['tagFilterExpression'], dict):
301
+ tag_expr = request_body['tagFilterExpression']
302
+
303
+ # Handle EXPRESSION type with nested elements
304
+ if tag_expr.get('type') == 'EXPRESSION' and 'elements' in tag_expr:
305
+ converted_elements = []
306
+ for element in tag_expr['elements']:
307
+ if isinstance(element, dict):
308
+ element_copy = element.copy()
309
+ element_copy.pop('value', None)
310
+ element_copy.pop('key', None)
311
+ converted_elements.append(TagFilter(**element_copy))
312
+ else:
313
+ converted_elements.append(element)
314
+ tag_expr['elements'] = converted_elements
315
+ request_body['tagFilterExpression'] = TagFilterExpression(**tag_expr)
316
+
317
+ # Handle TAG_FILTER type (simple filter)
318
+ elif tag_expr.get('type') == 'TAG_FILTER':
319
+ # For TAG_FILTER, ensure both 'value' and 'stringValue' are present
320
+ # Don't convert to TagFilter model - keep as dict to preserve both fields
321
+ tag_filter_copy = tag_expr.copy()
322
+ tag_filter_copy.pop('key', None)
323
+ if 'stringValue' not in tag_filter_copy and 'value' in tag_expr:
324
+ tag_filter_copy['stringValue'] = tag_expr['value']
325
+ if 'value' not in tag_filter_copy and 'stringValue' in tag_expr:
326
+ tag_filter_copy['value'] = tag_expr['stringValue']
327
+ # Keep as dictionary - don't convert to TagFilter model
328
+ request_body['tagFilterExpression'] = tag_filter_copy
329
+
330
+ return {"payload": request_body}
299
331
 
300
- debug_print(f"Successfully deleted application perspective with ID: {id}")
301
- return result_dict
302
- except Exception as e:
303
- debug_print(f"Error in delete_application_config: {e}")
304
- traceback.print_exc(file=sys.stderr)
305
- return {"error": f"Failed to delete application configuration: {e!s}"}
306
-
307
- @register_as_tool(
308
- title="Get Application Config",
309
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
310
- )
311
332
  @with_header_auth(ApplicationSettingsApi)
312
- async def get_application_config(self,
313
- id: str,
314
- ctx=None,
315
- api_client=None) -> Dict[str, Any]:
333
+ async def _add_application_config(self,
334
+ payload: Union[Dict[str, Any], str],
335
+ ctx=None,
336
+ api_client=None) -> Dict[str, Any]:
316
337
  """
317
- Get an Application Perspective configuration by ID.
318
- This tool retrieves the configuration settings for a specific Application Perspective.
338
+ Add a new Application Perspective configuration.
319
339
 
320
- Args:
321
- id: The ID of the application perspective to retrieve
322
- ctx: The MCP context (optional)
340
+ Required fields:
341
+ - label: Application perspective name
323
342
 
324
- Returns:
325
- Dictionary containing the application perspective configuration or error information
343
+ Optional fields (with defaults):
344
+ - tagFilterExpression: Tag filter (defaults to empty EXPRESSION)
345
+ - scope: Monitoring scope (defaults to 'INCLUDE_ALL_DOWNSTREAM')
346
+ - boundaryScope: Boundary scope (defaults to 'ALL')
347
+ - accessRules: Access rules (defaults to READ_WRITE GLOBAL)
326
348
  """
327
349
  try:
328
- debug_print(f"Fetching application perspective with ID: {id}")
329
- # Call the get_application_config method from the SDK
330
- result = api_client.get_application_config(id=id)
350
+ if not payload:
351
+ return {
352
+ "error": "payload is required",
353
+ "required_fields": {
354
+ "label": "Application perspective name (string, required)"
355
+ },
356
+ "example": {
357
+ "label": "My Application"
358
+ }
359
+ }
331
360
 
332
- # Convert the result to a dictionary
333
- if hasattr(result, 'to_dict'):
334
- result_dict = result.to_dict()
335
- else:
336
- result_dict = result
361
+ # Validate and prepare payload with defaults
362
+ validation_result = self._validate_and_prepare_application_payload(payload)
337
363
 
338
- debug_print(f"Result from get_application_config: {result_dict}")
339
- return result_dict
340
- except Exception as e:
341
- debug_print(f"Error in get_application_config: {e}")
342
- traceback.print_exc(file=sys.stderr)
343
- return {"error": f"Failed to get application configuration: {e!s}"}
364
+ if "error" in validation_result:
365
+ return validation_result
344
366
 
345
- @register_as_tool(
346
- title="Update Application Config",
347
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
348
- )
349
- @with_header_auth(ApplicationSettingsApi)
350
- async def update_application_config(
351
- self,
352
- id: str,
353
- payload: Union[Dict[str, Any], str],
354
- ctx=None,
355
- api_client=None
356
- ) -> Dict[str, Any]:
357
- """
358
- Update an existing Application Perspective configuration.
359
- This tool allows you to update an existing Application Perspective with specified application Id.
367
+ request_body = validation_result["payload"]
360
368
 
361
- Args:
362
- id: The ID of the application perspective to retrieve
363
- Sample payload: {
364
- "accessRules": [
365
- {
366
- "accessType": "READ",
367
- "relationType": "ROLE"
368
- }
369
- ],
370
- "boundaryScope": "INBOUND",
371
- "id": "CxJ55sRbQwqBIfw5DzpRmQ",
372
- "label": "Discount Build 1",
373
- "scope": "INCLUDE_NO_DOWNSTREAM",
374
- "tagFilterExpression": {
375
- "type": "EXPRESSION",
376
- "logicalOperator": "AND",
377
- "elements": [
378
- {
379
- "type": "TAG_FILTER",
380
- "name": "kubernetes.label",
381
- "stringValue": "stage=canary",
382
- "numberValue": null,
383
- "booleanValue": null,
384
- "key": "stage",
385
- "value": "canary",
386
- "operator": "EQUALS",
387
- "entity": "DESTINATION"
388
- },
389
- {
390
- "type": "TAG_FILTER",
391
- "name": "kubernetes.label",
392
- "stringValue": "build=6987",
393
- "numberValue": null,
394
- "booleanValue": null,
395
- "key": "build",
396
- "value": "6987",
397
- "operator": "EQUALS",
398
- "entity": "DESTINATION"
399
- },
400
- {
401
- "type": "TAG_FILTER",
402
- "name": "kubernetes.label",
403
- "stringValue": "app=discount",
404
- "numberValue": null,
405
- "booleanValue": null,
406
- "key": "app",
407
- "value": "discount",
408
- "operator": "EQUALS",
409
- "entity": "DESTINATION"
410
- }
411
- ]
412
- }
413
- }
414
- ctx: The MCP context (optional)
415
- Returns:
416
- Dictionary containing the created application perspective configuration or error information
417
- """
418
- try:
369
+ # Debug: Log the request body before creating the config object
370
+ debug_print(f"DEBUG: request_body before NewApplicationConfig: {request_body}")
419
371
 
420
- if not payload or not id:
421
- return {"error": "missing arguments"}
372
+ config_object = NewApplicationConfig(**request_body)
422
373
 
423
- # Parse the payload if it's a string
424
- if isinstance(payload, str):
425
- logger.debug("Payload is a string, attempting to parse")
426
- try:
427
- import json
428
- try:
429
- parsed_payload = json.loads(payload)
430
- logger.debug("Successfully parsed payload as JSON")
431
- request_body = parsed_payload
432
- except json.JSONDecodeError as e:
433
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
434
-
435
- # Try replacing single quotes with double quotes
436
- fixed_payload = payload.replace("'", "\"")
437
- try:
438
- parsed_payload = json.loads(fixed_payload)
439
- logger.debug("Successfully parsed fixed JSON")
440
- request_body = parsed_payload
441
- except json.JSONDecodeError:
442
- # Try as Python literal
443
- import ast
444
- try:
445
- parsed_payload = ast.literal_eval(payload)
446
- logger.debug("Successfully parsed payload as Python literal")
447
- request_body = parsed_payload
448
- except (SyntaxError, ValueError) as e2:
449
- logger.debug(f"Failed to parse payload string: {e2}")
450
- return {"error": f"Invalid payload format: {e2}", "payload": payload}
451
- except Exception as e:
452
- logger.debug(f"Error parsing payload string: {e}")
453
- return {"error": f"Failed to parse payload: {e}", "payload": payload}
454
- else:
455
- # If payload is already a dictionary, use it directly
456
- logger.debug("Using provided payload dictionary")
457
- request_body = payload
374
+ # Debug: Log what the config object looks like after creation
375
+ if hasattr(config_object, 'to_dict'):
376
+ debug_print(f"DEBUG: config_object.to_dict(): {config_object.to_dict()}")
377
+
378
+ result = api_client.add_application_config(new_application_config=config_object)
458
379
 
459
- # Import the ApplicationConfig class
460
- try:
461
- from instana_client.models.application_config import (
462
- ApplicationConfig,
463
- )
464
- logger.debug("Successfully imported ApplicationConfig")
465
- except ImportError as e:
466
- logger.debug(f"Error importing ApplicationConfig: {e}")
467
- return {"error": f"Failed to import ApplicationConfig: {e!s}"}
468
-
469
- # Create an ApplicationConfig object from the request body
470
- try:
471
- logger.debug(f"Creating ApplicationConfig with params: {request_body}")
472
- config_object = ApplicationConfig(**request_body)
473
- logger.debug("Successfully updated application config object")
474
- except Exception as e:
475
- logger.debug(f"Error updating ApplicationConfig: {e}")
476
- return {"error": f"Failed to update config object: {e!s}"}
477
-
478
- # Call the put_application_config method from the SDK
479
- logger.debug("Calling put_application_config with config object")
480
- result = api_client.put_application_config(
481
- id=id,
482
- application_config=config_object
483
- )
484
-
485
- # Convert the result to a dictionary
486
380
  if hasattr(result, 'to_dict'):
487
381
  result_dict = result.to_dict()
488
- else:
489
- # If it's already a dict or another format, use it as is
490
- result_dict = result or {
491
- "success": True,
492
- "message": "Update existing application config"
382
+ # Add helpful information about what was created
383
+ return {
384
+ **result_dict,
385
+ "message": f"Application perspective '{request_body.get('label')}' created successfully",
386
+ "applied_defaults": {
387
+ "scope": request_body.get('scope'),
388
+ "boundaryScope": request_body.get('boundaryScope'),
389
+ "accessRules": "READ_WRITE GLOBAL" if not payload or 'accessRules' not in (payload if isinstance(payload, dict) else {}) else "Custom"
390
+ }
493
391
  }
494
-
495
- logger.debug(f"Result from put_application_config: {result_dict}")
496
- return result_dict
392
+ return result or {"success": True, "message": "Application config created"}
497
393
  except Exception as e:
498
- logger.error(f"Error in put_application_config: {e}")
499
- return {"error": f"Failed to update existing application config: {e!s}"}
394
+ logger.error(f"Error in _add_application_config: {e}", exc_info=True)
395
+ return {"error": f"Failed to add application config: {e!s}"}
500
396
 
501
- @register_as_tool(
502
- title="Get All Endpoint Configs",
503
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
504
- )
505
397
  @with_header_auth(ApplicationSettingsApi)
506
- async def get_all_endpoint_configs(self,
507
- ctx=None,
508
- api_client=None) -> List[Dict[str, Any]]:
398
+ async def _get_application_config(self,
399
+ id: str,
400
+ ctx=None,
401
+ api_client=None) -> Dict[str, Any]:
509
402
  """
510
- All Endpoint Perspectives Configuration
511
- Get a list of all Endpoint Perspectives with their configuration settings.
403
+ Get an Application Perspective configuration by ID.
404
+
405
+ Note: To get by application name instead of ID, use the smart router tool
406
+ with application_name parameter. The router will automatically resolve
407
+ the name to ID and call this method.
408
+
512
409
  Args:
513
- ctx: The MCP context (optional)
410
+ id: Application perspective configuration ID
411
+ ctx: MCP context
412
+ api_client: API client instance
514
413
 
515
414
  Returns:
516
- Dictionary containing endpoints data or error information
415
+ Application configuration dictionary
517
416
  """
518
417
  try:
519
- debug_print("Fetching all endpoint configs")
520
- result = api_client.get_endpoint_configs()
521
- # Convert the result to a dictionary
522
- if hasattr(result, 'to_dict'):
523
- result_dict = result.to_dict()
524
- else:
525
- # If it's already a dict or another format, use it as is
526
- result_dict = result
527
-
528
- debug_print(f"Result from get_endpoint_configs: {result_dict}")
529
- return result_dict
418
+ if not id:
419
+ return {"error": "id is required"}
530
420
 
421
+ result = api_client.get_application_config(id=id)
422
+ if hasattr(result, 'to_dict'):
423
+ return result.to_dict()
424
+ return result
531
425
  except Exception as e:
532
- debug_print(f"Error in get_endpoint_configs: {e}")
533
- traceback.print_exc(file=sys.stderr)
534
- return [{"error": f"Failed to get endpoint configs: {e!s}"}]
426
+ logger.error(f"Error in _get_application_config: {e}", exc_info=True)
427
+ return {"error": f"Failed to get application config: {e!s}"}
535
428
 
536
- @register_as_tool(
537
- title="Create Endpoint Config",
538
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
539
- )
540
429
  @with_header_auth(ApplicationSettingsApi)
541
- async def create_endpoint_config(
542
- self,
543
- payload: Union[Dict[str, Any], str],
544
- ctx=None,
545
- api_client=None
546
- ) -> Dict[str, Any]:
547
- """
548
- Create or update endpoint configuration for a service.
549
-
550
- Sample Payload: {
551
- "serviceId": "d0cedae516f2182ede16f57f67476dd4c7dab9cd",
552
- "endpointCase": "LOWER",
553
- "endpointNameByFirstPathSegmentRuleEnabled": false,
554
- "endpointNameByCollectedPathTemplateRuleEnabled": false,
555
- "rules": null
556
- }
557
-
558
- Returns:
559
- Dict[str, Any]: Response from the create/update endpoint configuration API.
560
- """
430
+ async def _update_application_config(self,
431
+ id: str,
432
+ payload: Union[Dict[str, Any], str],
433
+ ctx=None,
434
+ api_client=None) -> Dict[str, Any]:
435
+ """Update an existing Application Perspective configuration."""
561
436
  try:
562
- if not payload:
563
- return {"error": "missing arguments"}
437
+ if not id or not payload:
438
+ return {"error": "id and payload are required"}
564
439
 
565
440
  # Parse the payload if it's a string
566
441
  if isinstance(payload, str):
567
- logger.debug("Payload is a string, attempting to parse")
442
+ import json
568
443
  try:
569
- import json
444
+ request_body = json.loads(payload)
445
+ except json.JSONDecodeError:
446
+ import ast
570
447
  try:
571
- parsed_payload = json.loads(payload)
572
- logger.debug("Successfully parsed payload as JSON")
573
- request_body = parsed_payload
574
- except json.JSONDecodeError as e:
575
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
576
-
577
- # Try replacing single quotes with double quotes
578
- fixed_payload = payload.replace("'", "\"")
579
- try:
580
- parsed_payload = json.loads(fixed_payload)
581
- logger.debug("Successfully parsed fixed JSON")
582
- request_body = parsed_payload
583
- except json.JSONDecodeError:
584
- # Try as Python literal
585
- import ast
586
- try:
587
- parsed_payload = ast.literal_eval(payload)
588
- logger.debug("Successfully parsed payload as Python literal")
589
- request_body = parsed_payload
590
- except (SyntaxError, ValueError) as e2:
591
- logger.debug(f"Failed to parse payload string: {e2}")
592
- return {"error": f"Invalid payload format: {e2}", "payload": payload}
593
- except Exception as e:
594
- logger.debug(f"Error parsing payload string: {e}")
595
- return {"error": f"Failed to parse payload: {e}", "payload": payload}
448
+ request_body = ast.literal_eval(payload)
449
+ except (SyntaxError, ValueError) as e:
450
+ return {"error": f"Invalid payload format: {e}"}
596
451
  else:
597
- # If payload is already a dictionary, use it directly
598
- logger.debug("Using provided payload dictionary")
599
452
  request_body = payload
600
453
 
601
- # Import the EndpointConfig class
602
- try:
603
- from instana_client.models.endpoint_config import (
604
- EndpointConfig,
605
- )
606
- logger.debug("Successfully imported EndpointConfig")
607
- except ImportError as e:
608
- logger.debug(f"Error importing EndpointConfig: {e}")
609
- return {"error": f"Failed to import EndpointConfig: {e!s}"}
610
-
611
- # Create an EndpointConfig object from the request body
612
- try:
613
- logger.debug(f"Creating EndpointConfig with params: {request_body}")
614
- config_object = EndpointConfig(**request_body)
615
- logger.debug("Successfully created endpoint config object")
616
- except Exception as e:
617
- logger.debug(f"Error creating EndpointConfig: {e}")
618
- return {"error": f"Failed to create config object: {e!s}"}
619
-
620
- # Call the create_endpoint_config method from the SDK
621
- logger.debug("Calling create_endpoint_config with config object")
622
- result = api_client.create_endpoint_config(
623
- endpoint_config=config_object
624
- )
625
- # Convert the result to a dictionary
626
- if hasattr(result, 'to_dict'):
627
- result_dict = result.to_dict()
628
- else:
629
- # If it's already a dict or another format, use it as is
630
- result_dict = result or {
631
- "success": True,
632
- "message": "Create new endpoint config"
633
- }
454
+ # Convert nested tagFilterExpression to model objects if present
455
+ if 'tagFilterExpression' in request_body and isinstance(request_body['tagFilterExpression'], dict):
456
+ tag_expr = request_body['tagFilterExpression']
457
+
458
+ # Handle EXPRESSION type with nested elements
459
+ if tag_expr.get('type') == 'EXPRESSION' and 'elements' in tag_expr:
460
+ converted_elements = []
461
+ for element in tag_expr['elements']:
462
+ if isinstance(element, dict):
463
+ element_copy = element.copy()
464
+ element_copy.pop('value', None)
465
+ element_copy.pop('key', None)
466
+ converted_elements.append(TagFilter(**element_copy))
467
+ else:
468
+ converted_elements.append(element)
469
+ tag_expr['elements'] = converted_elements
470
+ request_body['tagFilterExpression'] = TagFilterExpression(**tag_expr)
471
+
472
+ # Handle TAG_FILTER type (simple filter)
473
+ elif tag_expr.get('type') == 'TAG_FILTER':
474
+ # For TAG_FILTER, ensure both 'value' and 'stringValue' are present
475
+ # Don't convert to TagFilter model - keep as dict to preserve both fields
476
+ tag_filter_copy = tag_expr.copy()
477
+ tag_filter_copy.pop('key', None)
478
+ if 'stringValue' not in tag_filter_copy and 'value' in tag_expr:
479
+ tag_filter_copy['stringValue'] = tag_expr['value']
480
+ if 'value' not in tag_filter_copy and 'stringValue' in tag_expr:
481
+ tag_filter_copy['value'] = tag_expr['stringValue']
482
+ # Keep as dictionary - don't convert to TagFilter model
483
+ request_body['tagFilterExpression'] = tag_filter_copy
484
+
485
+ config_object = ApplicationConfig(**request_body)
486
+ result = api_client.put_application_config(id=id, application_config=config_object)
634
487
 
635
- logger.debug(f"Result from create_endpoint_config: {result_dict}")
636
- return result_dict
488
+ if hasattr(result, 'to_dict'):
489
+ return result.to_dict()
490
+ return result or {"success": True, "message": f"Application config '{id}' updated"}
637
491
  except Exception as e:
638
- logger.error(f"Error in create_endpoint_config: {e}")
639
- return {"error": f"Failed to create new endpoint config: {e!s}"}
492
+ logger.error(f"Error in _update_application_config: {e}", exc_info=True)
493
+ return {"error": f"Failed to update application config: {e!s}"}
640
494
 
641
- @register_as_tool(
642
- title="Delete Endpoint Config",
643
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True)
644
- )
645
495
  @with_header_auth(ApplicationSettingsApi)
646
- async def delete_endpoint_config(
647
- self,
648
- id: str,
649
- ctx=None,
650
- api_client=None
651
- ) -> Dict[str, Any]:
652
- """
653
- Delete an endpoint configuration of a service.
654
-
655
- Args:
656
- id: An Instana generated unique identifier for a Service.
657
- ctx: The MCP context (optional)
658
-
659
- Returns:
660
- Dict[str, Any]: Response from the delete endpoint configuration API.
661
- """
496
+ async def _delete_application_config(self,
497
+ id: str,
498
+ ctx=None,
499
+ api_client=None) -> Dict[str, Any]:
500
+ """Delete an Application Perspective configuration."""
662
501
  try:
663
- debug_print("Delete endpoint configs")
664
502
  if not id:
665
- return {"error": "Required enitities are missing or invalid"}
666
-
667
- api_client.delete_endpoint_config(id=id)
668
-
669
- result_dict = {
670
- "success": True,
671
- "message": f"Endpoint Confiuguration '{id}' has been successfully deleted"
672
- }
673
-
674
- debug_print(f"Successfully deleted endpoint perspective with ID: {id}")
675
- return result_dict
503
+ return {"error": "id is required"}
676
504
 
505
+ api_client.delete_application_config(id=id)
506
+ return {"success": True, "message": f"Application config '{id}' deleted successfully"}
677
507
  except Exception as e:
678
- debug_print(f"Error in delete_endpoint_config: {e}")
679
- traceback.print_exc(file=sys.stderr)
680
- return {"error": f"Failed to delete endpoint configs: {e!s}"}
508
+ logger.error(f"Error in _delete_application_config: {e}", exc_info=True)
509
+ return {"error": f"Failed to delete application config: {e!s}"}
681
510
 
682
- @register_as_tool(
683
- title="Get Endpoint Config",
684
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
685
- )
511
+ # Endpoint Config Operations
686
512
  @with_header_auth(ApplicationSettingsApi)
687
- async def get_endpoint_config(
688
- self,
689
- id: str,
690
- ctx=None,
691
- api_client=None
692
- ) -> Dict[str, Any]:
693
- """
694
- This MCP tool is used for endpoint if one wants to retrieve the endpoint configuration of a service.
695
- Args:
696
- id: An Instana generated unique identifier for a Service.
697
- ctx: The MCP context (optional)
698
-
699
- Returns:
700
- Dict[str, Any]: Response from the create/update endpoint configuration API.
513
+ async def _get_all_endpoint_configs(self,
514
+ ctx=None,
515
+ api_client=None) -> List[Dict[str, Any]]:
516
+ """Get all Endpoint Perspectives Configuration."""
517
+ try:
518
+ result = api_client.get_endpoint_configs_without_preload_content()
519
+ import json
520
+ response_text = result.data.decode('utf-8')
521
+ json_data = json.loads(response_text)
522
+ return json_data if isinstance(json_data, list) else [json_data] if json_data else []
523
+ except Exception as e:
524
+ logger.error(f"Error in _get_all_endpoint_configs: {e}", exc_info=True)
525
+ return [{"error": f"Failed to get endpoint configs: {e!s}"}]
701
526
 
702
- """
527
+ @with_header_auth(ApplicationSettingsApi)
528
+ async def _get_endpoint_config(self,
529
+ id: str,
530
+ ctx=None,
531
+ api_client=None) -> Dict[str, Any]:
532
+ """Get an Endpoint configuration by ID."""
703
533
  try:
704
- debug_print("get endpoint config")
705
534
  if not id:
706
- return {"error": "Required enitities are missing or invalid"}
707
-
708
- result = api_client.get_endpoint_config(
709
- id=id
710
- )
711
- # Convert the result to a dictionary
535
+ return {"error": "id is required"}
536
+ result = api_client.get_endpoint_config(id=id)
712
537
  if hasattr(result, 'to_dict'):
713
- result_dict = result.to_dict()
714
- else:
715
- # If it's already a dict or another format, use it as is
716
- result_dict = result
717
-
718
- debug_print(f"Result from get_endpoint_configs: {result_dict}")
719
- return result_dict
538
+ return result.to_dict()
539
+ return result
720
540
  except Exception as e:
721
- debug_print(f"Error in get_endpoint_configs: {e}")
722
- traceback.print_exc(file=sys.stderr)
723
- return {"error": f"Failed to get endpoint configs: {e!s}"}
541
+ logger.error(f"Error in _get_endpoint_config: {e}", exc_info=True)
542
+ return {"error": f"Failed to get endpoint config: {e!s}"}
724
543
 
725
- @register_as_tool(
726
- title="Update Endpoint Config",
727
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
728
- )
729
544
  @with_header_auth(ApplicationSettingsApi)
730
- async def update_endpoint_config(
731
- self,
732
- id: str,
733
- payload: Union[Dict[str, Any], str],
734
- ctx=None,
735
- api_client=None
736
- ) -> Dict[str, Any]:
737
- """
738
- update endpoint configuration for a service.
739
-
740
- Args:
741
- id: An Instana generated unique identifier for a Service.
742
- {
743
- "serviceId": "20ba31821b079e7d845a08096124880db3eeeb40",
744
- "endpointNameByCollectedPathTemplateRuleEnabled": true,
745
- "endpointNameByFirstPathSegmentRuleEnabled": true,
746
- "rules": [
747
- {
748
- "enabled": true,
749
- "pathSegments": [
750
- {
751
- "name": "api",
752
- "type": "FIXED"
753
- },
754
- {
755
- "name": "version",
756
- "type": "PARAMETER"
757
- }
758
- ],
759
- "testCases": [
760
- "/api/v2/users"
761
- ]
762
- }
763
- ],
764
- "endpointCase": "UPPER"
765
- }
766
- ctx: The MCP context (optional)
767
-
768
- Returns:
769
- Dict[str, Any]: Response from the create/update endpoint configuration API.
770
- """
545
+ async def _create_endpoint_config(self,
546
+ payload: Union[Dict[str, Any], str],
547
+ ctx=None,
548
+ api_client=None) -> Dict[str, Any]:
549
+ """Create or update endpoint configuration for a service."""
771
550
  try:
772
- if not payload or not id:
773
- return {"error": "missing arguments"}
551
+ if not payload:
552
+ return {"error": "payload is required"}
774
553
 
775
- # Parse the payload if it's a string
776
554
  if isinstance(payload, str):
777
- logger.debug("Payload is a string, attempting to parse")
555
+ import json
778
556
  try:
779
- import json
780
- try:
781
- parsed_payload = json.loads(payload)
782
- logger.debug("Successfully parsed payload as JSON")
783
- request_body = parsed_payload
784
- except json.JSONDecodeError as e:
785
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
786
-
787
- # Try replacing single quotes with double quotes
788
- fixed_payload = payload.replace("'", "\"")
789
- try:
790
- parsed_payload = json.loads(fixed_payload)
791
- logger.debug("Successfully parsed fixed JSON")
792
- request_body = parsed_payload
793
- except json.JSONDecodeError:
794
- # Try as Python literal
795
- import ast
796
- try:
797
- parsed_payload = ast.literal_eval(payload)
798
- logger.debug("Successfully parsed payload as Python literal")
799
- request_body = parsed_payload
800
- except (SyntaxError, ValueError) as e2:
801
- logger.debug(f"Failed to parse payload string: {e2}")
802
- return {"error": f"Invalid payload format: {e2}", "payload": payload}
803
- except Exception as e:
804
- logger.debug(f"Error parsing payload string: {e}")
805
- return {"error": f"Failed to parse payload: {e}", "payload": payload}
557
+ request_body = json.loads(payload)
558
+ except json.JSONDecodeError:
559
+ import ast
560
+ request_body = ast.literal_eval(payload)
806
561
  else:
807
- # If payload is already a dictionary, use it directly
808
- logger.debug("Using provided payload dictionary")
809
562
  request_body = payload
810
563
 
811
- # Import the EndpointConfig class
812
- try:
813
- from instana_client.models.endpoint_config import (
814
- EndpointConfig,
815
- )
816
- logger.debug("Successfully imported EndpointConfig")
817
- except ImportError as e:
818
- logger.debug(f"Error importing EndpointConfig: {e}")
819
- return {"error": f"Failed to import EndpointConfig: {e!s}"}
820
-
821
- # Create an EndpointConfig object from the request body
822
- try:
823
- logger.debug(f"Creating EndpointConfig with params: {request_body}")
824
- config_object = EndpointConfig(**request_body)
825
- logger.debug("Successfully updated endpoint config object")
826
- except Exception as e:
827
- logger.debug(f"Error updating EndpointConfig: {e}")
828
- return {"error": f"Failed to update config object: {e!s}"}
829
-
830
- # Call the update_endpoint_config method from the SDK
831
- logger.debug("Calling update_endpoint_config with config object")
832
- result = api_client.update_endpoint_config(
833
- id=id,
834
- endpoint_config=config_object
835
- )
836
-
837
- # Convert the result to a dictionary
564
+ config_object = EndpointConfig(**request_body)
565
+ result = api_client.create_endpoint_config(endpoint_config=config_object)
838
566
  if hasattr(result, 'to_dict'):
839
- result_dict = result.to_dict()
840
- else:
841
- # If it's already a dict or another format, use it as is
842
- result_dict = result or {
843
- "success": True,
844
- "message": "update existing endpoint config"
845
- }
846
-
847
- logger.debug(f"Result from update_endpoint_config: {result_dict}")
848
- return result_dict
567
+ return result.to_dict()
568
+ return result or {"success": True, "message": "Endpoint config created"}
849
569
  except Exception as e:
850
- logger.error(f"Error in update_endpoint_config: {e}")
851
- return {"error": f"Failed to update existing application config: {e!s}"}
852
-
570
+ logger.error(f"Error in _create_endpoint_config: {e}", exc_info=True)
571
+ return {"error": f"Failed to create endpoint config: {e!s}"}
853
572
 
854
- @register_as_tool(
855
- title="Get All Manual Service Configs",
856
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
857
- )
858
573
  @with_header_auth(ApplicationSettingsApi)
859
- async def get_all_manual_service_configs(self,
860
- ctx=None,
861
- api_client=None) -> List[Dict[str, Any]]:
862
- """
863
- All Manual Service Perspectives Configuration
864
- Get a list of all Manual Service Perspectives with their configuration settings.
865
- Args:
866
- ctx: The MCP context (optional)
867
-
868
- Returns:
869
- Dictionary containing endpoints data or error information
870
- """
574
+ async def _update_endpoint_config(self,
575
+ id: str,
576
+ payload: Union[Dict[str, Any], str],
577
+ ctx=None,
578
+ api_client=None) -> Dict[str, Any]:
579
+ """Update an endpoint configuration."""
871
580
  try:
872
- debug_print("Fetching all manual configs")
873
- result = api_client.get_all_manual_service_configs()
874
- # Convert the result to a dictionary
875
- if hasattr(result, 'to_dict'):
876
- result_dict = result.to_dict()
877
- else:
878
- # If it's already a dict or another format, use it as is
879
- result_dict = result
581
+ if not id or not payload:
582
+ return {"error": "id and payload are required"}
880
583
 
881
- debug_print(f"Result from get_all_manual_service_configs: {result_dict}")
882
- return result_dict
883
-
884
- except Exception as e:
885
- debug_print(f"Error in get_all_manual_service_configs: {e}")
886
- traceback.print_exc(file=sys.stderr)
887
- return [{"error": f"Failed to get manual service configs: {e!s}"}]
888
-
889
- @register_as_tool(
890
- title="Add Manual Service Config",
891
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
892
- )
893
- @with_header_auth(ApplicationSettingsApi)
894
- async def add_manual_service_config(
895
- self,
896
- payload: Union[Dict[str, Any], str],
897
- ctx=None,
898
- api_client=None
899
- ) -> Dict[str, Any]:
900
- """
901
- Create a manual service mapping configuration.
902
-
903
- Requires `CanConfigureServiceMapping` permission on the API token.
904
-
905
- Sample payload:
906
- {
907
- "description": "Map source service example",
908
- "enabled": true,
909
- "existingServiceId": "c467ca0fa21477fee3cde75a140b2963307388a7",
910
- "tagFilterExpression": {
911
- "type": "TAG_FILTER",
912
- "name": "service.name",
913
- "stringValue": "front",
914
- "numberValue": null,
915
- "booleanValue": null,
916
- "key": null,
917
- "value": "front",
918
- "operator": "EQUALS",
919
- "entity": "SOURCE"
920
- },
921
- "unmonitoredServiceName": null
922
- }
923
- ctx: Optional execution context.
924
-
925
- Returns:
926
- Dict[str, Any]: API response indicating success or failure.
927
- """
928
- try:
929
- if not payload:
930
- return {"error": "missing arguments"}
931
-
932
- # Parse the payload if it's a string
933
584
  if isinstance(payload, str):
934
- logger.debug("Payload is a string, attempting to parse")
585
+ import json
935
586
  try:
936
- import json
937
- try:
938
- parsed_payload = json.loads(payload)
939
- logger.debug("Successfully parsed payload as JSON")
940
- request_body = parsed_payload
941
- except json.JSONDecodeError as e:
942
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
943
-
944
- # Try replacing single quotes with double quotes
945
- fixed_payload = payload.replace("'", "\"")
946
- try:
947
- parsed_payload = json.loads(fixed_payload)
948
- logger.debug("Successfully parsed fixed JSON")
949
- request_body = parsed_payload
950
- except json.JSONDecodeError:
951
- # Try as Python literal
952
- import ast
953
- try:
954
- parsed_payload = ast.literal_eval(payload)
955
- logger.debug("Successfully parsed payload as Python literal")
956
- request_body = parsed_payload
957
- except (SyntaxError, ValueError) as e2:
958
- logger.debug(f"Failed to parse payload string: {e2}")
959
- return {"error": f"Invalid payload format: {e2}", "payload": payload}
960
- except Exception as e:
961
- logger.debug(f"Error parsing payload string: {e}")
962
- return {"error": f"Failed to parse payload: {e}", "payload": payload}
587
+ request_body = json.loads(payload)
588
+ except json.JSONDecodeError:
589
+ import ast
590
+ request_body = ast.literal_eval(payload)
963
591
  else:
964
- # If payload is already a dictionary, use it directly
965
- logger.debug("Using provided payload dictionary")
966
592
  request_body = payload
967
593
 
968
- # Import the NewManualServiceConfig class
969
- try:
970
- from instana_client.models.new_manual_service_config import (
971
- NewManualServiceConfig,
972
- )
973
- logger.debug("Successfully imported NewManualServiceConfig")
974
- except ImportError as e:
975
- logger.debug(f"Error importing NewManualServiceConfig: {e}")
976
- return {"error": f"Failed to import NewManualServiceConfig: {e!s}"}
977
-
978
- # Create an NewManualServiceConfig object from the request body
979
- try:
980
- logger.debug(f"Creating NewManualServiceConfig with params: {request_body}")
981
- config_object = NewManualServiceConfig(**request_body)
982
- logger.debug("Successfully created manual service config object")
983
- except Exception as e:
984
- logger.debug(f"Error creating NewManualServiceConfig: {e}")
985
- return {"error": f"Failed to create config object: {e!s}"}
986
-
987
- # Call the add_manual_service_config method from the SDK
988
- logger.debug("Calling add_manual_service_config with config object")
989
- result = api_client.add_manual_service_config(
990
- new_manual_service_config=config_object
991
- )
992
-
993
- # Convert the result to a dictionary
594
+ config_object = EndpointConfig(**request_body)
595
+ result = api_client.update_endpoint_config(id=id, endpoint_config=config_object)
994
596
  if hasattr(result, 'to_dict'):
995
- result_dict = result.to_dict()
996
- else:
997
- # If it's already a dict or another format, use it as is
998
- result_dict = result or {
999
- "success": True,
1000
- "message": "Create new manual service config"
1001
- }
1002
-
1003
- logger.debug(f"Result from add_manual_service_config: {result_dict}")
1004
- return result_dict
597
+ return result.to_dict()
598
+ return result or {"success": True, "message": f"Endpoint config '{id}' updated"}
1005
599
  except Exception as e:
1006
- logger.error(f"Error in add_manual_service_config: {e}")
1007
- return {"error": f"Failed to create new manual service config: {e!s}"}
600
+ logger.error(f"Error in _update_endpoint_config: {e}", exc_info=True)
601
+ return {"error": f"Failed to update endpoint config: {e!s}"}
1008
602
 
1009
- @register_as_tool(
1010
- title="Delete Manual Service Config",
1011
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True)
1012
- )
1013
603
  @with_header_auth(ApplicationSettingsApi)
1014
- async def delete_manual_service_config(
1015
- self,
1016
- id: str,
1017
- ctx=None,
1018
- api_client=None
1019
- ) -> Dict[str, Any]:
1020
- """
1021
- Delete a manual service configuration.
1022
-
1023
- Args:
1024
- id: A unique id of the manual service configuration.
1025
- ctx: The MCP context (optional)
1026
-
1027
- Returns:
1028
- Dict[str, Any]: Response from the delete manual service configuration API.
1029
- """
604
+ async def _delete_endpoint_config(self,
605
+ id: str,
606
+ ctx=None,
607
+ api_client=None) -> Dict[str, Any]:
608
+ """Delete an endpoint configuration."""
1030
609
  try:
1031
- debug_print("Delete manual service configs")
1032
610
  if not id:
1033
- return {"error": "Required enitities are missing or invalid"}
1034
-
1035
- api_client.delete_manual_service_config(id=id)
1036
-
1037
- result_dict = {
1038
- "success": True,
1039
- "message": f"Manual Service Confiuguration '{id}' has been successfully deleted"
1040
- }
1041
-
1042
- debug_print(f"Successfully deleted manual service config perspective with ID: {id}")
1043
- return result_dict
1044
-
611
+ return {"error": "id is required"}
612
+ api_client.delete_endpoint_config(id=id)
613
+ return {"success": True, "message": f"Endpoint config '{id}' deleted successfully"}
1045
614
  except Exception as e:
1046
- debug_print(f"Error in delete_manual_service_config: {e}")
1047
- traceback.print_exc(file=sys.stderr)
1048
- return {"error": f"Failed to delete manual service configs: {e!s}"}
615
+ logger.error(f"Error in _delete_endpoint_config: {e}", exc_info=True)
616
+ return {"error": f"Failed to delete endpoint config: {e!s}"}
1049
617
 
1050
- @register_as_tool(
1051
- title="Update Manual Service Config",
1052
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
1053
- )
618
+ # Service Config Operations
1054
619
  @with_header_auth(ApplicationSettingsApi)
1055
- async def update_manual_service_config(
1056
- self,
1057
- id: str,
1058
- payload: Union[Dict[str, Any], str],
1059
- ctx=None,
1060
- api_client=None
1061
- ) -> Dict[str, Any]:
1062
- """
1063
- The manual service configuration APIs enables mapping calls to services using tag filter expressions based on call tags.
1064
-
1065
- There are two use cases on the usage of these APIs:
1066
-
1067
- Map to an Unmonitored Service with a Custom Name. For example, Map HTTP calls to different Google domains (www.ibm.com, www.ibm.fr) into a single service named IBM using the call.http.host tag.
1068
- Link Calls to an Existing Monitored Service. For example, Link database calls (jdbc:mysql://10.128.0.1:3306) to an existing service like MySQL@3306 on demo-host by referencing its service ID.
620
+ async def _get_all_service_configs(self,
621
+ ctx=None,
622
+ api_client=None) -> List[Dict[str, Any]]:
623
+ """Get all Service configurations."""
624
+ try:
625
+ result = api_client.get_service_configs_without_preload_content()
626
+ import json
627
+ response_text = result.data.decode('utf-8')
628
+ json_data = json.loads(response_text)
629
+ return json_data if isinstance(json_data, list) else [json_data] if json_data else []
630
+ except Exception as e:
631
+ logger.error(f"Error in _get_all_service_configs: {e}", exc_info=True)
632
+ return [{"error": f"Failed to get service configs: {e!s}"}]
1069
633
 
1070
- Args:
1071
- id: A unique id of the manual service configuration.
1072
- Sample payload: {
1073
- "description": "Map source service example",
1074
- "enabled": true,
1075
- "existingServiceId": "c467ca0fa21477fee3cde75a140b2963307388a7",
1076
- "id": "BDGeDcG4TRSzRkJ1mGOk-Q",
1077
- "tagFilterExpression": {
1078
- "type": "TAG_FILTER",
1079
- "name": "service.name",
1080
- "stringValue": "front",
1081
- "numberValue": null,
1082
- "booleanValue": null,
1083
- "key": null,
1084
- "value": "front",
1085
- "operator": "EQUALS",
1086
- "entity": "SOURCE"
1087
- },
1088
- "unmonitoredServiceName": null
1089
- }
634
+ @with_header_auth(ApplicationSettingsApi)
635
+ async def _get_service_config(self,
636
+ id: str,
637
+ ctx=None,
638
+ api_client=None) -> Dict[str, Any]:
639
+ """Get a Service configuration by ID."""
640
+ try:
641
+ if not id:
642
+ return {"error": "id is required"}
643
+ result = api_client.get_service_config(id=id)
644
+ if hasattr(result, 'to_dict'):
645
+ return result.to_dict()
646
+ return result
647
+ except Exception as e:
648
+ logger.error(f"Error in _get_service_config: {e}", exc_info=True)
649
+ return {"error": f"Failed to get service config: {e!s}"}
1090
650
 
1091
- Returns:
1092
- Dict[str, Any]: API response indicating success or failure.
1093
- """
651
+ @with_header_auth(ApplicationSettingsApi)
652
+ async def _add_service_config(self,
653
+ payload: Union[Dict[str, Any], str],
654
+ ctx=None,
655
+ api_client=None) -> Dict[str, Any]:
656
+ """Add a new Service configuration."""
1094
657
  try:
1095
- if not payload or not id:
1096
- return {"error": "missing arguments"}
658
+ if not payload:
659
+ return {"error": "payload is required"}
1097
660
 
1098
- # Parse the payload if it's a string
1099
661
  if isinstance(payload, str):
1100
- logger.debug("Payload is a string, attempting to parse")
662
+ import json
1101
663
  try:
1102
- import json
1103
- try:
1104
- parsed_payload = json.loads(payload)
1105
- logger.debug("Successfully parsed payload as JSON")
1106
- request_body = parsed_payload
1107
- except json.JSONDecodeError as e:
1108
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
1109
-
1110
- # Try replacing single quotes with double quotes
1111
- fixed_payload = payload.replace("'", "\"")
1112
- try:
1113
- parsed_payload = json.loads(fixed_payload)
1114
- logger.debug("Successfully parsed fixed JSON")
1115
- request_body = parsed_payload
1116
- except json.JSONDecodeError:
1117
- # Try as Python literal
1118
- import ast
1119
- try:
1120
- parsed_payload = ast.literal_eval(payload)
1121
- logger.debug("Successfully parsed payload as Python literal")
1122
- request_body = parsed_payload
1123
- except (SyntaxError, ValueError) as e2:
1124
- logger.debug(f"Failed to parse payload string: {e2}")
1125
- return {"error": f"Invalid payload format: {e2}", "payload": payload}
1126
- except Exception as e:
1127
- logger.debug(f"Error parsing payload string: {e}")
1128
- return {"error": f"Failed to parse payload: {e}", "payload": payload}
664
+ request_body = json.loads(payload)
665
+ except json.JSONDecodeError:
666
+ import ast
667
+ request_body = ast.literal_eval(payload)
1129
668
  else:
1130
- # If payload is already a dictionary, use it directly
1131
- logger.debug("Using provided payload dictionary")
1132
669
  request_body = payload
1133
670
 
1134
- # Import the ManualServiceConfig class
1135
- try:
1136
- from instana_client.models.manual_service_config import (
1137
- ManualServiceConfig,
1138
- )
1139
- logger.debug("Successfully imported ManualServiceConfig")
1140
- except ImportError as e:
1141
- logger.debug(f"Error importing ManualServiceConfig: {e}")
1142
- return {"error": f"Failed to import ManualServiceConfig: {e!s}"}
1143
-
1144
- # Create an ManualServiceConfig object from the request body
1145
- try:
1146
- logger.debug(f"Creating ManualServiceConfig with params: {request_body}")
1147
- config_object = ManualServiceConfig(**request_body)
1148
- logger.debug("Successfully update manual service config object")
1149
- except Exception as e:
1150
- logger.debug(f"Error creating ManualServiceConfig: {e}")
1151
- return {"error": f"Failed to update manual config object: {e!s}"}
1152
-
1153
- # Call the update_manual_service_config method from the SDK
1154
- logger.debug("Calling update_manual_service_config with config object")
1155
- result = api_client.update_manual_service_config(
1156
- id=id,
1157
- manual_service_config=config_object
1158
- )
1159
-
1160
- # Convert the result to a dictionary
671
+ config_object = ServiceConfig(**request_body)
672
+ result = api_client.add_service_config(service_config=config_object)
1161
673
  if hasattr(result, 'to_dict'):
1162
- result_dict = result.to_dict()
1163
- else:
1164
- # If it's already a dict or another format, use it as is
1165
- result_dict = result or {
1166
- "success": True,
1167
- "message": "update manual service config"
1168
- }
1169
-
1170
- logger.debug(f"Result from update_manual_service_config: {result_dict}")
1171
- return result_dict
674
+ return result.to_dict()
675
+ return result or {"success": True, "message": "Service config created"}
1172
676
  except Exception as e:
1173
- logger.error(f"Error in update_manual_service_config: {e}")
1174
- return {"error": f"Failed to update manual config: {e!s}"}
1175
-
677
+ logger.error(f"Error in _add_service_config: {e}", exc_info=True)
678
+ return {"error": f"Failed to add service config: {e!s}"}
1176
679
 
1177
- @register_as_tool(
1178
- title="Replace All Manual Service Config",
1179
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True)
1180
- )
1181
680
  @with_header_auth(ApplicationSettingsApi)
1182
- async def replace_all_manual_service_config(
1183
- self,
1184
- payload: Union[Dict[str, Any], str],
1185
- ctx=None,
1186
- api_client=None
1187
- ) -> List[Dict[str, Any]]:
1188
- """
1189
- This tool is used if one wants to update more than 1 manual service configurations.
1190
-
1191
- There are two use cases on the usage of these APIs:
1192
-
1193
- Map to an Unmonitored Service with a Custom Name. For example, Map HTTP calls to different Google domains (www.ibm.com, www.ibm.fr) into a single service named IBM using the call.http.host tag.
1194
- Link Calls to an Existing Monitored Service. For example, Link database calls (jdbc:mysql://10.128.0.1:3306) to an existing service like MySQL@3306 on demo-host by referencing its service ID.
1195
-
1196
- Args:
1197
- Sample payload: [
1198
- {
1199
- "description": "Map source service",
1200
- "enabled": true,
1201
- "existingServiceId": "c467ca0fa21477fee3cde75a140b2963307388a7",
1202
- "tagFilterExpression": {
1203
- "type": "TAG_FILTER",
1204
- "name": "service.name",
1205
- "stringValue": "front",
1206
- "numberValue": null,
1207
- "booleanValue": null,
1208
- "key": null,
1209
- "value": "front",
1210
- "operator": "EQUALS",
1211
- "entity": "SOURCE"
1212
- },
1213
- "unmonitoredServiceName": null
1214
- }
1215
- ]
1216
- ctx: Optional execution context.
1217
-
1218
- Returns:
1219
- Dict[str, Any]: API response indicating success or failure.
1220
- """
681
+ async def _update_service_config(self,
682
+ id: str,
683
+ payload: Union[Dict[str, Any], str],
684
+ ctx=None,
685
+ api_client=None) -> Dict[str, Any]:
686
+ """Update a Service configuration."""
1221
687
  try:
1222
- if not payload:
1223
- return [{"error": "missing arguments"}]
688
+ if not id or not payload:
689
+ return {"error": "id and payload are required"}
1224
690
 
1225
- # Parse the payload if it's a string
1226
691
  if isinstance(payload, str):
1227
- logger.debug("Payload is a string, attempting to parse")
692
+ import json
1228
693
  try:
1229
- import json
1230
- try:
1231
- parsed_payload = json.loads(payload)
1232
- logger.debug("Successfully parsed payload as JSON")
1233
- request_body = parsed_payload
1234
- except json.JSONDecodeError as e:
1235
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
1236
-
1237
- # Try replacing single quotes with double quotes
1238
- fixed_payload = payload.replace("'", "\"")
1239
- try:
1240
- parsed_payload = json.loads(fixed_payload)
1241
- logger.debug("Successfully parsed fixed JSON")
1242
- request_body = parsed_payload
1243
- except json.JSONDecodeError:
1244
- # Try as Python literal
1245
- import ast
1246
- try:
1247
- parsed_payload = ast.literal_eval(payload)
1248
- logger.debug("Successfully parsed payload as Python literal")
1249
- request_body = parsed_payload
1250
- except (SyntaxError, ValueError) as e2:
1251
- logger.debug(f"Failed to parse payload string: {e2}")
1252
- return [{"error": f"Invalid payload format: {e2}", "payload": payload}]
1253
- except Exception as e:
1254
- logger.debug(f"Error parsing payload string: {e}")
1255
- return [{"error": f"Failed to parse payload: {e}", "payload": payload}]
694
+ request_body = json.loads(payload)
695
+ except json.JSONDecodeError:
696
+ import ast
697
+ request_body = ast.literal_eval(payload)
1256
698
  else:
1257
- # If payload is already a dictionary, use it directly
1258
- logger.debug("Using provided payload dictionary")
1259
699
  request_body = payload
1260
700
 
1261
- # Import the NewManualServiceConfig class
1262
- try:
1263
- from instana_client.models.new_manual_service_config import (
1264
- NewManualServiceConfig,
1265
- )
1266
- logger.debug("Successfully imported ManualServiceConfig")
1267
- except ImportError as e:
1268
- logger.debug(f"Error importing ManualServiceConfig: {e}")
1269
- return [{"error": f"Failed to import ManualServiceConfig: {e!s}"}]
1270
-
1271
- # Create an ManualServiceConfig object from the request body
1272
- try:
1273
- logger.debug(f"Creating ManualServiceConfig with params: {request_body}")
1274
- config_object = [NewManualServiceConfig(**request_body)]
1275
- logger.debug("Successfully replace all manual service config object")
1276
- except Exception as e:
1277
- logger.debug(f"Error creating ManualServiceConfig: {e}")
1278
- return [{"error": f"Failed to replace all manual config object: {e!s}"}]
1279
-
1280
- # Call the replace_all_manual_service_config method from the SDK
1281
- logger.debug("Calling replace_all_manual_service_config with config object")
1282
- result = api_client.replace_all_manual_service_config(
1283
- new_manual_service_config=config_object
1284
- )
1285
-
1286
- # Convert the result to a dictionary
701
+ config_object = ServiceConfig(**request_body)
702
+ result = api_client.update_service_config(id=id, service_config=config_object)
1287
703
  if hasattr(result, 'to_dict'):
1288
- result_dict = result.to_dict()
1289
- else:
1290
- # If it's already a dict or another format, use it as is
1291
- result_dict = result or {
1292
- "success": True,
1293
- "message": "Create replace all manual service config"
1294
- }
1295
-
1296
- logger.debug(f"Result from replace_all_manual_service_config: {result_dict}")
1297
- return [result_dict]
704
+ return result.to_dict()
705
+ return result or {"success": True, "message": f"Service config '{id}' updated"}
1298
706
  except Exception as e:
1299
- logger.error(f"Error in replace_all_manual_service_config: {e}")
1300
- return [{"error": f"Failed to replace all manual config: {e!s}"}]
707
+ logger.error(f"Error in _update_service_config: {e}", exc_info=True)
708
+ return {"error": f"Failed to update service config: {e!s}"}
1301
709
 
1302
- @register_as_tool(
1303
- title="Get All Service Configs",
1304
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
1305
- )
1306
710
  @with_header_auth(ApplicationSettingsApi)
1307
- async def get_all_service_configs(self,
1308
- ctx=None,
1309
- api_client=None) -> List[Dict[str, Any]]:
1310
- """
1311
- This tool gives list of All Service Perspectives Configuration
1312
- Get a list of all Service Perspectives with their configuration settings.
1313
- Args:
1314
- ctx: The MCP context (optional)
1315
-
1316
- Returns:
1317
- Dictionary containing endpoints data or error information
1318
- """
711
+ async def _delete_service_config(self,
712
+ id: str,
713
+ ctx=None,
714
+ api_client=None) -> Dict[str, Any]:
715
+ """Delete a Service configuration."""
1319
716
  try:
1320
- debug_print("Fetching all service configs")
1321
- result = api_client.get_service_configs()
1322
- # Convert the result to a dictionary
1323
- if hasattr(result, 'to_dict'):
1324
- result_dict = result.to_dict()
1325
- else:
1326
- # If it's already a dict or another format, use it as is
1327
- result_dict = result
1328
-
1329
- debug_print(f"Result from get_service_configs: {result_dict}")
1330
- return result_dict
1331
-
717
+ if not id:
718
+ return {"error": "id is required"}
719
+ api_client.delete_service_config(id=id)
720
+ return {"success": True, "message": f"Service config '{id}' deleted successfully"}
1332
721
  except Exception as e:
1333
- debug_print(f"Error in get_all_service_configs: {e}")
1334
- traceback.print_exc(file=sys.stderr)
1335
- return [{"error": f"Failed to get application data metrics: {e}"}]
722
+ logger.error(f"Error in _delete_service_config: {e}", exc_info=True)
723
+ return {"error": f"Failed to delete service config: {e!s}"}
1336
724
 
1337
- @register_as_tool(
1338
- title="Add Service Config",
1339
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
1340
- )
1341
725
  @with_header_auth(ApplicationSettingsApi)
1342
- async def add_service_config(self,
1343
- payload: Union[Dict[str, Any], str],
1344
- ctx=None,
1345
- api_client=None) -> Dict[str, Any]:
1346
- """
1347
- This tool gives is used to add new Service Perspectives Configuration
1348
- Get a list of all Service Perspectives with their configuration settings.
1349
- Args:
1350
- {
1351
- "comment": null,
1352
- "enabled": true,
1353
- "label": "{gce.zone}-{jvm.args.abc}",
1354
- "matchSpecification": [
1355
- {
1356
- "key": "gce.zone",
1357
- "value": ".*"
1358
- },
1359
- {
1360
- "key": "jvm.args.abc",
1361
- "value": ".*"
1362
- }
1363
- ],
1364
- "name": "ABC is good"
1365
- }
1366
- ctx: The MCP context (optional)
726
+ async def _order_service_config(self,
727
+ request_body: List[str],
728
+ ctx=None,
729
+ api_client=None) -> Dict[str, Any]:
730
+ """Order Service configurations."""
731
+ try:
732
+ if not request_body:
733
+ return {"error": "request_body is required"}
734
+ result = api_client.order_service_config(request_body=request_body)
735
+ return {"success": True, "message": "Service configs ordered successfully", "result": result}
736
+ except Exception as e:
737
+ logger.error(f"Error in _order_service_config: {e}", exc_info=True)
738
+ return {"error": f"Failed to order service configs: {e!s}"}
1367
739
 
1368
- Returns:
1369
- Dictionary containing endpoints data or error information
1370
- """
740
+ @with_header_auth(ApplicationSettingsApi)
741
+ async def _replace_all_service_configs(self,
742
+ payload: Union[Dict[str, Any], str],
743
+ ctx=None,
744
+ api_client=None) -> Dict[str, Any]:
745
+ """Replace all Service configurations."""
1371
746
  try:
1372
747
  if not payload:
1373
- return {"error": "missing arguments"}
748
+ return {"error": "payload is required"}
1374
749
 
1375
- # Parse the payload if it's a string
1376
750
  if isinstance(payload, str):
1377
- logger.debug("Payload is a string, attempting to parse")
751
+ import json
1378
752
  try:
1379
- import json
1380
- try:
1381
- parsed_payload = json.loads(payload)
1382
- logger.debug("Successfully parsed payload as JSON")
1383
- request_body = parsed_payload
1384
- except json.JSONDecodeError as e:
1385
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
1386
-
1387
- # Try replacing single quotes with double quotes
1388
- fixed_payload = payload.replace("'", "\"")
1389
- try:
1390
- parsed_payload = json.loads(fixed_payload)
1391
- logger.debug("Successfully parsed fixed JSON")
1392
- request_body = parsed_payload
1393
- except json.JSONDecodeError:
1394
- # Try as Python literal
1395
- import ast
1396
- try:
1397
- parsed_payload = ast.literal_eval(payload)
1398
- logger.debug("Successfully parsed payload as Python literal")
1399
- request_body = parsed_payload
1400
- except (SyntaxError, ValueError) as e2:
1401
- logger.debug(f"Failed to parse payload string: {e2}")
1402
- return {"error": f"Invalid payload format: {e2}", "payload": payload}
1403
- except Exception as e:
1404
- logger.debug(f"Error parsing payload string: {e}")
1405
- return {"error": f"Failed to parse payload: {e}", "payload": payload}
753
+ request_body = json.loads(payload)
754
+ except json.JSONDecodeError:
755
+ import ast
756
+ request_body = ast.literal_eval(payload)
1406
757
  else:
1407
- # If payload is already a dictionary, use it directly
1408
- logger.debug("Using provided payload dictionary")
1409
758
  request_body = payload
1410
759
 
1411
- # Import the ServiceConfig class
1412
- try:
1413
- from instana_client.models.service_config import (
1414
- ServiceConfig,
1415
- )
1416
- logger.debug("Successfully imported ServiceConfig")
1417
- except ImportError as e:
1418
- logger.debug(f"Error importing ServiceConfig: {e}")
1419
- return {"error": f"Failed to import ServiceConfig: {e!s}"}
1420
-
1421
- # Create an ServiceConfig object from the request body
1422
- try:
1423
- logger.debug(f"Creating ServiceConfig with params: {request_body}")
1424
- config_object = ServiceConfig(**request_body)
1425
- logger.debug("Successfully add service config object")
1426
- except Exception as e:
1427
- logger.debug(f"Error creating ServiceConfig: {e}")
1428
- return {"error": f"Failed to add service config object: {e!s}"}
1429
-
1430
- # Call the ServiceConfig method from the SDK
1431
- logger.debug("Calling add_service_config with config object")
1432
- result = api_client.add_service_config(
1433
- service_config=config_object
1434
- )
1435
-
1436
- # Convert the result to a dictionary
1437
- if hasattr(result, 'to_dict'):
1438
- result_dict = result.to_dict()
1439
- else:
1440
- # If it's already a dict or another format, use it as is
1441
- result_dict = result or {
1442
- "success": True,
1443
- "message": "Create service config"
1444
- }
1445
-
1446
- logger.debug(f"Result from add_service_config: {result_dict}")
1447
- return result_dict
760
+ # Assuming request_body is a list of ServiceConfig objects
761
+ config_objects = [ServiceConfig(**item) if isinstance(item, dict) else item for item in request_body]
762
+ result = api_client.replace_all_service_configs(service_config=config_objects)
763
+ return {"success": True, "message": "All service configs replaced successfully", "result": result}
1448
764
  except Exception as e:
1449
- logger.error(f"Error in add_service_config: {e}")
1450
- return {"error": f"Failed to add service config: {e!s}"}
765
+ logger.error(f"Error in _replace_all_service_configs: {e}", exc_info=True)
766
+ return {"error": f"Failed to replace all service configs: {e!s}"}
1451
767
 
1452
- @register_as_tool(
1453
- title="Replace All Service Configs",
1454
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True)
1455
- )
768
+ # Manual Service Config Operations
1456
769
  @with_header_auth(ApplicationSettingsApi)
1457
- async def replace_all_service_configs(self,
1458
- payload: Union[Dict[str, Any], str],
1459
- ctx=None,
1460
- api_client=None) -> List[Dict[str, Any]]:
1461
- """
1462
- Args:
1463
- [
1464
- {
1465
- "comment": null,
1466
- "enabled": true,
1467
- "id": "8C-jGYx8Rsue854tzkh8KQ",
1468
- "label": "{docker.container.name}",
1469
- "matchSpecification": [
1470
- {
1471
- "key": "docker.container.name",
1472
- "value": ".*"
1473
- }
1474
- ],
1475
- "name": "Rule"
1476
- }
1477
- ]
1478
- ctx: The MCP context (optional)
770
+ async def _get_all_manual_service_configs(self,
771
+ ctx=None,
772
+ api_client=None) -> List[Dict[str, Any]]:
773
+ """Get all Manual Service configurations."""
774
+ try:
775
+ result = api_client.get_manual_service_configs_without_preload_content()
776
+ import json
777
+ response_text = result.data.decode('utf-8')
778
+ json_data = json.loads(response_text)
779
+ return json_data if isinstance(json_data, list) else [json_data] if json_data else []
780
+ except Exception as e:
781
+ logger.error(f"Error in _get_all_manual_service_configs: {e}", exc_info=True)
782
+ return [{"error": f"Failed to get manual service configs: {e!s}"}]
1479
783
 
1480
- Returns:
1481
- Dictionary containing endpoints data or error information
1482
- """
784
+ @with_header_auth(ApplicationSettingsApi)
785
+ async def _add_manual_service_config(self,
786
+ payload: Union[Dict[str, Any], str],
787
+ ctx=None,
788
+ api_client=None) -> Dict[str, Any]:
789
+ """Add a new Manual Service configuration."""
1483
790
  try:
1484
791
  if not payload:
1485
- return [{"error": "missing arguments"}]
792
+ return {"error": "payload is required"}
1486
793
 
1487
- # Parse the payload if it's a string
1488
794
  if isinstance(payload, str):
1489
- logger.debug("Payload is a string, attempting to parse")
795
+ import json
1490
796
  try:
1491
- import json
1492
- try:
1493
- parsed_payload = json.loads(payload)
1494
- logger.debug("Successfully parsed payload as JSON")
1495
- request_body = parsed_payload
1496
- except json.JSONDecodeError as e:
1497
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
1498
-
1499
- # Try replacing single quotes with double quotes
1500
- fixed_payload = payload.replace("'", "\"")
1501
- try:
1502
- parsed_payload = json.loads(fixed_payload)
1503
- logger.debug("Successfully parsed fixed JSON")
1504
- request_body = parsed_payload
1505
- except json.JSONDecodeError:
1506
- # Try as Python literal
1507
- import ast
1508
- try:
1509
- parsed_payload = ast.literal_eval(payload)
1510
- logger.debug("Successfully parsed payload as Python literal")
1511
- request_body = parsed_payload
1512
- except (SyntaxError, ValueError) as e2:
1513
- logger.debug(f"Failed to parse payload string: {e2}")
1514
- return [{"error": f"Invalid payload format: {e2}", "payload": payload}]
1515
- except Exception as e:
1516
- logger.debug(f"Error parsing payload string: {e}")
1517
- return [{"error": f"Failed to parse payload: {e}", "payload": payload}]
797
+ request_body = json.loads(payload)
798
+ except json.JSONDecodeError:
799
+ import ast
800
+ request_body = ast.literal_eval(payload)
1518
801
  else:
1519
- # If payload is already a dictionary, use it directly
1520
- logger.debug("Using provided payload dictionary")
1521
802
  request_body = payload
1522
803
 
1523
- # Import the ServiceConfig class
1524
- try:
1525
- from instana_client.models.service_config import (
1526
- ServiceConfig,
1527
- )
1528
- logger.debug("Successfully imported ServiceConfig")
1529
- except ImportError as e:
1530
- logger.debug(f"Error importing ServiceConfig: {e}")
1531
- return [{"error": f"Failed to import ServiceConfig: {e!s}"}]
1532
-
1533
- # Create an ServiceConfig object from the request body
1534
- try:
1535
- logger.debug(f"Creating ServiceConfig with params: {request_body}")
1536
- config_object = [ServiceConfig(**request_body)]
1537
- logger.debug("Successfully replace all manual service config object")
1538
- except Exception as e:
1539
- logger.debug(f"Error creating ServiceConfig: {e}")
1540
- return [{"error": f"Failed to replace all manual config object: {e!s}"}]
1541
-
1542
- # Call the replace_all method from the SDK
1543
- logger.debug("Calling replace_all with config object")
1544
- result = api_client.replace_all(
1545
- service_config=config_object
1546
- )
1547
-
1548
- # Convert the result to a list of dictionaries
804
+ config_object = NewManualServiceConfig(**request_body)
805
+ result = api_client.add_manual_service_config(new_manual_service_config=config_object)
1549
806
  if hasattr(result, 'to_dict'):
1550
- result_dict = result.to_dict()
1551
- else:
1552
- # If it's already a dict or another format, use it as is
1553
- result_dict = result or {
1554
- "success": True,
1555
- "message": "replace all service config"
1556
- }
1557
-
1558
- logger.debug(f"Result from replace_all: {result_dict}")
1559
- return [result_dict]
807
+ return result.to_dict()
808
+ return result or {"success": True, "message": "Manual service config created"}
1560
809
  except Exception as e:
1561
- logger.error(f"Error in replace_all: {e}")
1562
- return [{"error": f"Failed to replace all service config: {e!s}"}]
810
+ logger.error(f"Error in _add_manual_service_config: {e}", exc_info=True)
811
+ return {"error": f"Failed to add manual service config: {e!s}"}
1563
812
 
1564
-
1565
- @register_as_tool(
1566
- title="Order Service Config",
1567
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
1568
- )
1569
813
  @with_header_auth(ApplicationSettingsApi)
1570
- async def order_service_config(self,
1571
- request_body: List[str],
1572
- ctx=None,
1573
- api_client=None) -> Dict[str, Any]:
1574
- """
1575
- order Service Configurations (Custom Service Rules)
1576
-
1577
- This tool changes the order of service configurations based on the provided list of IDs.
1578
- All service configuration IDs must be included in the request.
1579
-
1580
- Args:
1581
- request_body: List of service configuration IDs in the desired order.
1582
- ctx: The MCP context (optional)
1583
-
1584
- Returns:
1585
- A dictionary with the API response or error message.
1586
- """
814
+ async def _update_manual_service_config(self,
815
+ id: str,
816
+ payload: Union[Dict[str, Any], str],
817
+ ctx=None,
818
+ api_client=None) -> Dict[str, Any]:
819
+ """Update a Manual Service configuration."""
1587
820
  try:
1588
- debug_print("ordering service configurations")
1589
-
1590
- if not request_body:
1591
- return {"error": "The list of service configuration IDs cannot be empty."}
821
+ if not id or not payload:
822
+ return {"error": "id and payload are required"}
1592
823
 
1593
- result = api_client.order_service_config(
1594
- request_body=request_body
1595
- )
824
+ if isinstance(payload, str):
825
+ import json
826
+ try:
827
+ request_body = json.loads(payload)
828
+ except json.JSONDecodeError:
829
+ import ast
830
+ request_body = ast.literal_eval(payload)
831
+ else:
832
+ request_body = payload
1596
833
 
1597
- # Convert result to dict if needed
834
+ config_object = ManualServiceConfig(**request_body)
835
+ result = api_client.update_manual_service_config(id=id, manual_service_config=config_object)
1598
836
  if hasattr(result, 'to_dict'):
1599
837
  return result.to_dict()
1600
- return result
1601
-
1602
- except Exception as e:
1603
- debug_print(f"Error in order_service_config: {e}")
1604
- traceback.print_exc(file=sys.stderr)
1605
- return {"error": f"Failed to order service configs: {e!s}"}
1606
-
1607
- @register_as_tool(
1608
- title="Delete Service Config",
1609
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True)
1610
- )
1611
- @with_header_auth(ApplicationSettingsApi)
1612
- async def delete_service_config(self,
1613
- id: str,
1614
- ctx=None,
1615
- api_client=None) -> Dict[str, Any]:
1616
- """
1617
- Delete a Service Perspective configuration.
1618
- This tool allows you to delete an existing Service Config by its ID.
1619
-
1620
- Args:
1621
- id: The ID of the application perspective to delete
1622
- ctx: The MCP context (optional)
1623
-
1624
- Returns:
1625
- Dictionary containing the result of the deletion or error information
1626
- """
1627
- try:
1628
- if not id:
1629
- return {"error": "Service perspective ID is required for deletion"}
1630
-
1631
-
1632
- debug_print(f"Deleting application perspective with ID: {id}")
1633
- # Call the delete_service_config method from the SDK
1634
- api_client.delete_service_config(id=id)
1635
-
1636
- result_dict = {
1637
- "success": True,
1638
- "message": f"Service Confiuguration '{id}' has been successfully deleted"
1639
- }
1640
-
1641
- debug_print(f"Successfully deleted service perspective with ID: {id}")
1642
- return result_dict
838
+ return result or {"success": True, "message": f"Manual service config '{id}' updated"}
1643
839
  except Exception as e:
1644
- debug_print(f"Error in delete_service_config: {e}")
1645
- traceback.print_exc(file=sys.stderr)
1646
- return {"error": f"Failed to delete service configuration: {e!s}"}
840
+ logger.error(f"Error in _update_manual_service_config: {e}", exc_info=True)
841
+ return {"error": f"Failed to update manual service config: {e!s}"}
1647
842
 
1648
- @register_as_tool(
1649
- title="Get Service Config",
1650
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
1651
- )
1652
843
  @with_header_auth(ApplicationSettingsApi)
1653
- async def get_service_config(
1654
- self,
1655
- id: str,
1656
- ctx=None,
1657
- api_client=None
1658
- ) -> Dict[str, Any]:
1659
- """
1660
- This MCP tool is used if one wants to retrieve the particular custom service configuration.
1661
- Args:
1662
- id: An Instana generated unique identifier for a Service.
1663
- ctx: The MCP context (optional)
1664
-
1665
- Returns:
1666
- Dict[str, Any]: Response from the create/update endpoint configuration API.
1667
-
1668
- """
844
+ async def _delete_manual_service_config(self,
845
+ id: str,
846
+ ctx=None,
847
+ api_client=None) -> Dict[str, Any]:
848
+ """Delete a Manual Service configuration."""
1669
849
  try:
1670
- debug_print("get service config")
1671
850
  if not id:
1672
- return {"error": "Required entities are missing or invalid"}
1673
-
1674
- result = api_client.get_service_config(
1675
- id=id
1676
- )
1677
- # Convert the result to a dictionary
1678
- if hasattr(result, 'to_dict'):
1679
- result_dict = result.to_dict()
1680
- else:
1681
- # If it's already a dict or another format, use it as is
1682
- result_dict = result
1683
-
1684
- debug_print(f"Result from get_service_config: {result_dict}")
1685
- return result_dict
851
+ return {"error": "id is required"}
852
+ api_client.delete_manual_service_config(id=id)
853
+ return {"success": True, "message": f"Manual service config '{id}' deleted successfully"}
1686
854
  except Exception as e:
1687
- debug_print(f"Error in get_service_config: {e}")
1688
- traceback.print_exc(file=sys.stderr)
1689
- return {"error": f"Failed to get service config: {e!s}"}
855
+ logger.error(f"Error in _delete_manual_service_config: {e}", exc_info=True)
856
+ return {"error": f"Failed to delete manual service config: {e!s}"}
1690
857
 
1691
- @register_as_tool(
1692
- title="Update Service Config",
1693
- annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False)
1694
- )
1695
858
  @with_header_auth(ApplicationSettingsApi)
1696
- async def update_service_config(self,
1697
- id: str,
1698
- payload: Union[Dict[str, Any], str],
1699
- ctx=None,
1700
- api_client=None) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
1701
- """
1702
- This tool gives is used if one wants to update a particular custom service rule.
1703
- Args:
1704
- {
1705
- "comment": null,
1706
- "enabled": true,
1707
- "id": "9uma4MhnTTSyBzwu_FKBJA",
1708
- "label": "{gce.zone}-{jvm.args.abc}",
1709
- "matchSpecification": [
1710
- {
1711
- "key": "gce.zone",
1712
- "value": ".*"
1713
- },
1714
- {
1715
- "key": "jvm.args.abc",
1716
- "value": ".*"
1717
- }
1718
- ],
1719
- "name": "DEF is good"
1720
- }
1721
- ctx: The MCP context (optional)
1722
-
1723
- Returns:
1724
- Dictionary containing endpoints data or error information
1725
- """
859
+ async def _replace_all_manual_service_config(self,
860
+ payload: Union[Dict[str, Any], str],
861
+ ctx=None,
862
+ api_client=None) -> Dict[str, Any]:
863
+ """Replace all Manual Service configurations."""
1726
864
  try:
1727
- if not payload or not id:
1728
- return [{"error": "missing arguments"}]
865
+ if not payload:
866
+ return {"error": "payload is required"}
1729
867
 
1730
- # Parse the payload if it's a string
1731
868
  if isinstance(payload, str):
1732
- logger.debug("Payload is a string, attempting to parse")
869
+ import json
1733
870
  try:
1734
- import json
1735
- try:
1736
- parsed_payload = json.loads(payload)
1737
- logger.debug("Successfully parsed payload as JSON")
1738
- request_body = parsed_payload
1739
- except json.JSONDecodeError as e:
1740
- logger.debug(f"JSON parsing failed: {e}, trying with quotes replaced")
1741
-
1742
- # Try replacing single quotes with double quotes
1743
- fixed_payload = payload.replace("'", "\"")
1744
- try:
1745
- parsed_payload = json.loads(fixed_payload)
1746
- logger.debug("Successfully parsed fixed JSON")
1747
- request_body = parsed_payload
1748
- except json.JSONDecodeError:
1749
- # Try as Python literal
1750
- import ast
1751
- try:
1752
- parsed_payload = ast.literal_eval(payload)
1753
- logger.debug("Successfully parsed payload as Python literal")
1754
- request_body = parsed_payload
1755
- except (SyntaxError, ValueError) as e2:
1756
- logger.debug(f"Failed to parse payload string: {e2}")
1757
- return [{"error": f"Invalid payload format: {e2}", "payload": payload}]
1758
- except Exception as e:
1759
- logger.debug(f"Error parsing payload string: {e}")
1760
- return [{"error": f"Failed to parse payload: {e}", "payload": payload}]
871
+ request_body = json.loads(payload)
872
+ except json.JSONDecodeError:
873
+ import ast
874
+ request_body = ast.literal_eval(payload)
1761
875
  else:
1762
- # If payload is already a dictionary, use it directly
1763
- logger.debug("Using provided payload dictionary")
1764
876
  request_body = payload
1765
877
 
1766
- # Import the ServiceConfig class
1767
- try:
1768
- from instana_client.models.service_config import (
1769
- ServiceConfig,
1770
- )
1771
- logger.debug("Successfully imported ServiceConfig")
1772
- except ImportError as e:
1773
- logger.debug(f"Error importing ServiceConfig: {e}")
1774
- return [{"error": f"Failed to import ServiceConfig: {e!s}"}]
1775
-
1776
- # Create an ServiceConfig object from the request body
1777
- try:
1778
- logger.debug(f"Creating ServiceConfig with params: {request_body}")
1779
- config_object = [ServiceConfig(**request_body)]
1780
- logger.debug("Successfully update service config object")
1781
- except Exception as e:
1782
- logger.debug(f"Error creating ServiceConfig: {e}")
1783
- return [{"error": f"Failed to replace all manual config object: {e!s}"}]
1784
-
1785
- # Call the put_service_config method from the SDK
1786
- logger.debug("Calling put_service_config with config object")
1787
- result = api_client.put_service_config(
1788
- id=id,
1789
- service_config=config_object
1790
- )
1791
-
1792
- # Convert the result to a dictionary
1793
- if hasattr(result, 'to_dict'):
1794
- result_dict = result.to_dict()
1795
- else:
1796
- # If it's already a dict or another format, use it as is
1797
- result_dict = result or {
1798
- "success": True,
1799
- "message": "put service config"
1800
- }
1801
-
1802
- logger.debug(f"Result from put_service_config: {result_dict}")
1803
- return [result_dict]
878
+ # Assuming request_body is a list of ManualServiceConfig objects
879
+ config_objects = [ManualServiceConfig(**item) if isinstance(item, dict) else item for item in request_body]
880
+ result = api_client.replace_all_manual_service_configs(manual_service_config=config_objects)
881
+ return {"success": True, "message": "All manual service configs replaced successfully", "result": result}
1804
882
  except Exception as e:
1805
- logger.error(f"Error in put_service_config: {e}")
1806
- return [{"error": f"Failed to update service config: {e!s}"}]
883
+ logger.error(f"Error in _replace_all_manual_service_config: {e}", exc_info=True)
884
+ return {"error": f"Failed to replace all manual service configs: {e!s}"}
1807
885