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.
@@ -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
@@ -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