Class: Tools::CreateFaqTool
- Inherits:
-
ApplicationTool
- Object
- MCP::Tool
- ApplicationTool
- Tools::CreateFaqTool
- Defined in:
- app/mcp/tools/create_faq_tool.rb
Overview
MCP Tool for creating new FAQ articles.
Creates an ArticleFaq in draft state.
FAQs have two visibility flags that control where they appear on the website:
- show_on_sales_pages: true → shown on product/marketing pages (product line & catalog pages)
- show_on_support_pages: true → shown on support portal pages (/support/*)
IMPORTANT — when to set these flags:
- Set show_on_sales_pages: true ONLY when the FAQ is scoped to a specific product line +
product category, or to a specific SKU. These FAQs are surfaced inline on product pages. - Set show_on_support_pages: true ONLY when the FAQ should appear on the support portal
for a specific product line / category or SKU context. - When the FAQ is intended for a particular marketing or content page (indicated by a
"for-xxxx-page" tag), BOTH flags must be false. The FAQ is fetched solely by tag match.
Common combinations:
- sales=true, support=true: Product + support pages for a specific product line/category or SKU
- sales=true, support=false: Product pages only for a specific product line/category or SKU
- sales=false, support=true: Support portal only for a specific product line/category or SKU
- Both false (default): Tag-based pages only — use with a "for-xxxx-page" tag
Constant Summary collapse
- MAX_ANSWER_WORDS =
150
Class Method Summary collapse
-
.call(question:, answer:, description: nil, show_on_sales_pages: false, show_on_support_pages: false, product_line_names: nil, tags: nil, publish: false, _server_context: nil) ⇒ Object
rubocop:disable Lint/UnusedMethodArgument.
Class Method Details
.call(question:, answer:, description: nil, show_on_sales_pages: false, show_on_support_pages: false, product_line_names: nil, tags: nil, publish: false, _server_context: nil) ⇒ Object
rubocop:disable Lint/UnusedMethodArgument
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'app/mcp/tools/create_faq_tool.rb', line 114 def call(question:, answer:, description: nil, show_on_sales_pages: false, show_on_support_pages: false, product_line_names: nil, tags: nil, publish: false, _server_context: nil) # rubocop:disable Lint/UnusedMethodArgument # Validate inputs return error_response('Question cannot be blank') if question.blank? return error_response('Answer cannot be blank') if answer.blank? word_count = ActionController::Base.helpers.(answer).split.size if word_count > MAX_ANSWER_WORDS return error_response( "Answer is too long (#{word_count} words). FAQ answers must be 2-4 sentences, " \ "max #{MAX_ANSWER_WORDS} words. Shorten the answer to a single concise paragraph " \ "or a short bullet list (3-5 items). If the topic needs a longer explanation, " \ "write it as a blog section instead of an FAQ." ) end # Validate internal links in the answer HTML before saving if answer.present? link_validation = Seo::InternalLinkValidator.new.process(answer) unless link_validation.valid? details = link_validation.broken_links.map { |bl| "#{bl.href}#{bl.suggestion ? " → suggested: #{bl.suggestion}" : ''}" } return error_response( "Broken internal links detected — FAQ was NOT saved. Fix or remove these links: #{details.join('; ')}" ) end # Stage 4 of the Sunny blog editor fix plan: server-enforced FAQ # rules (no widgets/calculators in answers, no nested headings, # max 1,200 chars). The same registry powers insert_faqs. if defined?(Blog::ContentRules) violations = Blog::ContentRules.validate(scope: :faq_answer, html: answer) if violations.any? return error_response( Blog::ContentRules.format_error(violations, scope: :faq_answer) ) end end end # Page-specific FAQs (tagged with "for-xxxx-page") must not be shown on sales/support pages. # Those flags are reserved for FAQs scoped to a product line, product category, or SKU. if () && (show_on_sales_pages || show_on_support_pages) return error_response( 'FAQs with a "for-xxxx-page" tag are page-specific and must have ' \ 'show_on_sales_pages and show_on_support_pages both set to false. ' \ 'Remove the "for-xxxx-page" tag, or set both flags to false.' ) end # Build the FAQ faq = ArticleFaq.new( subject: question.strip, solution: answer.strip, description: description.presence || generate_description(answer), sales: show_on_sales_pages, support: show_on_support_pages ) # Associate product lines if product_line_names.present? product_lines = resolve_product_lines(product_line_names) faq.product_lines = product_lines end # Set tags faq. = () if .present? # Save the FAQ unless faq.save return error_response("Failed to create FAQ: #{faq.errors..join(', ')}") end # Publish if requested if publish unless faq.publish return error_response("FAQ created (ID: #{faq.id}) but could not be published: #{faq.errors..join(', ')}") end end # Populate editorial link graph immediately (non-fatal — FAQ is already saved) begin Seo::InternalLinkValidator.upsert_editorial_links!(faq) rescue StandardError => e Rails.logger.warn "[MCP CreateFaq] Link graph upsert failed for FAQ #{faq.id}: #{e.}" end # Generate embedding for semantic search (async-safe) (faq) json_response( success: true, faq: format_faq(faq), message: "FAQ created successfully#{publish ? ' and published' : ' as draft'}." ) rescue StandardError => e ErrorReporting.error(e, tool: 'create_faq') error_response("Unexpected error creating FAQ: #{e.}") end |