gemcp 0.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +126 -0
- data/bin/gemcp +7 -0
- data/lib/gemcp/cache.rb +102 -0
- data/lib/gemcp/client.rb +180 -0
- data/lib/gemcp/config.rb +35 -0
- data/lib/gemcp/dependency_graph.rb +130 -0
- data/lib/gemcp/errors.rb +24 -0
- data/lib/gemcp/local_repository.rb +105 -0
- data/lib/gemcp/rate_limiter.rb +31 -0
- data/lib/gemcp/server.rb +46 -0
- data/lib/gemcp/tools/base.rb +57 -0
- data/lib/gemcp/tools/check_compatibility.rb +86 -0
- data/lib/gemcp/tools/compare_versions.rb +64 -0
- data/lib/gemcp/tools/dependency_graph.rb +40 -0
- data/lib/gemcp/tools/get_context.rb +39 -0
- data/lib/gemcp/tools/get_gem.rb +21 -0
- data/lib/gemcp/tools/get_owners.rb +21 -0
- data/lib/gemcp/tools/get_releases_between.rb +61 -0
- data/lib/gemcp/tools/get_version.rb +25 -0
- data/lib/gemcp/tools/list_versions.rb +31 -0
- data/lib/gemcp/tools/reverse_dependencies.rb +36 -0
- data/lib/gemcp/tools/search_gems.rb +27 -0
- data/lib/gemcp/version.rb +5 -0
- data/lib/gemcp.rb +16 -0
- metadata +128 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems"
|
|
4
|
+
|
|
5
|
+
module GemCP
|
|
6
|
+
# Reads gem metadata from installed gemspecs on the local system.
|
|
7
|
+
# All methods return nil or empty arrays when nothing matches —
|
|
8
|
+
# never raises for missing gems.
|
|
9
|
+
class LocalRepository
|
|
10
|
+
# Find the latest installed version of a gem by name.
|
|
11
|
+
#
|
|
12
|
+
# @param name [String] gem name
|
|
13
|
+
# @return [Hash, nil] serialized gem metadata, or nil if not installed
|
|
14
|
+
def gem(name)
|
|
15
|
+
spec = Gem::Specification.find_all_by_name(name).max_by(&:version)
|
|
16
|
+
spec && serialize(spec, source: "local")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Find a specific installed version, optionally filtered by platform.
|
|
20
|
+
#
|
|
21
|
+
# @param name [String] gem name
|
|
22
|
+
# @param number [String] exact version number
|
|
23
|
+
# @param platform [String, nil] platform string (e.g. "ruby", "x86_64-linux")
|
|
24
|
+
# @return [Hash, nil] serialized version metadata, or nil if not installed
|
|
25
|
+
def version(name, number, platform: nil)
|
|
26
|
+
specs = Gem::Specification.find_all_by_name(name, Gem::Requirement.new("=#{number}"))
|
|
27
|
+
spec = if platform
|
|
28
|
+
specs.find { |candidate| candidate.platform.to_s == platform }
|
|
29
|
+
else
|
|
30
|
+
specs.first
|
|
31
|
+
end
|
|
32
|
+
spec && serialize(spec, source: "local")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# List all installed versions of a gem, newest first.
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] gem name
|
|
38
|
+
# @return [Array<Hash>] version metadata hashes (empty if not installed)
|
|
39
|
+
def versions(name)
|
|
40
|
+
Gem::Specification.find_all_by_name(name)
|
|
41
|
+
.sort_by(&:version)
|
|
42
|
+
.reverse
|
|
43
|
+
.map { |spec| serialize_version(spec) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Search installed gems by name or summary substring match.
|
|
47
|
+
#
|
|
48
|
+
# @param query [String] case-insensitive search text
|
|
49
|
+
# @return [Array<Hash>] matching gem metadata (latest version per gem)
|
|
50
|
+
def search(query)
|
|
51
|
+
needle = query.downcase
|
|
52
|
+
Gem::Specification.each
|
|
53
|
+
.select { |spec| spec.name.downcase.include?(needle) || spec.summary.to_s.downcase.include?(needle) }
|
|
54
|
+
.group_by(&:name)
|
|
55
|
+
.values
|
|
56
|
+
.map { |specs| serialize(specs.max_by(&:version), source: "local") }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# @param spec [Gem::Specification]
|
|
62
|
+
# @param source [String] provenance tag
|
|
63
|
+
# @return [Hash]
|
|
64
|
+
def serialize(spec, source:)
|
|
65
|
+
{
|
|
66
|
+
"name" => spec.name,
|
|
67
|
+
"version" => spec.version.to_s,
|
|
68
|
+
"platform" => spec.platform.to_s,
|
|
69
|
+
"authors" => Array(spec.authors).join(", "),
|
|
70
|
+
"info" => spec.description || spec.summary,
|
|
71
|
+
"summary" => spec.summary,
|
|
72
|
+
"licenses" => Array(spec.licenses),
|
|
73
|
+
"homepage_uri" => spec.homepage,
|
|
74
|
+
"source_code_uri" => spec.metadata["source_code_uri"],
|
|
75
|
+
"documentation_uri" => spec.metadata["documentation_uri"],
|
|
76
|
+
"required_ruby_version" => spec.required_ruby_version.to_s,
|
|
77
|
+
"required_rubygems_version" => spec.required_rubygems_version.to_s,
|
|
78
|
+
"dependencies" => {
|
|
79
|
+
"runtime" => spec.runtime_dependencies.map { |dep| serialize_dependency(dep) },
|
|
80
|
+
"development" => spec.development_dependencies.map { |dep| serialize_dependency(dep) }
|
|
81
|
+
},
|
|
82
|
+
"metadata_source" => source,
|
|
83
|
+
"full_gem_path" => spec.full_gem_path
|
|
84
|
+
}.compact
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param spec [Gem::Specification]
|
|
88
|
+
# @return [Hash]
|
|
89
|
+
def serialize_version(spec)
|
|
90
|
+
{
|
|
91
|
+
"number" => spec.version.to_s,
|
|
92
|
+
"platform" => spec.platform.to_s,
|
|
93
|
+
"ruby_version" => spec.required_ruby_version.to_s,
|
|
94
|
+
"rubygems_version" => spec.required_rubygems_version.to_s,
|
|
95
|
+
"metadata_source" => "local"
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param dep [Gem::Dependency]
|
|
100
|
+
# @return [Hash]
|
|
101
|
+
def serialize_dependency(dep)
|
|
102
|
+
{ "name" => dep.name, "requirements" => dep.requirement.to_s }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
# Thread-safe rate limiter that enforces a minimum interval between calls.
|
|
5
|
+
# Uses a Mutex to prevent interleaving during the sleep gap.
|
|
6
|
+
class RateLimiter
|
|
7
|
+
# @param requests_per_second [Float] maximum request rate (0 disables throttling)
|
|
8
|
+
def initialize(requests_per_second:)
|
|
9
|
+
@minimum_interval = requests_per_second.positive? ? 1.0 / requests_per_second : 0.0
|
|
10
|
+
@last_request_at = nil
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Blocks until enough time has elapsed since the last call.
|
|
15
|
+
# Safe to call from multiple threads.
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
def wait
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
|
+
|
|
22
|
+
if @last_request_at
|
|
23
|
+
sleep_for = @minimum_interval - (now - @last_request_at)
|
|
24
|
+
sleep(sleep_for) if sleep_for.positive?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@last_request_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/gemcp/server.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fast_mcp"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "tools/base"
|
|
6
|
+
require_relative "tools/search_gems"
|
|
7
|
+
require_relative "tools/get_gem"
|
|
8
|
+
require_relative "tools/list_versions"
|
|
9
|
+
require_relative "tools/get_version"
|
|
10
|
+
require_relative "tools/get_owners"
|
|
11
|
+
require_relative "tools/dependency_graph"
|
|
12
|
+
require_relative "tools/get_context"
|
|
13
|
+
require_relative "tools/check_compatibility"
|
|
14
|
+
require_relative "tools/get_releases_between"
|
|
15
|
+
require_relative "tools/reverse_dependencies"
|
|
16
|
+
require_relative "tools/compare_versions"
|
|
17
|
+
|
|
18
|
+
module GemCP
|
|
19
|
+
# Builds and configures the MCP server with all registered tools.
|
|
20
|
+
module Server
|
|
21
|
+
TOOLS = [
|
|
22
|
+
Tools::SearchGems,
|
|
23
|
+
Tools::GetGem,
|
|
24
|
+
Tools::ListVersions,
|
|
25
|
+
Tools::GetVersion,
|
|
26
|
+
Tools::GetOwners,
|
|
27
|
+
Tools::DependencyGraph,
|
|
28
|
+
Tools::GetContext,
|
|
29
|
+
Tools::CheckCompatibility,
|
|
30
|
+
Tools::GetReleasesBetween,
|
|
31
|
+
Tools::ReverseDependencies,
|
|
32
|
+
Tools::CompareVersions
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# Build a FastMcp::Server with all GemCP tools registered.
|
|
38
|
+
#
|
|
39
|
+
# @return [FastMcp::Server]
|
|
40
|
+
def build
|
|
41
|
+
server = FastMcp::Server.new(name: "gemcp", version: VERSION)
|
|
42
|
+
TOOLS.each { |tool| server.register_tool(tool) }
|
|
43
|
+
server
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/monads"
|
|
4
|
+
require "fast_mcp"
|
|
5
|
+
|
|
6
|
+
module GemCP
|
|
7
|
+
module Tools
|
|
8
|
+
# Base class for all MCP tools. Provides client injection, error handling
|
|
9
|
+
# via dry-monads, and rendering to MCP response format.
|
|
10
|
+
class Base < FastMcp::Tool
|
|
11
|
+
include Dry::Monads[:result]
|
|
12
|
+
|
|
13
|
+
annotations(
|
|
14
|
+
read_only_hint: true,
|
|
15
|
+
destructive_hint: false,
|
|
16
|
+
idempotent_hint: true,
|
|
17
|
+
open_world_hint: true
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# @param client [Client, nil] injected client (defaults to a new Client)
|
|
21
|
+
def initialize(client: nil)
|
|
22
|
+
super()
|
|
23
|
+
@client = client || Client.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :client
|
|
29
|
+
|
|
30
|
+
# Execute a block, wrapping the return value in Success or capturing
|
|
31
|
+
# errors as Failure. Handles both GemCP errors and unexpected exceptions.
|
|
32
|
+
#
|
|
33
|
+
# @yield the operation to execute
|
|
34
|
+
# @return [Dry::Monads::Result]
|
|
35
|
+
def with_errors
|
|
36
|
+
Success(yield)
|
|
37
|
+
rescue GemCP::Error => e
|
|
38
|
+
Failure(error: e.error_type, message: e.message)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
Failure(error: "InternalError", message: "#{e.class}: #{e.message}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Convert a dry-monads Result into an MCP tool response hash.
|
|
44
|
+
#
|
|
45
|
+
# @param result [Dry::Monads::Result]
|
|
46
|
+
# @return [Hash] MCP response with :content and :isError keys
|
|
47
|
+
def render(result)
|
|
48
|
+
case result
|
|
49
|
+
in Dry::Monads::Success(data)
|
|
50
|
+
{ content: [{ type: "text", text: JSON.pretty_generate(data) }], isError: false }
|
|
51
|
+
in Dry::Monads::Failure(data)
|
|
52
|
+
{ content: [{ type: "text", text: JSON.generate(data) }], isError: true }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Check if two gems can coexist by comparing their dependency constraints.
|
|
6
|
+
# Detects hard conflicts where declared requirements are mutually exclusive.
|
|
7
|
+
class CheckCompatibility < Base
|
|
8
|
+
tool_name "check_compatibility"
|
|
9
|
+
description \
|
|
10
|
+
"Check if two gems' dependency constraints conflict. Compares their runtime dependency " \
|
|
11
|
+
"requirements to find shared dependencies with incompatible version ranges."
|
|
12
|
+
|
|
13
|
+
arguments do
|
|
14
|
+
required(:gem_a).filled(:string).description("First gem name")
|
|
15
|
+
required(:version_a).filled(:string).description("First gem version")
|
|
16
|
+
required(:gem_b).filled(:string).description("Second gem name")
|
|
17
|
+
required(:version_b).filled(:string).description("Second gem version")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param gem_a [String] first gem name
|
|
21
|
+
# @param version_a [String] first gem version
|
|
22
|
+
# @param gem_b [String] second gem name
|
|
23
|
+
# @param version_b [String] second gem version
|
|
24
|
+
# @return [Hash] MCP response
|
|
25
|
+
def call(gem_a:, version_a:, gem_b:, version_b:)
|
|
26
|
+
result = with_errors do
|
|
27
|
+
meta_a = client.version(gem_a, version_a)
|
|
28
|
+
meta_b = client.version(gem_b, version_b)
|
|
29
|
+
|
|
30
|
+
deps_a = normalize_deps(meta_a)
|
|
31
|
+
deps_b = normalize_deps(meta_b)
|
|
32
|
+
|
|
33
|
+
shared_deps = deps_a.keys & deps_b.keys
|
|
34
|
+
conflicts = find_conflicts(shared_deps, deps_a, deps_b)
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
gem_a: { name: gem_a, version: version_a },
|
|
38
|
+
gem_b: { name: gem_b, version: version_b },
|
|
39
|
+
compatible: conflicts.empty?,
|
|
40
|
+
shared_dependencies: shared_deps.length,
|
|
41
|
+
conflicts: conflicts
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
render(result)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def normalize_deps(metadata)
|
|
50
|
+
Array(metadata.dig("dependencies", "runtime")).each_with_object({}) do |dep, hash|
|
|
51
|
+
hash[dep["name"]] = dep["requirements"]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def find_conflicts(shared_deps, deps_a, deps_b)
|
|
56
|
+
shared_deps.filter_map do |dep_name|
|
|
57
|
+
req_a = Gem::Requirement.new(deps_a[dep_name].split(",").map(&:strip))
|
|
58
|
+
req_b = Gem::Requirement.new(deps_b[dep_name].split(",").map(&:strip))
|
|
59
|
+
|
|
60
|
+
next if requirements_overlap?(req_a, req_b)
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
dependency: dep_name,
|
|
64
|
+
requirement_a: deps_a[dep_name],
|
|
65
|
+
requirement_b: deps_b[dep_name]
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def requirements_overlap?(req_a, req_b)
|
|
71
|
+
# Test a range of versions to see if any satisfy both requirements.
|
|
72
|
+
# This is a heuristic — not a full SAT solver — but covers common cases.
|
|
73
|
+
test_versions.any? { |v| req_a.satisfied_by?(v) && req_b.satisfied_by?(v) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_versions
|
|
77
|
+
@test_versions ||= begin
|
|
78
|
+
majors = (0..30)
|
|
79
|
+
majors.flat_map do |major|
|
|
80
|
+
[0, 1, 2, 5, 10, 15, 20].map { |minor| Gem::Version.new("#{major}.#{minor}.0") }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Compare dependency requirements between two versions of the same gem.
|
|
6
|
+
# Shows added, removed, and changed dependencies.
|
|
7
|
+
class CompareVersions < Base
|
|
8
|
+
tool_name "compare_versions"
|
|
9
|
+
description \
|
|
10
|
+
"Diff the dependency requirements between two versions of a gem. " \
|
|
11
|
+
"Shows added, removed, and changed runtime dependencies."
|
|
12
|
+
|
|
13
|
+
arguments do
|
|
14
|
+
required(:name).filled(:string).description("Gem name")
|
|
15
|
+
required(:old_version).filled(:string).description("Older version to compare from")
|
|
16
|
+
required(:new_version).filled(:string).description("Newer version to compare to")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param name [String] gem name
|
|
20
|
+
# @param old_version [String] older version
|
|
21
|
+
# @param new_version [String] newer version
|
|
22
|
+
# @return [Hash] MCP response
|
|
23
|
+
def call(name:, old_version:, new_version:)
|
|
24
|
+
result = with_errors do
|
|
25
|
+
old_meta = client.version(name, old_version)
|
|
26
|
+
new_meta = client.version(name, new_version)
|
|
27
|
+
|
|
28
|
+
old_deps = normalize_deps(old_meta)
|
|
29
|
+
new_deps = normalize_deps(new_meta)
|
|
30
|
+
|
|
31
|
+
added = (new_deps.keys - old_deps.keys).map { |n| { name: n, requirements: new_deps[n] } }
|
|
32
|
+
removed = (old_deps.keys - new_deps.keys).map { |n| { name: n, requirements: old_deps[n] } }
|
|
33
|
+
changed = (old_deps.keys & new_deps.keys).filter_map do |n|
|
|
34
|
+
next if old_deps[n] == new_deps[n]
|
|
35
|
+
{ name: n, old_requirements: old_deps[n], new_requirements: new_deps[n] }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
name: name,
|
|
40
|
+
old_version: old_version,
|
|
41
|
+
new_version: new_version,
|
|
42
|
+
old_ruby_version: old_meta["ruby_version"],
|
|
43
|
+
new_ruby_version: new_meta["ruby_version"],
|
|
44
|
+
dependencies: {
|
|
45
|
+
added: added,
|
|
46
|
+
removed: removed,
|
|
47
|
+
changed: changed,
|
|
48
|
+
unchanged: (old_deps.keys & new_deps.keys).count - changed.length
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
render(result)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def normalize_deps(metadata)
|
|
58
|
+
Array(metadata.dig("dependencies", "runtime")).each_with_object({}) do |dep, hash|
|
|
59
|
+
hash[dep["name"]] = dep["requirements"]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Build an exploratory dependency graph from RubyGems metadata.
|
|
6
|
+
class DependencyGraph < Base
|
|
7
|
+
tool_name "get_dependency_graph"
|
|
8
|
+
description \
|
|
9
|
+
"Build a bounded exploratory dependency graph from RubyGems metadata. Constraints are preserved on edges, " \
|
|
10
|
+
"but transitive nodes use each dependency's latest release; this is not a Bundler lockfile resolution."
|
|
11
|
+
|
|
12
|
+
arguments do
|
|
13
|
+
required(:name).filled(:string).description("Root gem name")
|
|
14
|
+
optional(:version).filled(:string).description("Defaults to the latest version")
|
|
15
|
+
optional(:platform).filled(:string).description("Platform filter")
|
|
16
|
+
optional(:depth).filled(:integer).description("Traversal depth (0-4)")
|
|
17
|
+
optional(:include_development).filled(:bool).description("Include development dependencies")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param name [String] root gem name
|
|
21
|
+
# @param version [String, nil] root version (nil = latest)
|
|
22
|
+
# @param platform [String, nil] platform filter
|
|
23
|
+
# @param depth [Integer] traversal depth
|
|
24
|
+
# @param include_development [Boolean] whether to include dev dependencies
|
|
25
|
+
# @return [Hash] MCP response
|
|
26
|
+
def call(name:, version: nil, platform: nil, depth: 1, include_development: false)
|
|
27
|
+
result = with_errors do
|
|
28
|
+
GemCP::DependencyGraph.new(client: client).build(
|
|
29
|
+
name: name,
|
|
30
|
+
version: version,
|
|
31
|
+
platform: platform,
|
|
32
|
+
depth: depth,
|
|
33
|
+
include_development: include_development
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
render(result)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Aggregate gem metadata, recent versions, and owners into a single response
|
|
6
|
+
# with provenance metadata for AI agent consumption.
|
|
7
|
+
class GetContext < Base
|
|
8
|
+
tool_name "get_gem_context"
|
|
9
|
+
description "Aggregate registry facts useful for grounded AI answers: gem metadata, recent releases, and owners."
|
|
10
|
+
|
|
11
|
+
arguments do
|
|
12
|
+
required(:name).filled(:string).description("Gem name")
|
|
13
|
+
optional(:recent_versions).filled(:integer).description("Number of recent versions (1-20)")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param name [String] gem name
|
|
17
|
+
# @param recent_versions [Integer] how many recent versions to include
|
|
18
|
+
# @return [Hash] MCP response
|
|
19
|
+
def call(name:, recent_versions: 5)
|
|
20
|
+
result = with_errors do
|
|
21
|
+
{
|
|
22
|
+
gem: client.gem(name),
|
|
23
|
+
versions: client.versions(name).first(recent_versions),
|
|
24
|
+
owners: client.owners(name),
|
|
25
|
+
provenance: {
|
|
26
|
+
source: "RubyGems.org public API",
|
|
27
|
+
retrieved_at: Time.now.utc.iso8601,
|
|
28
|
+
authoritative_fields: [
|
|
29
|
+
"versions", "dependencies", "downloads", "owners", "checksums", "compatibility", "URIs"
|
|
30
|
+
],
|
|
31
|
+
untrusted_documentation_included: false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
render(result)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Fetch metadata for the latest version of a gem.
|
|
6
|
+
class GetGem < Base
|
|
7
|
+
tool_name "get_gem"
|
|
8
|
+
description "Get current RubyGems.org metadata for a gem, including latest version, downloads, URIs, and dependencies."
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:name).filled(:string).description("Gem name")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param name [String] gem name
|
|
15
|
+
# @return [Hash] MCP response
|
|
16
|
+
def call(name:)
|
|
17
|
+
render(with_errors { client.gem(name) })
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Fetch public owner/maintainer data for a gem.
|
|
6
|
+
class GetOwners < Base
|
|
7
|
+
tool_name "get_gem_owners"
|
|
8
|
+
description "List the public owners and maintainers returned by RubyGems.org for a gem."
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:name).filled(:string).description("Gem name")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param name [String] gem name
|
|
15
|
+
# @return [Hash] MCP response
|
|
16
|
+
def call(name:)
|
|
17
|
+
render(with_errors { { name: name, owners: client.owners(name) } })
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# List all releases between two versions of a gem, including changelog links.
|
|
6
|
+
class GetReleasesBetween < Base
|
|
7
|
+
tool_name "get_releases_between"
|
|
8
|
+
description \
|
|
9
|
+
"List all versions released between two versions of a gem, including release dates " \
|
|
10
|
+
"and changelog URIs. Useful for understanding the upgrade path between versions."
|
|
11
|
+
|
|
12
|
+
arguments do
|
|
13
|
+
required(:name).filled(:string).description("Gem name")
|
|
14
|
+
required(:from_version).filled(:string).description("Starting version (exclusive)")
|
|
15
|
+
required(:to_version).filled(:string).description("Ending version (inclusive)")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param name [String] gem name
|
|
19
|
+
# @param from_version [String] starting version (exclusive lower bound)
|
|
20
|
+
# @param to_version [String] ending version (inclusive upper bound)
|
|
21
|
+
# @return [Hash] MCP response
|
|
22
|
+
def call(name:, from_version:, to_version:)
|
|
23
|
+
result = with_errors do
|
|
24
|
+
all_versions = client.versions(name)
|
|
25
|
+
|
|
26
|
+
from = Gem::Version.new(from_version)
|
|
27
|
+
to = Gem::Version.new(to_version)
|
|
28
|
+
|
|
29
|
+
between = all_versions.select do |v|
|
|
30
|
+
version = Gem::Version.new(v["number"])
|
|
31
|
+
version > from && version <= to
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
between.sort_by! { |v| Gem::Version.new(v["number"]) }
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
name: name,
|
|
38
|
+
from_version: from_version,
|
|
39
|
+
to_version: to_version,
|
|
40
|
+
count: between.length,
|
|
41
|
+
releases: between.map { |v| summarize_release(v) }
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
render(result)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def summarize_release(version_data)
|
|
50
|
+
{
|
|
51
|
+
number: version_data["number"],
|
|
52
|
+
platform: version_data["platform"],
|
|
53
|
+
released_at: version_data["created_at"],
|
|
54
|
+
prerelease: version_data["prerelease"],
|
|
55
|
+
changelog_uri: version_data.dig("metadata", "changelog_uri"),
|
|
56
|
+
yanked: version_data["yanked"]
|
|
57
|
+
}.compact
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Fetch metadata for a specific gem version and optional platform.
|
|
6
|
+
class GetVersion < Base
|
|
7
|
+
tool_name "get_gem_version"
|
|
8
|
+
description "Get authoritative metadata for one gem version and optional platform using the RubyGems.org v2 API."
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:name).filled(:string).description("Gem name")
|
|
12
|
+
required(:version).filled(:string).description("Version number")
|
|
13
|
+
optional(:platform).filled(:string).description("Platform, such as ruby or x86_64-linux")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param name [String] gem name
|
|
17
|
+
# @param version [String] version number
|
|
18
|
+
# @param platform [String, nil] platform filter
|
|
19
|
+
# @return [Hash] MCP response
|
|
20
|
+
def call(name:, version:, platform: nil)
|
|
21
|
+
render(with_errors { client.version(name, version, platform: platform) })
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# List published versions of a gem with optional prerelease filtering.
|
|
6
|
+
class ListVersions < Base
|
|
7
|
+
tool_name "list_gem_versions"
|
|
8
|
+
description "List published versions and compatibility metadata for a gem."
|
|
9
|
+
|
|
10
|
+
arguments do
|
|
11
|
+
required(:name).filled(:string).description("Gem name")
|
|
12
|
+
optional(:include_prerelease).filled(:bool).description("Include prerelease versions")
|
|
13
|
+
optional(:limit).filled(:integer).description("Maximum versions to return (1-200)")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param name [String] gem name
|
|
17
|
+
# @param include_prerelease [Boolean] whether to include pre-release versions
|
|
18
|
+
# @param limit [Integer] maximum versions to return
|
|
19
|
+
# @return [Hash] MCP response
|
|
20
|
+
def call(name:, include_prerelease: false, limit: 50)
|
|
21
|
+
result = with_errors do
|
|
22
|
+
versions = client.versions(name)
|
|
23
|
+
versions = versions.reject { |item| item["prerelease"] } unless include_prerelease
|
|
24
|
+
limited = versions.first(limit)
|
|
25
|
+
{ name: name, count: limited.length, versions: limited }
|
|
26
|
+
end
|
|
27
|
+
render(result)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemCP
|
|
4
|
+
module Tools
|
|
5
|
+
# Find gems that depend on a given gem (reverse dependencies).
|
|
6
|
+
class ReverseDependencies < Base
|
|
7
|
+
tool_name "reverse_dependencies"
|
|
8
|
+
description \
|
|
9
|
+
"List gems that declare a runtime dependency on this gem. " \
|
|
10
|
+
"Useful for understanding a gem's ecosystem impact and finding potential conflicts."
|
|
11
|
+
|
|
12
|
+
arguments do
|
|
13
|
+
required(:name).filled(:string).description("Gem name")
|
|
14
|
+
optional(:limit).filled(:integer).description("Maximum results to return (1-100)")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param name [String] gem name
|
|
18
|
+
# @param limit [Integer] maximum results
|
|
19
|
+
# @return [Hash] MCP response
|
|
20
|
+
def call(name:, limit: 50)
|
|
21
|
+
result = with_errors do
|
|
22
|
+
dependents = client.reverse_dependencies(name)
|
|
23
|
+
limited = Array(dependents).first(limit)
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
name: name,
|
|
27
|
+
total_dependents: Array(dependents).length,
|
|
28
|
+
count: limited.length,
|
|
29
|
+
dependents: limited
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
render(result)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|