Skip to main content

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:

plugins/my-plugin-backend/src/plugin.ts
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:

Config Manager Actions Configuration

Or update directly in app-config.yaml:

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 denied
  • NotFoundError — resource not found
  • InputError — 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 };
};