Posts

Improving the Backstage MCP Server with MCP Kernel

Updating my Backstage MCP server with MCP Kernel, correct Catalog queries, rotating credentials, and tests through the packaged server.

September 19, 2026 7 min read 1279 words

In this article

An MCP server for Backstage needs to preserve the meaning of a Catalog operation all the way from the tool call to the backend. A valid JSON response is only useful if the server asked the right question.

I’ve been updating my Backstage MCP Server around that requirement: improve the Catalog integration, make the runtime boundaries explicit, and verify the process an MCP client actually launches.

If you are new to the project, start with Introducing the Backstage MCP Server for what it does, example Catalog lookups, and client setup.

The server now uses @coderrob/mcp-kernel for its core. The generic runtime has become a published dependency, leaving the Backstage repository responsible for its application and Catalog behavior.

That separation is the architectural change. The useful part is what it makes easier to understand, maintain, and check.

Keep the Backstage Work About Backstage

The server exposes Catalog tools over stdio. They cover entity queries, references, ancestry, facets, source locations, descriptor validation, location registration, refresh requests, and removal operations, along with focused ownership, membership, and dependency lookups. The generated tool manifest records the actual surface.

For an assistant, that supports a practical sequence: find a component, inspect its Catalog metadata, locate its source descriptor, and validate a proposed descriptor before making an authorized change.

For the integration, each step needs a clear contract. Which inputs are accepted? What does a missing entity mean? Does this operation mutate anything? Which dependency makes the upstream call?

Those questions get harder to answer when protocol registration, service access, logging, and business behavior are tangled together. Updating one Catalog tool should be a change you can follow without reconstructing the entire server in your head.

MCP Kernel Owns the Shared Runtime

The kernel extraction moved generic MCP behavior into the published @coderrob/mcp-kernel package. The dependency direction is straightforward:

MCP client
  <-> stdio JSON-RPC
  <-> Backstage MCP process
       -> MCP Kernel runtime
       -> Backstage tool handler
       -> Catalog adapter
       -> official Backstage CatalogClient
       -> Backstage Catalog API

MCP Kernel owns feature definitions, registration, lifecycle, middleware, execution policies, transport support, results, and protocol-safe logging. The Backstage application supplies its Catalog plugin and authenticated service dependency. The architecture overview documents the split.

You can see it in the server composition code: createBackstageServer calls the kernel’s createMcpServer, registers backstageCatalogPlugin, adds request logging, and supplies catalogClient through application context.

Tool handlers receive that context. They do not need to construct a client, discover configuration, or manage the process lifecycle themselves.

The source-layout check also enforces the published dependency boundary and rejects local kernel imports. I like that detail. An architecture rule is much easier to preserve when the repository can tell you that you just broke it.

The Backstage package retains kernel API re-exports for compatibility. For a new generic MCP application, import the kernel directly. You should not need a Backstage dependency to build something that has nothing to do with Backstage.

Correct Queries Beat Plausible Results

One of the most useful improvements is keeping Backstage’s official CatalogClient responsible for Catalog routes and serialization. The adapter supplies configuration and authentication; the upstream client handles the API mechanics.

Filter semantics are a good example of why that matters. These arguments to get_entities select Components or APIs in the default namespace:

{
  "filter": {
    "kind": ["Component", "API"],
    "metadata.namespace": "default"
  },
  "fields": ["kind", "metadata.name", "metadata.namespace"],
  "limit": 25
}

The logic is:

(kind = Component OR kind = API)
AND metadata.namespace = default

Keys within one filter record are AND conditions. Values for one key are OR conditions. Multiple filter records are OR alternatives. Empty records, empty string values, and empty value arrays are rejected during MCP input validation. The Catalog integration guide documents these rules.

A filter that silently disappears can leave you with valid-looking results from a much broader query. The request succeeded. The question changed. That is an especially unhelpful kind of success.

The integration also uses the current queryEntities API for both get_entities and the compatibility name get_entities_by_query. Cursor continuation forwards cursor, fields, and limit; Backstage carries the initial query semantics in its opaque cursor.

Other adapter contracts cover reference batching, missing references, ancestry and location routes, validation bodies, and location dry runs. These are small details with visible consequences. If dryRun: true is supposed to avoid writing a location, its placement in the HTTP request is part of the behavior you need to verify.

Credentials Need an Operational Path

The server supports a bearer token supplied through BACKSTAGE_TOKEN or a file selected by BACKSTAGE_TOKEN_FILE. The file takes precedence and is read before each outgoing request, so an external process can rotate the credential without restarting the MCP server. The authentication documentation explains the supported Backstage external-access setup.

For example, in a shell environment with a mounted token file:

export BACKSTAGE_BASE_URL=https://backstage.example.com
export BACKSTAGE_TOKEN_FILE=/run/secrets/backstage-token
corepack yarn start

The rotation mechanism belongs to your secret-management setup. The server’s responsibility is to pick up the current token and attach it at the Catalog fetch boundary.

Backstage permissions still determine what that credential can do. Tool annotations identify read-only and destructive operations for MCP clients, but an annotation does not grant or enforce backend permission.

The distinction also applies to the kernel’s policies: local caching, rate limiting, and timeouts have a defined scope. They are useful application controls, and the deployment still owns its authentication and access decisions.

Test the Server the Client Will Launch

A handler test can pass while the packaged process fails to load, emits invalid protocol output, or constructs the wrong authenticated HTTP request.

The repository’s MCP verification gate exercises those boundaries:

corepack yarn test:mcp
corepack yarn manifest:check

test:mcp runs contract tests, builds the distribution, and launches dist/cli.cjs through stdio. An official MCP SDK client discovers and invokes the registered tools against a deterministic authenticated Catalog stub. The stub checks the requests produced by the server. A separate MCP Inspector stage validates discovery and a tool invocation through another client path.

That gives the test evidence from both ends: an MCP request crossed the process boundary, and the server made the expected Catalog request.

The normal quality gates add type checking, linting, architecture checks, unused-code checks, shell tests, and a 95% per-file coverage requirement. Coverage helps identify unexercised code; the protocol and adapter assertions check whether the exercised behavior makes sense.

The deterministic stub cannot prove that your deployment’s TLS, permissions, or Catalog data are configured correctly. For that, the repository provides an opt-in, read-only corepack yarn test:live check using your configured Backstage backend.

I want that distinction visible. A test result should tell you what it established.

Try the Updated Integration

The current source requires Node.js 24.15 or newer and Corepack. Build it from the repository:

git clone https://github.com/Coderrob/backstage-mcp-server.git
cd backstage-mcp-server
corepack enable
corepack yarn install --immutable
corepack yarn build

Configure the backend URL and credential source, then point your MCP client’s stdio configuration at node with the absolute path to dist/cli.cjs. The README includes the configuration shape. Keep credentials in the client’s environment or secret mechanism.

The Backstage repository’s dependency manifest currently uses MCP Kernel ^0.1.1 with MCP SDK v1 and Zod 3. Use its checked-in lockfile and setup instructions when trying this integration; the kernel’s own development branch can move ahead independently.

That independence is part of the improvement. MCP Kernel can evolve the reusable runtime, while the Backstage server concentrates on faithfully exposing the Catalog.

My goal is for you to spend less time wondering what an integration did with your request. Clear schemas, correct upstream behavior, explicit ownership, and tests through the built process all help answer that question.

An assistant can only use a tool reliably when the tool does what it says. That is the part I want to keep improving.

-Rob