Backstage As The Ultimate MCP Server

The evolution of developer platforms has always been about providing the right abstractions at the right time. But what if your platform could not just serve humans through beautiful UIs, but also empower AI agents to interact with your business logic in a standardized, secure way? This is the promise of Backstage’s recent integration with the Model Context Protocol (MCP) – and it’s a game-changer for platform engineering.

The MCP Revolution in Backstage

Over the past few releases, Backstage has undergone a significant transformation in how it exposes functionality to different consumers. Three key milestones have defined this journey:

Backstage 1.40: The Actions Registry and MCP Server

With version 1.40, Backstage introduced the Actions Registry – a centralized system for registering and discovering actions within backend plugins. This wasn’t just another API endpoint; it was the foundation for a new way of thinking about plugin functionality. Actions could now be defined once, with proper input/output schemas, descriptions, and metadata.

The Actions Registry provided:

  • Type-safe action definitions with Zod schema validation
  • Centralized discovery of available actions across all plugins
  • Standardized metadata including names, titles, and descriptions
  • Permission integration ensuring consistent authorization across all action invocations
  • Automatic MCP server exposure of all registered actions

But there was a critical limitation: authentication required static tokens that had to be pre-configured. These tokens weren’t tied to specific users, meaning:

  • No user-specific identity in audit logs
  • Manual token creation and management
  • Tokens shared across users or use cases
  • Permissions couldn’t be enforced at the user level for MCP calls

Backstage 1.43: Scaffolder Integration

Version 1.43 brought the Actions Registry into the scaffolder, allowing template authors to leverage any registered action as a scaffolder step. This meant that functionality previously locked in plugin APIs could now be orchestrated through templates, dramatically expanding the power of software templates.

Backstage 1.43: Dynamic Client Registration – The Game Changer

The experimental Dynamic Client Registration support for MCP Actions Backend solved the authentication problem that had limited 1.40’s MCP capabilities. Instead of static tokens, the system now supports OAuth flows that bring true user authentication to MCP interactions.

Here’s how the OAuth flow works in practice:

  1. When an AI assistant (like Claude Desktop or Cursor) attempts to call an MCP action
  2. A browser window pops up prompting you to log in to Backstage
  3. You authenticate using your normal Backstage login (SSO, GitHub, Microsoft, etc.)
  4. You authorize the MCP client to act on your behalf
  5. The MCP server receives your user token for all subsequent calls
  6. The AI assistant can now make authenticated calls as you until the token expires

This transformation meant:

  • Every MCP call is authenticated as YOU – proper user identity throughout
  • User-specific permissions are enforced (what you can do in the UI, you can do via MCP)
  • No pre-configuration needed – no static tokens to create or manage
  • Audit trails properly show which user performed each action
  • Zero trust security model – each user proves their identity dynamically
  • Session-based access – tokens can expire, requiring re-authentication just like the web UI

The difference between 1.40 and 1.43 is the difference between “the MCP bot has access” and “you have access through the MCP bot.” One is a security risk; the other is a security-first architecture.

The Security Breakthrough: User-Aware AI Interactions

The shift from static tokens to dynamic OAuth-based authentication in 1.43 cannot be overstated. This is the difference between treating AI assistants as service accounts with broad permissions versus treating them as extensions of individual users with specific permissions.

Consider what this enables:

  • A junior developer’s AI assistant can’t delete production resources they don’t have access to
  • Audit logs show “John deployed via AI assistant” not “MCP service account deployed”
  • Permission policies apply uniformly: if you can’t do it in the UI, your AI can’t do it either
  • Compliance and governance requirements are met automatically

This is true zero-trust architecture applied to AI interactions. The AI assistant is not a privileged actor – it’s an interface through which your existing identity and permissions flow.

The Platform Engineering Dream: Write Once, Expose Everywhere

The true power of this architecture lies in its simplicity. As a plugin developer, you define your action once:

actionsRegistry.register({
  name: 'get_crossplane_resources',
  title: 'Get Crossplane Resources',
  description: 'Returns Crossplane resources and their dependencies',
  schema: {
    input: z => z.object({
      backstageEntityName: z.string().describe('The name of the Backstage entity'),
      backstageEntityKind: z.string().describe('The kind of the Backstage entity. Defaults to component.').optional(),
      backstageEntityNamespace: z.string().describe('The namespace of the Backstage entity. Defaults to default.').optional(),
    }),
    output: z => z.object({
      resources: z.array(z.object({
        type: z.enum(['XRD', 'Claim', 'Resource']).describe('The type of the resource'),
        name: z.string().describe('The name of the resource'),
        // ... more schema definition
      })),
    }),
  },
  action: async ({ input, credentials }) => {
    // Permission check
    const authorized = await permissions.authorize(
      [
        { permission: listClaimsPermission },
        { permission: listCompositeResourcesPermission },
      ],
      { credentials }
    );

    if (authorized.every(a => a.result !== AuthorizeResult.ALLOW)) {
      throw new InputError('Access denied');
    }

    // Business logic
    const result = await service.getResources({...input});
    return { output: result };
  },
});

