graft. docsbeta

Astro

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

pnpm add @usegraft/sdk-astro @usegraft/auth @usegraft/mcp
npm i @usegraft/sdk-astro @usegraft/auth @usegraft/mcp
bun add @usegraft/sdk-astro @usegraft/auth @usegraft/mcp

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:

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

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

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

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

---
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 for mounts across adapters and the honest gaps.