rubygems_mcp 0.1.3 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 263030b634fd9d87f2a5d3d4ff68574e017438ec57a7caff2ebc0b5729dfccb2
4
- data.tar.gz: 8c9e6142f7c8c7cbb6695c5265c2a7655eb7606872963ec0db3bf5ffb9a7d140
3
+ metadata.gz: a1d6300f5e71b96d4ef7df1909e03c52555440cf794bc8afb46f5b1e6f75310e
4
+ data.tar.gz: 1727f6cbf963a35862eec6557da0d7008ea6fac4714614d2305aca1aec93a24c
5
5
  SHA512:
6
- metadata.gz: 0f6bc852cf223da1499bf87063b2f92acbc562d40b4bd7d7fdfd8a4af38bf79064616fb8c0275b50e1e1e148a390cf92fcbdd0547cd3d45ca96d7ccf6966c1ec
7
- data.tar.gz: 3ae00927439ed56b949e3d2a3ffa3830d87e3ee89d34d6e62adad8af51e7df54a33f2af870ee4b657b80300d335528b7627c8a2d986375a9a8aab7a81ce79516
6
+ metadata.gz: b4e6bf57c3302cac5c5c74e99b0060e12b192befec7d7007c36e81d41dd45f2892f12eb73e315a7502c29686ef38abbd0f652012ee9e310ca38eb96729590182
7
+ data.tar.gz: a756f8c5da8e2a7d36e4c9e1c07427a102e9ca97fae9ba895ff41ecfa37c4449d14c26e6991b656b74686cfa36ad9bfcfa42145136f9cd2ab72fb40d505ad614
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.4 (2025-11-26)
4
+
5
+ - Add `get_news_releases` tool to fetch all new gem releases from RubyGems.org with pagination (fetches from `/news?page=N`)
6
+ - Add `get_popular_releases` tool to fetch popular new gem releases from RubyGems.org with pagination (fetches from `/releases/popular?page=N`)
7
+ - Add pagination support for `search_gems` tool via `page` parameter (converts page to offset automatically)
8
+ - Remove old patch code: unused monkey patches for fast-mcp `send_error`
9
+
3
10
  ## 0.1.3 (2025-11-21)
4
11
 
5
12
  - Add Ruby roadmap tools: `get_ruby_roadmap` and `get_ruby_version_roadmap_details` for accessing Ruby version planning information from bugs.ruby-lang.org
data/README.md CHANGED
@@ -30,12 +30,18 @@ For Cursor IDE, create or update `.cursor/mcp.json` in your project:
30
30
  {
31
31
  "mcpServers": {
32
32
  "rubygems": {
33
- "command": "rubygems_mcp"
33
+ "command": "gem",
34
+ "args": ["exec", "rubygems_mcp"],
35
+ "env": {
36
+ "RUBY_VERSION": "3.4.7"
37
+ }
34
38
  }
35
39
  }
36
40
  }
37
41
  ```
38
42
 
43
+ **Note**: Using `gem exec` ensures the correct Ruby version is used. If you're using a Ruby version manager like [mise](https://mise.jdx.dev/) or [rbenv](https://github.com/rbenv/rbenv), set the `RUBY_VERSION` environment variable to match your desired Ruby version. The `gem exec` command will automatically use the correct Ruby version based on your version manager configuration.
44
+
39
45
  ### Claude Desktop Configuration
40
46
 
41
47
  For Claude Desktop, edit the MCP configuration file:
@@ -47,7 +53,11 @@ For Claude Desktop, edit the MCP configuration file:
47
53
  {
48
54
  "mcpServers": {
49
55
  "rubygems": {
50
- "command": "rubygems_mcp"
56
+ "command": "gem",
57
+ "args": ["exec", "rubygems_mcp"],
58
+ "env": {
59
+ "RUBY_VERSION": "3.4.7"
60
+ }
51
61
  }
52
62
  }
53
63
  }
@@ -214,6 +224,8 @@ recently_updated = client.get_recently_updated_gems(limit: 10)
214
224
 
215
225
  - `get_latest_gems(limit: 30)` - Get latest gems - most recently added gems to RubyGems.org
216
226
  - `get_recently_updated_gems(limit: 30)` - Get recently updated gems - most recently updated gem versions
227
+ - `get_news_releases(page: 1)` - Get news releases - all new gem releases from RubyGems.org with pagination (fetches from `/news?page=N`)
228
+ - `get_popular_releases(page: 1)` - Get popular releases - popular new gem releases from RubyGems.org with pagination (fetches from `/releases/popular?page=N`)
217
229
 
218
230
  ## MCP Server Integration
219
231
 
@@ -285,6 +297,14 @@ The MCP server provides the following tools:
285
297
  16. **get_ruby_version_github_changelog** - Get GitHub release changelog for a Ruby version from the ruby/ruby repository
286
298
  - Parameters: `version` (string, e.g., "3.4.7", "3.4.0")
287
299
 
300
+ 17. **get_news_releases** - Get news releases - all new gem releases from RubyGems.org with pagination
301
+ - Parameters: `page` (optional integer, default: 1) - Page number (1-based)
302
+ - Fetches from: `https://rubygems.org/news?page=N`
303
+
304
+ 18. **get_popular_releases** - Get popular releases - popular new gem releases from RubyGems.org with pagination
305
+ - Parameters: `page` (optional integer, default: 1) - Page number (1-based)
306
+ - Fetches from: `https://rubygems.org/releases/popular?page=N`
307
+
288
308
  ## MCP Resources
