Skip to content

Commit 83b89ef

Browse files
committed
init
1 parent 48b41dc commit 83b89ef

11 files changed

+87376
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,5 @@ cython_debug/
172172

173173
# PyPI configuration file
174174
.pypirc
175+
176+
.cursor

README.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Rootly MCP Server
2+
3+
A Model Context Protocol (MCP) server for Rootly API. This server dynamically generates MCP resources based on Rootly's OpenAPI (Swagger) specification.
4+
5+
## Features
6+
7+
- Dynamically generated MCP tools based on Rootly's OpenAPI specification
8+
- Authentication via Rootly API token
9+
- Default pagination (10 items) for incidents endpoints to prevent context window overflow
10+
- Easy integration with Claude and other MCP-compatible LLMs
11+
12+
## Prerequisites
13+
14+
- Python 3.12 or higher
15+
- `uv` package manager
16+
```bash
17+
# Install UV if you haven't already
18+
curl -LsSf https://astral.sh/uv/install.sh | sh
19+
```
20+
- Rootly API token
21+
22+
## Setup
23+
24+
1. Create and activate a virtual environment:
25+
```bash
26+
# Create a new virtual environment
27+
uv venv
28+
29+
# Activate the virtual environment
30+
# On macOS/Linux:
31+
source .venv/bin/activate
32+
# On Windows:
33+
.venv\Scripts\activate
34+
```
35+
36+
2. Install the package in development mode:
37+
```bash
38+
# Install all dependencies
39+
uv pip install -e .
40+
41+
# Install dev dependencies (optional)
42+
uv pip install -e ".[dev]"
43+
```
44+
45+
3. Set your Rootly API token:
46+
```bash
47+
export ROOTLY_API_TOKEN="your-api-token-here"
48+
```
49+
50+
## Running the Server
51+
52+
Start the server:
53+
```bash
54+
rootly-mcp
55+
```
56+
57+
## MCP Configuration
58+
59+
The server configuration is defined in `mcp.json`. To use this server with Claude or other MCP clients, add the following configuration to your MCP configuration file:
60+
61+
```json
62+
{
63+
"mcpServers": {
64+
"rootly": {
65+
"command": "uv",
66+
"args": [
67+
"run",
68+
"--directory",
69+
"/path/to/rootly-mcp-server",
70+
"rootly-mcp"
71+
],
72+
"env": {
73+
"ROOTLY_API_TOKEN": "YOUR_ROOTLY_API_TOKEN"
74+
}
75+
}
76+
}
77+
}
78+
```
79+
80+
Replace `/path/to/rootly-mcp-server` with the absolute path to your project directory.
81+

pyproject.toml

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[project]
2+
name = "rootly-mcp-server"
3+
version = "0.1.0"
4+
description = "A Model Context Protocol server for Rootly APIs using OpenAPI spec"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
authors = [{ name = "Anthropic, PBC." }]
8+
keywords = ["rootly", "mcp", "llm", "automation", "incidents"]
9+
license = { text = "MIT" }
10+
classifiers = [
11+
"Development Status :: 4 - Beta",
12+
"Intended Audience :: Developers",
13+
"License :: OSI Approved :: MIT License",
14+
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.12",
16+
]
17+
dependencies = [
18+
"mcp>=1.1.2", # MCP Python SDK
19+
"requests>=2.28.0", # For API calls
20+
"pydantic>=2.0.0", # For data validation
21+
]
22+
23+
[build-system]
24+
requires = ["hatchling"]
25+
build-backend = "hatchling.build"
26+
27+
[tool.hatch.build.targets.wheel]
28+
packages = ["src/rootly_mcp_server"]
29+
30+
[tool.hatch.metadata]
31+
allow-direct-references = true
32+
33+
[project.scripts]
34+
rootly-mcp = "rootly_mcp_server.__main__:main"
35+
36+
[project.optional-dependencies]
37+
dev = [
38+
"black>=23.0.0",
39+
"isort>=5.0.0",
40+
]
41+
42+
[tool.uv]
43+
dev-dependencies = [
44+
"pyright>=1.1.389",
45+
"ruff>=0.7.3",
46+
"pytest>=8.0.0"
47+
]
48+
49+
[tool.pytest.ini_options]
50+
testpaths = ["tests"]
51+
python_files = "test_*.py"
52+
python_classes = "Test*"
53+
python_functions = "test_*"

