cf-mcp 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f3d843ae9e0087f798cad60be61d347da0db7c1f26e5bee5050eccda2a38b29
4
- data.tar.gz: 98735400ef25d3a51d6d984a0daf12141c1c96b6843922069d71bb24a4183bdc
3
+ metadata.gz: 30c68992a64d36c8265af4bdcd018dd116c7d2e59479ab1fd83213896028b86d
4
+ data.tar.gz: 4a4e5b715cdca959296e1043bf3173a2c4c314a40ab5c1cb76154dd20b823557
5
5
  SHA512:
6
- metadata.gz: fb5143e02aca5453221893bdabd3cb945912cfa83c7693f34d0cba99ea1cbde14a93d4238d3dfb92ab1e39026f5b98ab416de2a40eb68f262745f028e6aecf68
7
- data.tar.gz: 918e16127b4a29c8e0d33fe162b5d1788d14c28f3e1c55774d97204ad1805a38c1407f6d10b07072fe9b55c1998436f833fb957ee4f3739434887e94eea84f07
6
+ metadata.gz: 1704a19c8b49fb37258b247e5689a15f7ed67a2cce9ff226ee5ac0c4689dbc45b547150d426cc91e15e09a2f36405752f58a98e5b14596e5282dc7b9784c4e15
7
+ data.tar.gz: d1c5c7f81871cddb2f0a1bb81b98272b7d3d07a8d7d40bfafd08c338ef2e6b8073d296407ae2dd74575b7ec2738864c1c5bd941408f9f681e373002af363a70d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.12.1] - 2026-01-14
9
+
10
+ ### Changed
11
+
12
+ - Refactored tool classes to use shared `ResponseHelpers` module, removing duplicate code
13
+ - Extracted `IndexBuilder` class to consolidate index building logic from CLI and HTTPServer
14
+ - Extracted `SearchResultFormatter` module for consistent search result formatting
15
+ - Simplified model `to_text()` methods using template method pattern in `DocItem` base class
16
+
8
17
  ## [0.12.0] - 2026-01-14
9
18
 
10
19
  ### Changed
@@ -139,6 +148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
139
148
  - `cf_list_category` - List items by category
140
149
  - `cf_get_details` - Get full documentation by name
141
150
 
151
+ [0.12.1]: https://github.com/pusewicz/cf-mcp/compare/v0.12.0...v0.12.1
142
152
  [0.12.0]: https://github.com/pusewicz/cf-mcp/compare/v0.10.1...v0.12.0
143
153
  [0.10.1]: https://github.com/pusewicz/cf-mcp/compare/v0.10.0...v0.10.1
144
154
  [0.10.0]: https://github.com/pusewicz/cf-mcp/compare/v0.9.3...v0.10.0
data/Manifest.txt CHANGED
@@ -9,6 +9,7 @@ lib/cf/mcp.rb
9
9
  lib/cf/mcp/cli.rb
10
10
  lib/cf/mcp/downloader.rb
11
11
  lib/cf/mcp/index.rb
12
+ lib/cf/mcp/index_builder.rb
12
13
  lib/cf/mcp/models/doc_item.rb
13
14
  lib/cf/mcp/models/enum_doc.rb
14
15
  lib/cf/mcp/models/function_doc.rb
@@ -26,8 +27,10 @@ lib/cf/mcp/tools/list_category.rb
26
27
  lib/cf/mcp/tools/list_topics.rb
27
28
  lib/cf/mcp/tools/member_search.rb
28
29
  lib/cf/mcp/tools/parameter_search.rb
30
+ lib/cf/mcp/tools/response_helpers.rb
29
31
  lib/cf/mcp/tools/search_enums.rb
30
32
  lib/cf/mcp/tools/search_functions.rb
33
+ lib/cf/mcp/tools/search_result_formatter.rb
31
34
  lib/cf/mcp/tools/search_structs.rb
32
35
  lib/cf/mcp/tools/search_tool.rb
33
36
  lib/cf/mcp/topic_parser.rb
data/lib/cf/mcp/cli.rb CHANGED
@@ -1,17 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
- require_relative "parser"
5
- require_relative "topic_parser"
6
- require_relative "index"
4
+ require_relative "index_builder"
7
5
  require_relative "server"
8
- require_relative "downloader"
9
6
 
10
7
  module CF
11
8
  module MCP
12
9
  class CLI
13
- DEFAULT_HEADERS_PATH = File.expand_path("~/Work/GitHub/pusewicz/cute_framework/include")
14
-
15
10
  def initialize(args)
16
11
  @args = args
17
12
  @options = parse_args
@@ -90,17 +85,19 @@ module CF
90
85
  end
91
86
 
92
87
  def run_server(mode)
93
- headers_path = resolve_headers_path
88
+ builder = IndexBuilder.new(root: @options[:root], download: @options[:download])
94
89
 
