Integrations

MCP server

Serve the same index to coding agents and AI apps as an MCP server, with search and fetch tools, and no model call on your side.

The index that the Ask box answers from can also be served to agents, such as Claude Code, Cursor, VS Code, ChatGPT and Claude, as an MCP server. createMcpHandler from ondocs/server serves it with three read-only tools: search, fetch and list_pages. The agent brings its own model: the endpoint never calls a language model, and with no embedding model, it calls no model at all.

This site serves its own docs this way, at https://ondocs.vercel.app/api/mcp. To try it in Claude Code:

Terminal
claude mcp add --transport http ondocs https://ondocs.vercel.app/api/mcp

Or add it to Cursor, VS Code or Claude, or in ChatGPT, add a custom MCP server with that URL at chatgpt.com/plugins.

Mount the endpoint

It is a Web-standard (Request) => Promise<Response>, like the ask endpoint, and it mounts the same way. In a Next.js app, app/api/mcp/route.ts:

TypeScript
// app/api/mcp/route.ts
import { createMcpHandler } from 'ondocs/server';
import index from '../../../ask-index.json';

const handler = createMcpHandler({ index, siteName: 'the Acme docs' });

export const POST = handler;
export const GET = handler;
export const DELETE = handler;
export const OPTIONS = handler;

Pass the same imported index to createAskHandler and createMcpHandler, and it is loaded into memory once for both. MCP clients send JSON-RPC with POST. The handler answers GET and DELETE with a 405, as a server without sessions does, and OPTIONS for browser clients.

For a static site, npx ondocs init writes it next to the ask endpoint: api/mcp.ts on Vercel, netlify/functions/mcp.mts on Netlify, functions/api/mcp.ts on Cloudflare Pages, or /api/mcp in the Worker it writes for a Cloudflare Worker or a GitHub Pages site, each rate-limited on the host's client IP header. On an Astro site with an SSR adapter, the integration serves /api/mcp itself. See Deploying.

The tools

ToolInputReturns
searchquery: keywords or a short questionUp to 8 sections, best first, each with an id, the page title, the section's heading path, its absolute url with the anchor, and text, a snippet of the part that matches the query.
fetchid: an id from search, or a page URLThe page as Markdown, or with an #anchor, that section. A page over 60,000 characters is cut, and the sections to fetch for the rest are listed.
list_pagesprefix (optional): a path, as /docs/guidesEvery page's id, title and url, or those under the prefix, up to 500.

Results come back as structured content, and as the same JSON in a text block for clients that read only text. That is the shape ChatGPT's connectors and deep research expect from search and fetch. fetch also takes a URL as an agent might write it: absolute, with a trailing slash, or ending in .md. A page or tool that does not exist is an error the agent reads, with what to try instead.

Without an embeddingModel, search matches on keywords (BM25), and agent traffic costs you no model call of any kind. Agents write good search queries, so this works well. On docusaurus.io's own docs (92 pages, 1,070 chunks), searched as an agent searches ("autogenerated sidebar from folder", "configureWebpack"), the right page is in the top three for 22 of 22 queries, and first for 19. Asked as a person asks ("How do I add a tip or warning callout box?"), it is in the top three for 20 of 22. Those counts are checked in CI on every change.

With embeddingModel, each search also embeds its query, one embedding call at your provider's price, and merges both rankings as the ask endpoint does. That helps queries that share no words with the page. It must be the model the index was built with. If the provider fails, that search falls back to keywords.

TypeScript
const handler = createMcpHandler({
  index,
  embeddingModel: openai.embedding('text-embedding-3-small'),
});

Limits

  • Rate limit. 60 tool calls a minute per client IP by default, per server instance, keyed as the ask endpoint's are (see which client IP header to trust). Discovery and listing the tools are not counted. Hosted clients, ChatGPT and claude.ai, call from their own servers, so their users share a few IPs; raise the limit if many people use your docs from them.
  • Budget. budget: { requestsPerDay } caps tool calls a day for the whole endpoint, apart from the ask endpoint's budget. There is no token budget, as nothing generates tokens.
  • Size. Bodies over 64 KiB get a 413 while they are read, and queries over 500 characters an error.

Protocol versions

The endpoint speaks MCP 2026-07-28, where every request names its version and there is no session, and, for clients that still start with initialize, 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05, answered without a session. Claude Code's newer runtime asks for 2026-07-28 first, with server/discover. Cursor and VS Code start with initialize. Both ways are tested with the official MCP TypeScript client, and against the official conformance suite's scenarios that apply to a tools-only server.

Browsers on other sites get no CORS headers, so they cannot read the answers. Requests are answered whatever their Origin, because hosted clients send one from their servers. allowedOrigins: ['https://app.example.com'] turns on MCP's origin check: requests from any other origin get a 403, and the listed ones get CORS headers.

Show it on your site

Give the plugin the endpoint's URL with mcp, and a page can show how to add it to Cursor, VS Code, Claude, Claude Code and ChatGPT:

  • Docusaurus: ['ondocs/docusaurus', { mcp: '/api/mcp' }], then in an MDX page, import OndocsMcp from '@theme/OndocsMcp' and <OndocsMcp />.
  • Astro and Starlight: ondocs({ mcp: '/api/mcp' }), with site set, then in an MDX page, import McpInstall from 'ondocs/astro/McpInstall.astro' and <McpInstall />.
  • VitePress: withOndocs(config, { mcp: '/api/mcp' }), with sitemap.hostname set, points llms.txt at it; for the links on a page, use mcpInstallLinks below.
  • React: <McpInstall url="https://docs.example.com/api/mcp" name="acme-docs" /> from ondocs/react.
  • Anything else: mcpInstallLinks({ url, name }) from ondocs returns the Cursor and VS Code install links, the claude.ai link, the Claude Code command and the JSON for other clients.

A path is resolved against the site's URL. The name defaults to the site title, as acme-docs.

Try it locally

npx ondocs dev serves the index at http://localhost:8787/api/mcp as well as the ask endpoint, keyword-only, with the same origin and host checks:

Terminal
npx ondocs dev
claude mcp add --transport http docs http://localhost:8787/api/mcp

Handler options

OptionDefaultNotes
indexrequiredAs for createAskHandler. The same imported object as the ask endpoint's is loaded once.
embeddingModelnoneEmbeds each search query. Without it, search is keyword-only and no model is called.
embeddingProviderOptionsnoneAs for createAskHandler.
siteName"this site"Names the docs in the tools' descriptions, which the agent reads.
siteUrlthe request'sThe origin to make URLs absolute with, when the endpoint runs on another host. false keeps the index's URLs.
name, instructionsondocsThe server's name, and what it tells the agent about using the tools.
retrievalkeyword gate 0.3Retrieval settings for search; it keeps sections with at least 30% of the query's keyword weight, looser than the ask endpoint.
rateLimit60 a minute per IPAny limiter, or false. rateLimitFailure works as for createAskHandler.
budgetnone{ requestsPerDay, store }: tool calls a day.
allowedOriginsnoneA list of origins, or '*', to check Origin and send CORS headers.
maxQueryLength500Longest search query, in characters.
maxBodyBytes64 KiBCounted while reading.
onToolCallnone{ tool, arguments, isError, results, retrieval, durationMs } after each call, to count agent traffic.
onErrorconsole.errorHandled errors: embedding fallbacks, a failing limiter, a misconfigured index.