SpandrelA framework that turns markdown file trees into governed knowledge graphs — philosophy, spec, and reference implementation

Static + flat-file MCP

Deploy a Spandrel knowledge graph as static files plus a thin MCP shim — works on GitHub Pages, Vercel, Cloudflare, or embedded in an existing site.

Static + flat-file MCP

The simplest production-ready deployment: spandrel publish writes a bundle of flat files; a thin serverless function translates MCP tool calls into fetches against those files. Read-only, cheap, embeddable in any existing site.

What gets emitted

spandrel publish <path> --static --base <path> --site-url <origin> produces:

_site/
├── graph.json                  Structural skeleton (no bodies)
├── robots.txt                  Keeps crawlers on HTML, off .md/.json
├── index.html                  Prerendered root page + SPA bundle shell
├── index.md                    Root node markdown
├── index.json                  Root node full JSON
├── <path>/
│   ├── index.html              Prerendered per-node page
│   ├── index.md
│   └── index.json
├── <path>.md                   Sibling form (scrape-friendly)
├── <path>.json
├── assets/                     SPA bundle (CSS, JS)
└── CNAME                       If one existed in the source repo

Three formats per node at each path, two URL layouts for .md and .json (sibling and directory), all with sensible MIME types and robots.txt pointing search engines at the HTML.

Where to host it

Anywhere that serves static files:

  • GitHub Pages — zero-config, GitHub Actions republishes on push to main. --base /<repo>/ matches project-pages URL structure.
  • Netlify / Vercel CDN — drag-and-drop or Git-integrated. Both offer built-in password protection at the edge.
  • S3 + CloudFront — classic static hosting. Any CDN in front of object storage works.
  • A subdirectory of an existing site — drop the bundle into /kb/ on your existing server, serve alongside the rest of your site.

Adding MCP to the bundle

The bundle alone gives humans a viewer and agents scrape-friendly URLs. To add a real MCP endpoint that agents can speak to, deploy a thin HTTP handler alongside the bundle. The wiring:

  1. Construct a RemoteGraphStore pointed at the bundle URL — reads graph.json and per-node files over HTTP.
  2. Build a Spandrel GraphQL schema from the store — createSchema(store) from spandrel/schema.
  3. Build an MCP server from the schema — createMcpServer(schema) from spandrel/server/mcp.
  4. Wrap the MCP server in the MCP SDK's StreamableHTTPServerTransport and mount under /mcp.
// api/mcp.ts on Vercel (or equivalent for any runtime)
import { createSchema } from "spandrel/schema";
import { createMcpServer } from "spandrel/server/mcp";
import { RemoteGraphStore } from "spandrel/storage/remote-graph-store";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp";

const store = new RemoteGraphStore({
  bundleUrl: process.env.SPANDREL_BUNDLE_URL!,
});
const schema = createSchema(store);
const server = await createMcpServer(schema, { graph: store });

export default async function handler(req, res) {
  const transport = new StreamableHTTPServerTransport(/* ... */);
  await server.connect(transport);
  await transport.handleRequest(req, res);
}

The RemoteGraphStore reads from the bundle URL on every tool call — graph.json once (cached), per-node index.json on demand. Write tools reject at the store layer, so agents cannot modify a static bundle no matter what they try.

The same pattern works on Vercel Edge Functions, Cloudflare Workers, Netlify Functions, or plain Node — only the outer handler signature changes.

No database, no compile step at request time. The serverless function is the only non-static piece.

Password-protecting the bundle

Static-file auth happens at the HTTP layer, not inside Spandrel. Options in ascending effort:

  1. Host-specific password — Netlify's Visitor Access, Vercel's Deployment Protection. Single shared password.
  2. Basic Auth via middleware — Vercel Edge Middleware, Cloudflare Workers, or .htaccess on Apache. Env-var driven.
  3. Cloudflare Access — identity-based (SSO, email OTP, device posture). Free tier up to 50 users. Works in front of any origin.

MCP clients pass the corresponding Authorization header via the headers field in the Claude Desktop config.

What this deployment can't do

  • Writes from agents or users — the bundle is immutable until the next publish. Authoring happens at the source, republish is the write path.
  • Identity-aware reads — all authenticated requests see the same bundle. For per-user views, use a writable backend.
  • Federation across repos — shared collections mounted across multiple tenants need a shared live backend.
  • Semantic search — embeddings require compute the static bundle can't provide. Ship a pre-built search index at publish time if you need search at scale.

Trade-off summary

Want this Use static + flat-file MCP
Public or shared-team read-only knowledge base
MCP access from agents without running a server
Drop into an existing website subdirectory
~$0 hosting
Agents write to the graph Use a writable backend
Per-user governed reads Use a writable backend
Live updates without a publish cycle Use a writable backend