95
- unless File.directory?(headers_path)
96
- warn "Error: Headers directory not found: #{headers_path}"
90
+ unless builder.valid?
91
+ warn "Error: Headers directory not found: #{builder.headers_path}"
97
92
  warn "Use --root to specify the path to Cute Framework headers"
98
93
  warn "Or use --download to fetch headers from GitHub"
99
94
  exit 1
100
95
  end
101
96
 
102
- warn "Parsing headers from: #{headers_path}"
103
- index = build_index(headers_path)
97
+ warn "Parsing headers from: #{builder.headers_path}"
98
+ index = builder.build do |event, path, count|
99
+ warn "Indexed #{count} topics from: #{path}" if event == :topics_indexed
100
+ end
104
101
  warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
105
102
 
106
103
  server = Server.new(index)
@@ -123,69 +120,6 @@ module CF
123
120
  warn "MCP endpoint available at http://localhost:#{port}/http"
124
121
  Rackup::Server.start(app: app, Host: host, Port: port, Logger: $stderr)
125
122
  end
126
-
127
- def resolve_headers_path
128
- return @options[:root] if @options[:root]
129
- return ENV["CF_HEADERS_PATH"] if ENV["CF_HEADERS_PATH"]
130
-
131
- if @options[:download]
132
- warn "Downloading Cute Framework headers from GitHub..."
133
- downloader = Downloader.new
134
- path = downloader.download_and_extract
135
- warn "Downloaded headers to: #{path}"
136
- return path
137
- end
138
-
139
- DEFAULT_HEADERS_PATH
140
- end
141
-
142
- def build_index(headers_path)
143
- parser = Parser.new
144
- index = Index.new
145
-
146
- parser.parse_directory(headers_path).each do |item|
147
- index.add(item)
148
- end
149
-
150
- # Parse topics if available
151
- topics_path = find_topics_path(headers_path)
152
- if topics_path && File.directory?(topics_path)
153
- topic_parser = TopicParser.new
154
- topic_parser.parse_directory(topics_path).each do |topic|
155
- refine_topic_references(topic, index)
156
- index.add(topic)
157
- end
158
- warn "Indexed #{index.stats[:topics]} topics from: #{topics_path}"
159
- end
160
-
161
- index
162
- end
163
-
164
- def find_topics_path(headers_path)
165
- # If headers_path is .../cute_framework/include, topics is at .../cute_framework/docs/topics
166
- base = File.dirname(headers_path)
167
- topics_path = File.join(base, "docs", "topics")
168
- return topics_path if File.directory?(topics_path)
169
-
170
- # Alternative: topics directly under headers parent
171
- topics_path = File.join(base, "topics")
172
- return topics_path if File.directory?(topics_path)
173
-
174
- nil
175
- end
176
-
177
- def refine_topic_references(topic, index)
178
- # Move items from struct_references to enum_references if they're actually enums
179
- topic.struct_references.dup.each do |ref|
180
- item = index.find(ref)
181
- next unless item
182
-
183
- if item.type == :enum
184
- topic.struct_references.delete(ref)
185
- topic.enum_references << ref unless topic.enum_references.include?(ref)
186
- end
187
- end
188
- end
189
123
  end
190
124
  end
191
125
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parser"
4
+ require_relative "topic_parser"
5
+ require_relative "index"
6
+ require_relative "downloader"
7
+
8
+ module CF
9
+ module MCP
10
+ class IndexBuilder
11
+ DEFAULT_HEADERS_PATH = File.expand_path("~/Work/GitHub/pusewicz/cute_framework/include")
12
+
13
+ attr_reader :headers_path
14
+
15
+ def initialize(root: nil, download: false)
16
+ @headers_path = resolve_headers_path(root: root, download: download)
17
+ end
18
+
19
+ def build
20
+ parser = Parser.new
21
+ index = Index.new
22
+
23
+ parser.parse_directory(headers_path).each do |item|
24
+ index.add(item)
25
+ end
26
+
27
+ # Parse topics if available
28
+ topics_path = find_topics_path(headers_path)
29
+ if topics_path && File.directory?(topics_path)
30
+ topic_parser = TopicParser.new
31
+ topic_parser.parse_directory(topics_path).each do |topic|
32
+ refine_topic_references(topic, index)
33
+ index.add(topic)
34
+ end
35
+ yield(:topics_indexed, topics_path, index.stats[:topics]) if block_given?
36
+ end
37
+
38
+ index
39
+ end
40
+
41
+ def valid?
42
+ File.directory?(headers_path)
43
+ end
44
+
45
+ private
46
+
47
+ def resolve_headers_path(root:, download:)
48
+ return root if root
49
+ return ENV["CF_HEADERS_PATH"] if ENV["CF_HEADERS_PATH"]
50
+
51
+ if download
52
+ warn "Downloading Cute Framework headers from GitHub..."
53
+ downloader = Downloader.new
54
+ path = downloader.download_and_extract
55
+ warn "Downloaded headers to: #{path}"
56
+ return path
57
+ end
58
+
59
+ DEFAULT_HEADERS_PATH
60
+ end
61
+
62
+ def find_topics_path(headers_path)
63
+ # If headers_path is .../cute_framework/include, topics is at .../cute_framework/docs/topics
64
+ base = File.dirname(headers_path)
65
+ topics_path = File.join(base, "docs", "topics")
66
+ return topics_path if File.directory?(topics_path)
67
+
68
+ # Alternative: topics directly under headers parent
69
+ topics_path = File.join(base, "topics")
70
+ return topics_path if File.directory?(topics_path)
71
+
72
+ nil
73
+ end
74
+
75
+ def refine_topic_references(topic, index)
76
+ # Move items from struct_references to enum_references if they're actually enums
77
+ topic.struct_references.dup.each do |ref|
78
+ item = index.find(ref)
79
+ next unless item
80
+
81
+ if item.type == :enum
82
+ topic.struct_references.delete(ref)
83
+ topic.enum_references << ref unless topic.enum_references.include?(ref)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -128,6 +128,23 @@ module CF
128
128
  end
