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:
list_docslist_pagesget_navigationsearch_docsread_pageget_code_examplesget_config_schema
It also exposes resources for:
docs://navigation- one
docs://…resource per page
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,
},
});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 } from "@farming-labs/docs";
import { docsServer } from "@/lib/docs.server";
export const Route = createFileRoute("/$")({
server: {
handlers: {
GET: async ({ request }) => {
const url = new URL(request.url);
if (isDocsMcpRequest(url)) return docsServer.MCP.GET({ request });
if (isDocsPublicGetRequest("docs", url, request)) return docsServer.GET({ request });
return new Response("Not Found", { status: 404 });
},
POST: async ({ request }) =>
isDocsMcpRequest(new URL(request.url))
? docsServer.MCP.POST({ request })
: new Response("Not Found", { status: 404 }),
DELETE: async ({ request }) =>
isDocsMcpRequest(new URL(request.url))
? docsServer.MCP.DELETE({ request })
: new Response("Not Found", { status: 404 }),
},
},
});SvelteKit
import { isDocsMcpRequest, isDocsPublicGetRequest } from "@farming-labs/docs";
import { GET, MCP } from "$lib/docs.server";
export async function handle({ event, resolve }) {
const method = event.request.method.toUpperCase();
if (isDocsMcpRequest(event.url)) {
if (method === "POST") return MCP.POST({ request: event.request });
if (method === "DELETE") return MCP.DELETE({ request: event.request });
return MCP.GET({ request: event.request });
}
if ((method === "GET" || method === "HEAD") && isDocsPublicGetRequest("docs", event.url, event.request)) {
return 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, POST, MCP } = createDocsServer({
...config,
});Astro
import { isDocsMcpRequest, isDocsPublicGetRequest } from "@farming-labs/docs";
import { GET, MCP } from "./lib/docs.server";
export async function onRequest(context, next) {
const method = context.request.method.toUpperCase();
if (isDocsMcpRequest(context.url)) {
if (method === "POST") return MCP.POST({ request: context.request });
if (method === "DELETE") return MCP.DELETE({ request: context.request });
return MCP.GET({ request: context.request });
}
if ((method === "GET" || method === "HEAD") && isDocsPublicGetRequest("docs", context.url, context.request)) {
return 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 } = 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.
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.
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/mcpDocs 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_docslist_pagesget_navigationsearch_docsread_pageget_code_examplesget_config_schema
What to ask it
Once your MCP client is connected, these are good first checks:
list_docsforgetting-startedbefore deciding which page to readsearch_docsforfeedback,page actions, ormcpread_pagefor/docs/configurationorinstallationget_navigationto inspect the docs treelist_pagesto confirm the server is loading the expected docs setget_code_examplesfor runnable Next.js or pnpm snippetsget_config_schemaformcp.toolsbefore editingdocs.config.ts
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 resultlist_pagesreturns page titles, slugs, and URLsget_navigationreturns the docs tree in a readable text outlinesearch_docsranks pages by title, description, and content matchesread_pageaccepts either a slug likeinstallationor a full docs path like/docs/installationget_code_examplesreturns fenced code blocks with parsed metadata liketitle,framework,packageManager, andrunnableget_config_schemareturnsdocs.config.tsoption metadata with paths, types, defaults, descriptions, and examples
Example tool calls:
{
"name": "list_docs",
"arguments": {
"section": "getting-started"
}
}{
"name": "get_code_examples",
"arguments": {
"topic": "docs.config.ts",
"framework": "nextjs"
}
}{
"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.
How is this guide?
