rubygems_mcp 0.1.2 → 0.1.3

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: a457f8426c2139f86ee038c95ceaca65c6b58ae60505f3f7c20e01f6d8df0b01
4
- data.tar.gz: d8076f2259213d6a4737bbf4c3e1ec9f479354fc24a15bec3c3cdf7bd343bcbe
3
+ metadata.gz: 263030b634fd9d87f2a5d3d4ff68574e017438ec57a7caff2ebc0b5729dfccb2
4
+ data.tar.gz: 8c9e6142f7c8c7cbb6695c5265c2a7655eb7606872963ec0db3bf5ffb9a7d140
5
5
  SHA512:
6
- metadata.gz: 52e7b9d695199beeabd3becceaf587c30e2d5b8b862a6a5648c3d290ef9a5a365c3709f2d43547dac81d3366beef9d23a3d12d32ad7bbec4208bf90c12b89704
7
- data.tar.gz: 5730b35728600a1b2f1526cd2d18d87167b62054910c4b3c026481743bd1bbaa36c4f05aa0722b51fc62439b40322d81aca96a57dc7687abff8a00beba4e67d8
6
+ metadata.gz: 0f6bc852cf223da1499bf87063b2f92acbc562d40b4bd7d7fdfd8a4af38bf79064616fb8c0275b50e1e1e148a390cf92fcbdd0547cd3d45ca96d7ccf6966c1ec
7
+ data.tar.gz: 3ae00927439ed56b949e3d2a3ffa3830d87e3ee89d34d6e62adad8af51e7df54a33f2af870ee4b657b80300d335528b7627c8a2d986375a9a8aab7a81ce79516
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.3 (2025-11-21)
4
+
5
+ - Add Ruby roadmap tools: `get_ruby_roadmap` and `get_ruby_version_roadmap_details` for accessing Ruby version planning information from bugs.ruby-lang.org
6
+ - Add `get_ruby_version_github_changelog` tool to fetch GitHub release changelogs for Ruby versions
7
+ - Improve error handling with new error classes (ValidationError, improved CorruptedDataError with URI information)
8
+ - Add input validation for all API methods (gem names, version strings, pagination parameters, sort orders)
9
+ - Enhance error messages with URI information for better debugging
10
+ - Increase maximum response size limit for Ruby version changelog retrieval
11
+
3
12
  ## 0.1.2 (2025-11-21)
4
13
 
