rootly-mcp-server 0.0.5__py3-none-any.whl → 2.0.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.
@@ -13,7 +13,7 @@ Features:
13
13
  from .server import RootlyMCPServer
14
14
  from .client import RootlyClient
15
15
 
16
- __version__ = "0.1.0"
16
+ __version__ = "2.0.0"
17
17
  __all__ = [
18
18
  'RootlyMCPServer',
19
19
  'RootlyClient',
@@ -1,5 +1,8 @@
1
+ #!/usr/bin/env python3
1
2
  """
2
- Command-line interface for starting the Rootly MCP server.
3
+ Rootly MCP Server - Main entry point
4
+
5
+ This module provides the main entry point for the Rootly MCP Server.
3
6
  """
4
7
 
5
8
  import argparse
@@ -7,8 +10,9 @@ import logging
7
10
  import os
8
11
  import sys
9
12
  from pathlib import Path
13
+ from typing import Optional, List
10
14
 
11
- from .server import RootlyMCPServer
15
+ from rootly_mcp_server.server import create_rootly_mcp_server
12
16
 
13
17
 
14
18
  def parse_args():
@@ -46,6 +50,27 @@ def parse_args():
46
50
  action="store_true",
47
51
  help="Enable debug mode (equivalent to --log-level DEBUG)",
48
52
  )
53
+ parser.add_argument(
54
+ "--base-url",
55
+ type=str,
56
+ help="Base URL for the Rootly API. Default: https://api.rootly.com",
57
+ )
58
+ parser.add_argument(
59
+ "--allowed-paths",
60
+ type=str,
61
+ help="Comma-separated list of allowed API paths to include",
62
+ )
63
+ parser.add_argument(
64
+ "--hosted",
65
+ action="store_true",
66
+ help="Enable hosted mode for remote MCP server",
67
+ )
68
+ # Backward compatibility: support deprecated --host argument
69
+ parser.add_argument(
70
+ "--host",
71
+ action="store_true",
72
+ help="(Deprecated) Use --hosted instead. Enable hosted mode for remote MCP server",
73
+ )
49
74
  return parser.parse_args()
50
75
 
51
76
 
@@ -67,11 +92,7 @@ def setup_logging(log_level, debug=False):
67
92
 
68
93
  # Set specific logger levels
69
94
  logging.getLogger("rootly_mcp_server").setLevel(numeric_level)
70
- logging.getLogger("rootly_mcp_server.server").setLevel(logging.WARNING) # Reduce server-specific logs
71
-
72
- # Always set MCP logger to ERROR level to fix Cline UI issue
73
- # This prevents INFO logs from causing problems with Cline tool display
74
- logging.getLogger("mcp").setLevel(logging.ERROR)
95
+ logging.getLogger("mcp").setLevel(numeric_level)
75
96
 
76
97
  # Log the configuration
77
98
  logger = logging.getLogger(__name__)
@@ -97,22 +118,69 @@ def check_api_token():
97
118
  logger.debug(f"Token starts with: {api_token[:5]}...")
98
119
 
99
120
 
121
+ # Create the server instance for FastMCP CLI (follows quickstart pattern)
122
+ def get_server():
123
+ """Get a configured Rootly MCP server instance."""
124
+ # Get configuration from environment variables
125
+ swagger_path = os.getenv("ROOTLY_SWAGGER_PATH")
126
+ server_name = os.getenv("ROOTLY_SERVER_NAME", "Rootly")
127
+ hosted = os.getenv("ROOTLY_HOSTED", "false").lower() in ("true", "1", "yes")
128
+ base_url = os.getenv("ROOTLY_BASE_URL")
129
+
130
+ # Parse allowed paths from environment variable
131
+ allowed_paths = None
132
+ allowed_paths_env = os.getenv("ROOTLY_ALLOWED_PATHS")
133
+ if allowed_paths_env:
134
+ allowed_paths = [path.strip() for path in allowed_paths_env.split(",")]
135
+
136
+ # Create and return the server
137
+ return create_rootly_mcp_server(
138
+ swagger_path=swagger_path,
139
+ name=server_name,
140
+ allowed_paths=allowed_paths,
141
+ hosted=hosted,
142
+ base_url=base_url,
143
+ )
144
+
145
+
146
+ # Create the server instance for FastMCP CLI (follows quickstart pattern)
147
+ mcp = get_server()
148
+
149
+
100
150
  def main():
101
- """Entry point for the Rootly MCP server."""
151
+ """Main entry point for the Rootly MCP Server."""
102
152
  args = parse_args()
103
153
  setup_logging(args.log_level, args.debug)
104
154
 
105
155
  logger = logging.getLogger(__name__)
106
156
  logger.info("Starting Rootly MCP Server")
107
157
 
158
+ # Handle backward compatibility for --host argument
159
+ hosted_mode = args.hosted
160
+ if args.host:
161
+ logger.warning("--host argument is deprecated, use --hosted instead")
162
+ hosted_mode = True
163
+
108
164
  check_api_token()
109
165
 
110
166
  try:
167
+ # Parse allowed paths from command line argument
168
+ allowed_paths = None
169
+ if args.allowed_paths:
170
+ allowed_paths = [path.strip() for path in args.allowed_paths.split(",")]
171
+
111
172
  logger.info(f"Initializing server with name: {args.name}")
112
- server = RootlyMCPServer(swagger_path=args.swagger_path, name=args.name)
173
+ server = create_rootly_mcp_server(
174
+ swagger_path=args.swagger_path,
175
+ name=args.name,
176
+ allowed_paths=allowed_paths,
177
+ hosted=hosted_mode,
178
+ base_url=args.base_url,
179
+ )
113
180
 
114
181
  logger.info(f"Running server with transport: {args.transport}...")
