yardmcp 0.2.1 → 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 +553 -151
- 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,21 +3,37 @@
|
|
|
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'
|
|
9
10
|
require_relative 'yardmcp/version'
|
|
10
11
|
|
|
11
12
|
# Utility class for YARD operations
|
|
12
|
-
class YardUtils
|
|
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,38 +47,51 @@ class YardUtils
|
|
|
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.
|
|
54
|
+
YARD::Registry.load!(dir)
|
|
41
55
|
@last_loaded_gem = gem_name
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
# Ensures the correct .yardoc is loaded for the given object path
|
|
45
59
|
def ensure_yardoc_loaded_for_object!(object_path)
|
|
46
|
-
|
|
47
|
-
gem_name
|
|
48
|
-
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
|
|
49
63
|
|
|
50
|
-
load_yardoc_for_gem(
|
|
64
|
+
load_yardoc_for_gem(gem_names.first)
|
|
51
65
|
end
|
|
52
66
|
|
|
53
67
|
# Lists all installed gems that have a .yardoc file available.
|
|
54
68
|
#
|
|
55
69
|
# @return [Array<String>] An array of gem names with .yardoc files.
|
|
56
70
|
def list_gems
|
|
71
|
+
libraries.keys.select do |name|
|
|
72
|
+
yardoc_exists?(yardoc_path_for(gem_spec!(name)))
|
|
73
|
+
end.sort
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def list_installed_gems
|
|
57
77
|
libraries.keys.sort
|
|
58
78
|
end
|
|
59
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
|
+
|
|
60
89
|
# Lists all classes and modules in the loaded YARD registry.
|
|
61
90
|
#
|
|
62
91
|
# @return [Array<String>] An array of fully qualified class/module paths.
|
|
63
92
|
def list_classes(gem_name)
|
|
64
93
|
load_yardoc_for_gem(gem_name)
|
|
65
|
-
YARD::Registry.all(:class, :module).map(&:path).sort
|
|
94
|
+
@class_cache[gem_name] ||= YARD::Registry.all(:class, :module).map(&:path).sort
|
|
66
95
|
end
|
|
67
96
|
|
|
68
97
|
# Fetches documentation and metadata for a YARD object (class/module/method).
|
|
@@ -71,14 +100,7 @@ class YardUtils
|
|
|
71
100
|
# @return [Hash] A hash containing type, name, namespace, visibility, docstring, parameters, return, and source.
|
|
72
101
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
73
102
|
def get_doc(path, gem_name = nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
|
|
74
|
-
|
|
75
|
-
# Load the specific gem's yardoc
|
|
76
|
-
load_yardoc_for_gem(gem_name)
|
|
77
|
-
else
|
|
78
|
-
ensure_yardoc_loaded_for_object!(path)
|
|
79
|
-
end
|
|
80
|
-
obj = YARD::Registry.at(path)
|
|
81
|
-
raise 'Object not found' unless obj
|
|
103
|
+
obj = object_for!(path, gem_name)
|
|
82
104
|
|
|
83
105
|
tags = obj.tags.map do |tag|
|
|
84
106
|
{
|
|
@@ -102,7 +124,7 @@ class YardUtils
|
|
|
102
124
|
text: obj.tag('return').text
|
|
103
125
|
}
|
|
104
126
|
end,
|
|
105
|
-
source: obj.respond_to?(:source) ? obj.source : nil,
|
|
127
|
+
source: capped_source(obj.respond_to?(:source) ? obj.source : nil),
|
|
106
128
|
tags:
|
|
107
129
|
}
|
|
108
130
|
|
|
@@ -121,13 +143,8 @@ class YardUtils
|
|
|
121
143
|
# @param path [String] The YARD path of the namespace.
|
|
122
144
|
# @return [Array<String>] An array of child object paths.
|
|
123
145
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
124
|
-
def children(path)
|
|
125
|
-
|
|
126
|
-
obj = YARD::Registry.at(path)
|
|
127
|
-
unless obj
|
|
128
|
-
logger.error "Object not found: #{path}"
|
|
129
|
-
return []
|
|
130
|
-
end
|
|
146
|
+
def children(path, gem_name = nil)
|
|
147
|
+
obj = object_for!(path, gem_name)
|
|
131
148
|
obj.respond_to?(:children) ? obj.children.map(&:path) : []
|
|
132
149
|
end
|
|
133
150
|
|
|
@@ -136,13 +153,8 @@ class YardUtils
|
|
|
136
153
|
# @param path [String] The YARD path of the class/module.
|
|
137
154
|
# @return [Array<String>] An array of method paths.
|
|
138
155
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
139
|
-
def methods_list(path)
|
|
140
|
-
|
|
141
|
-
obj = YARD::Registry.at(path)
|
|
142
|
-
unless obj
|
|
143
|
-
logger.error "Object not found: #{path}"
|
|
144
|
-
return []
|
|
145
|
-
end
|
|
156
|
+
def methods_list(path, gem_name = nil)
|
|
157
|
+
obj = object_for!(path, gem_name)
|
|
146
158
|
obj.respond_to?(:meths) ? obj.meths.map(&:path) : []
|
|
147
159
|
end
|
|
148
160
|
|
|
@@ -151,16 +163,11 @@ class YardUtils
|
|
|
151
163
|
# @param path [String] The YARD path of the class/module.
|
|
152
164
|
# @return [Hash] A hash with :superclass (String or nil), :included_modules (Array<String>), and :mixins (Array<String>).
|
|
153
165
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
154
|
-
def hierarchy(path
|
|
155
|
-
|
|
156
|
-
obj = YARD::Registry.at(path)
|
|
157
|
-
unless obj
|
|
158
|
-
logger.error "Object not found: #{path}"
|
|
159
|
-
return []
|
|
160
|
-
end
|
|
166
|
+
def hierarchy(path, gem_name = nil)
|
|
167
|
+
obj = object_for!(path, gem_name)
|
|
161
168
|
{
|
|
162
169
|
superclass: obj.respond_to?(:superclass) && obj.superclass ? obj.superclass.path : nil,
|
|
163
|
-
included_modules: obj.respond_to?(:
|
|
170
|
+
included_modules: obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : [],
|
|
164
171
|
mixins: obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : []
|
|
165
172
|
}
|
|
166
173
|
end
|
|
@@ -169,13 +176,8 @@ class YardUtils
|
|
|
169
176
|
#
|
|
170
177
|
# @param path [String] The YARD path of the class/module.
|
|
171
178
|
# @return [Array<String>] An array of ancestor paths.
|
|
172
|
-
def ancestors(path)
|
|
173
|
-
|
|
174
|
-
obj = YARD::Registry.at(path)
|
|
175
|
-
unless obj
|
|
176
|
-
logger.error "Object not found: #{path}"
|
|
177
|
-
return []
|
|
178
|
-
end
|
|
179
|
+
def ancestors(path, gem_name = nil)
|
|
180
|
+
obj = object_for!(path, gem_name)
|
|
179
181
|
obj.respond_to?(:inheritance_tree) ? obj.inheritance_tree(true).map(&:path) : []
|
|
180
182
|
end
|
|
181
183
|
|
|
@@ -183,17 +185,13 @@ class YardUtils
|
|
|
183
185
|
#
|
|
184
186
|
# @param path [String] The YARD path of the class/module.
|
|
185
187
|
# @return [Hash] A hash with :included_modules, :mixins, :subclasses.
|
|
186
|
-
def related_objects(path
|
|
187
|
-
|
|
188
|
-
obj = YARD::Registry.at(path)
|
|
189
|
-
unless obj
|
|
190
|
-
logger.error "Object not found: #{path}"
|
|
191
|
-
return {}
|
|
192
|
-
end
|
|
188
|
+
def related_objects(path, gem_name = nil)
|
|
189
|
+
obj = object_for!(path, gem_name)
|
|
193
190
|
subclasses = YARD::Registry.all(:class).select { |c| c.superclass && c.superclass.path == obj.path }.map(&:path)
|
|
191
|
+
mixins_list = obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : []
|
|
194
192
|
{
|
|
195
|
-
included_modules:
|
|
196
|
-
mixins:
|
|
193
|
+
included_modules: mixins_list,
|
|
194
|
+
mixins: mixins_list,
|
|
197
195
|
subclasses:
|
|
198
196
|
}
|
|
199
197
|
end
|
|
@@ -202,32 +200,12 @@ class YardUtils
|
|
|
202
200
|
#
|
|
203
201
|
# @param query [String] The search query string.
|
|
204
202
|
# @return [Array<Hash>] An array of hashes with :path and :score for matching object paths, ranked by relevance.
|
|
205
|
-
def search(query
|
|
203
|
+
def search(query, gem_name = nil, limit: 25, offset: 0)
|
|
206
204
|
require 'levenshtein' unless defined?(Levenshtein)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
path = obj.path.to_s
|
|
210
|
-
doc = obj.docstring.to_s
|
|
211
|
-
next if path.empty?
|
|
212
|
-
|
|
213
|
-
score = nil
|
|
214
|
-
if path == query
|
|
215
|
-
score = 100
|
|
216
|
-
elsif path.start_with?(query)
|
|
217
|
-
score = 90
|
|
218
|
-
elsif path.include?(query)
|
|
219
|
-
score = 80
|
|
220
|
-
elsif doc.include?(query)
|
|
221
|
-
score = 60
|
|
222
|
-
else
|
|
223
|
-
# Fuzzy match: allow up to 2 edits for short queries, 3 for longer
|
|
224
|
-
dist = Levenshtein.distance(path.downcase, query.downcase)
|
|
225
|
-
score = 70 - dist if dist <= [2, query.length / 3].max
|
|
226
|
-
end
|
|
227
|
-
results << { path:, score: } if score
|
|
228
|
-
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) }
|
|
229
207
|
# Sort by score descending, then alphabetically
|
|
230
|
-
results.sort_by { |r| [-r[:score], r[:path]] }
|
|
208
|
+
results.sort_by { |r| [-r[:score], r[:path]] }.slice(offset, limit) || []
|
|
231
209
|
end
|
|
232
210
|
|
|
233
211
|
# Returns the source file and line number for a YARD object (class/module/method).
|
|
@@ -235,13 +213,8 @@ class YardUtils
|
|
|
235
213
|
# @param path [String] The YARD path (e.g., 'String#upcase').
|
|
236
214
|
# @return [Hash] A hash with :file (String or nil) and :line (Integer or nil).
|
|
237
215
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
238
|
-
def source_location(path)
|
|
239
|
-
|
|
240
|
-
obj = YARD::Registry.at(path)
|
|
241
|
-
unless obj
|
|
242
|
-
logger.error "Object not found: #{path}"
|
|
243
|
-
return []
|
|
244
|
-
end
|
|
216
|
+
def source_location(path, gem_name = nil)
|
|
217
|
+
obj = object_for!(path, gem_name)
|
|
245
218
|
{
|
|
246
219
|
file: obj.respond_to?(:file) ? obj.file : nil,
|
|
247
220
|
line: obj.respond_to?(:line) ? obj.line : nil
|
|
@@ -253,22 +226,85 @@ class YardUtils
|
|
|
253
226
|
# @param path [String] The YARD path (e.g., 'String#upcase').
|
|
254
227
|
# @return [String, nil] The code snippet if available, otherwise nil.
|
|
255
228
|
# @raise [RuntimeError] if the object is not found in the registry.
|
|
256
|
-
def code_snippet(path)
|
|
257
|
-
|
|
258
|
-
obj
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
264
243
|
end
|
|
265
244
|
|
|
266
245
|
private
|
|
267
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
|
+
|
|
268
258
|
def yardoc_exists?(dir)
|
|
269
259
|
dir && File.directory?(dir)
|
|
270
260
|
end
|
|
271
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
|
+
|
|
272
308
|
# Build an index mapping object paths to gem names
|
|
273
309
|
def build_index # rubocop:disable Metrics/AbcSize
|
|
274
310
|
logger.info 'Building index...'
|
|
@@ -296,7 +332,7 @@ class YardUtils
|
|
|
296
332
|
def merge_gem_results(results)
|
|
297
333
|
results.each do |gem_objects|
|
|
298
334
|
gem_objects.each do |obj_path, gem_names|
|
|
299
|
-
(@object_to_gem[obj_path] ||= []).concat(gem_names)
|
|
335
|
+
(@object_to_gem[obj_path] ||= []).concat(gem_names).uniq!
|
|
300
336
|
end
|
|
301
337
|
end
|
|
302
338
|
end
|
|
@@ -311,7 +347,10 @@ class YardUtils
|
|
|
311
347
|
return {}
|
|
312
348
|
end
|
|
313
349
|
|
|
314
|
-
|
|
350
|
+
collect_current_gem_objects(gem_name)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def collect_current_gem_objects(gem_name)
|
|
315
354
|
gem_objects = {}
|
|
316
355
|
YARD::Registry.all.each do |obj|
|
|
317
356
|
logger.debug "Adding #{obj.path} to #{gem_name}"
|
|
@@ -319,148 +358,496 @@ class YardUtils
|
|
|
319
358
|
end
|
|
320
359
|
gem_objects
|
|
321
360
|
end
|
|
361
|
+
end
|
|
322
362
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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}"
|
|
326
640
|
end
|
|
327
641
|
end
|
|
328
642
|
|
|
329
643
|
# Tool: List all gems with .yardoc files
|
|
330
|
-
class ListGemsTool <
|
|
644
|
+
class ListGemsTool < YardTool
|
|
331
645
|
description 'List all installed gems that have a .yardoc file'
|
|
646
|
+
annotations(title: 'List all installed gems', read_only_hint: true)
|
|
647
|
+
returns YardSchemas::LIST_GEMS_SCHEMA
|
|
332
648
|
|
|
333
649
|
def call
|
|
334
650
|
gems = YardUtils.instance.list_gems
|
|
335
|
-
{
|
|
651
|
+
ok({ gems: }, text: gems.join("\n"))
|
|
336
652
|
end
|
|
337
653
|
end
|
|
338
654
|
|
|
339
655
|
# Tool: List all classes and modules in the loaded YARD registry
|
|
340
|
-
class ListClassesTool <
|
|
656
|
+
class ListClassesTool < YardTool
|
|
341
657
|
description 'List all classes and modules in the loaded YARD registry'
|
|
658
|
+
annotations(title: 'List all classes and modules', read_only_hint: true)
|
|
659
|
+
returns YardSchemas::LIST_CLASSES_SCHEMA
|
|
342
660
|
arguments do
|
|
343
661
|
required(:gem_name).filled(:string).description('Name of the gem to list classes for')
|
|
344
662
|
end
|
|
345
663
|
|
|
346
664
|
def call(gem_name:)
|
|
347
|
-
|
|
348
|
-
|
|
665
|
+
with_yard_errors do
|
|
666
|
+
classes = YardUtils.instance.list_classes(gem_name)
|
|
667
|
+
ok({ gem_name:, classes: }, text: classes.join("\n"))
|
|
668
|
+
end
|
|
349
669
|
end
|
|
350
670
|
end
|
|
351
671
|
|
|
352
672
|
# Tool: Fetch documentation for a YARD object
|
|
353
|
-
class GetDocTool <
|
|
673
|
+
class GetDocTool < YardTool
|
|
354
674
|
description 'Fetch documentation and metadata for a class/module/method from YARD'
|
|
675
|
+
annotations(title: 'Fetch documentation', read_only_hint: true)
|
|
676
|
+
returns YardSchemas::DOC_OBJECT_SCHEMA
|
|
355
677
|
arguments do
|
|
356
678
|
required(:path).filled(:string).description("YARD path (e.g. 'String#upcase')")
|
|
357
679
|
optional(:gem_name).filled(:string).description("Optional gem name to load specific gem's documentation")
|
|
358
680
|
end
|
|
359
681
|
|
|
360
682
|
def call(path:, gem_name: nil)
|
|
361
|
-
|
|
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
|
|
362
687
|
end
|
|
363
688
|
end
|
|
364
689
|
|
|
365
690
|
# Tool: List children under a namespace
|
|
366
|
-
class ChildrenTool <
|
|
691
|
+
class ChildrenTool < YardTool
|
|
367
692
|
description 'List children under a namespace (class/module) in YARD'
|
|
693
|
+
annotations(title: 'List children under a namespace', read_only_hint: true)
|
|
694
|
+
returns YardSchemas::CHILDREN_SCHEMA
|
|
368
695
|
arguments do
|
|
369
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")
|
|
370
698
|
end
|
|
371
699
|
|
|
372
|
-
def call(path:)
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
375
705
|
end
|
|
376
706
|
end
|
|
377
707
|
|
|
378
708
|
# Tool: List methods for a class/module
|
|
379
|
-
class MethodsListTool <
|
|
709
|
+
class MethodsListTool < YardTool
|
|
380
710
|
description 'List methods for a class/module in YARD'
|
|
711
|
+
annotations(title: 'List methods for a class/module', read_only_hint: true)
|
|
712
|
+
returns YardSchemas::METHODS_SCHEMA
|
|
381
713
|
arguments do
|
|
382
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")
|
|
383
716
|
end
|
|
384
717
|
|
|
385
|
-
def call(path:)
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
388
723
|
end
|
|
389
724
|
end
|
|
390
725
|
|
|
391
726
|
# Tool: Return inheritance and inclusion info
|
|
392
|
-
class HierarchyTool <
|
|
727
|
+
class HierarchyTool < YardTool
|
|
393
728
|
description 'Return inheritance and inclusion info for a class/module in YARD'
|
|
729
|
+
annotations(title: 'Return inheritance and inclusion info', read_only_hint: true)
|
|
730
|
+
returns YardSchemas::HIERARCHY_SCHEMA
|
|
394
731
|
arguments do
|
|
395
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")
|
|
396
734
|
end
|
|
397
735
|
|
|
398
|
-
def call(path:)
|
|
399
|
-
|
|
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
|
|
400
741
|
end
|
|
401
742
|
end
|
|
402
743
|
|
|
403
744
|
# Tool: Perform fuzzy/full-text search
|
|
404
|
-
class SearchTool <
|
|
745
|
+
class SearchTool < YardTool
|
|
405
746
|
description 'Perform fuzzy/full-text search in YARD registry'
|
|
747
|
+
annotations(title: 'Perform fuzzy/full-text search', read_only_hint: true)
|
|
748
|
+
returns YardSchemas::SEARCH_SCHEMA
|
|
406
749
|
arguments do
|
|
407
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')
|
|
408
754
|
end
|
|
409
755
|
|
|
410
|
-
def call(query:)
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
414
761
|
end
|
|
415
762
|
end
|
|
416
763
|
|
|
417
764
|
# Tool: Fetch source file and line number for a YARD object
|
|
418
|
-
class SourceLocationTool <
|
|
765
|
+
class SourceLocationTool < YardTool
|
|
419
766
|
description 'Fetch the source file and line number for a class/module/method from YARD'
|
|
767
|
+
annotations(title: 'Fetch the source file and line number', read_only_hint: true)
|
|
768
|
+
returns YardSchemas::SOURCE_LOCATION_SCHEMA
|
|
420
769
|
arguments do
|
|
421
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")
|
|
422
772
|
end
|
|
423
773
|
|
|
424
|
-
def call(path:)
|
|
425
|
-
|
|
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
|
|
426
779
|
end
|
|
427
780
|
end
|
|
428
781
|
|
|
429
782
|
# Tool: Fetch code snippet for a YARD object from installed gems
|
|
430
|
-
class CodeSnippetTool <
|
|
783
|
+
class CodeSnippetTool < YardTool
|
|
431
784
|
description 'Fetch the code snippet for a class/module/method from installed gems using YARD'
|
|
785
|
+
annotations(title: 'Fetch the code snippet', read_only_hint: true)
|
|
786
|
+
returns YardSchemas::CODE_SNIPPET_SCHEMA
|
|
432
787
|
arguments do
|
|
433
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')
|
|
434
791
|
end
|
|
435
792
|
|
|
436
|
-
def call(path:)
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
439
798
|
end
|
|
440
799
|
end
|
|
441
800
|
|
|
442
801
|
# Tool: Fetch the full ancestor chain (superclasses and included modules) for a class/module in YARD
|
|
443
|
-
class AncestorsTool <
|
|
802
|
+
class AncestorsTool < YardTool
|
|
444
803
|
description 'Fetch the full ancestor chain (superclasses and included modules) for a class/module in YARD'
|
|
804
|
+
annotations(title: 'Fetch the full ancestor chain', read_only_hint: true)
|
|
805
|
+
returns YardSchemas::ANCESTORS_SCHEMA
|
|
445
806
|
arguments do
|
|
446
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")
|
|
447
809
|
end
|
|
448
810
|
|
|
449
|
-
def call(path:)
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|
452
816
|
end
|
|
453
817
|
end
|
|
454
818
|
|
|
455
819
|
# Tool: List related objects: included modules, mixins, and subclasses for a class/module in YARD
|
|
456
|
-
class RelatedObjectsTool <
|
|
820
|
+
class RelatedObjectsTool < YardTool
|
|
457
821
|
description 'List related objects: included modules, mixins, and subclasses for a class/module in YARD'
|
|
822
|
+
annotations(title: 'List related objects', read_only_hint: true)
|
|
823
|
+
returns YardSchemas::RELATED_OBJECTS_SCHEMA
|
|
458
824
|
arguments do
|
|
459
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")
|
|
460
827
|
end
|
|
461
828
|
|
|
462
|
-
def call(path:)
|
|
463
|
-
|
|
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
|
|
464
851
|
end
|
|
465
852
|
end
|
|
466
853
|
|
|
@@ -468,19 +855,34 @@ module YardMCP
|
|
|
468
855
|
def self.start_server(preload: true)
|
|
469
856
|
YardUtils.instance if preload
|
|
470
857
|
server = FastMcp::Server.new(name: 'yard-mcp-server', version: YardMCP::VERSION)
|
|
471
|
-
server.
|
|
472
|
-
server.
|
|
473
|
-
server.
|
|
474
|
-
server
|
|
475
|
-
server
|
|
476
|
-
server.register_tool(HierarchyTool)
|
|
477
|
-
server.register_tool(SearchTool)
|
|
478
|
-
server.register_tool(SourceLocationTool)
|
|
479
|
-
server.register_tool(CodeSnippetTool)
|
|
480
|
-
server.register_tool(AncestorsTool)
|
|
481
|
-
server.register_tool(RelatedObjectsTool)
|
|
858
|
+
server.capabilities.clear
|
|
859
|
+
server.capabilities[:tools] = {}
|
|
860
|
+
server.capabilities[:resources] = {}
|
|
861
|
+
register_tools(server)
|
|
862
|
+
register_resources(server)
|
|
482
863
|
server.start
|
|
483
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
|
|
484
886
|
end
|
|
485
887
|
|
|
486
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: []
|