5
14
  - Enhance Ruby version changelog retrieval and increase maximum response size
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # rubygems_mcp
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/rubygems_mcp.svg?v=0.1.1)](https://badge.fury.io/rb/rubygems_mcp) [![Test Status](https://github.com/amkisko/rubygems_mcp.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/rubygems_mcp.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/rubygems_mcp.rb/graph/badge.svg?token=APQ6AK7EC9)](https://codecov.io/gh/amkisko/rubygems_mcp.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/rubygems_mcp.svg?v=0.1.3)](https://badge.fury.io/rb/rubygems_mcp) [![Test Status](https://github.com/amkisko/rubygems_mcp.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/rubygems_mcp.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/rubygems_mcp.rb/graph/badge.svg?token=APQ6AK7EC9)](https://codecov.io/gh/amkisko/rubygems_mcp.rb)
4
4
 
5
5
  Ruby gem providing RubyGems and Ruby version information via MCP (Model Context Protocol) server tools. Integrates with MCP-compatible clients like Cursor IDE, Claude Desktop, and other MCP-enabled tools.
6
6
 
@@ -93,7 +93,7 @@ The server will start and communicate via STDIN/STDOUT using the MCP protocol.
93
93
 
94
94
  - **RubyGems API Client**: Full-featured client for RubyGems REST API with comprehensive endpoint coverage
95
95
  - **Ruby Version Information**: Fetch Ruby release information, changelogs, and maintenance status from ruby-lang.org
96
- - **MCP Server Integration**: Ready-to-use MCP server with 12 tools and 4 resources, compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
96
+ - **MCP Server Integration**: Ready-to-use MCP server with 19 tools and 4 resources, compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
97
97
  - **Pagination & Sorting**: Support for large result sets with customizable pagination and sorting options
98
98
  - **Caching**: In-memory caching with configurable TTL for improved performance
99
99
  - **Error Handling**: Graceful error handling with custom exceptions and response size limits
@@ -196,11 +196,15 @@ recently_updated = client.get_recently_updated_gems(limit: 10)
196
196
  - `get_latest_ruby_version` - Get latest Ruby version with release date
197
197
  - `get_ruby_versions(limit: nil, offset: 0, sort: :version_desc)` - Get all Ruby versions with release dates, download URLs, and release notes URLs, sorted by version descending. Supports pagination and sorting.
198
198
  - `get_ruby_version_changelog(version)` - Get changelog summary for a specific Ruby version by fetching and parsing the release notes
199
+ - `get_ruby_version_github_changelog(version)` - Get GitHub release changelog for a Ruby version from the ruby/ruby repository
200
+ - `get_ruby_roadmap` - Get Ruby roadmap information from bugs.ruby-lang.org showing planned versions and their issues
201
+ - `get_ruby_version_roadmap_details(version)` - Get detailed roadmap information for a specific Ruby version from bugs.ruby-lang.org, including issues and features planned for that version
199
202
  - `get_ruby_maintenance_status` - Get maintenance status for all Ruby versions including EOL dates and maintenance phases
200
203
 
201
204
  ### Gem Information
202
205
 
203
206
  - `get_gem_info(gem_name, fields: nil)` - Get detailed information about a gem (summary, homepage, source code, documentation, licenses, authors, dependencies, downloads). Supports GraphQL-like field selection.
207
+ - `get_gem_version_info(gem_name, version, fields: nil)` - Get detailed information for a specific gem version using RubyGems API v2 (version-specific downloads, dependencies, SHA checksums, creation date, built date). Supports GraphQL-like field selection.
204
208
  - `get_gem_reverse_dependencies(gem_name)` - Get reverse dependencies - list of gems that depend on the specified gem
205
209
  - `get_gem_version_downloads(gem_name, version)` - Get download statistics for a specific gem version
206
210
  - `get_gem_changelog(gem_name, version: nil)` - Get changelog summary for a gem by fetching and parsing the changelog from its changelog_uri
@@ -251,23 +255,35 @@ The MCP server provides the following tools:
251
255
  6. **get_gem_info** - Get detailed information about a gem (summary, homepage, source code, documentation, licenses, authors, dependencies, downloads). Supports GraphQL-like field selection.
252
256
  - Parameters: `gem_name` (string), `fields` (optional array of strings)
253
257
 
254
- 7. **get_gem_reverse_dependencies** - Get reverse dependencies - list of gems that depend on the specified gem
258
+ 7. **get_gem_version_info** - Get detailed information for a specific gem version using RubyGems API v2 (version-specific downloads, dependencies, SHA checksums, creation date, built date). Supports GraphQL-like field selection.
259
+ - Parameters: `gem_name` (string), `version` (string, e.g., "0.1.0" or "1.15.0-x86_64-linux"), `fields` (optional array of strings)
260
+
261
+ 8. **get_gem_reverse_dependencies** - Get reverse dependencies - list of gems that depend on the specified gem
255
262
  - Parameters: `gem_name` (string)
256
263
 
257
- 8. **get_gem_version_downloads** - Get download statistics for a specific gem version
264
+ 9. **get_gem_version_downloads** - Get download statistics for a specific gem version
258
265
  - Parameters: `gem_name` (string), `version` (string)
259
266
 
260
- 9. **get_latest_gems** - Get latest gems - most recently added gems to RubyGems.org
267
+ 10. **get_latest_gems** - Get latest gems - most recently added gems to RubyGems.org
268
+ - Parameters: `limit` (optional integer, default: 30, max: 50)
269
+
270
+ 11. **get_recently_updated_gems** - Get recently updated gems - most recently updated gem versions
261
271
  - Parameters: `limit` (optional integer, default: 30, max: 50)
262
272
 
263
- 10. **get_recently_updated_gems** - Get recently updated gems - most recently updated gem versions
264
- - Parameters: `limit` (optional integer, default: 30, max: 50)
273
+ 12. **get_gem_changelog** - Get changelog summary for a gem by fetching and parsing the changelog from its changelog_uri
274
+ - Parameters: `gem_name` (string), `version` (optional string, uses latest if not provided)
275
+
276
+ 13. **search_gems** - Search for gems by name on RubyGems
277
+ - Parameters: `query` (string)
278
+
279
+ 14. **get_ruby_roadmap** - Get Ruby roadmap information from bugs.ruby-lang.org showing planned versions and their issues
280
+ - Parameters: none
265
281
 
266
- 11. **get_gem_changelog** - Get changelog summary for a gem by fetching and parsing the changelog from its changelog_uri
267
- - Parameters: `gem_name` (string), `version` (optional string, uses latest if not provided)
282
+ 15. **get_ruby_version_roadmap_details** - Get detailed roadmap information for a specific Ruby version from bugs.ruby-lang.org, including issues and features planned for that version
283
+ - Parameters: `version` (string, e.g., "3.4", "4.0")
268
284
 
269
- 12. **search_gems** - Search for gems by name on RubyGems
270
- - Parameters: `query` (string)
285
+ 16. **get_ruby_version_github_changelog** - Get GitHub release changelog for a Ruby version from the ruby/ruby repository
286
+ - Parameters: `version` (string, e.g., "3.4.7", "3.4.0")
271
287
 
272
288
  ## MCP Resources
273
289
 
@@ -17,30 +17,17 @@ module RubygemsMcp
17
17
  # Maximum response size (5MB) to protect against crawler protection pages
18
18
  MAX_RESPONSE_SIZE = 5 * 1024 * 1024 # 5MB
19
19
 
20
- # Custom exception for corrupted data
21
- class CorruptedDataError < StandardError
22
- attr_reader :original_error, :response_size
20
+ # Validation constants
21
+ VALID_SORT_ORDERS = %i[version_desc version_asc date_desc date_asc].freeze
22
+ MAX_LIMIT = 1000 # Reasonable upper bound for pagination
23
23
 
24
- def initialize(message, original_error: nil, response_size: nil)
25
- super(message)
26
- @original_error = original_error
27
- @response_size = response_size
28
- end
29
- end
30
-
31
- # Custom exception for response size exceeded
32
- class ResponseSizeExceededError < StandardError
33
- attr_reader :size, :max_size
34
-
35
- def initialize(size, max_size)
36
- @size = size
37
- @max_size = max_size
38
- super("Response size (#{size} bytes) exceeds maximum allowed size (#{max_size} bytes). This may indicate crawler protection.")
39
- end
40
- end
41
24
  RUBYGEMS_API_BASE = "https://rubygems.org/api/v1"
25
+ RUBYGEMS_API_V2_BASE = "https://rubygems.org/api/v2"
42
26
  RUBY_RELEASES_URL = "https://www.ruby-lang.org/en/downloads/releases/"
43
27
  RUBY_BRANCHES_URL = "https://www.ruby-lang.org/en/downloads/branches/"
28
+ RUBY_ROADMAP_URL = "https://bugs.ruby-lang.org/projects/ruby-master/roadmap"
29
+ RUBY_BUGS_BASE = "https://bugs.ruby-lang.org"
30
+ GITHUB_RUBY_REPO = "https://api.github.com/repos/ruby/ruby"
44
31
 
45
32
  # Simple in-memory cache with TTL
46
33
  class Cache
@@ -96,6 +83,8 @@ module RubygemsMcp
96
83
  # ruby_version, rubygems_version, downloads_count, sha, spec_sha, requirements, metadata
97
84
  # @return [Array<Hash>] Array of hashes with selected fields
98
85
  def get_latest_versions(gem_names, fields: nil)
86
+ raise ValidationError, "gem_names cannot be empty" if gem_names.nil? || gem_names.empty?
87
+ gem_names = gem_names.map { |name| validate_gem_name(name) }
99
88
  gem_names.map do |name|
100
89
  versions = get_gem_versions(name, limit: 1, fields: fields)
101
90
  latest = versions.first # Versions are sorted by version number descending
@@ -121,6 +110,11 @@ module RubygemsMcp
121
110
  # ruby_version, rubygems_version, downloads_count, sha, spec_sha, requirements, metadata
122
111
  # @return [Array<Hash>] Array of hashes with selected fields
123
112
  def get_gem_versions(gem_name, limit: nil, offset: 0, sort: :version_desc, fields: nil)
113
+ # Validate inputs
114
+ gem_name = validate_gem_name(gem_name)
115
+ validate_pagination_params(limit: limit, offset: offset)
116
+ sort = validate_sort_order(sort)
117
+
124
118
  cache_key = "gem_versions:#{gem_name}"
125
119
 
126
120
  if @cache_enabled
@@ -140,7 +134,8 @@ module RubygemsMcp
140
134
  unless response.is_a?(Array)
141
135
  raise CorruptedDataError.new(
142
136
  "Invalid JSON structure: expected Array, got #{response.class}",
143
- response_size: response.to_s.bytesize
137
+ response_size: response.to_s.bytesize,
138
+ uri: uri.to_s
144
139
  )
145
140
  end
146
141
 
@@ -296,6 +291,8 @@ module RubygemsMcp
296
291
  # @param sort [Symbol] Sort order: :version_desc (default), :version_asc, :date_desc, :date_asc
297
292
  # @return [Array<Hash>] Array of hashes with :version and :release_date
298
293
  def get_ruby_versions(limit: nil, offset: 0, sort: :version_desc)
294
+ validate_pagination_params(limit: limit, offset: offset)
295
+ sort = validate_sort_order(sort)
299
296
  cache_key = "ruby_versions"
300
297
 
301
298
  if @cache_enabled
@@ -354,6 +351,7 @@ module RubygemsMcp
354
351
  # @param version [String] Ruby version (e.g., "3.4.7" or "4.0.0-preview2")
355
352
  # @return [Hash] Hash with :version, :release_notes_url, and :content (full content)
356
353
  def get_ruby_version_changelog(version)
354
+ validate_version_string(version)
357
355
  # First get the release notes URL for this version
358
356
  versions = get_ruby_versions
359
357
 
@@ -389,32 +387,46 @@ module RubygemsMcp
389
387
 
390
388
  uri = URI(release_notes_url)
391
389
  response = make_request(uri, parse_html: true)
392
- return {version: version, release_notes_url: release_notes_url, content: nil, error: "Failed to fetch release notes"} unless response
393
390
 
394
- # Extract the main content - Ruby release notes use div#content
395
- content = response.css("div#content").first || response.css("div.content, div.entry-content, article, main").first
391
+ content = nil
392
+ github_changelog = nil
396
393
 
397
- if content
398
- # Remove navigation and metadata elements
399
- content.css("p.post-info, .post-info, nav, .navigation, header, footer, .sidebar").remove
394
+ if response
395
+ # Extract the main content - Ruby release notes use div#content
396
+ content_elem = response.css("div#content").first || response.css("div.content, div.entry-content, article, main").first
400
397
 
401
- # Get the full text content, preserving structure
402
- text = content.text.strip
398
+ if content_elem
399
+ # Remove navigation and metadata elements
400
+ content_elem.css("p.post-info, .post-info, nav, .navigation, header, footer, .sidebar").remove
403
401
 
404
- # Clean up excessive whitespace but preserve paragraph structure
405
- text = text.gsub(/\n{3,}/, "\n\n")
406
- text = text.gsub(/[ \t]+/, " ")
402
+ # Get the full text content, preserving structure
403
+ content = content_elem.text.strip
407
404
 
408
- # Remove empty lines at start/end
409
- text = text.strip
410
- else
411
- text = nil
405
+ # Clean up excessive whitespace but preserve paragraph structure
406
+ content = content.gsub(/\n{3,}/, "\n\n")
407
+ content = content.gsub(/[ \t]+/, " ")
408
+ content = content.strip
409
+ end
410
+ end
411
+
412
+ # If no content from release notes, try GitHub as fallback
413
+ if content.nil? || content.empty?
414
+ begin
415
+ github_data = get_ruby_version_github_changelog(version)
416
+ if github_data[:body] && !github_data[:body].empty?
417
+ github_changelog = github_data[:body]
418
+ content = github_changelog
419
+ end
420
+ rescue
421
+ # Silently fall through if GitHub fetch fails
422
+ end
412
423
  end
413
424
 
414
425
  result = {
415
426
  version: version,
416
427
  release_notes_url: release_notes_url,
417
- content: text
428
+ content: content,
429
+ github_changelog: github_changelog
418
430
  }
419
431
 
420
432
  # Cache for 24 hours
@@ -423,11 +435,224 @@ module RubygemsMcp
423
435
  result
424
436
  end
425
437
 
438
+ # Get Ruby roadmap information from bugs.ruby-lang.org
439
+ #
440
+ # @return [Hash] Hash with roadmap data:
441
+ # - :versions (Array<Hash>) - Array of version information with links
442
+ # Each version hash contains: :name, :due_date, :version_url, :issues_count
443
+ def get_ruby_roadmap
444
+ cache_key = "ruby_roadmap"
445
+
446
+ if @cache_enabled
447
+ cached = self.class.cache.get(cache_key)
448
+ return cached if cached
449
+ end
450
+
451
+ uri = URI(RUBY_ROADMAP_URL)
452
+ response = make_request(uri, parse_html: true)
453
+ return {versions: []} unless response
454
+
455
+ versions = []
456
+
457
+ # Find all version links in the roadmap
458
+ response.css('a[href*="/versions/"]').each do |link|
459
+ href = link["href"]
460
+ next unless href&.match?(%r{/versions/\d+})
461
+
462
+ version_name = link.text.strip
463
+ due_date = link["title"]
464
+ version_url = href.start_with?("http") ? href : "#{RUBY_BUGS_BASE}#{href}"
465
+
466
+ # Try to find issue count in nearby elements
467
+ issues_count = nil
468
+ parent = link.parent
469
+ if parent
470
+ # Look for text like "16 issues" or "87%"
471
+ issues_text = parent.text
472
+ issues_match = issues_text.match(/(\d+)\s+issues?/)
473
+ issues_count = issues_match[1].to_i if issues_match
474
+ end
475
+
476
+ # Avoid duplicates
477
+ next if versions.any? { |v| v[:name] == version_name }
478
+
479
+ versions << {
480
+ name: version_name,
481
+ due_date: due_date,
482
+ version_url: version_url,
483
+ issues_count: issues_count
484
+ }
485
+ end
486
+
487
+ result = {versions: versions}
488
+
489
+ # Cache for 6 hours (roadmap changes infrequently)
490
+ self.class.cache.set(cache_key, result, 21600) if @cache_enabled
491
+
492
+ result
493
+ end
494
+
495
+ # Get detailed roadmap information for a specific Ruby version from bugs.ruby-lang.org
496
+ #
497
+ # @param version [String] Ruby version (e.g., "3.4", "4.0")
498
+ # @return [Hash] Hash with version details:
499
+ # - :version (String) - Version name
500
+ # - :version_url (String) - URL to version page
501
+ # - :description (String, nil) - Version description
502
+ # - :issues (Array<Hash>) - Array of issues/features for this version
503
+ # Each issue hash contains: :id, :tracker, :subject, :status, :priority, :url
504
+ def get_ruby_version_roadmap_details(version)
505
+ validate_version_string(version)
506
+
507
+ # First get roadmap to find version URL
508
+ roadmap = get_ruby_roadmap
509
+ version_info = roadmap[:versions].find { |v| v[:name] == version || v[:name].start_with?("#{version}.") }
510
+
511
+ return {version: version, version_url: nil, description: nil, issues: [], error: "Version not found in roadmap"} unless version_info
512
+
513
+ version_url = version_info[:version_url]
514
+ cache_key = "ruby_version_roadmap:#{version}"
515
+
516
+ if @cache_enabled
517
+ cached = self.class.cache.get(cache_key)
518
+ return cached if cached
519
+ end
520
+
521
+ uri = URI(version_url)
522
+ response = make_request(uri, parse_html: true)
523
+ return {version: version, version_url: version_url, description: nil, issues: [], error: "Failed to fetch version page"} unless response
524
+
525
+ # Extract description
526
+ description = nil
527
+ desc_elem = response.css(".wiki, .description, .version-description").first
528
+ description = desc_elem.text.strip if desc_elem && !desc_elem.text.strip.empty?
529
+
530
+ # Extract issues
531
+ issues = []
532
+ response.css(".issues tbody tr, .issue-list tr").each do |row|
533
+ cells = row.css("td")
534
+ next if cells.empty?
535
+
536
+ # Try to extract issue information
537
+ issue_link = row.css("a[href*='/issues/']").first
538
+ next unless issue_link
539
+
540
+ issue_id = issue_link["href"].match(%r{/issues/(\d+)})&.[](1)
541
+ next unless issue_id
542
+
543
+ issue_url = issue_link["href"].start_with?("http") ? issue_link["href"] : "#{RUBY_BUGS_BASE}#{issue_link["href"]}"
544
+ subject = issue_link.text.strip
545
+
546
+ # Try to extract tracker, status, priority from cells
547
+ tracker = cells[0]&.text&.strip
548
+ status = cells[1]&.text&.strip || cells.find { |c| c.text.match?(/New|Assigned|Closed|Resolved/i) }&.text&.strip
549
+ priority = cells.find { |c| c.text.match?(/Low|Normal|High|Urgent/i) }&.text&.strip
550
+
551
+ issues << {
552
+ id: issue_id,
553
+ tracker: tracker,
554
+ subject: subject,
555
+ status: status,
556
+ priority: priority,
557
+ url: issue_url
558
+ }
559
+ end
560
+
561
+ result = {
562
+ version: version,
563
+ version_url: version_url,
564
+ description: description,
565
+ issues: issues
566
+ }
567
+
568
+ # Cache for 6 hours
569
+ self.class.cache.set(cache_key, result, 21600) if @cache_enabled
570
+
571
+ result
572
+ end
573
+
574
+ # Get GitHub release changelog for a Ruby version
575
+ #
576
+ # @param version [String] Ruby version (e.g., "3.4.7", "3.4.0")
577
+ # @return [Hash] Hash with release information:
578
+ # - :version (String) - Version string
579
+ # - :tag_name (String) - GitHub tag name (e.g., "v3_4_7")
580
+ # - :name (String, nil) - Release name
581
+ # - :body (String, nil) - Release notes/changelog
582
+ # - :published_at (String, nil) - Publication date as ISO 8601
583
+ # - :url (String, nil) - GitHub release URL
584
+ def get_ruby_version_github_changelog(version)
585
+ validate_version_string(version)
586
+
587
+ # Convert version to GitHub tag format (3.4.7 -> v3_4_7)
588
+ tag_name = "v#{version.tr(".", "_")}"
589
+ cache_key = "ruby_github_changelog:#{version}"
590
+
591
+ if @cache_enabled
592
+ cached = self.class.cache.get(cache_key)
593
+ return cached if cached
594
+ end
595
+
596
+ # Try GitHub API
597
+ uri = URI("#{GITHUB_RUBY_REPO}/releases/tags/#{tag_name}")
598
+ begin
599
+ http = build_http_client(uri)
600
+ request = Net::HTTP::Get.new(uri)
601
+ request["Accept"] = "application/vnd.github+json"
602
+ request["User-Agent"] = "rubygems_mcp/#{RubygemsMcp::VERSION}"
603
+
604
+ response = http.request(request)
605
+
606
+ case response
607
+ when Net::HTTPSuccess
608
+ # Parse JSON response
609
+ body = response.body
610
+ return {version: version, tag_name: tag_name, name: nil, body: nil, published_at: nil, url: nil, error: "Empty response"} if body.nil? || body.empty?
611
+
612
+ begin
613
+ data = JSON.parse(body.force_encoding("UTF-8"))
614
+ rescue JSON::ParserError => e
615
+ raise CorruptedDataError.new(
616
+ "Failed to parse GitHub API response: #{e.message}",
617
+ original_error: e,
618
+ uri: uri.to_s
619
+ )
620
+ end
621
+
622
+ result = {
623
+ version: version,
624
+ tag_name: tag_name,
625
+ name: data["name"],
626
+ body: data["body"],
627
+ published_at: data["published_at"],
628
+ url: data["html_url"]
629
+ }
630
+
631
+ # Cache for 24 hours
632
+ self.class.cache.set(cache_key, result, 86400) if @cache_enabled
633
+
634
+ result
635
+ when Net::HTTPNotFound
636
+ {version: version, tag_name: tag_name, name: nil, body: nil, published_at: nil, url: nil, error: "Release not found on GitHub"}
637
+ else
638
+ handle_http_error(response, uri)
639
+ end
640
+ rescue APIError
641
+ raise
642
+ rescue => e
643
+ raise APIError.new(
644
+ "Request to GitHub API failed: #{e.class} - #{e.message}",
645
+ uri: uri.to_s
646
+ )
647
+ end
648
+ end
649
+
426
650
  # Get reverse dependencies (gems that depend on this gem)
427
651
  #
428
652
  # @param gem_name [String] Gem name
429
653
  # @return [Array<String>] Array of gem names that depend on this gem
430
654
  def get_gem_reverse_dependencies(gem_name)
655
+ gem_name = validate_gem_name(gem_name)
431
656
  cache_key = "gem_reverse_deps:#{gem_name}"
432
657
 
433
658
  if @cache_enabled
@@ -452,6 +677,8 @@ module RubygemsMcp
452
677
  # @param version [String] Gem version (e.g., "1.0.0")
453
678
  # @return [Hash] Hash with :version_downloads and :total_downloads
454
679
  def get_gem_version_downloads(gem_name, version)
680
+ gem_name = validate_gem_name(gem_name)
681
+ validate_version_string(version)
455
682
  cache_key = "gem_downloads:#{gem_name}:#{version}"
456
683
 
457
684
  if @cache_enabled
@@ -482,6 +709,7 @@ module RubygemsMcp
482
709
  # @param limit [Integer, nil] Maximum number of gems to return (default: 30, max: 50)
483
710
  # @return [Array<Hash>] Array of gem information
484
711
  def get_latest_gems(limit: 30)
712
+ validate_pagination_params(limit: limit, offset: 0)
485
713
  limit = [limit || 30, 50].min # API returns max 50
486
714
  cache_key = "latest_gems:#{limit}"
487
715
 
@@ -520,6 +748,7 @@ module RubygemsMcp
520
748
  # @param limit [Integer, nil] Maximum number of gems to return (default: 30, max: 50)
521
749
  # @return [Array<Hash>] Array of gem version information
522
750
  def get_recently_updated_gems(limit: 30)
751
+ validate_pagination_params(limit: limit, offset: 0)
523
752
  limit = [limit || 30, 50].min # API returns max 50
524
753
  cache_key = "recently_updated_gems:#{limit}"
525
754
 
@@ -561,6 +790,8 @@ module RubygemsMcp
561
790
  # @param version [String, nil] Gem version (optional, uses latest if not provided)
562
791
  # @return [Hash] Hash with :gem_name, :version, :changelog_uri, and :summary
563
792
  def get_gem_changelog(gem_name, version: nil)
793
+ gem_name = validate_gem_name(gem_name)
794
+ validate_version_string(version) if version
564
795
  # Get gem info to find changelog_uri
565
796
  gem_info = get_gem_info(gem_name)
566
797
  return {gem_name: gem_name, version: nil, changelog_uri: nil, summary: nil, error: "Gem not found"} if gem_info.empty?
@@ -704,6 +935,7 @@ module RubygemsMcp
704
935
  # dependencies, changelog_uri, funding_uri, platform, sha, spec_sha, metadata
705
936
  # @return [Hash] Hash with selected gem information
706
937
  def get_gem_info(gem_name, fields: nil)
938
+ gem_name = validate_gem_name(gem_name)
707
939
  cache_key = "gem_info:#{gem_name}"
708
940
 
709
941
  if @cache_enabled
@@ -748,6 +980,82 @@ module RubygemsMcp
748
980
  select_fields([gem_info], fields).first || gem_info
749
981
  end
750
982
 
983
+ # Get detailed information for a specific gem version
984
+ #
985
+ # Uses RubyGems API v2 to get version-specific details including:
986
+ # - Version-specific download counts
987
+ # - Version creation date
988
+ # - Dependencies for that specific version
989
+ # - SHA checksums
990
+ # - Built date
991
+ #
992
+ # @param gem_name [String] Gem name (e.g., 'devise')
993
+ # @param version [String] Version string (e.g., '0.1.0', '4.9.4', '1.15.0-x86_64-linux')
994
+ # @param fields [Array<String>, nil] Optional field selection (GraphQL-like)
995
+ # @return [Hash] Hash with version-specific gem information
996
+ # @raise [ValidationError] If gem name or version is invalid
997
+ # @raise [NotFoundError] If gem or version doesn't exist
998
+ def get_gem_version_info(gem_name, version, fields: nil)
999
+ validate_gem_name(gem_name)
1000
+ # Gem versions can include platform suffixes (e.g., "1.15.0-x86_64-linux")
1001
+ # So we use a more lenient validation for gem versions
1002
+ validate_gem_version_string(version)
1003
+
1004
+ cache_key = "gem_version_info:#{gem_name}:#{version}"
1005
+ if @cache_enabled
1006
+ cached = self.class.cache.get(cache_key)
1007
+ if cached
1008
+ return select_fields([cached], fields).first if fields
1009
+ return cached
1010
+ end
1011
+ end
1012
+
1013
+ # Use v2 API for version-specific information
1014
+ uri = URI("#{RUBYGEMS_API_V2_BASE}/rubygems/#{gem_name}/versions/#{version}.json")
1015
+
1016
+ response = make_request(uri)
1017
+ return {} unless response.is_a?(Hash)
1018
+
1019
+ version_info = {
1020
+ name: response["name"],
1021
+ version: response["version"] || response["number"],
1022
+ summary: response["summary"] || response["info"],
1023
+ description: response["description"],
1024
+ homepage: response["homepage_uri"],
1025
+ source_code: response["source_code_uri"],
1026
+ documentation: response["documentation_uri"],
1027
+ licenses: response["licenses"] || [],
1028
+ authors: response["authors"],
1029
+ info: response["info"],
1030
+ downloads: response["downloads"], # Total downloads for the gem
1031
+ version_downloads: response["version_downloads"] || response["downloads_count"],
1032
+ yanked: response["yanked"] || false,
1033
+ dependencies: response["dependencies"] || {runtime: [], development: []},
1034
+ changelog_uri: response["changelog_uri"] || response.dig("metadata", "changelog_uri"),
1035
+ funding_uri: response["funding_uri"] || response.dig("metadata", "funding_uri"),
1036
+ platform: response["platform"] || "ruby",
1037
+ sha: response["sha"],
1038
+ spec_sha: response["spec_sha"],
1039
+ metadata: response["metadata"] || {},
1040
+ version_created_at: response["version_created_at"] || response["created_at"],
1041
+ built_at: response["built_at"],
1042
+ prerelease: response["prerelease"] || false,
1043
+ rubygems_version: response["rubygems_version"],
1044
+ ruby_version: response["ruby_version"],
1045
+ requirements: response["requirements"],
1046
+ gem_uri: response["gem_uri"],
1047
+ project_uri: response["project_uri"],
1048
+ wiki_uri: response["wiki_uri"],
1049
+ mailing_list_uri: response["mailing_list_uri"],
1050
+ bug_tracker_uri: response["bug_tracker_uri"]
1051
+ }
1052
+
1053
+ # Cache for 1 hour
1054
+ self.class.cache.set(cache_key, version_info, 3600) if @cache_enabled
1055
+
1056
+ select_fields([version_info], fields).first || version_info
1057
+ end
1058
+
751
1059
  # Search for gems by name
752
1060
  #
753
1061
  # @param query [String] Search query
@@ -755,6 +1063,8 @@ module RubygemsMcp
755
1063
  # @param offset [Integer] Number of results to skip (for pagination)
756
1064
  # @return [Array<Hash>] Array of hashes with gem information
757
1065
  def search_gems(query, limit: nil, offset: 0)
1066
+ raise ValidationError, "Search query cannot be empty" if query.nil? || query.strip.empty?
1067
+ validate_pagination_params(limit: limit, offset: offset)
758
1068
  # Don't cache search results as they can change frequently
759
1069
  uri = URI("#{RUBYGEMS_API_BASE}/search.json")
760
1070
  uri.query = URI.encode_www_form(query: query)
@@ -779,6 +1089,126 @@ module RubygemsMcp
779
1089
  results
780
1090
  end
781
1091
 
1092
+ # Validate gem name
1093
+ #
1094
+ # @param gem_name [String] Gem name to validate
1095
+ # @return [String] Validated and normalized gem name
1096
+ # @raise [ValidationError] If gem name is invalid
1097
+ def validate_gem_name(gem_name)
1098
+ raise ValidationError, "Gem name cannot be empty" if gem_name.nil? || gem_name.strip.empty?
1099
+ raise ValidationError, "Gem name contains invalid characters" unless gem_name.match?(/\A[a-z0-9_-]+\z/i)
1100
+ gem_name.strip
1101
+ end
1102
+
1103
+ # Validate version string (for Ruby versions)
1104
+ #
1105
+ # @param version [String, nil] Version string to validate
1106
+ # @return [String, nil] Validated version string or nil
1107
+ # @raise [ValidationError] If version format is invalid
1108
+ def validate_version_string(version)
1109
+ return nil if version.nil?
1110
+ # Ruby versions can be: "3.4.7", "4.0.0-preview2", "4.0.0.pre.preview2", etc.
1111
+ # Use Gem::Version to validate as it handles all Ruby version formats
1112
+ begin
1113
+ Gem::Version.new(version)
1114
+ rescue ArgumentError
1115
+ raise ValidationError, "Invalid version format: #{version.inspect}"
1116
+ end
1117
+ version
1118
+ end
1119
+
1120
+ # Validate gem version string (more lenient, supports platform suffixes)
1121
+ #
1122
+ # @param version [String, nil] Version string to validate
1123
+ # @return [String, nil] Validated version string or nil
1124
+ # @raise [ValidationError] If version format is invalid
1125
+ def validate_gem_version_string(version)
1126
+ return nil if version.nil?
1127
+ raise ValidationError, "Version cannot be empty" if version.strip.empty?
1128
+
1129
+ # Gem versions can include platform suffixes like "1.15.0-x86_64-linux"
1130
+ # Try to validate the base version part (before any platform suffix)
1131
+ base_version = version.split(/[-+]/).first
1132
+ begin
1133
+ Gem::Version.new(base_version)
1134
+ rescue ArgumentError
1135
+ raise ValidationError, "Invalid version format: #{version.inspect}"
1136
+ end
1137
+ version
1138
+ end
1139
+
1140
+ # Validate sort order
1141
+ #
1142
+ # @param sort [Symbol, String] Sort order to validate
1143
+ # @return [Symbol] Validated sort order symbol
1144
+ # @raise [ValidationError] If sort order is invalid
1145
+ def validate_sort_order(sort)
1146
+ sort_sym = sort.to_sym
1147
+ unless VALID_SORT_ORDERS.include?(sort_sym)
1148
+ raise ValidationError, "Invalid sort order. Must be one of: #{VALID_SORT_ORDERS.join(", ")}"
1149
+ end
1150
+ sort_sym
1151
+ end
1152
+
1153
+ # Validate pagination parameters
1154
+ #
1155
+ # @param limit [Integer, nil] Limit value
1156
+ # @param offset [Integer] Offset value
1157
+ # @raise [ValidationError] If pagination parameters are invalid
1158
+ def validate_pagination_params(limit:, offset:)
1159
+ raise ValidationError, "Limit must be positive" if limit && limit < 0
1160
+ raise ValidationError, "Offset must be non-negative" if offset < 0
1161
+ raise ValidationError, "Limit cannot exceed #{MAX_LIMIT}" if limit && limit > MAX_LIMIT
1162
+ end
1163
+
1164
+ # Handle HTTP errors and raise appropriate exception types
1165
+ #
1166
+ # @param response [Net::HTTPResponse] HTTP response
1167
+ # @param uri [URI] Request URI for context
1168
+ # @raise [NotFoundError, ServerError, ClientError, APIError] Based on HTTP status code
1169
+ def handle_http_error(response, uri)
1170
+ # Try to parse error response body
1171
+ error_data = begin
1172
+ JSON.parse(response.body) if response.body && !response.body.empty?
1173
+ rescue JSON::ParserError
1174
+ nil
1175
+ end
1176
+
1177
+ error_message = error_data&.dig("error") || error_data&.dig("message") || response.message
1178
+ error_message = "#{response.code} #{error_message}"
1179
+
1180
+ case response
1181
+ when Net::HTTPNotFound
1182
+ raise NotFoundError.new(
1183
+ "Resource not found: #{uri}",
1184
+ status_code: response.code.to_i,
1185
+ response_data: error_data,
1186
+ uri: uri.to_s
1187
+ )
1188
+ when Net::HTTPServerError
1189
+ raise ServerError.new(
1190
+ error_message,
1191
+ status_code: response.code.to_i,
1192
+ response_data: error_data,
1193
+ uri: uri.to_s
1194
+ )
1195
+ when Net::HTTPClientError
1196
+ raise ClientError.new(
1197
+ error_message,
1198
+ status_code: response.code.to_i,
1199
+ response_data: error_data,
1200
+ uri: uri.to_s
1201
+ )
1202
+ else
1203
+ raise APIError.new(
1204
+ error_message,
1205
+ status_code: response.code.to_i,
1206
+ response_data: error_data,
1207
+ uri: uri.to_s
1208
+ )
1209
+ end
1210
+ end
1211
+
782
1212
  private
783
1213
 
784
1214
  # Apply pagination and sorting to a version array
@@ -865,7 +1295,7 @@ module RubygemsMcp
865
1295
  response_body = response.body || ""
866
1296
  response_size = response_body.bytesize
867
1297
  if response_size > MAX_RESPONSE_SIZE
868
- raise ResponseSizeExceededError.new(response_size, MAX_RESPONSE_SIZE)
1298
+ raise ResponseSizeExceededError.new(response_size, MAX_RESPONSE_SIZE, uri: uri.to_s)
869
1299
  end
870
1300
 
871
1301
  # Validate and parse response
@@ -874,18 +1304,25 @@ module RubygemsMcp
874
1304
  else
875
1305
  validate_and_parse_json(response_body, uri)
876
1306
  end
877
- when Net::HTTPNotFound
878
- raise "Resource not found. Response: #{response.body[0..500]}"
879
1307
  else
880
- raise "API request failed: #{response.code} #{response.message}\n#{response.body[0..500]}"
1308
+ handle_http_error(response, uri)
881
1309
  end
882
1310
  rescue ResponseSizeExceededError, CorruptedDataError
883
1311
  # Re-raise our custom errors as-is (don't cache corrupted data)
884
1312
  raise
885
1313
  rescue OpenSSL::SSL::SSLError => e
886
- raise "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues."
1314
+ raise APIError.new(
1315
+ "SSL verification failed: #{e.message}. This may be due to system certificate configuration issues.",
1316
+ uri: uri.to_s
1317
+ )
1318
+ rescue APIError, NotFoundError, ServerError, ClientError
1319
+ # Re-raise API errors as-is
1320
+ raise
887
1321
  rescue => e
888
- raise "Request failed: #{e.class} - #{e.message}"
1322
+ raise APIError.new(
1323
+ "Request to #{uri} failed: #{e.class} - #{e.message}",
1324
+ uri: uri.to_s
1325
+ )
889
1326
  end
890
1327
 
891
1328
  # Validate and parse JSON response
@@ -899,7 +1336,8 @@ module RubygemsMcp
899
1336
  if body.strip.start_with?("<") && body.match?(/cloudflare|ddos protection|access denied|blocked|captcha/i)
900
1337
  raise CorruptedDataError.new(
901
1338
  "Response appears to be a crawler protection page from #{uri}",
902
- response_size: body.bytesize
1339
+ response_size: body.bytesize,
1340
+ uri: uri.to_s
903
1341
  )
904
1342
  end
905
1343
 
@@ -910,7 +1348,8 @@ module RubygemsMcp
910
1348
  unless parsed.is_a?(Hash) || parsed.is_a?(Array)
911
1349
  raise CorruptedDataError.new(
912
1350
  "Invalid JSON structure: expected Hash or Array, got #{parsed.class}",
913
- response_size: body.bytesize
1351
+ response_size: body.bytesize,
1352
+ uri: uri.to_s
914
1353
  )
915
1354
  end
916
1355
 
@@ -921,14 +1360,16 @@ module RubygemsMcp
921
1360
  raise CorruptedDataError.new(
922
1361
  "Received HTML instead of JSON from #{uri}. This may indicate an error page or crawler protection.",
923
1362
  original_error: e,
924
- response_size: body.bytesize
1363
+ response_size: body.bytesize,
1364
+ uri: uri.to_s
925
1365
  )
926
1366
  end
927
1367
 
928
1368
  raise CorruptedDataError.new(
929
1369
  "Failed to parse JSON response from #{uri}: #{e.message}",
930
1370
  original_error: e,
931
- response_size: body.bytesize
1371
+ response_size: body.bytesize,
1372
+ uri: uri.to_s
932
1373
  )
933
1374
  end
934
1375
  end
@@ -943,7 +1384,8 @@ module RubygemsMcp
943
1384
  if body.match?(/cloudflare|ddos protection|access denied|blocked|captcha|rate limit/i)
944
1385
  raise CorruptedDataError.new(
945
1386
  "Response appears to be a crawler protection page from #{uri}",
946
- response_size: body.bytesize
1387
+ response_size: body.bytesize,
1388
+ uri: uri.to_s
947
1389
  )
948
1390
  end
949
1391
 
@@ -951,7 +1393,8 @@ module RubygemsMcp
951
1393
  unless body.strip.start_with?("<!DOCTYPE", "<html", "<HTML") || body.include?("<html")
952
1394
  raise CorruptedDataError.new(
953
1395
  "Response from #{uri} does not appear to be HTML",
954
- response_size: body.bytesize
1396
+ response_size: body.bytesize,
1397
+ uri: uri.to_s
955
1398
  )
956
1399
  end
957
1400
 
@@ -962,7 +1405,8 @@ module RubygemsMcp
962
1405
  if doc.text.strip.length < 50
963
1406
  raise CorruptedDataError.new(
964
1407
  "HTML response from #{uri} appears to be empty or too short",
965
- response_size: body.bytesize
1408
+ response_size: body.bytesize,
1409
+ uri: uri.to_s
966
1410
  )
967
1411
  end
968
1412
 
@@ -978,7 +1422,8 @@ module RubygemsMcp
978
1422
  if error_indicators.any? { |pattern| doc.text.match?(pattern) }
979
1423
  raise CorruptedDataError.new(
980
1424
  "HTML response from #{uri} appears to be an error page",
981
- response_size: body.bytesize
1425
+ response_size: body.bytesize,
1426
+ uri: uri.to_s
982
1427
  )
983
1428
  end
984
1429
 
@@ -987,7 +1432,8 @@ module RubygemsMcp
987
1432
  raise CorruptedDataError.new(
988
1433
  "Failed to parse HTML from #{uri}: #{e.message}",
989
1434
  original_error: e,
990
- response_size: body.bytesize
1435
+ response_size: body.bytesize,
1436
+ uri: uri.to_s
991
1437
  )
992
1438
  end
993
1439
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubygemsMcp
4
+ # Base error class for all RubygemsMcp errors
5
+ class Error < StandardError; end
6
+
7
+ # HTTP/API errors
8
+ class APIError < Error
9
+ attr_reader :status_code, :response_data, :uri
10
+
11
+ def initialize(message, status_code: nil, response_data: nil, uri: nil)
12
+ super(message)
13
+ @status_code = status_code
14
+ @response_data = response_data
15
+ @uri = uri
16
+ end
17
+ end
18
+
19
+ class NotFoundError < APIError; end
20
+ class ServerError < APIError; end
21
+ class ClientError < APIError; end
22
+
23
+ # Data validation errors
24
+ class CorruptedDataError < Error
25
+ attr_reader :original_error, :response_size, :uri
26
+
27
+ def initialize(message, original_error: nil, response_size: nil, uri: nil)
28
+ super(message)
29
+ @original_error = original_error
30
+ @response_size = response_size
31
+ @uri = uri
32
+ end
33
+ end
34
+
35
+ class ResponseSizeExceededError < Error
36
+ attr_reader :size, :max_size, :uri
37
+
38
+ def initialize(size, max_size, uri: nil)
39
+ @size = size
40
+ @max_size = max_size
41
+ @uri = uri
42
+ super("Response size (#{size} bytes) exceeds maximum allowed size (#{max_size} bytes). This may indicate crawler protection.")
43
+ end
44
+ end
45
+
46
+ # Input validation errors
47
+ class ValidationError < Error; end
48
+ end
@@ -130,6 +130,10 @@ module RubygemsMcp
130
130
  server.register_tool(GetRecentlyUpdatedGemsTool)
131
131
  server.register_tool(GetGemChangelogTool)
132
132
  server.register_tool(SearchGemsTool)
133
+ server.register_tool(GetRubyRoadmapTool)
134
+ server.register_tool(GetRubyVersionRoadmapDetailsTool)
135
+ server.register_tool(GetRubyVersionGithubChangelogTool)
136
+ server.register_tool(GetGemVersionInfoTool)
133
137
  end
134
138
 
135
139
  def self.register_resources(server)
@@ -282,6 +286,22 @@ module RubygemsMcp
282
286
  end
283
287
  end
284
288
 
289
+ # Get detailed information for a specific gem version
290
+ class GetGemVersionInfoTool < BaseTool
291
+ tool_name "get_gem_version_info"
292
+ description "Get detailed information for a specific gem version using RubyGems API v2. Returns version-specific details including download counts, dependencies, SHA checksums, and creation date for that exact version."
293
+
294
+ arguments do
295
+ required(:gem_name).filled(:string).description("Gem name (e.g., 'devise')")
296
+ required(:version).filled(:string).description("Version string (e.g., '0.1.0', '4.9.4')")
297
+ optional(:fields).array(:string).description("GraphQL-like field selection. Available: name, version, summary, description, homepage, source_code, documentation, licenses, authors, info, downloads, version_downloads, yanked, dependencies, changelog_uri, funding_uri, platform, sha, spec_sha, metadata, version_created_at, built_at, prerelease, rubygems_version, ruby_version, requirements, gem_uri, project_uri, wiki_uri, mailing_list_uri, bug_tracker_uri")
298
+ end
299
+
300
+ def call(gem_name:, version:, fields: nil)
301
+ get_client.get_gem_version_info(gem_name, version, fields: fields)
302
+ end
303
+ end
304
+
285
305
  # Get reverse dependencies (gems that depend on this gem)
286
306
  class GetGemReverseDependenciesTool < BaseTool
287
307
  tool_name "get_gem_reverse_dependencies"
@@ -368,6 +388,48 @@ module RubygemsMcp
368
388
  end
369
389
  end
370
390
 
391
+ # Get Ruby roadmap information
392
+ class GetRubyRoadmapTool < BaseTool
393
+ tool_name "get_ruby_roadmap"
394
+ description "Get Ruby roadmap information from bugs.ruby-lang.org showing planned versions and their issues"
395
+
396
+ arguments do
397
+ # No arguments required
398
+ end
399
+
400
+ def call
401
+ get_client.get_ruby_roadmap
402
+ end
403
+ end
404
+
405
+ # Get detailed roadmap information for a specific Ruby version
406
+ class GetRubyVersionRoadmapDetailsTool < BaseTool
407
+ tool_name "get_ruby_version_roadmap_details"
408
+ description "Get detailed roadmap information for a specific Ruby version from bugs.ruby-lang.org, including issues and features planned for that version"
409
+
410
+ arguments do
411
+ required(:version).filled(:string).description("Ruby version (e.g., '3.4', '4.0')")
412
+ end
413
+
414
+ def call(version:)
415
+ get_client.get_ruby_version_roadmap_details(version)
416
+ end
417
+ end
418
+
419
+ # Get GitHub release changelog for a Ruby version
420
+ class GetRubyVersionGithubChangelogTool < BaseTool
421
+ tool_name "get_ruby_version_github_changelog"
422
+ description "Get GitHub release changelog for a Ruby version from the ruby/ruby repository"
423
+
424
+ arguments do
425
+ required(:version).filled(:string).description("Ruby version (e.g., '3.4.7', '3.4.0')")
426
+ end
427
+
428
+ def call(version:)
429
+ get_client.get_ruby_version_github_changelog(version)
430
+ end
431
+ end
432
+
371
433
  # Resource: Popular Ruby gems list
372
434
  class PopularGemsResource < FastMcp::Resource
373
435
  uri "rubygems://popular"
@@ -393,7 +455,7 @@ module RubygemsMcp
393
455
  else
394
456
  {name: gem_name, version: nil, release_date: nil}
395
457
  end
396
- rescue Client::ResponseSizeExceededError, Client::CorruptedDataError => e
458
+ rescue ResponseSizeExceededError, CorruptedDataError => e
397
459
  # Skip gems that exceed size limit or have corrupted data
398
460
  {name: gem_name, version: nil, release_date: nil, error: e.message}
399
461
  end
@@ -1,3 +1,3 @@
1
1
  module RubygemsMcp
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
data/lib/rubygems_mcp.rb CHANGED
@@ -4,6 +4,7 @@ require "json"
4
4
  require "date"
5
5
 
6
6
  require_relative "rubygems_mcp/version"
7
+ require_relative "rubygems_mcp/errors"
7
8
  require_relative "rubygems_mcp/client"
8
9
  # Server is loaded on-demand when running the executable
9
10
  # require_relative "rubygems_mcp/server"
data/sig/rubygems_mcp.rbs CHANGED
@@ -1,20 +1,43 @@
1
1
  module RubygemsMcp
2
2
  VERSION: String
3
3
 
4
- class Client
5
- MAX_RESPONSE_SIZE: Integer
4
+ # Base error class for all RubygemsMcp errors
5
+ class Error < StandardError; end
6
+
7
+ # HTTP/API errors
8
+ class APIError < Error
9
+ def initialize: (String message, ?status_code: Integer?, ?response_data: untyped?, ?uri: String?) -> void
10
+ attr_reader status_code: Integer?
11
+ attr_reader response_data: untyped?
12
+ attr_reader uri: String?
13
+ end
6
14
 
7
- class ResponseSizeExceededError < StandardError
8
- def initialize: (Integer size, Integer max_size) -> void
9
- attr_reader size: Integer
10
- attr_reader max_size: Integer
11
- end
15
+ class NotFoundError < APIError; end
16
+ class ServerError < APIError; end
17
+ class ClientError < APIError; end
12
18
 
13
- class CorruptedDataError < StandardError
14
- def initialize: (String message, ?original_error: Exception?, ?response_size: Integer?) -> void
15
- attr_reader original_error: Exception?
16
- attr_reader response_size: Integer?
17
- end
19
+ # Data validation errors
20
+ class CorruptedDataError < Error
21
+ def initialize: (String message, ?original_error: Exception?, ?response_size: Integer?, ?uri: String?) -> void
22
+ attr_reader original_error: Exception?
23
+ attr_reader response_size: Integer?
24
+ attr_reader uri: String?
25
+ end
26
+
27
+ class ResponseSizeExceededError < Error
28
+ def initialize: (Integer size, Integer max_size, ?uri: String?) -> void
29
+ attr_reader size: Integer
30
+ attr_reader max_size: Integer
31
+ attr_reader uri: String?
32
+ end
33
+
34
+ # Input validation errors
35
+ class ValidationError < Error; end
36
+
37
+ class Client
38
+ MAX_RESPONSE_SIZE: Integer
39
+ VALID_SORT_ORDERS: Array[Symbol]
40
+ MAX_LIMIT: Integer
18
41
 
19
42
  class Cache
20
43
  def initialize: () -> void
@@ -59,7 +82,16 @@ module RubygemsMcp
59
82
  ) -> Array[Hash[Symbol, (String | nil)]]