129
129
 
130
130
  def to_text(detailed: false, index: nil)
131
+ lines = []
132
+ lines.concat(build_header_lines)
133
+ lines.concat(build_description_lines)
134
+
135
+ if detailed
136
+ lines.concat(build_type_specific_lines)
137
+ lines.concat(build_remarks_lines)
138
+ lines.concat(build_example_lines)
139
+ lines.concat(build_related_lines(index))
140
+ end
141
+
142
+ lines.join("\n")
143
+ end
144
+
145
+ protected
146
+
147
+ def build_header_lines
131
148
  lines = []
132
149
  lines << "# #{name}"
133
150
  lines << ""
@@ -140,36 +157,44 @@ module CF
140
157
  lines << "- **Implementation:** #{urls[:impl_raw]}"
141
158
  end
142
159
  lines << ""
160
+ lines
161
+ end
162
+
163
+ def build_description_lines
164
+ lines = []
143
165
  lines << "## Description"
144
166
  lines << brief if brief
145
167
  lines << ""
168
+ lines
169
+ end
146
170
 
147
- if detailed
148
- if remarks && !remarks.empty?
149
- lines << "## Remarks"
150
- lines << remarks
151
- lines << ""
152
- end
171
+ def build_type_specific_lines
172
+ [] # Override in subclasses
173
+ end
153
174
 
154
- if example && !example.empty?
155
- lines << "## Example"
156
- lines << example_brief if example_brief
157
- lines << "```c"
158
- lines << example
159
- lines << "```"
160
- lines << ""
161
- end
175
+ def build_remarks_lines
176
+ return [] unless remarks && !remarks.empty?
177
+ ["## Remarks", remarks, ""]
178
+ end
162
179
 
163
- if related && !related.empty?
164
- lines << "## Related"
165
- lines << format_related_items(index)
166
- lines << ""
167
- end
168
- end
180
+ def build_example_lines
181
+ return [] unless example && !example.empty?
182
+ lines = ["## Example"]
183
+ lines << example_brief if example_brief
184
+ lines << "```c"
185
+ lines << example
186
+ lines << "```"
187
+ lines << ""
188
+ lines
189
+ end
169
190
 
170
- lines.join("\n")
191
+ def build_related_lines(index)
192
+ return [] unless related && !related.empty?
193
+ ["## Related", format_related_items(index), ""]
171
194
  end
172
195
 
196
+ public
197
+
173
198
  def format_related_items(index)
174
199
  return related.join(", ") unless index
175
200
 
@@ -24,58 +24,21 @@ module CF
24
24
  ).compact
25
25
  end
26
26
 
27
- def to_text(detailed: false, index: nil)
27
+ protected
28
+
29
+ def build_type_specific_lines
30
+ return [] unless entries && !entries.empty?
31
+
28
32
  lines = []
29
- lines << "# #{name}"
33
+ lines << "## Values"
30
34
  lines << ""
31
- lines << "- **Type:** enum"
32
- lines << "- **Category:** #{category}" if category
33
- if source_file
34
- urls = source_urls
35
- lines << "- **Source:** [include/#{source_file}](#{urls[:blob]})"
36
- lines << "- **Raw:** #{urls[:raw]}"
37
- lines << "- **Implementation:** #{urls[:impl_raw]}"
35
+ lines << "| Name | Value | Description |"
36
+ lines << "| --- | --- | --- |"
37
+ entries.each do |entry|
38
+ lines << "| `#{entry.name}` | #{entry.value} | #{entry.description} |"
38
39
  end
39
40
  lines << ""