This single action definition becomes:

  1. An MCP tool that AI assistants can call
  2. A scaffolder action usable in software templates
  3. A backend API accessible to frontend plugins

And critically, permission enforcement happens at the action level, ensuring consistent authorization regardless of how the action is invoked.

Real-World Implementation Patterns

In the backstage-plugins repository, we’ve implemented MCP integration using two distinct patterns:

Pattern 1: Direct MCP Actions in Plugins

For plugins with well-defined domain logic, we expose MCP actions directly from the backend plugin. This approach is used in:

  • crossplane-resources-backend: Exposes actions for managing Crossplane claims, composites, and managed resources
  • kyverno-policy-reports-backend: Provides policy validation and compliance checks
  • kro-resources-backend: Manages KRO (Kubernetes Resource Orchestrator) resources
  • vcf-automation-backend: Integrates with VMware Cloud Foundation Automation
  • vcf-operations-backend: Provides observability and operations insights
  • educates-backend: Manages training portal sessions and workshops

Here’s an example from the Kyverno plugin showing how simple it is:

export function registerMcpActions(
  actionsRegistry: typeof actionsRegistryServiceRef.T,
  service: KubernetesService,
  permissions: PermissionsService,
  auth: AuthService
) {
  actionsRegistry.register({
    name: 'get_kyverno_policy_reports',
    title: 'Get Kyverno Policy Reports',
    description: 'Returns policy reports for a given entity',
    schema: {
      input: z => z.object({
        entity: z.object({
          metadata: z.object({
            name: z.string().describe('The name of the entity'),
            namespace: z.string().describe('The namespace of the entity'),
          }),
        }).describe('The entity to get policy reports for'),
      }),
      output: z => z.object({
        reports: z.array(z.object({
          // Schema definition
        })),
      }),
    },
    action: async ({ input, credentials }) => {
      // Permission check
      const decision = await permissions.authorize(
        [{ permission: showKyvernoReportsPermission }],
        { credentials }
      );

      if (decision[0].result !== AuthorizeResult.ALLOW) {
        throw new InputError('Access denied');
      }

      // Execute business logic
      const reports = await service.getPolicyReports({ entity: input.entity });
      return { output: { reports } };
    },
  });
}

Pattern 2: Dedicated MCP Bridge Plugins

For core Backstage functionality that doesn’t naturally expose actions (like the catalog or RBAC systems), we’ve created dedicated bridge plugins that use the discovery service and plugin-to-plugin communication to call other plugins’ REST APIs:

  • catalog-mcp-backend: Bridges to the Catalog API for entity queries
  • scaffolder-mcp-backend: Bridges to the Scaffolder API for template operations
  • rbac-mcp-backend: Bridges to the RBAC system for permission management

This pattern is particularly elegant because it demonstrates how you can wrap any existing backend API with MCP actions. Here’s how the catalog-mcp plugin makes authenticated calls to the Catalog API:

export function registerMcpActions(
  actionsRegistry: typeof actionsRegistryServiceRef.T,
  discovery: DiscoveryService,
  auth: AuthService
) {
  actionsRegistry.register({
    name: 'get_entities_by_owner',
    title: 'Get Entities by Owner',
    description: 'Retrieves all catalog entities owned by a specific user or group',
    schema: {
      input: z => z.object({
        owner: z.string().describe('Owner reference in format "user:namespace/name"'),
      }),
      output: z => z.object({
        entities: z.array(z.any()).describe('Array of catalog entities'),
        count: z.number().describe('Total number of entities found'),
      }),
    },
    action: async ({ input, credentials }) => {
      // Discover the catalog service
      const catalogUrl = await discovery.getBaseUrl('catalog');
      
      // Get authenticated token for catalog API
      const { token } = await auth.getPluginRequestToken({
        onBehalfOf: credentials,
        targetPluginId: 'catalog',
      });

      // Make authenticated API call
      const response = await fetch(
        `${catalogUrl}/entities/by-query?filter=spec.owner=${encodeURIComponent(input.owner)}`,
        { headers: { Authorization: `Bearer ${token}` } }
      );

      const data = await response.json();
      return { output: { entities: data.items, count: data.items.length } };
    },
  });
}

