rubygems_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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afa4ece822d5b8556b280d14c3755876420957ba7e6b080cb20aa529730bb93e
4
- data.tar.gz: 5e96e90380d30f47fe2e2f765cf4e5831093bfefde80757a60aede6cf46f5753
3
+ metadata.gz: 1a2188f3290de5f410a66437e48bacda1dfd194b7c8446ce6f7db4b6dbb5d644
4
+ data.tar.gz: 1d6604125b76ed9493535466306d64d0f948b921d437b86faaa95016df2314b5
5
5
  SHA512:
6
- metadata.gz: 9f07be5cde29b994821781e0350711b02df4a4f22e93ace9b860dfa6e23d81354639c6d6078da12768ba50a32d95ea89a64224c355ae39efe5486acf9af432eb
7
- data.tar.gz: e648da457c4b257a2dabcf41e9b682825cb4d588012e8eefd0bd46c8b399578bb7ae9a50e87345228538efb2d7c5449904b4fc75f3206a28a229b1ebf9a80bbf
6
+ metadata.gz: be31aff229ddfd131b59e52fe42db7af5d9787e0833bf066baa90c7155f0973e5e2f17e7dd42e443650098464212b9c832671e5b7e01b22e3f9aa185eb13682f
7
+ data.tar.gz: 38237f1b2c2f1ead4ebf0e72c0587bd005eec054a153db2081c5211ab86f7e00b0b4ad2d0b4c349d4828850a457b748b9f2a4fee5cb2507522f11368cb62ab02
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.1 (2025-11-21)
4
+
5
+ - Update fast-mcp dependency version constraints to allow versions >= 0.1 and < 2.0
6
+ - Fix Resource class references to use FastMcp::Resource instead of MCP::Resource
7
+ - Update resource methods from `default_content` to `content` to match fast-mcp 1.0.0+ API
8
+ - Fix test expectations to match fast-mcp API (tool names without `::` separators, resources as Array)
9
+ - Add explicit `tool_name` definitions to all tools for user-friendly snake_case names (e.g., `get_latest_versions` instead of long class names)
10
+ - Fix array argument definitions: Change from `filled(:array)` to `array(:string)` to fix JSON schema conversion errors with dry-schema 1.14.1+ (fixes "Could not find an equivalent conversion for type :array" error)
11
+
3
12
  ## 0.1.0 (2025-01-15)
4
13
 
5
14
  - Initial release
@@ -349,18 +349,18 @@ module RubygemsMcp
349
349
  end
350
350
  end
351
351
 
352
- # Get changelog summary for a Ruby version from release notes
352
+ # Get full changelog content for a Ruby version from release notes
353
353
  #
354
354
  # @param version [String] Ruby version (e.g., "3.4.7")
355
- # @return [Hash] Hash with :version, :release_notes_url, and :summary
355
+ # @return [Hash] Hash with :version, :release_notes_url, and :content (full content)
356
356
  def get_ruby_version_changelog(version)
357
357
  # First get the release notes URL for this version
358
358
  versions = get_ruby_versions
359
359
  version_data = versions.find { |v| v[:version] == version }
360
- return {version: version, release_notes_url: nil, summary: nil, error: "Version not found"} unless version_data
360
+ return {version: version, release_notes_url: nil, content: nil, error: "Version not found"} unless version_data
361
361
 
362
362
  release_notes_url = version_data[:release_notes_url]
363
- return {version: version, release_notes_url: nil, summary: nil, error: "No release notes available"} unless release_notes_url
363
+ return {version: version, release_notes_url: nil, content: nil, error: "No release notes available"} unless release_notes_url
364
364
 
365
365
  cache_key = "ruby_changelog:#{version}"
366
366
 
@@ -371,29 +371,32 @@ module RubygemsMcp
371
371
 
372
372
  uri = URI(release_notes_url)
373
373
  response = make_request(uri, parse_html: true)
374
- return {version: version, release_notes_url: release_notes_url, summary: nil, error: "Failed to fetch release notes"} unless response
374
+ return {version: version, release_notes_url: release_notes_url, content: nil, error: "Failed to fetch release notes"} unless response
375
375
 
376
- # Extract the main content - typically in a div with class "content" or "entry-content"
377
- # Try multiple selectors to find the main content
378
- content = response.css("div.content, div.entry-content, article, main").first || response.css("body").first
376
+ # Extract the main content - Ruby release notes use div#content
377
+ content = response.css("div#content").first || response.css("div.content, div.entry-content, article, main").first
379
378
 
380
379
  if content
