feat(mcp): optional built-in MCP server — list/get/search docs over Streamable HTTP#36
Merged
Conversation
## Summary Any docs-kit site can now turn on a read-only MCP endpoint with one gem + one route — every current and future site becomes agent-queryable for free. A reader adds `https://docs.example.com/mcp` to Claude Code / Claude.ai / Cursor once and the docs become first-class tools (search, page retrieval) instead of scraped text. Built on the official `mcp` Ruby SDK, exposed as a stateless Streamable HTTP JSON-RPC endpoint mounted by the host app, OFF unless BOTH the gem is present and the route is drawn. Three tools over the SAME registry the docs render from (so an agent queries live docs, never a stale scrape): - `list_pages` → every authored page {slug, title, group, url} - `get_page(slug:)` → the page's Markdown twin, or a graceful not-found listing valid slugs - `search_docs(query:)` → ranked SearchIndex hits {page_title, section_title, url, snippet} Layering mirrors the existing llms.txt/search split — a pure, Rails-free core (DocsKit::McpTools) wrapped into an MCP::Server (DocsKit::McpServer), threaded by a thin controller (DocsKit::McpController) that owns only the Rails view context. ## Optional-dependency gate docs-kit adds `mcp` to no gemspec list. `Configuration#mcp_enabled?` gates on the `c.mcp` knob (default true) AND the gem being loadable. The `mcp` gem lives in its own bundler group; a new CI leg installs `--without mcp` and runs the suite green, proving the feature no-ops (the MCP specs self-skip) when the gem is absent — a site that never bundles it is byte-identical to before. ## Also - `/llms.txt` grows a final `## MCP` line advertising the endpoint when enabled, so agents discover it. - Install generator draws the /mcp route COMMENTED (opt-in); `docs_kit.rb.erb` and `new_site.rb` document the `gem "mcp"` + `c.mcp` opt-in. - Dogfooded on the gem's own docs/ app (real /mcp route + gem): verified from curl — tools/list, search_docs, get_page, and GET/DELETE → 405 all work end-to-end through the real Rails render path. - README "Add your docs to an agent (MCP)" section with the `claude mcp add --transport http` one-liner. ## Test Coverage - spec/docs_kit/mcp_tools_spec.rb — pure core: authored-only listing, markdown twin + graceful unknown slug, ranked hits with <mark> stripped (runs WITHOUT the mcp gem) - spec/docs_kit/mcp_server_spec.rb — real SDK via handle_json: tools/list + each tools/call round-trips; .build no-ops when the gem is absent (self-skips without the gem) - spec/docs_kit/mcp_controller_spec.rb — source-wiring (Rails-only class), mirrors llms_controller_spec - configuration_spec / llms_text_spec / install_generator_spec — the knob, the advertisement line, the commented route ## Verification - [x] bundle exec rspec — 448 examples, 0 failures, 96% line coverage - [x] bundle exec rubocop — no offenses - [x] suite green WITHOUT the mcp gem (optional-dependency gate) - [x] dogfooded end-to-end against docs/ (curl → all tools answer) Closes #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #20.
What & why
Developers increasingly read docs through agents.
llms.txtcovers fetch-style consumption, but MCP (Model Context Protocol) is the native protocol: a reader addshttps://docs.example.com/mcpto Claude Code / Claude.ai / Cursor once and the docs become first-class tools (search, targeted page retrieval) instead of scraped text. This makes every current and future docs-kit site agent-queryable for free.A read-only, stateless Streamable HTTP MCP endpoint built on the official
mcpRuby SDK, mounted by the host app, off unless both the gem is present and the route is drawn.The three tools
All over the same registry the docs render from — an agent queries live docs, never a stale scrape:
list_pagesslug,title,group,urlget_page(slug:).mdtwin/llms.txtlinks), or a graceful not-found listing valid slugssearch_docs(query:)DocsKit::SearchIndexhits —page_title,section_title,url,snippet(with the<mark>highlight stripped to plain text)Architecture — mirrors the existing llms.txt/search split
DocsKit::McpTools— the pure, Rails-free,mcp-free core (all three functions overLlmsText.pages/MarkdownExport/SearchIndex). Fully unit-testable without HTTP or the SDK.DocsKit::McpServer— wraps those into anMCP::Serverviadefine_tool, named fromc.brand, instructions fromc.tagline+ a/llms.txtpointer. No-ops (returnsnil) when the gem is absent.DocsKit::McpController— thin:POST /mcp→server.handle_json(request.body.read); owns only the Rails view context (for the twin render) + the read-only method policy.GET/DELETE→405.Optional-dependency gate
docs-kit adds
mcpto no gemspec list.Configuration#mcp_enabled?gates on thec.mcpknob (defaulttrue) AND the gem being loadable. Themcpgem lives in its own bundler group, and a new CI leg installs--without mcpand runs the suite — proving the feature no-ops (the MCP specs self-skip) when the gem is absent. A site that never bundlesmcpis byte-identical to before this PR.Discovery + opt-in wiring
/llms.txtgrows a final## MCPline advertising the endpoint when enabled, so agents discover it./mcproute commented out (opt-in — the gem is optional);config/initializers/docs_kit.rband thedocs-kit newtemplate document thegem "mcp"+c.mcpopt-in.claude mcp add --transport httpone-liner.Dogfooded end-to-end
Enabled on the gem's own
docs/app (real/mcproute +gem "mcp") and verified fromcurlagainst a booted server — through the real Rails render path, not just unit tests:tools/list→ the three toolssearch_docs("theme switcher")→ top hitComponents → ThemeSwitcherwith a correct anchored absolute URL, snippet as plain textget_page("installation")→ the real Markdown twin;get_page("nope")→ graceful not-foundGET/DELETE /mcp→405/llms.txt→ the## MCPadvertisement lineTest plan
spec/docs_kit/mcp_tools_spec.rb— the pure core (runs without the mcp gem): authored-only listing, twin + unknown-slug, ranked hits with<mark>strippedspec/docs_kit/mcp_server_spec.rb— the real SDK viahandle_json:tools/list+ eachtools/callround-trips;.buildno-ops gem-absent (self-skips without the gem)spec/docs_kit/mcp_controller_spec.rb— source-wiring (the class is Rails-only), mirrorsllms_controller_specconfiguration_spec/llms_text_spec/install_generator_spec— the knob +mcp_enabled?, the advertisement line, the commented routeVerification
bundle exec rspec— 448 examples, 0 failures, 96% line coveragebundle exec rubocop— no offensesbundle config set --local without mcp) — the optional-dependency gatedocs/(curl → every tool answers)Out of scope (per the issue)
Write tools (authoring is git), auth (public docs; a host wraps the route), and MCP resources/prompts primitives (tools cover the consumption story).