Merge pull request #30288 from backstage/blam/actions-mcp-backend
Add initial implementation of MCP Server
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-mcp-actions-backend': minor
|
||||
---
|
||||
|
||||
Initial implementation of an `mcp-actions` backend
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user