381
- # Extract text, remove excessive whitespace, and get first few paragraphs
380
+ # Remove navigation and metadata elements
381
+ content.css("p.post-info, .post-info, nav, .navigation, header, footer, .sidebar").remove
382
+
383
+ # Get the full text content, preserving structure
382
384
  text = content.text.strip
383
- # Split into paragraphs and take first 3-5 meaningful ones
384
- paragraphs = text.split(/\n\n+/).reject { |p| p.strip.length < 50 }
385
- summary = paragraphs.first(5).join("\n\n").strip
386
385
 
387
- # Limit summary length
388
- summary = summary[0..2000] + "..." if summary.length > 2000
386
+ # Clean up excessive whitespace but preserve paragraph structure
387
+ text = text.gsub(/\n{3,}/, "\n\n")
388
+ text = text.gsub(/[ \t]+/, " ")
389
+
390
+ # Remove empty lines at start/end
391
+ text = text.strip
389
392
  else
390
- summary = nil
393
+ text = nil
391
394
  end
392
395
 
393
396
  result = {
394
397
  version: version,
395
398
  release_notes_url: release_notes_url,
396
- summary: summary
399
+ content: text
397
400
  }
398
401
 
399
402
  # Cache for 24 hours
@@ -562,9 +565,10 @@ module RubygemsMcp
562
565
 
563
566
  # Extract the main content - try GitHub release page first, then generic selectors
564
567
  content = if changelog_uri.include?("github.com") && changelog_uri.include?("/releases/")
565
- # GitHub release page - look for release notes in markdown-body or release notes section
566
- response.css(".markdown-body, .release-body, [data-testid='release-body']").first ||
567
- response.css("div.repository-content, article").first
568
+ # GitHub release page - look for release notes in markdown-body
569
+ response.css(".markdown-body").first ||
570
+ response.css("[data-testid='release-body'], .release-body").first ||
571
+ response.css("div.repository-content article").first
568
572
  else
569
573
  # Generic changelog page
570
574
  response.css("div.content, div.entry-content, article, main, .markdown-body").first
@@ -573,29 +577,90 @@ module RubygemsMcp
573
577
  content ||= response.css("body").first
574
578
 
575
579
  summary = if content
580
+ # Remove UI elements, navigation, and error messages
581
+ content.css("nav, header, footer, .navigation, .sidebar, .blankslate, details, summary, .Box-footer, .Counter, [data-view-component], script, style").remove
582
+
583
+ # Remove elements with common UI classes
584
+ content.css("[class*='blankslate'], [class*='Box-footer'], [class*='Counter'], [class*='details-toggle']").remove
585
+
586
+ # Get text content
576
587
  text = content.text.strip
577
- # Remove common navigation/header text patterns
588
+
589
+ # Remove common GitHub UI text patterns
578
590
  text = text.gsub(/Notifications.*?signed in.*?reload/im, "")
579
591
  text = text.gsub(/You must be signed in.*?reload/im, "")
580
592
  text = text.gsub(/There was an error.*?reload/im, "")
593
+ text = text.gsub(/Please reload this page.*?/im, "")
594
+ text = text.gsub(/Loading.*?/im, "")
595
+ text = text.gsub(/Uh oh!.*?/im, "")
596
+ text = text.gsub(/Assets.*?\d+.*?/im, "")
597
+
598
+ # Remove commit hashes and issue references that are just links without context
599
+ text = text.gsub(/\b[a-f0-9]{7,40}\b/, "") # Remove commit hashes
600
+ text = text.gsub(/#\d+\s*$/, "") # Remove trailing issue numbers without context
601
+
602
+ # Clean up whitespace
603
+ text = text.gsub(/\n{3,}/, "\n\n")
604
+ text = text.gsub(/[ \t]{2,}/, " ")
605
+
606
+ # Split into lines and filter out irrelevant content
607
+ lines = text.split(/\n+/)
608
+
609
+ # Filter out lines that are likely UI elements or irrelevant
610
+ filtered_lines = []
611
+ prev_line_was_meaningful = false
612
+
613
+ lines.each_with_index do |line, idx|
614
+ stripped = line.strip
615
+ next if stripped.empty?
616
+
617
+ # Skip UI elements
618
+ next if stripped.match?(/^(Notifications|You must|There was|Please reload|Loading|Uh oh|Assets|\d+\s*$)/i)
619
+ next if stripped.match?(/^\/\s*$/)
620
+ next if stripped.match?(/^[a-f0-9]{7,40}$/) # Standalone commit hashes
621
+ next if stripped.match?(/^\s*#\d+\s*$/) # Standalone issue numbers
622
+
623
+ # Skip author names that appear alone (pattern: First Last or First Middle Last)
624
+ # Author names typically appear after a change description ends with punctuation
625
+ if stripped.match?(/^[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2}$/) && stripped.length < 50 && !stripped.match?(/^[A-Z][a-z]+ [A-Z]\./) # Not initials like "J. Smith"
626
+ # Check if previous line ends with punctuation (end of sentence = author attribution follows)
627
+ if idx > 0 && filtered_lines.any?
628
+ prev = filtered_lines.last.to_s.strip
629
+ # If previous line ends with punctuation, this standalone name is likely an author
630
+ if prev.match?(/[.!]$/)
631
+ next
632
+ end
633
+ elsif idx == 0
634
+ # First line that's just a name, skip it
635
+ next
636
+ end
637
+ end
581
638
 
582
- # Split into paragraphs and take first 5-10 meaningful ones
583
- # Try splitting by double newlines first, then by single newlines if that doesn't work
584
- paragraphs = if text.include?("\n\n")
585
- text.split(/\n\n+/)
586
- else
587
- text.split(/\n+/)
639
+ # Keep meaningful lines
640
+ if stripped.length >= 10
641
+ filtered_lines << line
642
+ prev_line_was_meaningful = true
643
+ end
588
644
  end
589
645
 
590
- paragraphs = paragraphs.reject { |p|
591
- p.strip.length < 30 ||
592
- p.match?(/^(rails|Notifications|You must|There was)/i) ||
593
- p.match?(/^\/\s*$/)
594
- }
595
- summary_text = paragraphs.first(10).join("\n\n").strip
646
+ # Remove trailing "No changes." and similar repetitive endings
647
+ while filtered_lines.last&.strip&.match?(/^(No changes\.?|Guides)$/i)
648
+ filtered_lines.pop
649
+ end
650
+
651
+ # Join back and clean up
652
+ summary_text = filtered_lines.join("\n").strip
653
+
654
+ # Remove excessive blank lines
655
+ summary_text = summary_text.gsub(/\n{3,}/, "\n\n")
656
+
657
+ # Limit length but keep it reasonable for changelogs
658
+ if summary_text.length > 10000
659
+ # Try to cut at a reasonable point (end of a section)
660
+ cut_point = summary_text[0..10000].rindex(/\n\n/)
661
+ summary_text = summary_text[0..(cut_point || 10000)].strip + "\n\n..."
662
+ end
596
663
 
597
- # Limit summary length
598
- summary_text = summary_text[0..3000] + "..." if summary_text.length > 3000
599
664
  summary_text.empty? ? nil : summary_text
600
665
  end
601
666
 
@@ -16,6 +16,21 @@ FastMcp = MCP unless defined?(FastMcp)
16
16
  module MCP
17
17
  module Transports
18
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)
19
34
  alias_method :original_send_error, :send_error
20
35
 
21
36
  def send_error(code, message, id = nil)
@@ -26,17 +41,6 @@ module MCP
26
41
  end
27
42
  end
28
43
  end
29
-
30
- class Server
31
- alias_method :original_send_error, :send_error
32
-
33
- def send_error(code, message, id = nil)
34
- # Use placeholder id if nil to satisfy strict MCP client validation
35
- # JSON-RPC 2.0 allows null for notifications, but MCP clients require valid id
36
- id = "error_#{SecureRandom.hex(8)}" if id.nil?
37
- original_send_error(code, message, id)
38
- end
39
- end
40
44
  end
41
45
 
42
46
  module RubygemsMcp
@@ -151,11 +155,12 @@ module RubygemsMcp
151
155
 
152
156
  # Get latest versions for a list of gems with release dates
153
157
  class GetLatestVersionsTool < BaseTool
158
+ tool_name "get_latest_versions"
154
159
  description "Get latest versions for a list of gems with release dates and licenses. Supports GraphQL-like field selection."
155
160
 
156
161
  arguments do
157
- required(:gem_names).filled(:array, min_size?: 1).description("Array of gem names (e.g., ['rails', 'nokogiri', 'rack'])")
158
- optional(:fields).filled(:array).description("GraphQL-like field selection. Available: name, version, release_date, license, built_at, prerelease, platform, ruby_version, rubygems_version, downloads_count, sha, spec_sha, requirements, metadata")
162
+ required(:gem_names).array(:string, min_size?: 1).description("Array of gem names (e.g., ['rails', 'nokogiri', 'rack'])")
163
+ optional(:fields).array(:string).description("GraphQL-like field selection. Available: name, version, release_date, license, built_at, prerelease, platform, ruby_version, rubygems_version, downloads_count, sha, spec_sha, requirements, metadata")
159
164
  end
160
165
 
161
166
  def call(gem_names:, fields: nil)
@@ -165,6 +170,7 @@ module RubygemsMcp
165
170
 
166
171
  # Get all versions for a single gem
167
172
  class GetGemVersionsTool < BaseTool
173
+ tool_name "get_gem_versions"
168
174
  description "Get all versions for a single gem with release dates and licenses, sorted by version descending. Supports GraphQL-like field selection."
169
175
 
170
176
  arguments do
@@ -172,7 +178,7 @@ module RubygemsMcp
172
178
  optional(:limit).filled(:integer).description("Maximum number of versions to return (for pagination)")
173
179
  optional(:offset).filled(:integer).description("Number of versions to skip (for pagination)")
174
180
  optional(:sort).filled(:string).description("Sort order: version_desc, version_asc, date_desc, or date_asc (default: version_desc)")
175
- optional(:fields).filled(:array).description("GraphQL-like field selection. Available: version, release_date, license, built_at, prerelease, platform, ruby_version, rubygems_version, downloads_count, sha, spec_sha, requirements, metadata")
181
+ optional(:fields).array(:string).description("GraphQL-like field selection. Available: version, release_date, license, built_at, prerelease, platform, ruby_version, rubygems_version, downloads_count, sha, spec_sha, requirements, metadata")
176
182
  end
177
183
 
178
184
  def call(gem_name:, limit: nil, offset: 0, sort: "version_desc", fields: nil)
@@ -189,6 +195,7 @@ module RubygemsMcp
189
195
 
190
196
  # Get latest Ruby version with release date
191
197
  class GetLatestRubyVersionTool < BaseTool
198
+ tool_name "get_latest_ruby_version"
192
199
  description "Get latest Ruby version with release date"
193
200
 
194
201
  arguments do
@@ -202,6 +209,7 @@ module RubygemsMcp
202
209
 
203
210
  # Get all Ruby versions with release dates
204
211
  class GetRubyVersionsTool < BaseTool
212
+ tool_name "get_ruby_versions"
205
213
  description "Get all Ruby versions with release dates, download URLs, and release notes URLs, sorted by version descending"
206
214
 
207
215
  arguments do
@@ -224,6 +232,7 @@ module RubygemsMcp
224
232
 
225
233
  # Get changelog summary for a Ruby version
226
234
  class GetRubyVersionChangelogTool < BaseTool
235
+ tool_name "get_ruby_version_changelog"
227
236
  description "Get changelog summary for a specific Ruby version by fetching and parsing the release notes"
228
237
 
229
238
  arguments do
@@ -237,11 +246,12 @@ module RubygemsMcp
237
246
 
238
247
  # Get gem information (summary, homepage, etc.)
239
248
  class GetGemInfoTool < BaseTool
249
+ tool_name "get_gem_info"
240
250
  description "Get detailed information about a gem (summary, homepage, source code, documentation, licenses, authors, dependencies, downloads). Supports GraphQL-like field selection."
241
251
 
242
252
  arguments do
243
253
  required(:gem_name).filled(:string).description("Gem name (e.g., 'rails')")
244
- optional(:fields).filled(:array).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")
254
+ 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")
245
255
  end
246
256
 
247
257
  def call(gem_name:, fields: nil)
@@ -251,6 +261,7 @@ module RubygemsMcp
251
261
 
252
262
  # Get reverse dependencies (gems that depend on this gem)
253
263
  class GetGemReverseDependenciesTool < BaseTool
264
+ tool_name "get_gem_reverse_dependencies"
254
265
  description "Get reverse dependencies - list of gems that depend on the specified gem"
255
266
 
256
267
  arguments do
@@ -264,6 +275,7 @@ module RubygemsMcp
264
275
 
265
276
  # Get download statistics for a gem version
266
277
  class GetGemVersionDownloadsTool < BaseTool
278
+ tool_name "get_gem_version_downloads"
267
279
  description "Get download statistics for a specific gem version"
268
280
 
269
281
  arguments do
@@ -278,6 +290,7 @@ module RubygemsMcp
278
290
 
279
291
  # Get latest gems (most recently added)
280
292
  class GetLatestGemsTool < BaseTool
293
+ tool_name "get_latest_gems"
281
294
  description "Get latest gems - most recently added gems to RubyGems.org"
282
295
 
283
296
  arguments do
@@ -291,6 +304,7 @@ module RubygemsMcp
291
304
 
292
305
  # Get recently updated gems
293
306
  class GetRecentlyUpdatedGemsTool < BaseTool
307
+ tool_name "get_recently_updated_gems"
294
308
  description "Get recently updated gems - most recently updated gem versions"
295
309
 
296
310
  arguments do
@@ -304,6 +318,7 @@ module RubygemsMcp
304
318
 
305
319
  # Get changelog summary for a gem
306
320
  class GetGemChangelogTool < BaseTool
321
+ tool_name "get_gem_changelog"
307
322
  description "Get changelog summary for a gem by fetching and parsing the changelog from its changelog_uri"
308
323
 
309
324
  arguments do
@@ -318,6 +333,7 @@ module RubygemsMcp
318
333
 
319
334
  # Search for gems by name
320
335
  class SearchGemsTool < BaseTool
336
+ tool_name "search_gems"
321
337
  description "Search for gems by name on RubyGems"
322
338
 
323
339
  arguments do
@@ -330,13 +346,13 @@ module RubygemsMcp
330
346
  end
331
347
 
332
348
  # Resource: Popular Ruby gems list
333
- class PopularGemsResource < MCP::Resource
349
+ class PopularGemsResource < FastMcp::Resource
334
350
  uri "rubygems://popular"
335
351
  resource_name "Popular Ruby Gems"
336
352
  description "A curated list of popular Ruby gems with their latest versions"
337
353
  mime_type "application/json"
338
354
 
339
- def default_content
355
+ def content
340
356
  client = Client.new
341
357
  popular_gems = %w[
342
358
  rails nokogiri bundler rake rspec devise puma sidekiq
@@ -366,13 +382,13 @@ module RubygemsMcp
366
382
  end
367
383
 
368
384
  # Resource: Ruby version compatibility information
369
- class RubyVersionCompatibilityResource < MCP::Resource
385
+ class RubyVersionCompatibilityResource < FastMcp::Resource
370
386
  uri "rubygems://ruby/compatibility"
371
387
  resource_name "Ruby Version Compatibility"
372
388
  description "Information about Ruby version compatibility and release dates"
373
389
  mime_type "application/json"
374
390
 
375
- def default_content
391
+ def content
376
392
  client = Client.new
377
393
  ruby_versions = client.get_ruby_versions(limit: 20, sort: :version_desc)
378
394
  latest = client.get_latest_ruby_version
@@ -402,13 +418,13 @@ module RubygemsMcp
402
418
  end
403
419
 
404
420
  # Resource: Ruby maintenance status for all versions
405
- class RubyMaintenanceStatusResource < MCP::Resource
421
+ class RubyMaintenanceStatusResource < FastMcp::Resource
406
422
  uri "rubygems://ruby/maintenance"
407
423
  resource_name "Ruby Maintenance Status"
408
424
  description "Detailed maintenance status for all Ruby versions including EOL dates and maintenance phases"
409
425
  mime_type "application/json"
410
426
 
411
- def default_content
427
+ def content
412
428
  client = Client.new
413
429
  maintenance_status = client.get_ruby_maintenance_status
414
430
 
@@ -428,13 +444,13 @@ module RubygemsMcp
428
444
  end
429
445
 
430
446
  # Resource: Latest Ruby version
431
- class LatestRubyVersionResource < MCP::Resource
447
+ class LatestRubyVersionResource < FastMcp::Resource
432
448
  uri "rubygems://ruby/latest"
433
449
  resource_name "Latest Ruby Version"
434
450
  description "The latest stable Ruby version with release date"
435
451
  mime_type "application/json"
436
452
 
437
- def default_content
453
+ def content
438
454
  client = Client.new
439
455
  latest = client.get_latest_ruby_version
440
456
  JSON.pretty_generate(latest)
@@ -1,3 +1,3 @@
1
1
  module RubygemsMcp
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/sig/rubygems_mcp.rbs CHANGED
@@ -59,7 +59,7 @@ module RubygemsMcp
59
59
  ) -> Array[Hash[Symbol, (String | nil)]]
60
60
 
61
61
  # Get changelog for a Ruby version
62
- def get_ruby_version_changelog: (String version) -> Hash[Symbol, (String | nil)]
62
+ def get_ruby_version_changelog: (String version) -> Hash[Symbol, (String | nil)] # Returns :version, :release_notes_url, :content
63
63
 
64
64
  # Get reverse dependencies (gems that depend on this gem)
65
65
  def get_gem_reverse_dependencies: (String gem_name) -> Array[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.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -13,16 +13,22 @@ dependencies:
13
13
  name: fast-mcp
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '0.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
- - - "~>"
26
+ - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: '0.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: nokogiri
28
34
  requirement: !ruby/object:Gem::Requirement