> ## Documentation Index
> Fetch the complete documentation index at: https://backstage.spotify.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Registering MCP Tools

> Register your Portal plugin's capabilities as MCP tools via the Actions Registry so AI agents can discover and use them.

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:

```typescript title="plugins/my-plugin-backend/src/plugin.ts" theme={"theme":{"light":"github-light","dark":"dracula"}}
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

Navigate to App Settings > Actions > Plugin sources to configure the plugin sources for loading your actions.

<Frame>
  <img src="https://mintcdn.com/spotify-89f50c35/O6Aai2s7btF_z0wG/portal/core-features-and-plugins/mcp/assets/config-manager-custom-tools.png?fit=max&auto=format&n=O6Aai2s7btF_z0wG&q=85&s=2c28742399d59f72c9ae8fb6dd163aee" alt="Custom Tools in Plugins" width="5190" height="3044" data-path="portal/core-features-and-plugins/mcp/assets/config-manager-custom-tools.png" />
</Frame>

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:

```typescript theme={"theme":{"light":"github-light","dark":"dracula"}}
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](https://backstage.io/docs/permissions/overview/) 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

```typescript theme={"theme":{"light":"github-light","dark":"dracula"}}
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`](https://www.npmjs.com/package/@backstage/plugin-mcp-actions-backend) — NPM package documentation
* [Backstage Actions Registry Service](https://backstage.io/docs/backend-system/core-services/actions-registry/) — Core service for action registration
* [Backstage Permissions](https://backstage.io/docs/permissions/overview/) — Permission framework documentation