60
83
 
61
84
  # Get changelog for a Ruby version
62
- def get_ruby_version_changelog: (String version) -> Hash[Symbol, (String | nil)] # Returns :version, :release_notes_url, :content
85
+ def get_ruby_version_changelog: (String version) -> Hash[Symbol, (String | nil)] # Returns :version, :release_notes_url, :content, :github_changelog
86
+
87
+ # Get Ruby roadmap information
88
+ def get_ruby_roadmap: () -> Hash[Symbol, Array[Hash[Symbol, (String | Integer | nil)]]] # Returns :versions array
89
+
90
+ # Get detailed roadmap information for a specific Ruby version
91
+ def get_ruby_version_roadmap_details: (String version) -> Hash[Symbol, (String | Array[Hash[Symbol, (String | nil)]] | nil)] # Returns :version, :version_url, :description, :issues
92
+
93
+ # Get GitHub release changelog for a Ruby version
94
+ def get_ruby_version_github_changelog: (String version) -> Hash[Symbol, (String | nil)] # Returns :version, :tag_name, :name, :body, :published_at, :url
63
95
 
64
96
  # Get reverse dependencies (gems that depend on this gem)
65
97
  def get_gem_reverse_dependencies: (String gem_name) -> Array[String]
@@ -88,6 +120,13 @@ module RubygemsMcp
88
120
  ?fields: Array[String]?
