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.
@@ -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