Home /

docs

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:

It also exposes resources for:

Default behavior

MCP is enabled by default.

docs.config.ts
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:

Public MCP routes
/mcp
/.well-known/mcp

This 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:

docs.config.ts
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.

next.config.ts
import { withDocs } from "@farming-labs/next/config";

export default withDocs();

TanStack Start

src/routes/$.ts
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

src/hooks.server.ts
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:

src/lib/docs.server.ts
import { createDocsServer } from "@farming-labs/svelte/server";
import config from "./docs.config";

export const { load, GET, POST, MCP } = createDocsServer({
  ...config,
});

Astro

src/middleware.ts
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

server/middleware/docs-public.ts
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.

docs.config.ts
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:

app/api/internal/docs/mcp/route.ts
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:

docs.config.ts
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:

docs.config.ts
export default defineDocs({
  ai: {
    enabled: true,
    useMcp: true,
  },
});

If you changed the canonical MCP route, useMcp: true follows it automatically:

docs.config.ts
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.

docs.config.ts
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:

terminal
pnpx @farming-labs/docs mcp

Or with pnpm in an installed project:

terminal
pnpm exec docs mcp

By default the CLI reads docs.config.ts[x] from the project root. If your config lives somewhere else:

terminal
pnpm exec docs mcp --config src/lib/docs.config.ts

Use the hosted MCP endpoint

If you just want to try the live docs server, you can point your MCP client at:

Live MCP endpoint
https://docs.farming-labs.dev/mcp
https://docs.farming-labs.dev/.well-known/mcp

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/mcp

MCP config

{
  "mcpServers": {
    "farming-labs-docs": {
      "url": "https://docs.farming-labs.dev/api/docs/mcp"
    }
  }
}

More setup docs

Manual setup

Cursor project or global config:

.cursor/mcp.json
{
  "mcpServers": {
    "farming-labs-docs": {
      "url": "https://docs.farming-labs.dev/mcp"
    }
  }
}

VS Code workspace config:

.vscode/mcp.json
{
  "servers": {
    "farming-labs-docs": {
      "type": "http",
      "url": "https://docs.farming-labs.dev/mcp"
    }
  }
}

Claude Code:

terminal
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:

terminal
pnpm --dir examples/next dev

Then connect your MCP client or inspector to:

~
http://127.0.0.1:3000/mcp
http://127.0.0.1:3000/.well-known/mcp

The built-in HTTP route exposes the full MCP surface:

What to ask it

Once your MCP client is connected, these are good first checks:

In Cursor or VS Code, natural prompts like these work well too:

What the tools return

Example tool calls:

list_docs
{
  "name": "list_docs",
  "arguments": {
    "section": "getting-started"
  }
}
get_code_examples
{
  "name": "get_code_examples",
  "arguments": {
    "topic": "docs.config.ts",
    "framework": "nextjs"
  }
}
get_config_schema
{
  "name": "get_config_schema",
  "arguments": {
    "option": "mcp.tools.listDocs"
  }
}

The built-in resources mirror the navigation and page content data:

When to use this vs markdown routes and llms.txt

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.