mcp-server-splunk-oncall 0.1.0__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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .env
7
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 amendezsap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-splunk-oncall
3
+ Version: 0.1.0
4
+ Summary: MCP server for Splunk On-Call (VictorOps) incident management
5
+ Project-URL: Homepage, https://github.com/amendezsap/mcp-server-splunk-oncall
6
+ Project-URL: Repository, https://github.com/amendezsap/mcp-server-splunk-oncall
7
+ Project-URL: Issues, https://github.com/amendezsap/mcp-server-splunk-oncall/issues
8
+ Author: amendezsap
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: incident-management,mcp,oncall,splunk,victorops
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Monitoring
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: mcp>=1.0.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # mcp-server-splunk-oncall
23
+
24
+ MCP server for Splunk On-Call (VictorOps) incident management. Provides tools for managing incidents, on-call schedules, maintenance windows, and team operations from any MCP client.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ uvx mcp-server-splunk-oncall
30
+ ```
31
+
32
+ Or install from PyPI:
33
+
34
+ ```bash
35
+ pip install mcp-server-splunk-oncall
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ The server requires two environment variables:
41
+
42
+ - `SPLUNK_ONCALL_API_ID` - Your Splunk On-Call API ID
43
+ - `SPLUNK_ONCALL_API_KEY` - Your Splunk On-Call API key
44
+
45
+ ### Claude Code
46
+
47
+ Add to your Claude Code MCP settings:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "splunk-oncall": {
53
+ "command": "uvx",
54
+ "args": ["mcp-server-splunk-oncall"],
55
+ "env": {
56
+ "SPLUNK_ONCALL_API_ID": "your-api-id",
57
+ "SPLUNK_ONCALL_API_KEY": "your-api-key"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ For read-only access, use a read-only API key. For full access (acknowledge, resolve, reroute incidents), use a full-access API key.
65
+
66
+ ## Available Tools
67
+
68
+ ### Incidents
69
+ - `list_incidents` - List all current incidents
70
+ - `acknowledge_incidents` - Acknowledge incidents by number
71
+ - `resolve_incidents` - Resolve incidents by number
72
+ - `reroute_incidents` - Reroute incidents to another user or policy
73
+ - `get_incident_timeline` - Get event timeline for an incident
74
+ - `get_incident_history` - Query historical incident data
75
+
76
+ ### On-Call
77
+ - `get_oncall` - Who is currently on call across all teams
78
+ - `get_team_oncall_schedule` - On-call schedule for a team
79
+ - `get_user_oncall_schedule` - On-call schedule for a user
80
+
81
+ ### Teams and Users
82
+ - `list_teams` - List all teams
83
+ - `get_team_members` - List members of a team
84
+ - `get_team_policies` - List escalation policies for a team
85
+ - `list_users` - List all users
86
+ - `get_user` - Get user details
87
+
88
+ ### Routing and Policies
89
+ - `list_routing_keys` - List routing keys and their policies
90
+ - `list_policies` - List all escalation policies
91
+ - `get_policy` - Get escalation policy details
92
+
93
+ ### Maintenance
94
+ - `list_maintenance` - List active and scheduled maintenance windows
95
+ - `create_maintenance` - Create a maintenance window
96
+ - `end_maintenance` - End a maintenance window early
97
+
98
+ ### Organization
99
+ - `get_org_info` - Get organization information
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,82 @@
1
+ # mcp-server-splunk-oncall
2
+
3
+ MCP server for Splunk On-Call (VictorOps) incident management. Provides tools for managing incidents, on-call schedules, maintenance windows, and team operations from any MCP client.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uvx mcp-server-splunk-oncall
9
+ ```
10
+
11
+ Or install from PyPI:
12
+
13
+ ```bash
14
+ pip install mcp-server-splunk-oncall
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ The server requires two environment variables:
20
+
21
+ - `SPLUNK_ONCALL_API_ID` - Your Splunk On-Call API ID
22
+ - `SPLUNK_ONCALL_API_KEY` - Your Splunk On-Call API key
23
+
24
+ ### Claude Code
25
+
26
+ Add to your Claude Code MCP settings:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "splunk-oncall": {
32
+ "command": "uvx",
33
+ "args": ["mcp-server-splunk-oncall"],
34
+ "env": {
35
+ "SPLUNK_ONCALL_API_ID": "your-api-id",
36
+ "SPLUNK_ONCALL_API_KEY": "your-api-key"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ For read-only access, use a read-only API key. For full access (acknowledge, resolve, reroute incidents), use a full-access API key.
44
+
45
+ ## Available Tools
46
+
47
+ ### Incidents
48
+ - `list_incidents` - List all current incidents
49
+ - `acknowledge_incidents` - Acknowledge incidents by number
50
+ - `resolve_incidents` - Resolve incidents by number
51
+ - `reroute_incidents` - Reroute incidents to another user or policy
52
+ - `get_incident_timeline` - Get event timeline for an incident
53
+ - `get_incident_history` - Query historical incident data
54
+
55
+ ### On-Call
56
+ - `get_oncall` - Who is currently on call across all teams
57
+ - `get_team_oncall_schedule` - On-call schedule for a team
58
+ - `get_user_oncall_schedule` - On-call schedule for a user
59
+
60
+ ### Teams and Users
61
+ - `list_teams` - List all teams
62
+ - `get_team_members` - List members of a team
63
+ - `get_team_policies` - List escalation policies for a team
64
+ - `list_users` - List all users
65
+ - `get_user` - Get user details
66
+
67
+ ### Routing and Policies
68
+ - `list_routing_keys` - List routing keys and their policies
69
+ - `list_policies` - List all escalation policies
70
+ - `get_policy` - Get escalation policy details
71
+
72
+ ### Maintenance
73
+ - `list_maintenance` - List active and scheduled maintenance windows
74
+ - `create_maintenance` - Create a maintenance window
75
+ - `end_maintenance` - End a maintenance window early
76
+
77
+ ### Organization
78
+ - `get_org_info` - Get organization information
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-server-splunk-oncall"
7
+ version = "0.1.0"
8
+ description = "MCP server for Splunk On-Call (VictorOps) incident management"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "amendezsap" }]
13
+ keywords = ["mcp", "splunk", "victorops", "oncall", "incident-management"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: System Administrators",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: System :: Monitoring",
20
+ ]
21
+ dependencies = [
22
+ "mcp>=1.0.0",
23
+ "httpx>=0.27.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ mcp-server-splunk-oncall = "mcp_server_splunk_oncall.server:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/amendezsap/mcp-server-splunk-oncall"
31
+ Repository = "https://github.com/amendezsap/mcp-server-splunk-oncall"
32
+ Issues = "https://github.com/amendezsap/mcp-server-splunk-oncall/issues"
@@ -0,0 +1,158 @@
1
+ """HTTP client for the Splunk On-Call (VictorOps) REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+
8
+ API_BASE = "https://api.victorops.com/api-public"
9
+
10
+
11
+ class SplunkOnCallClient:
12
+ """Thin wrapper around the Splunk On-Call public REST API."""
13
+
14
+ def __init__(self, api_id: str, api_key: str) -> None:
15
+ self._headers = {
16
+ "X-VO-Api-Id": api_id,
17
+ "X-VO-Api-Key": api_key,
18
+ "Content-Type": "application/json",
19
+ "Accept": "application/json",
20
+ }
21
+ self._client = httpx.AsyncClient(
22
+ base_url=API_BASE,
23
+ headers=self._headers,
24
+ timeout=30.0,
25
+ )
26
+
27
+ async def close(self) -> None:
28
+ await self._client.aclose()
29
+
30
+ async def _request(self, method: str, path: str, **kwargs) -> dict:
31
+ resp = await self._client.request(method, path, **kwargs)
32
+ resp.raise_for_status()
33
+ return resp.json()
34
+
35
+ # -- Incidents --
36
+
37
+ async def list_incidents(self) -> dict:
38
+ return await self._request("GET", "/v2/incidents")
39
+
40
+ async def acknowledge_incidents(
41
+ self, user_name: str, incident_names: list[str], message: str | None = None,
42
+ ) -> dict:
43
+ body: dict = {"userName": user_name, "incidentNames": incident_names}
44
+ if message:
45
+ body["message"] = message
46
+ return await self._request("POST", "/v1/incidents/acknowledge", json=body)
47
+
48
+ async def resolve_incidents(
49
+ self, user_name: str, incident_names: list[str], message: str | None = None,
50
+ ) -> dict:
51
+ body: dict = {"userName": user_name, "incidentNames": incident_names}
52
+ if message:
53
+ body["message"] = message
54
+ return await self._request("POST", "/v1/incidents/resolve", json=body)
55
+
56
+ async def reroute_incidents(
57
+ self, user_name: str, incident_names: list[str], targets: list[dict],
58
+ ) -> dict:
59
+ body = {
60
+ "userName": user_name,
61
+ "incidentNames": incident_names,
62
+ "targets": targets,
63
+ }
64
+ return await self._request("POST", "/v1/incidents/reroute", json=body)
65
+
66
+ async def get_incident_timeline(self, incident_number: str) -> dict:
67
+ return await self._request("GET", f"/v1/incidents/{incident_number}/timeline")
68
+
69
+ # -- On-Call --
70
+
71
+ async def get_oncall(self) -> dict:
72
+ return await self._request("GET", "/v2/oncall/current")
73
+
74
+ async def get_team_oncall_schedule(
75
+ self, team_slug: str, days_forward: int = 14, days_skip: int = 0,
76
+ ) -> dict:
77
+ params = {"daysForward": days_forward, "daysSkip": days_skip}
78
+ return await self._request(
79
+ "GET", f"/v2/team/{team_slug}/oncall/schedule", params=params,
80
+ )
81
+
82
+ # -- Teams --
83
+
84
+ async def list_teams(self) -> dict:
85
+ return await self._request("GET", "/v1/team")
86
+
87
+ async def get_team(self, team_slug: str) -> dict:
88
+ return await self._request("GET", f"/v1/team/{team_slug}")
89
+
90
+ async def get_team_members(self, team_slug: str) -> dict:
91
+ return await self._request("GET", f"/v1/team/{team_slug}/members")
92
+
93
+ async def get_team_policies(self, team_slug: str) -> dict:
94
+ return await self._request("GET", f"/v1/team/{team_slug}/policies")
95
+
96
+ # -- Users --
97
+
98
+ async def list_users(self) -> dict:
99
+ return await self._request("GET", "/v1/user")
100
+
101
+ async def get_user(self, user_name: str) -> dict:
102
+ return await self._request("GET", f"/v1/user/{user_name}")
103
+
104
+ async def get_user_oncall_schedule(self, user_name: str) -> dict:
105
+ return await self._request("GET", f"/v1/user/{user_name}/oncall/schedule")
106
+
107
+ # -- Routing Keys --
108
+
109
+ async def list_routing_keys(self) -> dict:
110
+ return await self._request("GET", "/v1/org/routing-keys")
111
+
112
+ # -- Escalation Policies --
113
+
114
+ async def list_policies(self) -> dict:
115
+ return await self._request("GET", "/v1/policies")
116
+
117
+ async def get_policy(self, policy_slug: str) -> dict:
118
+ return await self._request("GET", f"/v1/policies/{policy_slug}")
119
+
120
+ # -- Maintenance Mode --
121
+
122
+ async def list_maintenance(self) -> dict:
123
+ return await self._request("GET", "/v1/maintenancemode")
124
+
125
+ async def create_maintenance(
126
+ self,
127
+ names: list[str],
128
+ purpose: str,
129
+ start_date: str | None = None,
130
+ end_date: str | None = None,
131
+ is_global: bool = False,
132
+ ) -> dict:
133
+ body: dict = {"names": names, "purpose": purpose, "isGlobal": is_global}
134
+ if start_date:
135
+ body["startDate"] = start_date
136
+ if end_date:
137
+ body["endDate"] = end_date
138
+ return await self._request("POST", "/v1/maintenancemode", json=body)
139
+
140
+ async def end_maintenance(self, maintenance_id: str) -> dict:
141
+ return await self._request("DELETE", f"/v1/maintenancemode/{maintenance_id}")
142
+
143
+ # -- Organization --
144
+
145
+ async def get_org(self) -> dict:
146
+ return await self._request("GET", "/v1/org")
147
+
148
+ # -- Reporting --
149
+
150
+ async def get_incident_history(
151
+ self, start: str | None = None, end: str | None = None,
152
+ ) -> dict:
153
+ params: dict = {}
154
+ if start:
155
+ params["startedAfter"] = start
156
+ if end:
157
+ params["startedBefore"] = end
158
+ return await self._request("GET", "/v2/reporting/incidents", params=params)
@@ -0,0 +1,316 @@
1
+ """MCP server for Splunk On-Call (VictorOps) incident management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+
10
+ from .client import SplunkOnCallClient
11
+
12
+ mcp = FastMCP(
13
+ "splunk-oncall",
14
+ instructions="Manage Splunk On-Call (VictorOps) incidents, on-call schedules, and maintenance windows",
15
+ )
16
+
17
+ _client: SplunkOnCallClient | None = None
18
+
19
+
20
+ def _get_client() -> SplunkOnCallClient:
21
+ global _client
22
+ if _client is None:
23
+ api_id = os.environ.get("SPLUNK_ONCALL_API_ID")
24
+ api_key = os.environ.get("SPLUNK_ONCALL_API_KEY")
25
+ if not api_id or not api_key:
26
+ raise ValueError(
27
+ "SPLUNK_ONCALL_API_ID and SPLUNK_ONCALL_API_KEY environment variables are required"
28
+ )
29
+ _client = SplunkOnCallClient(api_id, api_key)
30
+ return _client
31
+
32
+
33
+ def _fmt(data: dict) -> str:
34
+ return json.dumps(data, indent=2, default=str)
35
+
36
+
37
+ # -- Incidents --
38
+
39
+
40
+ @mcp.tool()
41
+ async def list_incidents() -> str:
42
+ """List all current incidents (triggered, acknowledged, resolved)."""
43
+ result = await _get_client().list_incidents()
44
+ return _fmt(result)
45
+
46
+
47
+ @mcp.tool()
48
+ async def acknowledge_incidents(
49
+ user_name: str, incident_numbers: list[str], message: str = "",
50
+ ) -> str:
51
+ """Acknowledge one or more incidents.
52
+
53
+ Args:
54
+ user_name: Your Splunk On-Call username
55
+ incident_numbers: List of incident numbers to acknowledge (e.g. ["123", "456"])
56
+ message: Optional message to attach to the acknowledgment
57
+ """
58
+ result = await _get_client().acknowledge_incidents(
59
+ user_name, incident_numbers, message or None,
60
+ )
61
+ return _fmt(result)
62
+
63
+
64
+ @mcp.tool()
65
+ async def resolve_incidents(
66
+ user_name: str, incident_numbers: list[str], message: str = "",
67
+ ) -> str:
68
+ """Resolve one or more incidents.
69
+
70
+ Args:
71
+ user_name: Your Splunk On-Call username
72
+ incident_numbers: List of incident numbers to resolve (e.g. ["123", "456"])
73
+ message: Optional message to attach to the resolution
74
+ """
75
+ result = await _get_client().resolve_incidents(
76
+ user_name, incident_numbers, message or None,
77
+ )
78
+ return _fmt(result)
79
+
80
+
81
+ @mcp.tool()
82
+ async def reroute_incidents(
83
+ user_name: str,
84
+ incident_numbers: list[str],
85
+ target_user: str = "",
86
+ target_policy: str = "",
87
+ ) -> str:
88
+ """Reroute incidents to another user or escalation policy.
89
+
90
+ Args:
91
+ user_name: Your Splunk On-Call username
92
+ incident_numbers: List of incident numbers to reroute
93
+ target_user: Username to reroute to (provide this or target_policy)
94
+ target_policy: Escalation policy slug to reroute to (provide this or target_user)
95
+ """
96
+ targets = []
97
+ if target_user:
98
+ targets.append({"type": "User", "slug": target_user})
99
+ if target_policy:
100
+ targets.append({"type": "EscalationPolicy", "slug": target_policy})
101
+ if not targets:
102
+ return "Error: provide either target_user or target_policy"
103
+ result = await _get_client().reroute_incidents(user_name, incident_numbers, targets)
104
+ return _fmt(result)
105
+
106
+
107
+ @mcp.tool()
108
+ async def get_incident_timeline(incident_number: str) -> str:
109
+ """Get the event timeline for a specific incident.
110
+
111
+ Args:
112
+ incident_number: The incident number to get the timeline for
113
+ """
114
+ result = await _get_client().get_incident_timeline(incident_number)
115
+ return _fmt(result)
116
+
117
+
118
+ # -- On-Call --
119
+
120
+
121
+ @mcp.tool()
122
+ async def get_oncall() -> str:
123
+ """Get who is currently on call across all teams and policies."""
124
+ result = await _get_client().get_oncall()
125
+ return _fmt(result)
126
+
127
+
128
+ @mcp.tool()
129
+ async def get_team_oncall_schedule(
130
+ team_slug: str, days_forward: int = 14,
131
+ ) -> str:
132
+ """Get the on-call schedule for a specific team.
133
+
134
+ Args:
135
+ team_slug: The team slug identifier
136
+ days_forward: Number of days to look ahead (default 14)
137
+ """
138
+ result = await _get_client().get_team_oncall_schedule(team_slug, days_forward)
139
+ return _fmt(result)
140
+
141
+
142
+ @mcp.tool()
143
+ async def get_user_oncall_schedule(user_name: str) -> str:
144
+ """Get the on-call schedule for a specific user.
145
+
146
+ Args:
147
+ user_name: The username to look up
148
+ """
149
+ result = await _get_client().get_user_oncall_schedule(user_name)
150
+ return _fmt(result)
151
+
152
+
153
+ # -- Teams --
154
+
155
+
156
+ @mcp.tool()
157
+ async def list_teams() -> str:
158
+ """List all teams in the organization."""
159
+ result = await _get_client().list_teams()
160
+ return _fmt(result)
161
+
162
+
163
+ @mcp.tool()
164
+ async def get_team_members(team_slug: str) -> str:
165
+ """List members of a specific team.
166
+
167
+ Args:
168
+ team_slug: The team slug identifier
169
+ """
170
+ result = await _get_client().get_team_members(team_slug)
171
+ return _fmt(result)
172
+
173
+
174
+ @mcp.tool()
175
+ async def get_team_policies(team_slug: str) -> str:
176
+ """List escalation policies for a specific team.
177
+
178
+ Args:
179
+ team_slug: The team slug identifier
180
+ """
181
+ result = await _get_client().get_team_policies(team_slug)
182
+ return _fmt(result)
183
+
184
+
185
+ # -- Users --
186
+
187
+
188
+ @mcp.tool()
189
+ async def list_users() -> str:
190
+ """List all users in the organization."""
191
+ result = await _get_client().list_users()
192
+ return _fmt(result)
193
+
194
+
195
+ @mcp.tool()
196
+ async def get_user(user_name: str) -> str:
197
+ """Get details for a specific user.
198
+
199
+ Args:
200
+ user_name: The username to look up
201
+ """
202
+ result = await _get_client().get_user(user_name)
203
+ return _fmt(result)
204
+
205
+
206
+ # -- Routing Keys --
207
+
208
+
209
+ @mcp.tool()
210
+ async def list_routing_keys() -> str:
211
+ """List all routing keys and their associated escalation policies."""
212
+ result = await _get_client().list_routing_keys()
213
+ return _fmt(result)
214
+
215
+
216
+ # -- Escalation Policies --
217
+
218
+
219
+ @mcp.tool()
220
+ async def list_policies() -> str:
221
+ """List all escalation policies in the organization."""
222
+ result = await _get_client().list_policies()
223
+ return _fmt(result)
224
+
225
+
226
+ @mcp.tool()
227
+ async def get_policy(policy_slug: str) -> str:
228
+ """Get details for a specific escalation policy.
229
+
230
+ Args:
231
+ policy_slug: The escalation policy slug
232
+ """
233
+ result = await _get_client().get_policy(policy_slug)
234
+ return _fmt(result)
235
+
236
+
237
+ # -- Maintenance Mode --
238
+
239
+
240
+ @mcp.tool()
241
+ async def list_maintenance() -> str:
242
+ """List all active and scheduled maintenance windows."""
243
+ result = await _get_client().list_maintenance()
244
+ return _fmt(result)
245
+
246
+
247
+ @mcp.tool()
248
+ async def create_maintenance(
249
+ routing_keys: list[str],
250
+ purpose: str,
251
+ end_date: str = "",
252
+ start_date: str = "",
253
+ ) -> str:
254
+ """Create a maintenance window to suppress alerts.
255
+
256
+ Args:
257
+ routing_keys: List of routing key names to suppress alerts for
258
+ purpose: Description of why maintenance is being performed
259
+ end_date: ISO 8601 end time (e.g. "2026-04-09T06:00:00Z"). Required.
260
+ start_date: ISO 8601 start time. Defaults to now if omitted.
261
+ """
262
+ if not end_date:
263
+ return "Error: end_date is required"
264
+ result = await _get_client().create_maintenance(
265
+ names=routing_keys,
266
+ purpose=purpose,
267
+ start_date=start_date or None,
268
+ end_date=end_date,
269
+ )
270
+ return _fmt(result)
271
+
272
+
273
+ @mcp.tool()
274
+ async def end_maintenance(maintenance_id: str) -> str:
275
+ """End a maintenance window early.
276
+
277
+ Args:
278
+ maintenance_id: The maintenance window ID to end
279
+ """
280
+ result = await _get_client().end_maintenance(maintenance_id)
281
+ return _fmt(result)
282
+
283
+
284
+ # -- Organization --
285
+
286
+
287
+ @mcp.tool()
288
+ async def get_org_info() -> str:
289
+ """Get organization information."""
290
+ result = await _get_client().get_org()
291
+ return _fmt(result)
292
+
293
+
294
+ # -- Reporting --
295
+
296
+
297
+ @mcp.tool()
298
+ async def get_incident_history(start: str = "", end: str = "") -> str:
299
+ """Get historical incident data for reporting.
300
+
301
+ Args:
302
+ start: ISO 8601 start time filter (e.g. "2026-04-01T00:00:00Z")
303
+ end: ISO 8601 end time filter (e.g. "2026-04-08T00:00:00Z")
304
+ """
305
+ result = await _get_client().get_incident_history(
306
+ start=start or None, end=end or None,
307
+ )
308
+ return _fmt(result)
309
+
310
+
311
+ def main():
312
+ mcp.run()
313
+
314
+
315
+ if __name__ == "__main__":
316
+ main()