Skip to content

Commit ad336b6

Browse files
authored
Merge pull request #8 from arnesund/feature/pagination-support
Add pagination support to avoid overloading context window
2 parents 0ddb525 + 77c2d96 commit ad336b6

File tree

3 files changed

+523
-27
lines changed

3 files changed

+523
-27
lines changed

README.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Prometheus Alertmanager MCP is a [Model Context Protocol](https://modelcontextpr
2929
## 2. Features
3030

3131
- [x] Query Alertmanager status, alerts, silences, receivers, and alert groups
32+
- [x] **Smart pagination support** to prevent LLM context window overflow when handling large numbers of alerts
3233
- [x] Create, update, and delete silences
3334
- [x] Create new alerts
3435
- [x] Authentication support (Basic auth via environment variables)
@@ -177,13 +178,37 @@ This configuration passes the environment variables from Claude Desktop to the D
177178
## 4. Tools
178179

179180
The MCP server exposes tools for querying and managing Alertmanager, following [its API v2](https://github.com/prometheus/alertmanager/blob/main/api/v2/openapi.yaml):
180-
- Get status: `get_status()`
181-
- List alerts: `get_alerts()`
182-
- List silences: `get_silences()`
183-
- Create silence: `post_silence(silence_dict)`
184-
- Delete silence: `delete_silence(silence_id)`
185-
- List receivers: `get_receivers()`
186-
- List alert groups: `get_alert_groups()`
181+
182+
- **Get status**: `get_status()`
183+
- **List alerts**: `get_alerts(filter, silenced, inhibited, active, count, offset)`
184+
- **Pagination support**: Returns paginated results to avoid overwhelming LLM context
185+
- `count`: Number of alerts per page (default: 10, max: 25)
186+
- `offset`: Number of alerts to skip (default: 0)
187+
- Returns: `{ "data": [...], "pagination": { "total": N, "offset": M, "count": K, "has_more": bool } }`
188+
- **List silences**: `get_silences(filter, count, offset)`
189+
- **Pagination support**: Returns paginated results to avoid overwhelming LLM context
190+
- `count`: Number of silences per page (default: 10, max: 50)
191+
- `offset`: Number of silences to skip (default: 0)
192+
- Returns: `{ "data": [...], "pagination": { "total": N, "offset": M, "count": K, "has_more": bool } }`
193+
- **Create silence**: `post_silence(silence_dict)`
194+
- **Delete silence**: `delete_silence(silence_id)`
195+
- **List receivers**: `get_receivers()`
196+
- **List alert groups**: `get_alert_groups(silenced, inhibited, active, count, offset)`
197+
- **Pagination support**: Returns paginated results to avoid overwhelming LLM context
198+
- `count`: Number of alert groups per page (default: 3, max: 5)
199+
- `offset`: Number of alert groups to skip (default: 0)
200+
- Returns: `{ "data": [...], "pagination": { "total": N, "offset": M, "count": K, "has_more": bool } }`
201+
- Note: Alert groups have lower limits because they contain all alerts within each group
202+
203+
### Pagination Benefits
204+
205+
When working with environments that have many alerts, silences, or alert groups, the pagination feature helps:
206+
- **Prevent context overflow**: By default, only 10 items are returned per request
207+
- **Efficient browsing**: LLMs can iterate through results using `offset` and `count` parameters
208+
- **Smart limits**: Maximum of 50 items per page prevents excessive context usage
209+
- **Clear navigation**: `has_more` flag indicates when additional pages are available
210+
211+
Example: If you have 100 alerts, the LLM can fetch them in manageable chunks (e.g., 10 at a time) and only load what's needed for analysis.
187212

188213
See [src/alertmanager_mcp_server/server.py](src/alertmanager_mcp_server/server.py) for full API details.
189214

src/alertmanager_mcp_server/server.py

Lines changed: 157 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ class AlertmanagerConfig:
3333
password=os.environ.get("ALERTMANAGER_PASSWORD", ""),
3434
)
3535

36+
# Pagination defaults and limits (configurable via environment variables)
37+
DEFAULT_SILENCE_PAGE = int(os.environ.get("ALERTMANAGER_DEFAULT_SILENCE_PAGE", "10"))
38+
MAX_SILENCE_PAGE = int(os.environ.get("ALERTMANAGER_MAX_SILENCE_PAGE", "50"))
39+
DEFAULT_ALERT_PAGE = int(os.environ.get("ALERTMANAGER_DEFAULT_ALERT_PAGE", "10"))
40+
MAX_ALERT_PAGE = int(os.environ.get("ALERTMANAGER_MAX_ALERT_PAGE", "25"))
41+
DEFAULT_ALERT_GROUP_PAGE = int(os.environ.get("ALERTMANAGER_DEFAULT_ALERT_GROUP_PAGE", "3"))
42+
MAX_ALERT_GROUP_PAGE = int(os.environ.get("ALERTMANAGER_MAX_ALERT_GROUP_PAGE", "5"))
43+
3644

3745
def make_request(method="GET", route="/", **kwargs):
3846
"""Make HTTP request and return a requests.Response object.
@@ -67,6 +75,79 @@ def make_request(method="GET", route="/", **kwargs):
6775
return response.json()
6876

6977

78+
def validate_pagination_params(count: int, offset: int, max_count: int) -> tuple[int, int, Optional[str]]:
79+
"""Validate and normalize pagination parameters.
80+
81+
Parameters
82+
----------
83+
count : int
84+
Requested number of items per page
85+
offset : int
86+
Requested offset for pagination
87+
max_count : int
88+
Maximum allowed count value
89+
90+
Returns
91+
-------
92+
tuple[int, int, Optional[str]]
93+
A tuple of (normalized_count, normalized_offset, error_message).
94+
If error_message is not None, the parameters are invalid and should
95+
return an error to the caller.
96+
"""
97+
error = None
98+
99+
# Validate count parameter
100+
if count < 1:
101+
error = f"Count parameter ({count}) must be at least 1."
102+
elif count > max_count:
103+
error = (
104+
f"Count parameter ({count}) exceeds maximum allowed value ({max_count}). "
105+
f"Please use count <= {max_count} and paginate through results using the offset parameter."
106+
)
107+
108+
# Validate offset parameter
109+
if offset < 0:
110+
error = f"Offset parameter ({offset}) must be non-negative (>= 0)."
111+
112+
return count, offset, error
113+
114+
115+
def paginate_results(items: List[Any], count: int, offset: int) -> Dict[str, Any]:
116+
"""Apply pagination to a list of items and generate pagination metadata.
117+
118+
Parameters
119+
----------
120+
items : List[Any]
121+
The full list of items to paginate
122+
count : int
123+
Number of items to return per page (must be >= 1)
124+
offset : int
125+
Number of items to skip (must be >= 0)
126+
127+
Returns
128+
-------
129+
Dict[str, Any]
130+
A dictionary containing:
131+
- data: List of items for the current page
132+
- pagination: Metadata including total, offset, count, requested_count, and has_more
133+
"""
134+
total = len(items)
135+
end_index = offset + count
136+
paginated_items = items[offset:end_index]
137+
has_more = end_index < total
138+
139+
return {
140+
"data": paginated_items,
141+
"pagination": {
142+
"total": total,
143+
"offset": offset,
144+
"count": len(paginated_items),
145+
"requested_count": count,
146+
"has_more": has_more
147+
}
148+
}
149+
150+
70151
@mcp.tool(description="Get current status of an Alertmanager instance and its cluster")
71152
async def get_status():
72153
"""Get current status of an Alertmanager instance and its cluster
@@ -93,24 +174,44 @@ async def get_receivers():
93174

94175

95176
@mcp.tool(description="Get list of all silences")
96-
async def get_silences(filter: Optional[str] = None):
177+
async def get_silences(filter: Optional[str] = None,
178+
count: int = DEFAULT_SILENCE_PAGE,
179+
offset: int = 0):
97180
"""Get list of all silences
98181
99182
Parameters
100183
----------
101184
filter
102185
Filtering query (e.g. alertname=~'.*CPU.*')"),
186+
count
187+
Number of silences to return per page (default: 10, max: 50).
188+
offset
189+
Number of silences to skip before returning results (default: 0).
190+
To paginate through all results, make multiple calls with increasing
191+
offset values (e.g., offset=0, offset=10, offset=20, etc.).
103192
104193
Returns
105194
-------
106-
list:
107-
Return a list of Silence objects from Alertmanager instance.
195+
dict
196+
A dictionary containing:
197+
- data: List of Silence objects for the current page
198+
- pagination: Metadata about pagination (total, offset, count, has_more)
199+
Use the 'has_more' flag to determine if additional pages are available.
108200
"""
201+
# Validate pagination parameters
202+
count, offset, error = validate_pagination_params(count, offset, MAX_SILENCE_PAGE)
203+
if error:
204+
return {"error": error}
109205

110206
params = None
111207
if filter:
112208
params = {"filter": filter}
113-
return make_request(method="GET", route="/api/v2/silences", params=params)
209+
210+
# Get all silences from the API
211+
all_silences = make_request(method="GET", route="/api/v2/silences", params=params)
212+
213+
# Apply pagination and return results
214+
return paginate_results(all_silences, count, offset)
114215

115216

116217
@mcp.tool(description="Post a new silence or update an existing one")
@@ -176,7 +277,9 @@ async def delete_silence(silence_id: str):
176277
async def get_alerts(filter: Optional[str] = None,
177278
silenced: Optional[bool] = None,
178279
inhibited: Optional[bool] = None,
179-
active: Optional[bool] = None):
280+
active: Optional[bool] = None,
281+
count: int = DEFAULT_ALERT_PAGE,
282+
offset: int = 0):
180283
"""Get a list of alerts currently in Alertmanager.
181284
182285
Params
@@ -189,12 +292,26 @@ async def get_alerts(filter: Optional[str] = None,
189292
If true, include inhibited alerts.
190293
active
191294
If true, include active alerts.
295+
count
296+
Number of alerts to return per page (default: 10, max: 25).
297+
offset
298+
Number of alerts to skip before returning results (default: 0).
299+
To paginate through all results, make multiple calls with increasing
300+
offset values (e.g., offset=0, offset=10, offset=20, etc.).
192301
193302
Returns
194303
-------
195-
list
196-
Return a list of Alert objects from Alertmanager instance.
304+
dict
305+
A dictionary containing:
306+
- data: List of Alert objects for the current page
307+
- pagination: Metadata about pagination (total, offset, count, has_more)
308+
Use the 'has_more' flag to determine if additional pages are available.
197309
"""
310+
# Validate pagination parameters
311+
count, offset, error = validate_pagination_params(count, offset, MAX_ALERT_PAGE)
312+
if error:
313+
return {"error": error}
314+
198315
params = {"active": True}
199316
if filter:
200317
params = {"filter": filter}
@@ -204,7 +321,12 @@ async def get_alerts(filter: Optional[str] = None,
204321
params["inhibited"] = inhibited
205322
if active is not None:
206323
params["active"] = active
207-
return make_request(method="GET", route="/api/v2/alerts", params=params)
324+
325+
# Get all alerts from the API
326+
all_alerts = make_request(method="GET", route="/api/v2/alerts", params=params)
327+
328+
# Apply pagination and return results
329+
return paginate_results(all_alerts, count, offset)
208330

209331

210332
@mcp.tool(description="Create new alerts")
@@ -234,7 +356,9 @@ async def post_alerts(alerts: List[Dict]):
234356
@mcp.tool(description="Get a list of alert groups")
235357
async def get_alert_groups(silenced: Optional[bool] = None,
236358
inhibited: Optional[bool] = None,
237-
active: Optional[bool] = None):
359+
active: Optional[bool] = None,
360+
count: int = DEFAULT_ALERT_GROUP_PAGE,
361+
offset: int = 0):
238362
"""Get a list of alert groups
239363
240364
Params
@@ -245,21 +369,41 @@ async def get_alert_groups(silenced: Optional[bool] = None,
245369
If true, include inhibited alerts.
246370
active
247371
If true, include active alerts.
372+
count
373+
Number of alert groups to return per page (default: 3, max: 5).
374+
Alert groups can be large as they contain all alerts within the group.
375+
offset
376+
Number of alert groups to skip before returning results (default: 0).
377+
To paginate through all results, make multiple calls with increasing
378+
offset values (e.g., offset=0, offset=3, offset=6, etc.).
248379
249380
Returns
250381
-------
251-
list
252-
Return a list of AlertGroup objects from Alertmanager instance.
382+
dict
383+
A dictionary containing:
384+
- data: List of AlertGroup objects for the current page
385+
- pagination: Metadata about pagination (total, offset, count, has_more)
386+
Use the 'has_more' flag to determine if additional pages are available.
253387
"""
388+
# Validate pagination parameters
389+
count, offset, error = validate_pagination_params(count, offset, MAX_ALERT_GROUP_PAGE)
390+
if error:
391+
return {"error": error}
392+
254393
params = {"active": True}
255394
if silenced is not None:
256395
params["silenced"] = silenced
257396
if inhibited is not None:
258397
params["inhibited"] = inhibited
259398
if active is not None:
260399
params["active"] = active
261-
return make_request(method="GET", route="/api/v2/alerts/groups",
262-
params=params)
400+
401+
# Get all alert groups from the API
402+
all_groups = make_request(method="GET", route="/api/v2/alerts/groups",
403+
params=params)
404+
405+
# Apply pagination and return results
406+
return paginate_results(all_groups, count, offset)
263407

264408

265409
def setup_environment():

0 commit comments

Comments
 (0)