40
- lines << "## Description"
41
- lines << brief if brief
42
- lines << ""
43
-
44
- if detailed
45
- if entries && !entries.empty?
46
- lines << "## Values"
47
- lines << ""
48
- lines << "| Name | Value | Description |"
49
- lines << "| --- | --- | --- |"
50
- entries.each do |entry|
51
- lines << "| `#{entry.name}` | #{entry.value} | #{entry.description} |"
52
- end
53
- lines << ""
54
- end
55
-
56
- if remarks && !remarks.empty?
57
- lines << "## Remarks"
58
- lines << remarks
59
- lines << ""
60
- end
61
-
62
- if example && !example.empty?
63
- lines << "## Example"
64
- lines << example_brief if example_brief
65
- lines << "```c"
66
- lines << example
67
- lines << "```"
68
- lines << ""
69
- end
70
-
71
- if related && !related.empty?
72
- lines << "## Related"
73
- lines << format_related_items(index)
74
- lines << ""
75
- end
76
- end
77
-
78
- lines.join("\n")
41
+ lines
79
42
  end
80
43
  end
81
44
  end
@@ -38,71 +38,48 @@ module CF
38
38
 
39
39
  def to_text(detailed: false, index: nil)
40
40
  lines = []
41
- lines << "# #{name}"
42
- lines << ""
43
- lines << "- **Type:** function"
44
- lines << "- **Category:** #{category}" if category
45
- if source_file
46
- urls = source_urls
47
- lines << "- **Source:** [include/#{source_file}](#{urls[:blob]})"
48
- lines << "- **Raw:** #{urls[:raw]}"
49
- lines << "- **Implementation:** #{urls[:impl_raw]}"
50
- end
51
- lines << ""
41
+ lines.concat(build_header_lines)
42
+ lines.concat(build_signature_lines)
43
+ lines.concat(build_description_lines)
52
44
 
53
- if signature
54
- lines << "## Signature"
55
- lines << "```c"
56
- lines << signature
57
- lines << "```"
58
- lines << ""
45
+ if detailed
46
+ lines.concat(build_type_specific_lines)
47
+ lines.concat(build_remarks_lines)
48
+ lines.concat(build_example_lines)
49
+ lines.concat(build_related_lines(index))
59
50
  end
60
51
 
61
- lines << "## Description"
62
- lines << brief if brief
63
- lines << ""
52
+ lines.join("\n")
53
+ end
64
54
 
65
- if detailed
66
- if parameters && !parameters.empty?
67
- lines << "## Parameters"
68
- lines << ""
69
- lines << "| Parameter | Description |"
70
- lines << "| --- | --- |"
71
- parameters.each do |param|
72
- lines << "| `#{param.name}` | #{param.description} |"
73
- end
74
- lines << ""
75
- end
55
+ protected
76
56
 
77
- if return_value && !return_value.empty?
78
- lines << "## Return Value"
79
- lines << return_value
80
- lines << ""
81
- end
57
+ def build_signature_lines
58
+ return [] unless signature
59
+ ["## Signature", "```c", signature, "```", ""]
60
+ end
82
61
 
83
- if remarks && !remarks.empty?
84
- lines << "## Remarks"
85
- lines << remarks
86
- lines << ""
87
- end
62
+ def build_type_specific_lines
63
+ lines = []
88
64
 
89
- if example && !example.empty?
90
- lines << "## Example"
91
- lines << example_brief if example_brief
92
- lines << "```c"
93
- lines << example
94
- lines << "```"
95
- lines << ""
65
+ if parameters && !parameters.empty?
66
+ lines << "## Parameters"
67
+ lines << ""
68
+ lines << "| Parameter | Description |"
69
+ lines << "| --- | --- |"
70
+ parameters.each do |param|
71
+ lines << "| `#{param.name}` | #{param.description} |"
96
72
  end
73
+ lines << ""
74
+ end
97
75
 
98
- if related && !related.empty?
99
- lines << "## Related"
100
- lines << format_related_items(index)
101
- lines << ""
102
- end
76
+ if return_value && !return_value.empty?
77
+ lines << "## Return Value"
78
+ lines << return_value
79
+ lines << ""
103
80
  end
104
81
 
105
- lines.join("\n")
82
+ lines
106
83
  end
107
84
  end
108
85
  end
@@ -24,58 +24,21 @@ module CF
24
24
  ).compact
25
25
  end
26
26
 
27
- def to_text(detailed: false, index: nil)
27
+ protected
28
+
29
+ def build_type_specific_lines
30
+ return [] unless members && !members.empty?
31
+
28
32
  lines = []
29
- lines << "# #{name}"
33
+ lines << "## Members"
30
34
  lines << ""
31
- lines << "- **Type:** struct"
32
- lines << "- **Category:** #{category}" if category
33
- if source_file
34
- urls = source_urls
35
- lines << "- **Source:** [include/#{source_file}](#{urls[:blob]})"
36
- lines << "- **Raw:** #{urls[:raw]}"
37
- lines << "- **Implementation:** #{urls[:impl_raw]}"
35
+ lines << "| Member | Description |"
36
+ lines << "| --- | --- |"
37
+ members.each do |member|
38
+ lines << "| `#{member.declaration}` | #{member.description} |"
38
39
  end