src/rootly_mcp_server/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Rootly MCP Server - A Model Context Protocol server for Rootly API integration.
3+
4+
This package provides a Model Context Protocol (MCP) server for Rootly API integration.
5+
It dynamically generates MCP tools based on the Rootly API's OpenAPI (Swagger) specification.
6+
7+
Features:
8+
- Automatic tool generation from Swagger spec
9+
- Authentication via ROOTLY_API_TOKEN environment variable
10+
- Default pagination (10 items) for incidents endpoints to prevent context window overflow
11+
"""
12+
13+
from .server import RootlyMCPServer
14+
from .client import RootlyClient
15+
16+
__version__ = "0.1.0"
17+
__all__ = [
18+
'RootlyMCPServer',
19+
'RootlyClient',
20+
]

src/rootly_mcp_server/__main__.py

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
Command-line interface for starting the Rootly MCP server.
3+
"""
4+
5+
import argparse
6+
import logging
7+
import os
8+
import sys
9+
from pathlib import Path
10+
11+
from .server import RootlyMCPServer
12+
13+
14+
def parse_args():
15+
"""Parse command-line arguments."""
16+
parser = argparse.ArgumentParser(
17+
description="Start the Rootly MCP server for API integration."
18+
)
19+
parser.add_argument(
20+
"--swagger-path",
21+
type=str,
22+
help="Path to the Swagger JSON file. If not provided, will look for swagger.json in the current directory and parent directories.",
23+
)
24+
parser.add_argument(
25+
"--log-level",
26+
type=str,
27+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
28+
default="INFO",
29+
help="Set the logging level. Default: INFO",
30+
)
31+
parser.add_argument(
32+
"--name",
33+
type=str,
34+
default="Rootly",
35+
help="Name of the MCP server. Default: Rootly",
36+
)
37+
parser.add_argument(
38+
"--transport",
39+
type=str,
40+
choices=["stdio", "sse"],
41+
default="stdio",
42+
help="Transport protocol to use. Default: stdio",
43+
)
44+
parser.add_argument(
45+
"--debug",
46+
action="store_true",
47+
help="Enable debug mode (equivalent to --log-level DEBUG)",
48+
)
49+
return parser.parse_args()
50+
51+
52+
def setup_logging(log_level, debug=False):
53+
"""Set up logging configuration."""
54+
if debug or os.getenv("DEBUG", "").lower() in ("true", "1", "yes"):
55+
log_level = "DEBUG"
56+
57+
numeric_level = getattr(logging, log_level.upper(), None)
58+
if not isinstance(numeric_level, int):
59+
raise ValueError(f"Invalid log level: {log_level}")
60+
61+
# Configure root logger
62+
logging.basicConfig(
63+
level=numeric_level,
64+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
65+
handlers=[logging.StreamHandler(sys.stderr)], # Log to stderr for stdio transport
66+
)
67+
68+
# Set specific logger levels
69+
logging.getLogger("rootly_mcp_server").setLevel(numeric_level)
70+
logging.getLogger("mcp").setLevel(numeric_level)
71+
72+
# Log the configuration
73+
logger = logging.getLogger(__name__)
74+
logger.info(f"Logging configured with level: {log_level}")
75+
logger.debug(f"Python version: {sys.version}")
76+
logger.debug(f"Current directory: {Path.cwd()}")
77+
logger.debug(f"Environment variables: {', '.join([f'{k}={v[:3]}...' if k.endswith('TOKEN') else f'{k}={v}' for k, v in os.environ.items() if k.startswith('ROOTLY_') or k in ['DEBUG']])}")
78+
79+
80+
def check_api_token():
81+
"""Check if the Rootly API token is set."""
82+
logger = logging.getLogger(__name__)
83+
84+
api_token = os.environ.get("ROOTLY_API_TOKEN")
85+
if not api_token:
86+
logger.error("ROOTLY_API_TOKEN environment variable is not set.")
87+
print("Error: ROOTLY_API_TOKEN environment variable is not set.", file=sys.stderr)
88+
print("Please set it with: export ROOTLY_API_TOKEN='your-api-token-here'", file=sys.stderr)
89+
sys.exit(1)
90+
else:
91+
logger.info("ROOTLY_API_TOKEN is set")
92+
# Log the first few characters of the token for debugging
93+
logger.debug(f"Token starts with: {api_token[:5]}...")
94+
95+
96+
def main():
97+
"""Entry point for the Rootly MCP server."""
98+
args = parse_args()
99+
setup_logging(args.log_level, args.debug)
100+
101+
logger = logging.getLogger(__name__)
102+
logger.info("Starting Rootly MCP Server")
103+
104+
check_api_token()
105+
106+
try:
107+
logger.info(f"Initializing server with name: {args.name}")
108+
server = RootlyMCPServer(swagger_path=args.swagger_path, name=args.name)
109+
110+
logger.info(f"Running server with transport: {args.transport}...")
111+
server.run(transport=args.transport)
112+
except FileNotFoundError as e:
113+
logger.error(f"File not found: {e}")
114+
print(f"Error: {e}", file=sys.stderr)
115+
sys.exit(1)
116+
except Exception as e:
117+
logger.error(f"Failed to start server: {e}", exc_info=True)
118+
print(f"Error: {e}", file=sys.stderr)
119+
sys.exit(1)
120+
121+
122+
if __name__ == "__main__":
123+
main()

