cf-mcp 0.20.0 → 0.21.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/CHANGELOG.md +24 -1
- data/Manifest.txt +3 -1
- data/README.md +15 -0
- data/Rakefile +28 -4
- data/exe/cf-mcp +1 -1
- data/lib/cf/mcp/cli.rb +128 -53
- data/lib/cf/mcp/downloader.rb +9 -5
- data/lib/cf/mcp/github_client.rb +2 -1
- data/lib/cf/mcp/index.rb +21 -18
- data/lib/cf/mcp/index_builder.rb +8 -4
- data/lib/cf/mcp/index_cache.rb +152 -0
- data/lib/cf/mcp/models/doc_item.rb +6 -13
- data/lib/cf/mcp/models/enum_doc.rb +3 -6
- data/lib/cf/mcp/models/function_doc.rb +15 -22
- data/lib/cf/mcp/models/struct_doc.rb +3 -6
- data/lib/cf/mcp/models/topic_doc.rb +3 -5
- data/lib/cf/mcp/parser.rb +29 -57
- data/lib/cf/mcp/server.rb +11 -16
- data/lib/cf/mcp/tool_command.rb +108 -0
- data/lib/cf/mcp/tools/category_enum.rb +31 -0
- data/lib/cf/mcp/tools/find_related.rb +6 -3
- data/lib/cf/mcp/tools/get_details.rb +4 -0
- data/lib/cf/mcp/tools/get_topic.rb +4 -0
- data/lib/cf/mcp/tools/list_category.rb +7 -1
- data/lib/cf/mcp/tools/list_topics.rb +4 -0
- data/lib/cf/mcp/tools/member_search.rb +14 -16
- data/lib/cf/mcp/tools/parameter_search.rb +23 -30
- data/lib/cf/mcp/tools/search_tool.rb +7 -1
- data/lib/cf/mcp/topic_parser.rb +30 -77
- data/lib/cf/mcp/version.rb +1 -1
- data/lib/cf/mcp.rb +9 -0
- metadata +4 -16
- data/sig/cf/mcp.rbs +0 -299
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74d1434cafd05f9f937dffdf7ae0f332ddb30816b9584bd20a5cf7b0a964914a
|
|
4
|
+
data.tar.gz: c513c509f8cc74e414312be4caf351ecb36b12c95ac23a98f43e4fa68721f29d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f037629edf72ea2a7a9b221d51a7108376e8136c7fa4f749fb10088c96a134b6ab1d62e77297da5167189ce940c22f5519d6303112a2c0d76b5b8b16ddf543c
|
|
7
|
+
data.tar.gz: 465e434412b383fa0f99d2210e86131cf320bb59f2dccd2d8781960b000a7a7f1a77155951ced1d191c5f5923527453f33f7c31d67d105a1e2de308c2ebeef52
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.21.0] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Every tool is now a command (`cf-mcp search sprite --type function`, `cf-mcp get_details CF_Sprite`, and so on) with arguments and `--help` generated from the tool's input schema
|
|
15
|
+
- `cf-mcp index` caches the parsed index in `~/.cache/cf-mcp` (`CF_MCP_CACHE_DIR` and `XDG_CACHE_HOME` are honoured), one cache per `--root` so projects on different Cute Framework versions do not evict each other; tool commands read it and rebuild it when a header or topic changes. A cache directory that belongs to another user or that everyone can write to is refused, since cache files are read back with `Marshal`
|
|
16
|
+
- RBS signatures for the whole library, checked in CI with Steep (strict, and every method must have a signature) and by running the test suite under RBS's runtime type checker; run them with `rake rbs`. They are development-only and not packaged in the gem.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `CLI#run` returns the exit status instead of exiting, and an unknown command or option now fails with a message (previously an unknown command printed the usage and exited 0)
|
|
21
|
+
- `search` and `list_category` list the index's categories when their schema is asked for instead of when the class loads, so tools can be loaded before the index is filled; the `category` property has no `enum` while the index is empty
|
|
22
|
+
- `name:` is now a required keyword on `DocItem` and its subclasses (`FunctionDoc`, `StructDoc`, `EnumDoc`, `TopicDoc`); previously a nameless item could be built and indexed
|
|
23
|
+
- `Index#by_type` now returns an `Index::ByType` (`functions`, `structs`, `enums`, `topics`) instead of a Hash keyed by type symbol, and `Index#add` files items by class rather than by their `type`
|
|
24
|
+
- Small internal refactors so the code type checks; no behavior change for valid input
|
|
25
|
+
|
|
26
|
+
### Removed
|
|
27
|
+
|
|
28
|
+
- Dropped the unused `listen` runtime dependency
|
|
29
|
+
|
|
10
30
|
## [0.20.0] - 2026-09-19
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -14,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
34
|
- `CF_MCP_ALLOWED_HOSTS` environment variable (comma-separated) to allow extra `Host` header values on the HTTP endpoint; set to `cf-mcp.fly.dev` in `fly.toml`
|
|
15
35
|
- Support MCP protocol version 2026-07-28: stateless requests carrying their version in `_meta`, and the `server/discover` method
|
|
16
36
|
- Allow the `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers in CORS preflight responses
|
|
37
|
+
- Server `description` in `serverInfo`
|
|
17
38
|
|
|
18
39
|
### Changed
|
|
19
40
|
|
|
@@ -21,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
42
|
- Updated `mcp` from 0.25.0 to 1.5.1
|
|
22
43
|
- Updated `puma` from 7.2.1 to 8.0.2 (the gemspec constraint is now `~> 8.0`) and `rubyzip` from 3.6.0 to 3.7.0
|
|
23
44
|
- Removed the `2025-03-26` protocol pin; clients using the `initialize` handshake now negotiate up to 2025-11-25
|
|
45
|
+
- Restored the server `title`, `website_url` and tool titles removed in 0.17.2 for that pin; the SDK still omits the server fields for clients that negotiate an older version
|
|
24
46
|
- The web UI now calls tools through the stateless 2026-07-28 protocol
|
|
25
47
|
- The web UI shows both the modern and the legacy handshake protocol versions
|
|
26
48
|
- Bump Ruby to 4.0.7 in `.ruby-version`, the Dockerfile, and the CI matrix
|
|
@@ -332,7 +354,8 @@ This version was prepared but never released. Changes rolled into 0.17.2.
|
|
|
332
354
|
- `cf_list_category` - List items by category
|
|
333
355
|
- `cf_get_details` - Get full documentation by name
|
|
334
356
|
|
|
335
|
-
[Unreleased]: https://github.com/pusewicz/cf-mcp/compare/v0.
|
|
357
|
+
[Unreleased]: https://github.com/pusewicz/cf-mcp/compare/v0.21.0...HEAD
|
|
358
|
+
[0.21.0]: https://github.com/pusewicz/cf-mcp/compare/v0.20.0...v0.21.0
|
|
336
359
|
[0.20.0]: https://github.com/pusewicz/cf-mcp/compare/v0.19.0...v0.20.0
|
|
337
360
|
[0.19.0]: https://github.com/pusewicz/cf-mcp/compare/v0.18.0...v0.19.0
|
|
338
361
|
[0.17.2]: https://github.com/pusewicz/cf-mcp/compare/v0.17.0...v0.17.2
|
data/Manifest.txt
CHANGED
|
@@ -12,6 +12,7 @@ lib/cf/mcp/downloader.rb
|
|
|
12
12
|
lib/cf/mcp/github_client.rb
|
|
13
13
|
lib/cf/mcp/index.rb
|
|
14
14
|
lib/cf/mcp/index_builder.rb
|
|
15
|
+
lib/cf/mcp/index_cache.rb
|
|
15
16
|
lib/cf/mcp/models/doc_item.rb
|
|
16
17
|
lib/cf/mcp/models/enum_doc.rb
|
|
17
18
|
lib/cf/mcp/models/function_doc.rb
|
|
@@ -29,6 +30,8 @@ lib/cf/mcp/server.rb
|
|
|
29
30
|
lib/cf/mcp/templates/index.erb
|
|
30
31
|
lib/cf/mcp/templates/script.js
|
|
31
32
|
lib/cf/mcp/templates/style.css
|
|
33
|
+
lib/cf/mcp/tool_command.rb
|
|
34
|
+
lib/cf/mcp/tools/category_enum.rb
|
|
32
35
|
lib/cf/mcp/tools/find_related.rb
|
|
33
36
|
lib/cf/mcp/tools/get_details.rb
|
|
34
37
|
lib/cf/mcp/tools/get_topic.rb
|
|
@@ -41,4 +44,3 @@ lib/cf/mcp/tools/search_result_formatter.rb
|
|
|
41
44
|
lib/cf/mcp/tools/search_tool.rb
|
|
42
45
|
lib/cf/mcp/topic_parser.rb
|
|
43
46
|
lib/cf/mcp/version.rb
|
|
44
|
-
sig/cf/mcp.rbs
|
data/README.md
CHANGED
|
@@ -35,6 +35,21 @@ cf-mcp stdio --root /path/to/cute_framework_project # STDIO mode
|
|
|
35
35
|
cf-mcp http --root /path/to/cute_framework_project # HTTP mode with web UI
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Command line
|
|
39
|
+
|
|
40
|
+
Every tool is also a command, so no MCP client is needed. Index the docs once, then look things up:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cf-mcp index --root /path/to/cute_framework # re-run after updating Cute Framework
|
|
44
|
+
cf-mcp search sprite --type function --limit 5
|
|
45
|
+
cf-mcp get_details CF_Sprite
|
|
46
|
+
cf-mcp list_category sprite
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The commands are `search`, `get_details`, `find_related`, `parameter_search`, `member_search`, `list_category`, `list_topics` and `get_topic`. Run `cf-mcp <command> --help` for a command's arguments.
|
|
50
|
+
|
|
51
|
+
The index is cached in `~/.cache/cf-mcp` (or `$XDG_CACHE_HOME/cf-mcp`, or `$CF_MCP_CACHE_DIR`) and rebuilt automatically when a header changes. Each `--root` keeps its own cache, so projects on different Cute Framework versions can take turns without reindexing: pass `--root` (or set `CF_HEADERS_PATH`) to say which one a command should use. Without either, a command uses the last one it was given. Cache files are read back with `Marshal`, so a cache directory that belongs to another user or that everyone can write to is refused.
|
|
52
|
+
|
|
38
53
|
## Inspecting with MCP Inspector
|
|
39
54
|
|
|
40
55
|
You can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to debug and test the server.
|
data/Rakefile
CHANGED
|
@@ -9,7 +9,7 @@ require "standard/rake"
|
|
|
9
9
|
|
|
10
10
|
desc "Generate Manifest.txt from git ls-files"
|
|
11
11
|
task :manifest do
|
|
12
|
-
ignore_patterns = %w[bin/ Gemfile .gitignore test/ .github/ .standard.yml cf-mcp.gemspec .ruby-version CLAUDE.md AGENTS.md fly.toml Procfile Dockerfile .dockerignore .claude/ .mcp.json .serena/]
|
|
12
|
+
ignore_patterns = %w[bin/ Gemfile .gitignore test/ .github/ .standard.yml cf-mcp.gemspec .ruby-version CLAUDE.md AGENTS.md fly.toml Procfile Dockerfile .dockerignore .claude/ .mcp.json .serena/ sig/ sig-stubs/ Steepfile rbs_collection.yaml rbs_collection.lock.yaml]
|
|
13
13
|
|
|
14
14
|
files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
|
15
15
|
ls.readlines("\x0", chomp: true)
|
|
@@ -23,11 +23,35 @@ task :manifest do
|
|
|
23
23
|
puts "Generated Manifest.txt with #{files.size} files"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
namespace :rbs do
|
|
27
|
+
desc "Validate RBS type signatures"
|
|
28
|
+
task :validate do
|
|
29
|
+
sh "rbs", "--collection", "rbs_collection.yaml", "-I", "sig", "-I", "sig-stubs", "validate"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "Type check lib/ against the RBS signatures with Steep"
|
|
33
|
+
task :steep do
|
|
34
|
+
sh "steep", "check"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Run the test suite under the RBS runtime type checker"
|
|
38
|
+
task :test do
|
|
39
|
+
env = {
|
|
40
|
+
"RBS_TEST_TARGET" => "CF::MCP::*",
|
|
41
|
+
"RBS_TEST_OPT" => "-I sig -I sig-stubs --collection rbs_collection.yaml",
|
|
42
|
+
"RBS_TEST_DOUBLE_SUITE" => "minitest",
|
|
43
|
+
"RBS_TEST_LOGLEVEL" => "warn",
|
|
44
|
+
# Append rather than replace: under `bundle exec`, RUBYOPT already carries
|
|
45
|
+
# -rbundler/setup, and dropping it would make the child resolve system gems.
|
|
46
|
+
"RUBYOPT" => "#{ENV["RUBYOPT"]} -rrbs/test/setup".strip
|
|
47
|
+
}
|
|
48
|
+
sh env, "bundle", "exec", "rake", "test"
|
|
49
|
+
end
|
|
29
50
|
end
|
|
30
51
|
|
|
52
|
+
desc "Validate signatures, type check with Steep, and run the tests under the runtime type checker"
|
|
53
|
+
task rbs: %w[rbs:validate rbs:steep rbs:test]
|
|
54
|
+
|
|
31
55
|
task default: %i[test standard rbs manifest]
|
|
32
56
|
|
|
33
57
|
desc "Deploy to Fly.io (runs tests and linting first)"
|
data/exe/cf-mcp
CHANGED
data/lib/cf/mcp/cli.rb
CHANGED
|
@@ -5,92 +5,117 @@ require "optparse"
|
|
|
5
5
|
module CF
|
|
6
6
|
module MCP
|
|
7
7
|
class CLI
|
|
8
|
+
USAGE = "Usage: cf-mcp [options] <command> [options]"
|
|
9
|
+
|
|
10
|
+
COMMANDS = {
|
|
11
|
+
"stdio" => "Run in STDIO mode (for CLI integration)",
|
|
12
|
+
"http" => "Run as HTTP server with web interface",
|
|
13
|
+
"index" => "Index the documentation into a cache for fast lookups"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
8
16
|
def initialize(args)
|
|
9
17
|
@args = args
|
|
10
|
-
@options =
|
|
18
|
+
@options = {
|
|
19
|
+
port: nil,
|
|
20
|
+
host: "0.0.0.0",
|
|
21
|
+
root: nil,
|
|
22
|
+
download: false,
|
|
23
|
+
help: false,
|
|
24
|
+
version: false
|
|
25
|
+
} #: options
|
|
26
|
+
@option_parser = build_option_parser
|
|
11
27
|
end
|
|
12
28
|
|
|
29
|
+
# Returns the process exit status.
|
|
13
30
|
def run
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
@option_parser.order!(@args)
|
|
32
|
+
command = @args.shift&.tr("-", "_")
|
|
33
|
+
tool = find_tool(command)
|
|
34
|
+
# A tool's flags are checked against the index, so the options that choose it come out first.
|
|
35
|
+
tool ? take_source_options : @option_parser.parse!(@args)
|
|
36
|
+
|
|
37
|
+
return print_version if @options[:version]
|
|
38
|
+
return print_usage if @options[:help] || [nil, "help"].include?(command)
|
|
39
|
+
return run_tool(tool) if tool
|
|
40
|
+
return fail_with("Unexpected arguments: #{@args.join(" ")}") unless @args.empty?
|
|
41
|
+
|
|
42
|
+
case command
|
|
43
|
+
when "stdio" then run_stdio
|
|
44
|
+
when "http" then run_http
|
|
45
|
+
when "index" then run_index
|
|
46
|
+
else fail_with("Unknown command '#{command}'. Use --help for usage information.")
|
|
24
47
|
end
|
|
48
|
+
rescue OptionParser::ParseError, Error => e
|
|
49
|
+
fail_with(e.message)
|
|
25
50
|
end
|
|
26
51
|
|
|
27
52
|
private
|
|
28
53
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
port: nil,
|
|
33
|
-
host: "0.0.0.0",
|
|
34
|
-
root: nil,
|
|
35
|
-
download: false
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
@option_parser = OptionParser.new do |opts|
|
|
39
|
-
opts.banner = "Usage: cf-mcp <command> [options]"
|
|
40
|
-
opts.separator ""
|
|
41
|
-
opts.separator "Commands:"
|
|
42
|
-
opts.separator " stdio Run in STDIO mode (for CLI integration)"
|
|
43
|
-
opts.separator " http Run as HTTP server with web interface"
|
|
54
|
+
def build_option_parser
|
|
55
|
+
OptionParser.new do |opts|
|
|
56
|
+
opts.banner = USAGE
|
|
44
57
|
opts.separator ""
|
|
45
58
|
opts.separator "Options:"
|
|
46
59
|
|
|
47
|
-
opts.on("-r", "--root PATH", "Path to Cute Framework
|
|
48
|
-
options[:root] = path
|
|
60
|
+
opts.on("-r", "--root PATH", "Path to a Cute Framework checkout, or its include directory") do |path|
|
|
61
|
+
@options[:root] = path
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
opts.on("-p", "--port PORT", Integer, "Port for HTTP server (default: 9292)") do |port|
|
|
52
|
-
options[:port] = port
|
|
65
|
+
@options[:port] = port
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
opts.on("-H", "--host HOST", "Host to bind to (default: 0.0.0.0)") do |host|
|
|
56
|
-
options[:host] = host
|
|
69
|
+
@options[:host] = host
|
|
57
70
|
end
|
|
58
71
|
|
|
59
72
|
opts.on("-d", "--download", "Download Cute Framework headers from GitHub") do
|
|
60
|
-
options[:download] = true
|
|
73
|
+
@options[:download] = true
|
|
61
74
|
end
|
|
62
75
|
|
|
63
76
|
opts.on("-h", "--help", "Show this help message") do
|
|
64
|
-
options[:
|
|
77
|
+
@options[:help] = true
|
|
65
78
|
end
|
|
66
79
|
|
|
67
80
|
opts.on("-v", "--version", "Show version") do
|
|
68
|
-
|
|
69
|
-
exit 0
|
|
81
|
+
@options[:version] = true
|
|
70
82
|
end
|
|
71
83
|
end
|
|
84
|
+
end
|
|
72
85
|
|
|
73
|
-
|
|
86
|
+
def print_version
|
|
87
|
+
puts "cf-mcp #{VERSION}"
|
|
88
|
+
0
|
|
89
|
+
end
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
# The commands go in the banner because the tools are only loaded when asked for.
|
|
92
|
+
def print_usage
|
|
93
|
+
@option_parser.banner = [
|
|
94
|
+
USAGE, "",
|
|
95
|
+
"Commands:", *command_lines(COMMANDS), "",
|
|
96
|
+
"Documentation commands (hyphens work too, as in get-details; see `cf-mcp <command> --help`):",
|
|
97
|
+
*command_lines(tool_descriptions)
|
|
98
|
+
].join("\n")
|
|
99
|
+
puts @option_parser
|
|
100
|
+
0
|
|
101
|
+
end
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
def command_lines(descriptions)
|
|
104
|
+
descriptions.map { |name, description| " #{name.ljust(18)}#{description}" }
|
|
83
105
|
end
|
|
84
106
|
|
|
85
|
-
def
|
|
86
|
-
|
|
107
|
+
def tool_descriptions
|
|
108
|
+
Tools.all.to_h { |tool| [tool.tool_name.to_s, tool.description.to_s] }
|
|
109
|
+
end
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
def fail_with(message)
|
|
112
|
+
warn "Error: #{message}"
|
|
113
|
+
1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def run_stdio
|
|
117
|
+
builder = IndexBuilder.new(root: @options[:root], download: @options[:download])
|
|
118
|
+
return fail_with(missing_headers_message(builder)) unless builder.valid?
|
|
94
119
|
|
|
95
120
|
warn "Parsing headers from: #{builder.headers_path}"
|
|
96
121
|
index = builder.build do |event, path, count|
|
|
@@ -99,11 +124,11 @@ module CF
|
|
|
99
124
|
warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
|
|
100
125
|
warn "Cute Framework revision: #{builder.revision}" if builder.revision
|
|
101
126
|
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
Server.new(index, revision: builder.revision).run_stdio
|
|
128
|
+
0
|
|
104
129
|
end
|
|
105
130
|
|
|
106
|
-
def
|
|
131
|
+
def run_http
|
|
107
132
|
require "rackup"
|
|
108
133
|
|
|
109
134
|
port = @options[:port] || 9292
|
|
@@ -118,6 +143,56 @@ module CF
|
|
|
118
143
|
warn "Web interface available at http://localhost:#{port}/"
|
|
119
144
|
warn "MCP endpoint available at http://localhost:#{port}/http"
|
|
120
145
|
Rackup::Server.start(app: app, Host: host, Port: port, Logger: $stderr)
|
|
146
|
+
0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def run_index
|
|
150
|
+
cache = IndexCache.new
|
|
151
|
+
stats = cache.refresh(index_source) { |source| IndexBuilder.new(**source) }.stats
|
|
152
|
+
|
|
153
|
+
puts "Indexed #{stats[:total]} items (#{stats[:functions]} functions, #{stats[:structs]} structs, #{stats[:enums]} enums, #{stats[:topics]} topics)"
|
|
154
|
+
puts "Cute Framework revision: #{cache.revision}" if cache.revision
|
|
155
|
+
puts "Cached at #{cache.path}"
|
|
156
|
+
0
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Takes --root and --download out of the arguments wherever they stand, leaving the rest for the tool.
|
|
160
|
+
def take_source_options
|
|
161
|
+
rest = [] #: Array[String]
|
|
162
|
+
while (arg = @args.shift)
|
|
163
|
+
case arg
|
|
164
|
+
when "-r", "--root" then @options[:root] = @args.shift || raise(OptionParser::MissingArgument, arg)
|
|
165
|
+
when /\A--root=(.*)\z/ then @options[:root] = $1
|
|
166
|
+
when "-d", "--download" then @options[:download] = true
|
|
167
|
+
else rest << arg
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
@args.replace(rest)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The tool named by the command, if it names one. Asking loads the tools, which the
|
|
174
|
+
# other commands have no need of.
|
|
175
|
+
def find_tool(command)
|
|
176
|
+
return if command.nil? || COMMANDS.key?(command)
|
|
177
|
+
|
|
178
|
+
Tools.all.find { |tool| tool.tool_name == command }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Help describes the tool from its schema alone, so it goes without the index.
|
|
182
|
+
def run_tool(tool)
|
|
183
|
+
IndexCache.new.index(index_source) { |source| IndexBuilder.new(**source) } unless @args.intersect?(%w[-h --help])
|
|
184
|
+
ToolCommand.new(tool).run(@args)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# What the user asked to index, by flag or CF_HEADERS_PATH; nil leaves the choice to the
|
|
188
|
+
# cache. The root is expanded so every spelling of it names the same index.
|
|
189
|
+
def index_source
|
|
190
|
+
root = @options[:root] || ENV["CF_HEADERS_PATH"]
|
|
191
|
+
{root: root && File.expand_path(root), download: @options[:download]} if root || @options[:download]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def missing_headers_message(builder)
|
|
195
|
+
"Headers directory not found: #{builder.headers_path}. Use --root to specify the path to Cute Framework headers, or --download to fetch them from GitHub."
|
|
121
196
|
end
|
|
122
197
|
end
|
|
123
198
|
end
|
data/lib/cf/mcp/downloader.rb
CHANGED
|
@@ -63,15 +63,19 @@ module CF
|
|
|
63
63
|
|
|
64
64
|
def download_zip(destination, url = CUTE_FRAMEWORK_ZIP_URL)
|
|
65
65
|
uri = URI.parse(url)
|
|
66
|
+
host = uri.host || raise(DownloadError, "No host in #{url}")
|
|
66
67
|
|
|
67
|
-
Net::HTTP.start(
|
|
68
|
+
Net::HTTP.start(host, uri.port, use_ssl: true) do |http|
|
|
68
69
|
request = Net::HTTP::Get.new(uri)
|
|
70
|
+
# @type var response: Net::HTTPResponse
|
|
69
71
|
response = http.request(request)
|
|
70
72
|
|
|
71
73
|
# Handle redirects (GitHub redirects to codeload.github.com)
|
|
72
74
|
if response.is_a?(Net::HTTPRedirection)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
location = response["location"] || raise(DownloadError, "Redirect from #{url} has no Location header")
|
|
76
|
+
redirect_uri = URI.parse(location)
|
|
77
|
+
redirect_host = redirect_uri.host || raise(DownloadError, "No host in redirect to #{location}")
|
|
78
|
+
Net::HTTP.start(redirect_host, redirect_uri.port, use_ssl: true) do |redirect_http|
|
|
75
79
|
redirect_request = Net::HTTP::Get.new(redirect_uri)
|
|
76
80
|
response = redirect_http.request(redirect_request)
|
|
77
81
|
end
|
|
@@ -92,12 +96,12 @@ module CF
|
|
|
92
96
|
Zip::File.open(zip_path) do |zip_file|
|
|
93
97
|
# The zip contains a top-level directory like "cute_framework-master/"
|
|
94
98
|
# We want to extract "include/" and "docs/topics/" subdirectories
|
|
95
|
-
top_level_prefix = nil
|
|
99
|
+
top_level_prefix = nil #: String?
|
|
96
100
|
|
|
97
101
|
zip_file.each do |entry|
|
|
98
102
|
# Find the top-level directory prefix (e.g., "cute_framework-master/" or "cute_framework-abc1234/")
|
|
99
103
|
if top_level_prefix.nil? && entry.name.match?(%r{^cute_framework-[^/]+/include/})
|
|
100
|
-
top_level_prefix = entry.name
|
|
104
|
+
top_level_prefix = entry.name[%r{^(cute_framework-[^/]+/)}, 1]
|
|
101
105
|
break
|
|
102
106
|
end
|
|
103
107
|
end
|
data/lib/cf/mcp/github_client.rb
CHANGED
|
@@ -25,7 +25,8 @@ module CF
|
|
|
25
25
|
request["Authorization"] = "Bearer #{@token}" if @token
|
|
26
26
|
request["User-Agent"] = "cf-mcp/#{CF::MCP::VERSION}"
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
hostname = uri.hostname || raise(ArgumentError, "No host in #{uri}")
|
|
29
|
+
response = Net::HTTP.start(hostname, uri.port, use_ssl: true) do |http|
|
|
29
30
|
http.request(request)
|
|
30
31
|
end
|
|
31
32
|
|
data/lib/cf/mcp/index.rb
CHANGED
|
@@ -7,6 +7,8 @@ module CF
|
|
|
7
7
|
class Index
|
|
8
8
|
include Singleton
|
|
9
9
|
|
|
10
|
+
ByType = Data.define(:functions, :structs, :enums, :topics)
|
|
11
|
+
|
|
10
12
|
attr_reader :items, :by_type, :by_category, :topic_references
|
|
11
13
|
|
|
12
14
|
def initialize
|
|
@@ -15,27 +17,28 @@ module CF
|
|
|
15
17
|
|
|
16
18
|
def reset!
|
|
17
19
|
@items = {}
|
|
18
|
-
@by_type =
|
|
19
|
-
function: [],
|
|
20
|
-
struct: [],
|
|
21
|
-
enum: [],
|
|
22
|
-
topic: []
|
|
23
|
-
}
|
|
20
|
+
@by_type = ByType.new(functions: [], structs: [], enums: [], topics: [])
|
|
24
21
|
@by_category = {}
|
|
25
22
|
@topic_references = {}
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def add(item)
|
|
29
26
|
@items[item.name] = item
|
|
30
|
-
|
|
27
|
+
|
|
28
|
+
case item
|
|
29
|
+
when Models::FunctionDoc then @by_type.functions << item
|
|
30
|
+
when Models::StructDoc then @by_type.structs << item
|
|
31
|
+
when Models::EnumDoc then @by_type.enums << item
|
|
32
|
+
when Models::TopicDoc
|
|
33
|
+
@by_type.topics << item
|
|
34
|
+
# Build reverse reference index for topics
|
|
35
|
+
build_topic_reverse_index(item)
|
|
36
|
+
end
|
|
31
37
|
|
|
32
38
|
if item.category
|
|
33
39
|
@by_category[item.category] ||= []
|
|
34
40
|
@by_category[item.category] << item
|
|
35
41
|
end
|
|
36
|
-
|
|
37
|
-
# Build reverse reference index for topics
|
|
38
|
-
build_topic_reverse_index(item) if item.type == :topic
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def find(name)
|
|
@@ -73,19 +76,19 @@ module CF
|
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def functions
|
|
76
|
-
@by_type
|
|
79
|
+
@by_type.functions
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
def structs
|
|
80
|
-
@by_type
|
|
83
|
+
@by_type.structs
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
def enums
|
|
84
|
-
@by_type
|
|
87
|
+
@by_type.enums
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
def topics
|
|
88
|
-
@by_type
|
|
91
|
+
@by_type.topics
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def topics_ordered
|
|
@@ -111,10 +114,10 @@ module CF
|
|
|
111
114
|
def stats
|
|
112
115
|
{
|
|
113
116
|
total: @items.size,
|
|
114
|
-
functions: @by_type
|
|
115
|
-
structs: @by_type
|
|
116
|
-
enums: @by_type
|
|
117
|
-
topics: @by_type
|
|
117
|
+
functions: @by_type.functions.size,
|
|
118
|
+
structs: @by_type.structs.size,
|
|
119
|
+
enums: @by_type.enums.size,
|
|
120
|
+
topics: @by_type.topics.size,
|
|
118
121
|
categories: @by_category.size
|
|
119
122
|
}
|
|
120
123
|
end
|
data/lib/cf/mcp/index_builder.rb
CHANGED
|
@@ -23,14 +23,13 @@ module CF
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# Parse topics if available
|
|
26
|
-
|
|
27
|
-
if topics_path && File.directory?(topics_path)
|
|
26
|
+
if (path = topics_path)
|
|
28
27
|
topic_parser = TopicParser.new
|
|
29
|
-
topic_parser.parse_directory(
|
|
28
|
+
topic_parser.parse_directory(path).each do |topic|
|
|
30
29
|
refine_topic_references(topic, index)
|
|
31
30
|
index.add(topic)
|
|
32
31
|
end
|
|
33
|
-
yield(:topics_indexed,
|
|
32
|
+
yield(:topics_indexed, path, index.stats[:topics]) if block_given?
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
index
|
|
@@ -40,6 +39,11 @@ module CF
|
|
|
40
39
|
File.directory?(headers_path)
|
|
41
40
|
end
|
|
42
41
|
|
|
42
|
+
# The topic guides directory, when the checkout has one.
|
|
43
|
+
def topics_path
|
|
44
|
+
@topics_path ||= find_topics_path(headers_path)
|
|
45
|
+
end
|
|
46
|
+
|
|
43
47
|
private
|
|
44
48
|
|
|
45
49
|
def resolve_headers_path(root:, download:)
|