39
40
  lines << ""
40
- lines << "## Description"
41
- lines << brief if brief
42
- lines << ""
43
-
44
- if detailed
45
- if members && !members.empty?
46
- lines << "## Members"
47
- lines << ""
48
- lines << "| Member | Description |"
49
- lines << "| --- | --- |"
50
- members.each do |member|
51
- lines << "| `#{member.declaration}` | #{member.description} |"
52
- end
53
- lines << ""
54
- end
55
-
56
- if remarks && !remarks.empty?
57
- lines << "## Remarks"
58
- lines << remarks
59
- lines << ""
60
- end
61
-
62
- if example && !example.empty?
63
- lines << "## Example"
64
- lines << example_brief if example_brief
65
- lines << "```c"
66
- lines << example
67
- lines << "```"
68
- lines << ""
69
- end
70
-
71
- if related && !related.empty?
72
- lines << "## Related"
73
- lines << format_related_items(index)
74
- lines << ""
75
- end
76
- end
77
-
78
- lines.join("\n")
41
+ lines
79
42
  end
80
43
  end
81
44
  end
data/lib/cf/mcp/server.rb CHANGED
@@ -91,86 +91,23 @@ module CF
91
91
  # Build a rack app with automatic header downloading and indexing
92
92
  # This is the shared entry point for both config.ru and CLI
93
93
  def self.build_rack_app(root: nil, download: false)
94
- require_relative "parser"
95
- require_relative "topic_parser"
96
- require_relative "index"
97
- require_relative "downloader"
94
+ require_relative "index_builder"
98
95
 
99
- headers_path = resolve_headers_path(root: root, download: download)
96
+ builder = IndexBuilder.new(root: root, download: download)
100
97
 
101
- unless File.directory?(headers_path)
102
- raise "Headers directory not found: #{headers_path}. Use root: or download: true"
98
+ unless builder.valid?
99
+ raise "Headers directory not found: #{builder.headers_path}. Use root: or download: true"
103
100
  end
104
101
 
105
- warn "Parsing headers from: #{headers_path}"
106
- index = build_index(headers_path)
102
+ warn "Parsing headers from: #{builder.headers_path}"
103
+ index = builder.build do |event, path, count|
104
+ warn "Indexed #{count} topics from: #{path}" if event == :topics_indexed
105
+ end
107
106
  warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
108
107
 
109
108
  new(index).rack_app
110
109
  end
111
110
 
112
- def self.resolve_headers_path(root:, download:)
113
- return root if root
114
- return ENV["CF_HEADERS_PATH"] if ENV["CF_HEADERS_PATH"]
115
-
116
- if download
117
- warn "Downloading Cute Framework headers from GitHub..."
118
- downloader = Downloader.new
119
- path = downloader.download_and_extract
120
- warn "Downloaded headers to: #{path}"
121
- return path
122
- end
123
-
124
- File.expand_path("~/Work/GitHub/pusewicz/cute_framework/include")
125
- end
126
-
127
- def self.build_index(headers_path)
128
- parser = Parser.new
129
- index = Index.new
130
-
131
- parser.parse_directory(headers_path).each do |item|
132
- index.add(item)
133
- end
134
-
135
- # Parse topics if available
136
- topics_path = find_topics_path(headers_path)
137
- if topics_path && File.directory?(topics_path)
138
- topic_parser = TopicParser.new
139
- topic_parser.parse_directory(topics_path).each do |topic|
140
- refine_topic_references(topic, index)
141
- index.add(topic)
142
- end
143
- warn "Indexed #{index.stats[:topics]} topics from: #{topics_path}"
144
- end
145
-
146
- index
147
- end
148
-
149
- def self.find_topics_path(headers_path)
150
- base = File.dirname(headers_path)
151
- topics_path = File.join(base, "docs", "topics")
152
- return topics_path if File.directory?(topics_path)
153
-
154
- topics_path = File.join(base, "topics")
155
- return topics_path if File.directory?(topics_path)
156
-
157
- nil
158
- end
159
-
160
- def self.refine_topic_references(topic, index)
161
- topic.struct_references.dup.each do |ref|
162
- item = index.find(ref)
163
- next unless item
164
-
165
- if item.type == :enum
166
- topic.struct_references.delete(ref)
167
- topic.enum_references << ref unless topic.enum_references.include?(ref)
168
- end
169
- end
170
- end
171
-
172
- private_class_method :resolve_headers_path, :build_index, :find_topics_path, :refine_topic_references
173
-
174
111
  def initialize(index)
175
112
  @index = index
176
113
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
4
5
 
5
6
  module CF
6
7
  module MCP
7
8
  module Tools
8
9
  class FindRelated < ::MCP::Tool