89
121
  ) -> Hash[Symbol, untyped]
90
122
 
123
+ # Get detailed information for a specific gem version (uses API v2)
124
+ def get_gem_version_info: (
125
+ String gem_name,
126
+ String version,
127
+ ?fields: Array[String]?
128
+ ) -> Hash[Symbol, untyped]
129
+
91
130
  # Search for gems with pagination
92
131
  def search_gems: (
93
132
  String query,
@@ -138,6 +177,10 @@ module RubygemsMcp
138
177
  def call: (String gem_name, ?Array[String]? fields) -> Hash[Symbol, untyped]
139
178
  end
140
179
 
180
+ class GetGemVersionInfoTool < BaseTool
181
+ def call: (String gem_name, String version, ?Array[String]? fields) -> Hash[Symbol, untyped]
182
+ end
183
+
141
184
  class GetGemReverseDependenciesTool < BaseTool
142
185
  def call: (String gem_name) -> Array[String]
143
186
  end
@@ -162,6 +205,18 @@ module RubygemsMcp
162
205
  def call: (String query) -> Array[Hash[Symbol, untyped]]
163
206
  end
164
207
 
208
+ class GetRubyRoadmapTool < BaseTool
209
+ def call: () -> Hash[Symbol, Array[Hash[Symbol, (String | Integer | nil)]]]
210
+ end
211
+
212
+ class GetRubyVersionRoadmapDetailsTool < BaseTool
213
+ def call: (String version) -> Hash[Symbol, (String | Array[Hash[Symbol, (String | nil)]] | nil)]
214
+ end
215
+
216
+ class GetRubyVersionGithubChangelogTool < BaseTool
217
+ def call: (String version) -> Hash[Symbol, (String | nil)]
218
+ end
219
+
165
220
  class PopularGemsResource
166
221
  def self.uri: () -> String
167
222
  def self.resource_name: () -> String
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.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -240,6 +240,7 @@ files:
240
240
  - bin/rubygems_mcp
241
241
  - lib/rubygems_mcp.rb
242
242
  - lib/rubygems_mcp/client.rb
243
+ - lib/rubygems_mcp/errors.rb
243
244
  - lib/rubygems_mcp/server.rb
244
245
  - lib/rubygems_mcp/version.rb
245
246
  - sig/rubygems_mcp.rbs