Registering MCP Tools
Portal plugin developers can expose their plugin's capabilities as MCP tools by registering them with the Actions Registry. Once registered, tools are automatically available to any connected AI agent (AiKA, Claude Code, Cursor, etc.) through the MCP Actions Backend.
How to register a tool
Step 1: Register with the Actions Registry
In your plugin backend, inject the actionsRegistry service and register your tool during initialization:
import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
import { actionsRegistryExtensionPoint } from '@backstage/plugin-mcp-actions-node';
import { z } from 'zod';
export const myPlugin = createBackendPlugin({
pluginId: 'my-plugin',
register(env) {
env.registerInit({
deps: {
actionsRegistry: actionsRegistryExtensionPoint,
permissions: coreServices.permissions,
},
async init({ actionsRegistry, permissions }) {
actionsRegistry.register({
name: 'get-service-health',
title: 'Get Service Health',
description:
'Retrieve the current health status of a service, including uptime, error rate, and active alerts.',
schema: {
input: z.object({
serviceName: z.string().describe('The name of the service to check'),
}),
output: z.object({
status: z.enum(['healthy', 'degraded', 'down']),
errorRate: z.number(),
alerts: z.array(z.string()),
}),
},
action: async ({ input, credentials }) => {
// Your implementation here
const health = await getServiceHealth(input.serviceName);
return { output: health };
},
});
},
});
},
});
Step 2: Add your plugin to the configuration
Add your plugin ID to backend.actions.pluginSources so its tools are exposed:

Or update directly in app-config.yaml:
backend:
actions:
pluginSources:
- catalog
- my-plugin # your plugin ID
That's it. The MCP Actions Backend discovers your registered tools and exposes them to agents.
Writing effective tool descriptions
AI agents select tools based on their name and description. Good descriptions are critical:
- Be specific. "Retrieve detailed information about a specific entity in the software catalog" is better than "Get entity".
- Describe what it returns, not just what it does. Agents need to know if a tool will answer their question.
- Use plain language. Descriptions are read by LLMs, not parsed by machines.
- Describe parameter schemas clearly. Use
.describe()on Zod fields so agents know what to pass.
Permissions
The Actions Registry does not automatically enforce permissions. Each tool is responsible for checking authorization in its action handler:
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { NotAllowedError } from '@backstage/errors';
actionsRegistry.register({
name: 'update-entity-metadata',
// ... schema ...
action: async ({ input, credentials }) => {
const decision = await permissions.authorize(
[{ permission: entityUpdatePermission, resourceRef: input.entityRef }],
{ credentials },
);
if (decision[0].result !== AuthorizeResult.ALLOW) {
throw new NotAllowedError('You do not have permission to update this entity');
}
// Proceed with the update
},
});
Write tools must include permission checks. Read tools should include them if the data is access-controlled. See the Backstage Permissions documentation for details on defining and enforcing permissions.
Error handling
Use errors from @backstage/errors so agents receive meaningful feedback:
NotAllowedError— permission deniedNotFoundError— resource not foundInputError— invalid parameters
import { NotFoundError } from '@backstage/errors';
action: async ({ input }) => {
const entity = await catalog.getEntityByRef(input.entityRef);
if (!entity) {
throw new NotFoundError(`Entity ${input.entityRef} not found`);
}
return { output: entity };
};
Related documentation
@backstage/plugin-mcp-actions-backend— NPM package documentation- Backstage Actions Registry Service — Core service for action registration
- Backstage Permissions — Permission framework documentation