10
+ extend ResponseHelpers
11
+
9
12
  tool_name "cf_find_related"
10
13
  description "Find all items related to a given Cute Framework item (bidirectional relationship search)"
11
14
 
@@ -63,14 +66,6 @@ module CF
63
66
 
64
67
  text_response(lines.join("\n"))
65
68
  end
66
-
67
- def self.text_response(text)
68
- ::MCP::Tool::Response.new([{type: "text", text: text}])
69
- end
70
-
71
- def self.error_response(message)
72
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
73
- end
74
69
  end
75
70
  end
76
71
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
4
5
 
5
6
  module CF
6
7
  module MCP
7
8
  module Tools
8
9
  class GetDetails < ::MCP::Tool
10
+ extend ResponseHelpers
11
+
9
12
  tool_name "cf_get_details"
10
13
  description "Get detailed documentation for a specific Cute Framework item by exact name"
11
14
 
@@ -50,14 +53,6 @@ module CF
50
53
  text_response(output)
51
54
  end
52
55
  end
53
-
54
- def self.text_response(text)
55
- ::MCP::Tool::Response.new([{type: "text", text: text}])
56
- end
57
-
58
- def self.error_response(message)
59
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
60
- end
61
56
  end
62
57
  end
63
58
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
4
5
 
5
6
  module CF
6
7
  module MCP
7
8
  module Tools
8
9
  class GetTopic < ::MCP::Tool
10
+ extend ResponseHelpers
11
+
9
12
  tool_name "cf_get_topic"
10
13
  description "Get the full content of a Cute Framework topic guide document"
11
14
 
@@ -39,14 +42,6 @@ module CF
39
42
  text_response(topic.to_text(detailed: true, index: index))
40
43
  end
41
44
  end
42
-
43
- def self.text_response(text)
44
- ::MCP::Tool::Response.new([{type: "text", text: text}])
45
- end
46
-
47
- def self.error_response(message)
48
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
49
- end
50
45
  end
51
46
  end
52
47
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
4
5
 
5
6
  module CF
6
7
  module MCP
7
8
  module Tools
8
9
  class ListCategory < ::MCP::Tool
10
+ extend ResponseHelpers
11
+
9
12
  tool_name "cf_list_category"
10
13
  description "List all items in a specific category, or list all available categories"
11
14
 
@@ -63,14 +66,6 @@ module CF
63
66
  end
64
67
  end
65
68
  end
66
-
67
- def self.text_response(text)
68
- ::MCP::Tool::Response.new([{type: "text", text: text}])
69
- end
70
-
71
- def self.error_response(message)
72
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
73
- end
74
69
  end
75
70
  end
76
71
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
4
5
 
5
6
  module CF
6
7
  module MCP
7
8
  module Tools
8
9
  class ListTopics < ::MCP::Tool
10
+ extend ResponseHelpers
11
+
9
12
  tool_name "cf_list_topics"
10
13
  description "List all Cute Framework topic guides, optionally filtered by category or in recommended reading order"
11
14
 
@@ -50,14 +53,6 @@ module CF
50
53
  end
51
54
 
52
55
  CATEGORY_TIP = "Use `cf_list_topics` without a category to see all available topics."
53
-
54
- def self.text_response(text)
55
- ::MCP::Tool::Response.new([{type: "text", text: text}])
56
- end
57
-
58
- def self.error_response(message)
59
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
60
- end
61
56
  end
62
57
  end
63
58
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
4
5
 
5
6
  module CF
6
7
  module MCP
7
8
  module Tools
8
9
  class MemberSearch < ::MCP::Tool
10
+ extend ResponseHelpers
11
+
9
12
  tool_name "cf_member_search"
10
13
  description "Search Cute Framework structs by member name or type"
11
14
 
@@ -62,14 +65,6 @@ module CF
62
65
 
63
66
  text_response(lines.join("\n"))
64
67
  end
65
-
66
- def self.text_response(text)
67
- ::MCP::Tool::Response.new([{type: "text", text: text}])
68
- end
69
-
70
- def self.error_response(message)
71
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
72
- end
73
68
  end
74
69
  end
75
70
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
4
5
 
5
6
  module CF
6
7
  module MCP
7
8
  module Tools
8
9
  class ParameterSearch < ::MCP::Tool
10
+ extend ResponseHelpers
11
+
9
12
  tool_name "cf_parameter_search"
10
13
  description "Find Cute Framework functions by parameter or return type"
11
14
 
@@ -88,14 +91,6 @@ module CF
88
91
 
89
92
  text_response(lines.join("\n"))
90
93
  end
91
-
92
- def self.text_response(text)
93
- ::MCP::Tool::Response.new([{type: "text", text: text}])
94
- end
95
-
96
- def self.error_response(message)
97
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
98
- end
99
94
  end
100
95
  end
