@backstage/plugin-mcp-actions-backend 0.0.0-nightly-20250620024248

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # @backstage/plugin-mcp-actions-backend
2
+
3
+ ## 0.0.0-nightly-20250620024248
4
+
5
+ ### Patch Changes
6
+
7
+ - 6bc0799: Fixed the example in the README for generating a static token by adding a subject field
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 4ed0fb6: Initial implementation of an `mcp-actions` backend
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+ - @backstage/catalog-client@1.10.1
19
+ - @backstage/backend-defaults@0.11.0
20
+ - @backstage/plugin-catalog-node@1.17.1
21
+ - @backstage/backend-plugin-api@1.4.0
22
+ - @backstage/errors@1.2.7
23
+ - @backstage/types@1.2.1
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # MCP Actions Backend
2
+
3
+ This plugin exposes Backstage actions as MCP (Model Context Protocol) tools, allowing AI clients to discover and invoke registered actions in your Backstage backend.
4
+
5
+ ## Installation
6
+
7
+ This plugin is installed via the `@backstage/plugin-mcp-actions-backend` package. To install it to your backend package, run the following command:
8
+
9
+ ```bash
10
+ # From your root directory
11
+ yarn --cwd packages/backend add @backstage/plugin-mcp-actions-backend
12
+ ```
13
+
14
+ Then add the plugin to your backend in `packages/backend/src/index.ts`:
15
+
16
+ ```ts
17
+ const backend = createBackend();
18
+ // ...
19
+ backend.add(import('@backstage/plugin-mcp-actions-backend'));
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ ### Configuring Actions Registry
25
+
26
+ The MCP Actions Backend exposes actions that are registered with the Actions Registry. You can register actions from specific plugins by configuring the `pluginSources` in your app configuration:
27
+
28
+ ```yaml
29
+ backend:
30
+ actions:
31
+ pluginSources:
32
+ - 'catalog'
33
+ - 'my-custom-plugin'
34
+ ```
35
+
36
+ Actions from these plugins will be discovered and exposed as MCP tools. Each action must be registered using the Actions Registry Service in the respective plugin:
37
+
38
+ ```ts
39
+ // In your plugin
40
+ import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
41
+
42
+ export const myPlugin = createBackendPlugin({
43
+ pluginId: 'my-custom-plugin',
44
+ register(env) {
45
+ env.registerInit({
46
+ deps: {
47
+ actionsRegistry: actionsRegistryServiceRef,
48
+ },
49
+ async init({ actionsRegistry }) {
50
+ actionsRegistry.register({
51
+ name: 'greet-user',
52
+ title: 'Greet User',
53
+ description: 'Generate a personalized greeting',
54
+ schema: {
55
+ input: z =>
56
+ z.object({
57
+ name: z.string().describe('The name of the person to greet'),
58
+ }),
59
+ output: z =>
60
+ z.object({
61
+ greeting: z.string().describe('The generated greeting'),
62
+ }),
63
+ },
64
+ action: async ({ input }) => ({
65
+ output: { greeting: `Hello ${input.name}!` },
66
+ }),
67
+ });
68
+ },
69
+ });
70
+ },
71
+ });
72
+ ```
73
+
74
+ ### Authentication Configuration
75
+
76
+ By default, the Backstage backend requires authentication for all requests.
77
+
78
+ #### External Access with Static Tokens
79
+
80
+ > This is meant to be a temporary workaround until work on [device authentication](https://github.com/backstage/backstage/pull/27680) is completed.
81
+ > This will make authentication for MCP clients and CLI's in Backstage easier than having to configure static tokens.
82
+
83
+ Configure external access with static tokens in your app configuration:
84
+
85
+ ```yaml
86
+ backend:
87
+ auth:
88
+ externalAccess:
89
+ - type: static
90
+ options:
91
+ token: ${MCP_TOKEN}
92
+ subject: mcp-clients
93
+ accessRestrictions:
94
+ - plugin: mcp-actions
95
+ - plugin: catalog
96
+ ```
97
+
98
+ Generate a secure token:
99
+
100
+ ```bash
101
+ node -p 'require("crypto").randomBytes(24).toString("base64")'
102
+ ```
103
+
104
+ Set the `MCP_TOKEN` environment variable with this token, and configure your MCP client to use it in the [Authorization header](#configuring-mcp-clients)
105
+
106
+ ## Configuring MCP Clients
107
+
108
+ The MCP server supports both Server-Sent Events (SSE) and Streamable HTTP protocols.
109
+
110
+ The SSE protocol is deprecated, and should be avoided as it will be removed in a future release.
111
+
112
+ - `Streamable HTTP`: `http://localhost:7007/api/mcp-actions/v1`
113
+ - `SSE`: `http://localhost:7007/api/mcp-actions/v1/sse`
114
+
115
+ There's a few different ways to configure MCP tools, but here's a snippet of the most common.
116
+
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "backstage-actions": {
121
+ // you can also replace this with the public / internal URL of the deployed backend.
122
+ "url": "http://localhost:7007/api/mcp-actions/v1",
123
+ "headers": {
124
+ "Authorization": "Bearer ${MCP_TOKEN}"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ## Development
132
+
133
+ 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.
134
+
135
+ If you want to run the entire project, including the frontend, run `yarn start` from the root directory.
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var plugin = require('./plugin.cjs.js');
6
+
7
+
8
+
9
+ exports.default = plugin.mcpPlugin;
10
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,10 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+
3
+ /**
4
+ * mcpPlugin backend plugin
5
+ *
6
+ * @public
7
+ */
8
+ declare const mcpPlugin: _backstage_backend_plugin_api.BackendFeature;
9
+
10
+ export { mcpPlugin as default };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var express = require('express');
5
+ var McpService = require('./services/McpService.cjs.js');
6
+ var createStreamableRouter = require('./routers/createStreamableRouter.cjs.js');
7
+ var createSseRouter = require('./routers/createSseRouter.cjs.js');
8
+ var alpha = require('@backstage/backend-plugin-api/alpha');
9
+
10
+ const mcpPlugin = backendPluginApi.createBackendPlugin({
11
+ pluginId: "mcp-actions",
12
+ register(env) {
13
+ env.registerInit({
14
+ deps: {
15
+ logger: backendPluginApi.coreServices.logger,
16
+ auth: backendPluginApi.coreServices.auth,
17
+ httpAuth: backendPluginApi.coreServices.httpAuth,
18
+ httpRouter: backendPluginApi.coreServices.httpRouter,
19
+ actions: alpha.actionsServiceRef,
20
+ registry: alpha.actionsRegistryServiceRef
21
+ },
22
+ async init({ actions, logger, httpRouter, httpAuth }) {
23
+ const mcpService = await McpService.McpService.create({
24
+ actions
25
+ });
26
+ const sseRouter = createSseRouter.createSseRouter({
27
+ mcpService,
28
+ httpAuth
29
+ });
30
+ const streamableRouter = createStreamableRouter.createStreamableRouter({
31
+ mcpService,
32
+ httpAuth,
33
+ logger
34
+ });
35
+ const router = express.Router();
36
+ router.use(express.json());
37
+ router.use("/v1/sse", sseRouter);
38
+ router.use("/v1", streamableRouter);
39
+ httpRouter.use(router);
40
+ }
41
+ });
42
+ }
43
+ });
44
+
45
+ exports.mcpPlugin = mcpPlugin;
46
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { json, Router } from 'express';\nimport { McpService } from './services/McpService';\nimport { createStreamableRouter } from './routers/createStreamableRouter';\nimport { createSseRouter } from './routers/createSseRouter';\nimport {\n actionsRegistryServiceRef,\n actionsServiceRef,\n} from '@backstage/backend-plugin-api/alpha';\n\n/**\n * mcpPlugin backend plugin\n *\n * @public\n */\nexport const mcpPlugin = createBackendPlugin({\n pluginId: 'mcp-actions',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n auth: coreServices.auth,\n httpAuth: coreServices.httpAuth,\n httpRouter: coreServices.httpRouter,\n actions: actionsServiceRef,\n registry: actionsRegistryServiceRef,\n },\n async init({ actions, logger, httpRouter, httpAuth }) {\n const mcpService = await McpService.create({\n actions,\n });\n\n const sseRouter = createSseRouter({\n mcpService,\n httpAuth,\n });\n\n const streamableRouter = createStreamableRouter({\n mcpService,\n httpAuth,\n logger,\n });\n\n const router = Router();\n router.use(json());\n\n router.use('/v1/sse', sseRouter);\n router.use('/v1', streamableRouter);\n\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","actionsServiceRef","actionsRegistryServiceRef","McpService","createSseRouter","createStreamableRouter","Router","json"],"mappings":";;;;;;;;;AAiCO,MAAM,YAAYA,oCAAoB,CAAA;AAAA,EAC3C,QAAU,EAAA,aAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,QAAQC,6BAAa,CAAA,MAAA;AAAA,QACrB,MAAMA,6BAAa,CAAA,IAAA;AAAA,QACnB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,YAAYA,6BAAa,CAAA,UAAA;AAAA,QACzB,OAAS,EAAAC,uBAAA;AAAA,QACT,QAAU,EAAAC;AAAA,OACZ;AAAA,MACA,MAAM,IAAK,CAAA,EAAE,SAAS,MAAQ,EAAA,UAAA,EAAY,UAAY,EAAA;AACpD,QAAM,MAAA,UAAA,GAAa,MAAMC,qBAAA,CAAW,MAAO,CAAA;AAAA,UACzC;AAAA,SACD,CAAA;AAED,QAAA,MAAM,YAAYC,+BAAgB,CAAA;AAAA,UAChC,UAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,mBAAmBC,6CAAuB,CAAA;AAAA,UAC9C,UAAA;AAAA,UACA,QAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,SAASC,cAAO,EAAA;AACtB,QAAO,MAAA,CAAA,GAAA,CAAIC,cAAM,CAAA;AAEjB,QAAO,MAAA,CAAA,GAAA,CAAI,WAAW,SAAS,CAAA;AAC/B,QAAO,MAAA,CAAA,GAAA,CAAI,OAAO,gBAAgB,CAAA;AAElC,QAAA,UAAA,CAAW,IAAI,MAAM,CAAA;AAAA;AACvB,KACD,CAAA;AAAA;AAEL,CAAC;;;;"}
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ var PromiseRouter = require('express-promise-router');
4
+ var sse_js = require('@modelcontextprotocol/sdk/server/sse.js');
5
+
6
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
7
+
8
+ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
9
+
10
+ const createSseRouter = ({
11
+ mcpService,
12
+ httpAuth
13
+ }) => {
14
+ const router = PromiseRouter__default.default();
15
+ const transportsToSessionId = /* @__PURE__ */ new Map();
16
+ router.get("/", async (req, res) => {
17
+ const server = mcpService.getServer({
18
+ credentials: await httpAuth.credentials(req)
19
+ });
20
+ const transport = new sse_js.SSEServerTransport(
21
+ `${req.originalUrl}/messages`,
22
+ res
23
+ );
24
+ transportsToSessionId.set(transport.sessionId, transport);
25
+ res.on("close", () => {
26
+ transportsToSessionId.delete(transport.sessionId);
27
+ });
28
+ await server.connect(transport);
29
+ });
30
+ router.post("/messages", async (req, res) => {
31
+ const sessionId = req.query.sessionId;
32
+ if (!sessionId) {
33
+ res.status(400).contentType("text/plain").write("sessionId is required");
34
+ return;
35
+ }
36
+ const transport = transportsToSessionId.get(sessionId);
37
+ if (transport) {
38
+ await transport.handlePostMessage(req, res, req.body);
39
+ } else {
40
+ res.status(400).contentType("text/plain").write(`No transport found for sessionId "${sessionId}"`);
41
+ }
42
+ });
43
+ return router;
44
+ };
45
+
46
+ exports.createSseRouter = createSseRouter;
47
+ //# sourceMappingURL=createSseRouter.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createSseRouter.cjs.js","sources":["../../src/routers/createSseRouter.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport PromiseRouter from 'express-promise-router';\nimport { Router } from 'express';\nimport { McpService } from '../services/McpService';\nimport { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';\nimport { HttpAuthService } from '@backstage/backend-plugin-api';\n\n/**\n * Legacy SSE endpoint for older clients, hopefully will not be needed for much longer.\n */\nexport const createSseRouter = ({\n mcpService,\n httpAuth,\n}: {\n mcpService: McpService;\n httpAuth: HttpAuthService;\n}): Router => {\n const router = PromiseRouter();\n const transportsToSessionId = new Map<string, SSEServerTransport>();\n\n router.get('/', async (req, res) => {\n const server = mcpService.getServer({\n credentials: await httpAuth.credentials(req),\n });\n\n const transport = new SSEServerTransport(\n `${req.originalUrl}/messages`,\n res,\n );\n\n transportsToSessionId.set(transport.sessionId, transport);\n\n res.on('close', () => {\n transportsToSessionId.delete(transport.sessionId);\n });\n\n await server.connect(transport);\n });\n\n router.post('/messages', async (req, res) => {\n const sessionId = req.query.sessionId as string;\n\n if (!sessionId) {\n res.status(400).contentType('text/plain').write('sessionId is required');\n return;\n }\n\n const transport = transportsToSessionId.get(sessionId);\n if (transport) {\n await transport.handlePostMessage(req, res, req.body);\n } else {\n res\n .status(400)\n .contentType('text/plain')\n .write(`No transport found for sessionId \"${sessionId}\"`);\n }\n });\n return router;\n};\n"],"names":["PromiseRouter","SSEServerTransport"],"mappings":";;;;;;;;;AAwBO,MAAM,kBAAkB,CAAC;AAAA,EAC9B,UAAA;AAAA,EACA;AACF,CAGc,KAAA;AACZ,EAAA,MAAM,SAASA,8BAAc,EAAA;AAC7B,EAAM,MAAA,qBAAA,uBAA4B,GAAgC,EAAA;AAElE,EAAA,MAAA,CAAO,GAAI,CAAA,GAAA,EAAK,OAAO,GAAA,EAAK,GAAQ,KAAA;AAClC,IAAM,MAAA,MAAA,GAAS,WAAW,SAAU,CAAA;AAAA,MAClC,WAAa,EAAA,MAAM,QAAS,CAAA,WAAA,CAAY,GAAG;AAAA,KAC5C,CAAA;AAED,IAAA,MAAM,YAAY,IAAIC,yBAAA;AAAA,MACpB,CAAA,EAAG,IAAI,WAAW,CAAA,SAAA,CAAA;AAAA,MAClB;AAAA,KACF;AAEA,IAAsB,qBAAA,CAAA,GAAA,CAAI,SAAU,CAAA,SAAA,EAAW,SAAS,CAAA;AAExD,IAAI,GAAA,CAAA,EAAA,CAAG,SAAS,MAAM;AACpB,MAAsB,qBAAA,CAAA,MAAA,CAAO,UAAU,SAAS,CAAA;AAAA,KACjD,CAAA;AAED,IAAM,MAAA,MAAA,CAAO,QAAQ,SAAS,CAAA;AAAA,GAC/B,CAAA;AAED,EAAA,MAAA,CAAO,IAAK,CAAA,WAAA,EAAa,OAAO,GAAA,EAAK,GAAQ,KAAA;AAC3C,IAAM,MAAA,SAAA,GAAY,IAAI,KAAM,CAAA,SAAA;AAE5B,IAAA,IAAI,CAAC,SAAW,EAAA;AACd,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,YAAY,YAAY,CAAA,CAAE,MAAM,uBAAuB,CAAA;AACvE,MAAA;AAAA;AAGF,IAAM,MAAA,SAAA,GAAY,qBAAsB,CAAA,GAAA,CAAI,SAAS,CAAA;AACrD,IAAA,IAAI,SAAW,EAAA;AACb,MAAA,MAAM,SAAU,CAAA,iBAAA,CAAkB,GAAK,EAAA,GAAA,EAAK,IAAI,IAAI,CAAA;AAAA,KAC/C,MAAA;AACL,MACG,GAAA,CAAA,MAAA,CAAO,GAAG,CACV,CAAA,WAAA,CAAY,YAAY,CACxB,CAAA,KAAA,CAAM,CAAqC,kCAAA,EAAA,SAAS,CAAG,CAAA,CAAA,CAAA;AAAA;AAC5D,GACD,CAAA;AACD,EAAO,OAAA,MAAA;AACT;;;;"}
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ var PromiseRouter = require('express-promise-router');
4
+ var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
5
+ var errors = require('@backstage/errors');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+
9
+ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
10
+
11
+ const createStreamableRouter = ({
12
+ mcpService,
13
+ httpAuth,
14
+ logger
15
+ }) => {
16
+ const router = PromiseRouter__default.default();
17
+ router.post("/", async (req, res) => {
18
+ try {
19
+ const server = mcpService.getServer({
20
+ credentials: await httpAuth.credentials(req)
21
+ });
22
+ const transport = new streamableHttp_js.StreamableHTTPServerTransport({
23
+ // stateless implementation for now, so that we can support multiple
24
+ // instances of the server backend, and avoid sticky sessions.
25
+ sessionIdGenerator: void 0
26
+ });
27
+ await server.connect(transport);
28
+ await transport.handleRequest(req, res, req.body);
29
+ res.on("close", () => {
30
+ transport.close();
31
+ server.close();
32
+ });
33
+ } catch (error) {
34
+ if (errors.isError(error)) {
35
+ logger.error(error.message);
36
+ }
37
+ if (!res.headersSent) {
38
+ res.status(500).json({
39
+ jsonrpc: "2.0",
40
+ error: {
41
+ code: -32603,
42
+ message: "Internal server error"
43
+ },
44
+ id: null
45
+ });
46
+ }
47
+ }
48
+ });
49
+ router.get("/", async (_, res) => {
50
+ res.writeHead(405).end(
51
+ JSON.stringify({
52
+ jsonrpc: "2.0",
53
+ error: {
54
+ code: -32e3,
55
+ message: "Method not allowed."
56
+ },
57
+ id: null
58
+ })
59
+ );
60
+ });
61
+ router.delete("/", async (_, res) => {
62
+ res.writeHead(405).end(
63
+ JSON.stringify({
64
+ jsonrpc: "2.0",
65
+ error: {
66
+ code: -32e3,
67
+ message: "Method not allowed."
68
+ },
69
+ id: null
70
+ })
71
+ );
72
+ });
73
+ return router;
74
+ };
75
+
76
+ exports.createStreamableRouter = createStreamableRouter;
77
+ //# sourceMappingURL=createStreamableRouter.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createStreamableRouter.cjs.js","sources":["../../src/routers/createStreamableRouter.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport PromiseRouter from 'express-promise-router';\nimport { Router } from 'express';\nimport { McpService } from '../services/McpService';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { isError } from '@backstage/errors';\n\nexport const createStreamableRouter = ({\n mcpService,\n httpAuth,\n logger,\n}: {\n mcpService: McpService;\n logger: LoggerService;\n httpAuth: HttpAuthService;\n}): Router => {\n const router = PromiseRouter();\n\n router.post('/', async (req, res) => {\n try {\n const server = mcpService.getServer({\n credentials: await httpAuth.credentials(req),\n });\n\n const transport = new StreamableHTTPServerTransport({\n // stateless implementation for now, so that we can support multiple\n // instances of the server backend, and avoid sticky sessions.\n sessionIdGenerator: undefined,\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n } catch (error) {\n if (isError(error)) {\n logger.error(error.message);\n }\n\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: 'Internal server error',\n },\n id: null,\n });\n }\n }\n });\n\n router.get('/', async (_, res) => {\n // We only support POST requests, so we return a 405 error for all other methods.\n res.writeHead(405).end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed.',\n },\n id: null,\n }),\n );\n });\n\n router.delete('/', async (_, res) => {\n // We only support POST requests, so we return a 405 error for all other methods.\n res.writeHead(405).end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed.',\n },\n id: null,\n }),\n );\n });\n\n return router;\n};\n"],"names":["PromiseRouter","StreamableHTTPServerTransport","isError"],"mappings":";;;;;;;;;;AAsBO,MAAM,yBAAyB,CAAC;AAAA,EACrC,UAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAIc,KAAA;AACZ,EAAA,MAAM,SAASA,8BAAc,EAAA;AAE7B,EAAA,MAAA,CAAO,IAAK,CAAA,GAAA,EAAK,OAAO,GAAA,EAAK,GAAQ,KAAA;AACnC,IAAI,IAAA;AACF,MAAM,MAAA,MAAA,GAAS,WAAW,SAAU,CAAA;AAAA,QAClC,WAAa,EAAA,MAAM,QAAS,CAAA,WAAA,CAAY,GAAG;AAAA,OAC5C,CAAA;AAED,MAAM,MAAA,SAAA,GAAY,IAAIC,+CAA8B,CAAA;AAAA;AAAA;AAAA,QAGlD,kBAAoB,EAAA,KAAA;AAAA,OACrB,CAAA;AAED,MAAM,MAAA,MAAA,CAAO,QAAQ,SAAS,CAAA;AAC9B,MAAA,MAAM,SAAU,CAAA,aAAA,CAAc,GAAK,EAAA,GAAA,EAAK,IAAI,IAAI,CAAA;AAEhD,MAAI,GAAA,CAAA,EAAA,CAAG,SAAS,MAAM;AACpB,QAAA,SAAA,CAAU,KAAM,EAAA;AAChB,QAAA,MAAA,CAAO,KAAM,EAAA;AAAA,OACd,CAAA;AAAA,aACM,KAAO,EAAA;AACd,MAAI,IAAAC,cAAA,CAAQ,KAAK,CAAG,EAAA;AAClB,QAAO,MAAA,CAAA,KAAA,CAAM,MAAM,OAAO,CAAA;AAAA;AAG5B,MAAI,IAAA,CAAC,IAAI,WAAa,EAAA;AACpB,QAAI,GAAA,CAAA,MAAA,CAAO,GAAG,CAAA,CAAE,IAAK,CAAA;AAAA,UACnB,OAAS,EAAA,KAAA;AAAA,UACT,KAAO,EAAA;AAAA,YACL,IAAM,EAAA,CAAA,KAAA;AAAA,YACN,OAAS,EAAA;AAAA,WACX;AAAA,UACA,EAAI,EAAA;AAAA,SACL,CAAA;AAAA;AACH;AACF,GACD,CAAA;AAED,EAAA,MAAA,CAAO,GAAI,CAAA,GAAA,EAAK,OAAO,CAAA,EAAG,GAAQ,KAAA;AAEhC,IAAI,GAAA,CAAA,SAAA,CAAU,GAAG,CAAE,CAAA,GAAA;AAAA,MACjB,KAAK,SAAU,CAAA;AAAA,QACb,OAAS,EAAA,KAAA;AAAA,QACT,KAAO,EAAA;AAAA,UACL,IAAM,EAAA,CAAA,IAAA;AAAA,UACN,OAAS,EAAA;AAAA,SACX;AAAA,QACA,EAAI,EAAA;AAAA,OACL;AAAA,KACH;AAAA,GACD,CAAA;AAED,EAAA,MAAA,CAAO,MAAO,CAAA,GAAA,EAAK,OAAO,CAAA,EAAG,GAAQ,KAAA;AAEnC,IAAI,GAAA,CAAA,SAAA,CAAU,GAAG,CAAE,CAAA,GAAA;AAAA,MACjB,KAAK,SAAU,CAAA;AAAA,QACb,OAAS,EAAA,KAAA;AAAA,QACT,KAAO,EAAA;AAAA,UACL,IAAM,EAAA,CAAA,IAAA;AAAA,UACN,OAAS,EAAA;AAAA,SACX;AAAA,QACA,EAAI,EAAA;AAAA,OACL;AAAA,KACH;AAAA,GACD,CAAA;AAED,EAAO,OAAA,MAAA;AACT;;;;"}
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ var index_js = require('@modelcontextprotocol/sdk/server/index.js');
4
+ var types_js = require('@modelcontextprotocol/sdk/types.js');
5
+ var package_json = require('@backstage/plugin-mcp-actions-backend/package.json');
6
+ var errors = require('@backstage/errors');
7
+
8
+ class McpService {
9
+ constructor(actions) {
10
+ this.actions = actions;
11
+ }
12
+ static async create({ actions }) {
13
+ return new McpService(actions);
14
+ }
15
+ getServer({ credentials }) {
16
+ const server = new index_js.Server(
17
+ {
18
+ name: "backstage",
19
+ // TODO: this version will most likely change in the future.
20
+ version: package_json.version
21
+ },
22
+ { capabilities: { tools: {} } }
23
+ );
24
+ server.setRequestHandler(types_js.ListToolsRequestSchema, async () => {
25
+ const { actions } = await this.actions.list({ credentials });
26
+ return {
27
+ tools: actions.map((action) => ({
28
+ inputSchema: action.schema.input,
29
+ // todo(blam): this is unfortunately not supported by most clients yet.
30
+ // When this is provided you need to provide structuredContent instead.
31
+ // outputSchema: action.schema.output,
32
+ name: action.name,
33
+ description: action.description,
34
+ annotations: {
35
+ title: action.title,
36
+ destructiveHint: action.attributes.destructive,
37
+ idempotentHint: action.attributes.idempotent,
38
+ readOnlyHint: action.attributes.readOnly,
39
+ openWorldHint: false
40
+ }
41
+ }))
42
+ };
43
+ });
44
+ server.setRequestHandler(types_js.CallToolRequestSchema, async ({ params }) => {
45
+ const { actions } = await this.actions.list({ credentials });
46
+ const action = actions.find((a) => a.name === params.name);
47
+ if (!action) {
48
+ throw new errors.NotFoundError(`Action "${params.name}" not found`);
49
+ }
50
+ const { output } = await this.actions.invoke({
51
+ id: action.id,
52
+ input: params.arguments,
53
+ credentials
54
+ });
55
+ return {
56
+ // todo(blam): unfortunately structuredContent is not supported by most clients yet.
57
+ // so the validation for the output happens in the default actions registry
58
+ // and we return it as json text instead for now.
59
+ content: [
60
+ {
61
+ type: "text",
62
+ text: ["```json", JSON.stringify(output, null, 2), "```"].join(
63
+ "\n"
64
+ )
65
+ }
66
+ ]
67
+ };
68
+ });
69
+ return server;
70
+ }
71
+ }
72
+
73
+ exports.McpService = McpService;
74
+ //# sourceMappingURL=McpService.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpService.cjs.js","sources":["../../src/services/McpService.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { BackstageCredentials } from '@backstage/backend-plugin-api';\nimport { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { JsonObject } from '@backstage/types';\nimport { ActionsService } from '@backstage/backend-plugin-api/alpha';\nimport { version } from '@backstage/plugin-mcp-actions-backend/package.json';\nimport { NotFoundError } from '@backstage/errors';\n\nexport class McpService {\n constructor(private readonly actions: ActionsService) {}\n\n static async create({ actions }: { actions: ActionsService }) {\n return new McpService(actions);\n }\n\n getServer({ credentials }: { credentials: BackstageCredentials }) {\n const server = new McpServer(\n {\n name: 'backstage',\n // TODO: this version will most likely change in the future.\n version,\n },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n // TODO: switch this to be configuration based later\n const { actions } = await this.actions.list({ credentials });\n\n return {\n tools: actions.map(action => ({\n inputSchema: action.schema.input,\n // todo(blam): this is unfortunately not supported by most clients yet.\n // When this is provided you need to provide structuredContent instead.\n // outputSchema: action.schema.output,\n name: action.name,\n description: action.description,\n annotations: {\n title: action.title,\n destructiveHint: action.attributes.destructive,\n idempotentHint: action.attributes.idempotent,\n readOnlyHint: action.attributes.readOnly,\n openWorldHint: false,\n },\n })),\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {\n const { actions } = await this.actions.list({ credentials });\n const action = actions.find(a => a.name === params.name);\n\n if (!action) {\n throw new NotFoundError(`Action \"${params.name}\" not found`);\n }\n\n const { output } = await this.actions.invoke({\n id: action.id,\n input: params.arguments as JsonObject,\n credentials,\n });\n\n return {\n // todo(blam): unfortunately structuredContent is not supported by most clients yet.\n // so the validation for the output happens in the default actions registry\n // and we return it as json text instead for now.\n content: [\n {\n type: 'text',\n text: ['```json', JSON.stringify(output, null, 2), '```'].join(\n '\\n',\n ),\n },\n ],\n };\n });\n\n return server;\n }\n}\n"],"names":["McpServer","version","ListToolsRequestSchema","CallToolRequestSchema","NotFoundError"],"mappings":";;;;;;;AA0BO,MAAM,UAAW,CAAA;AAAA,EACtB,YAA6B,OAAyB,EAAA;AAAzB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA;AAA0B,EAEvD,aAAa,MAAA,CAAO,EAAE,OAAA,EAAwC,EAAA;AAC5D,IAAO,OAAA,IAAI,WAAW,OAAO,CAAA;AAAA;AAC/B,EAEA,SAAA,CAAU,EAAE,WAAA,EAAsD,EAAA;AAChE,IAAA,MAAM,SAAS,IAAIA,eAAA;AAAA,MACjB;AAAA,QACE,IAAM,EAAA,WAAA;AAAA;AAAA,iBAENC;AAAA,OACF;AAAA,MACA,EAAE,YAAc,EAAA,EAAE,KAAO,EAAA,IAAK;AAAA,KAChC;AAEA,IAAO,MAAA,CAAA,iBAAA,CAAkBC,iCAAwB,YAAY;AAE3D,MAAM,MAAA,EAAE,SAAY,GAAA,MAAM,KAAK,OAAQ,CAAA,IAAA,CAAK,EAAE,WAAA,EAAa,CAAA;AAE3D,MAAO,OAAA;AAAA,QACL,KAAA,EAAO,OAAQ,CAAA,GAAA,CAAI,CAAW,MAAA,MAAA;AAAA,UAC5B,WAAA,EAAa,OAAO,MAAO,CAAA,KAAA;AAAA;AAAA;AAAA;AAAA,UAI3B,MAAM,MAAO,CAAA,IAAA;AAAA,UACb,aAAa,MAAO,CAAA,WAAA;AAAA,UACpB,WAAa,EAAA;AAAA,YACX,OAAO,MAAO,CAAA,KAAA;AAAA,YACd,eAAA,EAAiB,OAAO,UAAW,CAAA,WAAA;AAAA,YACnC,cAAA,EAAgB,OAAO,UAAW,CAAA,UAAA;AAAA,YAClC,YAAA,EAAc,OAAO,UAAW,CAAA,QAAA;AAAA,YAChC,aAAe,EAAA;AAAA;AACjB,SACA,CAAA;AAAA,OACJ;AAAA,KACD,CAAA;AAED,IAAA,MAAA,CAAO,iBAAkB,CAAAC,8BAAA,EAAuB,OAAO,EAAE,QAAa,KAAA;AACpE,MAAM,MAAA,EAAE,SAAY,GAAA,MAAM,KAAK,OAAQ,CAAA,IAAA,CAAK,EAAE,WAAA,EAAa,CAAA;AAC3D,MAAA,MAAM,SAAS,OAAQ,CAAA,IAAA,CAAK,OAAK,CAAE,CAAA,IAAA,KAAS,OAAO,IAAI,CAAA;AAEvD,MAAA,IAAI,CAAC,MAAQ,EAAA;AACX,QAAA,MAAM,IAAIC,oBAAA,CAAc,CAAW,QAAA,EAAA,MAAA,CAAO,IAAI,CAAa,WAAA,CAAA,CAAA;AAAA;AAG7D,MAAA,MAAM,EAAE,MAAO,EAAA,GAAI,MAAM,IAAA,CAAK,QAAQ,MAAO,CAAA;AAAA,QAC3C,IAAI,MAAO,CAAA,EAAA;AAAA,QACX,OAAO,MAAO,CAAA,SAAA;AAAA,QACd;AAAA,OACD,CAAA;AAED,MAAO,OAAA;AAAA;AAAA;AAAA;AAAA,QAIL,OAAS,EAAA;AAAA,UACP;AAAA,YACE,IAAM,EAAA,MAAA;AAAA,YACN,IAAA,EAAM,CAAC,SAAA,EAAW,IAAK,CAAA,SAAA,CAAU,QAAQ,IAAM,EAAA,CAAC,CAAG,EAAA,KAAK,CAAE,CAAA,IAAA;AAAA,cACxD;AAAA;AACF;AACF;AACF,OACF;AAAA,KACD,CAAA;AAED,IAAO,OAAA,MAAA;AAAA;AAEX;;;;"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@backstage/plugin-mcp-actions-backend",
3
+ "version": "0.0.0-nightly-20250620024248",
4
+ "backstage": {
5
+ "role": "backend-plugin",
6
+ "pluginId": "mcp-actions",
7
+ "pluginPackages": [
8
+ "@backstage/plugin-mcp-actions-backend"
9
+ ],
10
+ "features": {
11
+ ".": "@backstage/BackendFeature"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "main": "dist/index.cjs.js",
17
+ "types": "dist/index.d.ts"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/backstage/backstage",
22
+ "directory": "plugins/mcp-actions-backend"
23
+ },
24
+ "license": "Apache-2.0",
25
+ "main": "dist/index.cjs.js",
26
+ "types": "dist/index.d.ts",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "scripts": {
31
+ "build": "backstage-cli package build",
32
+ "clean": "backstage-cli package clean",
33
+ "lint": "backstage-cli package lint",
34
+ "prepack": "backstage-cli package prepack",
35
+ "postpack": "backstage-cli package postpack",
36
+ "start": "backstage-cli package start",
37
+ "test": "backstage-cli package test"
38
+ },
39
+ "dependencies": {
40
+ "@backstage/backend-defaults": "0.11.0",
41
+ "@backstage/backend-plugin-api": "1.4.0",
42
+ "@backstage/catalog-client": "1.10.1",
43
+ "@backstage/errors": "1.2.7",
44
+ "@backstage/plugin-catalog-node": "1.17.1",
45
+ "@backstage/types": "1.2.1",
46
+ "@modelcontextprotocol/sdk": "^1.12.3",
47
+ "express": "^4.17.1",
48
+ "express-promise-router": "^4.1.0",
49
+ "zod": "^3.22.4"
50
+ },
51
+ "devDependencies": {
52
+ "@backstage/backend-test-utils": "1.6.0",
53
+ "@backstage/cli": "0.33.0",
54
+ "@types/express": "^4.17.6"
55
+ },
56
+ "typesVersions": {
57
+ "*": {
58
+ "package.json": [
59
+ "package.json"
60
+ ]
61
+ }
62
+ }
63
+ }