For agents · MCP
Four read-only actions, returning Markdown
The agent-facing surface is not a second product. It is the same index the portal reads, exposed through Backstage's own MCP plumbing, returning the same Markdown an author wrote.
01
There is no bespoke MCP server here
Colophon registers four actions with Backstage's Actions Registry. The MCP Actions Backend does the rest: transport, session handling, authentication, and which actions a given server exposes. Writing a standalone MCP server would have meant re-implementing all of that, and re-implementing the auth part badly.
All four actions declare readOnly and idempotent.
Nothing an agent can call here mutates a bundle, a channel, or the index.
02
The surface
| Action | Returns | Reached for when |
|---|---|---|
colophon:list-entities |
Catalog entities that have documentation, the bundle each points at, its channels, and a portal URL. | Orientation. Without it an agent has to guess whether a component has docs at all, which usually means a wasted search. |
colophon:list-pages |
The navigation tree as an indented outline, plus a page index with slugs, types, tags and status. | Before searching, when the shape of the corpus is unknown. Far cheaper than guessing at search terms. |
colophon:search |
Ranked sections — not whole pages — each with its heading trail, its slug and anchor, and a citable portal URL. | The default entry point. Narrows by entity, bundle, Diátaxis type, tags and channel. |
colophon:get-page |
One page in full as raw Markdown — or just one section, if an anchor is supplied. | After a search or a listing returns a promising slug. |
An action's description is the only thing a model reads before deciding whether to call it, so those descriptions are written as carefully as the code. The search description has to say what a "chunk" is, why the breadcrumb is there, and that the results are citable — a model choosing between this and a generic web search decides on those few sentences alone.
03
Markdown, never HTML
This is the whole reason the plugin stores Markdown as its canonical artifact. An agent asked to reason about a configuration table or reproduce a code sample should not have to unpick a rendered DOM first — and rendered documentation DOMs are hostile: syntax-highlighting spans threaded through every token, tables expressed as nested containers, anchors injected beside every heading.
Keeping the wire format identical to the stored format also means there is one format to get right. The frontend's API client returns pages as Markdown too, for the same reason.
04
Designed for how agents actually fail
Truncation is stated, never silent
Every paginated response carries total, returned,
remaining, a nextOffset when there is more, and a
plain-language note saying so. An agent that does not know its
results were cut will confidently answer from a third of them.
The nav tree arrives as an outline, not as nested JSON
colophon:list-pages renders the recursive navigation to an
indented Markdown list. Agents read it in one glance, it survives JSON-schema
conversion without a self-referential $ref, and it costs a
fraction of the tokens the nested structure would.
Scope is enforced, not advisory
When an entity is scoped to a subtree with the #subpath form of
the annotation, both the page index and the navigation tree are filtered to
that subtree, and a get-page for a slug outside it returns not
found. One monorepo docs tree can serve many components without any of them
being able to read the others' pages by guessing a slug.
Anchors keep the context bill down
get-page accepts the anchor that search just
returned and gives back only that section, flagged with
partial: true. The status field is documented in the
schema as something the agent must surface: a
draft or deprecated page has to be flagged to the
user rather than quietly answered from.
05
A typical sequence
Orient, narrow, read. The listing actions exist so that the search does not have to carry the whole job.
▸ colophon:list-entities
{}
→ payments-api github.com/brnby/payments-api channels: latest, 1.x
billing github.com/brnby/platform#services/billing channels: latest
note: "Showing all 2 entities."
▸ colophon:search
{ "query": "rotate database credentials",
"entityRefs": ["component:default/payments-api"],
"type": "how-to" }
→ breadcrumb: ["Payments API", "Operations", "Rotating credentials"]
slug: "operations/credentials"
anchor: "rotating-credentials"
url: "https://backstage.example.com/catalog/default/component/payments-api/docs/…"
text: "## Rotating credentials\n\nCredentials are issued by …"
note: "Showing all 3 matches."
▸ colophon:get-page
{ "entityRef": "component:default/payments-api",
"slug": "operations/credentials",
"anchor": "rotating-credentials" }
→ status: "current"
partial: true
markdown: "## Rotating credentials\n\n1. Request a new secret …"
The sequence above is written to show the shape of the exchange. The action names, parameters and output fields are the ones the backend really declares; the entities, pages and text are made up, because there is no running instance to draw them from.
06
An agent sees what its user sees
Because the actions run inside Backstage rather than beside it, they inherit Backstage's identity model. Enabling per-user OAuth for MCP clients means a developer's agent retrieves exactly the documentation that developer is allowed to read — not a service account's view of the catalog.
backend:
actions:
pluginSources: [colophon, catalog]
mcpActions:
servers:
colophon:
name: Colophon Documentation
filter:
include:
- id: 'colophon:*'
# Per-user OAuth for MCP clients. For service agents, use
# backend.auth.externalAccess with a static token instead.
auth:
clientIdMetadataDocuments:
enabled: true
Exposure is configured, not coded. Colophon registers its
actions with the registry; which of them any MCP server exposes is decided by
the mcpActions filter. Narrowing an organisation's agents to
colophon:search and colophon:get-page is a config
change, not a fork.