101
96
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+
5
+ module CF
6
+ module MCP
7
+ module Tools
8
+ module ResponseHelpers
9
+ def text_response(text)
10
+ ::MCP::Tool::Response.new([{type: "text", text: text}])
11
+ end
12
+
13
+ def error_response(message)
14
+ ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
5
+ require_relative "search_result_formatter"
4
6
 
5
7
  module CF
6
8
  module MCP
7
9
  module Tools
8
10
  class SearchEnums < ::MCP::Tool
11
+ extend ResponseHelpers
12
+ extend SearchResultFormatter
13
+
9
14
  tool_name "cf_search_enums"
10
15
  description "Search Cute Framework enums"
11
16
 
@@ -27,29 +32,15 @@ module CF
27
32
 
28
33
  results = index.search(query, type: :enum, category: category, limit: limit)
29
34
 
30
- if results.empty?
31
- text_response("No enums found for '#{query}'")
32
- else
33
- formatted = results.map(&:to_summary).join("\n")
34
- header = if results.size >= limit
35
- "Found #{results.size} enum(s) (limit reached, more may exist):"
36
- else
37
- "Found #{results.size} enum(s):"
38
- end
39
-
40
- footer = "\n\n#{DETAILS_TIP}"
41
- footer += "\nTo find more results, narrow your search with a `category` filter." if results.size >= limit
42
-
43
- text_response("#{header}\n\n#{formatted}#{footer}")
44
- end
45
- end
46
-
47
- def self.text_response(text)
48
- ::MCP::Tool::Response.new([{type: "text", text: text}])
49
- end
50
-
51
- def self.error_response(message)
52
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
35
+ text = format_search_results(
36
+ results,
37
+ query: query,
38
+ type_label: "enum(s)",
39
+ limit: limit,
40
+ details_tip: DETAILS_TIP,
41
+ filter_suggestion: "To find more results, narrow your search with a `category` filter."
42
+ )
43
+ text_response(text)
53
44
  end
54
45
  end
55
46
  end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
5
+ require_relative "search_result_formatter"
4
6
 
5
7
  module CF
6
8
  module MCP
7
9
  module Tools
8
10
  class SearchFunctions < ::MCP::Tool
11
+ extend ResponseHelpers
12
+ extend SearchResultFormatter
13
+
9
14
  tool_name "cf_search_functions"
10
15
  description "Search Cute Framework functions"
11
16
 
@@ -27,29 +32,15 @@ module CF
27
32
 
28
33
  results = index.search(query, type: :function, category: category, limit: limit)
29
34
 
30
- if results.empty?
31
- text_response("No functions found for '#{query}'")
32
- else
33
- formatted = results.map(&:to_summary).join("\n")
34
- header = if results.size >= limit
35
- "Found #{results.size} function(s) (limit reached, more may exist):"
36
- else
37
- "Found #{results.size} function(s):"
38
- end
39
-
40
- footer = "\n\n#{DETAILS_TIP}"
41
- footer += "\nTo find more results, narrow your search with a `category` filter." if results.size >= limit
42
-
43
- text_response("#{header}\n\n#{formatted}#{footer}")
44
- end
45
- end
46
-
47
- def self.text_response(text)
48
- ::MCP::Tool::Response.new([{type: "text", text: text}])
49
- end
50
-
51
- def self.error_response(message)
52
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
35
+ text = format_search_results(
36
+ results,
37
+ query: query,
38
+ type_label: "function(s)",
39
+ limit: limit,
40
+ details_tip: DETAILS_TIP,
41
+ filter_suggestion: "To find more results, narrow your search with a `category` filter."
42
+ )
43
+ text_response(text)
53
44
  end
54
45
  end
55
46
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CF
4
+ module MCP
5
+ module Tools
6
+ module SearchResultFormatter
7
+ def format_search_results(results, query:, type_label:, limit:, details_tip:, filter_suggestion: nil)
8
+ if results.empty?
9
+ # Use plural form for "no results" message (e.g., "No functions found")
10
+ plural_label = type_label.sub(/\(s\)$/, "s")
11
+ return "No #{plural_label} found for '#{query}'"
12
+ end
13
+
14
+ formatted = results.map(&:to_summary).join("\n")
15
+
16
+ header = if results.size >= limit
17
+ "Found #{results.size} #{type_label} (limit reached, more may exist):"
18
+ else
19
+ "Found #{results.size} #{type_label}:"
20
+ end
21
+
22
+ footer = "\n\n#{details_tip}"
23
+ footer += "\n#{filter_suggestion}" if results.size >= limit && filter_suggestion
24
+
25
+ "#{header}\n\n#{formatted}#{footer}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
5
+ require_relative "search_result_formatter"
4
6
 
5
7
  module CF
6
8
  module MCP
7
9
  module Tools
8
10
  class SearchStructs < ::MCP::Tool
11
+ extend ResponseHelpers
12
+ extend SearchResultFormatter
13
+
9
14
  tool_name "cf_search_structs"
