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.
- {mcp_instana-0.3.0.dist-info → mcp_instana-0.6.2.dist-info}/METADATA +18 -174
- {mcp_instana-0.3.0.dist-info → mcp_instana-0.6.2.dist-info}/RECORD +18 -17
- {mcp_instana-0.3.0.dist-info → mcp_instana-0.6.2.dist-info}/WHEEL +1 -1
- src/application/application_alert_config.py +10 -4
- src/application/application_analyze.py +13 -10
- src/application/application_global_alert_config.py +22 -21
- src/application/application_metrics.py +21 -21
- src/application/application_resources.py +44 -4
- src/application/application_settings.py +190 -66
- src/core/server.py +3 -0
- src/event/events_tools.py +57 -9
- src/infrastructure/infrastructure_catalog.py +30 -4
- src/infrastructure/infrastructure_metrics.py +1 -0
- src/infrastructure/infrastructure_resources.py +1 -4
- src/infrastructure/infrastructure_topology.py +27 -15
- src/observability.py +29 -0
- {mcp_instana-0.3.0.dist-info → mcp_instana-0.6.2.dist-info}/entry_points.txt +0 -0
- {mcp_instana-0.3.0.dist-info → mcp_instana-0.6.2.dist-info}/licenses/LICENSE.md +0 -0
src/event/events_tools.py
CHANGED
|
@@ -578,11 +578,12 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
|
|
|
578
578
|
"""
|
|
579
579
|
|
|
580
580
|
try:
|
|
581
|
-
logger.debug(f"
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|