289
309
 
290
310
  The MCP server provides the following resources:
@@ -23,6 +23,7 @@ module RubygemsMcp
23
23
 
24
24
  RUBYGEMS_API_BASE = "https://rubygems.org/api/v1"
25
25
  RUBYGEMS_API_V2_BASE = "https://rubygems.org/api/v2"
26
+ RUBYGEMS_BASE = "https://rubygems.org"
26
27
  RUBY_RELEASES_URL = "https://www.ruby-lang.org/en/downloads/releases/"
27
28
  RUBY_BRANCHES_URL = "https://www.ruby-lang.org/en/downloads/branches/"
28
29
  RUBY_ROADMAP_URL = "https://bugs.ruby-lang.org/projects/ruby-master/roadmap"
@@ -1061,9 +1062,17 @@ module RubygemsMcp
1061
1062
  # @param query [String] Search query
1062
1063
  # @param limit [Integer, nil] Maximum number of results to return (nil = all)
1063
1064
  # @param offset [Integer] Number of results to skip (for pagination)
1065
+ # @param page [Integer, nil] Page number (1-based). If provided, overrides offset (page 1 = offset 0, page 2 = offset 30, etc.)
1064
1066
  # @return [Array<Hash>] Array of hashes with gem information
1065
- def search_gems(query, limit: nil, offset: 0)
1067
+ def search_gems(query, limit: nil, offset: 0, page: nil)
1066
1068
  raise ValidationError, "Search query cannot be empty" if query.nil? || query.strip.empty?
1069
+
1070
+ # Convert page to offset if provided (assuming 30 items per page, which is RubyGems default)
1071
+ if page
1072
+ raise ValidationError, "Page must be positive" if page < 1
1073
+ offset = (page - 1) * 30
1074
+ end
1075
+
1067
1076
  validate_pagination_params(limit: limit, offset: offset)
1068
1077
  # Don't cache search results as they can change frequently
1069
1078
  uri = URI("#{RUBYGEMS_API_BASE}/search.json")
@@ -1089,6 +1098,149 @@ module RubygemsMcp
1089
1098
  results
1090
1099
  end
1091
1100
 
