Merge pull request #30288 from backstage/blam/actions-mcp-backend

Add initial implementation of MCP Server
This commit is contained in:
Ben Lambert
2025-06-17 13:08:43 +02:00
committed by GitHub
17 changed files with 1179 additions and 18 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-mcp-actions-backend': minor
---
Initial implementation of an `mcp-actions` backend
+1
View File
@@ -47,6 +47,7 @@
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-events-backend-module-google-pubsub": "workspace:^",
"@backstage/plugin-kubernetes-backend": "workspace:^",
"@backstage/plugin-mcp-actions-backend": "workspace:^",
"@backstage/plugin-notifications-backend": "workspace:^",
"@backstage/plugin-permission-backend": "workspace:^",
"@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^",
+1
View File
@@ -62,4 +62,5 @@ backend.add(import('@backstage/plugin-notifications-backend'));
backend.add(import('./instanceMetadata'));
backend.add(import('@backstage/plugin-events-backend-module-google-pubsub'));
backend.add(import('@backstage/plugin-mcp-actions-backend'));
backend.start();
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+28
View File
@@ -0,0 +1,28 @@
# MCP Backend
This plugin backend was templated using the Backstage CLI. You should replace this text with a description of your plugin backend.
## Installation
This plugin is installed via the `@backstage/plugin-mcp-actions-backend` package. To install it to your backend package, run the following command:
```bash
# From your root directory
yarn --cwd packages/backend add @backstage/plugin-mcp-actions-backend
```
Then add the plugin to your backend in `packages/backend/src/index.ts`:
```ts
const backend = createBackend();
// ...
backend.add(import('@backstage/plugin-mcp-actions-backend'));
```
## Development
This plugin backend can be started in a standalone mode from directly in this
package with `yarn start`. It is a limited setup that is most convenient when
developing the plugin backend itself.
If you want to run the entire project, including the frontend, run `yarn start` from the root directory.
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-mcp-actions-backend
title: '@backstage/plugin-mcp-actions-backend'
spec:
lifecycle: experimental
type: backstage-backend-plugin
owner: maintainers
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createBackend } from '@backstage/backend-defaults';
import { createBackendPlugin } from '@backstage/backend-plugin-api';
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
import { mockServices } from '@backstage/backend-test-utils';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
const backend = createBackend();
backend.add(mockServices.auth.factory());
backend.add(mockServices.httpAuth.factory());
// TEMPLATE NOTE:
// Rather than using a real catalog you can use a mock with a fixed set of entities.
backend.add(
catalogServiceMock.factory({
entities: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'sample',
title: 'Sample Component',
},
spec: {
type: 'service',
},
},
],
}),
);
backend.add(
createBackendPlugin({
pluginId: 'local',
register({ registerInit }) {
registerInit({
deps: {
actionsRegistry: actionsRegistryServiceRef,
},
async init({ actionsRegistry }) {
actionsRegistry.register({
name: 'make-greeting',
title: 'Test Action',
description: 'Test Action',
schema: {
input: z =>
z.object({
name: z.string(),
}),
output: z =>
z.object({
greeting: z.string(),
}),
},
action: async ({ input }) => ({
output: {
greeting: `Hello ${input.name}!`,
},
}),
});
},
});
},
}),
);
backend.add(import('../src'));
backend.start();
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@backstage/plugin-mcp-actions-backend",
"version": "0.0.0",
"backstage": {
"role": "backend-plugin",
"pluginId": "mcp-actions",
"pluginPackages": [
"@backstage/plugin-mcp-actions-backend"
]
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/mcp-actions-backend"
},
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/types": "workspace:^",
"@modelcontextprotocol/sdk": "^1.12.3",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/express": "^4.17.6"
}
}
+13
View File
@@ -0,0 +1,13 @@
## API Report File for "@backstage/plugin-mcp-actions-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
// @public
const mcpPlugin: BackendFeature;
export default mcpPlugin;
// (No @packageDocumentation comment for this package)
```
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { mcpPlugin as default } from './plugin';
@@ -0,0 +1,187 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { mcpPlugin } from './plugin';
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
import { createBackendPlugin } from '@backstage/backend-plugin-api';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types';
describe('Mcp Backend', () => {
const mockPluginWithActions = createBackendPlugin({
pluginId: 'local',
register({ registerInit }) {
registerInit({
deps: { actionsRegistry: actionsRegistryServiceRef },
async init({ actionsRegistry }) {
actionsRegistry.register({
name: 'make-greeting',
title: 'Make Greeting',
description: 'Make a greeting',
schema: {
input: z => z.object({ name: z.string() }),
output: z => z.object({ greeting: z.string() }),
},
action: async ({ input }) => ({
output: { greeting: `Hello ${input.name}!` },
}),
});
},
});
},
});
const getContext = async () => {
const { server } = await startTestBackend({
features: [
mcpPlugin,
mockPluginWithActions,
mockServices.rootConfig.factory({
data: {
backend: {
actions: {
pluginSources: ['local'],
},
},
},
}),
],
});
const client = new Client({
name: 'test client',
version: '1.0',
});
const address = server.address();
if (typeof address !== 'object' || !('port' in address!)) {
throw new Error('server broke');
}
return {
client,
serverAddress: `http://localhost:${address.port}`,
};
};
it('should support streamable spec', async () => {
const { client, serverAddress } = await getContext();
const transport = new StreamableHTTPClientTransport(
new URL(`${serverAddress}/api/mcp-actions/v1`),
);
await client.connect(transport);
const result = await client.request(
{
method: 'tools/list',
},
ListToolsResultSchema,
);
expect(result.tools).toEqual([
{
annotations: {
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
readOnlyHint: false,
title: 'Make Greeting',
},
description: 'Make a greeting',
inputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: {
name: {
type: 'string',
},
},
required: ['name'],
type: 'object',
},
name: 'make-greeting',
outputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: {
greeting: {
type: 'string',
},
},
required: ['greeting'],
type: 'object',
},
},
]);
});
it('should support sse spec', async () => {
const { client, serverAddress } = await getContext();
const transport = new SSEClientTransport(
new URL(`${serverAddress}/api/mcp-actions/v1/sse`),
);
await client.connect(transport);
const result = await client.request(
{
method: 'tools/list',
},
ListToolsResultSchema,
);
await client.close();
expect(result.tools).toEqual([
{
annotations: {
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
readOnlyHint: false,
title: 'Make Greeting',
},
description: 'Make a greeting',
inputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: {
name: {
type: 'string',
},
},
required: ['name'],
type: 'object',
},
name: 'make-greeting',
outputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: {
greeting: {
type: 'string',
},
},
required: ['greeting'],
type: 'object',
},
},
]);
});
});
+72
View File
@@ -0,0 +1,72 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { json, Router } from 'express';
import { McpService } from './services/McpService';
import { createStreamableRouter } from './routers/createStreamableRouter';
import { createSseRouter } from './routers/createSseRouter';
import {
actionsRegistryServiceRef,
actionsServiceRef,
} from '@backstage/backend-plugin-api/alpha';
/**
* mcpPlugin backend plugin
*
* @public
*/
export const mcpPlugin = createBackendPlugin({
pluginId: 'mcp-actions',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
auth: coreServices.auth,
httpAuth: coreServices.httpAuth,
httpRouter: coreServices.httpRouter,
actions: actionsServiceRef,
registry: actionsRegistryServiceRef,
},
async init({ actions, logger, httpRouter, httpAuth }) {
const mcpService = await McpService.create({
actions,
});
const sseRouter = createSseRouter({
mcpService,
httpAuth,
});
const streamableRouter = createStreamableRouter({
mcpService,
httpAuth,
logger,
});
const router = Router();
router.use(json());
router.use('/v1/sse', sseRouter);
router.use('/v1', streamableRouter);
httpRouter.use(router);
},
});
},
});
@@ -0,0 +1,73 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import PromiseRouter from 'express-promise-router';
import { Router } from 'express';
import { McpService } from '../services/McpService';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { HttpAuthService } from '@backstage/backend-plugin-api';
/**
* Legacy SSE endpoint for older clients, hopefully will not be needed for much longer.
*/
export const createSseRouter = ({
mcpService,
httpAuth,
}: {
mcpService: McpService;
httpAuth: HttpAuthService;
}): Router => {
const router = PromiseRouter();
const transportsToSessionId = new Map<string, SSEServerTransport>();
router.get('/', async (req, res) => {
const server = mcpService.getServer({
credentials: await httpAuth.credentials(req),
});
const transport = new SSEServerTransport(
`${req.originalUrl}/messages`,
res,
);
transportsToSessionId.set(transport.sessionId, transport);
res.on('close', () => {
transportsToSessionId.delete(transport.sessionId);
});
await server.connect(transport);
});
router.post('/messages', async (req, res) => {
const sessionId = req.query.sessionId as string;
if (!sessionId) {
res.status(400).contentType('text/plain').write('sessionId is required');
return;
}
const transport = transportsToSessionId.get(sessionId);
if (transport) {
await transport.handlePostMessage(req, res, req.body);
} else {
res
.status(400)
.contentType('text/plain')
.write(`No transport found for sessionId "${sessionId}"`);
}
});
return router;
};
@@ -0,0 +1,100 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import PromiseRouter from 'express-promise-router';
import { Router } from 'express';
import { McpService } from '../services/McpService';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';
import { isError } from '@backstage/errors';
export const createStreamableRouter = ({
mcpService,
httpAuth,
logger,
}: {
mcpService: McpService;
logger: LoggerService;
httpAuth: HttpAuthService;
}): Router => {
const router = PromiseRouter();
router.post('/', async (req, res) => {
try {
const server = mcpService.getServer({
credentials: await httpAuth.credentials(req),
});
const transport = new StreamableHTTPServerTransport({
// stateless implementation for now, so that we can support multiple
// instances of the server backend, and avoid sticky sessions.
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on('close', () => {
transport.close();
server.close();
});
} catch (error) {
if (isError(error)) {
logger.error(error.message);
}
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
router.get('/', async (_, res) => {
// We only support POST requests, so we return a 405 error for all other methods.
res.writeHead(405).end(
JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Method not allowed.',
},
id: null,
}),
);
});
router.delete('/', async (_, res) => {
// We only support POST requests, so we return a 405 error for all other methods.
res.writeHead(405).end(
JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Method not allowed.',
},
id: null,
}),
);
});
return router;
};
@@ -0,0 +1,193 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mockCredentials } from '@backstage/backend-test-utils';
import { McpService } from './McpService';
import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
CallToolResultSchema,
ListToolsResultSchema,
} from '@modelcontextprotocol/sdk/types.js';
describe('McpService', () => {
it('should list the available actions as tools in the mcp backend', async () => {
const mockActionsRegistry = actionsRegistryServiceMock();
mockActionsRegistry.register({
name: 'mock-action',
title: 'Test',
description: 'Test',
schema: {
input: z => z.object({ input: z.string() }),
output: z => z.object({ output: z.string() }),
},
action: async () => ({ output: { output: 'test' } }),
});
const mcpService = await McpService.create({
actions: mockActionsRegistry,
});
const server = mcpService.getServer({
credentials: mockCredentials.user(),
});
const client = new Client({
name: 'test client',
version: '1.0',
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
const result = await client.request(
{
method: 'tools/list',
},
ListToolsResultSchema,
);
expect(result.tools).toEqual([
{
annotations: {
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
readOnlyHint: false,
title: 'Test',
},
description: 'Test',
inputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: {
input: {
type: 'string',
},
},
required: ['input'],
type: 'object',
},
name: 'mock-action',
outputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: {
output: {
type: 'string',
},
},
required: ['output'],
type: 'object',
},
},
]);
});
it('should call the action when the tool is invoked', async () => {
const mockActionsRegistry = actionsRegistryServiceMock();
const mockAction = jest.fn(async () => ({ output: { output: 'test' } }));
mockActionsRegistry.register({
name: 'mock-action',
title: 'Test',
description: 'Test',
schema: {
input: z => z.object({ input: z.string() }),
output: z => z.object({ output: z.string() }),
},
action: mockAction,
});
const mcpService = await McpService.create({
actions: mockActionsRegistry,
});
const server = mcpService.getServer({
credentials: mockCredentials.user(),
});
const client = new Client({
name: 'test client',
version: '1.0',
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
const result = await client.request(
{
method: 'tools/call',
params: { name: 'mock-action', arguments: { input: 'test' } },
},
CallToolResultSchema,
);
expect(mockAction).toHaveBeenCalledWith(
expect.objectContaining({
credentials: mockCredentials.user(),
input: { input: 'test' },
logger: expect.anything(),
}),
);
expect(result.structuredContent).toEqual({ output: 'test' });
});
it('should return an error when the action is not found', async () => {
const mcpService = await McpService.create({
actions: actionsRegistryServiceMock(),
});
const server = mcpService.getServer({
credentials: mockCredentials.user(),
});
const client = new Client({
name: 'test client',
version: '1.0',
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
await expect(
client.request(
{
method: 'tools/call',
params: { name: 'mock-action', arguments: { input: 'test' } },
},
CallToolResultSchema,
),
).rejects.toThrow('Action "mock-action" not found');
});
});
@@ -0,0 +1,89 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { JsonObject } from '@backstage/types';
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
import { version } from '@backstage/plugin-mcp-actions-backend/package.json';
import { NotFoundError } from '@backstage/errors';
export class McpService {
constructor(private readonly actions: ActionsService) {}
static async create({ actions }: { actions: ActionsService }) {
return new McpService(actions);
}
getServer({ credentials }: { credentials: BackstageCredentials }) {
const server = new McpServer(
{
name: 'backstage',
// TODO: this version will most likely change in the future.
version,
},
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
// TODO: switch this to be configuration based later
const { actions } = await this.actions.list({ credentials });
return {
tools: actions.map(action => ({
inputSchema: action.schema.input,
outputSchema: action.schema.output,
name: action.name,
description: action.description,
annotations: {
title: action.title,
destructiveHint: action.attributes.destructive,
idempotentHint: action.attributes.idempotent,
readOnlyHint: action.attributes.readOnly,
openWorldHint: false,
},
})),
};
});
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
const { actions } = await this.actions.list({ credentials });
const action = actions.find(a => a.name === params.name);
if (!action) {
throw new NotFoundError(`Action "${params.name}" not found`);
}
const { output } = await this.actions.invoke({
id: action.id,
input: params.arguments as JsonObject,
credentials,
});
return {
// all actions need to be defined with an structured output,
// so we can return the output using this format rather than
// just text.
structuredContent: output,
};
});
return server;
}
}
+254 -18
View File
@@ -7099,6 +7099,26 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-mcp-actions-backend@workspace:^, @backstage/plugin-mcp-actions-backend@workspace:plugins/mcp-actions-backend":
version: 0.0.0-use.local
resolution: "@backstage/plugin-mcp-actions-backend@workspace:plugins/mcp-actions-backend"
dependencies:
"@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/types": "workspace:^"
"@modelcontextprotocol/sdk": "npm:^1.12.3"
"@types/express": "npm:^4.17.6"
express: "npm:^4.17.1"
express-promise-router: "npm:^4.1.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
"@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email"
@@ -12062,6 +12082,25 @@ __metadata:
languageName: node
linkType: hard
"@modelcontextprotocol/sdk@npm:^1.12.3":
version: 1.12.3
resolution: "@modelcontextprotocol/sdk@npm:1.12.3"
dependencies:
ajv: "npm:^6.12.6"
content-type: "npm:^1.0.5"
cors: "npm:^2.8.5"
cross-spawn: "npm:^7.0.5"
eventsource: "npm:^3.0.2"
express: "npm:^5.0.1"
express-rate-limit: "npm:^7.5.0"
pkce-challenge: "npm:^5.0.0"
raw-body: "npm:^3.0.0"
zod: "npm:^3.23.8"
zod-to-json-schema: "npm:^3.24.1"
checksum: 10/abab9b3fcce45370aadc260e9f02587308455ea907cf58b5e7f2e27c0ef4d19bf591f80284592b69f3d1656e4b362b2a96788eb1492daa3e7e2129c49b3eea98
languageName: node
linkType: hard
"@module-federation/bridge-react-webpack-plugin@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.9.1"
@@ -24723,6 +24762,16 @@ __metadata:
languageName: node
linkType: hard
"accepts@npm:^2.0.0":
version: 2.0.0
resolution: "accepts@npm:2.0.0"
dependencies:
mime-types: "npm:^3.0.0"
negotiator: "npm:^1.0.0"
checksum: 10/ea1343992b40b2bfb3a3113fa9c3c2f918ba0f9197ae565c48d3f84d44b174f6b1d5cd9989decd7655963eb03a272abc36968cc439c2907f999bd5ef8653d5a7
languageName: node
linkType: hard
"acorn-globals@npm:^6.0.0":
version: 6.0.0
resolution: "acorn-globals@npm:6.0.0"
@@ -24938,7 +24987,7 @@ __metadata:
languageName: node
linkType: hard
"ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:^6.5.5":
"ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:^6.12.6, ajv@npm:^6.5.5":
version: 6.12.6
resolution: "ajv@npm:6.12.6"
dependencies:
@@ -26281,6 +26330,23 @@ __metadata:
languageName: node
linkType: hard
"body-parser@npm:^2.2.0":
version: 2.2.0
resolution: "body-parser@npm:2.2.0"
dependencies:
bytes: "npm:^3.1.2"
content-type: "npm:^1.0.5"
debug: "npm:^4.4.0"
http-errors: "npm:^2.0.0"
iconv-lite: "npm:^0.6.3"
on-finished: "npm:^2.4.1"
qs: "npm:^6.14.0"
raw-body: "npm:^3.0.0"
type-is: "npm:^2.0.0"
checksum: 10/e9d844b036bd15970df00a16f373c7ed28e1ef870974a0a1d4d6ef60d70e01087cc20a0dbb2081c49a88e3c08ce1d87caf1e2898c615dffa193f63e8faa8a84e
languageName: node
linkType: hard
"bonjour-service@npm:^1.2.1":
version: 1.2.1
resolution: "bonjour-service@npm:1.2.1"
@@ -26645,7 +26711,7 @@ __metadata:
languageName: node
linkType: hard
"bytes@npm:3.1.2":
"bytes@npm:3.1.2, bytes@npm:^3.1.2":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388
@@ -27999,6 +28065,15 @@ __metadata:
languageName: node
linkType: hard
"content-disposition@npm:^1.0.0":
version: 1.0.0
resolution: "content-disposition@npm:1.0.0"
dependencies:
safe-buffer: "npm:5.2.1"
checksum: 10/0dcc1a2d7874526b0072df3011b134857b49d97a3bc135bb464a299525d4972de6f5f464fd64da6c4d8406d26a1ffb976f62afaffef7723b1021a44498d10e08
languageName: node
linkType: hard
"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
@@ -28051,6 +28126,13 @@ __metadata:
languageName: node
linkType: hard
"cookie-signature@npm:^1.2.1":
version: 1.2.2
resolution: "cookie-signature@npm:1.2.2"
checksum: 10/be44a3c9a56f3771aea3a8bd8ad8f0a8e2679bcb967478267f41a510b4eb5ec55085386ba79c706c4ac21605ca76f4251973444b90283e0eb3eeafe8a92c7708
languageName: node
linkType: hard
"cookie@npm:0.7.1":
version: 0.7.1
resolution: "cookie@npm:0.7.1"
@@ -28058,7 +28140,7 @@ __metadata:
languageName: node
linkType: hard
"cookie@npm:0.7.2, cookie@npm:^0.7.0, cookie@npm:^0.7.2":
"cookie@npm:0.7.2, cookie@npm:^0.7.0, cookie@npm:^0.7.1, cookie@npm:^0.7.2":
version: 0.7.2
resolution: "cookie@npm:0.7.2"
checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f
@@ -29952,7 +30034,7 @@ __metadata:
languageName: node
linkType: hard
"encodeurl@npm:~2.0.0":
"encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0":
version: 2.0.0
resolution: "encodeurl@npm:2.0.0"
checksum: 10/abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe
@@ -31031,7 +31113,7 @@ __metadata:
languageName: node
linkType: hard
"etag@npm:~1.8.1":
"etag@npm:^1.8.1, etag@npm:~1.8.1":
version: 1.8.1
resolution: "etag@npm:1.8.1"
checksum: 10/571aeb3dbe0f2bbd4e4fadbdb44f325fc75335cd5f6f6b6a091e6a06a9f25ed5392f0863c5442acb0646787446e816f13cbfc6edce5b07658541dff573cab1ff
@@ -31099,7 +31181,7 @@ __metadata:
languageName: node
linkType: hard
"eventsource@npm:^3.0.6":
"eventsource@npm:^3.0.2, eventsource@npm:^3.0.6":
version: 3.0.7
resolution: "eventsource@npm:3.0.7"
dependencies:
@@ -31282,6 +31364,7 @@ __metadata:
"@backstage/plugin-events-backend": "workspace:^"
"@backstage/plugin-events-backend-module-google-pubsub": "workspace:^"
"@backstage/plugin-kubernetes-backend": "workspace:^"
"@backstage/plugin-mcp-actions-backend": "workspace:^"
"@backstage/plugin-notifications-backend": "workspace:^"
"@backstage/plugin-permission-backend": "workspace:^"
"@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^"
@@ -31515,6 +31598,41 @@ __metadata:
languageName: node
linkType: hard
"express@npm:^5.0.1":
version: 5.1.0
resolution: "express@npm:5.1.0"
dependencies:
accepts: "npm:^2.0.0"
body-parser: "npm:^2.2.0"
content-disposition: "npm:^1.0.0"
content-type: "npm:^1.0.5"
cookie: "npm:^0.7.1"
cookie-signature: "npm:^1.2.1"
debug: "npm:^4.4.0"
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
etag: "npm:^1.8.1"
finalhandler: "npm:^2.1.0"
fresh: "npm:^2.0.0"
http-errors: "npm:^2.0.0"
merge-descriptors: "npm:^2.0.0"
mime-types: "npm:^3.0.0"
on-finished: "npm:^2.4.1"
once: "npm:^1.4.0"
parseurl: "npm:^1.3.3"
proxy-addr: "npm:^2.0.7"
qs: "npm:^6.14.0"
range-parser: "npm:^1.2.1"
router: "npm:^2.2.0"
send: "npm:^1.1.0"
serve-static: "npm:^2.2.0"
statuses: "npm:^2.0.1"
type-is: "npm:^2.0.1"
vary: "npm:^1.1.2"
checksum: 10/6dba00bbdf308f43a84ed3f07a7e9870d5208f2a0b8f60f39459dda089750379747819863fad250849d3c9163833f33f94ce69d73938df31e0c5a430800d7e56
languageName: node
linkType: hard
"ext@npm:^1.1.2":
version: 1.7.0
resolution: "ext@npm:1.7.0"
@@ -31920,6 +32038,20 @@ __metadata:
languageName: node
linkType: hard
"finalhandler@npm:^2.1.0":
version: 2.1.0
resolution: "finalhandler@npm:2.1.0"
dependencies:
debug: "npm:^4.4.0"
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
on-finished: "npm:^2.4.1"
parseurl: "npm:^1.3.3"
statuses: "npm:^2.0.1"
checksum: 10/b2bd68c310e2c463df0ab747ab05f8defbc540b8c3f2442f86e7d084ac8acbc31f8cae079931b7f5a406521501941e3395e963de848a0aaf45dd414adeb5ff4e
languageName: node
linkType: hard
"find-cache-dir@npm:^2.0.0":
version: 2.1.0
resolution: "find-cache-dir@npm:2.1.0"
@@ -32347,6 +32479,13 @@ __metadata:
languageName: node
linkType: hard
"fresh@npm:^2.0.0":
version: 2.0.0
resolution: "fresh@npm:2.0.0"
checksum: 10/44e1468488363074641991c1340d2a10c5a6f6d7c353d89fd161c49d120c58ebf9890720f7584f509058385836e3ce50ddb60e9f017315a4ba8c6c3461813bfc
languageName: node
linkType: hard
"from2@npm:^2.3.0":
version: 2.3.0
resolution: "from2@npm:2.3.0"
@@ -38646,6 +38785,13 @@ __metadata:
languageName: node
linkType: hard
"merge-descriptors@npm:^2.0.0":
version: 2.0.0
resolution: "merge-descriptors@npm:2.0.0"
checksum: 10/e383332e700a94682d0125a36c8be761142a1320fc9feeb18e6e36647c9edf064271645f5669b2c21cf352116e561914fd8aa831b651f34db15ef4038c86696a
languageName: node
linkType: hard
"merge-stream@npm:^2.0.0":
version: 2.0.0
resolution: "merge-stream@npm:2.0.0"
@@ -39029,13 +39175,20 @@ __metadata:
languageName: node
linkType: hard
"mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2":
"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
checksum: 10/54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7
languageName: node
linkType: hard
"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.54.0":
version: 1.54.0
resolution: "mime-db@npm:1.54.0"
checksum: 10/9e7834be3d66ae7f10eaa69215732c6d389692b194f876198dca79b2b90cbf96688d9d5d05ef7987b20f749b769b11c01766564264ea5f919c88b32a29011311
languageName: node
linkType: hard
"mime-db@npm:~1.33.0":
version: 1.33.0
resolution: "mime-db@npm:1.33.0"
@@ -39070,6 +39223,15 @@ __metadata:
languageName: node
linkType: hard
"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1":
version: 3.0.1
resolution: "mime-types@npm:3.0.1"
dependencies:
mime-db: "npm:^1.54.0"
checksum: 10/fa1d3a928363723a8046c346d87bf85d35014dae4285ad70a3ff92bd35957992b3094f8417973cfe677330916c6ef30885109624f1fb3b1e61a78af509dba120
languageName: node
linkType: hard
"mime@npm:1.6.0, mime@npm:^1.3.4":
version: 1.6.0
resolution: "mime@npm:1.6.0"
@@ -42126,6 +42288,13 @@ __metadata:
languageName: node
linkType: hard
"pkce-challenge@npm:^5.0.0":
version: 5.0.0
resolution: "pkce-challenge@npm:5.0.0"
checksum: 10/e60c06a0e0481cb82f80072053d5c479a7490758541c4226460450285dd5d72a995c44b3c553731ca7c2f64cc34b35f1d2e5f9de08d276b59899298f9efe1ddf
languageName: node
linkType: hard
"pkg-dir@npm:^3.0.0":
version: 3.0.0
resolution: "pkg-dir@npm:3.0.0"
@@ -43146,7 +43315,7 @@ __metadata:
languageName: node
linkType: hard
"proxy-addr@npm:~2.0.7":
"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
dependencies:
@@ -43270,7 +43439,7 @@ __metadata:
languageName: node
linkType: hard
"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.7.0, qs@npm:^6.9.4":
"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4":
version: 6.14.0
resolution: "qs@npm:6.14.0"
dependencies:
@@ -43466,6 +43635,18 @@ __metadata:
languageName: node
linkType: hard
"raw-body@npm:^3.0.0":
version: 3.0.0
resolution: "raw-body@npm:3.0.0"
dependencies:
bytes: "npm:3.1.2"
http-errors: "npm:2.0.0"
iconv-lite: "npm:0.6.3"
unpipe: "npm:1.0.0"
checksum: 10/2443429bbb2f9ae5c50d3d2a6c342533dfbde6b3173740b70fa0302b30914ff400c6d31a46b3ceacbe7d0925dc07d4413928278b494b04a65736fc17ca33e30c
languageName: node
linkType: hard
"raw-loader@npm:^4.0.2":
version: 4.0.2
resolution: "raw-loader@npm:4.0.2"
@@ -45490,6 +45671,19 @@ __metadata:
languageName: unknown
linkType: soft
"router@npm:^2.2.0":
version: 2.2.0
resolution: "router@npm:2.2.0"
dependencies:
debug: "npm:^4.4.0"
depd: "npm:^2.0.0"
is-promise: "npm:^4.0.0"
parseurl: "npm:^1.3.3"
path-to-regexp: "npm:^8.0.0"
checksum: 10/8949bd1d3da5403cc024e2989fee58d7fda0f3ffe9f2dc5b8a192f295f400b3cde307b0b554f7d44851077640f36962ca469a766b3d57410d7d96245a7ba6c91
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.7.1":
version: 0.7.1
resolution: "rrweb-cssom@npm:0.7.1"
@@ -45873,6 +46067,25 @@ __metadata:
languageName: node
linkType: hard
"send@npm:^1.1.0, send@npm:^1.2.0":
version: 1.2.0
resolution: "send@npm:1.2.0"
dependencies:
debug: "npm:^4.3.5"
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
etag: "npm:^1.8.1"
fresh: "npm:^2.0.0"
http-errors: "npm:^2.0.0"
mime-types: "npm:^3.0.1"
ms: "npm:^2.1.3"
on-finished: "npm:^2.4.1"
range-parser: "npm:^1.2.1"
statuses: "npm:^2.0.1"
checksum: 10/9fa3b1a3b9a06b7b4ab00c25e8228326d9665a9745753a34d1ffab8ac63c7c206727331d1dc5be73647f1b658d259a1aa8e275b0e0eee51349370af02e9da506
languageName: node
linkType: hard
"seq-queue@npm:^0.0.5":
version: 0.0.5
resolution: "seq-queue@npm:0.0.5"
@@ -45949,6 +46162,18 @@ __metadata:
languageName: node
linkType: hard
"serve-static@npm:^2.2.0":
version: 2.2.0
resolution: "serve-static@npm:2.2.0"
dependencies:
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
parseurl: "npm:^1.3.3"
send: "npm:^1.2.0"
checksum: 10/9f1a900738c5bb02258275ce3bd1273379c4c3072b622e15d44e8f47d89a1ba2d639ec2d63b11c263ca936096b40758acb7a0d989cd6989018a65a12f9433ada
languageName: node
linkType: hard
"set-blocking@npm:^2.0.0":
version: 2.0.0
resolution: "set-blocking@npm:2.0.0"
@@ -48692,6 +48917,17 @@ __metadata:
languageName: node
linkType: hard
"type-is@npm:^2.0.0, type-is@npm:^2.0.1":
version: 2.0.1
resolution: "type-is@npm:2.0.1"
dependencies:
content-type: "npm:^1.0.5"
media-typer: "npm:^1.1.0"
mime-types: "npm:^3.0.0"
checksum: 10/bacdb23c872dacb7bd40fbd9095e6b2fca2895eedbb689160c05534d7d4810a7f4b3fd1ae87e96133c505958f6d602967a68db5ff577b85dd6be76eaa75d58af
languageName: node
linkType: hard
"type@npm:^1.0.1":
version: 1.2.0
resolution: "type@npm:1.2.0"
@@ -50827,12 +51063,12 @@ __metadata:
languageName: node
linkType: hard
"zod-to-json-schema@npm:^3.20.4, zod-to-json-schema@npm:^3.21.4":
version: 3.23.5
resolution: "zod-to-json-schema@npm:3.23.5"
"zod-to-json-schema@npm:^3.20.4, zod-to-json-schema@npm:^3.21.4, zod-to-json-schema@npm:^3.24.1":
version: 3.24.5
resolution: "zod-to-json-schema@npm:3.24.5"
peerDependencies:
zod: ^3.23.3
checksum: 10/53d07a419f0f194e0b96711dc11e7e6fa52a366b0ed5fceb405dc55f13252a1bf433712e4fb496c2a5fdc851018ee1acba7b39c2265c43d6fbb180e12c110c3b
zod: ^3.24.1
checksum: 10/1af291b4c429945c9568c2e924bdb7c66ab8d139cbeb9a99b6e9fc9e1b02863f85d07759b9303714f07ceda3993dcaf0ebcb80d2c18bb2aaf5502b2c1016affd
languageName: node
linkType: hard
@@ -50845,10 +51081,10 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.22.4":
version: 3.24.4
resolution: "zod@npm:3.24.4"
checksum: 10/3d545792fa54bb27ee5dbc34a5709e81f603185fcc94c8204b5d95c20dc4c81d870ff9c51f3884a30ef05cdc601449f4c4df254ac4783f0827b1faed7c1cdb48
"zod@npm:^3.22.4, zod@npm:^3.23.8":
version: 3.25.67
resolution: "zod@npm:3.25.67"
checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa
languageName: node
linkType: hard