yardmcp 0.2.2 → 0.3.0
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/README.md +36 -2
- data/lib/yardmcp/version.rb +1 -1
- data/lib/yardmcp.rb +536 -152
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f804ad6257e87c3c7a2e9ea805b7fb778f4f026006f5c231ab444906ad18c9a
|
|
4
|
+
data.tar.gz: 90177775b4c69504a1cb6a127137f9bee11b91748815f673a7bf85f0e23af9ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff475a131b82fa0a0292e0409828047089cb647d4ee0ff439420107e29970f468e5ba25394781536e31538fd21ad083548e8b83479af67f6e82bd1330a3ae719
|
|
7
|
+
data.tar.gz: 6b3e94a29e92f4057e327f5df3a42540b79f66977f48b981563f56bc7d66378e780932c1bbf246295ccb56bd3ba15baacc9169b27be570097d3b230e3a2caf41
|
data/README.md
CHANGED
|
@@ -16,6 +16,7 @@ This is useful for building documentation browsers, code assistants, or integrat
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- **List Gems:** See all installed gems with YARD documentation
|
|
19
|
+
- **Build Gem Docs:** Explicitly build a local YARD index for an installed gem
|
|
19
20
|
- **List Classes/Modules:** Explore all classes/modules in a gem
|
|
20
21
|
- **Fetch Documentation:** Get docstrings, tags, parameters, return types, and more for any class/module/method
|
|
21
22
|
- **List Children:** List constants, classes, modules, and methods under a namespace
|
|
@@ -26,6 +27,7 @@ This is useful for building documentation browsers, code assistants, or integrat
|
|
|
26
27
|
- **Search:** Fuzzy/full-text search across all documentation
|
|
27
28
|
- **Source Location:** Find the file and line number for any object
|
|
28
29
|
- **Code Snippet:** Fetch the source code for any object
|
|
30
|
+
- **MCP Resources:** Read YARD docs and source via `yard://` resource URIs
|
|
29
31
|
|
|
30
32
|
## Installation
|
|
31
33
|
|
|
@@ -55,10 +57,20 @@ Start the server:
|
|
|
55
57
|
yardmcp
|
|
56
58
|
```
|
|
57
59
|
|
|
60
|
+
Read-only query tools do not build missing YARD indexes. If a gem is installed
|
|
61
|
+
but has no local YARD index yet, run:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
yard gems <gemname>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
or call `BuildGemDocsTool` explicitly.
|
|
68
|
+
|
|
58
69
|
### Tool List
|
|
59
70
|
|
|
60
71
|
The following tools are available (use `tools/list` to discover):
|
|
61
72
|
- ListGemsTool
|
|
73
|
+
- BuildGemDocsTool
|
|
62
74
|
- ListClassesTool
|
|
63
75
|
- GetDocTool
|
|
64
76
|
- ChildrenTool
|
|
@@ -70,7 +82,29 @@ The following tools are available (use `tools/list` to discover):
|
|
|
70
82
|
- AncestorsTool
|
|
71
83
|
- RelatedObjectsTool
|
|
72
84
|
|
|
73
|
-
|
|
85
|
+
Tool results return standard MCP text content and machine-readable data in
|
|
86
|
+
`structuredContent`. Tool execution failures return `isError: true` without
|
|
87
|
+
local stack traces.
|
|
88
|
+
|
|
89
|
+
All tools publish `outputSchema` metadata in `tools/list`.
|
|
90
|
+
|
|
91
|
+
Object-oriented tool results include `resource_uris` when `gem_name` is supplied,
|
|
92
|
+
so clients can pivot from a tool result to the corresponding documentation or
|
|
93
|
+
source resource.
|
|
94
|
+
|
|
95
|
+
### Resources
|
|
96
|
+
|
|
97
|
+
The server exposes templated MCP resources:
|
|
98
|
+
|
|
99
|
+
- `yard://gem/{gem_name}/object/{+path}` returns JSON documentation for a YARD object.
|
|
100
|
+
- `yard://gem/{gem_name}/source/{+path}` returns source text for a YARD object.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
yard://gem/yard/object/YARD::Registry
|
|
106
|
+
yard://gem/yard/source/YARD::CodeObjects::Base#name
|
|
107
|
+
```
|
|
74
108
|
|
|
75
109
|
## Development
|
|
76
110
|
|
|
@@ -96,4 +130,4 @@ See the code in `lib/yardmcp.rb` for argument details and return formats.
|
|
|
96
130
|
|
|
97
131
|
## License
|
|
98
132
|
|
|
99
|
-
MIT License. See [LICENSE](LICENSE) for details.
|
|
133
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
data/lib/yardmcp/version.rb
CHANGED
data/lib/yardmcp.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require 'fast_mcp'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'logger'
|
|
6
7
|
require 'rubygems'
|
|
7
8
|
require 'yard'
|
|
8
9
|
require 'singleton'
|
|
@@ -12,12 +13,27 @@ require_relative 'yardmcp/version'
|
|
|
12
13
|
class YardUtils # rubocop:disable Metrics/ClassLength
|
|
13
14
|
include Singleton
|
|
14
15
|
|
|
16
|
+
MAX_SOURCE_CHARS = 20_000
|
|
17
|
+
|
|
18
|
+
class DocumentationError < StandardError; end
|
|
19
|
+
|
|
20
|
+
class AmbiguousObjectError < DocumentationError
|
|
21
|
+
attr_reader :path, :candidates
|
|
22
|
+
|
|
23
|
+
def initialize(path, candidates)
|
|
24
|
+
@path = path
|
|
25
|
+
@candidates = candidates
|
|
26
|
+
super("Multiple gems contain '#{path}'. Pass gem_name.")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
15
30
|
attr_reader :libraries, :logger, :object_to_gem
|
|
16
31
|
|
|
17
32
|
def initialize
|
|
18
33
|
@libraries = {}
|
|
19
34
|
@object_to_gem = {}
|
|
20
35
|
@last_loaded_gem = nil
|
|
36
|
+
@class_cache = {}
|
|
21
37
|
@logger = Logger.new($stderr)
|
|
22
38
|
@logger.level = Logger::INFO unless ENV['DEBUG']
|
|
23
39
|
build_index
|
|
@@ -31,24 +47,21 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
31
47
|
def load_yardoc_for_gem(gem_name)
|
|
32
48
|
return if @last_loaded_gem == gem_name
|
|
33
49
|
|
|
34
|
-
spec =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
build_docs(gem_name) unless yardoc_exists?(dir)
|
|
38
|
-
raise "Yardoc not found for #{gem_name}" unless yardoc_exists?(dir)
|
|
50
|
+
spec = gem_spec!(gem_name)
|
|
51
|
+
dir = yardoc_path_for(spec)
|
|
52
|
+
raise DocumentationError, "YARD documentation is not indexed for gem '#{gem_name}'" unless yardoc_exists?(dir)
|
|
39
53
|
|
|
40
|
-
YARD::Registry.
|
|
41
|
-
YARD::Registry.load_all
|
|
54
|
+
YARD::Registry.load!(dir)
|
|
42
55
|
@last_loaded_gem = gem_name
|
|
43
56
|
end
|
|
44
57
|
|
|
45
58
|
# Ensures the correct .yardoc is loaded for the given object path
|
|
46
59
|
def ensure_yardoc_loaded_for_object!(object_path)
|
|
47
|
-
|
|
48
|
-
gem_name
|
|
49
|
-
raise
|
|
60
|
+
gem_names = @object_to_gem[object_path]
|
|
61
|
+
raise DocumentationError, "No indexed documentation contains '#{object_path}'. Pass gem_name if you know the gem." if gem_names.nil? || gem_names.empty?
|
|
62
|
+
raise AmbiguousObjectError.new(object_path, gem_candidates(gem_names)) if gem_names.uniq.size > 1
|
|
50
63
|
|
|
51
|
-
load_yardoc_for_gem(
|
|
64
|
+
load_yardoc_for_gem(gem_names.first)
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
# Lists all installed gems that have a .yardoc file available.
|
|
@@ -56,19 +69,29 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
56
69
|
# @return [Array<String>] An array of gem names with .yardoc files.
|
|
57
70
|
def list_gems
|
|
58
71
|
libraries.keys.select do |name|
|
|
59
|
-
|
|
60
|
-
ver = "= #{lib.version}"
|
|
61
|
-
dir = YARD::Registry.yardoc_file_for_gem(name, ver)
|
|
62
|
-
dir && File.directory?(dir)
|
|
72
|
+
yardoc_exists?(yardoc_path_for(gem_spec!(name)))
|
|
63
73
|
end.sort
|
|
64
74
|
end
|
|
65
75
|
|
|
76
|
+
def list_installed_gems
|
|
77
|
+
libraries.keys.sort
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def gem_candidates(gem_names)
|
|
81
|
+
gem_names.uniq.sort.map do |gem_name|
|
|
82
|
+
{
|
|
83
|
+
gem_name:,
|
|
84
|
+
versions: Array(libraries[gem_name]).map { |library| library.version.to_s }.uniq.sort
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
66
89
|
# Lists all classes and modules in the loaded YARD registry.
|
|
67
90
|
#
|
|
68
91
|
# @return [Array<String>] An array of fully qualified class/module paths.
|
|
69
92
|
def list_classes(gem_name)
|
|
70
93
|
load_yardoc_for_gem(gem_name)
|
|
71
|
-
YARD::Registry.all(:class, :module).map(&:path).sort
|
|
94
|
+
@class_cache[gem_name] ||= YARD::Registry.all(:class, :module).map(&:path).sort
|
|
72
95
|
end
|
|
73
96
|
|
|
74
97
|
# Fetches documentation and metadata for a YARD object (class/module/method).
|
|
@@ -77,14 +100,7 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
77
100
|
# @return [Hash] A hash containing type, name, namespace, visibility, docstring, parameters, return, and source.
|
|
78
101
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
79
102
|
def get_doc(path, gem_name = nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
|
|
80
|
-
|
|
81
|
-
# Load the specific gem's yardoc
|
|
82
|
-
load_yardoc_for_gem(gem_name)
|
|
83
|
-
else
|
|
84
|
-
ensure_yardoc_loaded_for_object!(path)
|
|
85
|
-
end
|
|
86
|
-
obj = YARD::Registry.at(path)
|
|
87
|
-
raise 'Object not found' unless obj
|
|
103
|
+
obj = object_for!(path, gem_name)
|
|
88
104
|
|
|
89
105
|
tags = obj.tags.map do |tag|
|
|
90
106
|
{
|
|
@@ -108,7 +124,7 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
108
124
|
text: obj.tag('return').text
|
|
109
125
|
}
|
|
110
126
|
end,
|
|
111
|
-
source: obj.respond_to?(:source) ? obj.source : nil,
|
|
127
|
+
source: capped_source(obj.respond_to?(:source) ? obj.source : nil),
|
|
112
128
|
tags:
|
|
113
129
|
}
|
|
114
130
|
|
|
@@ -127,13 +143,8 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
127
143
|
# @param path [String] The YARD path of the namespace.
|
|
128
144
|
# @return [Array<String>] An array of child object paths.
|
|
129
145
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
130
|
-
def children(path)
|
|
131
|
-
|
|
132
|
-
obj = YARD::Registry.at(path)
|
|
133
|
-
unless obj
|
|
134
|
-
logger.error "Object not found: #{path}"
|
|
135
|
-
return []
|
|
136
|
-
end
|
|
146
|
+
def children(path, gem_name = nil)
|
|
147
|
+
obj = object_for!(path, gem_name)
|
|
137
148
|
obj.respond_to?(:children) ? obj.children.map(&:path) : []
|
|
138
149
|
end
|
|
139
150
|
|
|
@@ -142,13 +153,8 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
142
153
|
# @param path [String] The YARD path of the class/module.
|
|
143
154
|
# @return [Array<String>] An array of method paths.
|
|
144
155
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
145
|
-
def methods_list(path)
|
|
146
|
-
|
|
147
|
-
obj = YARD::Registry.at(path)
|
|
148
|
-
unless obj
|
|
149
|
-
logger.error "Object not found: #{path}"
|
|
150
|
-
return []
|
|
151
|
-
end
|
|
156
|
+
def methods_list(path, gem_name = nil)
|
|
157
|
+
obj = object_for!(path, gem_name)
|
|
152
158
|
obj.respond_to?(:meths) ? obj.meths.map(&:path) : []
|
|
153
159
|
end
|
|
154
160
|
|
|
@@ -157,13 +163,8 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
157
163
|
# @param path [String] The YARD path of the class/module.
|
|
158
164
|
# @return [Hash] A hash with :superclass (String or nil), :included_modules (Array<String>), and :mixins (Array<String>).
|
|
159
165
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
160
|
-
def hierarchy(path
|
|
161
|
-
|
|
162
|
-
obj = YARD::Registry.at(path)
|
|
163
|
-
unless obj
|
|
164
|
-
logger.error "Object not found: #{path}"
|
|
165
|
-
return []
|
|
166
|
-
end
|
|
166
|
+
def hierarchy(path, gem_name = nil)
|
|
167
|
+
obj = object_for!(path, gem_name)
|
|
167
168
|
{
|
|
168
169
|
superclass: obj.respond_to?(:superclass) && obj.superclass ? obj.superclass.path : nil,
|
|
169
170
|
included_modules: obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : [],
|
|
@@ -175,13 +176,8 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
175
176
|
#
|
|
176
177
|
# @param path [String] The YARD path of the class/module.
|
|
177
178
|
# @return [Array<String>] An array of ancestor paths.
|
|
178
|
-
def ancestors(path)
|
|
179
|
-
|
|
180
|
-
obj = YARD::Registry.at(path)
|
|
181
|
-
unless obj
|
|
182
|
-
logger.error "Object not found: #{path}"
|
|
183
|
-
return []
|
|
184
|
-
end
|
|
179
|
+
def ancestors(path, gem_name = nil)
|
|
180
|
+
obj = object_for!(path, gem_name)
|
|
185
181
|
obj.respond_to?(:inheritance_tree) ? obj.inheritance_tree(true).map(&:path) : []
|
|
186
182
|
end
|
|
187
183
|
|
|
@@ -189,13 +185,8 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
189
185
|
#
|
|
190
186
|
# @param path [String] The YARD path of the class/module.
|
|
191
187
|
# @return [Hash] A hash with :included_modules, :mixins, :subclasses.
|
|
192
|
-
def related_objects(path)
|
|
193
|
-
|
|
194
|
-
obj = YARD::Registry.at(path)
|
|
195
|
-
unless obj
|
|
196
|
-
logger.error "Object not found: #{path}"
|
|
197
|
-
return {}
|
|
198
|
-
end
|
|
188
|
+
def related_objects(path, gem_name = nil)
|
|
189
|
+
obj = object_for!(path, gem_name)
|
|
199
190
|
subclasses = YARD::Registry.all(:class).select { |c| c.superclass && c.superclass.path == obj.path }.map(&:path)
|
|
200
191
|
mixins_list = obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : []
|
|
201
192
|
{
|
|
@@ -209,32 +200,12 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
209
200
|
#
|
|
210
201
|
# @param query [String] The search query string.
|
|
211
202
|
# @return [Array<Hash>] An array of hashes with :path and :score for matching object paths, ranked by relevance.
|
|
212
|
-
def search(query
|
|
203
|
+
def search(query, gem_name = nil, limit: 25, offset: 0)
|
|
213
204
|
require 'levenshtein' unless defined?(Levenshtein)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
path = obj.path.to_s
|
|
217
|
-
doc = obj.docstring.to_s
|
|
218
|
-
next if path.empty?
|
|
219
|
-
|
|
220
|
-
score = nil
|
|
221
|
-
if path == query
|
|
222
|
-
score = 100
|
|
223
|
-
elsif path.start_with?(query)
|
|
224
|
-
score = 90
|
|
225
|
-
elsif path.include?(query)
|
|
226
|
-
score = 80
|
|
227
|
-
elsif doc.include?(query)
|
|
228
|
-
score = 60
|
|
229
|
-
else
|
|
230
|
-
# Fuzzy match: allow up to 2 edits for short queries, 3 for longer
|
|
231
|
-
dist = Levenshtein.distance(path.downcase, query.downcase)
|
|
232
|
-
score = 70 - dist if dist <= [2, query.length / 3].max
|
|
233
|
-
end
|
|
234
|
-
results << { path:, score: } if score
|
|
235
|
-
end
|
|
205
|
+
candidates = gem_name ? loaded_objects_for_search(gem_name) : indexed_paths_for_search
|
|
206
|
+
results = candidates.filter_map { |candidate| score_search_candidate(candidate, query) }
|
|
236
207
|
# Sort by score descending, then alphabetically
|
|
237
|
-
results.sort_by { |r| [-r[:score], r[:path]] }
|
|
208
|
+
results.sort_by { |r| [-r[:score], r[:path]] }.slice(offset, limit) || []
|
|
238
209
|
end
|
|
239
210
|
|
|
240
211
|
# Returns the source file and line number for a YARD object (class/module/method).
|
|
@@ -242,13 +213,8 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
242
213
|
# @param path [String] The YARD path (e.g., 'String#upcase').
|
|
243
214
|
# @return [Hash] A hash with :file (String or nil) and :line (Integer or nil).
|
|
244
215
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
245
|
-
def source_location(path)
|
|
246
|
-
|
|
247
|
-
obj = YARD::Registry.at(path)
|
|
248
|
-
unless obj
|
|
249
|
-
logger.error "Object not found: #{path}"
|
|
250
|
-
return []
|
|
251
|
-
end
|
|
216
|
+
def source_location(path, gem_name = nil)
|
|
217
|
+
obj = object_for!(path, gem_name)
|
|
252
218
|
{
|
|
253
219
|
file: obj.respond_to?(:file) ? obj.file : nil,
|
|
254
220
|
line: obj.respond_to?(:line) ? obj.line : nil
|
|
@@ -260,22 +226,85 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
260
226
|
# @param path [String] The YARD path (e.g., 'String#upcase').
|
|
261
227
|
# @return [String, nil] The code snippet if available, otherwise nil.
|
|
262
228
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
263
|
-
def code_snippet(path)
|
|
264
|
-
|
|
265
|
-
obj
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
229
|
+
def code_snippet(path, gem_name = nil, max_chars: MAX_SOURCE_CHARS)
|
|
230
|
+
obj = object_for!(path, gem_name)
|
|
231
|
+
capped_source(obj.respond_to?(:source) ? obj.source : nil, max_chars:)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_docs(gem_name)
|
|
235
|
+
gem_spec!(gem_name)
|
|
236
|
+
logger.info "Building docs for #{gem_name}..."
|
|
237
|
+
YARD::CLI::Gems.new.run(gem_name)
|
|
238
|
+
@class_cache.delete(gem_name)
|
|
239
|
+
@last_loaded_gem = nil
|
|
240
|
+
load_yardoc_for_gem(gem_name)
|
|
241
|
+
merge_gem_results([collect_current_gem_objects(gem_name)])
|
|
242
|
+
true
|
|
271
243
|
end
|
|
272
244
|
|
|
273
245
|
private
|
|
274
246
|
|
|
247
|
+
def gem_spec!(gem_name)
|
|
248
|
+
spec = libraries[gem_name]&.first
|
|
249
|
+
raise DocumentationError, "Gem '#{gem_name}' is not installed" unless spec
|
|
250
|
+
|
|
251
|
+
spec
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def yardoc_path_for(spec)
|
|
255
|
+
YARD::Registry.yardoc_file_for_gem(spec.name, "= #{spec.version}")
|
|
256
|
+
end
|
|
257
|
+
|
|
275
258
|
def yardoc_exists?(dir)
|
|
276
259
|
dir && File.directory?(dir)
|
|
277
260
|
end
|
|
278
261
|
|
|
262
|
+
def object_for!(path, gem_name = nil)
|
|
263
|
+
gem_name ? load_yardoc_for_gem(gem_name) : ensure_yardoc_loaded_for_object!(path)
|
|
264
|
+
obj = YARD::Registry.at(path)
|
|
265
|
+
raise DocumentationError, "Object '#{path}' was not found" unless obj
|
|
266
|
+
|
|
267
|
+
obj
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def capped_source(source, max_chars: MAX_SOURCE_CHARS)
|
|
271
|
+
return source unless source && source.length > max_chars
|
|
272
|
+
|
|
273
|
+
"#{source.byteslice(0, max_chars)}\n... truncated at #{max_chars} bytes"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def loaded_objects_for_search(gem_name)
|
|
277
|
+
load_yardoc_for_gem(gem_name)
|
|
278
|
+
YARD::Registry.all.map do |obj|
|
|
279
|
+
{ path: obj.path.to_s, docstring: obj.docstring.to_s }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def indexed_paths_for_search
|
|
284
|
+
@object_to_gem.keys.map { |path| { path:, docstring: '' } }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def score_search_candidate(candidate, query)
|
|
288
|
+
path = candidate[:path]
|
|
289
|
+
doc = candidate[:docstring]
|
|
290
|
+
return if path.empty?
|
|
291
|
+
|
|
292
|
+
score = search_score(path, doc, query)
|
|
293
|
+
{ path:, score: } if score
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def search_score(path, doc, query)
|
|
297
|
+
query_downcase = query.downcase
|
|
298
|
+
path_downcase = path.downcase
|
|
299
|
+
return 100 if path == query
|
|
300
|
+
return 90 if path_downcase.start_with?(query_downcase)
|
|
301
|
+
return 80 if path_downcase.include?(query_downcase)
|
|
302
|
+
return 60 if doc.downcase.include?(query_downcase)
|
|
303
|
+
|
|
304
|
+
distance = Levenshtein.distance(path_downcase, query_downcase)
|
|
305
|
+
distance <= [2, query.length / 3].max ? 70 - distance : nil
|
|
306
|
+
end
|
|
307
|
+
|
|
279
308
|
# Build an index mapping object paths to gem names
|
|
280
309
|
def build_index # rubocop:disable Metrics/AbcSize
|
|
281
310
|
logger.info 'Building index...'
|
|
@@ -303,7 +332,7 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
303
332
|
def merge_gem_results(results)
|
|
304
333
|
results.each do |gem_objects|
|
|
305
334
|
gem_objects.each do |obj_path, gem_names|
|
|
306
|
-
(@object_to_gem[obj_path] ||= []).concat(gem_names)
|
|
335
|
+
(@object_to_gem[obj_path] ||= []).concat(gem_names).uniq!
|
|
307
336
|
end
|
|
308
337
|
end
|
|
309
338
|
end
|
|
@@ -318,7 +347,10 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
318
347
|
return {}
|
|
319
348
|
end
|
|
320
349
|
|
|
321
|
-
|
|
350
|
+
collect_current_gem_objects(gem_name)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def collect_current_gem_objects(gem_name)
|
|
322
354
|
gem_objects = {}
|
|
323
355
|
YARD::Registry.all.each do |obj|
|
|
324
356
|
logger.debug "Adding #{obj.path} to #{gem_name}"
|
|
@@ -326,159 +358,496 @@ class YardUtils # rubocop:disable Metrics/ClassLength
|
|
|
326
358
|
end
|
|
327
359
|
gem_objects
|
|
328
360
|
end
|
|
361
|
+
end
|
|
329
362
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
363
|
+
module YardSchemas
|
|
364
|
+
RESOURCE_URIS_SCHEMA = {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
object: { type: 'string' },
|
|
368
|
+
source: { type: 'string' }
|
|
369
|
+
},
|
|
370
|
+
required: %w[object source]
|
|
371
|
+
}.freeze
|
|
372
|
+
|
|
373
|
+
def self.array_schema(name)
|
|
374
|
+
{
|
|
375
|
+
type: 'object',
|
|
376
|
+
properties: {
|
|
377
|
+
name => { type: 'array', items: { type: 'string' } }
|
|
378
|
+
},
|
|
379
|
+
required: [name.to_s]
|
|
380
|
+
}.freeze
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def self.path_array_schema(name)
|
|
384
|
+
{
|
|
385
|
+
type: 'object',
|
|
386
|
+
properties: {
|
|
387
|
+
path: { type: 'string' },
|
|
388
|
+
gem_name: { type: %w[string null] },
|
|
389
|
+
resource_uris: RESOURCE_URIS_SCHEMA,
|
|
390
|
+
name => { type: 'array', items: { type: 'string' } }
|
|
391
|
+
},
|
|
392
|
+
required: %w[path resource_uris] + [name.to_s]
|
|
393
|
+
}.freeze
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
LIST_GEMS_SCHEMA = array_schema(:gems)
|
|
397
|
+
|
|
398
|
+
LIST_CLASSES_SCHEMA = {
|
|
399
|
+
type: 'object',
|
|
400
|
+
properties: {
|
|
401
|
+
gem_name: { type: 'string' },
|
|
402
|
+
classes: { type: 'array', items: { type: 'string' } }
|
|
403
|
+
},
|
|
404
|
+
required: %w[gem_name classes]
|
|
405
|
+
}.freeze
|
|
406
|
+
|
|
407
|
+
DOC_OBJECT_SCHEMA = {
|
|
408
|
+
type: 'object',
|
|
409
|
+
properties: {
|
|
410
|
+
path: { type: 'string' },
|
|
411
|
+
gem_name: { type: %w[string null] },
|
|
412
|
+
resource_uris: RESOURCE_URIS_SCHEMA,
|
|
413
|
+
document: {
|
|
414
|
+
type: 'object',
|
|
415
|
+
properties: {
|
|
416
|
+
type: { type: 'string' },
|
|
417
|
+
name: { type: 'string' },
|
|
418
|
+
namespace: { type: %w[string null] },
|
|
419
|
+
visibility: { type: %w[string null] },
|
|
420
|
+
docstring: { type: 'string' },
|
|
421
|
+
parameters: { type: %w[array null] },
|
|
422
|
+
return: { type: %w[object null] },
|
|
423
|
+
source: { type: %w[string null] },
|
|
424
|
+
tags: { type: 'array' }
|
|
425
|
+
},
|
|
426
|
+
required: %w[type name docstring tags]
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
required: %w[resource_uris document]
|
|
430
|
+
}.freeze
|
|
431
|
+
|
|
432
|
+
CHILDREN_SCHEMA = path_array_schema(:children)
|
|
433
|
+
|
|
434
|
+
METHODS_SCHEMA = path_array_schema(:methods)
|
|
435
|
+
|
|
436
|
+
SOURCE_LOCATION_SCHEMA = {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {
|
|
439
|
+
path: { type: 'string' },
|
|
440
|
+
gem_name: { type: %w[string null] },
|
|
441
|
+
resource_uris: RESOURCE_URIS_SCHEMA,
|
|
442
|
+
source_location: {
|
|
443
|
+
type: 'object',
|
|
444
|
+
properties: {
|
|
445
|
+
file: { type: %w[string null] },
|
|
446
|
+
line: { type: %w[integer null] }
|
|
447
|
+
},
|
|
448
|
+
required: %w[file line]
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
required: %w[path resource_uris source_location]
|
|
452
|
+
}.freeze
|
|
453
|
+
|
|
454
|
+
HIERARCHY_SCHEMA = {
|
|
455
|
+
type: 'object',
|
|
456
|
+
properties: {
|
|
457
|
+
path: { type: 'string' },
|
|
458
|
+
gem_name: { type: %w[string null] },
|
|
459
|
+
resource_uris: RESOURCE_URIS_SCHEMA,
|
|
460
|
+
hierarchy: {
|
|
461
|
+
type: 'object',
|
|
462
|
+
properties: {
|
|
463
|
+
superclass: { type: %w[string null] },
|
|
464
|
+
included_modules: { type: 'array', items: { type: 'string' } },
|
|
465
|
+
mixins: { type: 'array', items: { type: 'string' } }
|
|
466
|
+
},
|
|
467
|
+
required: %w[superclass included_modules mixins]
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
required: %w[path resource_uris hierarchy]
|
|
471
|
+
}.freeze
|
|
472
|
+
|
|
473
|
+
SEARCH_SCHEMA = {
|
|
474
|
+
type: 'object',
|
|
475
|
+
properties: {
|
|
476
|
+
query: { type: 'string' },
|
|
477
|
+
gem_name: { type: %w[string null] },
|
|
478
|
+
limit: { type: 'integer' },
|
|
479
|
+
offset: { type: 'integer' },
|
|
480
|
+
results: {
|
|
481
|
+
type: 'array',
|
|
482
|
+
items: {
|
|
483
|
+
type: 'object',
|
|
484
|
+
properties: {
|
|
485
|
+
path: { type: 'string' },
|
|
486
|
+
score: { type: 'integer' }
|
|
487
|
+
},
|
|
488
|
+
required: %w[path score]
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
required: %w[query limit offset results]
|
|
493
|
+
}.freeze
|
|
494
|
+
|
|
495
|
+
CODE_SNIPPET_SCHEMA = {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
path: { type: 'string' },
|
|
499
|
+
gem_name: { type: %w[string null] },
|
|
500
|
+
resource_uris: RESOURCE_URIS_SCHEMA,
|
|
501
|
+
snippet: { type: 'string' }
|
|
502
|
+
},
|
|
503
|
+
required: %w[path resource_uris snippet]
|
|
504
|
+
}.freeze
|
|
505
|
+
|
|
506
|
+
ANCESTORS_SCHEMA = path_array_schema(:ancestors)
|
|
507
|
+
|
|
508
|
+
RELATED_OBJECTS_SCHEMA = {
|
|
509
|
+
type: 'object',
|
|
510
|
+
properties: {
|
|
511
|
+
path: { type: 'string' },
|
|
512
|
+
gem_name: { type: %w[string null] },
|
|
513
|
+
resource_uris: RESOURCE_URIS_SCHEMA,
|
|
514
|
+
related_objects: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
included_modules: { type: 'array', items: { type: 'string' } },
|
|
518
|
+
mixins: { type: 'array', items: { type: 'string' } },
|
|
519
|
+
subclasses: { type: 'array', items: { type: 'string' } }
|
|
520
|
+
},
|
|
521
|
+
required: %w[included_modules mixins subclasses]
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
required: %w[path resource_uris related_objects]
|
|
525
|
+
}.freeze
|
|
526
|
+
|
|
527
|
+
BUILD_GEM_DOCS_SCHEMA = {
|
|
528
|
+
type: 'object',
|
|
529
|
+
properties: {
|
|
530
|
+
gem_name: { type: 'string' },
|
|
531
|
+
indexed: { type: 'boolean' }
|
|
532
|
+
},
|
|
533
|
+
required: %w[gem_name indexed]
|
|
534
|
+
}.freeze
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
module YardMcpToolListOutputSchema
|
|
538
|
+
private
|
|
539
|
+
|
|
540
|
+
def handle_tools_list(id)
|
|
541
|
+
tools_list = @tools.values.map do |tool|
|
|
542
|
+
tool_info = {
|
|
543
|
+
name: tool.tool_name,
|
|
544
|
+
description: tool.description || '',
|
|
545
|
+
inputSchema: tool.input_schema_to_json || { type: 'object', properties: {}, required: [] }
|
|
546
|
+
}
|
|
547
|
+
tool_info[:outputSchema] = tool.output_schema if tool.respond_to?(:output_schema) && tool.output_schema
|
|
548
|
+
tool_info[:annotations] = camel_case_annotations(tool.annotations) unless tool.annotations.empty?
|
|
549
|
+
tool_info
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
send_result({ tools: tools_list }, id)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def camel_case_annotations(annotations)
|
|
556
|
+
annotations.to_h do |key, value|
|
|
557
|
+
camel_key = key.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }.to_sym
|
|
558
|
+
[camel_key, value]
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
FastMcp::Server.prepend(YardMcpToolListOutputSchema)
|
|
564
|
+
|
|
565
|
+
class YardTool < FastMcp::Tool
|
|
566
|
+
class << self
|
|
567
|
+
attr_reader :output_schema
|
|
568
|
+
|
|
569
|
+
def returns(schema)
|
|
570
|
+
@output_schema = schema
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
private
|
|
575
|
+
|
|
576
|
+
def ok(structured_content, text: nil)
|
|
577
|
+
{
|
|
578
|
+
content: [{ type: 'text', text: text || JSON.pretty_generate(structured_content) }],
|
|
579
|
+
structuredContent: structured_content,
|
|
580
|
+
isError: false
|
|
581
|
+
}
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def with_yard_errors
|
|
585
|
+
yield
|
|
586
|
+
rescue YardUtils::AmbiguousObjectError => e
|
|
587
|
+
{
|
|
588
|
+
content: [{ type: 'text', text: e.message }],
|
|
589
|
+
structuredContent: {
|
|
590
|
+
error: 'ambiguous_object',
|
|
591
|
+
path: e.path,
|
|
592
|
+
candidates: e.candidates
|
|
593
|
+
},
|
|
594
|
+
isError: true
|
|
595
|
+
}
|
|
596
|
+
rescue YardUtils::DocumentationError, ArgumentError => e
|
|
597
|
+
{
|
|
598
|
+
content: [{ type: 'text', text: e.message }],
|
|
599
|
+
structuredContent: {
|
|
600
|
+
error: 'documentation_error',
|
|
601
|
+
message: e.message
|
|
602
|
+
},
|
|
603
|
+
isError: true
|
|
604
|
+
}
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def resource_uris(gem_name, path)
|
|
608
|
+
return nil unless gem_name
|
|
609
|
+
|
|
610
|
+
{
|
|
611
|
+
object: "yard://gem/#{gem_name}/object/#{path}",
|
|
612
|
+
source: "yard://gem/#{gem_name}/source/#{path}"
|
|
613
|
+
}
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
class YardObjectResource < FastMcp::Resource
|
|
618
|
+
uri 'yard://gem/{gem_name}/object/{+path}'
|
|
619
|
+
resource_name 'YARD object documentation'
|
|
620
|
+
description 'Read structured YARD documentation for a gem object'
|
|
621
|
+
mime_type 'application/json'
|
|
622
|
+
|
|
623
|
+
def content
|
|
624
|
+
JSON.pretty_generate(document: YardUtils.instance.get_doc(params[:path], params[:gem_name]))
|
|
625
|
+
rescue YardUtils::DocumentationError, ArgumentError => e
|
|
626
|
+
JSON.pretty_generate(error: e.message)
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
class YardSourceResource < FastMcp::Resource
|
|
631
|
+
uri 'yard://gem/{gem_name}/source/{+path}'
|
|
632
|
+
resource_name 'YARD object source'
|
|
633
|
+
description 'Read source code for a documented YARD object'
|
|
634
|
+
mime_type 'text/plain'
|
|
635
|
+
|
|
636
|
+
def content
|
|
637
|
+
YardUtils.instance.code_snippet(params[:path], params[:gem_name]).to_s
|
|
638
|
+
rescue YardUtils::DocumentationError, ArgumentError => e
|
|
639
|
+
"Error: #{e.message}"
|
|
333
640
|
end
|
|
334
641
|
end
|
|
335
642
|
|
|
336
643
|
# Tool: List all gems with .yardoc files
|
|
337
|
-
class ListGemsTool <
|
|
644
|
+
class ListGemsTool < YardTool
|
|
338
645
|
description 'List all installed gems that have a .yardoc file'
|
|
339
646
|
annotations(title: 'List all installed gems', read_only_hint: true)
|
|
647
|
+
returns YardSchemas::LIST_GEMS_SCHEMA
|
|
340
648
|
|
|
341
649
|
def call
|
|
342
650
|
gems = YardUtils.instance.list_gems
|
|
343
|
-
{
|
|
651
|
+
ok({ gems: }, text: gems.join("\n"))
|
|
344
652
|
end
|
|
345
653
|
end
|
|
346
654
|
|
|
347
655
|
# Tool: List all classes and modules in the loaded YARD registry
|
|
348
|
-
class ListClassesTool <
|
|
656
|
+
class ListClassesTool < YardTool
|
|
349
657
|
description 'List all classes and modules in the loaded YARD registry'
|
|
350
658
|
annotations(title: 'List all classes and modules', read_only_hint: true)
|
|
659
|
+
returns YardSchemas::LIST_CLASSES_SCHEMA
|
|
351
660
|
arguments do
|
|
352
661
|
required(:gem_name).filled(:string).description('Name of the gem to list classes for')
|
|
353
662
|
end
|
|
354
663
|
|
|
355
664
|
def call(gem_name:)
|
|
356
|
-
|
|
357
|
-
|
|
665
|
+
with_yard_errors do
|
|
666
|
+
classes = YardUtils.instance.list_classes(gem_name)
|
|
667
|
+
ok({ gem_name:, classes: }, text: classes.join("\n"))
|
|
668
|
+
end
|
|
358
669
|
end
|
|
359
670
|
end
|
|
360
671
|
|
|
361
672
|
# Tool: Fetch documentation for a YARD object
|
|
362
|
-
class GetDocTool <
|
|
673
|
+
class GetDocTool < YardTool
|
|
363
674
|
description 'Fetch documentation and metadata for a class/module/method from YARD'
|
|
364
675
|
annotations(title: 'Fetch documentation', read_only_hint: true)
|
|
676
|
+
returns YardSchemas::DOC_OBJECT_SCHEMA
|
|
365
677
|
arguments do
|
|
366
678
|
required(:path).filled(:string).description("YARD path (e.g. 'String#upcase')")
|
|
367
679
|
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
368
680
|
end
|
|
369
681
|
|
|
370
682
|
def call(path:, gem_name: nil)
|
|
371
|
-
|
|
683
|
+
with_yard_errors do
|
|
684
|
+
doc = YardUtils.instance.get_doc(path, gem_name)
|
|
685
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), document: doc }, text: JSON.pretty_generate(doc))
|
|
686
|
+
end
|
|
372
687
|
end
|
|
373
688
|
end
|
|
374
689
|
|
|
375
690
|
# Tool: List children under a namespace
|
|
376
|
-
class ChildrenTool <
|
|
691
|
+
class ChildrenTool < YardTool
|
|
377
692
|
description 'List children under a namespace (class/module) in YARD'
|
|
378
693
|
annotations(title: 'List children under a namespace', read_only_hint: true)
|
|
694
|
+
returns YardSchemas::CHILDREN_SCHEMA
|
|
379
695
|
arguments do
|
|
380
696
|
required(:path).filled(:string).description('YARD path of the namespace')
|
|
697
|
+
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
381
698
|
end
|
|
382
699
|
|
|
383
|
-
def call(path:)
|
|
384
|
-
|
|
385
|
-
|
|
700
|
+
def call(path:, gem_name: nil)
|
|
701
|
+
with_yard_errors do
|
|
702
|
+
children = YardUtils.instance.children(path, gem_name)
|
|
703
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), children: }, text: children.join("\n"))
|
|
704
|
+
end
|
|
386
705
|
end
|
|
387
706
|
end
|
|
388
707
|
|
|
389
708
|
# Tool: List methods for a class/module
|
|
390
|
-
class MethodsListTool <
|
|
709
|
+
class MethodsListTool < YardTool
|
|
391
710
|
description 'List methods for a class/module in YARD'
|
|
392
711
|
annotations(title: 'List methods for a class/module', read_only_hint: true)
|
|
712
|
+
returns YardSchemas::METHODS_SCHEMA
|
|
393
713
|
arguments do
|
|
394
714
|
required(:path).filled(:string).description('YARD path of the class/module')
|
|
715
|
+
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
395
716
|
end
|
|
396
717
|
|
|
397
|
-
def call(path:)
|
|
398
|
-
|
|
399
|
-
|
|
718
|
+
def call(path:, gem_name: nil)
|
|
719
|
+
with_yard_errors do
|
|
720
|
+
methods = YardUtils.instance.methods_list(path, gem_name)
|
|
721
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), methods: }, text: methods.join("\n"))
|
|
722
|
+
end
|
|
400
723
|
end
|
|
401
724
|
end
|
|
402
725
|
|
|
403
726
|
# Tool: Return inheritance and inclusion info
|
|
404
|
-
class HierarchyTool <
|
|
727
|
+
class HierarchyTool < YardTool
|
|
405
728
|
description 'Return inheritance and inclusion info for a class/module in YARD'
|
|
406
729
|
annotations(title: 'Return inheritance and inclusion info', read_only_hint: true)
|
|
730
|
+
returns YardSchemas::HIERARCHY_SCHEMA
|
|
407
731
|
arguments do
|
|
408
732
|
required(:path).filled(:string).description('YARD path of the class/module')
|
|
733
|
+
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
409
734
|
end
|
|
410
735
|
|
|
411
|
-
def call(path:)
|
|
412
|
-
|
|
736
|
+
def call(path:, gem_name: nil)
|
|
737
|
+
with_yard_errors do
|
|
738
|
+
hierarchy = YardUtils.instance.hierarchy(path, gem_name)
|
|
739
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), hierarchy: })
|
|
740
|
+
end
|
|
413
741
|
end
|
|
414
742
|
end
|
|
415
743
|
|
|
416
744
|
# Tool: Perform fuzzy/full-text search
|
|
417
|
-
class SearchTool <
|
|
745
|
+
class SearchTool < YardTool
|
|
418
746
|
description 'Perform fuzzy/full-text search in YARD registry'
|
|
419
747
|
annotations(title: 'Perform fuzzy/full-text search', read_only_hint: true)
|
|
748
|
+
returns YardSchemas::SEARCH_SCHEMA
|
|
420
749
|
arguments do
|
|
421
750
|
required(:query).filled(:string).description('Search query')
|
|
751
|
+
optional(:gem_name).filled(:string).description('Optional gem name to search docstrings and paths within')
|
|
752
|
+
optional(:limit).filled(:integer, gt?: 0, lteq?: 100).description('Maximum number of results to return')
|
|
753
|
+
optional(:offset).filled(:integer, gteq?: 0).description('Number of results to skip')
|
|
422
754
|
end
|
|
423
755
|
|
|
424
|
-
def call(query:)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
756
|
+
def call(query:, gem_name: nil, limit: 25, offset: 0)
|
|
757
|
+
with_yard_errors do
|
|
758
|
+
results = YardUtils.instance.search(query, gem_name, limit:, offset:)
|
|
759
|
+
ok({ query:, gem_name:, limit:, offset:, results: }, text: results.map { |result| "#{result[:score]}\t#{result[:path]}" }.join("\n"))
|
|
760
|
+
end
|
|
428
761
|
end
|
|
429
762
|
end
|
|
430
763
|
|
|
431
764
|
# Tool: Fetch source file and line number for a YARD object
|
|
432
|
-
class SourceLocationTool <
|
|
765
|
+
class SourceLocationTool < YardTool
|
|
433
766
|
description 'Fetch the source file and line number for a class/module/method from YARD'
|
|
434
767
|
annotations(title: 'Fetch the source file and line number', read_only_hint: true)
|
|
768
|
+
returns YardSchemas::SOURCE_LOCATION_SCHEMA
|
|
435
769
|
arguments do
|
|
436
770
|
required(:path).filled(:string).description("YARD path (e.g. 'String#upcase')")
|
|
771
|
+
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
437
772
|
end
|
|
438
773
|
|
|
439
|
-
def call(path:)
|
|
440
|
-
|
|
774
|
+
def call(path:, gem_name: nil)
|
|
775
|
+
with_yard_errors do
|
|
776
|
+
location = YardUtils.instance.source_location(path, gem_name)
|
|
777
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), source_location: location })
|
|
778
|
+
end
|
|
441
779
|
end
|
|
442
780
|
end
|
|
443
781
|
|
|
444
782
|
# Tool: Fetch code snippet for a YARD object from installed gems
|
|
445
|
-
class CodeSnippetTool <
|
|
783
|
+
class CodeSnippetTool < YardTool
|
|
446
784
|
description 'Fetch the code snippet for a class/module/method from installed gems using YARD'
|
|
447
785
|
annotations(title: 'Fetch the code snippet', read_only_hint: true)
|
|
786
|
+
returns YardSchemas::CODE_SNIPPET_SCHEMA
|
|
448
787
|
arguments do
|
|
449
788
|
required(:path).filled(:string).description("YARD path (e.g. 'String#upcase')")
|
|
789
|
+
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
790
|
+
optional(:max_chars).filled(:integer, gt?: 0, lteq?: 100_000).description('Maximum number of source characters to return')
|
|
450
791
|
end
|
|
451
792
|
|
|
452
|
-
def call(path:)
|
|
453
|
-
|
|
454
|
-
|
|
793
|
+
def call(path:, gem_name: nil, max_chars: YardUtils::MAX_SOURCE_CHARS)
|
|
794
|
+
with_yard_errors do
|
|
795
|
+
snippet = YardUtils.instance.code_snippet(path, gem_name, max_chars:)
|
|
796
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), snippet: }, text: snippet.to_s)
|
|
797
|
+
end
|
|
455
798
|
end
|
|
456
799
|
end
|
|
457
800
|
|
|
458
801
|
# Tool: Fetch the full ancestor chain (superclasses and included modules) for a class/module in YARD
|
|
459
|
-
class AncestorsTool <
|
|
802
|
+
class AncestorsTool < YardTool
|
|
460
803
|
description 'Fetch the full ancestor chain (superclasses and included modules) for a class/module in YARD'
|
|
461
804
|
annotations(title: 'Fetch the full ancestor chain', read_only_hint: true)
|
|
805
|
+
returns YardSchemas::ANCESTORS_SCHEMA
|
|
462
806
|
arguments do
|
|
463
807
|
required(:path).filled(:string).description('YARD path of the class/module')
|
|
808
|
+
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
464
809
|
end
|
|
465
810
|
|
|
466
|
-
def call(path:)
|
|
467
|
-
|
|
468
|
-
|
|
811
|
+
def call(path:, gem_name: nil)
|
|
812
|
+
with_yard_errors do
|
|
813
|
+
ancestors = YardUtils.instance.ancestors(path, gem_name)
|
|
814
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), ancestors: }, text: ancestors.join("\n"))
|
|
815
|
+
end
|
|
469
816
|
end
|
|
470
817
|
end
|
|
471
818
|
|
|
472
819
|
# Tool: List related objects: included modules, mixins, and subclasses for a class/module in YARD
|
|
473
|
-
class RelatedObjectsTool <
|
|
820
|
+
class RelatedObjectsTool < YardTool
|
|
474
821
|
description 'List related objects: included modules, mixins, and subclasses for a class/module in YARD'
|
|
475
822
|
annotations(title: 'List related objects', read_only_hint: true)
|
|
823
|
+
returns YardSchemas::RELATED_OBJECTS_SCHEMA
|
|
476
824
|
arguments do
|
|
477
825
|
required(:path).filled(:string).description('YARD path of the class/module')
|
|
826
|
+
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
478
827
|
end
|
|
479
828
|
|
|
480
|
-
def call(path:)
|
|
481
|
-
|
|
829
|
+
def call(path:, gem_name: nil)
|
|
830
|
+
with_yard_errors do
|
|
831
|
+
related = YardUtils.instance.related_objects(path, gem_name)
|
|
832
|
+
ok({ path:, gem_name:, resource_uris: resource_uris(gem_name, path), related_objects: related })
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# Tool: Explicitly build YARD docs for an installed gem
|
|
838
|
+
class BuildGemDocsTool < YardTool
|
|
839
|
+
description 'Build the local YARD documentation index for an installed gem'
|
|
840
|
+
annotations(title: 'Build YARD docs for a gem', read_only_hint: false)
|
|
841
|
+
returns YardSchemas::BUILD_GEM_DOCS_SCHEMA
|
|
842
|
+
arguments do
|
|
843
|
+
required(:gem_name).filled(:string).description('Name of the installed gem to index')
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def call(gem_name:)
|
|
847
|
+
with_yard_errors do
|
|
848
|
+
YardUtils.instance.build_docs(gem_name)
|
|
849
|
+
ok({ gem_name:, indexed: true }, text: "Indexed #{gem_name}")
|
|
850
|
+
end
|
|
482
851
|
end
|
|
483
852
|
end
|
|
484
853
|
|
|
@@ -486,19 +855,34 @@ module YardMCP
|
|
|
486
855
|
def self.start_server(preload: true)
|
|
487
856
|
YardUtils.instance if preload
|
|
488
857
|
server = FastMcp::Server.new(name: 'yard-mcp-server', version: YardMCP::VERSION)
|
|
489
|
-
server.
|
|
490
|
-
server.
|
|
491
|
-
server.
|
|
492
|
-
server
|
|
493
|
-
server
|
|
494
|
-
server.register_tool(HierarchyTool)
|
|
495
|
-
server.register_tool(SearchTool)
|
|
496
|
-
server.register_tool(SourceLocationTool)
|
|
497
|
-
server.register_tool(CodeSnippetTool)
|
|
498
|
-
server.register_tool(AncestorsTool)
|
|
499
|
-
server.register_tool(RelatedObjectsTool)
|
|
858
|
+
server.capabilities.clear
|
|
859
|
+
server.capabilities[:tools] = {}
|
|
860
|
+
server.capabilities[:resources] = {}
|
|
861
|
+
register_tools(server)
|
|
862
|
+
register_resources(server)
|
|
500
863
|
server.start
|
|
501
864
|
end
|
|
865
|
+
|
|
866
|
+
def self.register_tools(server)
|
|
867
|
+
server.register_tools(
|
|
868
|
+
ListGemsTool,
|
|
869
|
+
ListClassesTool,
|
|
870
|
+
GetDocTool,
|
|
871
|
+
ChildrenTool,
|
|
872
|
+
MethodsListTool,
|
|
873
|
+
HierarchyTool,
|
|
874
|
+
SearchTool,
|
|
875
|
+
SourceLocationTool,
|
|
876
|
+
CodeSnippetTool,
|
|
877
|
+
AncestorsTool,
|
|
878
|
+
RelatedObjectsTool,
|
|
879
|
+
BuildGemDocsTool
|
|
880
|
+
)
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def self.register_resources(server)
|
|
884
|
+
server.register_resources(YardObjectResource, YardSourceResource)
|
|
885
|
+
end
|
|
502
886
|
end
|
|
503
887
|
|
|
504
888
|
YardMCP.start_server(preload: true) if __FILE__ == $PROGRAM_NAME
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yardmcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Shterenzon
|
|
@@ -82,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
82
82
|
- !ruby/object:Gem::Version
|
|
83
83
|
version: '0'
|
|
84
84
|
requirements: []
|
|
85
|
-
rubygems_version:
|
|
85
|
+
rubygems_version: 4.0.10
|
|
86
86
|
specification_version: 4
|
|
87
87
|
summary: Programmable server for Ruby gem/YARD documentation via FastMCP.
|
|
88
88
|
test_files: []
|