1101
+ # Get news releases (all new gem releases) with pagination
1102
+ #
1103
+ # @param page [Integer] Page number (1-based, default: 1)
1104
+ # @return [Array<Hash>] Array of hashes with gem release information
1105
+ def get_news_releases(page: 1)
1106
+ raise ValidationError, "Page must be positive" if page < 1
1107
+ cache_key = "news_releases:#{page}"
1108
+
1109
+ if @cache_enabled
1110
+ cached = self.class.cache.get(cache_key)
1111
+ return cached if cached
1112
+ end
1113
+
1114
+ uri = URI("#{RUBYGEMS_BASE}/news")
1115
+ uri.query = URI.encode_www_form(page: page) if page > 1
1116
+
1117
+ response = make_request(uri, parse_html: true)
1118
+ return [] unless response
1119
+
1120
+ releases = []
1121
+ # RubyGems news page structure: each gem release is in a list item or similar structure
1122
+ # Look for gem links and extract information
1123
+ response.css("a[href^='/gems/']").each do |link|
1124
+ gem_name = link["href"].match(%r{/gems/([^/]+)})&.[](1)
1125
+ next unless gem_name
1126
+
1127
+ # Find the parent container to extract version and date
1128
+ container = link.ancestors.find { |e| e.name == "li" || e.name == "div" || e.name == "article" }
1129
+ next unless container
1130
+
1131
+ # Extract version from link text or nearby text
1132
+ version_match = link.text.match(/v?(\d+\.\d+\.\d+)/)
1133
+ version = version_match ? version_match[1] : nil
1134
+
1135
+ # Extract date (look for date patterns in nearby text)
1136
+ date_text = container.text
1137
+ date_match = date_text.match(/(\w+ \d{1,2}, \d{4})|(\d{4}-\d{2}-\d{2})/)
1138
+ release_date = if date_match
1139
+ begin
1140
+ Date.parse(date_match[0]).iso8601
1141
+ rescue Date::Error
1142
+ nil
1143
+ end
1144
+ end
1145
+
1146
+ # Extract downloads count if available
1147
+ downloads_match = container.text.match(/([\d,]+)\s*Downloads?/i)
1148
+ downloads = downloads_match ? downloads_match[1].delete(",").to_i : nil
1149
+
1150
+ # Extract description/info
1151
+ info_elem = container.css("p, .info, .description").first
1152
+ info = info_elem&.text&.strip
1153
+
1154
+ # Avoid duplicates
1155
+ next if releases.any? { |r| r[:name] == gem_name && r[:version] == version }
1156
+
1157
+ releases << {
1158
+ name: gem_name,
1159
+ version: version,
1160
+ release_date: release_date,
1161
+ downloads: downloads,
1162
+ info: info,
1163
+ gem_url: "#{RUBYGEMS_BASE}/gems/#{gem_name}"
1164
+ }
1165
+ end
1166
+
1167
+ # Cache for 15 minutes (news changes frequently)
1168
+ self.class.cache.set(cache_key, releases, 900) if @cache_enabled
1169
+
1170
+ releases
1171
+ end
1172
+
1173
+ # Get popular releases (popular new gem releases) with pagination
1174
+ #
1175
+ # @param page [Integer] Page number (1-based, default: 1)
1176
+ # @return [Array<Hash>] Array of hashes with popular gem release information
1177
+ def get_popular_releases(page: 1)
1178
+ raise ValidationError, "Page must be positive" if page < 1
1179
+ cache_key = "popular_releases:#{page}"
1180
+
1181
+ if @cache_enabled
1182
+ cached = self.class.cache.get(cache_key)
1183
+ return cached if cached
1184
+ end
1185
+
1186
+ uri = URI("#{RUBYGEMS_BASE}/releases/popular")
1187
+ uri.query = URI.encode_www_form(page: page) if page > 1
1188
+
1189
+ response = make_request(uri, parse_html: true)
1190
+ return [] unless response
1191
+
1192
+ releases = []
1193
+ # RubyGems popular releases page structure: similar to news page
1194
+ response.css("a[href^='/gems/']").each do |link|
1195
+ gem_name = link["href"].match(%r{/gems/([^/]+)})&.[](1)
1196
+ next unless gem_name
1197
+
1198
+ # Find the parent container to extract version and date
1199
+ container = link.ancestors.find { |e| e.name == "li" || e.name == "div" || e.name == "article" }
1200
+ next unless container
1201
+
1202
+ # Extract version from link text or nearby text
1203
+ version_match = link.text.match(/v?(\d+\.\d+\.\d+)/)
1204
+ version = version_match ? version_match[1] : nil
1205
+
1206
+ # Extract date (look for date patterns in nearby text)
1207
+ date_text = container.text
1208
+ date_match = date_text.match(/(\w+ \d{1,2}, \d{4})|(\d{4}-\d{2}-\d{2})/)
1209
+ release_date = if date_match
1210
+ begin
1211
+ Date.parse(date_match[0]).iso8601
1212
+ rescue Date::Error
1213
+ nil
1214
+ end
1215
+ end
1216
+
1217
+ # Extract downloads count if available
1218
+ downloads_match = container.text.match(/([\d,]+)\s*Downloads?/i)
1219
+ downloads = downloads_match ? downloads_match[1].delete(",").to_i : nil
1220
+
1221
+ # Extract description/info
1222
+ info_elem = container.css("p, .info, .description").first
1223
+ info = info_elem&.text&.strip
1224
+
1225
+ # Avoid duplicates
1226
+ next if releases.any? { |r| r[:name] == gem_name && r[:version] == version }
1227
+
1228
+ releases << {
1229
+ name: gem_name,
1230
+ version: version,
1231
+ release_date: release_date,
1232
+ downloads: downloads,
1233
+ info: info,
1234
+ gem_url: "#{RUBYGEMS_BASE}/gems/#{gem_name}"
1235
+ }
1236
+ end
1237
+
1238
+ # Cache for 15 minutes (popular releases change frequently)
1239
+ self.class.cache.set(cache_key, releases, 900) if @cache_enabled
1240
+
1241
+ releases
1242
+ end
1243
+
1092
1244
  # Validate gem name