115
182
  server.run(transport=args.transport)
183
+
116
184
  except FileNotFoundError as e:
117
185
  logger.error(f"File not found: {e}")
118
186
  print(f"Error: {e}", file=sys.stderr)
@@ -124,4 +192,4 @@ def main():
124
192
 
125
193
 
126
194
  if __name__ == "__main__":
127
- main()
195
+ main()
@@ -11,10 +11,13 @@ from typing import Optional, Dict, Any, Union
11
11
  # Set up logger
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
+
14
15
  class RootlyClient:
15
- def __init__(self, base_url: Optional[str] = None):
16
+ def __init__(self, base_url: Optional[str] = None, hosted: bool = False):
16
17
  self.base_url = base_url or "https://api.rootly.com"
17
- self._api_token = self._get_api_token()
18
+ self.hosted = hosted
19
+ if not self.hosted:
20
+ self._api_token = self._get_api_token()
18
21
  logger.debug(f"Initialized RootlyClient with base_url: {self.base_url}")
19
22
 
20
23
  def _get_api_token(self) -> str:
@@ -24,40 +27,65 @@ class RootlyClient:
24
27
  raise ValueError("ROOTLY_API_TOKEN environment variable is not set")
25
28
  return api_token
26
29
 
27
- def make_request(self, method: str, path: str, query_params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> str:
30
+ def make_request(
31
+ self,
32
+ method: str,
33
+ path: str,
34
+ query_params: Optional[Dict[str, Any]] = None,
35
+ json_data: Optional[Dict[str, Any]] = None,
36
+ json_api_type: Optional[str] = None,
37
+ api_token: Optional[str] = None,
38
+ ) -> str:
28
39
  """
29
40
  Make an authenticated request to the Rootly API.
30
-
41
+
31
42
  Args:
32
43
  method: The HTTP method to use.
33
44
  path: The API path.
34
45
  query_params: Query parameters for the request.
35
46
  json_data: JSON data for the request body.
36
-
47
+ json_api_type: If set, use JSON-API format and this type value.
48
+
37
49
  Returns:
38
50
  The API response as a JSON string.
39
51
  """
52
+ if self.hosted:
53
+ if not api_token:
54
+ return json.dumps({"error": "No API token provided"})
55
+ else:
56
+ api_token = self._api_token
57
+
58
+ # Default headers
40
59
  headers = {
41
- "Authorization": f"Bearer {self._api_token}",
60
+ "Authorization": f"Bearer {api_token}",
42
61
  "Content-Type": "application/json",
43
- "Accept": "application/json"
62
+ "Accept": "application/json",
44
63
  }
45
-
64
+
65
+ # If JSON-API, update headers and wrap payload
66
+ if json_api_type and method.upper() in ["POST", "PUT", "PATCH"]:
67
+ headers["Content-Type"] = "application/vnd.api+json"
68
+ headers["Accept"] = "application/vnd.api+json"
69
+ if json_data:
70
+ json_data = {"data": {"type": json_api_type, "attributes": json_data}}
71
+ else:
72
+ json_data = None
73
+
46
74
  # Ensure path starts with a slash
47
75
  if not path.startswith("/"):
48
76
  path = f"/{path}"
49
-
77
+
50
78
  # Ensure path starts with /v1 if not already
51
79
  if not path.startswith("/v1"):
52
80
  path = f"/v1{path}"
53
-
81
+
54
82
  url = f"{self.base_url}{path}"
55
-
83
+
56
84
  logger.debug(f"Making {method} request to {url}")
57
85
  logger.debug(f"Headers: {headers}")
58
86
  logger.debug(f"Query params: {query_params}")
59
87
  logger.debug(f"JSON data: {json_data}")
60
-
88
+
61
89
  try:
62
90
  response = requests.request(
63
91
  method=method.upper(),
@@ -65,17 +93,19 @@ class RootlyClient:
65
93
  headers=headers,
66
94
  params=query_params,
67
95
  json=json_data,
68
- timeout=30 # Add a timeout to prevent hanging
96
+ timeout=30, # Add a timeout to prevent hanging
69
97
  )
70
-
98
+
71
99
  # Log the response status and headers
72
100
  logger.debug(f"Response status: {response.status_code}")
73
101
  logger.debug(f"Response headers: {response.headers}")
74
-
102
+
75
103
  # Try to parse the response as JSON
76
104
  try:
77
105
  response_json = response.json()
78
- logger.debug(f"Response parsed as JSON: {json.dumps(response_json)[:200]}...")
106
+ logger.debug(
107
+ f"Response parsed as JSON: {json.dumps(response_json)[:200]}..."
108
+ )
79
109
  response.raise_for_status()
80
110
  return json.dumps(response_json, indent=2)
81
111
  except ValueError:
@@ -83,20 +113,20 @@ class RootlyClient:
83
113
  logger.debug(f"Response is not JSON: {response.text[:200]}...")
84
114
  response.raise_for_status()
85
115
  return json.dumps({"text": response.text}, indent=2)
86
-
116
+
87
117
  except requests.exceptions.RequestException as e:
88
118
  logger.error(f"Request failed: {e}")
89
119
  error_response = {"error": str(e)}
90
-
120
+
91
121
  # Add response details if available
92
- if hasattr(e, 'response') and e.response is not None:
122
+ if hasattr(e, "response") and e.response is not None:
93
123
  try:
94
124
  error_response["status_code"] = e.response.status_code
95
125
  error_response["response_text"] = e.response.text
96
126
  except:
97
127
  pass
98
-
128
+
99
129
  return json.dumps(error_response, indent=2)
100
130
  except Exception as e:
101
131
  logger.error(f"Unexpected error: {e}")
102
- return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2)
132
+ return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2)