rootly-mcp-server 2.0.4__tar.gz → 2.0.6__tar.gz

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 (27) hide show
  1. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/PKG-INFO +1 -1
  2. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/pyproject.toml +1 -1
  3. rootly_mcp_server-2.0.6/rootly_fastmcp_server_routemap.py +285 -0
  4. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/src/rootly_mcp_server/__main__.py +1 -1
  5. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/src/rootly_mcp_server/rootly_openapi_loader.py +19 -6
  6. rootly_mcp_server-2.0.6/src/rootly_mcp_server/routemap_server.py +291 -0
  7. rootly_mcp_server-2.0.6/uv.lock +932 -0
  8. rootly_mcp_server-2.0.4/rootly_fastmcp_server_routemap.py +0 -210
  9. rootly_mcp_server-2.0.4/src/rootly_mcp_server/routemap_server.py +0 -172
  10. rootly_mcp_server-2.0.4/uv.lock +0 -660
  11. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/.github/workflows/pypi-release.yml +0 -0
  12. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/.gitignore +0 -0
  13. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/.semaphore/deploy.yml +0 -0
  14. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/.semaphore/semaphore.yml +0 -0
  15. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/.semaphore/update-task-definition.sh +0 -0
  16. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/CLAUDE.md +0 -0
  17. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/Dockerfile +0 -0
  18. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/LICENSE +0 -0
  19. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/README.md +0 -0
  20. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/rootly-mcp-server-demo.gif +0 -0
  21. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/rootly_fastmcp_server.py +0 -0
  22. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/rootly_openapi_loader.py +0 -0
  23. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/src/rootly_mcp_server/__init__.py +0 -0
  24. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/src/rootly_mcp_server/client.py +0 -0
  25. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/src/rootly_mcp_server/data/__init__.py +0 -0
  26. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/src/rootly_mcp_server/server.py +0 -0
  27. {rootly_mcp_server-2.0.4 → rootly_mcp_server-2.0.6}/src/rootly_mcp_server/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.0.4
3
+ Version: 2.0.6
4
4
  Summary: A Model Context Protocol server for Rootly APIs using OpenAPI spec
5
5
  Project-URL: Homepage, https://github.com/Rootly-AI-Labs/Rootly-MCP-server
6
6
  Project-URL: Issues, https://github.com/Rootly-AI-Labs/Rootly-MCP-server/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.0.4"
3
+ version = "2.0.6"
4
4
  description = "A Model Context Protocol server for Rootly APIs using OpenAPI spec"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Rootly FastMCP Server (RouteMap Version)
4
+
5
+ Alternative implementation using FastMCP's RouteMap system for filtering
6
+ instead of pre-filtering the OpenAPI spec.
7
+ """
8
+
9
+ import httpx
10
+ from fastmcp import FastMCP
11
+ from fastmcp.server.openapi import RouteMap, MCPType
12
+ import os
13
+ import logging
14
+ from rootly_openapi_loader import load_rootly_openapi_spec
15
+
16
+ # Configure logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ def create_rootly_mcp_server():
21
+ """Create and configure the Rootly MCP server using RouteMap filtering."""
22
+
23
+ # Get Rootly API token from environment
24
+ ROOTLY_API_TOKEN = os.getenv("ROOTLY_API_TOKEN")
25
+ if not ROOTLY_API_TOKEN:
26
+ raise ValueError("ROOTLY_API_TOKEN environment variable is required")
27
+
28
+ logger.info("Creating authenticated HTTP client...")
29
+ # Create a custom HTTP client wrapper that ensures string responses
30
+ class StringifyingClient:
31
+ def __init__(self, base_url: str, headers: dict, timeout: float):
32
+ self._client = httpx.AsyncClient(
33
+ base_url=base_url,
34
+ headers=headers,
35
+ timeout=timeout
36
+ )
37
+
38
+ async def __aenter__(self):
39
+ await self._client.__aenter__()
40
+ return self
41
+
42
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
43
+ await self._client.__aexit__(exc_type, exc_val, exc_tb)
44
+
45
+ async def request(self, method: str, url: str, **kwargs):
46
+ """Override request to return responses that FastMCP can handle."""
47
+ response = await self._client.request(method, url, **kwargs)
48
+
49
+ # Create a response that returns the raw text instead of structured JSON
50
+ class TextResponse:
51
+ def __init__(self, original_response):
52
+ self.status_code = original_response.status_code
53
+ self.headers = original_response.headers
54
+ self.url = original_response.url
55
+ self.request = original_response.request
56
+ self._original = original_response
57
+
58
+ # Pre-compute the text response
59
+ if original_response.status_code == 200:
60
+ try:
61
+ import json
62
+ data = original_response.json()
63
+ self._text_response = json.dumps(data, indent=2)
64
+ except Exception:
65
+ self._text_response = original_response.text or "No content"
66
+ else:
67
+ self._text_response = f"Error: HTTP {original_response.status_code} - {original_response.text}"
68
+
69
+ def json(self):
70
+ """Return the original JSON data for structured_content."""
71
+ try:
72
+ # Return the original JSON data so FastMCP can handle structured_content
73
+ return self._original.json()
74
+ except Exception:
75
+ # If JSON parsing fails, return a wrapper structure
76
+ return {"result": self._text_response}
77
+
78
+ @property
79
+ def text(self):
80
+ """Return the formatted JSON as text."""
81
+ return self._text_response
82
+
83
+ @property
84
+ def content(self):
85
+ return self._text_response.encode('utf-8')
86
+
87
+ def raise_for_status(self):
88
+ """Delegate to original response."""
89
+ return self._original.raise_for_status()
90
+
91
+ def __getattr__(self, name):
92
+ """Delegate any missing attributes to original response."""
93
+ return getattr(self._original, name)
94
+
95
+ return TextResponse(response)
96
+
97
+ def __getattr__(self, name):
98
+ return getattr(self._client, name)
99
+
100
+ # Create authenticated HTTP client with string conversion
101
+ client = StringifyingClient(
102
+ base_url="https://api.rootly.com",
103
+ headers={
104
+ "Authorization": f"Bearer {ROOTLY_API_TOKEN}",
105
+ "Content-Type": "application/vnd.api+json",
106
+ "User-Agent": "Rootly-FastMCP-Server/1.0"
107
+ },
108
+ timeout=30.0
109
+ )
110
+
111
+ logger.info("Loading OpenAPI specification...")
112
+ # Load OpenAPI spec with smart fallback logic
113
+ openapi_spec = load_rootly_openapi_spec()
114
+ logger.info("✅ Successfully loaded OpenAPI specification")
115
+
116
+ logger.info("Fixing OpenAPI spec for FastMCP compatibility...")
117
+ # Fix array types for FastMCP compatibility
118
+ def fix_array_types(obj):
119
+ if isinstance(obj, dict):
120
+ keys_to_process = list(obj.keys())
121
+ for key in keys_to_process:
122
+ value = obj[key]
123
+ if key == 'type' and isinstance(value, list):
124
+ non_null_types = [t for t in value if t != 'null']
125
+ if len(non_null_types) >= 1:
126
+ obj[key] = non_null_types[0]
127
+ obj['nullable'] = True
128
+ else:
129
+ fix_array_types(value)
130
+ elif isinstance(obj, list):
131
+ for item in obj:
132
+ fix_array_types(item)
133
+
134
+ fix_array_types(openapi_spec)
135
+ logger.info("✅ Fixed OpenAPI spec compatibility issues")
136
+
137
+ logger.info("Creating FastMCP server with RouteMap filtering...")
138
+
139
+ # Define custom route maps for filtering specific endpoints
140
+ route_maps = [
141
+ # Core incident management - list endpoints
142
+ RouteMap(
143
+ pattern=r"^/v1/incidents$",
144
+ mcp_type=MCPType.TOOL,
145
+ mcp_tags={"incidents", "core", "list"}
146
+ ),
147
+ # Incident detail endpoints
148
+ RouteMap(
149
+ pattern=r"^/v1/incidents/\{.*\}$",
150
+ mcp_type=MCPType.TOOL,
151
+ mcp_tags={"incidents", "detail"}
152
+ ),
153
+ # Incident relationships
154
+ RouteMap(
155
+ pattern=r"^/v1/incidents/\{.*\}/.*$",
156
+ mcp_type=MCPType.TOOL,
157
+ mcp_tags={"incidents", "relationships"}
158
+ ),
159
+
160
+ # Alert management
161
+ RouteMap(
162
+ pattern=r"^/v1/alerts$",
163
+ mcp_type=MCPType.TOOL,
164
+ mcp_tags={"alerts", "core", "list"}
165
+ ),
166
+ RouteMap(
167
+ pattern=r"^/v1/alerts/\{.*\}$",
168
+ mcp_type=MCPType.TOOL,
169
+ mcp_tags={"alerts", "detail"}
170
+ ),
171
+
172
+ # Users - both list and detail
173
+ RouteMap(
174
+ pattern=r"^/v1/users$",
175
+ mcp_type=MCPType.TOOL,
176
+ mcp_tags={"users", "list"}
177
+ ),
178
+ RouteMap(
179
+ pattern=r"^/v1/users/me$",
180
+ mcp_type=MCPType.TOOL,
181
+ mcp_tags={"users", "current"}
182
+ ),
183
+ RouteMap(
184
+ pattern=r"^/v1/users/\{.*\}$",
185
+ mcp_type=MCPType.TOOL,
186
+ mcp_tags={"users", "detail"}
187
+ ),
188
+
189
+ # Teams
190
+ RouteMap(
191
+ pattern=r"^/v1/teams$",
192
+ mcp_type=MCPType.TOOL,
193
+ mcp_tags={"teams", "list"}
194
+ ),
195
+ RouteMap(
196
+ pattern=r"^/v1/teams/\{.*\}$",
197
+ mcp_type=MCPType.TOOL,
198
+ mcp_tags={"teams", "detail"}
199
+ ),
200
+
201
+ # Services
202
+ RouteMap(
203
+ pattern=r"^/v1/services$",
204
+ mcp_type=MCPType.TOOL,
205
+ mcp_tags={"services", "list"}
206
+ ),
207
+ RouteMap(
208
+ pattern=r"^/v1/services/\{.*\}$",
209
+ mcp_type=MCPType.TOOL,
210
+ mcp_tags={"services", "detail"}
211
+ ),
212
+
213
+ # Configuration entities - list patterns
214
+ RouteMap(
215
+ pattern=r"^/v1/(severities|incident_types|environments)$",
216
+ mcp_type=MCPType.TOOL,
217
+ mcp_tags={"configuration", "list"}
218
+ ),
219
+ # Configuration entities - detail patterns
220
+ RouteMap(
221
+ pattern=r"^/v1/(severities|incident_types|environments)/\{.*\}$",
222
+ mcp_type=MCPType.TOOL,
223
+ mcp_tags={"configuration", "detail"}
224
+ ),
225
+
226
+ # Exclude everything else
227
+ RouteMap(
228
+ pattern=r".*",
229
+ mcp_type=MCPType.EXCLUDE
230
+ )
231
+ ]
232
+
233
+ # Custom response handler to ensure proper MCP output format
234
+ def ensure_mcp_response(route, component):
235
+ """Ensure all responses work with FastMCP's structured content system."""
236
+ # Set output schema to handle structured JSON data
237
+ component.output_schema = {
238
+ "type": "object",
239
+ "description": "Rootly API response data",
240
+ "additionalProperties": True
241
+ }
242
+
243
+ # Add description
244
+ if hasattr(component, 'description'):
245
+ component.description = f"🔧 {component.description or 'Rootly API endpoint'}"
246
+
247
+ # Create MCP server with custom route maps and response handling
248
+ mcp = FastMCP.from_openapi(
249
+ openapi_spec=openapi_spec,
250
+ client=client,
251
+ name="Rootly API Server (RouteMap Filtered)",
252
+ timeout=30.0,
253
+ tags={"rootly", "incident-management", "evaluation"},
254
+ route_maps=route_maps,
255
+ mcp_component_fn=ensure_mcp_response
256
+ )
257
+
258
+ logger.info(f"✅ Created MCP server with RouteMap filtering successfully")
259
+ logger.info("🚀 Selected Rootly API endpoints are now available as MCP tools")
260
+
261
+ return mcp
262
+
263
+
264
+
265
+
266
+ def main():
267
+ """Main entry point."""
268
+ try:
269
+ logger.info("🚀 Starting Rootly FastMCP Server (RouteMap Version)...")
270
+ mcp = create_rootly_mcp_server()
271
+
272
+ logger.info("🌐 Server starting on stdio transport...")
273
+ logger.info("Ready for MCP client connections!")
274
+
275
+ # Run the MCP server
276
+ mcp.run()
277
+
278
+ except KeyboardInterrupt:
279
+ logger.info("🛑 Server stopped by user")
280
+ except Exception as e:
281
+ logger.error(f"❌ Server error: {e}")
282
+ raise
283
+
284
+ if __name__ == "__main__":
285
+ main()
@@ -173,7 +173,7 @@ def main():
173
173
  server = create_rootly_mcp_server(
174
174
  swagger_path=args.swagger_path,
175
175
  name=args.name,
176
- allowed_paths=allowed_paths,
176
+ custom_allowed_paths=allowed_paths,
177
177
  hosted=hosted_mode,
178
178
  base_url=args.base_url,
179
179
  )
