MCP Server
This document describes a planned subsystem. It sets out the intended shape of the MCP server so its dependencies — service-account auth, the shared operation layer, and relationship completeness — are settled first. Treat it as the design the implementation follows rather than a description of shipped code.
Companions:
- Transports — MCP is a peer transport in that family. It shares the operation layer with the HTTP bindings and differs only in how it surfaces operations (tools/resources/prompts vs HTTP routes). Read that first.
- Client SDK — every MCP tool delegates to
CollectionHandle/document-lifecycle. The hard problems (populate, status-aware reads, validation) are solved below the transport line. - Authentication & Authorization — MCP needs a non-interactive service-account token actor;
assertActorCanPerformgates every tool inside the service, not in the tool handler. - Relationships —
populate+hasManyare what make MCP reads rich; relationship completeness gates a satisfying MCP experience, hence the sequencing. - Markdown Export — the agent-readable representation already shipped:
documentToMarkdown,.mdroutes,llms.txt. The MCP content tools should serve these same representations rather than invent a parallel shape (that doc'sllms-full.txt/ MCP phase names this server as its consumer). - Content Management in the Time of AI — the "why": structured versioning, workflow, and provenance matter more when an LLM is the author. MCP is where that thesis meets a keyboard.
Overview
The Model Context Protocol lets an MCP host (Claude Desktop, Claude in the browser, IDE agents, Cowork) discover and invoke tools, read resources, and run prompts exposed by a server. An MCP server for Byline turns a Byline installation into a first-class participant in an agentic workflow: a model can query collections, draft documents, populate relationships, and move content through the publishing lifecycle — under a scoped actor, gated by the same abilities the admin UI obeys.
For an "AI-first headless CMS," this is not a side feature. It is the literal proof of the positioning: the place where Byline's structured storage, immutable versioning, and workflow lifecycle become tools an autonomous agent can use safely.
Why it fits Byline cleanly
The MCP server is a thin binding, not a new subsystem, because the work is already done below the transport line:
- The SDK is the substrate. A tool handler resolves a
RequestContext, calls oneCollectionHandle/document-lifecyclemethod, and serializes — the same shape as a TanStack server fn or an HTTP route. Populate, status-aware reads,beforeReadrow-scoping, andafterReadshaping all come for free. - Zod schemas are tool schemas. Byline already derives a Zod schema per collection (the schema builder). MCP tool
inputSchemais JSON Schema. So tool definitions are derived from collection definitions rather than hand-written — most CMSes have no equivalent asset. - The workflow lifecycle is the safety model. Immutable versioning +
defineWorkflowgive "AI drafts, humans publish" for free: the model creates a draft version; a human promotes it. No new safety machinery to invent.
Scope discipline (the demo is mostly not Byline)
The motivating demo — Claude researches a topic, gathers content, publishes to several social channels, then publishes to a production Byline — is Claude orchestrating several MCP servers: a web/research MCP, social-channel MCPs, and Byline's. Byline is one node in that graph.
┌──────────────── MCP host (Claude Desktop / Cowork) ───────────────┐ │ │ research MCP ──┐ ┌── social-X MCP ├──▶ model plans & orchestrates ──────────────────┼── social-Y MCP web/fetch ────┘ └── Byline MCP ──▶ production Byline (draft → review → publish)This is a feature — it showcases MCP composability — but it must not inflate Byline's build scope. Byline's job is one tight, safe MCP over its own SDK, nothing more. The social and research nodes are other people's servers.
Tool surface
Favor a small set of generic, collection-parameterised tools over a combinatorial explosion of per-collection-per-verb tools. A handful of well-named tools the model can reason about beats hundreds it must disambiguate:
Tool | Maps to | Notes |
| config / collection registry | discovery — what can I work with? |
|
| returns the Zod-derived field shape |
|
| status-aware; published-only by default |
|
| optional |
|
| always creates a draft (see Safety) |
|
| whole-document / field-level; patches stay admin-internal |
|
| gated; the human-promotion seam |
Two MCP affordances beyond tools are worth shipping:
- Resources — collections and individual documents exposed as browseable, attachable read-only context (
byline://collection/{path},byline://document/{path}/{id}). Lets a user attach content into a conversation rather than the model having to query for it. - Prompts — pre-baked workflows ("draft a blog post for the
postscollection from this research"), parameterised by collection. These encode the safe, intended authoring path so the model falls into the pit of success.
How a tool is wired
MCP host calls tool create_document { collection: "posts", data: {...} } │ ▼ ┌──────────────────────── packages/mcp ───────────────────────────┐ │ resolve service-account token ─▶ RequestContext (scoped Actor)│ │ inputSchema = Zod(posts) ─▶ validate args ─▶ structured errs │ ◀─ model self-corrects │ op.invoke(ctx, input) │ └──────────────────────────────┬───────────────────────────────────┘ ▼ document-lifecycle.create ──▶ assertActorCanPerform('collections.posts.create') ──▶ beforeCreate hook · write DRAFT version ──▶ serialize ──▶ tool resultThe shared operation layer from Transports means create_document and the HTTP POST /api/posts route invoke the same OperationDefinition — MCP differs only in binding (tool envelope, JSON-RPC over stdio / Streamable HTTP) rather than re-deciding what "create a post" means. Get this right and MCP is another binding; get it wrong and MCP is a fourth re-implementation of the contract.
Safety model
Autonomous writes to a production CMS are genuinely risky; the design leans on machinery Byline already has:
- Draft-by-default.
create_document/update_documentalways write a draft version. They cannot publish. - Publish is a separate, gated transition.
publish_documentis a workflowchangeStatuscall, ability-gated, and ideally surfaced as the human-promotion seam — the model proposes, a person disposes. The service account can be provisioned without the publish ability entirely, so a misbehaving agent physically cannot push live content. - Validation feedback loop. Bad input returns structured Zod errors (Byline's stable validation codes), which the model reads and self-corrects against — turning the schema into a guardrail rather than a wall.
- Provenance. Versions are immutable and attributable to the service-account actor, so "what did the AI write, and when" is answerable after the fact — the Content Management in the Time of AI thesis in action.
Authentication
MCP is non-interactive — there is no cookie, no login screen. It needs a service-account token model: a token mints a scoped Actor whose abilities are provisioned explicitly (e.g. collections.posts.read, collections.posts.create, but not collections.posts.publish). This is new auth work — today's auth is JWT-session, built for the admin UI — and it is shared with the HTTP API's bearer-token need (see Transports → the auth seam). The SessionProvider interface in @byline/auth is the extension point.
Transport and deployment
MCP defines two transports; both are relevant:
A) stdio — host launches the MCP process locally Claude Desktop ──spawn──▶ byline-mcp (local) ──HTTP──▶ remote Byline /api requires the HTTP boundary (Transports) to exist first.
B) Streamable HTTP — MCP server is a deployed, networked endpoint Claude (any host) ──HTTPS──▶ byline-mcp (co-located w/ Byline) ──in-proc──▶ @byline/client ──▶ Postgres the MCP server embeds the SDK and IS the remote boundary.Recommendation: (B), co-located Streamable HTTP, embedding @byline/client and talking straight to the adapter. It sidesteps needing a separate REST round-trip for the "publish to production Byline" story and lets MCP be its own remote boundary. Option (A) is viable too but couples MCP's ship date to the HTTP family's. Either way the MCP server is packages/mcp and consumes the shared operation layer.
Sequencing — why after hasMany
The thing that makes an MCP read experience impressive is rich, populated content. An MCP that cannot represent many-to-many relationships will feel thin, and relationship completeness (hasMany) is high on the TODO already. So:
- Land
hasManyand confirmpopulateis complete across relation cardinalities. - Land the operation layer +
http-nitro(proves the contract; see Transports). - Then build
packages/mcpas a binding over that proven surface.
Building MCP before the operation layer exists would mean inventing the contract inside the MCP package and re-inventing it again for HTTP — the precise drift Transports is structured to avoid.
Code map
Concern | Intended location |
MCP server entry / transport |
|
Tool definitions (generic verbs) |
|
Resource providers |
|
Prompt templates |
|
Zod → MCP inputSchema bridge |
|
Shared operation layer |
|
Service-account actor + provider | extends |