1093
1245
  #
1094
1246
  # @param gem_name [String] Gem name to validate
@@ -10,39 +10,6 @@ require "securerandom"
10
10
  # Alias MCP to FastMcp for compatibility
11
11
  FastMcp = MCP unless defined?(FastMcp)
12
12
 
13
- # Monkey-patch fast-mcp to ensure error responses always have a valid id
14
- # JSON-RPC 2.0 allows id: null for notifications, but MCP clients (Cursor/Inspector)
15
- # use strict Zod validation that requires id to be a string or number
16
- module MCP
17
- module Transports
18
- class StdioTransport
19
- if method_defined?(:send_error)
20
- alias_method :original_send_error, :send_error
21
-
22
- def send_error(code, message, id = nil)
23
- # Use placeholder id if nil to satisfy strict MCP client validation
24
- # JSON-RPC 2.0 allows null for notifications, but MCP clients require valid id
25
- id = "error_#{SecureRandom.hex(8)}" if id.nil?
26
- original_send_error(code, message, id)
27
- end
28
- end
29
- end
30
- end
31
-
32
- class Server
33
- if method_defined?(:send_error)
34
- alias_method :original_send_error, :send_error
35
-
36
- def send_error(code, message, id = nil)
37
- # Use placeholder id if nil to satisfy strict MCP client validation
38
- # JSON-RPC 2.0 allows null for notifications, but MCP clients require valid id
39
- id = "error_#{SecureRandom.hex(8)}" if id.nil?
40
- original_send_error(code, message, id)
41
- end
42
- end
43
- end
44
- end
45
-
46
13
  module RubygemsMcp
47
14
  # MCP Server for RubyGems integration
48
15
  #
@@ -134,6 +101,8 @@ module RubygemsMcp
134
101
  server.register_tool(GetRubyVersionRoadmapDetailsTool)
135
102
  server.register_tool(GetRubyVersionGithubChangelogTool)
136
103
  server.register_tool(GetGemVersionInfoTool)
104
+ server.register_tool(GetNewsReleasesTool)
105
+ server.register_tool(GetPopularReleasesTool)
137
106
  end
138
107
 
139
108
  def self.register_resources(server)
@@ -381,10 +350,41 @@ module RubygemsMcp
381
350
 
382
351
  arguments do
383
352
  required(:query).filled(:string).description("Search query (e.g., 'rails')")
353
+ optional(:page).filled(:integer).description("Page number (1-based). If provided, overrides offset")
354
+ optional(:limit).filled(:integer).description("Maximum number of results to return")
355
+ optional(:offset).filled(:integer).description("Number of results to skip (for pagination)")
356
+ end
357
+
358
+ def call(query:, page: nil, limit: nil, offset: 0)
359
+ get_client.search_gems(query, page: page, limit: limit, offset: offset)
360
+ end
361
+ end
362
+
363
+ # Get news releases (all new gem releases)
364
+ class GetNewsReleasesTool < BaseTool
365
+ tool_name "get_news_releases"
366
+ description "Get news releases - all new gem releases from RubyGems.org with pagination"
367
+
368
+ arguments do
369
+ optional(:page).filled(:integer).description("Page number (1-based, default: 1)")
384
370
  end
385
371
 
386
- def call(query:)
387
- get_client.search_gems(query)
372
+ def call(page: 1)
373
+ get_client.get_news_releases(page: page)
374
+ end
375
+ end
376
+
377
+ # Get popular releases (popular new gem releases)
378
+ class GetPopularReleasesTool < BaseTool
379
+ tool_name "get_popular_releases"
380
+ description "Get popular releases - popular new gem releases from RubyGems.org with pagination"
381
+
382
+ arguments do
383
+ optional(:page).filled(:integer).description("Page number (1-based, default: 1)")
384
+ end
385
+
386
+ def call(page: 1)
387
+ get_client.get_popular_releases(page: page)
388
388
  end
389
389
  end
390
390
 
@@ -430,39 +430,50 @@ module RubygemsMcp
430
430
  end
431
431
  end
432
432
 
433
- # Resource: Popular Ruby gems list
433
+ # Resource: Popular Ruby gems list with real-time data
434
434
  class PopularGemsResource < FastMcp::Resource
435
435
  uri "rubygems://popular"
436
436
  resource_name "Popular Ruby Gems"
437
- description "A curated list of popular Ruby gems with their latest versions"
437
+ description "Popular new gem releases from RubyGems.org with their versions, download counts, and metadata"
438
438
  mime_type "application/json"
439
439
 
440
440
  def content
441
441
  client = Client.new
442
- popular_gems = %w[
443
- rails nokogiri bundler rake rspec devise puma sidekiq
444
- pg mysql2 redis json webrick sinatra haml sass
445
- jekyll octokit faraday httparty rest-client
446
- ]
447
-
448
- gems_data = popular_gems.map do |gem_name|
449
- versions = client.get_gem_versions(gem_name, limit: 1, fields: ["name", "version", "release_date"])
450
- latest = versions.first
451
- if latest
452
- result = latest.dup
453
- result[:name] = gem_name
454
- result
455
- else
456
- {name: gem_name, version: nil, release_date: nil}
457
- end
458
- rescue ResponseSizeExceededError, CorruptedDataError => e
459
- # Skip gems that exceed size limit or have corrupted data
460
- {name: gem_name, version: nil, release_date: nil, error: e.message}
442
+ # Get popular releases from the first 3 pages (up to ~30 gems)
443
+ all_releases = []
444
+ (1..3).each do |page|
445
+ releases = client.get_popular_releases(page: page)
446
+ break if releases.empty?
447
+ all_releases.concat(releases)
448
+ rescue
449
+ # If a page fails, continue with what we have
450
+ break
461
451
  end
462
452
 
463
- # Filter out gems that weren't found (nil versions)
464
- gems_data = gems_data.reject { |g| g[:version].nil? }
465
- JSON.pretty_generate(gems_data)
453
+ # Limit to top 20 most popular by downloads
454
+ gems_data = all_releases
455
+ .select { |g| g[:downloads] && g[:downloads] > 0 }
456
+ .sort_by { |g| g[:downloads] || 0 }
457
+ .last(20).reverse
458
+ .map do |release|
459
+ {
460
+ name: release[:name],
461
+ version: release[:version],
462
+ release_date: release[:release_date],
463
+ downloads: release[:downloads],
464
+ info: release[:info],
465
+ gem_url: release[:gem_url]
466
+ }
467
+ end
468
+
469
+ result = {
470
+ updated_at: Time.now.iso8601,
471
+ total_gems: gems_data.length,
472
+ source: "rubygems.org/releases/popular",
473
+ gems: gems_data
474
+ }
475
+
476
+ JSON.pretty_generate(result)
466
477
  end
467
478
  end
468
479
 
@@ -528,17 +539,32 @@ module RubygemsMcp
528
539
  end
529
540
  end
530
541
 
531
- # Resource: Latest Ruby version
542
+ # Resource: Latest Ruby version with additional context
532
543
  class LatestRubyVersionResource < FastMcp::Resource
533
544
  uri "rubygems://ruby/latest"
534
545
  resource_name "Latest Ruby Version"
535
- description "The latest stable Ruby version with release date"
546
+ description "The latest stable Ruby version with release date, maintenance status, and compatibility information"
536
547
  mime_type "application/json"
537
548
 
538
549
  def content
539
550
  client = Client.new
540
551
  latest = client.get_latest_ruby_version
541
- JSON.pretty_generate(latest)
552
+ maintenance_status = client.get_ruby_maintenance_status
553
+
554
+ # Find maintenance info for the latest version
555
+ latest_major_minor = latest[:version]&.match(/^(\d+\.\d+)/)&.[](1)
556
+ maintenance_info = maintenance_status.find { |m| m[:version] == latest_major_minor } if latest_major_minor
557
+
558
+ result = {
559
+ version: latest[:version],
560
+ release_date: latest[:release_date],
561
+ maintenance_status: maintenance_info&.dig(:status),
562
+ normal_maintenance_until: maintenance_info&.dig(:normal_maintenance_until),
563
+ eol: maintenance_info&.dig(:eol),
564
+ updated_at: Time.now.iso8601
565
+ }
566
+
567
+ JSON.pretty_generate(result)
542
568
  end
543
569
  end
544
570
  end
@@ -1,3 +1,3 @@
1
1
  module RubygemsMcp
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubygems_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov