Module: DocsKit::McpServer
- Defined in:
- lib/docs_kit/mcp_server.rb
Overview
Builds the read-only MCP::Server a docs-kit site exposes at POST /mcp — a stateless JSON-RPC skin over DocsKit::McpTools, so an agent (Claude Code, Claude.ai, Cursor) adds one URL and the docs become first-class tools:
server = DocsKit::McpServer.build(DocsKit.configuration, base_url:, view_context:)
server.handle_json(request.body.read) # → the JSON-RPC response string
The mcp gem is OPTIONAL and runtime-detected (docs-kit depends on it in no
gemspec list). .build returns nil when the gem is absent, and the controller
only reaches here when DocsKit.configuration#mcp_enabled? — so a site without
the gem, or with c.mcp = false, is byte-identical to before this feature.
base_url + view_context ride in the server_context (the SDK threads it to every tool block), so the tools render each page's Markdown twin through the Rails view context and absolutize URLs — the same seam LlmsController#full uses. The three tools mirror DocsKit::McpTools one-to-one.
Class Method Summary collapse
-
.build(config, base_url: nil, view_context: nil) ⇒ Object
The MCP::Server for this config, or nil when the
mcpgem isn't loadable. - .define_get_page(server) ⇒ Object
- .define_list_pages(server) ⇒ Object
- .define_search_docs(server) ⇒ Object
-
.define_tools(server) ⇒ Object
Register the three read-only tools.
-
.instructions_for(config, base_url) ⇒ Object
Server instructions: the site's tagline (when set) plus a pointer to /llms.txt, so an agent knows what these docs cover and where the full index lives.
-
.json_response(payload) ⇒ Object
Wrap a Ruby data payload as a single-text MCP tool response, the data as pretty JSON so an agent parses structured fields (not prose).
-
.mcp_available? ⇒ Boolean
Whether the official MCP SDK is loadable — the runtime-detection gate.
Class Method Details
.build(config, base_url: nil, view_context: nil) ⇒ Object
The MCP::Server for this config, or nil when the mcp gem isn't loadable.
base_url/view_context flow to the tools via server_context.
27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/docs_kit/mcp_server.rb', line 27 def build(config, base_url: nil, view_context: nil) return unless mcp_available? server = MCP::Server.new( name: config.brand.to_s, version: DocsKit::VERSION, instructions: instructions_for(config, base_url), server_context: { config:, base_url:, view_context: } ) define_tools(server) server end |
.define_get_page(server) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/docs_kit/mcp_server.rb', line 84 def define_get_page(server) server.define_tool( name: "get_page", description: "Fetch one documentation page as Markdown, by its slug (see list_pages). " \ "Returns the page's full Markdown twin.", input_schema: { type: "object", properties: { slug: { type: "string", description: "The page slug, e.g. \"installation\"." } }, required: ["slug"] } ) do |slug:, server_context:| cfg = server_context[:config] McpServer.json_response( McpTools.get_page(cfg, slug:, base_url: server_context[:base_url], view_context: server_context[:view_context]) ) end end |
.define_list_pages(server) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/docs_kit/mcp_server.rb', line 72 def define_list_pages(server) server.define_tool( name: "list_pages", description: "List every documentation page: its slug, title, group, and URL. " \ "Use the slug with get_page.", input_schema: { type: "object", properties: {}, required: [] } ) do |server_context:| cfg = server_context[:config] McpServer.json_response(McpTools.list_pages(cfg, base_url: server_context[:base_url])) end end |
.define_search_docs(server) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/docs_kit/mcp_server.rb', line 103 def define_search_docs(server) server.define_tool( name: "search_docs", description: "Full-text search across all documentation pages. " \ "Returns ranked hits with the page, section, URL, and a snippet.", input_schema: { type: "object", properties: { query: { type: "string", description: "The search terms." } }, required: ["query"] } ) do |query:, server_context:| cfg = server_context[:config] McpServer.json_response( McpTools.search_docs(cfg, query:, base_url: server_context[:base_url], view_context: server_context[:view_context]) ) end end |
.define_tools(server) ⇒ Object
Register the three read-only tools. Each block pulls config + render context from server_context, calls the matching DocsKit::McpTools function, and returns the result as pretty JSON text (agents consume structured data).
66 67 68 69 70 |
# File 'lib/docs_kit/mcp_server.rb', line 66 def define_tools(server) define_list_pages(server) define_get_page(server) define_search_docs(server) end |
.instructions_for(config, base_url) ⇒ Object
Server instructions: the site's tagline (when set) plus a pointer to /llms.txt, so an agent knows what these docs cover and where the full index lives. base_url absolutizes the /llms.txt hint when available.
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/docs_kit/mcp_server.rb', line 52 def instructions_for(config, base_url) llms = base_url ? "#{base_url.chomp('/')}/llms.txt" : "/llms.txt" lines = [] tagline = config.tagline lines << tagline.to_s if tagline && !tagline.to_s.empty? lines << "Read-only documentation tools for #{config.brand}. " \ "Use search_docs to find sections, get_page to read a page's Markdown, " \ "and list_pages to enumerate the docs. Full index: #{llms}." lines.join(" ") end |
.json_response(payload) ⇒ Object
Wrap a Ruby data payload as a single-text MCP tool response, the data as pretty JSON so an agent parses structured fields (not prose).
124 125 126 |
# File 'lib/docs_kit/mcp_server.rb', line 124 def json_response(payload) MCP::Tool::Response.new([{ type: "text", text: JSON.pretty_generate(payload) }]) end |
.mcp_available? ⇒ Boolean
Whether the official MCP SDK is loadable — the runtime-detection gate. Kept as a seam so specs can force the gem-absent branch without unloading it.
42 43 44 45 46 47 |
# File 'lib/docs_kit/mcp_server.rb', line 42 def mcp_available? require "mcp" defined?(::MCP::Server) ? true : false rescue LoadError false end |