mcp-instana 0.1.0__py3-none-any.whl → 0.2.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.
- mcp_instana-0.2.0.dist-info/METADATA +1229 -0
- mcp_instana-0.2.0.dist-info/RECORD +59 -0
- {mcp_instana-0.1.0.dist-info → mcp_instana-0.2.0.dist-info}/WHEEL +1 -1
- mcp_instana-0.2.0.dist-info/entry_points.txt +4 -0
- mcp_instana-0.1.0.dist-info/LICENSE → mcp_instana-0.2.0.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 +628 -0
- src/application/application_catalog.py +155 -0
- src/application/application_global_alert_config.py +653 -0
- src/{client/application_metrics_mcp_tools.py → application/application_metrics.py} +113 -131
- src/{client/application_resources_mcp_tools.py → application/application_resources.py} +131 -151
- src/application/application_settings.py +1731 -0
- src/application/application_topology.py +111 -0
- src/automation/action_catalog.py +416 -0
- src/automation/action_history.py +338 -0
- src/core/__init__.py +1 -0
- src/core/server.py +586 -0
- src/core/utils.py +213 -0
- src/event/__init__.py +1 -0
- src/event/events_tools.py +850 -0
- src/infrastructure/__init__.py +1 -0
- src/{client/infrastructure_analyze_mcp_tools.py → infrastructure/infrastructure_analyze.py} +207 -206
- src/{client/infrastructure_catalog_mcp_tools.py → infrastructure/infrastructure_catalog.py} +197 -265
- src/infrastructure/infrastructure_metrics.py +171 -0
- src/{client/infrastructure_resources_mcp_tools.py → infrastructure/infrastructure_resources.py} +198 -227
- src/{client/infrastructure_topology_mcp_tools.py → infrastructure/infrastructure_topology.py} +110 -109
- src/log/__init__.py +1 -0
- src/log/log_alert_configuration.py +331 -0
- src/prompts/__init__.py +16 -0
- src/prompts/application/__init__.py +1 -0
- src/prompts/application/application_alerts.py +54 -0
- src/prompts/application/application_catalog.py +26 -0
- src/prompts/application/application_metrics.py +57 -0
- src/prompts/application/application_resources.py +26 -0
- src/prompts/application/application_settings.py +75 -0
- src/prompts/application/application_topology.py +30 -0
- src/prompts/events/__init__.py +1 -0
- src/prompts/events/events_tools.py +161 -0
- src/prompts/infrastructure/infrastructure_analyze.py +72 -0
- src/prompts/infrastructure/infrastructure_catalog.py +53 -0
- src/prompts/infrastructure/infrastructure_metrics.py +45 -0
- src/prompts/infrastructure/infrastructure_resources.py +74 -0
- src/prompts/infrastructure/infrastructure_topology.py +38 -0
- src/prompts/settings/__init__.py +0 -0
- src/prompts/settings/custom_dashboard.py +157 -0
- src/prompts/website/__init__.py +1 -0
- src/prompts/website/website_analyze.py +35 -0
- src/prompts/website/website_catalog.py +40 -0
- src/prompts/website/website_configuration.py +105 -0
- src/prompts/website/website_metrics.py +34 -0
- src/settings/__init__.py +1 -0
- src/settings/custom_dashboard_tools.py +417 -0
- src/website/__init__.py +0 -0
- src/website/website_analyze.py +433 -0
- src/website/website_catalog.py +171 -0
- src/website/website_configuration.py +770 -0
- src/website/website_metrics.py +241 -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/events_mcp_tools.py +0 -531
- 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
src/core/server.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
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 import PROMPT_REGISTRY
|
|
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
|
+
app_global_alert_client: Any = None
|
|
81
|
+
website_metrics_client: Any = None
|
|
82
|
+
website_catalog_client: Any = None
|
|
83
|
+
website_analyze_client: Any = None
|
|
84
|
+
website_configuration_client: Any = None
|
|
85
|
+
|
|
86
|
+
# Global variables to store credentials for lifespan
|
|
87
|
+
_global_token = None
|
|
88
|
+
_global_base_url = None
|
|
89
|
+
|
|
90
|
+
def get_instana_credentials():
|
|
91
|
+
"""Get Instana credentials from environment variables for stdio mode."""
|
|
92
|
+
# For stdio mode, use INSTANA_API_TOKEN and INSTANA_BASE_URL
|
|
93
|
+
token = (os.getenv("INSTANA_API_TOKEN") or "")
|
|
94
|
+
base_url = (os.getenv("INSTANA_BASE_URL") or "")
|
|
95
|
+
|
|
96
|
+
return token, base_url
|
|
97
|
+
|
|
98
|
+
def validate_credentials(token: str, base_url: str) -> bool:
|
|
99
|
+
"""Validate that Instana credentials are provided for stdio mode."""
|
|
100
|
+
# For stdio mode, validate INSTANA_API_TOKEN and INSTANA_BASE_URL
|
|
101
|
+
return not (not token or not base_url)
|
|
102
|
+
|
|
103
|
+
def create_clients(token: str, base_url: str, enabled_categories: str = "all") -> MCPState:
|
|
104
|
+
"""Create only the enabled Instana clients"""
|
|
105
|
+
state = MCPState()
|
|
106
|
+
|
|
107
|
+
# Get enabled client configurations
|
|
108
|
+
enabled_client_configs = get_enabled_client_configs(enabled_categories)
|
|
109
|
+
|
|
110
|
+
for attr_name, client_class in enabled_client_configs:
|
|
111
|
+
try:
|
|
112
|
+
client = client_class(read_token=token, base_url=base_url)
|
|
113
|
+
setattr(state, attr_name, client)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"Failed to create {attr_name}: {e}", exc_info=True)
|
|
116
|
+
setattr(state, attr_name, None)
|
|
117
|
+
|
|
118
|
+
return state
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@asynccontextmanager
|
|
122
|
+
async def lifespan(server: FastMCP) -> AsyncIterator[MCPState]:
|
|
123
|
+
"""Set up and tear down the Instana clients."""
|
|
124
|
+
# Get credentials from environment variables
|
|
125
|
+
token, base_url = get_instana_credentials()
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# For lifespan, we'll create all clients since we don't have access to command line args here
|
|
129
|
+
state = create_clients(token, base_url, "all")
|
|
130
|
+
|
|
131
|
+
yield state
|
|
132
|
+
except Exception:
|
|
133
|
+
logger.error("Error during lifespan", exc_info=True)
|
|
134
|
+
|
|
135
|
+
# Yield empty state if client creation failed
|
|
136
|
+
yield MCPState()
|
|
137
|
+
|
|
138
|
+
def create_app(token: str, base_url: str, port: int = int(os.getenv("PORT", "8080")), enabled_categories: str = "all") -> tuple[FastMCP, int]:
|
|
139
|
+
"""Create and configure the MCP server with the given credentials."""
|
|
140
|
+
try:
|
|
141
|
+
server = FastMCP(name="Instana MCP Server", host="0.0.0.0", port=port)
|
|
142
|
+
|
|
143
|
+
# Only create and register enabled clients/tools
|
|
144
|
+
clients_state = create_clients(token, base_url, enabled_categories)
|
|
145
|
+
|
|
146
|
+
tools_registered = 0
|
|
147
|
+
for tool_name, _tool_func in MCP_TOOLS.items():
|
|
148
|
+
try:
|
|
149
|
+
client_attr_names = [field.name for field in fields(MCPState)]
|
|
150
|
+
for attr_name in client_attr_names:
|
|
151
|
+
client = getattr(clients_state, attr_name, None)
|
|
152
|
+
if client and hasattr(client, tool_name):
|
|
153
|
+
bound_method = getattr(client, tool_name)
|
|
154
|
+
server.tool()(bound_method)
|
|
155
|
+
tools_registered += 1
|
|
156
|
+
break
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Failed to register tool {tool_name}: {e}", exc_info=True)
|
|
159
|
+
|
|
160
|
+
# Register prompts from the prompt registry
|
|
161
|
+
# Get enabled prompt categories - use the same categories as tools
|
|
162
|
+
prompt_categories = get_prompt_categories()
|
|
163
|
+
|
|
164
|
+
# Use the same categories for prompts as for tools
|
|
165
|
+
enabled_prompt_categories = []
|
|
166
|
+
if enabled_categories.lower() == "all" or not enabled_categories:
|
|
167
|
+
enabled_prompt_categories = list(prompt_categories.keys())
|
|
168
|
+
logger.info("Enabling all prompt categories")
|
|
169
|
+
else:
|
|
170
|
+
enabled_prompt_categories = [cat.strip() for cat in enabled_categories.split(",") if cat.strip() in prompt_categories]
|
|
171
|
+
logger.info(f"Enabling prompt categories: {', '.join(enabled_prompt_categories)}")
|
|
172
|
+
|
|
173
|
+
# Register prompts to the server
|
|
174
|
+
logger.info("Registering prompts by category:")
|
|
175
|
+
registered_prompts = set()
|
|
176
|
+
|
|
177
|
+
for category, prompt_groups in prompt_categories.items():
|
|
178
|
+
if category in enabled_prompt_categories:
|
|
179
|
+
logger.info(f" - {category}: {len(prompt_groups)} prompt groups")
|
|
180
|
+
|
|
181
|
+
for group_name, prompts in prompt_groups:
|
|
182
|
+
prompt_count = len(prompts)
|
|
183
|
+
logger.info(f" - {group_name}: {prompt_count} prompts")
|
|
184
|
+
|
|
185
|
+
for prompt_name, prompt_func in prompts:
|
|
186
|
+
server.add_prompt(prompt_func)
|
|
187
|
+
registered_prompts.add(prompt_name)
|
|
188
|
+
logger.debug(f" * Registered prompt: {prompt_name}")
|
|
189
|
+
else:
|
|
190
|
+
logger.info(f" - {category}: DISABLED")
|
|
191
|
+
|
|
192
|
+
# Register any remaining prompts that might not be in categories
|
|
193
|
+
uncategorized_count = 0
|
|
194
|
+
|
|
195
|
+
# Just log the count of remaining prompts
|
|
196
|
+
remaining_prompts = len(PROMPT_REGISTRY) - len(registered_prompts)
|
|
197
|
+
if remaining_prompts > 0:
|
|
198
|
+
logger.info(f" - uncategorized: {remaining_prompts} prompts (not registered)")
|
|
199
|
+
|
|
200
|
+
if uncategorized_count > 0:
|
|
201
|
+
logger.info(f" - uncategorized: {uncategorized_count} prompts")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
return server, tools_registered
|
|
205
|
+
|
|
206
|
+
except Exception:
|
|
207
|
+
logger.error("Error creating app", exc_info=True)
|
|
208
|
+
fallback_server = FastMCP("Instana Tools")
|
|
209
|
+
return fallback_server, 0 # Return a tuple with 0 tools registered
|
|
210
|
+
|
|
211
|
+
async def execute_tool(tool_name: str, arguments: dict, clients_state) -> str:
|
|
212
|
+
"""Execute a tool and return result"""
|
|
213
|
+
try:
|
|
214
|
+
# Get all field names from MCPState dataclass
|
|
215
|
+
client_attr_names = [field.name for field in fields(MCPState)]
|
|
216
|
+
|
|
217
|
+
for attr_name in client_attr_names:
|
|
218
|
+
client = getattr(clients_state, attr_name, None)
|
|
219
|
+
if client and hasattr(client, tool_name):
|
|
220
|
+
method = getattr(client, tool_name)
|
|
221
|
+
result = await method(**arguments)
|
|
222
|
+
return str(result)
|
|
223
|
+
|
|
224
|
+
return f"Tool {tool_name} not found"
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return f"Error executing tool {tool_name}: {e!s}"
|
|
227
|
+
|
|
228
|
+
def get_client_categories():
|
|
229
|
+
"""Get client categories with lazy imports to avoid circular dependencies"""
|
|
230
|
+
try:
|
|
231
|
+
from src.application.application_alert_config import ApplicationAlertMCPTools
|
|
232
|
+
from src.application.application_analyze import ApplicationAnalyzeMCPTools
|
|
233
|
+
from src.application.application_catalog import ApplicationCatalogMCPTools
|
|
234
|
+
from src.application.application_global_alert_config import (
|
|
235
|
+
ApplicationGlobalAlertMCPTools,
|
|
236
|
+
)
|
|
237
|
+
from src.application.application_metrics import ApplicationMetricsMCPTools
|
|
238
|
+
from src.application.application_resources import ApplicationResourcesMCPTools
|
|
239
|
+
from src.application.application_settings import ApplicationSettingsMCPTools
|
|
240
|
+
from src.application.application_topology import ApplicationTopologyMCPTools
|
|
241
|
+
from src.automation.action_catalog import ActionCatalogMCPTools
|
|
242
|
+
from src.automation.action_history import ActionHistoryMCPTools
|
|
243
|
+
from src.event.events_tools import AgentMonitoringEventsMCPTools
|
|
244
|
+
from src.infrastructure.infrastructure_analyze import (
|
|
245
|
+
InfrastructureAnalyzeMCPTools,
|
|
246
|
+
)
|
|
247
|
+
from src.infrastructure.infrastructure_catalog import (
|
|
248
|
+
InfrastructureCatalogMCPTools,
|
|
249
|
+
)
|
|
250
|
+
from src.infrastructure.infrastructure_metrics import (
|
|
251
|
+
InfrastructureMetricsMCPTools,
|
|
252
|
+
)
|
|
253
|
+
from src.infrastructure.infrastructure_resources import (
|
|
254
|
+
InfrastructureResourcesMCPTools,
|
|
255
|
+
)
|
|
256
|
+
from src.infrastructure.infrastructure_topology import (
|
|
257
|
+
InfrastructureTopologyMCPTools,
|
|
258
|
+
)
|
|
259
|
+
from src.settings.custom_dashboard_tools import CustomDashboardMCPTools
|
|
260
|
+
from src.website.website_analyze import WebsiteAnalyzeMCPTools
|
|
261
|
+
from src.website.website_catalog import WebsiteCatalogMCPTools
|
|
262
|
+
from src.website.website_configuration import WebsiteConfigurationMCPTools
|
|
263
|
+
from src.website.website_metrics import WebsiteMetricsMCPTools
|
|
264
|
+
except ImportError as e:
|
|
265
|
+
logger.warning(f"Could not import client classes: {e}")
|
|
266
|
+
return {}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"infra": [
|
|
270
|
+
('infra_client', InfrastructureResourcesMCPTools),
|
|
271
|
+
('infra_catalog_client', InfrastructureCatalogMCPTools),
|
|
272
|
+
('infra_topo_client', InfrastructureTopologyMCPTools),
|
|
273
|
+
('infra_analyze_client', InfrastructureAnalyzeMCPTools),
|
|
274
|
+
('infra_metrics_client', InfrastructureMetricsMCPTools),
|
|
275
|
+
],
|
|
276
|
+
"app": [
|
|
277
|
+
('app_resource_client', ApplicationResourcesMCPTools),
|
|
278
|
+
('app_metrics_client', ApplicationMetricsMCPTools),
|
|
279
|
+
('app_alert_client', ApplicationAlertMCPTools),
|
|
280
|
+
('app_catalog_client', ApplicationCatalogMCPTools),
|
|
281
|
+
('app_topology_client', ApplicationTopologyMCPTools),
|
|
282
|
+
('app_analyze_client', ApplicationAnalyzeMCPTools),
|
|
283
|
+
('app_settings_client', ApplicationSettingsMCPTools),
|
|
284
|
+
('app_global_alert_client', ApplicationGlobalAlertMCPTools),
|
|
285
|
+
],
|
|
286
|
+
"events": [
|
|
287
|
+
('events_client', AgentMonitoringEventsMCPTools),
|
|
288
|
+
],
|
|
289
|
+
"automation": [
|
|
290
|
+
('action_catalog_client', ActionCatalogMCPTools),
|
|
291
|
+
('action_history_client', ActionHistoryMCPTools),
|
|
292
|
+
],
|
|
293
|
+
"website": [
|
|
294
|
+
('website_metrics_client', WebsiteMetricsMCPTools),
|
|
295
|
+
('website_catalog_client', WebsiteCatalogMCPTools),
|
|
296
|
+
('website_analyze_client', WebsiteAnalyzeMCPTools),
|
|
297
|
+
('website_configuration_client', WebsiteConfigurationMCPTools),
|
|
298
|
+
],
|
|
299
|
+
"settings": [
|
|
300
|
+
('custom_dashboard_client', CustomDashboardMCPTools),
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
def get_prompt_categories():
|
|
305
|
+
"""Get prompt categories organized by functionality"""
|
|
306
|
+
# Import the class-based prompts
|
|
307
|
+
from src.prompts.application.application_alerts import ApplicationAlertsPrompts
|
|
308
|
+
from src.prompts.application.application_catalog import ApplicationCatalogPrompts
|
|
309
|
+
from src.prompts.application.application_metrics import ApplicationMetricsPrompts
|
|
310
|
+
from src.prompts.application.application_resources import (
|
|
311
|
+
ApplicationResourcesPrompts,
|
|
312
|
+
)
|
|
313
|
+
from src.prompts.application.application_settings import ApplicationSettingsPrompts
|
|
314
|
+
from src.prompts.application.application_topology import ApplicationTopologyPrompts
|
|
315
|
+
from src.prompts.infrastructure.infrastructure_analyze import (
|
|
316
|
+
InfrastructureAnalyzePrompts,
|
|
317
|
+
)
|
|
318
|
+
from src.prompts.infrastructure.infrastructure_catalog import (
|
|
319
|
+
InfrastructureCatalogPrompts,
|
|
320
|
+
)
|
|
321
|
+
from src.prompts.infrastructure.infrastructure_metrics import (
|
|
322
|
+
InfrastructureMetricsPrompts,
|
|
323
|
+
)
|
|
324
|
+
from src.prompts.infrastructure.infrastructure_resources import (
|
|
325
|
+
InfrastructureResourcesPrompts,
|
|
326
|
+
)
|
|
327
|
+
from src.prompts.infrastructure.infrastructure_topology import (
|
|
328
|
+
InfrastructureTopologyPrompts,
|
|
329
|
+
)
|
|
330
|
+
from src.prompts.settings.custom_dashboard import CustomDashboardPrompts
|
|
331
|
+
from src.prompts.website.website_analyze import WebsiteAnalyzePrompts
|
|
332
|
+
from src.prompts.website.website_catalog import WebsiteCatalogPrompts
|
|
333
|
+
from src.prompts.website.website_configuration import WebsiteConfigurationPrompts
|
|
334
|
+
from src.prompts.website.website_metrics import WebsiteMetricsPrompts
|
|
335
|
+
|
|
336
|
+
# Use the get_prompts method to get all prompts from the classes
|
|
337
|
+
infra_analyze_prompts = InfrastructureAnalyzePrompts.get_prompts()
|
|
338
|
+
infra_metrics_prompts = InfrastructureMetricsPrompts.get_prompts()
|
|
339
|
+
infra_catalog_prompts = InfrastructureCatalogPrompts.get_prompts()
|
|
340
|
+
infra_topology_prompts = InfrastructureTopologyPrompts.get_prompts()
|
|
341
|
+
infra_resources_prompts = InfrastructureResourcesPrompts.get_prompts()
|
|
342
|
+
app_resources_prompts = ApplicationResourcesPrompts.get_prompts()
|
|
343
|
+
app_metrics_prompts = ApplicationMetricsPrompts.get_prompts()
|
|
344
|
+
app_catalog_prompts = ApplicationCatalogPrompts.get_prompts()
|
|
345
|
+
app_settings_prompts = ApplicationSettingsPrompts.get_prompts()
|
|
346
|
+
app_topology_prompts = ApplicationTopologyPrompts.get_prompts()
|
|
347
|
+
app_alert_prompts = ApplicationAlertsPrompts.get_prompts()
|
|
348
|
+
website_metrics_prompts = WebsiteMetricsPrompts.get_prompts()
|
|
349
|
+
website_catalog_prompts = WebsiteCatalogPrompts.get_prompts()
|
|
350
|
+
website_analyze_prompts = WebsiteAnalyzePrompts.get_prompts()
|
|
351
|
+
website_configuration_prompts = WebsiteConfigurationPrompts.get_prompts()
|
|
352
|
+
custom_dashboard_prompts = CustomDashboardPrompts.get_prompts()
|
|
353
|
+
|
|
354
|
+
# Return the categories with their prompt groups
|
|
355
|
+
return {
|
|
356
|
+
"infra": [
|
|
357
|
+
('infra_resources_prompts', infra_resources_prompts),
|
|
358
|
+
('infra_catalog_prompts', infra_catalog_prompts),
|
|
359
|
+
('infra_topology_prompts', infra_topology_prompts),
|
|
360
|
+
('infra_analyze_prompts', infra_analyze_prompts),
|
|
361
|
+
('infra_metrics_prompts', infra_metrics_prompts),
|
|
362
|
+
],
|
|
363
|
+
"app": [
|
|
364
|
+
('app_resources_prompts', app_resources_prompts),
|
|
365
|
+
('app_metrics_prompts', app_metrics_prompts),
|
|
366
|
+
('app_catalog_prompts', app_catalog_prompts),
|
|
367
|
+
('app_settings_prompts', app_settings_prompts),
|
|
368
|
+
('app_topology_prompts', app_topology_prompts),
|
|
369
|
+
('app_alert_prompts', app_alert_prompts),
|
|
370
|
+
],
|
|
371
|
+
"website": [
|
|
372
|
+
('website_metrics_prompts', website_metrics_prompts),
|
|
373
|
+
('website_catalog_prompts', website_catalog_prompts),
|
|
374
|
+
('website_analyze_prompts', website_analyze_prompts),
|
|
375
|
+
('website_configuration_prompts', website_configuration_prompts),
|
|
376
|
+
],
|
|
377
|
+
"settings": [
|
|
378
|
+
('custom_dashboard_prompts', custom_dashboard_prompts),
|
|
379
|
+
],
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
def get_enabled_client_configs(enabled_categories: str):
|
|
383
|
+
"""Get client configurations based on enabled categories"""
|
|
384
|
+
# Get client categories with lazy imports
|
|
385
|
+
client_categories = get_client_categories()
|
|
386
|
+
|
|
387
|
+
if not enabled_categories or enabled_categories.lower() == "all":
|
|
388
|
+
all_configs = []
|
|
389
|
+
for category_clients in client_categories.values():
|
|
390
|
+
all_configs.extend(category_clients)
|
|
391
|
+
return all_configs
|
|
392
|
+
categories = [cat.strip() for cat in enabled_categories.split(",")]
|
|
393
|
+
enabled_configs = []
|
|
394
|
+
for category in categories:
|
|
395
|
+
if category in client_categories:
|
|
396
|
+
enabled_configs.extend(client_categories[category])
|
|
397
|
+
else:
|
|
398
|
+
logger.warning(f"Unknown category '{category}'")
|
|
399
|
+
return enabled_configs
|
|
400
|
+
|
|
401
|
+
def main():
|
|
402
|
+
"""Main entry point for the MCP server."""
|
|
403
|
+
try:
|
|
404
|
+
# Create and configure the MCP server
|
|
405
|
+
parser = argparse.ArgumentParser(description="Instana MCP Server", add_help=False)
|
|
406
|
+
parser.add_argument(
|
|
407
|
+
"-h", "--help",
|
|
408
|
+
action="store_true",
|
|
409
|
+
dest="help",
|
|
410
|
+
help="show this help message and exit"
|
|
411
|
+
)
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
"--transport",
|
|
414
|
+
type=str,
|
|
415
|
+
choices=["streamable-http","stdio"],
|
|
416
|
+
metavar='<mode>',
|
|
417
|
+
help="Transport mode. Choose from: streamable-http, stdio."
|
|
418
|
+
)
|
|
419
|
+
parser.add_argument(
|
|
420
|
+
"--log-level",
|
|
421
|
+
type=str,
|
|
422
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
423
|
+
default="INFO",
|
|
424
|
+
help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
|
|
425
|
+
)
|
|
426
|
+
parser.add_argument(
|
|
427
|
+
"--debug",
|
|
428
|
+
action="store_true",
|
|
429
|
+
help="Enable debug mode with additional logging (shortcut for --log-level DEBUG)"
|
|
430
|
+
)
|
|
431
|
+
parser.add_argument(
|
|
432
|
+
"--tools",
|
|
433
|
+
type=str,
|
|
434
|
+
metavar='<categories>',
|
|
435
|
+
help="Comma-separated list of tool categories to enable (--tools infra,app,events,automation,website, settings). Also controls which prompts are enabled. If not provided, all tools and prompts are enabled."
|
|
436
|
+
)
|
|
437
|
+
parser.add_argument(
|
|
438
|
+
"--list-tools",
|
|
439
|
+
action="store_true",
|
|
440
|
+
help="List all available tool categories and exit."
|
|
441
|
+
)
|
|
442
|
+
parser.add_argument(
|
|
443
|
+
"--port",
|
|
444
|
+
type=int,
|
|
445
|
+
default=int(os.getenv("PORT", "8080")),
|
|
446
|
+
help="Port to listen on (default: 8080, can be overridden with PORT env var)"
|
|
447
|
+
)
|
|
448
|
+
# Check for help arguments before parsing
|
|
449
|
+
if len(sys.argv) > 1 and any(arg in ['-h','--h','--help','-help'] for arg in sys.argv[1:]):
|
|
450
|
+
# Check if help is combined with other arguments
|
|
451
|
+
help_args = ['-h','--h','--help','-help']
|
|
452
|
+
other_args = [arg for arg in sys.argv[1:] if arg not in help_args]
|
|
453
|
+
|
|
454
|
+
if other_args:
|
|
455
|
+
logger.error("Argument -h/--h/--help/-help: not allowed with other arguments")
|
|
456
|
+
sys.exit(2)
|
|
457
|
+
|
|
458
|
+
# Show help and exit
|
|
459
|
+
try:
|
|
460
|
+
logger.info("Available options:")
|
|
461
|
+
for action in parser._actions:
|
|
462
|
+
# Only print options that start with '--' and have a help string
|
|
463
|
+
if any(opt.startswith('--') for opt in action.option_strings) and action.help:
|
|
464
|
+
# Find the first long option
|
|
465
|
+
long_opt = next((opt for opt in action.option_strings if opt.startswith('--')), None)
|
|
466
|
+
metavar = action.metavar or ''
|
|
467
|
+
opt_str = f"{long_opt} {metavar}".strip()
|
|
468
|
+
logger.info(f"{opt_str:<24} {action.help}")
|
|
469
|
+
sys.exit(0)
|
|
470
|
+
except Exception as e:
|
|
471
|
+
logger.error(f"Error displaying help: {e}")
|
|
472
|
+
sys.exit(0) # Still exit with 0 for help
|
|
473
|
+
|
|
474
|
+
args = parser.parse_args()
|
|
475
|
+
|
|
476
|
+
# Set log level based on command line arguments
|
|
477
|
+
if args.debug:
|
|
478
|
+
set_log_level("DEBUG")
|
|
479
|
+
else:
|
|
480
|
+
set_log_level(args.log_level)
|
|
481
|
+
|
|
482
|
+
all_categories = {"infra", "app", "events", "automation", "website", "settings"}
|
|
483
|
+
|
|
484
|
+
# Handle --list-tools option
|
|
485
|
+
if args.list_tools:
|
|
486
|
+
logger.info("Available tool categories:")
|
|
487
|
+
client_categories = get_client_categories()
|
|
488
|
+
for category, tools in client_categories.items():
|
|
489
|
+
tool_names = [cls.__name__ for _, cls in tools]
|
|
490
|
+
logger.info(f" {category}: {len(tool_names)} tools")
|
|
491
|
+
for tool_name in tool_names:
|
|
492
|
+
logger.info(f" - {tool_name}")
|
|
493
|
+
sys.exit(0)
|
|
494
|
+
|
|
495
|
+
# By default, enable all categories
|
|
496
|
+
enabled = set(all_categories)
|
|
497
|
+
invalid = set()
|
|
498
|
+
|
|
499
|
+
# Enable only specified categories if --tools is provided
|
|
500
|
+
if args.tools:
|
|
501
|
+
specified_tools = {cat.strip() for cat in args.tools.split(",")}
|
|
502
|
+
invalid = specified_tools - all_categories
|
|
503
|
+
enabled = specified_tools & all_categories
|
|
504
|
+
|
|
505
|
+
# If no valid tools specified, default to all
|
|
506
|
+
if not enabled:
|
|
507
|
+
enabled = set(all_categories)
|
|
508
|
+
|
|
509
|
+
if invalid:
|
|
510
|
+
logger.error(f"Error: Unknown category/categories: {', '.join(invalid)}. Available categories: infra, app, events, automation, website, settings")
|
|
511
|
+
sys.exit(2)
|
|
512
|
+
|
|
513
|
+
# Print enabled tools for user information
|
|
514
|
+
enabled_tool_classes = []
|
|
515
|
+
client_categories = get_client_categories()
|
|
516
|
+
|
|
517
|
+
# Log enabled categories and tools
|
|
518
|
+
logger.info(f"Enabled tool categories: {', '.join(enabled)}")
|
|
519
|
+
|
|
520
|
+
for category in enabled:
|
|
521
|
+
if category in client_categories:
|
|
522
|
+
category_tools = [cls.__name__ for _, cls in client_categories[category]]
|
|
523
|
+
enabled_tool_classes.extend(category_tools)
|
|
524
|
+
logger.info(f" - {category}: {len(category_tools)} tools")
|
|
525
|
+
for tool_name in category_tools:
|
|
526
|
+
logger.info(f" * {tool_name}")
|
|
527
|
+
|
|
528
|
+
if enabled_tool_classes:
|
|
529
|
+
logger.info(
|
|
530
|
+
f"Total enabled tools: {len(enabled_tool_classes)}"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Get credentials from environment variables for stdio mode
|
|
534
|
+
INSTANA_API_TOKEN, INSTANA_BASE_URL = get_instana_credentials()
|
|
535
|
+
|
|
536
|
+
if args.transport == "stdio" or args.transport is None:
|
|
537
|
+
if not validate_credentials(INSTANA_API_TOKEN, INSTANA_BASE_URL):
|
|
538
|
+
logger.error("Error: Instana credentials are required for stdio mode but not provided. Please set INSTANA_API_TOKEN and INSTANA_BASE_URL environment variables.")
|
|
539
|
+
sys.exit(1)
|
|
540
|
+
|
|
541
|
+
# Create and configure the MCP server
|
|
542
|
+
try:
|
|
543
|
+
enabled_categories = ",".join(enabled)
|
|
544
|
+
# Ensure create_app is always called, even if credentials are missing
|
|
545
|
+
# This is needed for test_main_function_missing_token
|
|
546
|
+
app, registered_tool_count = create_app(INSTANA_API_TOKEN, INSTANA_BASE_URL, args.port, enabled_categories)
|
|
547
|
+
except Exception as e:
|
|
548
|
+
print(f"Failed to create MCP server: {e}", file=sys.stderr)
|
|
549
|
+
sys.exit(1)
|
|
550
|
+
|
|
551
|
+
# Run the server with the appropriate transport
|
|
552
|
+
if args.transport == "streamable-http":
|
|
553
|
+
if args.debug:
|
|
554
|
+
logger.info(f"FastMCP instance: {app}")
|
|
555
|
+
logger.info(f"Registered tools: {registered_tool_count}")
|
|
556
|
+
try:
|
|
557
|
+
app.run(transport="streamable-http")
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error(f"Failed to start HTTP server: {e}")
|
|
560
|
+
if args.debug:
|
|
561
|
+
logger.error("HTTP server error details", exc_info=True)
|
|
562
|
+
sys.exit(1)
|
|
563
|
+
else:
|
|
564
|
+
logger.info("Starting stdio transport")
|
|
565
|
+
try:
|
|
566
|
+
app.run(transport="stdio")
|
|
567
|
+
except AttributeError as e:
|
|
568
|
+
# Handle the case where sys.stdout is a StringIO object (in tests)
|
|
569
|
+
if "'_io.StringIO' object has no attribute 'buffer'" in str(e):
|
|
570
|
+
logger.info("Running in test mode, skipping stdio server")
|
|
571
|
+
else:
|
|
572
|
+
raise
|
|
573
|
+
|
|
574
|
+
except KeyboardInterrupt:
|
|
575
|
+
logger.info("Server stopped by user")
|
|
576
|
+
sys.exit(0)
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(f"Server error: {e}", exc_info=True)
|
|
579
|
+
sys.exit(1)
|
|
580
|
+
|
|
581
|
+
if __name__ == "__main__":
|
|
582
|
+
try:
|
|
583
|
+
main()
|
|
584
|
+
except Exception:
|
|
585
|
+
logger.error("Unhandled exception in main", exc_info=True)
|
|
586
|
+
sys.exit(1)
|