MCP Server
@farming-labs/docs can expose your docs as a built-in MCP server, so AI clients can search the docs, read pages, list sections, inspect code examples, and understand config options without scraping HTML.
The current built-in surface includes:
-
Facets tool:
list_search_facetsis also enabled by default. Disable it withmcp.tools.searchFacets: false. -
list_docs -
list_content_changes -
hydrate_content_changes -
list_pages -
get_navigation -
list_search_facets -
list_page_sections -
search_docs -
read_page -
read_pages -
submit_feedbackwhen machine feedback is enabled -
get_code_examples -
get_config_schema -
get_context -
get_trust_metadatawhenagent.okfis enabled -
explicitly allowlisted
api_*operation tools whenapiReference.mcpis configured
It also exposes resources for:
docs://navigation- one
docs://…resource per page
read_pages accepts up to 20 page paths and applies one shared conservative token budget, which
reduces MCP round trips without letting a batch grow without bounds. submit_feedback uses the
same { context?, payload } envelope as the HTTP feedback endpoint and validates payload against
feedback.agent.schema before calling feedback.agent.onFeedback.
export default defineDocs({
feedback: {
agent: {
schema: {
type: "object",
properties: { outcome: { type: "string" } },
required: ["outcome"],
},
async onFeedback(data) {
await saveAgentFeedback(data);
},
},
},
});Project selected OpenAPI operations as tools
apiReference.mcp can project API operations into MCP tools. It is deny-by-default: enabling the
object alone exposes no operation unless its operationId, its METHOD /path selector, or its
OpenAPI x-farming-labs-mcp extension explicitly allows it. Write methods need the additional
allowMutations: true opt-in.
export default defineDocs({
apiReference: {
enabled: true,
mcp: {
operations: ["getUser", "GET /projects/{id}"],
baseUrl: "https://api.example.com",
headers: ({ security }) => ({
Authorization: `Bearer ${process.env.PRODUCT_API_TOKEN}`,
"X-Declared-Security": JSON.stringify(security),
}),
// allowMutations: true,
},
},
});Each projected tool carries its HTTP method, path, OpenAPI security requirements and referenced security schemes in MCP metadata. Server-owned headers are applied after tool input, so a model cannot replace configured credentials. Keep tokens on the server and restrict them to the smallest scopes required by the allowlist.
Public MCP tools call your API
A public MCP endpoint makes every projected operation callable by public clients. Protect the MCP endpoint, keep mutations disabled unless required, and use narrowly scoped service credentials.
Protected authoring MCP
The public docs MCP remains read-only. Run the separate local authoring server when an agent needs to propose documentation changes:
pnpm exec docs mcp author --branch-prefix docs/Its workflow tools cover status, branch creation, hash-checked reads and writes, machine-readable
page preview, docs doctor, Git diff review, and recurring-feedback analysis. Writes are restricted
to .md, .mdx, and .svx files inside contentDir; overwriting a file requires the SHA-256 from
the preceding read, which prevents a stale agent from replacing newer work.
Draft-PR publishing is absent by default. Register it only for an operator-controlled session:
pnpm exec docs mcp author --branch-prefix docs/ --base-branch main --allow-publishThe publishing tool requires explicit confirmation, rejects dirty files outside contentDir,
requires a matching branch prefix and authenticated GitHub CLI, then commits docs-only changes,
pushes the branch, and opens a draft PR. createDocsAuthoringMcpServer() is also exported from the
server entry for teams that place this distinct surface behind authoring-scoped OAuth. It is never
wired into /mcp, /.well-known/mcp, or a framework public forwarder.
Keep authoring separate
Do not expose the authoring server through the public docs MCP route. Use local stdio or a separately authenticated transport with an authoring-specific OAuth scope.
Default behavior
MCP is enabled by default.
import { defineDocs } from "@farming-labs/docs";
import { pixelBorder } from "@farming-labs/theme/pixel-border";
export default defineDocs({
entry: "docs",
theme: pixelBorder(),
});That gives you the built-in MCP surface with the default public Streamable HTTP routes:
/mcp
/.well-known/mcpThis docs site already exposes it live
The hosted docs site has MCP enabled at https://docs.farming-labs.dev/mcp and
https://docs.farming-labs.dev/.well-known/mcp.
Opt out explicitly:
export default defineDocs({
entry: "docs",
mcp: {
enabled: false,
},
});HTTP security and opt-in authentication
The HTTP MCP endpoint is public by default. Authentication only becomes required when you add
mcp.security.authenticate. This lets public documentation stay zero-config while private docs can
reuse any authentication provider that can inspect a Web Request.
Adding authenticate by itself keeps the existing callback-only contract. Add
security.protectedResource alongside it when OAuth-aware MCP clients should discover an
authorization server, requested scopes, and RFC 9728 protected-resource metadata.
Even for public endpoints, the HTTP transport validates a supplied Origin header against the
request origin and limits POST bodies to 1 MiB by default. Origin-less requests remain supported
for non-browser MCP clients. The body is buffered through that limit before an Origin or
authentication callback runs, so callbacks never receive an oversized POST. These HTTP controls do
not affect the local stdio command.
export default defineDocs({
mcp: {
security: {
async authenticate({ request }) {
const session = await getSession(request.headers);
if (!session) return null;
return {
id: session.user.id,
scopes: ["docs:read"],
claims: {
organizationId: session.user.organizationId,
},
};
},
},
},
});The callback result controls the request:
- Return
{ id, scopes?, claims? }to continue as an authenticated principal. - Return
nullto receive401 Unauthorized. WithprotectedResource, the response also includes a BearerWWW-Authenticatechallenge that points to the resource metadata. - Return a Web
Responseto control the rejection, redirect, or challenge yourself. The framework preserves its status, body, and headers instead of adding an OAuth challenge. - Throw to receive a sanitized
500response; provider error details are not exposed.
OAuth protected-resource discovery
Configure both authenticate and protectedResource to opt into RFC 9728 discovery and
framework-generated Bearer challenges:
export default defineDocs({
mcp: {
security: {
protectedResource: {
authorizationServers: ["https://auth.example.com"],
scopesSupported: ["docs:read", "docs:search"],
requiredScopes: ["docs:read"],
resourceName: "Private product docs",
resourceDocumentation: "https://docs.example.com/auth/mcp",
},
async authenticate({ request, resource }) {
const token = await verifyAccessToken(request);
if (!token) return null;
if (!token.audiences.includes(resource)) return null;
return {
id: token.subject,
scopes: token.scopes,
};
},
},
},
});protectedResource makes the docs endpoint an OAuth resource server; it does not issue or validate
tokens. The callback must read the Bearer token, validate its signature, issuer, expiry, and the
exact resource audience supplied in the callback context, and return only scopes verified from
that token. The request retains the real incoming URL, including its query string. Never pass an
upstream token through to another service. The configured authorization server must publish RFC
8414 or OpenID Connect discovery and support a client-registration approach your MCP clients can
use.
authorizationServers is required and must contain at least one HTTPS authorization-server issuer
URL without a query or fragment; loopback HTTP is accepted for development. scopesSupported is
advertised to clients. requiredScopes is enforced against the
scopes returned by authenticate for every HTTP MCP request.
When this configuration is active:
- missing or invalid credentials that make
authenticatereturnnullreceive401with a Bearer challenge containingresource_metadataand the requiredscope - an authenticated principal missing any
requiredScopesreceives403witherror="insufficient_scope" - a
Responsereturned byauthenticateremains authoritative, so addWWW-Authenticateto that response yourself when a custom rejection should participate in OAuth discovery - the stdio transport is unchanged
The framework publishes a public metadata document for each MCP HTTP alias:
/.well-known/oauth-protected-resource/mcp
/.well-known/oauth-protected-resource/.well-known/mcp
/.well-known/oauth-protected-resource/api/docs/mcpIf mcp.route is customized, the last path uses that configured route instead. Each document's
resource value identifies the corresponding public MCP URL, and its
authorization_servers, optional scopes_supported, resource name, and documentation URL come
from protectedResource. Metadata stays public so a client can discover how to authenticate before
it has a token. GET and HEAD are supported, and browser discovery receives public CORS headers.
Each alias is a distinct OAuth resource identifier. Configure token audience validation for the
exact endpoint your clients use; prefer /mcp as the shared public endpoint unless clients require
the canonical or well-known alias.
protectedResource is inactive without authenticate: the MCP endpoint remains public and the
metadata routes return 404. This prevents discovery metadata from claiming that a public endpoint
is protected.
Generated TanStack Start, SvelteKit, and Astro forwarders pass config.mcp to
isDocsMcpRequest(url, config.mcp). Update that call when enabling protected auth in a project
generated by an older release. The legacy one-argument form continues to match MCP endpoints but
deliberately does not reserve OAuth metadata paths in public projects.
Use HTTPS for hosted OAuth
Use absolute https:// authorization-server and resource-documentation URLs in production, and
serve the hosted MCP endpoint over HTTPS. Plain HTTP should be limited to loopback development.
Next.js basePath
Protected MCP cannot use Next.js basePath: RFC 9728 inserts its well-known path at the origin
root, while Next.js only supports disabling basePath for external rewrites. Host the protected
docs app at the origin root, or publish the MCP proxy and protected-resource metadata at the edge.
For an inline MCP object, withDocs() detects this combination. If the MCP configuration is
imported, spread, or shorthand, pass the live docs config as the second argument so protected
rewrites and the basePath check use the resolved object:
import { withDocs } from "@farming-labs/next/config";
import docsConfig from "./docs.config";
export default withDocs({}, docsConfig);Custom low-level DocsMcpSource implementations receive that principal in the optional second
argument to getPages(locale, context) and getNavigation(locale, context). They can use scopes or
claims to filter private content. The framework does not serialize the principal into MCP tool
results or telemetry.
Configure browser origins and the request limit independently:
export default defineDocs({
mcp: {
security: {
allowedOrigins: [
"https://docs.example.com",
"https://internal-tools.example.com",
],
maxBodyBytes: 512 * 1024,
},
},
});allowedOrigins also accepts "same-origin" (the default) or an async policy callback:
export default defineDocs({
mcp: {
security: {
allowedOrigins: async ({ origin, request }) => {
const hostname = new URL(origin).hostname;
return (
origin === new URL(request.url).origin ||
hostname === "example.com" ||
hostname.endsWith(".example.com")
);
},
},
},
});Browser CORS
Cross-origin browser access is opt-in through allowedOrigins. When an Origin is accepted, the
framework answers OPTIONS preflight without running authenticate and emits that exact Origin in
Access-Control-Allow-Origin. It never combines wildcard Origin responses with credentials.
The standard MCP request headers, including Authorization, Content-Type,
MCP-Protocol-Version, and MCP-Session-Id, are allowed by default. Add application-specific auth
headers explicitly:
export default defineDocs({
mcp: {
security: {
allowedOrigins: ["https://internal-tools.example.com"],
cors: {
allowedHeaders: ["X-API-Key"],
exposedHeaders: ["X-Docs-Version"],
allowCredentials: true,
maxAgeSeconds: 600,
},
},
},
});allowCredentials is false by default. Enable it only for an explicitly allowed browser Origin
when the auth provider needs cookies or another browser credential. Set cors: false to keep Origin
validation but suppress browser CORS headers.
Generated routes and public forwarders include OPTIONS. On a manually maintained MCP route,
export or forward MCP.OPTIONS alongside GET, POST, and DELETE.
Origin is not authentication
Origin validation protects the browser-facing transport; it does not identify a user or agent.
Use authenticate when the documentation itself needs access control. Headers such as
Signature-Agent are content-negotiation hints and must not be treated as proof of identity.
Default HTTP Routes
/mcp is the short public MCP endpoint in Next.js, and /.well-known/mcp is the discovery-friendly
public MCP endpoint. Both rewrite to the canonical framework route at /api/docs/mcp, so the MCP
handler still lives in one place.
Minimal route behavior
Next.js auto-generates the default /api/docs/mcp route when you use withDocs(), and also
adds public /mcp and /.well-known/mcp rewrites unless you explicitly disable MCP.
TanStack Start, SvelteKit, Astro, and Nuxt use one public forwarder each so
/api/docs/mcp, /mcp, and /.well-known/mcp share the same built-in handler without creating
one route file per alias.
Next.js
With withDocs(), no extra route file is needed for the default path.
import { withDocs } from "@farming-labs/next/config";
export default withDocs();TanStack Start
import { createFileRoute } from "@tanstack/react-router";
import {
isDocsMcpRequest,
isDocsPublicGetRequest,
isDocsStandardsDiscoveryRequest,
} from "@farming-labs/docs";
import { docsServer } from "@/lib/docs.server";
import docsConfig from "../../docs.config";
const docsEntry = docsConfig.entry ?? "docs";
async function handlePublicDocsRequest(request: Request) {
const url = new URL(request.url);
const method = request.method.toUpperCase();
if (isDocsMcpRequest(url, docsConfig.mcp)) {
if (method === "OPTIONS") return docsServer.MCP.OPTIONS({ request });
if (method === "POST") return docsServer.MCP.POST({ request });
if (method === "DELETE") return docsServer.MCP.DELETE({ request });
if (method === "GET" || method === "HEAD") return docsServer.MCP.GET({ request });
return new Response("Method Not Allowed", {
status: 405,
headers: { Allow: "GET, HEAD, POST, DELETE, OPTIONS" },
});
}
if (
isDocsStandardsDiscoveryRequest(url, {
apiRoute: docsConfig.cloud?.apiRoute,
})
) {
if (method === "HEAD") return docsServer.HEAD({ request });
if (method === "POST") return docsServer.POST({ request });
return docsServer.GET({ request });
}
if (
(method === "GET" || method === "HEAD") &&
isDocsPublicGetRequest(docsEntry, url, request, {
apiRoute: docsConfig.cloud?.apiRoute,
sitemap: docsConfig.sitemap,
llms: docsConfig.llmsTxt,
robots: docsConfig.robots,
})
) {
return method === "HEAD" ? docsServer.HEAD({ request }) : docsServer.GET({ request });
}
return new Response("Not Found", { status: 404 });
}
export const Route = createFileRoute("/$")({
server: {
handlers: {
GET: async ({ request }) => handlePublicDocsRequest(request),
HEAD: async ({ request }) => handlePublicDocsRequest(request),
POST: async ({ request }) => handlePublicDocsRequest(request),
DELETE: async ({ request }) => handlePublicDocsRequest(request),
OPTIONS: async ({ request }) => handlePublicDocsRequest(request),
ANY: async ({ request }) => handlePublicDocsRequest(request),
},
},
});SvelteKit
import {
isDocsLlmsTxtPublicRequest,
isDocsMcpRequest,
isDocsPublicGetRequest,
isDocsStandardsDiscoveryRequest,
} from "@farming-labs/docs";
import config from "$lib/docs.config";
import { GET, HEAD, POST, MCP } from "$lib/docs.server";
const docsEntry = config.entry ?? "docs";
export async function handle({ event, resolve }) {
const method = event.request.method.toUpperCase();
if (isDocsMcpRequest(event.url, config.mcp)) {
if (method === "OPTIONS") return MCP.OPTIONS({ request: event.request });
if (method === "POST") return MCP.POST({ request: event.request });
if (method === "DELETE") return MCP.DELETE({ request: event.request });
if (method === "GET" || method === "HEAD") return MCP.GET({ request: event.request });
return new Response("Method Not Allowed", {
status: 405,
headers: { Allow: "GET, HEAD, POST, DELETE, OPTIONS" },
});
}
if (
isDocsStandardsDiscoveryRequest(event.url, {
apiRoute: config.cloud?.apiRoute,
})
) {
if (method === "HEAD") return HEAD({ url: event.url, request: event.request });
if (method === "POST") return POST({ url: event.url, request: event.request });
return GET({ url: event.url, request: event.request });
}
if (
(method === "GET" || method === "HEAD") &&
isDocsLlmsTxtPublicRequest(event.url, config.llmsTxt, docsEntry, {
apiRoute: config.cloud?.apiRoute,
})
) {
const nativeResponse = await resolve(event);
if (nativeResponse.status !== 404) return nativeResponse;
}
if (
(method === "GET" || method === "HEAD") &&
isDocsPublicGetRequest(docsEntry, event.url, event.request, {
apiRoute: config.cloud?.apiRoute,
sitemap: config.sitemap,
llms: config.llmsTxt,
robots: config.robots,
})
) {
return method === "HEAD"
? HEAD({ url: event.url, request: event.request })
: GET({ url: event.url, request: event.request });
}
return resolve(event);
}Your src/lib/docs.server.ts can keep using the normal helper:
import { createDocsServer } from "@farming-labs/svelte/server";
import config from "./docs.config";
export const { load, GET, HEAD, POST, MCP } = createDocsServer({
...config,
});Astro
import {
isDocsLlmsTxtPublicRequest,
isDocsMcpRequest,
isDocsPublicGetRequest,
isDocsStandardsDiscoveryRequest,
} from "@farming-labs/docs";
import config from "./lib/docs.config";
import { GET, HEAD, POST, MCP } from "./lib/docs.server";
const docsEntry = config.entry ?? "docs";
export async function onRequest(context, next) {
const method = context.request.method.toUpperCase();
if (isDocsMcpRequest(context.url, config.mcp)) {
if (method === "OPTIONS") return MCP.OPTIONS({ request: context.request });
if (method === "POST") return MCP.POST({ request: context.request });
if (method === "DELETE") return MCP.DELETE({ request: context.request });
if (method === "GET" || method === "HEAD") return MCP.GET({ request: context.request });
return new Response("Method Not Allowed", {
status: 405,
headers: { Allow: "GET, HEAD, POST, DELETE, OPTIONS" },
});
}
if (
isDocsStandardsDiscoveryRequest(context.url, {
apiRoute: config.cloud?.apiRoute,
})
) {
if (method === "HEAD") return HEAD({ request: context.request });
if (method === "POST") return POST({ request: context.request });
return GET({ request: context.request });
}
if (
(method === "GET" || method === "HEAD") &&
isDocsLlmsTxtPublicRequest(context.url, config.llmsTxt, docsEntry, {
apiRoute: config.cloud?.apiRoute,
})
) {
const nativeResponse = await next();
if (nativeResponse.status !== 404) return nativeResponse;
}
if (
(method === "GET" || method === "HEAD") &&
isDocsPublicGetRequest(docsEntry, context.url, context.request, {
apiRoute: config.cloud?.apiRoute,
sitemap: config.sitemap,
llms: config.llmsTxt,
robots: config.robots,
})
) {
return method === "HEAD"
? HEAD({ request: context.request })
: GET({ request: context.request });
}
return next();
}Nuxt
import { defineDocsPublicHandler } from "@farming-labs/nuxt/server";
import config from "../../docs.config";
export default defineDocsPublicHandler(config, useStorage);Custom route
If you want a custom MCP route, set it in docs.config and add the route file yourself.
export default defineDocs({
entry: "docs",
mcp: {
enabled: true,
route: "/api/internal/docs/mcp",
},
});Custom routes are explicit
The package only auto-generates the default Next.js route at /api/docs/mcp. If you choose a
custom route, keep that path in docs.config and update the framework public forwarder so the app
and the MCP client point at the same endpoint.
Example custom Next.js route:
import docsConfig from "@/docs.config";
import { createDocsMCPAPI } from "@farming-labs/next/api";
export const { GET, POST, DELETE, OPTIONS } = createDocsMCPAPI(docsConfig);
export const revalidate = false;@farming-labs/theme/api compatibility
@farming-labs/theme/api still works if you already rely on it, but prefer
@farming-labs/next/api for Next.js routes going forward. The theme-level path will be
deprecated. If mcp.security is configured, pass the imported live docsConfig to
createDocsMCPAPI; the legacy no-argument form intentionally refuses to reconstruct executable
authentication callbacks from source text.
When search is configured, the MCP search_docs tool uses that same adapter pipeline too. That
means your docs UI search and MCP search can share the same provider, chunking strategy, and custom
ranking logic instead of drifting apart.
That also means you can flip docs search itself over to MCP:
export default defineDocs({
entry: "docs",
search: {
provider: "mcp",
endpoint: "/mcp",
},
mcp: {
enabled: true,
},
});For Ask AI only, keep the top-level search provider as-is and enable MCP retrieval in the AI config. This uses the MCP server the docs site already exposes:
export default defineDocs({
ai: {
enabled: true,
useMcp: true,
},
});If you changed the canonical MCP route, useMcp: true follows it automatically:
export default defineDocs({
mcp: {
route: "/custom/docs/mcp",
},
ai: {
enabled: true,
useMcp: true,
},
});For a hosted or external MCP endpoint, configure the endpoint on ai.useMcp instead. This does not
replace your LLM provider config; the MCP endpoint retrieves docs context, then Ask AI still sends
the final prompt to your configured model provider.
export default defineDocs({
ai: {
enabled: true,
model: "gpt-4o-mini",
apiKey: process.env.OPENAI_API_KEY,
useMcp: {
endpoint: "https://docs.example.com/mcp",
headers: {
Authorization: `Bearer ${process.env.DOCS_MCP_TOKEN}`,
},
toolName: "search_docs",
},
},
});The custom MCP endpoint must support Streamable HTTP MCP and expose a search tool that returns docs
results. The default tool name is search_docs; override toolName only if your server uses a
different name. Stateful MCP servers may return an mcp-session-id on initialize; stateless
servers can omit it, and the docs client will only send the session header when one is provided.
For local self-hosted setups, relative MCP endpoints like /mcp and /.well-known/mcp are
supported. The canonical /api/docs/mcp route remains available too. The built-in search_docs
tool automatically falls back to simple search internally when it detects that same-route loop, so
the route stays usable for testing and local examples.
The fallback is triggered whenever the configured search.endpoint resolves to the same server as the incoming request and the endpoint path matches one of the built-in MCP aliases (/mcp, /.well-known/mcp, /api/docs/mcp) or the custom mcp.route value set in docs.config. When the match is detected, search_docs performs direct simple search instead of issuing a loopback HTTP call to itself. External MCP search endpoints — for example https://other-service.example.com/mcp or a path like /external/search/mcp that does not match any configured MCP route — are not affected and continue to proxy normally.
Stdio transport
The same docs graph is available locally over stdio:
pnpx @farming-labs/docs mcpOr with pnpm in an installed project:
pnpm exec docs mcpBy default the CLI reads docs.config.ts[x] from the project root. If your config lives somewhere else:
pnpm exec docs mcp --config src/lib/docs.config.tsUse the hosted MCP endpoint
If you just want to try the live docs server, you can point your MCP client at:
https://docs.farming-labs.dev/mcp
https://docs.farming-labs.dev/.well-known/mcpTo print the MCP client setup instructions for your own Docs Cloud deployment, use mcp setup:
pnpm exec docs mcp setup --deployment <your-deployment-id>The generated config connects directly to the hosted Streamable HTTP endpoint; it does not run
another setup command or embed the secret. The default output targets Claude Code and reads
${DOCS_CLOUD_API_KEY} from that client's environment. Use --client cursor for Cursor's
${env:DOCS_CLOUD_API_KEY} syntax, or --client vscode for VS Code's native servers schema and
secure input prompt. Add --json to print only the selected JSON config block, or
--api-base-url <url> to target a self-hosted Docs Cloud API. See
CLI for the full flag reference.
Docs MCP
Add to MCP
Connect the live docs endpoint directly in clients that support one-click or manual MCP setup.
Endpoint
https://docs.farming-labs.dev/api/docs/mcpMCP config
{
"mcpServers": {
"farming-labs-docs": {
"url": "https://docs.farming-labs.dev/api/docs/mcp"
}
}
}More setup docs
Manual setup
Cursor project or global config:
{
"mcpServers": {
"farming-labs-docs": {
"url": "https://docs.farming-labs.dev/mcp"
}
}
}VS Code workspace config:
{
"servers": {
"farming-labs-docs": {
"type": "http",
"url": "https://docs.farming-labs.dev/mcp"
}
}
}Claude Code:
claude mcp add-json farming-labs-docs '{"type":"http","url":"https://docs.farming-labs.dev/mcp"}'Local development uses http, not https
If you are connecting to a local Next dev server, use http://127.0.0.1:3000/mcp or
http://127.0.0.1:3000/.well-known/mcp.
Using https://localhost:3000/... against a non-TLS dev server will fail with SSL errors.
Test the Next example
This repo's Next example exposes the default MCP endpoint directly.
Start the example:
pnpm --dir examples/next devThen connect your MCP client or inspector to:
http://127.0.0.1:3000/mcp
http://127.0.0.1:3000/.well-known/mcpThe built-in HTTP route exposes the full MCP surface:
-
list_search_facets -
list_docs -
list_pages -
get_navigation -
search_docs -
read_page -
get_code_examples -
get_config_schema -
get_context -
list_content_changes -
hydrate_content_changes
What to ask it
-
list_page_sectionsOnce your MCP client is connected, these are good first checks: -
list_docsforgetting-startedbefore deciding which page to read -
search_docsforfeedback,page actions, ormcp -
read_pagefor/docs/configurationorinstallation -
get_navigationto inspect the docs tree -
list_pagesto confirm the server is loading the expected docs set -
get_code_examplesfor runnable Next.js or pnpm snippets -
get_config_schemaformcp.toolsbefore editingdocs.config.ts -
get_contextwhen you want section-level context constrained to a token budget
In Cursor or VS Code, natural prompts like these work well too:
Search the Farming Labs docs for page feedback setupRead the configuration page and summarize MCP configFind the page actions docs and tell me how openDocs works
What the tools return
-
list_docsreturns page summaries grouped by section; passsectionsuch asgetting-startedto narrow the result -
list_pagesreturns page titles, slugs, and URLs -
list_page_sectionsreturns body-free section discovery metadata (ids, headings, levels, parent ids, line ranges, token estimates, and markdown URLs) for a given page -
get_navigationreturns the docs tree in a readable text outline -
list_search_facetsreturns facet values forframework,version,package, andtags. Three optional inputs refine the request:facet(one offramework | version | package | tags) pins the response to a single field and enables cursor-based pagination;limit(1–100) caps the number of values returned;cursoris the opaque continuation token from a previous response. When the response includeshasMore: trueon a facet, pass that facet'snextCursorvalue ascursor(with the samefacetargument) to retrieve the next page of values. -
search_docsranks pages by title, description, and content matches -
read_pageaccepts either a slug likeinstallationor a full docs path like/docs/installation; passsectionfor one heading andmaxCharsto cap both successful content and section-not-found output -
get_code_examplesreturns fenced code blocks with parsed metadata liketitle,framework,packageManager, andrunnable -
get_config_schemareturnsdocs.config.tsoption metadata with paths, types, defaults, descriptions, and examples -
get_contextdeterministically selects matching sections, applies optionalframework,version, andlocalescopes, and reports source anchors plus conservative UTF-8 byte accounting -
hydrate_content_changesis called afterlist_content_changesto retrieve the full section bodies and deletion tombstones for a batch of changed documents; it accepts atokenBudgetparameter (default5000; min4; max32000) that caps the total hydrated content returned. Enable it withmcp.tools.hydrateContentChanges: trueindocs.config.ts. Forframework,version, andlocale, a page that declares a different value is excluded. A page that omits a scope field is treated as general documentation and remains eligible for that scope. Results are ranked deterministically, with source URLs resolving equal-score ties.
Declare scopes with optional top-level page frontmatter. Quote numeric versions so they remain strings:
---
title: "Next.js setup"
framework: "nextjs"
version: "16"
locale: "en"
---Every successful built-in tool result includes both MCP structuredContent validated against its
advertised outputSchema and the existing text content for clients that have not adopted structured
results yet.
Example tool calls:
{
"name": "list_docs",
"arguments": {
"section": "getting-started"
}
}{
"name": "get_code_examples",
"arguments": {
"query": "docs.config.ts",
"framework": "nextjs"
}
}{
"name": "get_context",
"arguments": {
"query": "configure MCP search",
"framework": "nextjs",
"version": "16",
"locale": "en",
"tokenBudget": 4000
}
}tokenBudget defaults to 4000 and accepts values from 256 through 32000. The dependency-free
budget strategy treats that value as a conservative UTF-8 byte ceiling: the complete assembled
context, including source headers and separators, never exceeds tokenBudget encoded bytes. The
response reports strategy: "utf8-bytes", maxUtf8Bytes, usedUtf8Bytes,
remainingUtf8Bytes, and conservativeTokenUpperBound; each source reports its own utf8Bytes.
The upper bound intentionally overstates many tokenizer counts instead of presenting a
tokenizer-specific estimate as a hard guarantee.
Section parsing is shared by search, read_page, and get_context. It ignores headings inside
fenced code, supports ATX headings indented by up to three spaces and Setext headings, cleans inline
links and formatting from labels, and assigns stable -1, -2, and later suffixes to duplicate
anchors. Anchored search hits that no longer resolve are skipped instead of being attributed to
whole-page content. When a requested read_page.section is missing, maxChars also bounds the JSON
error and the included list of available headings.
{
"name": "get_config_schema",
"arguments": {
"option": "mcp.tools.listDocs"
}
}The built-in resources mirror the navigation and page content data:
docs://navigationdocs://docsdocs://docs/installationdocs://docs/guides/quickstart
When to use this vs markdown routes and llms.txt
- Use markdown routes when you want a plain HTTP URL for one page; in Next.js,
Signature-Agentcan return markdown from the canonical page URL too - Use
llms.txtwhen you want a static, crawler-friendly docs summary - Use
robots.txtwhen crawlers and AI agents need an explicit policy that allows docs and machine-readable routes - Use MCP when you want a structured, queryable docs interface for agents and IDE tools
They complement each other well. Markdown routes are the simplest page-level HTTP surface,
llms.txt is lightweight and public, robots.txt makes access policy explicit, and MCP is
interactive and tool-based.
Expose your docs as MCP tools and resources over stdio, /mcp, or /.well-known/mcp
How is this guide?
