rubygems_mcp 0.1.1 → 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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +27 -38
- data/lib/rubygems_mcp/client.rb +520 -56
- data/lib/rubygems_mcp/errors.rb +48 -0
- data/lib/rubygems_mcp/server.rb +87 -2
- data/lib/rubygems_mcp/version.rb +1 -1
- data/lib/rubygems_mcp.rb +1 -0
- data/sig/rubygems_mcp.rbs +68 -13
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 263030b634fd9d87f2a5d3d4ff68574e017438ec57a7caff2ebc0b5729dfccb2
|
|
4
|
+
data.tar.gz: 8c9e6142f7c8c7cbb6695c5265c2a7655eb7606872963ec0db3bf5ffb9a7d140
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f6bc852cf223da1499bf87063b2f92acbc562d40b4bd7d7fdfd8a4af38bf79064616fb8c0275b50e1e1e148a390cf92fcbdd0547cd3d45ca96d7ccf6966c1ec
|
|
7
|
+
data.tar.gz: 3ae00927439ed56b949e3d2a3ffa3830d87e3ee89d34d6e62adad8af51e7df54a33f2af870ee4b657b80300d335528b7627c8a2d986375a9a8aab7a81ce79516
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.1.2 (2025-11-21)
|
|
13
|
+
|
|
14
|
+
- Enhance Ruby version changelog retrieval and increase maximum response size
|
|
15
|
+
- Rename rubygems key to rubygems-dev in mcp.json configuration for development environment setup
|
|
16
|
+
|
|
3
17
|
## 0.1.1 (2025-11-21)
|
|
4
18
|
|
|
5
19
|
- Update fast-mcp dependency version constraints to allow versions >= 0.1 and < 2.0
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# rubygems_mcp
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/rubygems_mcp) [](https://github.com/amkisko/rubygems_mcp.rb/actions/workflows/test.yml)
|
|
3
|
+
[](https://badge.fury.io/rb/rubygems_mcp) [](https://github.com/amkisko/rubygems_mcp.rb/actions/workflows/test.yml) [](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
|
|
|
@@ -26,19 +26,6 @@ gem install rubygems_mcp
|
|
|
26
26
|
|
|
27
27
|
For Cursor IDE, create or update `.cursor/mcp.json` in your project:
|
|
28
28
|
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"mcpServers": {
|
|
32
|
-
"rubygems": {
|
|
33
|
-
"command": "bundle",
|
|
34
|
-
"args": ["exec", "rubygems_mcp"]
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Or if installed globally:
|
|
41
|
-
|
|
42
29
|
```json
|
|
43
30
|
{
|
|
44
31
|
"mcpServers": {
|
|
@@ -56,20 +43,6 @@ For Claude Desktop, edit the MCP configuration file:
|
|
|
56
43
|
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
57
44
|
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
58
45
|
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"mcpServers": {
|
|
62
|
-
"rubygems": {
|
|
63
|
-
"command": "bundle",
|
|
64
|
-
"args": ["exec", "rubygems_mcp"],
|
|
65
|
-
"cwd": "/path/to/your/project"
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Or if installed globally:
|
|
72
|
-
|
|
73
46
|
```json
|
|
74
47
|
{
|
|
75
48
|
"mcpServers": {
|
|
@@ -120,7 +93,7 @@ The server will start and communicate via STDIN/STDOUT using the MCP protocol.
|
|
|
120
93
|
|
|
121
94
|
- **RubyGems API Client**: Full-featured client for RubyGems REST API with comprehensive endpoint coverage
|
|
122
95
|
- **Ruby Version Information**: Fetch Ruby release information, changelogs, and maintenance status from ruby-lang.org
|
|
123
|
-
- **MCP Server Integration**: Ready-to-use MCP server with
|
|
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
|
|
124
97
|
- **Pagination & Sorting**: Support for large result sets with customizable pagination and sorting options
|
|
125
98
|
- **Caching**: In-memory caching with configurable TTL for improved performance
|
|
126
99
|
- **Error Handling**: Graceful error handling with custom exceptions and response size limits
|
|
@@ -223,11 +196,15 @@ recently_updated = client.get_recently_updated_gems(limit: 10)
|
|
|
223
196
|
- `get_latest_ruby_version` - Get latest Ruby version with release date
|
|
224
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.
|
|
225
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
|
|
226
202
|
- `get_ruby_maintenance_status` - Get maintenance status for all Ruby versions including EOL dates and maintenance phases
|
|
227
203
|
|
|
228
204
|
### Gem Information
|
|
229
205
|
|
|
230
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.
|
|
231
208
|
- `get_gem_reverse_dependencies(gem_name)` - Get reverse dependencies - list of gems that depend on the specified gem
|
|
232
209
|
- `get_gem_version_downloads(gem_name, version)` - Get download statistics for a specific gem version
|
|
233
210
|
- `get_gem_changelog(gem_name, version: nil)` - Get changelog summary for a gem by fetching and parsing the changelog from its changelog_uri
|
|
@@ -278,23 +255,35 @@ The MCP server provides the following tools:
|
|
|
278
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.
|
|
279
256
|
- Parameters: `gem_name` (string), `fields` (optional array of strings)
|
|
280
257
|
|
|
281
|
-
7. **
|
|
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
|
|
282
262
|
- Parameters: `gem_name` (string)
|
|
283
263
|
|
|
284
|
-
|
|
264
|
+
9. **get_gem_version_downloads** - Get download statistics for a specific gem version
|
|
285
265
|
- Parameters: `gem_name` (string), `version` (string)
|
|
286
266
|
|
|
287
|
-
|
|
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
|
|
288
271
|
- Parameters: `limit` (optional integer, default: 30, max: 50)
|
|
289
272
|
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
292
281
|
|
|
293
|
-
|
|
294
|
-
|
|
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")
|
|
295
284
|
|
|
296
|
-
|
|
297
|
-
|
|
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")
|
|
298
287
|
|
|
299
288
|
## MCP Resources
|
|
300
289
|
|
data/lib/rubygems_mcp/client.rb
CHANGED
|
@@ -14,33 +14,20 @@ module RubygemsMcp
|
|
|
14
14
|
# all_versions = client.get_gem_versions("rails")
|
|
15
15
|
# ruby_version = client.get_latest_ruby_version
|
|
16
16
|
class Client
|
|
17
|
-
# Maximum response size (
|
|
18
|
-
MAX_RESPONSE_SIZE = 1024 * 1024 #
|
|
17
|
+
# Maximum response size (5MB) to protect against crawler protection pages
|
|
18
|
+
MAX_RESPONSE_SIZE = 5 * 1024 * 1024 # 5MB
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
@@ -351,12 +348,31 @@ module RubygemsMcp
|
|
|
351
348
|
|
|
352
349
|
# Get full changelog content for a Ruby version from release notes
|
|
353
350
|
#
|
|
354
|
-
# @param version [String] Ruby version (e.g., "3.4.7")
|
|
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
|
+
|
|
358
|
+
# Normalize the input version for comparison (handles formats like "4.0.0-preview2" -> "4.0.0.pre.preview2")
|
|
359
|
+
normalized_input = begin
|
|
360
|
+
Gem::Version.new(version).to_s
|
|
361
|
+
rescue ArgumentError
|
|
362
|
+
version
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Try exact match first, then normalized match
|
|
366
|
+
version_data = versions.find do |v|
|
|
367
|
+
v[:version] == version ||
|
|
368
|
+
v[:version] == normalized_input ||
|
|
369
|
+
begin
|
|
370
|
+
Gem::Version.new(v[:version]).to_s == normalized_input
|
|
371
|
+
rescue ArgumentError
|
|
372
|
+
false
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
360
376
|
return {version: version, release_notes_url: nil, content: nil, error: "Version not found"} unless version_data
|
|
361
377
|
|
|
362
378
|
release_notes_url = version_data[:release_notes_url]
|
|
@@ -371,32 +387,46 @@ module RubygemsMcp
|
|
|
371
387
|
|
|
372
388
|
uri = URI(release_notes_url)
|
|
373
389
|
response = make_request(uri, parse_html: true)
|
|
374
|
-
return {version: version, release_notes_url: release_notes_url, content: nil, error: "Failed to fetch release notes"} unless response
|
|
375
390
|
|
|
376
|
-
|
|
377
|
-
|
|
391
|
+
content = nil
|
|
392
|
+
github_changelog = nil
|
|
378
393
|
|
|
379
|
-
if
|
|
380
|
-
#
|
|
381
|
-
content.css("
|
|
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
|
|
382
397
|
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
385
401
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
text = text.gsub(/[ \t]+/, " ")
|
|
402
|
+
# Get the full text content, preserving structure
|
|
403
|
+
content = content_elem.text.strip
|
|
389
404
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
394
423
|
end
|
|
395
424
|
|
|
396
425
|
result = {
|
|
397
426
|
version: version,
|
|
398
427
|
release_notes_url: release_notes_url,
|
|
399
|
-
content:
|
|
428
|
+
content: content,
|
|
429
|
+
github_changelog: github_changelog
|
|
400
430
|
}
|
|
401
431
|
|
|
402
432
|
# Cache for 24 hours
|
|
@@ -405,11 +435,224 @@ module RubygemsMcp
|
|
|
405
435
|
result
|
|
406
436
|
end
|
|
407
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
|
+
|
|
408
650
|
# Get reverse dependencies (gems that depend on this gem)
|
|
409
651
|
#
|
|
410
652
|
# @param gem_name [String] Gem name
|
|
411
653
|
# @return [Array<String>] Array of gem names that depend on this gem
|
|
412
654
|
def get_gem_reverse_dependencies(gem_name)
|
|
655
|
+
gem_name = validate_gem_name(gem_name)
|
|
413
656
|
cache_key = "gem_reverse_deps:#{gem_name}"
|
|
414
657
|
|
|
415
658
|
if @cache_enabled
|
|
@@ -434,6 +677,8 @@ module RubygemsMcp
|
|
|
434
677
|
# @param version [String] Gem version (e.g., "1.0.0")
|
|
435
678
|
# @return [Hash] Hash with :version_downloads and :total_downloads
|
|
436
679
|
def get_gem_version_downloads(gem_name, version)
|
|
680
|
+
gem_name = validate_gem_name(gem_name)
|
|
681
|
+
validate_version_string(version)
|
|
437
682
|
cache_key = "gem_downloads:#{gem_name}:#{version}"
|
|
438
683
|
|
|
439
684
|
if @cache_enabled
|
|
@@ -464,6 +709,7 @@ module RubygemsMcp
|
|
|
464
709
|
# @param limit [Integer, nil] Maximum number of gems to return (default: 30, max: 50)
|
|
465
710
|
# @return [Array<Hash>] Array of gem information
|
|
466
711
|
def get_latest_gems(limit: 30)
|
|
712
|
+
validate_pagination_params(limit: limit, offset: 0)
|
|
467
713
|
limit = [limit || 30, 50].min # API returns max 50
|
|
468
714
|
cache_key = "latest_gems:#{limit}"
|
|
469
715
|
|
|
@@ -502,6 +748,7 @@ module RubygemsMcp
|
|
|
502
748
|
# @param limit [Integer, nil] Maximum number of gems to return (default: 30, max: 50)
|
|
503
749
|
# @return [Array<Hash>] Array of gem version information
|
|
504
750
|
def get_recently_updated_gems(limit: 30)
|
|
751
|
+
validate_pagination_params(limit: limit, offset: 0)
|
|
505
752
|
limit = [limit || 30, 50].min # API returns max 50
|
|
506
753
|
cache_key = "recently_updated_gems:#{limit}"
|
|
507
754
|
|
|
@@ -543,6 +790,8 @@ module RubygemsMcp
|
|
|
543
790
|
# @param version [String, nil] Gem version (optional, uses latest if not provided)
|
|
544
791
|
# @return [Hash] Hash with :gem_name, :version, :changelog_uri, and :summary
|
|
545
792
|
def get_gem_changelog(gem_name, version: nil)
|
|
793
|
+
gem_name = validate_gem_name(gem_name)
|
|
794
|
+
validate_version_string(version) if version
|
|
546
795
|
# Get gem info to find changelog_uri
|
|
547
796
|
gem_info = get_gem_info(gem_name)
|
|
548
797
|
return {gem_name: gem_name, version: nil, changelog_uri: nil, summary: nil, error: "Gem not found"} if gem_info.empty?
|
|
@@ -686,6 +935,7 @@ module RubygemsMcp
|
|
|
686
935
|
# dependencies, changelog_uri, funding_uri, platform, sha, spec_sha, metadata
|
|
687
936
|
# @return [Hash] Hash with selected gem information
|
|
688
937
|
def get_gem_info(gem_name, fields: nil)
|
|
938
|
+
gem_name = validate_gem_name(gem_name)
|
|
689
939
|
cache_key = "gem_info:#{gem_name}"
|
|
690
940
|
|
|
691
941
|
if @cache_enabled
|
|
@@ -730,6 +980,82 @@ module RubygemsMcp
|
|
|
730
980
|
select_fields([gem_info], fields).first || gem_info
|
|
731
981
|
end
|
|
732
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
|
+
|
|
733
1059
|
# Search for gems by name
|
|
734
1060
|
#
|
|
735
1061
|
# @param query [String] Search query
|
|
@@ -737,6 +1063,8 @@ module RubygemsMcp
|
|
|
737
1063
|
# @param offset [Integer] Number of results to skip (for pagination)
|
|
738
1064
|
# @return [Array<Hash>] Array of hashes with gem information
|
|
739
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)
|
|
740
1068
|
# Don't cache search results as they can change frequently
|
|
741
1069
|
uri = URI("#{RUBYGEMS_API_BASE}/search.json")
|
|
742
1070
|
uri.query = URI.encode_www_form(query: query)
|
|
@@ -761,6 +1089,126 @@ module RubygemsMcp
|
|
|
761
1089
|
results
|
|
762
1090
|
end
|
|
763
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
|
+
|
|
764
1212
|
private
|
|
765
1213
|
|
|
766
1214
|
# Apply pagination and sorting to a version array
|
|
@@ -847,7 +1295,7 @@ module RubygemsMcp
|
|
|
847
1295
|
response_body = response.body || ""
|
|
848
1296
|
response_size = response_body.bytesize
|
|
849
1297
|
if response_size > MAX_RESPONSE_SIZE
|
|
850
|
-
raise ResponseSizeExceededError.new(response_size, MAX_RESPONSE_SIZE)
|
|
1298
|
+
raise ResponseSizeExceededError.new(response_size, MAX_RESPONSE_SIZE, uri: uri.to_s)
|
|
851
1299
|
end
|
|
852
1300
|
|
|
853
1301
|
# Validate and parse response
|
|
@@ -856,18 +1304,25 @@ module RubygemsMcp
|
|
|
856
1304
|
else
|
|
857
1305
|
validate_and_parse_json(response_body, uri)
|
|
858
1306
|
end
|
|
859
|
-
when Net::HTTPNotFound
|
|
860
|
-
raise "Resource not found. Response: #{response.body[0..500]}"
|
|
861
1307
|
else
|
|
862
|
-
|
|
1308
|
+
handle_http_error(response, uri)
|
|
863
1309
|
end
|
|
864
1310
|
rescue ResponseSizeExceededError, CorruptedDataError
|
|
865
1311
|
# Re-raise our custom errors as-is (don't cache corrupted data)
|
|
866
1312
|
raise
|
|
867
1313
|
rescue OpenSSL::SSL::SSLError => e
|
|
868
|
-
raise
|
|
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
|
|
869
1321
|
rescue => e
|
|
870
|
-
raise
|
|
1322
|
+
raise APIError.new(
|
|
1323
|
+
"Request to #{uri} failed: #{e.class} - #{e.message}",
|
|
1324
|
+
uri: uri.to_s
|
|
1325
|
+
)
|
|
871
1326
|
end
|
|
872
1327
|
|
|
873
1328
|
# Validate and parse JSON response
|
|
@@ -881,7 +1336,8 @@ module RubygemsMcp
|
|
|
881
1336
|
if body.strip.start_with?("<") && body.match?(/cloudflare|ddos protection|access denied|blocked|captcha/i)
|
|
882
1337
|
raise CorruptedDataError.new(
|
|
883
1338
|
"Response appears to be a crawler protection page from #{uri}",
|
|
884
|
-
response_size: body.bytesize
|
|
1339
|
+
response_size: body.bytesize,
|
|
1340
|
+
uri: uri.to_s
|
|
885
1341
|
)
|
|
886
1342
|
end
|
|
887
1343
|
|
|
@@ -892,7 +1348,8 @@ module RubygemsMcp
|
|
|
892
1348
|
unless parsed.is_a?(Hash) || parsed.is_a?(Array)
|
|
893
1349
|
raise CorruptedDataError.new(
|
|
894
1350
|
"Invalid JSON structure: expected Hash or Array, got #{parsed.class}",
|
|
895
|
-
response_size: body.bytesize
|
|
1351
|
+
response_size: body.bytesize,
|
|
1352
|
+
uri: uri.to_s
|
|
896
1353
|
)
|
|
897
1354
|
end
|
|
898
1355
|
|
|
@@ -903,14 +1360,16 @@ module RubygemsMcp
|
|
|
903
1360
|
raise CorruptedDataError.new(
|
|
904
1361
|
"Received HTML instead of JSON from #{uri}. This may indicate an error page or crawler protection.",
|
|
905
1362
|
original_error: e,
|
|
906
|
-
response_size: body.bytesize
|
|
1363
|
+
response_size: body.bytesize,
|
|
1364
|
+
uri: uri.to_s
|
|
907
1365
|
)
|
|
908
1366
|
end
|
|
909
1367
|
|
|
910
1368
|
raise CorruptedDataError.new(
|
|
911
1369
|
"Failed to parse JSON response from #{uri}: #{e.message}",
|
|
912
1370
|
original_error: e,
|
|
913
|
-
response_size: body.bytesize
|
|
1371
|
+
response_size: body.bytesize,
|
|
1372
|
+
uri: uri.to_s
|
|
914
1373
|
)
|
|
915
1374
|
end
|
|
916
1375
|
end
|
|
@@ -925,7 +1384,8 @@ module RubygemsMcp
|
|
|
925
1384
|
if body.match?(/cloudflare|ddos protection|access denied|blocked|captcha|rate limit/i)
|
|
926
1385
|
raise CorruptedDataError.new(
|
|
927
1386
|
"Response appears to be a crawler protection page from #{uri}",
|
|
928
|
-
response_size: body.bytesize
|
|
1387
|
+
response_size: body.bytesize,
|
|
1388
|
+
uri: uri.to_s
|
|
929
1389
|
)
|
|
930
1390
|
end
|
|
931
1391
|
|
|
@@ -933,7 +1393,8 @@ module RubygemsMcp
|
|
|
933
1393
|
unless body.strip.start_with?("<!DOCTYPE", "<html", "<HTML") || body.include?("<html")
|
|
934
1394
|
raise CorruptedDataError.new(
|
|
935
1395
|
"Response from #{uri} does not appear to be HTML",
|
|
936
|
-
response_size: body.bytesize
|
|
1396
|
+
response_size: body.bytesize,
|
|
1397
|
+
uri: uri.to_s
|
|
937
1398
|
)
|
|
938
1399
|
end
|
|
939
1400
|
|
|
@@ -944,7 +1405,8 @@ module RubygemsMcp
|
|
|
944
1405
|
if doc.text.strip.length < 50
|
|
945
1406
|
raise CorruptedDataError.new(
|
|
946
1407
|
"HTML response from #{uri} appears to be empty or too short",
|
|
947
|
-
response_size: body.bytesize
|
|
1408
|
+
response_size: body.bytesize,
|
|
1409
|
+
uri: uri.to_s
|
|
948
1410
|
)
|
|
949
1411
|
end
|
|
950
1412
|
|
|
@@ -960,7 +1422,8 @@ module RubygemsMcp
|
|
|
960
1422
|
if error_indicators.any? { |pattern| doc.text.match?(pattern) }
|
|
961
1423
|
raise CorruptedDataError.new(
|
|
962
1424
|
"HTML response from #{uri} appears to be an error page",
|
|
963
|
-
response_size: body.bytesize
|
|
1425
|
+
response_size: body.bytesize,
|
|
1426
|
+
uri: uri.to_s
|
|
964
1427
|
)
|
|
965
1428
|
end
|
|
966
1429
|
|
|
@@ -969,7 +1432,8 @@ module RubygemsMcp
|
|
|
969
1432
|
raise CorruptedDataError.new(
|
|
970
1433
|
"Failed to parse HTML from #{uri}: #{e.message}",
|
|
971
1434
|
original_error: e,
|
|
972
|
-
response_size: body.bytesize
|
|
1435
|
+
response_size: body.bytesize,
|
|
1436
|
+
uri: uri.to_s
|
|
973
1437
|
)
|
|
974
1438
|
end
|
|
975
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
|
data/lib/rubygems_mcp/server.rb
CHANGED
|
@@ -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)
|
|
@@ -240,7 +244,30 @@ module RubygemsMcp
|
|
|
240
244
|
end
|
|
241
245
|
|
|
242
246
|
def call(version:)
|
|
243
|
-
get_client.get_ruby_version_changelog(version)
|
|
247
|
+
result = get_client.get_ruby_version_changelog(version)
|
|
248
|
+
# Convert content to MCP-compliant format
|
|
249
|
+
# MCP expects content to be an array of content items with type and text fields
|
|
250
|
+
if result.is_a?(Hash)
|
|
251
|
+
if result[:content].is_a?(String) && !result[:content].empty?
|
|
252
|
+
result[:content] = [{type: "text", text: result[:content]}]
|
|
253
|
+
elsif result[:content].is_a?(String) && result[:content].empty?
|
|
254
|
+
result[:content] = []
|
|
255
|
+
elsif result[:content].nil?
|
|
256
|
+
result[:content] = []
|
|
257
|
+
elsif result[:content].is_a?(Array)
|
|
258
|
+
# Ensure array elements have the correct format
|
|
259
|
+
result[:content] = result[:content].map do |item|
|
|
260
|
+
if item.is_a?(String)
|
|
261
|
+
{type: "text", text: item}
|
|
262
|
+
elsif item.is_a?(Hash) && !item.key?(:type)
|
|
263
|
+
item.merge(type: "text")
|
|
264
|
+
else
|
|
265
|
+
item
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
result
|
|
244
271
|
end
|
|
245
272
|
end
|
|
246
273
|
|
|
@@ -259,6 +286,22 @@ module RubygemsMcp
|
|
|
259
286
|
end
|
|
260
287
|
end
|
|
261
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
|
+
|
|
262
305
|
# Get reverse dependencies (gems that depend on this gem)
|
|
263
306
|
class GetGemReverseDependenciesTool < BaseTool
|
|
264
307
|
tool_name "get_gem_reverse_dependencies"
|
|
@@ -345,6 +388,48 @@ module RubygemsMcp
|
|
|
345
388
|
end
|
|
346
389
|
end
|
|
347
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
|
+
|
|
348
433
|
# Resource: Popular Ruby gems list
|
|
349
434
|
class PopularGemsResource < FastMcp::Resource
|
|
350
435
|
uri "rubygems://popular"
|
|
@@ -370,7 +455,7 @@ module RubygemsMcp
|
|
|
370
455
|
else
|
|
371
456
|
{name: gem_name, version: nil, release_date: nil}
|
|
372
457
|
end
|
|
373
|
-
rescue
|
|
458
|
+
rescue ResponseSizeExceededError, CorruptedDataError => e
|
|
374
459
|
# Skip gems that exceed size limit or have corrupted data
|
|
375
460
|
{name: gem_name, version: nil, release_date: nil, error: e.message}
|
|
376
461
|
end
|
data/lib/rubygems_mcp/version.rb
CHANGED
data/lib/rubygems_mcp.rb
CHANGED
data/sig/rubygems_mcp.rbs
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
module RubygemsMcp
|
|
2
2
|
VERSION: String
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
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
|