@@ -18,10 +18,11 @@ def load_rootly_openapi_spec() -> dict:
18
18
  Load Rootly OpenAPI spec with smart fallback logic.
19
19
 
20
20
  Loading priority:
21
- 1. Check current directory for rootly_openapi.json
22
- 2. Check parent directories for rootly_openapi.json
23
- 3. Check for swagger.json files
24
- 4. Only as last resort, fetch from URL and cache locally
21
+ 1. Check for Claude-compatible swagger_claude_fixed.json
22
+ 2. Check current directory for rootly_openapi.json
23
+ 3. Check parent directories for rootly_openapi.json
24
+ 4. Check for swagger.json files
25
+ 5. Only as last resort, fetch from URL and cache locally
25
26
 
26
27
  Returns:
27
28
  dict: The OpenAPI specification
@@ -31,7 +32,19 @@ def load_rootly_openapi_spec() -> dict:
31
32
  """
32
33
  current_dir = Path.cwd()
33
34
 
34
- # Check for rootly_openapi.json in current directory and parents
35
+ # Priority 1: Check for Claude-compatible swagger file first
36
+ for check_dir in [current_dir] + list(current_dir.parents):
37
+ spec_file = check_dir / "swagger_claude_fixed.json"
38
+ if spec_file.is_file():
39
+ logger.info(f"Found Claude-compatible OpenAPI spec at {spec_file}")
40
+ try:
41
+ with open(spec_file, "r") as f:
42
+ return json.load(f)
43
+ except Exception as e:
44
+ logger.warning(f"Failed to load {spec_file}: {e}")
45
+ continue
46
+
47
+ # Priority 2: Check for rootly_openapi.json in current directory and parents
35
48
  for check_dir in [current_dir] + list(current_dir.parents):
36
49
  spec_file = check_dir / "rootly_openapi.json"
37
50
  if spec_file.is_file():
@@ -43,7 +56,7 @@ def load_rootly_openapi_spec() -> dict:
43
56
  logger.warning(f"Failed to load {spec_file}: {e}")
44
57
  continue
45
58
 
46
- # Check for swagger.json in current directory and parents
59
+ # Priority 3: Check for swagger.json in current directory and parents
47
60
  for check_dir in [current_dir] + list(current_dir.parents):
48
61
  spec_file = check_dir / "swagger.json"
49
62
  if spec_file.is_file():
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Rootly FastMCP Server (RouteMap Version)
4
+
5
+ Working implementation using FastMCP's RouteMap system with proper response handling.
6
+ """
7
+
8
+ import httpx
9
+ from fastmcp import FastMCP
10
+ from fastmcp.server.openapi import RouteMap, MCPType
11
+ import os
12
+ import logging
13
+ from typing import Optional, List
14
+
15
+ # Import the shared OpenAPI loader
16
+ from .rootly_openapi_loader import load_rootly_openapi_spec
17
+
18
+ # Configure logging
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+ def create_rootly_mcp_server(
23
+ swagger_path: Optional[str] = None,
24
+ name: str = "Rootly API Server (RouteMap Filtered)",
25
+ custom_allowed_paths: Optional[List[str]] = None,
26
+ hosted: bool = False,
27
+ base_url: Optional[str] = None,
28
+ ):
29
+ """Create and configure the Rootly MCP server using RouteMap filtering."""
30
+
31
+ # Get Rootly API token from environment
32
+ ROOTLY_API_TOKEN = os.getenv("ROOTLY_API_TOKEN")
33
+ if not ROOTLY_API_TOKEN:
34
+ raise ValueError("ROOTLY_API_TOKEN environment variable is required")
35
+
36
+ logger.info("Creating authenticated HTTP client...")
37
+ # Create a custom HTTP client wrapper that ensures string responses
38
+ class StringifyingClient:
39
+ def __init__(self, base_url: str, headers: dict, timeout: float):
40
+ self._client = httpx.AsyncClient(
41
+ base_url=base_url,
42
+ headers=headers,
43
+ timeout=timeout
44
+ )
45
+
46
+ async def __aenter__(self):
47
+ await self._client.__aenter__()
48
+ return self
49
+
50
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
51
+ await self._client.__aexit__(exc_type, exc_val, exc_tb)
52
+
53
+ async def request(self, method: str, url: str, **kwargs):
54
+ """Override request to return responses that FastMCP can handle."""
55
+ response = await self._client.request(method, url, **kwargs)
56
+
57
+ # Create a response that returns the raw text instead of structured JSON
58
+ class TextResponse:
59
+ def __init__(self, original_response):
60
+ self.status_code = original_response.status_code
61
+ self.headers = original_response.headers
62
+ self.url = original_response.url
63
+ self.request = original_response.request
64
+ self._original = original_response
65
+
66
+ # Pre-compute the text response
67
+ if original_response.status_code == 200:
68
+ try:
69
+ import json
70
+ data = original_response.json()
71
+ self._text_response = json.dumps(data, indent=2)
72
+ except Exception:
73
+ self._text_response = original_response.text or "No content"
74
+ else:
75
+ self._text_response = f"Error: HTTP {original_response.status_code} - {original_response.text}"
76
+
77
+ def json(self):
78
+ """Return the original JSON data for structured_content."""
79
+ try:
80
+ # Return the original JSON data so FastMCP can handle structured_content
81
+ return self._original.json()
82
+ except Exception:
83
+ # If JSON parsing fails, return a wrapper structure
84
+ return {"result": self._text_response}
85
+
86
+ @property
87
+ def text(self):
88
+ """Return the formatted JSON as text."""
89
+ return self._text_response
90
+
91
+ @property
92
+ def content(self):
93
+ return self._text_response.encode('utf-8')
94
+
95
+ def raise_for_status(self):
96
+ """Delegate to original response."""
97
+ return self._original.raise_for_status()
98
+
99
+ def __getattr__(self, name):
100
+ """Delegate any missing attributes to original response."""
101
+ return getattr(self._original, name)
102
+
103
+ return TextResponse(response)
104
+
105
+ def __getattr__(self, name):
106
+ return getattr(self._client, name)
107
+
108
+ # Create authenticated HTTP client with string conversion
109
+ client = StringifyingClient(
110
+ base_url=base_url or "https://api.rootly.com",
111
+ headers={
112
+ "Authorization": f"Bearer {ROOTLY_API_TOKEN}",
113
+ "Content-Type": "application/vnd.api+json",
114
+ "User-Agent": "Rootly-FastMCP-Server/1.0"
115
+ },
116
+ timeout=30.0
117
+ )
118
+
119
+ logger.info("Loading OpenAPI specification...")
120
+ # Load OpenAPI spec with smart fallback logic
121
+ openapi_spec = load_rootly_openapi_spec()
122
+ logger.info("✅ Successfully loaded OpenAPI specification")
123
+
124
+ logger.info("Fixing OpenAPI spec for FastMCP compatibility...")
125
+ # Fix array types for FastMCP compatibility
126
+ def fix_array_types(obj):
127
+ if isinstance(obj, dict):
128
+ keys_to_process = list(obj.keys())
129
+ for key in keys_to_process:
130
+ value = obj[key]
131
+ if key == 'type' and isinstance(value, list):
132
+ non_null_types = [t for t in value if t != 'null']
133
+ if len(non_null_types) >= 1:
134
+ obj[key] = non_null_types[0]
135
+ obj['nullable'] = True
136
+ else:
137
+ fix_array_types(value)
138
+ elif isinstance(obj, list):
139
+ for item in obj:
140
+ fix_array_types(item)
141
+
142
+ fix_array_types(openapi_spec)
143
+ logger.info("✅ Fixed OpenAPI spec compatibility issues")
144
+
145
+ logger.info("Creating FastMCP server with RouteMap filtering...")
146
+
147
+ # Define custom route maps for filtering specific endpoints
148
+ route_maps = [
149
+ # Core incident management - list endpoints
150
+ RouteMap(
151
+ pattern=r"^/v1/incidents$",
152
+ mcp_type=MCPType.TOOL,
153
+ mcp_tags={"incidents", "core", "list"}
154
+ ),
155
+ # Incident detail endpoints
156
+ RouteMap(
157
+ pattern=r"^/v1/incidents/\{.*\}$",
158
+ mcp_type=MCPType.TOOL,
159
+ mcp_tags={"incidents", "detail"}
160
+ ),
161
+ # Incident relationships
162
+ RouteMap(
163
+ pattern=r"^/v1/incidents/\{.*\}/.*$",
164
+ mcp_type=MCPType.TOOL,
165
+ mcp_tags={"incidents", "relationships"}
166
+ ),
167
+
168
+ # Alert management
169
+ RouteMap(
170
+ pattern=r"^/v1/alerts$",
171
+ mcp_type=MCPType.TOOL,
172
+ mcp_tags={"alerts", "core", "list"}
173
+ ),
174
+ RouteMap(
175
+ pattern=r"^/v1/alerts/\{.*\}$",
176
+ mcp_type=MCPType.TOOL,
177
+ mcp_tags={"alerts", "detail"}
178
+ ),
179
+
180
+ # Users - both list and detail
181
+ RouteMap(
182
+ pattern=r"^/v1/users$",
183
+ mcp_type=MCPType.TOOL,
184
+ mcp_tags={"users", "list"}
185
+ ),
186
+ RouteMap(
187
+ pattern=r"^/v1/users/me$",
188
+ mcp_type=MCPType.TOOL,
189
+ mcp_tags={"users", "current"}
190
+ ),
191
+ RouteMap(
192
+ pattern=r"^/v1/users/\{.*\}$",
193
+ mcp_type=MCPType.TOOL,
194
+ mcp_tags={"users", "detail"}
195
+ ),
196
+
197
+ # Teams
198
+ RouteMap(
199
+ pattern=r"^/v1/teams$",
200
+ mcp_type=MCPType.TOOL,
201
+ mcp_tags={"teams", "list"}
202
+ ),
203
+ RouteMap(
204
+ pattern=r"^/v1/teams/\{.*\}$",
205
+ mcp_type=MCPType.TOOL,
206
+ mcp_tags={"teams", "detail"}
207
+ ),
208
+
209
+ # Services
210
+ RouteMap(
211
+ pattern=r"^/v1/services$",
212
+ mcp_type=MCPType.TOOL,
213
+ mcp_tags={"services", "list"}
214
+ ),
215
+ RouteMap(
216
+ pattern=r"^/v1/services/\{.*\}$",
217
+ mcp_type=MCPType.TOOL,
218
+ mcp_tags={"services", "detail"}
219
+ ),
220
+
221
+ # Configuration entities - list patterns
222
+ RouteMap(
223
+ pattern=r"^/v1/(severities|incident_types|environments)$",
224
+ mcp_type=MCPType.TOOL,
225
+ mcp_tags={"configuration", "list"}
226
+ ),
227
+ # Configuration entities - detail patterns
228
+ RouteMap(
229
+ pattern=r"^/v1/(severities|incident_types|environments)/\{.*\}$",
230
+ mcp_type=MCPType.TOOL,
231
+ mcp_tags={"configuration", "detail"}
232
+ ),
233
+
234
+ # Exclude everything else
235
+ RouteMap(
236
+ pattern=r".*",
237
+ mcp_type=MCPType.EXCLUDE
238
+ )
239
+ ]
240
+
241
+ # Custom response handler to ensure proper MCP output format
242
+ def ensure_mcp_response(route, component):
243
+ """Ensure all responses work with FastMCP's structured content system."""
244
+ # Set output schema to handle structured JSON data
245
+ component.output_schema = {
246
+ "type": "object",
247
+ "description": "Rootly API response data",
248
+ "additionalProperties": True
249
+ }
250
+
251
+ # Add description
252
+ if hasattr(component, 'description'):
253
+ component.description = f"🔧 {component.description or 'Rootly API endpoint'}"
254
+
255
+ # Create MCP server with custom route maps and response handling
256
+ mcp = FastMCP.from_openapi(
257
+ openapi_spec=openapi_spec,
258
+ client=client,
259
+ name=name,
260
+ timeout=30.0,
261
+ tags={"rootly", "incident-management", "evaluation"},
262
+ route_maps=route_maps,
263
+ mcp_component_fn=ensure_mcp_response
264
+ )
265
+
266
+ logger.info(f"✅ Created MCP server with RouteMap filtering successfully")
267
+ logger.info("🚀 Selected Rootly API endpoints are now available as MCP tools")
268
+
269
+ return mcp
270
+
271
+
272
+ def main():
273
+ """Main entry point."""
274
+ try:
275
+ logger.info("🚀 Starting Rootly FastMCP Server (RouteMap Version)...")
276
+ mcp = create_rootly_mcp_server()
277
+
278
+ logger.info("🌐 Server starting on stdio transport...")
279
+ logger.info("Ready for MCP client connections!")
280
+
281
+ # Run the MCP server
282
+ mcp.run()
283
+
284
+ except KeyboardInterrupt:
285
+ logger.info("🛑 Server stopped by user")
286
+ except Exception as e:
287
+ logger.error(f"❌ Server error: {e}")
288
+ raise
289
+
290
+ if __name__ == "__main__":
291
+ main()