cf-mcp 0.9.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4fdaf411a4f3224a85c8399b9bd6964688ad3a6e8ce68ab61abbd76c32833c8e
4
+ data.tar.gz: b23b96e53d98c56b9fc3364ec76a3a67c50df807b152d8b0e43f1f07db626d2e
5
+ SHA512:
6
+ metadata.gz: 23b5cfa8ed27a9f71d8c031cefabad9b12c83ae5e109f9306b2456fcc0979c305287bcfde0e0dd2581009f84683b78b0e16630a52e8c665aba15c0dc26639453
7
+ data.tar.gz: 1e1785c0bac762cf2b77dfa4bf26faa4e9093124ccf2be74972cc34552e315ca20dbe9a530136b8b34f485d6b1ae727bd617028e5ef3bdad3feb6684787d1fb6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Piotr Usewicz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,34 @@
1
+ LICENSE.txt
2
+ Manifest.txt
3
+ README.md
4
+ Rakefile
5
+ config.ru
6
+ exe/cf-mcp
7
+ lib/cf/mcp.rb
8
+ lib/cf/mcp/cli.rb
9
+ lib/cf/mcp/downloader.rb
10
+ lib/cf/mcp/index.rb
11
+ lib/cf/mcp/models/doc_item.rb
12
+ lib/cf/mcp/models/enum_doc.rb
13
+ lib/cf/mcp/models/function_doc.rb
14
+ lib/cf/mcp/models/struct_doc.rb
15
+ lib/cf/mcp/models/topic_doc.rb
16
+ lib/cf/mcp/parser.rb
17
+ lib/cf/mcp/server.rb
18
+ lib/cf/mcp/templates/index.erb
19
+ lib/cf/mcp/templates/script.js
20
+ lib/cf/mcp/templates/style.css
21
+ lib/cf/mcp/tools/find_related.rb
22
+ lib/cf/mcp/tools/get_details.rb
23
+ lib/cf/mcp/tools/get_topic.rb
24
+ lib/cf/mcp/tools/list_category.rb
25
+ lib/cf/mcp/tools/list_topics.rb
26
+ lib/cf/mcp/tools/member_search.rb
27
+ lib/cf/mcp/tools/parameter_search.rb
28
+ lib/cf/mcp/tools/search_enums.rb
29
+ lib/cf/mcp/tools/search_functions.rb
30
+ lib/cf/mcp/tools/search_structs.rb
31
+ lib/cf/mcp/tools/search_tool.rb
32
+ lib/cf/mcp/topic_parser.rb
33
+ lib/cf/mcp/version.rb
34
+ sig/cf/mcp.rbs
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # CF::MCP
2
+
3
+ CF::MCP is an MCP server providing documentation tools for the [Cute Framework](https://github.com/RandyGaul/cute_framework), a C/C++ 2D game framework.
4
+
5
+ The MCP server supports three modes of operation:
6
+
7
+ - **STDIO Mode**: Communicates via standard input and output streams, suitable for integration with CLI tools and desktop applications.
8
+ - **HTTP Mode**: Operates as a stateless HTTP server, suitable for simple request/response interactions and multi-node deployments.
9
+ - **SSE Mode**: Operates as a stateful HTTP server with Server-Sent Events support, enabling real-time notifications and streaming responses.
10
+
11
+ ## Features
12
+
13
+ - **Documentation Generation**: Automatically generates documentation for Cute Framework projects by indexing the the header files and extracting the documentation from the comments.
14
+ - **Search Functionality**: Provides a search feature to quickly find structs, classes, functions, and other elements within the documentation.
15
+
16
+ ## Installation
17
+
18
+ Install the gem and add to the application's Gemfile by executing:
19
+
20
+ ```bash
21
+ bundle add cf-mcp
22
+ ```
23
+
24
+ If bundler is not being used to manage dependencies, install the gem by executing:
25
+
26
+ ```bash
27
+ gem install cf-mcp
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ To start the MCP server, run the following command in your terminal:
33
+
34
+ ```bash
35
+ cf-mcp stdio --root /path/to/cute_framework_project # STDIO mode
36
+ cf-mcp http --root /path/to/cute_framework_project # HTTP mode (stateless)
37
+ cf-mcp sse --root /path/to/cute_framework_project # SSE mode (stateful, real-time)
38
+ ```
39
+
40
+ ## Development
41
+
42
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
43
+
44
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
45
+
46
+ ## Contributing
47
+
48
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pusewicz/cf-mcp.
49
+
50
+ ## License
51
+
52
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ desc "Generate Manifest.txt from git ls-files"
11
+ task :manifest do
12
+ ignore_patterns = %w[bin/ Gemfile .gitignore test/ .github/ .standard.yml cf-mcp.gemspec .ruby-version CLAUDE.md fly.toml Procfile Dockerfile .dockerignore .claude/]
13
+
14
+ files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
15
+ ls.readlines("\x0", chomp: true)
16
+ end.reject { |f| f.start_with?(*ignore_patterns) }.sort
17
+
18
+ # Add Manifest.txt itself so it's included in the gem
19
+ files << "Manifest.txt" unless files.include?("Manifest.txt")
20
+ files.sort!
21
+
22
+ File.write("Manifest.txt", files.join("\n") + "\n")
23
+ puts "Generated Manifest.txt with #{files.size} files"
24
+ end
25
+
26
+ task default: %i[test standard manifest]
27
+
28
+ desc "Deploy to Fly.io (runs tests and linting first)"
29
+ task deploy: %i[test standard] do
30
+ sh "fly deploy"
31
+ end
32
+
33
+ desc "Create a git tag for the current version"
34
+ task :tag do
35
+ require_relative "lib/cf/mcp/version"
36
+ version = CF::MCP::VERSION
37
+ tag = "v#{version}"
38
+
39
+ if system("git", "rev-parse", tag, out: File::NULL, err: File::NULL)
40
+ puts "Tag #{tag} already exists"
41
+ else
42
+ sh "git", "tag", "-a", tag, "-m", "Release #{version}"
43
+ puts "Created tag #{tag}"
44
+ end
45
+ end
46
+
47
+ desc "Create and push git tag for current version"
48
+ task "release:tag" => %i[test standard tag] do
49
+ require_relative "lib/cf/mcp/version"
50
+ tag = "v#{CF::MCP::VERSION}"
51
+ sh "git", "push", "origin", tag
52
+ puts "Pushed #{tag} to origin"
53
+ end
data/config.ru ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/cf/mcp"
4
+
5
+ # Download headers from GitHub if not already present
6
+ warn "Initializing CF::MCP server..."
7
+ downloader = CF::MCP::Downloader.new
8
+ headers_path = downloader.download_and_extract
9
+ warn "Using headers from: #{headers_path}"
10
+
11
+ # Build the index
12
+ warn "Parsing headers..."
13
+ parser = CF::MCP::Parser.new
14
+ index = CF::MCP::Index.new
15
+
16
+ parser.parse_directory(headers_path).each do |item|
17
+ index.add(item)
18
+ end
19
+
20
+ warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
21
+
22
+ # Create and run the combined server with both SSE and HTTP transports
23
+ # - / and /sse - SSE transport (stateful, for Claude Desktop)
24
+ # - /http - HTTP transport (stateless, for simple integrations)
25
+ server = CF::MCP::CombinedServer.new(index)
26
+ run server.rack_app
data/exe/cf-mcp ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "cf/mcp"
5
+
6
+ CF::MCP::CLI.new(ARGV).run
data/lib/cf/mcp/cli.rb ADDED
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "parser"
5
+ require_relative "topic_parser"
6
+ require_relative "index"
7
+ require_relative "server"
8
+ require_relative "downloader"
9
+
10
+ module CF
11
+ module MCP
12
+ class CLI
13
+ DEFAULT_HEADERS_PATH = File.expand_path("~/Work/GitHub/pusewicz/cute_framework/include")
14
+
15
+ def initialize(args)
16
+ @args = args
17
+ @options = parse_args
18
+ end
19
+
20
+ def run
21
+ case @options[:command]
22
+ when :stdio
23
+ run_server(:stdio)
24
+ when :http
25
+ run_server(:http)
26
+ when :sse
27
+ run_server(:sse)
28
+ when :combined
29
+ run_combined_server
30
+ when :help
31
+ puts @option_parser
32
+ else
33
+ warn "Unknown command. Use --help for usage information."
34
+ exit 1
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def parse_args
41
+ options = {
42
+ command: nil,
43
+ port: nil,
44
+ root: nil,
45
+ download: false
46
+ }
47
+
48
+ @option_parser = OptionParser.new do |opts|
49
+ opts.banner = "Usage: cf-mcp <command> [options]"
50
+ opts.separator ""
51
+ opts.separator "Commands:"
52
+ opts.separator " stdio Run in STDIO mode (for CLI integration)"
53
+ opts.separator " http Run as HTTP server (stateless)"
54
+ opts.separator " sse Run as SSE server (stateful with real-time updates)"
55
+ opts.separator " combined Run as combined server (SSE + HTTP with web interface)"
56
+ opts.separator ""
57
+ opts.separator "Options:"
58
+
59
+ opts.on("-r", "--root PATH", "Path to Cute Framework headers directory") do |path|
60
+ options[:root] = path
61
+ end
62
+
63
+ opts.on("-p", "--port PORT", Integer, "Port for HTTP/SSE server (default: 9292 for HTTP, 9393 for SSE)") do |port|
64
+ options[:port] = port
65
+ end
66
+
67
+ opts.on("-d", "--download", "Download Cute Framework headers from GitHub") do
68
+ options[:download] = true
69
+ end
70
+
71
+ opts.on("-h", "--help", "Show this help message") do
72
+ options[:command] = :help
73
+ end
74
+
75
+ opts.on("-v", "--version", "Show version") do
76
+ puts "cf-mcp #{CF::MCP::VERSION}"
77
+ exit 0
78
+ end
79
+ end
80
+
81
+ @option_parser.parse!(@args)
82
+
83
+ # Parse command from remaining args
84
+ if options[:command].nil? && !@args.empty?
85
+ command = @args.shift.to_sym
86
+ options[:command] = command if [:stdio, :http, :sse, :combined].include?(command)
87
+ end
88
+
89
+ options[:command] ||= :help
90
+ options
91
+ end
92
+
93
+ def run_server(mode)
94
+ headers_path = resolve_headers_path
95
+
96
+ unless File.directory?(headers_path)
97
+ warn "Error: Headers directory not found: #{headers_path}"
98
+ warn "Use --root to specify the path to Cute Framework headers"
99
+ warn "Or use --download to fetch headers from GitHub"
100
+ exit 1
101
+ end
102
+
103
+ warn "Parsing headers from: #{headers_path}"
104
+ index = build_index(headers_path)
105
+ warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
106
+
107
+ server = Server.new(index)
108
+
109
+ case mode
110
+ when :stdio
111
+ server.run_stdio
112
+ when :http
113
+ port = @options[:port] || 9292
114
+ server.run_http(port: port)
115
+ when :sse
116
+ port = @options[:port] || 9393
117
+ server.run_sse(port: port)
118
+ end
119
+ end
120
+
121
+ def run_combined_server
122
+ require "rack"
123
+ require "rackup"
124
+
125
+ headers_path = resolve_headers_path
126
+
127
+ unless File.directory?(headers_path)
128
+ warn "Error: Headers directory not found: #{headers_path}"
129
+ warn "Use --root to specify the path to Cute Framework headers"
130
+ warn "Or use --download to fetch headers from GitHub"
131
+ exit 1
132
+ end
133
+
134
+ warn "Parsing headers from: #{headers_path}"
135
+ index = build_index(headers_path)
136
+ warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
137
+
138
+ port = @options[:port] || 9292
139
+ server = CombinedServer.new(index)
140
+ app = server.rack_app
141
+
142
+ warn "Starting combined server on port #{port}..."
143
+ warn "Web interface available at http://localhost:#{port}/"
144
+ Rackup::Server.start(app: app, Port: port, Logger: $stderr)
145
+ end
146
+
147
+ def resolve_headers_path
148
+ return @options[:root] if @options[:root]
149
+ return ENV["CF_HEADERS_PATH"] if ENV["CF_HEADERS_PATH"]
150
+
151
+ if @options[:download]
152
+ warn "Downloading Cute Framework headers from GitHub..."
153
+ downloader = Downloader.new
154
+ path = downloader.download_and_extract
155
+ warn "Downloaded headers to: #{path}"
156
+ return path
157
+ end
158
+
159
+ DEFAULT_HEADERS_PATH
160
+ end
161
+
162
+ def build_index(headers_path)
163
+ parser = Parser.new
164
+ index = Index.new
165
+
166
+ parser.parse_directory(headers_path).each do |item|
167
+ index.add(item)
168
+ end
169
+
170
+ # Parse topics if available
171
+ topics_path = find_topics_path(headers_path)
172
+ if topics_path && File.directory?(topics_path)
173
+ topic_parser = TopicParser.new
174
+ topic_parser.parse_directory(topics_path).each do |topic|
175
+ refine_topic_references(topic, index)
176
+ index.add(topic)
177
+ end
178
+ warn "Indexed #{index.stats[:topics]} topics from: #{topics_path}"
179
+ end
180
+
181
+ index
182
+ end
183
+
184
+ def find_topics_path(headers_path)
185
+ # If headers_path is .../cute_framework/include, topics is at .../cute_framework/docs/topics
186
+ base = File.dirname(headers_path)
187
+ topics_path = File.join(base, "docs", "topics")
188
+ return topics_path if File.directory?(topics_path)
189
+
190
+ # Alternative: topics directly under headers parent
191
+ topics_path = File.join(base, "topics")
192
+ return topics_path if File.directory?(topics_path)
193
+
194
+ nil
195
+ end
196
+
197
+ def refine_topic_references(topic, index)
198
+ # Move items from struct_references to enum_references if they're actually enums
199
+ topic.struct_references.dup.each do |ref|
200
+ item = index.find(ref)
201
+ next unless item
202
+
203
+ if item.type == :enum
204
+ topic.struct_references.delete(ref)
205
+ topic.enum_references << ref unless topic.enum_references.include?(ref)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "fileutils"
6
+ require "zip"
7
+ require_relative "version"
8
+
9
+ module CF
10
+ module MCP
11
+ class Downloader
12
+ CUTE_FRAMEWORK_ZIP_URL = "https://github.com/RandyGaul/cute_framework/archive/refs/heads/master.zip"
13
+ DEFAULT_DOWNLOAD_DIR = File.join(Dir.tmpdir, "cf-mcp-#{VERSION}")
14
+
15
+ class DownloadError < StandardError; end
16
+
17
+ def initialize(download_dir: DEFAULT_DOWNLOAD_DIR)
18
+ @download_dir = download_dir
19
+ end
20
+
21
+ def download_and_extract
22
+ FileUtils.mkdir_p(@download_dir)
23
+
24
+ zip_path = File.join(@download_dir, "cute_framework.zip")
25
+ base_path = File.join(@download_dir, "cute_framework")
26
+ include_path = File.join(base_path, "include")
27
+ File.join(base_path, "docs", "topics")
28
+
29
+ # Return existing path if already downloaded
30
+ if File.directory?(include_path) && !Dir.empty?(include_path)
31
+ return include_path
32
+ end
33
+
34
+ download_zip(zip_path)
35
+ extract_directories(zip_path, base_path)
36
+
37
+ include_path
38
+ end
39
+
40
+ private
41
+
42
+ def download_zip(destination)
43
+ uri = URI.parse(CUTE_FRAMEWORK_ZIP_URL)
44
+
45
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
46
+ request = Net::HTTP::Get.new(uri)
47
+ response = http.request(request)
48
+
49
+ # Handle redirects (GitHub redirects to codeload.github.com)
50
+ if response.is_a?(Net::HTTPRedirection)
51
+ redirect_uri = URI.parse(response["location"])
52
+ Net::HTTP.start(redirect_uri.host, redirect_uri.port, use_ssl: true) do |redirect_http|
53
+ redirect_request = Net::HTTP::Get.new(redirect_uri)
54
+ response = redirect_http.request(redirect_request)
55
+ end
56
+ end
57
+
58
+ unless response.is_a?(Net::HTTPSuccess)
59
+ raise DownloadError, "Failed to download Cute Framework: #{response.code} #{response.message}"
60
+ end
61
+
62
+ File.binwrite(destination, response.body)
63
+ end
64
+ end
65
+
66
+ def extract_directories(zip_path, base_path)
67
+ FileUtils.rm_rf(base_path)
68
+ FileUtils.mkdir_p(base_path)
69
+
70
+ Zip::File.open(zip_path) do |zip_file|
71
+ # The zip contains a top-level directory like "cute_framework-master/"
72
+ # We want to extract "include/" and "docs/topics/" subdirectories
73
+ top_level_prefix = nil
74
+
75
+ zip_file.each do |entry|
76
+ # Find the top-level directory prefix (e.g., "cute_framework-master/")
77
+ if top_level_prefix.nil? && entry.name.match?(%r{^[^/]+/include/})
78
+ top_level_prefix = entry.name.match(%r{^([^/]+/)})[1]
79
+ break
80
+ end
81
+ end
82
+
83
+ raise DownloadError, "Could not find include directory in zip" unless top_level_prefix
84
+
85
+ # Directories to extract
86
+ extract_prefixes = [
87
+ "#{top_level_prefix}include/",
88
+ "#{top_level_prefix}docs/topics/"
89
+ ]
90
+
91
+ zip_file.each do |entry|
92
+ extract_prefix = extract_prefixes.find { |p| entry.name.start_with?(p) }
93
+ next unless extract_prefix
94
+
95
+ # Calculate the relative path from the top-level directory
96
+ relative_path = entry.name.sub(top_level_prefix, "")
97
+ next if relative_path.empty?
98
+
99
+ target_path = File.join(base_path, relative_path)
100
+
101
+ if entry.directory?
102
+ FileUtils.mkdir_p(target_path)
103
+ else
104
+ FileUtils.mkdir_p(File.dirname(target_path))
105
+ entry.extract(target_path)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CF
4
+ module MCP
5
+ class Index
6
+ attr_reader :items, :by_type, :by_category, :topic_references
7
+
8
+ def initialize
9
+ @items = {}
10
+ @by_type = {
11
+ function: [],
12
+ struct: [],
13
+ enum: [],
14
+ topic: []
15
+ }
16
+ @by_category = {}
17
+ @topic_references = {}
18
+ end
19
+
20
+ def add(item)
21
+ @items[item.name] = item
22
+ @by_type[item.type] << item if @by_type.key?(item.type)
23
+
24
+ if item.category
25
+ @by_category[item.category] ||= []
26
+ @by_category[item.category] << item
27
+ end
28
+
29
+ # Build reverse reference index for topics
30
+ build_topic_reverse_index(item) if item.type == :topic
31
+ end
32
+
33
+ def find(name)
34
+ @items[name]
35
+ end
36
+
37
+ def brief_for(name)
38
+ item = find(name)
39
+ return nil unless item
40
+ {name: item.name, type: item.type, brief: item.brief}
41
+ end
42
+
43
+ def search(query, type: nil, category: nil, limit: 20)
44
+ results = all_items
45
+
46
+ # Filter by type
47
+ if type
48
+ type_sym = type.to_sym
49
+ results = results.select { |item| item.type == type_sym }
50
+ end
51
+
52
+ # Filter by category
53
+ if category
54
+ results = results.select { |item| item.category == category }
55
+ end
56
+
57
+ # Filter by query and sort by relevance
58
+ if query && !query.empty?
59
+ results = results
60
+ .select { |item| item.matches?(query) }
61
+ .sort_by { |item| -item.relevance_score(query) }
62
+ end
63
+
64
+ results.take(limit)
65
+ end
66
+
67
+ def functions
68
+ @by_type[:function]
69
+ end
70
+
71
+ def structs
72
+ @by_type[:struct]
73
+ end
74
+
75
+ def enums
76
+ @by_type[:enum]
77
+ end
78
+
79
+ def topics
80
+ @by_type[:topic]
81
+ end
82
+
83
+ def topics_ordered
84
+ topics.sort_by { |t| t.reading_order || Float::INFINITY }
85
+ end
86
+
87
+ def topics_for(api_name)
88
+ (@topic_references[api_name] || []).map { |name| find(name) }.compact
89
+ end
90
+
91
+ def categories
92
+ @by_category.keys.sort
93
+ end
94
+
95
+ def items_in_category(category)
96
+ @by_category[category] || []
97
+ end
98
+
99
+ def size
100
+ @items.size
101
+ end
102
+
103
+ def stats
104
+ {
105
+ total: @items.size,
106
+ functions: @by_type[:function].size,
107
+ structs: @by_type[:struct].size,
108
+ enums: @by_type[:enum].size,
109
+ topics: @by_type[:topic].size,
110
+ categories: @by_category.size
111
+ }
112
+ end
113
+
114
+ private
115
+
116
+ def all_items
117
+ @items.values
118
+ end
119
+
120
+ def build_topic_reverse_index(topic)
121
+ topic.all_api_references.each do |ref_name|
122
+ @topic_references[ref_name] ||= []
123
+ @topic_references[ref_name] << topic.name unless @topic_references[ref_name].include?(topic.name)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end