status_mcp 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +26 -6
- data/lib/status_mcp/fetch_budget.rb +25 -0
- data/lib/status_mcp/network_policy.rb +55 -0
- data/lib/status_mcp/server.rb +88 -48
- data/lib/status_mcp/version.rb +1 -1
- data/lib/status_mcp.rb +1 -0
- metadata +121 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a0271e4490a1ed9fc6c565061e640d192db1c76dfcd2ab58b8928ff232acf95
|
|
4
|
+
data.tar.gz: c6dc17f1b39bd6642c2f637c404bcfa33ee71c6e83ea92b800b75e1835e61bb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d2fac4896efa144cbce6e096f8166aff1b5004eeb9f58d1b2ffc912b5d6191a6ac1fd2de3f771b25b160dd63d51ebe5da3d24ce0a10497a0fbc7248ffa225ba
|
|
7
|
+
data.tar.gz: 8f0a30f18e0a99c7371da9e0ad9425d24fad73c72a60988305d94b346551fff2fb0f97046ba1cf7781d94d9b396f51cfd30820a69e9c4b967aa1e716cb190319
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.1.1 (2026-09-08)
|
|
4
|
+
|
|
5
|
+
- Clamp `list_services` to 200 results and `fetch_status` extract length to 10000 characters
|
|
6
|
+
- Bound `fetch_status` HTTP hops and wall clock, and cap HTTP bodies at 1MB before they fill memory
|
|
7
|
+
- Fetch status pages live over HTTP; search, details, and list still use the bundled catalog
|
|
8
|
+
- Raise the json floor to 2.21.2 and require patched nokogiri 1.19.4 and rack 3.2.6
|
|
9
|
+
- Require Ruby 3.4, matching the gemspec and CI
|
|
10
|
+
|
|
3
11
|
## 0.1.0 (2025-11-26)
|
|
4
12
|
|
|
5
13
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# status_mcp
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/status_mcp) [](https://github.com/amkisko/status_mcp.rb/actions/workflows/test.yml) [](https://codecov.io/
|
|
3
|
+
[](https://badge.fury.io/rb/status_mcp) [](https://github.com/amkisko/status_mcp.rb/actions/workflows/test.yml) [](https://app.codecov.io/github/amkisko/status_mcp.rb)
|
|
4
4
|
|
|
5
5
|
Ruby gem providing status page information from [awesome-status](https://github.com/amkisko/awesome-status) via MCP (Model Context Protocol) server tools. Integrates with MCP-compatible clients like Cursor IDE, Claude Desktop, and other MCP-enabled tools.
|
|
6
6
|
|
|
@@ -10,9 +10,13 @@ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
|
10
10
|
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
11
11
|
</a>
|
|
12
12
|
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
This project is in low-priority support / maintenance mode. Prefer [status-cli](https://github.com/amkisko/status-cli.rs) for new status-page CLI and automation work. Existing installs keep working; expect security and critical fixes only, not new features.
|
|
16
|
+
|
|
13
17
|
## Requirements
|
|
14
18
|
|
|
15
|
-
- **Ruby 3.
|
|
19
|
+
- **Ruby 3.4 or higher**
|
|
16
20
|
|
|
17
21
|
## Quick Start
|
|
18
22
|
|
|
@@ -81,8 +85,8 @@ The server will start and communicate via STDIN/STDOUT using the MCP protocol.
|
|
|
81
85
|
|
|
82
86
|
- **Status Page Information**: Access to over 1700 status page links from `awesome-status`
|
|
83
87
|
- **MCP Server Integration**: Ready-to-use MCP server with tools for searching and retrieving status page details
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
88
|
+
- **Bundled catalog**: Search, details, and list tools read local data from the gem
|
|
89
|
+
- **Live fetch**: `fetch_status` requests the public status URL over HTTP
|
|
86
90
|
|
|
87
91
|
## MCP Tools
|
|
88
92
|
|
|
@@ -94,8 +98,11 @@ The MCP server provides the following tools:
|
|
|
94
98
|
2. **get_service_details** - Get detailed status links for a specific service
|
|
95
99
|
- Parameters: `name` (string)
|
|
96
100
|
|
|
97
|
-
3. **list_services** - List
|
|
98
|
-
- Parameters: `limit` (optional integer, default: 50)
|
|
101
|
+
3. **list_services** - List available services
|
|
102
|
+
- Parameters: `limit` (optional integer, default: 50, maximum: 200)
|
|
103
|
+
|
|
104
|
+
4. **fetch_status** - Fetch a public status page over HTTP and extract latest status, history, and messages
|
|
105
|
+
- Parameters: `status_url` (string), `max_length` (optional integer, default: 10000, maximum: 10000)
|
|
99
106
|
|
|
100
107
|
## Development
|
|
101
108
|
|
|
@@ -124,6 +131,19 @@ For more information, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
124
131
|
|
|
125
132
|
If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for details.
|
|
126
133
|
|
|
134
|
+
## Links
|
|
135
|
+
|
|
136
|
+
- [GitHub](https://github.com/amkisko/status_mcp.rb)
|
|
137
|
+
- [GitLab](https://gitlab.com/amkisko/status_mcp.rb)
|
|
138
|
+
- [RubyGems](https://rubygems.org/gems/status_mcp)
|
|
139
|
+
- [Versions Atom](https://rubygems.org/gems/status_mcp/versions.atom) (feed id `10002864981316`)
|
|
140
|
+
- [libraries.io](https://libraries.io/rubygems/status_mcp)
|
|
141
|
+
- [Deps.dev](https://deps.dev/rubygems/status_mcp)
|
|
142
|
+
- [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_status_mcp.rb)
|
|
143
|
+
- [Snyk](https://snyk.io/test/github/amkisko/status_mcp.rb)
|
|
144
|
+
- [Codecov](https://app.codecov.io/github/amkisko/status_mcp.rb)
|
|
145
|
+
- [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/status_mcp.rb)
|
|
146
|
+
|
|
127
147
|
## License
|
|
128
148
|
|
|
129
149
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatusMcp
|
|
4
|
+
class FetchBudget
|
|
5
|
+
class ExhaustedError < Error; end
|
|
6
|
+
|
|
7
|
+
def self.monotonic_now
|
|
8
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(max_requests:, deadline_seconds:)
|
|
12
|
+
@remaining = max_requests
|
|
13
|
+
@deadline_at = self.class.monotonic_now + deadline_seconds
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def consume!
|
|
17
|
+
raise ExhaustedError, "Fetch hop budget exceeded" if @remaining <= 0
|
|
18
|
+
if self.class.monotonic_now >= @deadline_at
|
|
19
|
+
raise ExhaustedError, "Fetch deadline exceeded"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@remaining -= 1
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module StatusMcp
|
|
8
|
+
class NetworkPolicy
|
|
9
|
+
ALLOWED_SCHEMES = %w[http https].freeze
|
|
10
|
+
FORBIDDEN_RANGES = %w[
|
|
11
|
+
0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8
|
|
12
|
+
169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.0.2.0/24
|
|
13
|
+
192.88.99.0/24 192.168.0.0/16 198.18.0.0/15 198.51.100.0/24
|
|
14
|
+
203.0.113.0/24 224.0.0.0/4 240.0.0.0/4
|
|
15
|
+
::/128 ::1/128 64:ff9b::/96 100::/64 2001::/23 2002::/16
|
|
16
|
+
fc00::/7 fe80::/10 ff00::/8
|
|
17
|
+
].map { |range| IPAddr.new(range) }.freeze
|
|
18
|
+
|
|
19
|
+
def self.validate!(url)
|
|
20
|
+
uri = URI.parse(url.to_s)
|
|
21
|
+
reject! unless ALLOWED_SCHEMES.include?(uri.scheme&.downcase)
|
|
22
|
+
reject! if uri.host.to_s.empty? || uri.userinfo
|
|
23
|
+
|
|
24
|
+
[uri, allowed_ip_address(uri)]
|
|
25
|
+
rescue URI::InvalidURIError
|
|
26
|
+
reject!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.allowed_ip_address(uri)
|
|
30
|
+
addresses = Addrinfo.getaddrinfo(uri.host, uri.port, nil, :STREAM)
|
|
31
|
+
.map(&:ip_address)
|
|
32
|
+
.uniq
|
|
33
|
+
reject! if addresses.empty? || addresses.any? { |address| forbidden?(address) }
|
|
34
|
+
|
|
35
|
+
addresses.first
|
|
36
|
+
rescue SocketError, SystemCallError
|
|
37
|
+
reject!
|
|
38
|
+
end
|
|
39
|
+
private_class_method :allowed_ip_address
|
|
40
|
+
|
|
41
|
+
def self.forbidden?(address)
|
|
42
|
+
parsed = IPAddr.new(address.split("%", 2).first)
|
|
43
|
+
parsed = parsed.native if parsed.ipv4_mapped?
|
|
44
|
+
FORBIDDEN_RANGES.any? { |range| range.include?(parsed) }
|
|
45
|
+
rescue IPAddr::InvalidAddressError
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
private_class_method :forbidden?
|
|
49
|
+
|
|
50
|
+
def self.reject!
|
|
51
|
+
raise UnsafeUrlError, "URL destination is not allowed"
|
|
52
|
+
end
|
|
53
|
+
private_class_method :reject!
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/status_mcp/server.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "uri"
|
|
|
7
7
|
require "openssl"
|
|
8
8
|
require "nokogiri"
|
|
9
9
|
require_relative "../status_mcp"
|
|
10
|
+
require_relative "network_policy"
|
|
11
|
+
require_relative "fetch_budget"
|
|
10
12
|
|
|
11
13
|
module StatusMcp
|
|
12
14
|
class Server
|
|
@@ -191,16 +193,18 @@ module StatusMcp
|
|
|
191
193
|
end
|
|
192
194
|
|
|
193
195
|
class ListServicesTool < BaseTool
|
|
196
|
+
MAX_LIST_LIMIT = 200
|
|
197
|
+
|
|
194
198
|
tool_name "list_services"
|
|
195
|
-
description "List
|
|
199
|
+
description "List available services. Default 50. Maximum 200."
|
|
196
200
|
|
|
197
201
|
arguments do
|
|
198
|
-
optional(:limit).filled(:integer).description("Limit number of results (default 50)")
|
|
202
|
+
optional(:limit).filled(:integer).description("Limit number of results (default 50, maximum 200)")
|
|
199
203
|
end
|
|
200
204
|
|
|
201
205
|
def call(limit: 50)
|
|
202
206
|
services = load_data
|
|
203
|
-
limit
|
|
207
|
+
limit = (limit || 50).clamp(1, MAX_LIST_LIMIT)
|
|
204
208
|
|
|
205
209
|
list = services.take(limit).map { |s| s["name"] }
|
|
206
210
|
|
|
@@ -218,16 +222,27 @@ module StatusMcp
|
|
|
218
222
|
class FetchStatusTool < BaseTool
|
|
219
223
|
# Maximum response size (1MB) to protect against zip bombs and crawler protection pages
|
|
220
224
|
MAX_RESPONSE_SIZE = 1 * 1024 * 1024 # 1MB
|
|
225
|
+
MAX_EXTRACT_LENGTH = 10_000
|
|
226
|
+
# Two common feed paths plus main, history, and a few redirects. Raise MAX_FETCH_REQUESTS if a vendor needs a longer hop chain.
|
|
227
|
+
MAX_FEED_PROBES = 2
|
|
228
|
+
MAX_FETCH_REQUESTS = 6
|
|
229
|
+
FETCH_DEADLINE_SECONDS = 20
|
|
221
230
|
|
|
222
231
|
tool_name "fetch_status"
|
|
223
232
|
description "Fetch status from a status_url with HTML purification. Extracts latest status, history, and messages from status pages."
|
|
224
233
|
|
|
225
234
|
arguments do
|
|
226
235
|
required(:status_url).filled(:string).description("Status page URL to fetch")
|
|
227
|
-
optional(:max_length).filled(:integer).description("Maximum length of extracted text in characters (default: 10000)")
|
|
236
|
+
optional(:max_length).filled(:integer).description("Maximum length of extracted text in characters (default: 10000, maximum: 10000)")
|
|
228
237
|
end
|
|
229
238
|
|
|
230
239
|
def call(status_url:, max_length: 10000)
|
|
240
|
+
max_length = (max_length || MAX_EXTRACT_LENGTH).clamp(1, MAX_EXTRACT_LENGTH)
|
|
241
|
+
@fetch_budget = StatusMcp::FetchBudget.new(
|
|
242
|
+
max_requests: MAX_FETCH_REQUESTS,
|
|
243
|
+
deadline_seconds: FETCH_DEADLINE_SECONDS
|
|
244
|
+
)
|
|
245
|
+
|
|
231
246
|
# Try incident.io API first (only if we detect it's an incident.io page)
|
|
232
247
|
api_info = nil
|
|
233
248
|
api_url = nil
|
|
@@ -242,7 +257,11 @@ module StatusMcp
|
|
|
242
257
|
api_info = nil
|
|
243
258
|
end
|
|
244
259
|
end
|
|
245
|
-
rescue
|
|
260
|
+
rescue StatusMcp::UnsafeUrlError, StatusMcp::ResponseSizeExceededError
|
|
261
|
+
raise
|
|
262
|
+
rescue StatusMcp::FetchBudget::ExhaustedError
|
|
263
|
+
api_info = nil
|
|
264
|
+
rescue
|
|
246
265
|
# Not an incident.io page or API failed, continue with other methods
|
|
247
266
|
end
|
|
248
267
|
end
|
|
@@ -259,8 +278,11 @@ module StatusMcp
|
|
|
259
278
|
successful_feed_url = feed_url
|
|
260
279
|
break
|
|
261
280
|
end
|
|
262
|
-
rescue
|
|
263
|
-
|
|
281
|
+
rescue StatusMcp::UnsafeUrlError, StatusMcp::ResponseSizeExceededError
|
|
282
|
+
raise
|
|
283
|
+
rescue StatusMcp::FetchBudget::ExhaustedError
|
|
284
|
+
break
|
|
285
|
+
rescue
|
|
264
286
|
next
|
|
265
287
|
end
|
|
266
288
|
end
|
|
@@ -269,8 +291,11 @@ module StatusMcp
|
|
|
269
291
|
main_info = nil
|
|
270
292
|
begin
|
|
271
293
|
main_info = fetch_and_extract(status_url, max_length)
|
|
272
|
-
rescue
|
|
273
|
-
|
|
294
|
+
rescue StatusMcp::UnsafeUrlError, StatusMcp::ResponseSizeExceededError
|
|
295
|
+
raise
|
|
296
|
+
rescue StatusMcp::FetchBudget::ExhaustedError
|
|
297
|
+
main_info = {latest_status: nil, history: [], messages: [], error: nil} unless feed_info || api_info
|
|
298
|
+
rescue
|
|
274
299
|
main_info = {latest_status: nil, history: [], messages: [], error: nil} unless feed_info
|
|
275
300
|
end
|
|
276
301
|
|
|
@@ -281,9 +306,12 @@ module StatusMcp
|
|
|
281
306
|
if history_url && history_url != status_url
|
|
282
307
|
begin
|
|
283
308
|
history_info = fetch_and_extract(history_url, max_length, history_only: true)
|
|
284
|
-
rescue
|
|
285
|
-
|
|
286
|
-
|
|
309
|
+
rescue StatusMcp::UnsafeUrlError, StatusMcp::ResponseSizeExceededError
|
|
310
|
+
raise
|
|
311
|
+
rescue StatusMcp::FetchBudget::ExhaustedError
|
|
312
|
+
history_info = nil
|
|
313
|
+
rescue
|
|
314
|
+
# History page missing is common
|
|
287
315
|
end
|
|
288
316
|
end
|
|
289
317
|
|
|
@@ -348,6 +376,8 @@ module StatusMcp
|
|
|
348
376
|
rescue => e
|
|
349
377
|
error_message = if e.is_a?(StatusMcp::ResponseSizeExceededError)
|
|
350
378
|
"Response size limit exceeded: #{e.message}"
|
|
379
|
+
elsif e.is_a?(StatusMcp::UnsafeUrlError)
|
|
380
|
+
e.message
|
|
351
381
|
else
|
|
352
382
|
"Error fetching status: #{e.message}"
|
|
353
383
|
end
|
|
@@ -366,8 +396,11 @@ module StatusMcp
|
|
|
366
396
|
redirect_count = 0
|
|
367
397
|
|
|
368
398
|
while redirect_count < max_redirects
|
|
369
|
-
|
|
399
|
+
@fetch_budget&.consume!
|
|
400
|
+
|
|
401
|
+
uri, ip_address = NetworkPolicy.validate!(current_url)
|
|
370
402
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
403
|
+
http.ipaddr = ip_address
|
|
371
404
|
http.use_ssl = (uri.scheme == "https")
|
|
372
405
|
if http.use_ssl?
|
|
373
406
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
@@ -380,43 +413,52 @@ module StatusMcp
|
|
|
380
413
|
request["User-Agent"] = "Mozilla/5.0 (compatible; StatusMcp/1.0)"
|
|
381
414
|
request["Accept"] = accept
|
|
382
415
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
# Check response size before returning (protect against zip bombs and crawler protection)
|
|
395
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
396
|
-
# Check Content-Length header first if available (optimization to avoid reading large bodies)
|
|
397
|
-
content_length = response["Content-Length"]
|
|
398
|
-
if content_length
|
|
399
|
-
content_length_int = content_length.to_i
|
|
400
|
-
if content_length_int > MAX_RESPONSE_SIZE
|
|
401
|
-
raise StatusMcp::ResponseSizeExceededError.new(content_length_int, MAX_RESPONSE_SIZE, uri: uri.to_s)
|
|
402
|
-
end
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
# Read body and check actual size (Content-Length might be missing or incorrect)
|
|
406
|
-
response_body = response.body || ""
|
|
407
|
-
response_size = response_body.bytesize
|
|
408
|
-
if response_size > MAX_RESPONSE_SIZE
|
|
409
|
-
raise StatusMcp::ResponseSizeExceededError.new(response_size, MAX_RESPONSE_SIZE, uri: uri.to_s)
|
|
416
|
+
redirected = false
|
|
417
|
+
captured = nil
|
|
418
|
+
http.request(request) do |response|
|
|
419
|
+
if response.is_a?(Net::HTTPRedirection) && response["location"]
|
|
420
|
+
drain_body(response)
|
|
421
|
+
redirect_count += 1
|
|
422
|
+
current_url = URI.join(current_url, response["location"]).to_s
|
|
423
|
+
redirected = true
|
|
424
|
+
else
|
|
425
|
+
attach_capped_body!(response, uri)
|
|
426
|
+
captured = response
|
|
410
427
|
end
|
|
411
428
|
end
|
|
412
429
|
|
|
413
|
-
|
|
430
|
+
next if redirected
|
|
431
|
+
return captured
|
|
414
432
|
end
|
|
415
433
|
|
|
416
|
-
# Too many redirects
|
|
417
434
|
raise "Too many redirects (max: #{max_redirects})"
|
|
418
435
|
end
|
|
419
436
|
|
|
437
|
+
def drain_body(response)
|
|
438
|
+
response.read_body { |_| }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def attach_capped_body!(response, uri)
|
|
442
|
+
content_length = response["Content-Length"]
|
|
443
|
+
if content_length
|
|
444
|
+
content_length_int = content_length.to_i
|
|
445
|
+
if content_length_int > MAX_RESPONSE_SIZE
|
|
446
|
+
drain_body(response)
|
|
447
|
+
raise StatusMcp::ResponseSizeExceededError.new(content_length_int, MAX_RESPONSE_SIZE, uri: uri.to_s)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
buffer = +""
|
|
452
|
+
response.read_body do |chunk|
|
|
453
|
+
next_size = buffer.bytesize + chunk.bytesize
|
|
454
|
+
if next_size > MAX_RESPONSE_SIZE
|
|
455
|
+
raise StatusMcp::ResponseSizeExceededError.new(next_size, MAX_RESPONSE_SIZE, uri: uri.to_s)
|
|
456
|
+
end
|
|
457
|
+
buffer << chunk
|
|
458
|
+
end
|
|
459
|
+
response.define_singleton_method(:body) { buffer }
|
|
460
|
+
end
|
|
461
|
+
|
|
420
462
|
def fetch_and_extract(url, max_length, history_only: false)
|
|
421
463
|
response = fetch_with_redirects(url)
|
|
422
464
|
http_status_code = response.code.to_i
|
|
@@ -523,8 +565,7 @@ module StatusMcp
|
|
|
523
565
|
|
|
524
566
|
# Parse JSON response
|
|
525
567
|
parse_incident_io_api(json_body, max_length)
|
|
526
|
-
rescue StatusMcp::ResponseSizeExceededError
|
|
527
|
-
# Re-raise response size errors
|
|
568
|
+
rescue StatusMcp::ResponseSizeExceededError, StatusMcp::UnsafeUrlError, StatusMcp::FetchBudget::ExhaustedError
|
|
528
569
|
raise
|
|
529
570
|
rescue JSON::ParserError => e
|
|
530
571
|
{
|
|
@@ -644,7 +685,7 @@ module StatusMcp
|
|
|
644
685
|
uri = URI(status_url)
|
|
645
686
|
base_path = uri.path.chomp("/")
|
|
646
687
|
|
|
647
|
-
# Common RSS/Atom feed patterns
|
|
688
|
+
# Common RSS/Atom feed patterns. MAX_FEED_PROBES is the known ceiling.
|
|
648
689
|
feed_patterns = [
|
|
649
690
|
"/feed.rss",
|
|
650
691
|
"/feed.atom",
|
|
@@ -653,7 +694,7 @@ module StatusMcp
|
|
|
653
694
|
"/feed",
|
|
654
695
|
"/status.rss",
|
|
655
696
|
"/status.atom"
|
|
656
|
-
]
|
|
697
|
+
].first(MAX_FEED_PROBES)
|
|
657
698
|
|
|
658
699
|
feed_urls = []
|
|
659
700
|
feed_patterns.each do |pattern|
|
|
@@ -686,8 +727,7 @@ module StatusMcp
|
|
|
686
727
|
|
|
687
728
|
# Parse RSS/Atom feed
|
|
688
729
|
parse_feed(feed_body, max_length)
|
|
689
|
-
rescue StatusMcp::ResponseSizeExceededError
|
|
690
|
-
# Re-raise response size errors
|
|
730
|
+
rescue StatusMcp::ResponseSizeExceededError, StatusMcp::UnsafeUrlError, StatusMcp::FetchBudget::ExhaustedError
|
|
691
731
|
raise
|
|
692
732
|
rescue => e
|
|
693
733
|
{
|
data/lib/status_mcp/version.rb
CHANGED
data/lib/status_mcp.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: status_mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -30,6 +30,9 @@ dependencies:
|
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '1.18'
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: 1.19.4
|
|
33
36
|
type: :runtime
|
|
34
37
|
prerelease: false
|
|
35
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +40,9 @@ dependencies:
|
|
|
37
40
|
- - "~>"
|
|
38
41
|
- !ruby/object:Gem::Version
|
|
39
42
|
version: '1.18'
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 1.19.4
|
|
40
46
|
- !ruby/object:Gem::Dependency
|
|
41
47
|
name: rack
|
|
42
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -44,6 +50,9 @@ dependencies:
|
|
|
44
50
|
- - "~>"
|
|
45
51
|
- !ruby/object:Gem::Version
|
|
46
52
|
version: '3.2'
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.2.6
|
|
47
56
|
type: :runtime
|
|
48
57
|
prerelease: false
|
|
49
58
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +60,9 @@ dependencies:
|
|
|
51
60
|
- - "~>"
|
|
52
61
|
- !ruby/object:Gem::Version
|
|
53
62
|
version: '3.2'
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 3.2.6
|
|
54
66
|
- !ruby/object:Gem::Dependency
|
|
55
67
|
name: base64
|
|
56
68
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +77,20 @@ dependencies:
|
|
|
65
77
|
- - "~>"
|
|
66
78
|
- !ruby/object:Gem::Version
|
|
67
79
|
version: '0.3'
|
|
80
|
+
- !ruby/object:Gem::Dependency
|
|
81
|
+
name: json
|
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: 2.21.2
|
|
87
|
+
type: :runtime
|
|
88
|
+
prerelease: false
|
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: 2.21.2
|
|
68
94
|
- !ruby/object:Gem::Dependency
|
|
69
95
|
name: rspec
|
|
70
96
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,6 +105,20 @@ dependencies:
|
|
|
79
105
|
- - "~>"
|
|
80
106
|
- !ruby/object:Gem::Version
|
|
81
107
|
version: '3.13'
|
|
108
|
+
- !ruby/object:Gem::Dependency
|
|
109
|
+
name: polyrun
|
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: 2.2.0
|
|
115
|
+
type: :development
|
|
116
|
+
prerelease: false
|
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ">="
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: 2.2.0
|
|
82
122
|
- !ruby/object:Gem::Dependency
|
|
83
123
|
name: webmock
|
|
84
124
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -100,6 +140,9 @@ dependencies:
|
|
|
100
140
|
- - "~>"
|
|
101
141
|
- !ruby/object:Gem::Version
|
|
102
142
|
version: '6.3'
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: 6.4.0
|
|
103
146
|
type: :development
|
|
104
147
|
prerelease: false
|
|
105
148
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -107,6 +150,9 @@ dependencies:
|
|
|
107
150
|
- - "~>"
|
|
108
151
|
- !ruby/object:Gem::Version
|
|
109
152
|
version: '6.3'
|
|
153
|
+
- - ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: 6.4.0
|
|
110
156
|
- !ruby/object:Gem::Dependency
|
|
111
157
|
name: rake
|
|
112
158
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -177,6 +223,76 @@ dependencies:
|
|
|
177
223
|
- - "~>"
|
|
178
224
|
- !ruby/object:Gem::Version
|
|
179
225
|
version: '1.52'
|
|
226
|
+
- !ruby/object:Gem::Dependency
|
|
227
|
+
name: standard-custom
|
|
228
|
+
requirement: !ruby/object:Gem::Requirement
|
|
229
|
+
requirements:
|
|
230
|
+
- - "~>"
|
|
231
|
+
- !ruby/object:Gem::Version
|
|
232
|
+
version: '1.0'
|
|
233
|
+
type: :development
|
|
234
|
+
prerelease: false
|
|
235
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
236
|
+
requirements:
|
|
237
|
+
- - "~>"
|
|
238
|
+
- !ruby/object:Gem::Version
|
|
239
|
+
version: '1.0'
|
|
240
|
+
- !ruby/object:Gem::Dependency
|
|
241
|
+
name: standard-performance
|
|
242
|
+
requirement: !ruby/object:Gem::Requirement
|
|
243
|
+
requirements:
|
|
244
|
+
- - "~>"
|
|
245
|
+
- !ruby/object:Gem::Version
|
|
246
|
+
version: '1.8'
|
|
247
|
+
type: :development
|
|
248
|
+
prerelease: false
|
|
249
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
250
|
+
requirements:
|
|
251
|
+
- - "~>"
|
|
252
|
+
- !ruby/object:Gem::Version
|
|
253
|
+
version: '1.8'
|
|
254
|
+
- !ruby/object:Gem::Dependency
|
|
255
|
+
name: standard-rspec
|
|
256
|
+
requirement: !ruby/object:Gem::Requirement
|
|
257
|
+
requirements:
|
|
258
|
+
- - "~>"
|
|
259
|
+
- !ruby/object:Gem::Version
|
|
260
|
+
version: '0.3'
|
|
261
|
+
type: :development
|
|
262
|
+
prerelease: false
|
|
263
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
264
|
+
requirements:
|
|
265
|
+
- - "~>"
|
|
266
|
+
- !ruby/object:Gem::Version
|
|
267
|
+
version: '0.3'
|
|
268
|
+
- !ruby/object:Gem::Dependency
|
|
269
|
+
name: rubocop-rspec
|
|
270
|
+
requirement: !ruby/object:Gem::Requirement
|
|
271
|
+
requirements:
|
|
272
|
+
- - "~>"
|
|
273
|
+
- !ruby/object:Gem::Version
|
|
274
|
+
version: '3.8'
|
|
275
|
+
type: :development
|
|
276
|
+
prerelease: false
|
|
277
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
278
|
+
requirements:
|
|
279
|
+
- - "~>"
|
|
280
|
+
- !ruby/object:Gem::Version
|
|
281
|
+
version: '3.8'
|
|
282
|
+
- !ruby/object:Gem::Dependency
|
|
283
|
+
name: rubocop-thread_safety
|
|
284
|
+
requirement: !ruby/object:Gem::Requirement
|
|
285
|
+
requirements:
|
|
286
|
+
- - "~>"
|
|
287
|
+
- !ruby/object:Gem::Version
|
|
288
|
+
version: '0.7'
|
|
289
|
+
type: :development
|
|
290
|
+
prerelease: false
|
|
291
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
292
|
+
requirements:
|
|
293
|
+
- - "~>"
|
|
294
|
+
- !ruby/object:Gem::Version
|
|
295
|
+
version: '0.7'
|
|
180
296
|
- !ruby/object:Gem::Dependency
|
|
181
297
|
name: appraisal
|
|
182
298
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -234,6 +350,8 @@ files:
|
|
|
234
350
|
- assets/data.json
|
|
235
351
|
- bin/status_mcp
|
|
236
352
|
- lib/status_mcp.rb
|
|
353
|
+
- lib/status_mcp/fetch_budget.rb
|
|
354
|
+
- lib/status_mcp/network_policy.rb
|
|
237
355
|
- lib/status_mcp/server.rb
|
|
238
356
|
- lib/status_mcp/version.rb
|
|
239
357
|
homepage: https://github.com/amkisko/status_mcp.rb
|
|
@@ -247,14 +365,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
247
365
|
requirements:
|
|
248
366
|
- - ">="
|
|
249
367
|
- !ruby/object:Gem::Version
|
|
250
|
-
version: '3.
|
|
368
|
+
version: '3.4'
|
|
251
369
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
252
370
|
requirements:
|
|
253
371
|
- - ">="
|
|
254
372
|
- !ruby/object:Gem::Version
|
|
255
373
|
version: '0'
|
|
256
374
|
requirements: []
|
|
257
|
-
rubygems_version:
|
|
375
|
+
rubygems_version: 4.0.6
|
|
258
376
|
specification_version: 4
|
|
259
377
|
summary: Status MCP (Model Context Protocol) server for status page information
|
|
260
378
|
test_files: []
|