mcp-instana 0.3.0__py3-none-any.whl → 0.6.2__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.
src/event/events_tools.py CHANGED
@@ -578,11 +578,12 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
578
578
  """
579
579
 
580
580
  try:
581
- logger.debug(f"get_issue_events called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
581
+ logger.debug(f"get_issues called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
582
582
  from_time, to_time = self._process_time_range(time_range, from_time, to_time)
583
583
  if not from_time:
584
584
  from_time = to_time - (60 * 60 * 1000)
585
585
  try:
586
+ # Use the optimized without_preload_content approach for faster response
586
587
  response_data = api_client.get_events_without_preload_content(
587
588
  var_from=from_time,
588
589
  to=to_time,
@@ -591,20 +592,35 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
591
592
  exclude_triggered_before=exclude_triggered_before,
592
593
  event_type_filters=["issue"]
593
594
  )
595
+
596
+ # Check response status immediately
594
597
  if response_data.status != 200:
595
598
  return {"error": f"Failed to get issue events: HTTP {response_data.status}"}
599
+
600
+ # Process the response data directly without additional parsing
596
601
  response_text = response_data.data.decode('utf-8')
597
602
  result = json.loads(response_text)
603
+
604
+ # Create a standardized result format
598
605
  if isinstance(result, list):
599
- result_dict = {"events": result, "events_count": len(result)}
606
+ # Include a summary of the events for quicker analysis
607
+ events_count = len(result)
608
+ result_dict = {
609
+ "events": result[:max_events], # Limit to max_events for performance
610
+ "events_count": events_count,
611
+ "total_events": events_count,
612
+ "time_range": f"From {datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')} to {datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')}"
613
+ }
600
614
  else:
601
615
  result_dict = result
616
+
617
+ logger.debug(f"Successfully retrieved {result_dict.get('events_count', 0)} issue events")
602
618
  return result_dict
603
619
  except Exception as api_error:
604
620
  logger.error(f"API call failed: {api_error}", exc_info=True)
605
621
  return {"error": f"Failed to get issue events: {api_error}"}
606
622
  except Exception as e:
607
- logger.error(f"Error in get_issue_events: {e}", exc_info=True)
623
+ logger.error(f"Error in get_issues: {e}", exc_info=True)
608
624
  return {"error": f"Failed to get issue events: {e!s}"}
609
625
 
610
626
  @register_as_tool(
@@ -649,11 +665,12 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
649
665
  """
650
666
 
651
667
  try:
652
- logger.debug(f"get_incident_events called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
668
+ logger.debug(f"get_incidents called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
653
669
  from_time, to_time = self._process_time_range(time_range, from_time, to_time)
654
670
  if not from_time:
655
671
  from_time = to_time - (60 * 60 * 1000)
656
672
  try:
673
+ # Use the optimized without_preload_content approach for faster response
657
674
  response_data = api_client.get_events_without_preload_content(
658
675
  var_from=from_time,
659
676
  to=to_time,
@@ -662,20 +679,35 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
662
679
  exclude_triggered_before=exclude_triggered_before,
663
680
  event_type_filters=["incident"]
664
681
  )
682
+
683
+ # Check response status immediately
665
684
  if response_data.status != 200:
666
685
  return {"error": f"Failed to get incident events: HTTP {response_data.status}"}
686
+
687
+ # Process the response data directly without additional parsing
667
688
  response_text = response_data.data.decode('utf-8')
668
689
  result = json.loads(response_text)
690
+
691
+ # Create a standardized result format
669
692
  if isinstance(result, list):
670
- result_dict = {"events": result, "events_count": len(result)}
693
+ # Include a summary of the events for quicker analysis
694
+ events_count = len(result)
695
+ result_dict = {
696
+ "events": result[:max_events], # Limit to max_events for performance
697
+ "events_count": events_count,
698
+ "total_events": events_count,
699
+ "time_range": f"From {datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')} to {datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')}"
700
+ }
671
701
  else:
672
702
  result_dict = result
703
+
704
+ logger.debug(f"Successfully retrieved {result_dict.get('events_count', 0)} incident events")
673
705
  return result_dict
674
706
  except Exception as api_error:
675
707
  logger.error(f"API call failed: {api_error}", exc_info=True)
676
708
  return {"error": f"Failed to get incident events: {api_error}"}
677
709
  except Exception as e:
678
- logger.error(f"Error in get_incident_events: {e}", exc_info=True)
710
+ logger.error(f"Error in get_incidents: {e}", exc_info=True)
679
711
  return {"error": f"Failed to get incident events: {e!s}"}
680
712
 
681
713
  @register_as_tool(
@@ -720,11 +752,12 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
720
752
  """
721
753
 
722
754
  try:
723
- logger.debug(f"get_change_events called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
755
+ logger.debug(f"get_changes called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
724
756
  from_time, to_time = self._process_time_range(time_range, from_time, to_time)
725
757
  if not from_time:
726
758
  from_time = to_time - (60 * 60 * 1000)
727
759
  try:
760
+ # Use the optimized without_preload_content approach for faster response
728
761
  response_data = api_client.get_events_without_preload_content(
729
762
  var_from=from_time,
730
763
  to=to_time,
@@ -733,20 +766,35 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
733
766
  exclude_triggered_before=exclude_triggered_before,
734
767
  event_type_filters=["change"]
735
768
  )
769
+
770
+ # Check response status immediately
736
771
  if response_data.status != 200:
737
772
  return {"error": f"Failed to get change events: HTTP {response_data.status}"}
773
+
774
+ # Process the response data directly without additional parsing
738
775
  response_text = response_data.data.decode('utf-8')
739
776
  result = json.loads(response_text)
777
+
778
+ # Create a standardized result format
740
779
  if isinstance(result, list):
741
- result_dict = {"events": result, "events_count": len(result)}
780
+ # Include a summary of the events for quicker analysis
781
+ events_count = len(result)
782
+ result_dict = {
783
+ "events": result[:max_events], # Limit to max_events for performance
784
+ "events_count": events_count,
785
+ "total_events": events_count,
786
+ "time_range": f"From {datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')} to {datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')}"
787
+ }
742
788
  else:
743
789
  result_dict = result
790
+
791
+ logger.debug(f"Successfully retrieved {result_dict.get('events_count', 0)} change events")
744
792
  return result_dict
745
793
  except Exception as api_error:
746
794
  logger.error(f"API call failed: {api_error}", exc_info=True)
747
795
  return {"error": f"Failed to get change events: {api_error}"}
748
796
  except Exception as e:
749
- logger.error(f"Error in get_change_events: {e}", exc_info=True)
797
+ logger.error(f"Error in get_changes: {e}", exc_info=True)
750
798
  return {"error": f"Failed to get change events: {e!s}"}
751
799
 
752
800
  @register_as_tool(
@@ -88,6 +88,11 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
88
88
  result_dict = {"data": str(result), "plugin_id": plugin_id}
89
89
 
90
90
  logger.debug(f"Result from get_available_payload_keys_by_plugin_id: {result_dict}")
91
+
92
+ # Safety check: ensure we never return a raw list
93
+ if isinstance(result_dict, list):
94
+ result_dict = {"payload_keys": result_dict, "plugin_id": plugin_id}
95
+
91
96
  return result_dict
92
97
 
93
98
  except Exception as sdk_error:
@@ -111,7 +116,16 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
111
116
  # Try to parse as JSON first
112
117
  import json
113
118
  try:
114
- result_dict = json.loads(response_text)
119
+ parsed_result = json.loads(response_text)
120
+
121
+ # Ensure we always return a dictionary, not a raw list
122
+ if isinstance(parsed_result, list):
123
+ result_dict = {"payload_keys": parsed_result, "plugin_id": plugin_id}
124
+ elif isinstance(parsed_result, dict):
125
+ result_dict = parsed_result
126
+ else:
127
+ result_dict = {"data": parsed_result, "plugin_id": plugin_id}
128
+
115
129
  logger.debug(f"Result from fallback method (JSON): {result_dict}")
116
130
  return result_dict
117
131
  except json.JSONDecodeError:
@@ -390,14 +404,26 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
390
404
  return result_dict
391
405
 
392
406
  except Exception as sdk_error:
393
- logger.error(f"SDK method failed: {sdk_error}, trying with custom headers")
407
+ logger.error(f"SDK method failed: {sdk_error}, evaluating fallback conditions")
394
408
 
395
409
  # Check if it's a 406 error
396
410
  is_406_error = False
397
411
  if hasattr(sdk_error, 'status') and sdk_error.status == 406 or "406" in str(sdk_error) and "Not Acceptable" in str(sdk_error):
398
412
  is_406_error = True
399
413
 
400
- if is_406_error:
414
+ # Check for Pydantic ValidationError (SDK model deserialization issues)
415
+ is_pydantic_error = False
416
+ try:
417
+ from pydantic import (
418
+ ValidationError as _PydanticValidationError, # type: ignore
419
+ )
420
+ is_pydantic_error = isinstance(sdk_error, _PydanticValidationError)
421
+ except Exception:
422
+ # Fallback to string inspection if pydantic not importable in runtime
423
+ err_str = str(sdk_error).lower()
424
+ is_pydantic_error = ("pydantic" in err_str and "validation" in err_str) or ("validation error" in err_str)
425
+
426
+ if is_406_error or is_pydantic_error:
401
427
  # Try using the SDK's method with custom headers
402
428
  # The SDK should have a method that allows setting custom headers
403
429
  custom_headers = {
@@ -430,7 +456,7 @@ class InfrastructureCatalogMCPTools(BaseInstanaClient):
430
456
  logger.error(error_message)
431
457
  return {"error": error_message}
432
458
  else:
433
- # Re-raise if it's not a 406 error
459
+ # Re-raise if it's not a 406 or Pydantic validation error
434
460
  raise
435
461
 
436
462
  except Exception as e:
@@ -87,6 +87,7 @@ class InfrastructureMetricsMCPTools(BaseInstanaClient):
87
87
  if not plugin:
88
88
  return {"error": "Plugin is required for this operation"}
89
89
 
90
+ # If no query is provided, return an error
90
91
  if not query:
91
92
  return {"error": "Query is required for this operation"}
92
93
 
@@ -68,10 +68,7 @@ class InfrastructureResourcesMCPTools(BaseInstanaClient):
68
68
  logger.error(f"Error in get_monitoring_state: {e}", exc_info=True)
69
69
  return {"error": f"Failed to get monitoring state: {e!s}"}
70
70
 
71
- @register_as_tool(
72
- title="Get Plugin Payload",
73
- annotations=ToolAnnotations(readOnlyHint=True, destructiveHint=False)
74
- )
71
+ # This tool is disabled since the underlying API is not giving a proper response.
75
72
  @with_header_auth(InfrastructureResourcesApi)
76
73
  async def get_plugin_payload(self,
77
74
  snapshot_id: str,
@@ -133,6 +133,9 @@ class InfrastructureTopologyMCPTools(BaseInstanaClient):
133
133
  The topology includes nodes (representing entities like hosts, processes, containers) and edges (representing
134
134
  connections between entities). This is useful for understanding the overall structure of your environment.
135
135
 
136
+ This implementation uses the `get_topology_without_preload_content` method from the SDK to bypass validation
137
+ issues that can occur with complex Kubernetes infrastructure data.
138
+
136
139
  For example, use this tool when:
137
140
  - You need a complete map of your infrastructure
138
141
  - You want to understand how components are connected
@@ -152,22 +155,31 @@ class InfrastructureTopologyMCPTools(BaseInstanaClient):
152
155
 
153
156
  # Use the API client from the decorator
154
157
  try:
155
- result = api_client.get_topology(include_data=include_data)
156
- logger.debug("SDK call successful, processing result")
157
- except Exception as sdk_error:
158
- logger.error(f"SDK validation error: {sdk_error}")
158
+ # Use get_topology_without_preload_content to bypass validation
159
+ response = api_client.get_topology_without_preload_content(include_data=include_data)
160
+ logger.debug("SDK call successful using get_topology_without_preload_content")
159
161
 
160
- # If it's a validation error, try to extract useful information from the error
161
- if "validation error" in str(sdk_error).lower():
162
- return {
163
- "error": "SDK validation error occurred",
164
- "details": str(sdk_error),
165
- "suggestion": "The API response format may not match the expected SDK model structure. This often happens with complex Kubernetes or cloud infrastructure data.",
166
- "workaround": "Consider using other topology tools like get_related_hosts with specific snapshot IDs, or check if the include_data parameter affects the response format."
167
- }
168
- else:
169
- # Re-raise if it's not a validation error
170
- raise sdk_error
162
+ # Parse the JSON response manually following the pattern from application_topology.py
163
+ import json
164
+ try:
165
+ # The result from get_topology_without_preload_content is a response object
166
+ # We need to read the response data and parse it as JSON
167
+ response_text = response.data.decode('utf-8')
168
+ result = json.loads(response_text)
169
+ logger.debug("Successfully parsed topology data as JSON")
170
+ except (json.JSONDecodeError, AttributeError) as json_err:
171
+ error_message = f"Failed to parse JSON response: {json_err}"
172
+ logger.error(error_message)
173
+ return {"error": error_message}
174
+
175
+ except Exception as sdk_error:
176
+ logger.error(f"SDK error: {sdk_error}")
177
+ return {
178
+ "error": "Failed to get topology data",
179
+ "details": str(sdk_error),
180
+ "suggestion": "The API may be unavailable or the request format is incorrect.",
181
+ "workaround": "Try again later or check if the include_data parameter affects the response."
182
+ }
171
183
 
172
184
  # Convert the result to a dictionary
173
185
  result_dict = None
src/observability.py ADDED
@@ -0,0 +1,29 @@
1
+ import os
2
+ import sys
3
+
4
+
5
+ def workflow(name=None):
6
+ def decorator(func):
7
+ return func
8
+ return decorator
9
+
10
+ def task(name=None):
11
+ def decorator(func):
12
+ return func
13
+ return decorator
14
+
15
+ TRACELOOP_ENABLED = os.getenv("ENABLE_MCP_OBSERVABILITY", "false").lower() in ("true", "1", "yes", "on")
16
+
17
+ if TRACELOOP_ENABLED:
18
+ try:
19
+ from traceloop.sdk import Traceloop
20
+ from traceloop.sdk.decorators import task as traceloop_task
21
+ from traceloop.sdk.decorators import workflow as traceloop_workflow
22
+ Traceloop.init(app_name="Instana-MCP-Server")
23
+ print("Traceloop enabled and initialized for MCP Client", file=sys.stderr)
24
+ # Override the no-op decorators with real ones
25
+ workflow = traceloop_workflow
26
+ task = traceloop_task
27
+ except ImportError:
28
+ print("Traceloop requested but not installed. Install with: pip install traceloop-sdk", file=sys.stderr)
29
+ TRACELOOP_ENABLED = False