Posts
Improving the Backstage MCP Server with MCP Kernel
How MCP Kernel simplifies my Backstage MCP server, with clearer Catalog queries and token rotation without restarts.
In this article
I’ve updated my Backstage MCP Server to use MCP Kernel for the shared MCP code. Alongside that change, I’ve tightened up Catalog queries and added support for rotating credentials without restarting the server.
If you are new to the project, start with Introducing the Backstage MCP Server for what it does, example Catalog lookups, and client setup.
Moving the Shared Code into MCP Kernel
The server lets an assistant look up Catalog entities, inspect ownership and dependencies, validate descriptors, and manage locations. Updating one of those tools should not require working through protocol registration and process cleanup too.
The kernel extraction moved that shared code into @coderrob/mcp-kernel. A request now follows this path:
MCP client
<-> stdio JSON-RPC
<-> Backstage MCP process
-> MCP Kernel runtime
-> Backstage tool handler
-> Catalog adapter
-> official Backstage CatalogClient
-> Backstage Catalog API
MCP Kernel handles registration, lifecycle, middleware, policies, and protocol-safe logging. The Backstage application registers backstageCatalogPlugin and supplies an authenticated catalogClient through context. Handlers receive the client instead of constructing it themselves.
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.
Getting Catalog Queries Right
The adapter now leaves Catalog routes and serialization to Backstage’s official CatalogClient. It supplies configuration and authentication, then lets the upstream client build the request.
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 return a much broader set of entities. You get a successful response to a question you didn’t ask. That’s a difficult bug to spot if you’re only checking for errors.
Both get_entities and the compatibility name get_entities_by_query use queryEntities. Subsequent pages forward cursor, fields, and limit; Backstage’s cursor preserves the original query.
Rotating Tokens Without a Restart
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
Your secret-management setup replaces the file’s contents. The server picks up the token on its next request.
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.
Moving the shared code into MCP Kernel gives me less plumbing to maintain here. I can spend that time on the Backstage details: whether a filter asks the right question, pagination preserves it, and the next request uses the right credential. Those are the improvements I care about when I’m using the server too.
-Rob