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:

  • Facets tool: list_search_facets is also enabled by default. Disable it with mcp.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_feedback when machine feedback is enabled

  • get_code_examples

  • get_config_schema

  • get_context

  • get_trust_metadata when agent.okf is enabled

  • explicitly allowlisted api_* operation tools when apiReference.mcp is 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.

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

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

The 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.

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,
  },
});

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.

docs.config.ts
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 null to receive 401 Unauthorized. With protectedResource, the response also includes a Bearer WWW-Authenticate challenge that points to the resource metadata.
  • Return a Web Response to 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 500 response; 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:

docs.config.ts
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 authenticate return null receive 401 with a Bearer challenge containing resource_metadata and the required scope
  • an authenticated principal missing any requiredScopes receives 403 with error="insufficient_scope"
  • a Response returned by authenticate remains authoritative, so add WWW-Authenticate to 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:

RFC 9728 protected-resource metadata
/.well-known/oauth-protected-resource/mcp
/.well-known/oauth-protected-resource/.well-known/mcp
/.well-known/oauth-protected-resource/api/docs/mcp

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

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

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

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

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

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,
  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

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

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

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

Astro

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

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

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.

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:

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

To print the MCP client setup instructions for your own Docs Cloud deployment, use mcp setup:

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

  • 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_sections Once your MCP client is connected, these are good first checks:

  • list_docs for getting-started before deciding which page to read

  • search_docs for feedback, page actions, or mcp

  • read_page for /docs/configuration or installation

  • get_navigation to inspect the docs tree

  • list_pages to confirm the server is loading the expected docs set

  • get_code_examples for runnable Next.js or pnpm snippets

  • get_config_schema for mcp.tools before editing docs.config.ts

  • get_context when 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 setup
  • Read the configuration page and summarize MCP config
  • Find the page actions docs and tell me how openDocs works

What the tools return

  • list_docs returns page summaries grouped by section; pass section such as getting-started to narrow the result

  • list_pages returns page titles, slugs, and URLs

  • list_page_sections returns body-free section discovery metadata (ids, headings, levels, parent ids, line ranges, token estimates, and markdown URLs) for a given page

  • get_navigation returns the docs tree in a readable text outline

  • list_search_facets returns facet values for framework, version, package, and tags. Three optional inputs refine the request: facet (one of framework | version | package | tags) pins the response to a single field and enables cursor-based pagination; limit (1–100) caps the number of values returned; cursor is the opaque continuation token from a previous response. When the response includes hasMore: true on a facet, pass that facet's nextCursor value as cursor (with the same facet argument) to retrieve the next page of values.

  • search_docs ranks pages by title, description, and content matches

  • read_page accepts either a slug like installation or a full docs path like /docs/installation; pass section for one heading and maxChars to cap both successful content and section-not-found output

  • get_code_examples returns fenced code blocks with parsed metadata like title, framework, packageManager, and runnable

  • get_config_schema returns docs.config.ts option metadata with paths, types, defaults, descriptions, and examples

  • get_context deterministically selects matching sections, applies optional framework, version, and locale scopes, and reports source anchors plus conservative UTF-8 byte accounting

  • hydrate_content_changes is called after list_content_changes to retrieve the full section bodies and deletion tombstones for a batch of changed documents; it accepts a tokenBudget parameter (default 5000; min 4; max 32000) that caps the total hydrated content returned. Enable it with mcp.tools.hydrateContentChanges: true in docs.config.ts. For framework, version, and locale, 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:

docs/guides/next-setup.mdx
---
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:

list_docs
{
  "name": "list_docs",
  "arguments": {
    "section": "getting-started"
  }
}
get_code_examples
{
  "name": "get_code_examples",
  "arguments": {
    "query": "docs.config.ts",
    "framework": "nextjs"
  }
}
get_context
{
  "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.

get_config_schema
{
  "name": "get_config_schema",
  "arguments": {
    "option": "mcp.tools.listDocs"
  }
}

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

  • docs://navigation
  • docs://docs
  • docs://docs/installation
  • docs://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-Agent can return markdown from the canonical page URL too
  • Use llms.txt when you want a static, crawler-friendly docs summary
  • Use robots.txt when 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.

Customize
Live preview on these docs
Reset
Customize
Export Code
Preset
Explore more themes
Primary Color
Primary
Colors
Background
Foreground
Muted Text
Border
Card
Muted BG
Ring
Layout
SidebarBordered
TOC StyleDirectional (tree)
TOC DepthH2 + H3
Ask AI
ModeFloating
Floating StyleFull Modal
PositionBottom Right
Features