mcp-instana 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_instana-0.1.1.dist-info/METADATA +908 -0
- mcp_instana-0.1.1.dist-info/RECORD +30 -0
- {mcp_instana-0.1.0.dist-info → mcp_instana-0.1.1.dist-info}/WHEEL +1 -1
- mcp_instana-0.1.1.dist-info/entry_points.txt +4 -0
- mcp_instana-0.1.0.dist-info/LICENSE → mcp_instana-0.1.1.dist-info/licenses/LICENSE.md +3 -3
- src/application/__init__.py +1 -0
- src/{client/application_alert_config_mcp_tools.py → application/application_alert_config.py} +251 -273
- src/application/application_analyze.py +415 -0
- src/application/application_catalog.py +153 -0
- src/{client/application_metrics_mcp_tools.py → application/application_metrics.py} +107 -129
- src/{client/application_resources_mcp_tools.py → application/application_resources.py} +128 -150
- src/application/application_settings.py +1135 -0
- src/application/application_topology.py +107 -0
- src/core/__init__.py +1 -0
- src/core/server.py +436 -0
- src/core/utils.py +213 -0
- src/event/__init__.py +1 -0
- src/{client/events_mcp_tools.py → event/events_tools.py} +128 -136
- src/infrastructure/__init__.py +1 -0
- src/{client/infrastructure_analyze_mcp_tools.py → infrastructure/infrastructure_analyze.py} +200 -203
- src/{client/infrastructure_catalog_mcp_tools.py → infrastructure/infrastructure_catalog.py} +194 -264
- src/infrastructure/infrastructure_metrics.py +167 -0
- src/{client/infrastructure_resources_mcp_tools.py → infrastructure/infrastructure_resources.py} +192 -223
- src/{client/infrastructure_topology_mcp_tools.py → infrastructure/infrastructure_topology.py} +105 -106
- src/log/__init__.py +1 -0
- src/log/log_alert_configuration.py +331 -0
- src/prompts/mcp_prompts.py +900 -0
- src/prompts/prompt_loader.py +29 -0
- src/prompts/prompt_registry.json +21 -0
- mcp_instana-0.1.0.dist-info/METADATA +0 -649
- mcp_instana-0.1.0.dist-info/RECORD +0 -19
- mcp_instana-0.1.0.dist-info/entry_points.txt +0 -3
- src/client/What is the sum of queue depth for all q +0 -55
- src/client/instana_client_base.py +0 -93
- src/client/log_alert_configuration_mcp_tools.py +0 -316
- src/client/show the top 5 services with the highest +0 -28
- src/mcp_server.py +0 -343
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application Topology MCP Tools Module
|
|
3
|
+
|
|
4
|
+
This module provides application topology-specific MCP tools for Instana monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from src.core.utils import BaseInstanaClient, register_as_tool
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from instana_client.api.application_topology_api import ApplicationTopologyApi
|
|
15
|
+
from instana_client.api_client import ApiClient
|
|
16
|
+
from instana_client.configuration import Configuration
|
|
17
|
+
except ImportError as e:
|
|
18
|
+
import logging
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
logger.error(f"Error importing Instana SDK: {e}", exc_info=True)
|
|
21
|
+
raise
|
|
22
|
+
|
|
23
|
+
# Configure logger for this module
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
class ApplicationTopologyMCPTools(BaseInstanaClient):
|
|
27
|
+
"""Tools for application topology in Instana MCP."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, read_token: str, base_url: str):
|
|
30
|
+
"""Initialize the Application Topology MCP tools client."""
|
|
31
|
+
super().__init__(read_token=read_token, base_url=base_url)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
|
|
35
|
+
# Configure the API client with the correct base URL and authentication
|
|
36
|
+
configuration = Configuration()
|
|
37
|
+
configuration.host = base_url
|
|
38
|
+
configuration.api_key['ApiKeyAuth'] = read_token
|
|
39
|
+
configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
|
|
40
|
+
|
|
41
|
+
# Create an API client with this configuration
|
|
42
|
+
api_client = ApiClient(configuration=configuration)
|
|
43
|
+
|
|
44
|
+
# Initialize the Instana SDK's ApplicationTopologyMCPTools with our configured client
|
|
45
|
+
self.topology_api = ApplicationTopologyApi(api_client=api_client)
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Error initializing ApplicationTopologyMCPTools: {e}", exc_info=True)
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
@register_as_tool
|
|
52
|
+
async def get_application_topology(self,
|
|
53
|
+
window_size: Optional[int] = None,
|
|
54
|
+
to_timestamp: Optional[int] = None,
|
|
55
|
+
application_id: Optional[str] = None,
|
|
56
|
+
application_boundary_scope: Optional[str] = None,
|
|
57
|
+
ctx = None) -> Dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Get the service topology from Instana Server.
|
|
60
|
+
This tool retrieves services and connections (call paths) between them for calls in the scope given by the parameters.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
window_size: Size of time window in milliseconds
|
|
64
|
+
to_timestamp: Timestamp since Unix Epoch in milliseconds of the end of the time window
|
|
65
|
+
application_id: Filter by application ID
|
|
66
|
+
application_boundary_scope: Filter by application scope, i.e., INBOUND or ALL. The default value is INBOUND.
|
|
67
|
+
ctx: Context information
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A dictionary containing the service topology data
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
logger.debug("Fetching service topology data")
|
|
75
|
+
|
|
76
|
+
# Set default values if not provided
|
|
77
|
+
if not to_timestamp:
|
|
78
|
+
to_timestamp = int(datetime.now().timestamp() * 1000)
|
|
79
|
+
|
|
80
|
+
if not window_size:
|
|
81
|
+
window_size = 3600000 # Default to 1 hour in milliseconds
|
|
82
|
+
|
|
83
|
+
# Call the API
|
|
84
|
+
# Note: The SDK expects parameters in camelCase, but we use snake_case in Python
|
|
85
|
+
# The SDK will handle the conversion
|
|
86
|
+
result = self.topology_api.get_services_map(
|
|
87
|
+
window_size=window_size,
|
|
88
|
+
to=to_timestamp,
|
|
89
|
+
application_id=application_id,
|
|
90
|
+
application_boundary_scope=application_boundary_scope
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Ensure we always return a dictionary
|
|
94
|
+
if hasattr(result, "to_dict"):
|
|
95
|
+
result_dict = result.to_dict()
|
|
96
|
+
elif isinstance(result, dict):
|
|
97
|
+
result_dict = result
|
|
98
|
+
else:
|
|
99
|
+
# Convert to dictionary using __dict__ or as a fallback, create a new dict with string representation
|
|
100
|
+
result_dict = getattr(result, "__dict__", {"data": str(result)})
|
|
101
|
+
|
|
102
|
+
logger.debug("Successfully retrieved service topology data")
|
|
103
|
+
return result_dict
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Error in get_application_topology: {e}", exc_info=True)
|
|
107
|
+
return {"error": f"Failed to get application topology: {e!s}"}
|
src/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Core module for MCP Instana
|
src/core/server.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standalone MCP Server for Instana Events and Infrastructure Resources
|
|
3
|
+
|
|
4
|
+
This module provides a dedicated MCP server that exposes Instana MCP Server.
|
|
5
|
+
Supports stdio and Streamable HTTP transports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
from contextlib import asynccontextmanager
|
|
14
|
+
from dataclasses import dataclass, fields
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
|
|
19
|
+
from src.prompts.prompt_loader import register_prompts
|
|
20
|
+
|
|
21
|
+
load_dotenv()
|
|
22
|
+
|
|
23
|
+
# Configure logging
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
level=logging.INFO, # Default level, can be overridden
|
|
26
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
27
|
+
handlers=[
|
|
28
|
+
logging.StreamHandler(sys.stderr)
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
def set_log_level(level_name):
|
|
34
|
+
"""Set the logging level based on the provided level name"""
|
|
35
|
+
level_map = {
|
|
36
|
+
"DEBUG": logging.DEBUG,
|
|
37
|
+
"INFO": logging.INFO,
|
|
38
|
+
"WARNING": logging.WARNING,
|
|
39
|
+
"ERROR": logging.ERROR,
|
|
40
|
+
"CRITICAL": logging.CRITICAL
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
level = level_map.get(level_name.upper(), logging.INFO)
|
|
44
|
+
logger.setLevel(level)
|
|
45
|
+
logging.getLogger().setLevel(level)
|
|
46
|
+
logger.info(f"Log level set to {level_name.upper()}")
|
|
47
|
+
|
|
48
|
+
# Add the project root to the Python path
|
|
49
|
+
current_path = os.path.abspath(__file__)
|
|
50
|
+
project_root = os.path.dirname(os.path.dirname(current_path))
|
|
51
|
+
if project_root not in sys.path:
|
|
52
|
+
sys.path.insert(0, project_root)
|
|
53
|
+
|
|
54
|
+
# Import the necessary modules
|
|
55
|
+
try:
|
|
56
|
+
from src.core.utils import MCP_TOOLS, register_as_tool
|
|
57
|
+
except ImportError:
|
|
58
|
+
logger.error("Failed to import required modules", exc_info=True)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
from fastmcp import FastMCP
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class MCPState:
|
|
66
|
+
"""State for the MCP server."""
|
|
67
|
+
events_client: Any = None
|
|
68
|
+
infra_client: Any = None
|
|
69
|
+
app_resource_client: Any = None
|
|
70
|
+
app_metrics_client: Any = None
|
|
71
|
+
app_alert_client: Any = None
|
|
72
|
+
infra_catalog_client: Any = None
|
|
73
|
+
infra_topo_client: Any = None
|
|
74
|
+
infra_analyze_client: Any = None
|
|
75
|
+
infra_metrics_client: Any = None
|
|
76
|
+
app_catalog_client: Any = None
|
|
77
|
+
app_topology_client: Any = None
|
|
78
|
+
app_analyze_client: Any = None
|
|
79
|
+
app_settings_client: Any = None
|
|
80
|
+
|
|
81
|
+
# Global variables to store credentials for lifespan
|
|
82
|
+
_global_token = None
|
|
83
|
+
_global_base_url = None
|
|
84
|
+
|
|
85
|
+
def get_instana_credentials():
|
|
86
|
+
"""Get Instana credentials from environment variables for stdio mode."""
|
|
87
|
+
# For stdio mode, use INSTANA_API_TOKEN and INSTANA_BASE_URL
|
|
88
|
+
token = (os.getenv("INSTANA_API_TOKEN") or "")
|
|
89
|
+
base_url = (os.getenv("INSTANA_BASE_URL") or "")
|
|
90
|
+
|
|
91
|
+
return token, base_url
|
|
92
|
+
|
|
93
|
+
def validate_credentials(token: str, base_url: str) -> bool:
|
|
94
|
+
"""Validate that Instana credentials are provided for stdio mode."""
|
|
95
|
+
# For stdio mode, validate INSTANA_API_TOKEN and INSTANA_BASE_URL
|
|
96
|
+
return not (not token or not base_url)
|
|
97
|
+
|
|
98
|
+
def create_clients(token: str, base_url: str, enabled_categories: str = "all") -> MCPState:
|
|
99
|
+
"""Create only the enabled Instana clients"""
|
|
100
|
+
state = MCPState()
|
|
101
|
+
|
|
102
|
+
# Get enabled client configurations
|
|
103
|
+
enabled_client_configs = get_enabled_client_configs(enabled_categories)
|
|
104
|
+
|
|
105
|
+
for attr_name, client_class in enabled_client_configs:
|
|
106
|
+
try:
|
|
107
|
+
client = client_class(read_token=token, base_url=base_url)
|
|
108
|
+
setattr(state, attr_name, client)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to create {attr_name}: {e}", exc_info=True)
|
|
111
|
+
setattr(state, attr_name, None)
|
|
112
|
+
|
|
113
|
+
return state
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@asynccontextmanager
|
|
117
|
+
async def lifespan(server: FastMCP) -> AsyncIterator[MCPState]:
|
|
118
|
+
"""Set up and tear down the Instana clients."""
|
|
119
|
+
# Get credentials from environment variables
|
|
120
|
+
token, base_url = get_instana_credentials()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# For lifespan, we'll create all clients since we don't have access to command line args here
|
|
124
|
+
state = create_clients(token, base_url, "all")
|
|
125
|
+
|
|
126
|
+
yield state
|
|
127
|
+
except Exception:
|
|
128
|
+
logger.error("Error during lifespan", exc_info=True)
|
|
129
|
+
|
|
130
|
+
# Yield empty state if client creation failed
|
|
131
|
+
yield MCPState()
|
|
132
|
+
|
|
133
|
+
def create_app(token: str, base_url: str, port: int = 8000, enabled_categories: str = "all") -> tuple[FastMCP, int]:
|
|
134
|
+
"""Create and configure the MCP server with the given credentials."""
|
|
135
|
+
try:
|
|
136
|
+
server = FastMCP(name="Instana MCP Server", host="0.0.0.0", port=port)
|
|
137
|
+
|
|
138
|
+
# Only create and register enabled clients/tools
|
|
139
|
+
clients_state = create_clients(token, base_url, enabled_categories)
|
|
140
|
+
|
|
141
|
+
tools_registered = 0
|
|
142
|
+
for tool_name, _tool_func in MCP_TOOLS.items():
|
|
143
|
+
try:
|
|
144
|
+
client_attr_names = [field.name for field in fields(MCPState)]
|
|
145
|
+
for attr_name in client_attr_names:
|
|
146
|
+
client = getattr(clients_state, attr_name, None)
|
|
147
|
+
if client and hasattr(client, tool_name):
|
|
148
|
+
bound_method = getattr(client, tool_name)
|
|
149
|
+
server.tool()(bound_method)
|
|
150
|
+
tools_registered += 1
|
|
151
|
+
break
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"Failed to register tool {tool_name}: {e}", exc_info=True)
|
|
154
|
+
|
|
155
|
+
# Register prompts from the prompt registry
|
|
156
|
+
register_prompts(server)
|
|
157
|
+
|
|
158
|
+
return server, tools_registered
|
|
159
|
+
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.error("Error creating app", exc_info=True)
|
|
162
|
+
fallback_server = FastMCP("Instana Tools")
|
|
163
|
+
return fallback_server, 0 # Return a tuple with 0 tools registered
|
|
164
|
+
|
|
165
|
+
async def execute_tool(tool_name: str, arguments: dict, clients_state) -> str:
|
|
166
|
+
"""Execute a tool and return result"""
|
|
167
|
+
try:
|
|
168
|
+
# Get all field names from MCPState dataclass
|
|
169
|
+
client_attr_names = [field.name for field in fields(MCPState)]
|
|
170
|
+
|
|
171
|
+
for attr_name in client_attr_names:
|
|
172
|
+
client = getattr(clients_state, attr_name, None)
|
|
173
|
+
if client and hasattr(client, tool_name):
|
|
174
|
+
method = getattr(client, tool_name)
|
|
175
|
+
result = await method(**arguments)
|
|
176
|
+
return str(result)
|
|
177
|
+
|
|
178
|
+
return f"Tool {tool_name} not found"
|
|
179
|
+
except Exception as e:
|
|
180
|
+
return f"Error executing tool {tool_name}: {e!s}"
|
|
181
|
+
|
|
182
|
+
def get_client_categories():
|
|
183
|
+
"""Get client categories with lazy imports to avoid circular dependencies"""
|
|
184
|
+
try:
|
|
185
|
+
from src.application.application_alert_config import ApplicationAlertMCPTools
|
|
186
|
+
from src.application.application_analyze import ApplicationAnalyzeMCPTools
|
|
187
|
+
from src.application.application_catalog import ApplicationCatalogMCPTools
|
|
188
|
+
from src.application.application_metrics import ApplicationMetricsMCPTools
|
|
189
|
+
from src.application.application_resources import ApplicationResourcesMCPTools
|
|
190
|
+
from src.application.application_settings import ApplicationSettingsMCPTools
|
|
191
|
+
from src.application.application_topology import ApplicationTopologyMCPTools
|
|
192
|
+
from src.event.events_tools import AgentMonitoringEventsMCPTools
|
|
193
|
+
from src.infrastructure.infrastructure_analyze import (
|
|
194
|
+
InfrastructureAnalyzeMCPTools,
|
|
195
|
+
)
|
|
196
|
+
from src.infrastructure.infrastructure_catalog import (
|
|
197
|
+
InfrastructureCatalogMCPTools,
|
|
198
|
+
)
|
|
199
|
+
from src.infrastructure.infrastructure_metrics import (
|
|
200
|
+
InfrastructureMetricsMCPTools,
|
|
201
|
+
)
|
|
202
|
+
from src.infrastructure.infrastructure_resources import (
|
|
203
|
+
InfrastructureResourcesMCPTools,
|
|
204
|
+
)
|
|
205
|
+
from src.infrastructure.infrastructure_topology import (
|
|
206
|
+
InfrastructureTopologyMCPTools,
|
|
207
|
+
)
|
|
208
|
+
except ImportError as e:
|
|
209
|
+
logger.warning(f"Could not import client classes: {e}")
|
|
210
|
+
return {}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"infra": [
|
|
214
|
+
('infra_client', InfrastructureResourcesMCPTools),
|
|
215
|
+
('infra_catalog_client', InfrastructureCatalogMCPTools),
|
|
216
|
+
('infra_topo_client', InfrastructureTopologyMCPTools),
|
|
217
|
+
('infra_analyze_client', InfrastructureAnalyzeMCPTools),
|
|
218
|
+
('infra_metrics_client', InfrastructureMetricsMCPTools),
|
|
219
|
+
],
|
|
220
|
+
"app": [
|
|
221
|
+
('app_resource_client', ApplicationResourcesMCPTools),
|
|
222
|
+
('app_metrics_client', ApplicationMetricsMCPTools),
|
|
223
|
+
('app_alert_client', ApplicationAlertMCPTools),
|
|
224
|
+
('app_catalog_client', ApplicationCatalogMCPTools),
|
|
225
|
+
('app_topology_client', ApplicationTopologyMCPTools),
|
|
226
|
+
('app_analyze_client', ApplicationAnalyzeMCPTools),
|
|
227
|
+
('app_settings_client', ApplicationSettingsMCPTools),
|
|
228
|
+
],
|
|
229
|
+
"events": [
|
|
230
|
+
('events_client', AgentMonitoringEventsMCPTools),
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def get_enabled_client_configs(enabled_categories: str):
|
|
235
|
+
"""Get client configurations based on enabled categories"""
|
|
236
|
+
# Get client categories with lazy imports
|
|
237
|
+
client_categories = get_client_categories()
|
|
238
|
+
|
|
239
|
+
if enabled_categories.lower() == "all":
|
|
240
|
+
all_configs = []
|
|
241
|
+
for category_clients in client_categories.values():
|
|
242
|
+
all_configs.extend(category_clients)
|
|
243
|
+
return all_configs
|
|
244
|
+
categories = [cat.strip() for cat in enabled_categories.split(",")]
|
|
245
|
+
enabled_configs = []
|
|
246
|
+
for category in categories:
|
|
247
|
+
if category in client_categories:
|
|
248
|
+
enabled_configs.extend(client_categories[category])
|
|
249
|
+
else:
|
|
250
|
+
logger.warning(f"Unknown category '{category}'")
|
|
251
|
+
return enabled_configs
|
|
252
|
+
|
|
253
|
+
def main():
|
|
254
|
+
"""Main entry point for the MCP server."""
|
|
255
|
+
try:
|
|
256
|
+
# Create and configure the MCP server
|
|
257
|
+
parser = argparse.ArgumentParser(description="Instana MCP Server", add_help=False)
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
"-h", "--help",
|
|
260
|
+
action="store_true",
|
|
261
|
+
dest="help",
|
|
262
|
+
help="show this help message and exit"
|
|
263
|
+
)
|
|
264
|
+
parser.add_argument(
|
|
265
|
+
"--transport",
|
|
266
|
+
type=str,
|
|
267
|
+
choices=["streamable-http","stdio"],
|
|
268
|
+
metavar='<mode>',
|
|
269
|
+
help="Transport mode. Choose from: streamable-http, stdio."
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"--log-level",
|
|
273
|
+
type=str,
|
|
274
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
275
|
+
default="INFO",
|
|
276
|
+
help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
|
|
277
|
+
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"--debug",
|
|
280
|
+
action="store_true",
|
|
281
|
+
help="Enable debug mode with additional logging (shortcut for --log-level DEBUG)"
|
|
282
|
+
)
|
|
283
|
+
parser.add_argument(
|
|
284
|
+
"--tools",
|
|
285
|
+
type=str,
|
|
286
|
+
metavar='<categories>',
|
|
287
|
+
help="Comma-separated list of tool categories to enable (--tools infra,app,events). If not provided, all tools are enabled."
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--list-tools",
|
|
291
|
+
action="store_true",
|
|
292
|
+
help="List all available tool categories and exit."
|
|
293
|
+
)
|
|
294
|
+
parser.add_argument(
|
|
295
|
+
"--port",
|
|
296
|
+
type=int,
|
|
297
|
+
default=8000,
|
|
298
|
+
help="Port to listen on (default: 8000)"
|
|
299
|
+
)
|
|
300
|
+
# Check for help arguments before parsing
|
|
301
|
+
if len(sys.argv) > 1 and any(arg in ['-h','--h','--help','-help'] for arg in sys.argv[1:]):
|
|
302
|
+
# Check if help is combined with other arguments
|
|
303
|
+
help_args = ['-h','--h','--help','-help']
|
|
304
|
+
other_args = [arg for arg in sys.argv[1:] if arg not in help_args]
|
|
305
|
+
|
|
306
|
+
if other_args:
|
|
307
|
+
logger.error("Argument -h/--h/--help/-help: not allowed with other arguments")
|
|
308
|
+
sys.exit(2)
|
|
309
|
+
|
|
310
|
+
# Show help and exit
|
|
311
|
+
try:
|
|
312
|
+
logger.info("Available options:")
|
|
313
|
+
for action in parser._actions:
|
|
314
|
+
# Only print options that start with '--' and have a help string
|
|
315
|
+
if any(opt.startswith('--') for opt in action.option_strings) and action.help:
|
|
316
|
+
# Find the first long option
|
|
317
|
+
long_opt = next((opt for opt in action.option_strings if opt.startswith('--')), None)
|
|
318
|
+
metavar = action.metavar or ''
|
|
319
|
+
opt_str = f"{long_opt} {metavar}".strip()
|
|
320
|
+
logger.info(f"{opt_str:<24} {action.help}")
|
|
321
|
+
sys.exit(0)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error(f"Error displaying help: {e}")
|
|
324
|
+
sys.exit(0) # Still exit with 0 for help
|
|
325
|
+
|
|
326
|
+
args = parser.parse_args()
|
|
327
|
+
|
|
328
|
+
# Set log level based on command line arguments
|
|
329
|
+
if args.debug:
|
|
330
|
+
set_log_level("DEBUG")
|
|
331
|
+
else:
|
|
332
|
+
set_log_level(args.log_level)
|
|
333
|
+
|
|
334
|
+
all_categories = {"infra", "app", "events"}
|
|
335
|
+
|
|
336
|
+
# Handle --list-tools option
|
|
337
|
+
if args.list_tools:
|
|
338
|
+
logger.info("Available tool categories:")
|
|
339
|
+
client_categories = get_client_categories()
|
|
340
|
+
for category, tools in client_categories.items():
|
|
341
|
+
tool_names = [cls.__name__ for _, cls in tools]
|
|
342
|
+
logger.info(f" {category}: {len(tool_names)} tools")
|
|
343
|
+
for tool_name in tool_names:
|
|
344
|
+
logger.info(f" - {tool_name}")
|
|
345
|
+
sys.exit(0)
|
|
346
|
+
|
|
347
|
+
# By default, enable all categories
|
|
348
|
+
enabled = set(all_categories)
|
|
349
|
+
invalid = set()
|
|
350
|
+
|
|
351
|
+
# Enable only specified categories if --tools is provided
|
|
352
|
+
if args.tools:
|
|
353
|
+
specified_tools = {cat.strip() for cat in args.tools.split(",")}
|
|
354
|
+
invalid = specified_tools - all_categories
|
|
355
|
+
enabled = specified_tools & all_categories
|
|
356
|
+
|
|
357
|
+
# If no valid tools specified, default to all
|
|
358
|
+
if not enabled:
|
|
359
|
+
enabled = set(all_categories)
|
|
360
|
+
|
|
361
|
+
if invalid:
|
|
362
|
+
logger.error(f"Error: Unknown category/categories: {', '.join(invalid)}. Available categories: infra, app, events")
|
|
363
|
+
sys.exit(2)
|
|
364
|
+
|
|
365
|
+
# Print enabled tools for user information
|
|
366
|
+
enabled_tool_classes = []
|
|
367
|
+
client_categories = get_client_categories()
|
|
368
|
+
|
|
369
|
+
# Log enabled categories and tools
|
|
370
|
+
logger.info(f"Enabled tool categories: {', '.join(enabled)}")
|
|
371
|
+
|
|
372
|
+
for category in enabled:
|
|
373
|
+
if category in client_categories:
|
|
374
|
+
category_tools = [cls.__name__ for _, cls in client_categories[category]]
|
|
375
|
+
enabled_tool_classes.extend(category_tools)
|
|
376
|
+
logger.info(f" - {category}: {len(category_tools)} tools")
|
|
377
|
+
for tool_name in category_tools:
|
|
378
|
+
logger.info(f" * {tool_name}")
|
|
379
|
+
|
|
380
|
+
if enabled_tool_classes:
|
|
381
|
+
logger.info(
|
|
382
|
+
f"Total enabled tools: {len(enabled_tool_classes)}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Get credentials from environment variables for stdio mode
|
|
386
|
+
INSTANA_API_TOKEN, INSTANA_BASE_URL = get_instana_credentials()
|
|
387
|
+
|
|
388
|
+
if args.transport == "stdio" or args.transport is None:
|
|
389
|
+
if not validate_credentials(INSTANA_API_TOKEN, INSTANA_BASE_URL):
|
|
390
|
+
logger.error("Error: Instana credentials are required for stdio mode but not provided. Please set INSTANA_API_TOKEN and INSTANA_BASE_URL environment variables.")
|
|
391
|
+
sys.exit(1)
|
|
392
|
+
|
|
393
|
+
# Create and configure the MCP server
|
|
394
|
+
try:
|
|
395
|
+
enabled_categories = ",".join(enabled)
|
|
396
|
+
app, registered_tool_count = create_app(INSTANA_API_TOKEN, INSTANA_BASE_URL, args.port, enabled_categories)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
print(f"Failed to create MCP server: {e}", file=sys.stderr)
|
|
399
|
+
sys.exit(1)
|
|
400
|
+
|
|
401
|
+
# Run the server with the appropriate transport
|
|
402
|
+
if args.transport == "streamable-http":
|
|
403
|
+
if args.debug:
|
|
404
|
+
logger.info(f"FastMCP instance: {app}")
|
|
405
|
+
logger.info(f"Registered tools: {registered_tool_count}")
|
|
406
|
+
try:
|
|
407
|
+
app.run(transport="streamable-http")
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.error(f"Failed to start HTTP server: {e}")
|
|
410
|
+
if args.debug:
|
|
411
|
+
logger.error("HTTP server error details", exc_info=True)
|
|
412
|
+
sys.exit(1)
|
|
413
|
+
else:
|
|
414
|
+
logger.info("Starting stdio transport")
|
|
415
|
+
try:
|
|
416
|
+
app.run(transport="stdio")
|
|
417
|
+
except AttributeError as e:
|
|
418
|
+
# Handle the case where sys.stdout is a StringIO object (in tests)
|
|
419
|
+
if "'_io.StringIO' object has no attribute 'buffer'" in str(e):
|
|
420
|
+
logger.info("Running in test mode, skipping stdio server")
|
|
421
|
+
else:
|
|
422
|
+
raise
|
|
423
|
+
|
|
424
|
+
except KeyboardInterrupt:
|
|
425
|
+
logger.info("Server stopped by user")
|
|
426
|
+
sys.exit(0)
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logger.error(f"Server error: {e}", exc_info=True)
|
|
429
|
+
sys.exit(1)
|
|
430
|
+
|
|
431
|
+
if __name__ == "__main__":
|
|
432
|
+
try:
|
|
433
|
+
main()
|
|
434
|
+
except Exception:
|
|
435
|
+
logger.error("Unhandled exception in main", exc_info=True)
|
|
436
|
+
sys.exit(1)
|