10
15
  description "Search Cute Framework structs"
11
16
 
@@ -27,29 +32,15 @@ module CF
27
32
 
28
33
  results = index.search(query, type: :struct, category: category, limit: limit)
29
34
 
30
- if results.empty?
31
- text_response("No structs found for '#{query}'")
32
- else
33
- formatted = results.map(&:to_summary).join("\n")
34
- header = if results.size >= limit
35
- "Found #{results.size} struct(s) (limit reached, more may exist):"
36
- else
37
- "Found #{results.size} struct(s):"
38
- end
39
-
40
- footer = "\n\n#{DETAILS_TIP}"
41
- footer += "\nTo find more results, narrow your search with a `category` filter." if results.size >= limit
42
-
43
- text_response("#{header}\n\n#{formatted}#{footer}")
44
- end
45
- end
46
-
47
- def self.text_response(text)
48
- ::MCP::Tool::Response.new([{type: "text", text: text}])
49
- end
50
-
51
- def self.error_response(message)
52
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
35
+ text = format_search_results(
36
+ results,
37
+ query: query,
38
+ type_label: "struct(s)",
39
+ limit: limit,
40
+ details_tip: DETAILS_TIP,
41
+ filter_suggestion: "To find more results, narrow your search with a `category` filter."
42
+ )
43
+ text_response(text)
53
44
  end
54
45
  end
55
46
  end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
+ require_relative "response_helpers"
5
+ require_relative "search_result_formatter"
4
6
 
5
7
  module CF
6
8
  module MCP
7
9
  module Tools
8
10
  class SearchTool < ::MCP::Tool
11
+ extend ResponseHelpers
12
+ extend SearchResultFormatter
13
+
9
14
  tool_name "cf_search"
10
15
  description "Search Cute Framework documentation across all types (functions, structs, enums, topics)"
11
16
 
@@ -28,29 +33,15 @@ module CF
28
33
 
29
34
  results = index.search(query, type: type, category: category, limit: limit)
30
35
 
31
- if results.empty?
32
- text_response("No results found for '#{query}'")
33
- else
34
- formatted = results.map(&:to_summary).join("\n")
35
- header = if results.size >= limit
36
- "Found #{results.size} result(s) (limit reached, more may exist):"
37
- else
38
- "Found #{results.size} result(s):"
39
- end
40
-
41
- footer = "\n\n#{DETAILS_TIP}"
42
- footer += "\nTo find more results, narrow your search with `type` or `category` filters." if results.size >= limit
43
-
44
- text_response("#{header}\n\n#{formatted}#{footer}")
45
- end
46
- end
47
-
48
- def self.text_response(text)
49
- ::MCP::Tool::Response.new([{type: "text", text: text}])
50
- end
51
-
52
- def self.error_response(message)
53
- ::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
36
+ text = format_search_results(
37
+ results,
38
+ query: query,
39
+ type_label: "result(s)",
40
+ limit: limit,
41
+ details_tip: DETAILS_TIP,
42
+ filter_suggestion: "To find more results, narrow your search with `type` or `category` filters."
43
+ )
44
+ text_response(text)
54
45
  end
55
46
  end
56
47
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CF
4
4
  module MCP
5
- VERSION = "0.12.0"
5
+ VERSION = "0.12.1"
6
6
  end
7
7
  end
data/lib/cf/mcp.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "mcp/models/struct_doc"
8
8
  require_relative "mcp/models/enum_doc"
9
9
  require_relative "mcp/parser"
10
10
  require_relative "mcp/index"
11
+ require_relative "mcp/index_builder"
11
12
  require_relative "mcp/server"
12
13
  require_relative "mcp/downloader"
13
14
  require_relative "mcp/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cf-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Usewicz
@@ -99,6 +99,7 @@ files:
99
99
  - lib/cf/mcp/cli.rb
100
100
  - lib/cf/mcp/downloader.rb
101
101
  - lib/cf/mcp/index.rb
102
+ - lib/cf/mcp/index_builder.rb
102
103
  - lib/cf/mcp/models/doc_item.rb
103
104
  - lib/cf/mcp/models/enum_doc.rb
104
105
  - lib/cf/mcp/models/function_doc.rb
@@ -116,8 +117,10 @@ files:
116
117
  - lib/cf/mcp/tools/list_topics.rb
117
118
  - lib/cf/mcp/tools/member_search.rb
118
119
  - lib/cf/mcp/tools/parameter_search.rb
120
+ - lib/cf/mcp/tools/response_helpers.rb
119
121
  - lib/cf/mcp/tools/search_enums.rb
120
122
  - lib/cf/mcp/tools/search_functions.rb
123
+ - lib/cf/mcp/tools/search_result_formatter.rb
121
124
  - lib/cf/mcp/tools/search_structs.rb
122
125
  - lib/cf/mcp/tools/search_tool.rb
123
126
  - lib/cf/mcp/topic_parser.rb