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.

How an agent reaches Colophon's actions A coding agent acting as an MCP client talks to Backstage's MCP Actions Backend, which handles transport, authentication and filtering. That backend reads from the Actions Registry, where plugins register their actions. Colophon contributes four of them: colophon:list-entities, colophon:list-pages, colophon:search and colophon:get-page. All four read from the same Postgres index and object storage the portal uses. REGISTERED BY COLOPHON Coding agent MCP client MCP Actions Backend transport · auth · filtering Actions Registry plugin-contributed colophon:list-entities colophon:list-pages colophon:search colophon:get-page Postgres index · object storage the same source the portal reads
Colophon supplies the four boxes on the right. Everything to their left is Backstage, configured rather than written.

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.

Illustrative — real action names and fields, invented content
▸ 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.

app-config.yaml
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.