This pattern means that any existing Backstage plugin can be MCP-enabled without modifying its core functionality.

Configuration: Declaring Your MCP Sources

To enable MCP actions in your Backstage instance, you simply declare which plugins should have their actions exposed in your app-config.yaml:

backend:
  actions:
    pluginSources:
      - 'catalog'
      - 'vcf-automation'
      - 'kro'
      - 'vcf-operations'
      - 'ai-rules'
      - 'kyverno'
      - 'educates'
      - 'crossplane'
      - 'scaffolder-mcp'
      - 'rbac-mcp'
      - 'catalog-mcp'

That’s it. No complex routing rules, no additional authentication setup. The MCP Actions Backend automatically discovers and exposes all registered actions from these plugins, maintaining the same permission model that protects your REST APIs and frontend components.

Why This Matters: The AI Platform Play

We’re witnessing a fundamental shift in how platforms are consumed. The assumption that humans are the only consumers is being challenged daily. AI agents, automation systems, and intelligent workflows need programmatic access to platform capabilities – and they need it to be:

  1. Discoverable: AI agents should be able to explore what’s possible
  2. Well-documented: Each action needs clear descriptions and type information
  3. Secure: Authorization must be consistent across all access methods
  4. Reliable: Type-safe interfaces prevent integration bugs

Backstage’s MCP integration delivers on all these requirements. But more importantly, it does so without asking platform teams to duplicate effort. The same business logic that powers your internal developer portal now powers your AI agents, your automation workflows, and your software templates.

This is what a mature platform looks like – multiple interfaces over a single source of truth, with security and governance baked in at the core.

The Future Is Multi-Interface

The days of single-purpose platforms are behind us. Modern platforms need to serve:

  • Humans through intuitive web interfaces
  • Developers through CLI tools and APIs
  • AI agents through protocols like MCP
  • Automation systems through event-driven architectures

Backstage’s architecture has evolved to support this multi-interface reality. By treating actions as first-class citizens with proper schemas, permissions, and metadata, it enables a single implementation to serve all consumers.

This is the promise of platform engineering realized: build once, expose everywhere, secure by default.

Try It Yourself

Want to see this in action? At KubeCon NA 2025, I had the privilege of presenting a hands-on workshop alongside industry experts Ana Margarita Medina (Upbound), Cortney Nickerson (Nirmata), and Christian Hernandez (GitOps Advocate). Together, we led “Build Your Internal Developer Platform With the Experts: A Hands-On Workshop” covering:

  • Building a complete IDP with Backstage, Crossplane, Argo CD, and Kyverno
  • Implementing MCP integration across multiple plugins
  • Leveraging AI agents to interact with your platform
  • Production-proven patterns from real-world implementations

The complete workshop materials, including all the plugins mentioned in this post, are available at:

https://github.com/back-stack/kubecon-na-2025

The repository includes:

  • Complete plugin implementations with MCP actions from terasky-oss/backstage-plugins
  • Configuration examples and best practices
  • A fully functional local development environment
  • Documentation on extending the patterns for your use cases

Conclusion

Backstage’s MCP integration represents more than just another feature – it’s a paradigm shift in how we think about platform engineering. By providing a standardized way to expose business logic across multiple interfaces while maintaining security and type safety, Backstage has positioned itself as the ultimate MCP server for enterprise platforms.

What makes this truly revolutionary is the 1.43 authentication breakthrough. By enabling dynamic OAuth flows instead of static tokens, Backstage ensures that AI assistants operate within the same security boundaries as their human users. This isn’t just convenient – it’s the security model that enterprises require before they can truly embrace AI-assisted workflows.

The future of developer platforms isn’t just about serving web pages to humans. It’s about creating intelligent, composable systems that can be consumed by humans, AI agents, and automation systems alike – all while maintaining the same rigorous security, governance, and audit capabilities. With the Actions Registry and dynamic MCP authentication, Backstage has given us the tools to build that future today.

The question is no longer “Can we build a platform that AI agents can use securely?” but rather “What will we build now that AI agents can use our platform as us?”


This post covers Backstage features introduced in versions 1.40 and 1.43. For the latest updates and documentation, visit the Backstage documentation.

Leave a Reply

Discover more from vRabbi's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading