# Astro

> Frontmatter-only reads, graftRoute mounts, and no request memo.

<Tabs labels="pnpm, npm, bun">

<Tab>

```sh
pnpm add @usegraft/sdk-astro @usegraft/auth @usegraft/mcp
```

</Tab>

<Tab>

```sh
npm i @usegraft/sdk-astro @usegraft/auth @usegraft/mcp
```

</Tab>

<Tab>

```sh
bun add @usegraft/sdk-astro @usegraft/auth @usegraft/mcp
```

</Tab>

</Tabs>

Build the handle once in a server-only module, then import it from frontmatter,
endpoints, and middleware. Never from a client island.

## Create the handle

`graft init` scaffolds the static index. This docs site is that path:

```ts
// src/lib/graft.ts
import { openStaticIndex } from "@usegraft/db";
import { createGraft } from "@usegraft/sdk-astro";
import { collections } from "../../graft.config";

export const graft = createGraft({
  index: await openStaticIndex(".graft/index.db"),
  collections,
});
```

On Postgres:

```ts
import { createDb } from "@usegraft/db";
import { createGraft } from "@usegraft/sdk-astro";
import { collections } from "../../graft.config";

export const db = createDb(process.env.DATABASE_URL!).db;
export const graft = createGraft({ db, collections });
```

## Read one document

```astro
---
// src/pages/[slug].astro
import { graft } from "../lib/graft";

const page = await graft.getContent("pages", Astro.params.slug!);
---
```

Astro frontmatter is server-side unconditionally — the only adapter here that
is. There is no request-level memo, because `React.cache` has no Astro
equivalent. Reads go straight to the index, which is the right default:
prerendered pages read at build time, and an SSR page makes a handful of reads.

## Mount functions and MCP

`graftRoute` binds a Graft handler to an Astro endpoint. It takes the handler,
not a config object: Astro hands over an `APIContext`, and the handlers want
its `request`.

```ts
// src/pages/api/fn/[name].ts
import { createFunctionsHandler } from "@usegraft/core";
import { graftRoute } from "@usegraft/sdk-astro";
import { functions } from "../../../graft.config";
import { db } from "../../lib/graft";

const handler = createFunctionsHandler({ db, functions });

export const POST = graftRoute(handler);
export const GET = graftRoute(handler); // 405s with Allow and a fix
```

```ts
// src/pages/api/mcp.ts
import { createActorResolver } from "@usegraft/auth";
import { createGraftMcpHandler } from "@usegraft/mcp";
import { graftRoute } from "@usegraft/sdk-astro";
import { collections, functions } from "../../graft.config";
import { db } from "../lib/graft";

const actor = createActorResolver({
  issuers: (process.env.GRAFT_TRUSTED_ISSUERS ?? "")
    .split(/[,\s]+/)
    .filter(Boolean)
    .map((issuer) => ({ issuer })),
  devTokens: process.env.GRAFT_DEV_TOKEN
    ? {
        [process.env.GRAFT_DEV_TOKEN]: {
          kind: "human",
          id: "owner",
          scopes: ["content:write"],
        },
      }
    : undefined,
});

export const POST = graftRoute(
  createGraftMcpHandler({
    contentDir: "./content",
    db,
    collections,
    functions,
    actor,
  }),
);
```

This endpoint serves content writes and asset uploads. JWT verification runs
only when `issuers` is passed; the library does not read
`GRAFT_TRUSTED_ISSUERS` (`graft serve` does). See [Auth](/docs/auth).
Unauthenticated callers get 401. `allowAnonymous: true` is only for a
loopback local server — never on anything reachable from a network. A public
docs surface is [`createDocsMcpHandler`](/docs/docs-mcp), which has no write
tools and no anonymous opt-in.

The parameter is typed structurally as `{ request: Request }`, so this package
needs no `astro` dependency.

## Cache and invalidation

Astro has no tag-based data cache. Stamp `tagsFor(...)` into a CDN
surrogate-key header on SSR responses, and purge
`tagsForChanges(branch, changeSet)` from your compile webhook.

```astro
---
import { tagsFor } from "@usegraft/sdk-astro";
import { graft } from "../lib/graft";

const slug = Astro.params.slug!;
const page = await graft.getContent("pages", slug);
Astro.response.headers.set("Cache-Tag", tagsFor("main", "pages", slug).join(","));
---
```

`Cache-Tag` is the surrogate-key header. Purge the same strings
`tagsForChanges` returns from your compile webhook. The Postgres handle
snippet should `export const db` next to `graft` so the mounts above can
import it.

## The caveat you cannot infer

Bodies come back as authored source. Render them with Astro's own MDX pipeline.
There is no `MdxBody` here — that evaluator is Next-only.

See [SDK reference](/docs/sdk-reference) for mounts across adapters and the
honest gaps.
