# Public docs MCP

> A second, read-only MCP endpoint for your documentation, separate from the private one.

*Let anyone's agent read your published docs, with no login.*

If your Graft project hosts documentation, people will ask their coding agents
about it. A public docs endpoint lets those agents search and read your docs
directly, instead of scraping web pages. It is read-only and needs no
authentication.

It is a separate server from your project's main MCP endpoint, not a setting on
it:

| Endpoint | Surface | Auth |
| --- | --- | --- |
| `POST /api/mcp` | Everything an agent can do | Required |
| `POST /mcp` | Documentation only | None |

## Why a separate endpoint

Docs platforms already publish this shape, and agents arrive expecting it.
Mintlify generates a docs MCP at `/mcp` for every site it hosts: public,
unauthenticated, strictly read-only. Cloudflare runs a documentation server
separately from its authenticated API server.

Keeping it separate also keeps it safe. If it were a flag on the main endpoint,
which also handles writes, one wrong setting could publish more than your docs.
As its own server, it can only ever serve the tools listed below.

## What it serves

Five tools and the document resources.

| Tool | Purpose |
| --- | --- |
| `list_collections` | What kinds of documents exist |
| `list_content` | The documents in one collection |
| `get_content` | One document's frontmatter and body |
| `search_content` | Full-text search across the published content |
| `explain_error` | What a Graft error code means |

Documents are also addressable as
`graft://<branch>/<collection>/<slug>` resources, so a client can attach a page
rather than paste it.

## What it leaves out, and why

It leaves out every write, as you would expect. It also leaves out several
reads, because they reveal how your project works rather than what your docs
say:

| Left out | Reason |
| --- | --- |
| `describe_schema` | Carries the project's functions |
| `list_registry`, `describe_item` | Carry the project's owned primitives |
| `list_branches`, `list_compilations`, `list_approvals` | Carry the project's operations |
| `graft://<branch>/schema` | Same payload as `describe_schema` |
| Prompts | Three of the four instruct `write_content`, which is not here |
| `write_content`, `delete_content`, `put_asset`, `run_function`, `decide_approval` | Writes and execution |

So the test for what belongs on a public endpoint is not "is it read-only?" but
"is it documentation?".

## Set one up

Create the handler with `createDocsMcpHandler` and mount it at `/mcp` on your
docs domain, which is where agents look. This is the route this site runs, on
the static engine:

`src/pages/mcp.ts` (Astro):

```ts
import type { APIRoute } from "astro";
import { resolve } from "node:path";
import { createDocsMcpHandler, type GraftMcpHandler } from "@usegraft/mcp";
import { collections } from "../../graft.config";
import { staticIndexPath } from "../lib/graft";

export const prerender = false;

let handler: GraftMcpHandler | null = null;

function getHandler(): GraftMcpHandler {
  handler ??= createDocsMcpHandler({
    name: "graft-docs",
    contentDir: resolve(import.meta.dirname, "../..", "content"),
    collections,
    staticIndexPath, // or db: createDb(url).db on Postgres
  });
  return handler;
}

export const POST: APIRoute = ({ request }) => getHandler()(request);
export const GET: APIRoute = ({ request }) => getHandler()(request);
```

`staticIndexPath` is the same absolute path your app's own reads open, imported
from where you resolve it. Avoid a relative path such as `".graft/index.db"`:
it is resolved from the working directory, and on a serverless host that is not
your project folder, so the handler would not find the index.

Export both `POST` and `GET`. Agents only send `POST`, but a `GET` should
answer `405` rather than `404`, which tells a client the endpoint exists and
only accepts `POST`.

### Options it does not accept

The docs handler only accepts `contentDir`, `collections`, `db`,
`staticIndexPath`, `branchId`, `scope`, `name`, and `version`. It has no option
for functions, identities, file uploads, or approvals, so they cannot be turned
on by mistake. Even if JavaScript code passes `functions` anyway, the handler
ignores it. Setting up the public endpoint can never expose the private one.

## Choose what is published

The server publishes the collections you hand it. To publish your docs but not
your product catalog, pass only the collections you mean:

```ts
createDocsMcpHandler({
  contentDir,
  collections: { docs },
  db,
});
```

> **Drafts are published too.**
> The endpoint reads your files directly. Anything in a published collection's
> folder is readable, including drafts you have not linked from any page.

## Other ways agents can read your docs

An MCP endpoint is one of several ways an agent can read documentation without
scraping rendered HTML. This site also serves:

| Surface | What it is |
| --- | --- |
| `/llms.txt` | An index of pages with descriptions and `.md` links |
| `/llms-full.txt` | Every page's content in one file |
| `/docs/<slug>.md` | Any page as markdown |

They are generated from the same content and grouping as the sidebar, so they
always match the site. Each page also answers `Accept: text/markdown` with its
Markdown version.

> **In the dev server.**
> `/llms.txt` is shadowed under `astro dev`, because Vite serves the project
> root as static and a Graft project has an `llms.txt` at its root. Production
> is correct. Verify that route with `astro build`, not the dev server.

## Next steps

- [Connecting agents](/docs/connecting-agents) documents the private endpoint.
- [MCP tools](/docs/mcp-tools) lists the full tool surface.
- [Agents overview](/docs/agent-surface) explains what agents can do.
