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 +7 -0
- data/LICENSE.txt +21 -0
- data/Manifest.txt +34 -0
- data/README.md +52 -0
- data/Rakefile +53 -0
- data/config.ru +26 -0
- data/exe/cf-mcp +6 -0
- data/lib/cf/mcp/cli.rb +211 -0
- data/lib/cf/mcp/downloader.rb +112 -0
- data/lib/cf/mcp/index.rb +128 -0
- data/lib/cf/mcp/models/doc_item.rb +171 -0
- data/lib/cf/mcp/models/enum_doc.rb +83 -0
- data/lib/cf/mcp/models/function_doc.rb +110 -0
- data/lib/cf/mcp/models/struct_doc.rb +83 -0
- data/lib/cf/mcp/models/topic_doc.rb +113 -0
- data/lib/cf/mcp/parser.rb +246 -0
- data/lib/cf/mcp/server.rb +316 -0
- data/lib/cf/mcp/templates/index.erb +94 -0
- data/lib/cf/mcp/templates/script.js +292 -0
- data/lib/cf/mcp/templates/style.css +165 -0
- data/lib/cf/mcp/tools/find_related.rb +77 -0
- data/lib/cf/mcp/tools/get_details.rb +64 -0
- data/lib/cf/mcp/tools/get_topic.rb +53 -0
- data/lib/cf/mcp/tools/list_category.rb +77 -0
- data/lib/cf/mcp/tools/list_topics.rb +64 -0
- data/lib/cf/mcp/tools/member_search.rb +76 -0
- data/lib/cf/mcp/tools/parameter_search.rb +102 -0
- data/lib/cf/mcp/tools/search_enums.rb +57 -0
- data/lib/cf/mcp/tools/search_functions.rb +57 -0
- data/lib/cf/mcp/tools/search_structs.rb +57 -0
- data/lib/cf/mcp/tools/search_tool.rb +58 -0
- data/lib/cf/mcp/topic_parser.rb +199 -0
- data/lib/cf/mcp/version.rb +7 -0
- data/lib/cf/mcp.rb +23 -0
- data/sig/cf/mcp.rbs +84 -0
- metadata +150 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class ListCategory < ::MCP::Tool
|
|
9
|
+
tool_name "cf_list_category"
|
|
10
|
+
description "List all items in a specific category, or list all available categories"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
category: {type: "string", description: "Category name (e.g., 'app', 'sprite', 'graphics'). Leave empty to list all categories."},
|
|
16
|
+
type: {type: "string", enum: ["function", "struct", "enum"], description: "Optional: filter by item type"}
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def self.call(category: nil, type: nil, server_context: {})
|
|
21
|
+
index = server_context[:index]
|
|
22
|
+
return error_response("Index not available") unless index
|
|
23
|
+
|
|
24
|
+
if category.nil? || category.empty?
|
|
25
|
+
# List all categories with counts by type
|
|
26
|
+
categories = index.categories
|
|
27
|
+
if categories.empty?
|
|
28
|
+
text_response("No categories found")
|
|
29
|
+
else
|
|
30
|
+
formatted = categories.map do |cat|
|
|
31
|
+
items = index.items_in_category(cat)
|
|
32
|
+
counts = items.group_by(&:type).transform_values(&:size)
|
|
33
|
+
type_breakdown = [:function, :struct, :enum]
|
|
34
|
+
.filter_map { |t| "#{counts[t]} #{t}s" if counts[t]&.positive? }
|
|
35
|
+
.join(", ")
|
|
36
|
+
"- **#{cat}** — #{items.size} items (#{type_breakdown})"
|
|
37
|
+
end.join("\n")
|
|
38
|
+
text_response("Available categories:\n\n#{formatted}\n\n**Tip:** Use `cf_list_category` with a category name to see all items in that category.")
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
# List items in the specified category
|
|
42
|
+
items = index.items_in_category(category)
|
|
43
|
+
|
|
44
|
+
if type
|
|
45
|
+
type_sym = type.to_sym
|
|
46
|
+
items = items.select { |item| item.type == type_sym }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if items.empty?
|
|
50
|
+
text_response("No items found in category '#{category}'#{" of type #{type}" if type}")
|
|
51
|
+
else
|
|
52
|
+
formatted = items.map(&:to_summary).join("\n")
|
|
53
|
+
|
|
54
|
+
# Suggest related topics
|
|
55
|
+
related_topics = index.topics.select { |t| t.category == category }
|
|
56
|
+
topic_suggestion = if related_topics.any?
|
|
57
|
+
"\n\n**Related Topics:**\n" + related_topics.map { |t| "- **#{t.name}** — #{t.brief}" }.join("\n")
|
|
58
|
+
else
|
|
59
|
+
""
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
text_response("Items in '#{category}':\n\n#{formatted}#{topic_suggestion}\n\n**Tip:** Use `cf_get_details` with an exact name to get full documentation.")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
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
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class ListTopics < ::MCP::Tool
|
|
9
|
+
tool_name "cf_list_topics"
|
|
10
|
+
description "List all Cute Framework topic guides, optionally filtered by category or in recommended reading order"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
category: {type: "string", description: "Optional: filter topics by category (e.g., 'audio', 'draw', 'graphics')"},
|
|
16
|
+
ordered: {type: "boolean", description: "If true, return topics in recommended reading order (default: false)"}
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def self.call(category: nil, ordered: false, server_context: {})
|
|
21
|
+
index = server_context[:index]
|
|
22
|
+
return error_response("Index not available") unless index
|
|
23
|
+
|
|
24
|
+
topics = ordered ? index.topics_ordered : index.topics
|
|
25
|
+
|
|
26
|
+
if category
|
|
27
|
+
topics = topics.select { |t| t.category == category }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if topics.empty?
|
|
31
|
+
return text_response("No topics found#{" in category '#{category}'" if category}\n\n#{CATEGORY_TIP}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
lines = ["# Cute Framework Topics", ""]
|
|
35
|
+
|
|
36
|
+
if ordered
|
|
37
|
+
lines << "_Listed in recommended reading order_"
|
|
38
|
+
lines << ""
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
topics.each_with_index do |topic, i|
|
|
42
|
+
prefix = (ordered && topic.reading_order) ? "#{i + 1}. " : "- "
|
|
43
|
+
lines << "#{prefix}**#{topic.name}** — #{topic.brief}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
lines << ""
|
|
47
|
+
lines << "**Tip:** Use `cf_get_topic` with a topic name to read the full content."
|
|
48
|
+
|
|
49
|
+
text_response(lines.join("\n"))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
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
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class MemberSearch < ::MCP::Tool
|
|
9
|
+
tool_name "cf_member_search"
|
|
10
|
+
description "Search Cute Framework structs by member name or type"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
query: {type: "string", description: "Search query (matches member name or type in declaration)"},
|
|
16
|
+
limit: {type: "integer", description: "Maximum number of results to return (default: 20)"}
|
|
17
|
+
},
|
|
18
|
+
required: ["query"]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def self.call(query:, limit: 20, server_context: {})
|
|
22
|
+
index = server_context[:index]
|
|
23
|
+
return error_response("Index not available") unless index
|
|
24
|
+
|
|
25
|
+
pattern = Regexp.new(Regexp.escape(query), Regexp::IGNORECASE)
|
|
26
|
+
results = []
|
|
27
|
+
|
|
28
|
+
index.structs.each do |struct|
|
|
29
|
+
next unless struct.members&.any?
|
|
30
|
+
|
|
31
|
+
matching_members = struct.members.select { |member|
|
|
32
|
+
member.declaration&.match?(pattern)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
next if matching_members.empty?
|
|
36
|
+
|
|
37
|
+
results << {struct: struct, members: matching_members}
|
|
38
|
+
break if results.size >= limit
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if results.empty?
|
|
42
|
+
return text_response("No structs found with members matching '#{query}'")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
lines = ["# Structs with members matching '#{query}'", ""]
|
|
46
|
+
|
|
47
|
+
results.each do |result|
|
|
48
|
+
struct = result[:struct]
|
|
49
|
+
lines << "- **#{struct.name}** (#{struct.category}) — #{struct.brief}"
|
|
50
|
+
result[:members].each do |member|
|
|
51
|
+
lines << " - `#{member.declaration}` — #{member.description}"
|
|
52
|
+
end
|
|
53
|
+
lines << ""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if results.size >= limit
|
|
57
|
+
lines << "_Results limited to #{limit}. Narrow your search for more specific results._"
|
|
58
|
+
lines << ""
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
lines << "**Tip:** Use `cf_get_details` with a struct name for full documentation."
|
|
62
|
+
|
|
63
|
+
text_response(lines.join("\n"))
|
|
64
|
+
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
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class ParameterSearch < ::MCP::Tool
|
|
9
|
+
tool_name "cf_parameter_search"
|
|
10
|
+
description "Find Cute Framework functions by parameter or return type"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
type: {type: "string", description: "Type name to search for (e.g., 'CF_Sprite', 'const char*', 'int')"},
|
|
16
|
+
direction: {
|
|
17
|
+
type: "string",
|
|
18
|
+
enum: ["input", "output", "both"],
|
|
19
|
+
description: "Search direction: 'input' for parameters, 'output' for return types, 'both' for either (default: both)"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
required: ["type"]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def self.call(type:, direction: "both", server_context: {})
|
|
26
|
+
index = server_context[:index]
|
|
27
|
+
return error_response("Index not available") unless index
|
|
28
|
+
|
|
29
|
+
pattern = Regexp.new(Regexp.escape(type), Regexp::IGNORECASE)
|
|
30
|
+
input_matches = []
|
|
31
|
+
output_matches = []
|
|
32
|
+
|
|
33
|
+
index.functions.each do |func|
|
|
34
|
+
next unless func.signature
|
|
35
|
+
|
|
36
|
+
# Check return type (text before function name in signature)
|
|
37
|
+
if direction != "input"
|
|
38
|
+
# Extract return type: everything before the function name
|
|
39
|
+
if func.signature =~ /^(.+?)\s+#{Regexp.escape(func.name)}\s*\(/
|
|
40
|
+
return_type = ::Regexp.last_match(1).strip
|
|
41
|
+
if return_type.match?(pattern)
|
|
42
|
+
output_matches << func
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check input parameters
|
|
48
|
+
if direction != "output"
|
|
49
|
+
# Check the signature for parameter types
|
|
50
|
+
if func.signature =~ /\(([^)]*)\)/
|
|
51
|
+
params_str = ::Regexp.last_match(1)
|
|
52
|
+
if params_str.match?(pattern)
|
|
53
|
+
input_matches << func unless input_matches.include?(func)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Remove duplicates between input and output
|
|
60
|
+
input_matches.uniq!
|
|
61
|
+
output_matches.uniq!
|
|
62
|
+
|
|
63
|
+
if input_matches.empty? && output_matches.empty?
|
|
64
|
+
return text_response("No functions found using type '#{type}'")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
lines = ["# Functions using '#{type}'", ""]
|
|
68
|
+
|
|
69
|
+
unless input_matches.empty?
|
|
70
|
+
lines << "## Takes as input (#{input_matches.size})"
|
|
71
|
+
input_matches.each do |func|
|
|
72
|
+
lines << "- **#{func.name}** — #{func.brief}"
|
|
73
|
+
lines << " `#{func.signature}`" if func.signature
|
|
74
|
+
end
|
|
75
|
+
lines << ""
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless output_matches.empty?
|
|
79
|
+
lines << "## Returns (#{output_matches.size})"
|
|
80
|
+
output_matches.each do |func|
|
|
81
|
+
lines << "- **#{func.name}** — #{func.brief}"
|
|
82
|
+
lines << " `#{func.signature}`" if func.signature
|
|
83
|
+
end
|
|
84
|
+
lines << ""
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
lines << "**Tip:** Use `cf_get_details` with a function name for full documentation."
|
|
88
|
+
|
|
89
|
+
text_response(lines.join("\n"))
|
|
90
|
+
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
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class SearchEnums < ::MCP::Tool
|
|
9
|
+
tool_name "cf_search_enums"
|
|
10
|
+
description "Search Cute Framework enums"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
query: {type: "string", description: "Search query"},
|
|
16
|
+
category: {type: "string", description: "Optional: filter by category"},
|
|
17
|
+
limit: {type: "integer", description: "Maximum results (default: 20)"}
|
|
18
|
+
},
|
|
19
|
+
required: ["query"]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DETAILS_TIP = "**Tip:** Use `cf_get_details` with an exact name to get full documentation including values and examples."
|
|
23
|
+
|
|
24
|
+
def self.call(query:, category: nil, limit: 20, server_context: {})
|
|
25
|
+
index = server_context[:index]
|
|
26
|
+
return error_response("Index not available") unless index
|
|
27
|
+
|
|
28
|
+
results = index.search(query, type: :enum, category: category, limit: limit)
|
|
29
|
+
|
|
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)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class SearchFunctions < ::MCP::Tool
|
|
9
|
+
tool_name "cf_search_functions"
|
|
10
|
+
description "Search Cute Framework functions"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
query: {type: "string", description: "Search query"},
|
|
16
|
+
category: {type: "string", description: "Optional: filter by category"},
|
|
17
|
+
limit: {type: "integer", description: "Maximum results (default: 20)"}
|
|
18
|
+
},
|
|
19
|
+
required: ["query"]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DETAILS_TIP = "**Tip:** Use `cf_get_details` with an exact name to get full documentation including signature, parameters, and examples."
|
|
23
|
+
|
|
24
|
+
def self.call(query:, category: nil, limit: 20, server_context: {})
|
|
25
|
+
index = server_context[:index]
|
|
26
|
+
return error_response("Index not available") unless index
|
|
27
|
+
|
|
28
|
+
results = index.search(query, type: :function, category: category, limit: limit)
|
|
29
|
+
|
|
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)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class SearchStructs < ::MCP::Tool
|
|
9
|
+
tool_name "cf_search_structs"
|
|
10
|
+
description "Search Cute Framework structs"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
query: {type: "string", description: "Search query"},
|
|
16
|
+
category: {type: "string", description: "Optional: filter by category"},
|
|
17
|
+
limit: {type: "integer", description: "Maximum results (default: 20)"}
|
|
18
|
+
},
|
|
19
|
+
required: ["query"]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DETAILS_TIP = "**Tip:** Use `cf_get_details` with an exact name to get full documentation including members and examples."
|
|
23
|
+
|
|
24
|
+
def self.call(query:, category: nil, limit: 20, server_context: {})
|
|
25
|
+
index = server_context[:index]
|
|
26
|
+
return error_response("Index not available") unless index
|
|
27
|
+
|
|
28
|
+
results = index.search(query, type: :struct, category: category, limit: limit)
|
|
29
|
+
|
|
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)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class SearchTool < ::MCP::Tool
|
|
9
|
+
tool_name "cf_search"
|
|
10
|
+
description "Search Cute Framework documentation across all types (functions, structs, enums, topics)"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
query: {type: "string", description: "Search query (searches in name, description, and remarks)"},
|
|
16
|
+
type: {type: "string", enum: ["function", "struct", "enum", "topic"], description: "Optional: filter by item type"},
|
|
17
|
+
category: {type: "string", description: "Optional: filter by category (e.g., 'app', 'sprite', 'graphics')"},
|
|
18
|
+
limit: {type: "integer", description: "Maximum number of results to return (default: 20)"}
|
|
19
|
+
},
|
|
20
|
+
required: ["query"]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DETAILS_TIP = "**Tip:** Use `cf_get_details` for API items or `cf_get_topic` for topic guides to get full documentation."
|
|
24
|
+
|
|
25
|
+
def self.call(query:, type: nil, category: nil, limit: 20, server_context: {})
|
|
26
|
+
index = server_context[:index]
|
|
27
|
+
return error_response("Index not available") unless index
|
|
28
|
+
|
|
29
|
+
results = index.search(query, type: type, category: category, limit: limit)
|
|
30
|
+
|
|
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)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|