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:
- Construct a
RemoteGraphStorepointed at the bundle URL — readsgraph.jsonand per-node files over HTTP. - Build a Spandrel GraphQL schema from the store —
createSchema(store)fromspandrel/schema. - Build an MCP server from the schema —
createMcpServer(schema)fromspandrel/server/mcp. - Wrap the MCP server in the MCP SDK's
StreamableHTTPServerTransportand 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:
- Host-specific password — Netlify's Visitor Access, Vercel's Deployment Protection. Single shared password.
- Basic Auth via middleware — Vercel Edge Middleware, Cloudflare Workers, or
.htaccesson Apache. Env-var driven. - 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 |