src/rootly_mcp_server/client.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Rootly API client for making authenticated requests to the Rootly API.
3+
"""
4+
5+
import os
6+
import json
7+
import logging
8+
import requests
9+
from typing import Optional, Dict, Any, Union
10+
11+
# Set up logger
12+
logger = logging.getLogger(__name__)
13+
14+
class RootlyClient:
15+
def __init__(self, base_url: Optional[str] = None):
16+
self.base_url = base_url or "https://api.rootly.com"
17+
self._api_token = self._get_api_token()
18+
logger.debug(f"Initialized RootlyClient with base_url: {self.base_url}")
19+
20+
def _get_api_token(self) -> str:
21+
"""Get the API token from environment variables."""
22+
api_token = os.getenv("ROOTLY_API_TOKEN")
23+
if not api_token:
24+
raise ValueError("ROOTLY_API_TOKEN environment variable is not set")
25+
return api_token
26+
27+
def make_request(self, method: str, path: str, query_params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> str:
28+
"""
29+
Make an authenticated request to the Rootly API.
30+
31+
Args:
32+
method: The HTTP method to use.
33+
path: The API path.
34+
query_params: Query parameters for the request.
35+
json_data: JSON data for the request body.
36+
37+
Returns:
38+
The API response as a JSON string.
39+
"""
40+
headers = {
41+
"Authorization": f"Bearer {self._api_token}",
42+
"Content-Type": "application/json",
43+
"Accept": "application/json"
44+
}
45+
46+
# Ensure path starts with a slash
47+
if not path.startswith("/"):
48+
path = f"/{path}"
49+
50+
# Ensure path starts with /v1 if not already
51+
if not path.startswith("/v1"):
52+
path = f"/v1{path}"
53+
54+
url = f"{self.base_url}{path}"
55+
56+
logger.debug(f"Making {method} request to {url}")
57+
logger.debug(f"Headers: {headers}")
58+
logger.debug(f"Query params: {query_params}")
59+
logger.debug(f"JSON data: {json_data}")
60+
61+
try:
62+
response = requests.request(
63+
method=method.upper(),
64+
url=url,
65+
headers=headers,
66+
params=query_params,
67+
json=json_data,
68+
timeout=30 # Add a timeout to prevent hanging
69+
)
70+
71+
# Log the response status and headers
72+
logger.debug(f"Response status: {response.status_code}")
73+
logger.debug(f"Response headers: {response.headers}")
74+
75+
# Try to parse the response as JSON
76+
try:
77+
response_json = response.json()
78+
logger.debug(f"Response parsed as JSON: {json.dumps(response_json)[:200]}...")
79+
response.raise_for_status()
80+
return json.dumps(response_json, indent=2)
81+
except ValueError:
82+
# If the response is not JSON, return the text
83+
logger.debug(f"Response is not JSON: {response.text[:200]}...")
84+
response.raise_for_status()
85+
return json.dumps({"text": response.text}, indent=2)
86+
87+
except requests.exceptions.RequestException as e:
88+
logger.error(f"Request failed: {e}")
89+
error_response = {"error": str(e)}
90+
91+
# Add response details if available
92+
if hasattr(e, 'response') and e.response is not None:
93+
try:
94+
error_response["status_code"] = e.response.status_code
95+
error_response["response_text"] = e.response.text
96+
except:
97+
pass
98+
99+
return json.dumps(error_response, indent=2)
100+
except Exception as e:
101+
logger.error(f"Unexpected error: {e}")
102+
return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2)

0 commit comments

Comments
 (0)