@backstage/plugin-mcp-actions-backend 0.1.10-next.1 → 0.1.10

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # @backstage/plugin-mcp-actions-backend
2
2
 
3
+ ## 0.1.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500.
8
+ - dee4283: Added `mcpActions.name` and `mcpActions.description` config options to customize the MCP server identity. Namespaced tool names now use dot separator to align with the MCP spec convention.
9
+ - a49a40d: Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports.
10
+ - c74b697: Added support for splitting MCP actions into multiple servers via `mcpActions.servers` configuration. Each server gets its own endpoint at `/api/mcp-actions/v1/{key}` with actions scoped using include/exclude filter rules. Tool names are now namespaced with the plugin ID by default, configurable via `mcpActions.namespacedToolNames`. When `mcpActions.servers` is not configured, the plugin continues to serve a single server at `/api/mcp-actions/v1`.
11
+ - dc81af1: Adds two new metrics to track MCP server operations and sessions.
12
+
13
+ - `mcp.server.operation.duration`: The duration taken to process an individual MCP operation
14
+ - `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server
15
+
16
+ - Updated dependencies
17
+ - @backstage/backend-plugin-api@1.8.0
18
+ - @backstage/catalog-client@1.14.0
19
+ - @backstage/plugin-catalog-node@2.1.0
20
+
21
+ ## 0.1.10-next.2
22
+
23
+ ### Patch Changes
24
+
25
+ - c74b697: Added support for splitting MCP actions into multiple servers via `mcpActions.servers` configuration. Each server gets its own endpoint at `/api/mcp-actions/v1/{key}` with actions scoped using include/exclude filter rules. Tool names are now namespaced with the plugin ID by default, configurable via `mcpActions.namespacedToolNames`. When `mcpActions.servers` is not configured, the plugin continues to serve a single server at `/api/mcp-actions/v1`.
26
+ - Updated dependencies
27
+ - @backstage/backend-plugin-api@1.8.0-next.1
28
+ - @backstage/catalog-client@1.14.0-next.2
29
+ - @backstage/plugin-catalog-node@2.1.0-next.2
30
+
3
31
  ## 0.1.10-next.1
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -71,6 +71,64 @@ export const myPlugin = createBackendPlugin({
71
71
  });
72
72
  ```
73
73
 
74
+ ### Namespaced Tool Names
75
+
76
+ By default, MCP tool names include the plugin ID prefix to avoid collisions across plugins. For example, an action registered as `greet-user` by `my-custom-plugin` is exposed as `my-custom-plugin.greet-user`.
77
+
78
+ You can disable this if you need the short names for backward compatibility:
79
+
80
+ ```yaml
81
+ mcpActions:
82
+ namespacedToolNames: false
83
+ ```
84
+
85
+ ### Multiple MCP Servers
86
+
87
+ By default, the plugin serves a single MCP server at `/api/mcp-actions/v1` that exposes all available actions. You can split actions into multiple focused servers by configuring `mcpActions.servers`, where each key becomes a separate MCP server endpoint.
88
+
89
+ ```yaml
90
+ mcpActions:
91
+ servers:
92
+ catalog:
93
+ name: 'Backstage Catalog'
94
+ description: 'Tools for interacting with the software catalog'
95
+ filter:
96
+ include:
97
+ - id: 'catalog:*'
98
+ scaffolder:
99
+ name: 'Backstage Scaffolder'
100
+ description: 'Tools for creating new software from templates'
101
+ filter:
102
+ include:
103
+ - id: 'scaffolder:*'
104
+ ```
105
+
106
+ This creates two MCP server endpoints:
107
+
108
+ - `http://localhost:7007/api/mcp-actions/v1/catalog`
109
+ - `http://localhost:7007/api/mcp-actions/v1/scaffolder`
110
+
111
+ Each server uses include filter rules with glob patterns on action IDs to control which actions are exposed. For example, `id: 'catalog:*'` matches all actions registered by the catalog plugin.
112
+
113
+ When `mcpActions.servers` is not configured, the plugin behaves exactly as before with a single server at `/api/mcp-actions/v1`.
114
+
115
+ #### Filter Rules
116
+
117
+ Include and exclude filter rules support glob patterns on action IDs and attribute matching. Exclude rules take precedence over include rules. When include rules are specified, actions must match at least one include rule to be exposed.
118
+
119
+ ```yaml
120
+ mcpActions:
121
+ servers:
122
+ catalog:
123
+ name: 'Backstage Catalog'
124
+ filter:
125
+ include:
126
+ - id: 'catalog:*'
127
+ exclude:
128
+ - attributes:
129
+ destructive: true
130
+ ```
131
+
74
132
  ### Error Handling
75
133
 
76
134
  When errors are thrown from MCP actions, the backend will handle and surface error message for any error from `@backstage/errors`. Unknown errors will be handled by `@modelcontextprotocol/sdk`'s default error handling, which may result in a generic `500 Server Error` being returned. As a result, we recommend using errors from `@backstage/errors` when applicable.
@@ -159,16 +217,15 @@ The MCP server supports both Server-Sent Events (SSE) and Streamable HTTP protoc
159
217
 
160
218
  The SSE protocol is deprecated, and should be avoided as it will be removed in a future release.
161
219
 
220
+ ### Single Server (default)
221
+
162
222
  - `Streamable HTTP`: `http://localhost:7007/api/mcp-actions/v1`
163
223
  - `SSE`: `http://localhost:7007/api/mcp-actions/v1/sse`
164
224
 
165
- There's a few different ways to configure MCP tools, but here's a snippet of the most common.
166
-
167
225
  ```json
168
226
  {
169
227
  "mcpServers": {
170
228
  "backstage-actions": {
171
- // you can also replace this with the public / internal URL of the deployed backend.
172
229
  "url": "http://localhost:7007/api/mcp-actions/v1",
173
230
  "headers": {
174
231
  "Authorization": "Bearer ${MCP_TOKEN}"
@@ -178,6 +235,32 @@ There's a few different ways to configure MCP tools, but here's a snippet of the
178
235
  }
179
236
  ```
180
237
 
238
+ ### Multiple Servers
239
+
240
+ When `mcpActions.servers` is configured, each server key becomes part of the URL. For example, with servers named `catalog` and `scaffolder`:
241
+
242
+ - `http://localhost:7007/api/mcp-actions/v1/catalog`
243
+ - `http://localhost:7007/api/mcp-actions/v1/scaffolder`
244
+
245
+ ```json
246
+ {
247
+ "mcpServers": {
248
+ "backstage-catalog": {
249
+ "url": "http://localhost:7007/api/mcp-actions/v1/catalog",
250
+ "headers": {
251
+ "Authorization": "Bearer ${MCP_TOKEN}"
252
+ }
253
+ },
254
+ "backstage-scaffolder": {
255
+ "url": "http://localhost:7007/api/mcp-actions/v1/scaffolder",
256
+ "headers": {
257
+ "Authorization": "Bearer ${MCP_TOKEN}"
258
+ }
259
+ }
260
+ }
261
+ }
262
+ ```
263
+
181
264
  ## Metrics
182
265
 
183
266
  The MCP Actions Backend emits metrics for the following operations:
package/config.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ /*
2
+ * Copyright 2026 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ export interface Config {
18
+ mcpActions?: {
19
+ /**
20
+ * Display name for the MCP server. Defaults to "backstage".
21
+ * Used when running a single bundled server without mcpActions.servers.
22
+ */
23
+ name?: string;
24
+
25
+ /**
26
+ * Description of the MCP server.
27
+ * Used when running a single bundled server without mcpActions.servers.
28
+ */
29
+ description?: string;
30
+
31
+ /**
32
+ * When true, MCP tool names include the plugin ID prefix to avoid
33
+ * collisions across plugins. For example an action registered as
34
+ * "get-entity" by the catalog plugin becomes "catalog.get-entity".
35
+ * Defaults to true.
36
+ */
37
+ namespacedToolNames?: boolean;
38
+
39
+ /**
40
+ * Named MCP servers, each exposed at /api/mcp-actions/v1/{key}.
41
+ * When not configured, the plugin serves a single server at /api/mcp-actions/v1.
42
+ */
43
+ servers?: {
44
+ [serverKey: string]: {
45
+ /** Display name for the MCP server. */
46
+ name: string;
47
+ /** Description of the MCP server. */
48
+ description?: string;
49
+ /** Filter rules to include or exclude specific actions. */
50
+ filter?: {
51
+ include?: Array<{
52
+ /** Glob pattern matched against action ID. */
53
+ id?: string;
54
+ /** Match actions by their attribute flags. */
55
+ attributes?: {
56
+ destructive?: boolean;
57
+ readOnly?: boolean;
58
+ idempotent?: boolean;
59
+ };
60
+ }>;
61
+ exclude?: Array<{
62
+ /** Glob pattern matched against action ID. */
63
+ id?: string;
64
+ /** Match actions by their attribute flags. */
65
+ attributes?: {
66
+ destructive?: boolean;
67
+ readOnly?: boolean;
68
+ idempotent?: boolean;
69
+ };
70
+ }>;
71
+ };
72
+ };
73
+ };
74
+ };
75
+ }
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ var minimatch = require('minimatch');
4
+
5
+ const SERVER_KEY_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
6
+ function parseFilterRules(configArray) {
7
+ return configArray.map((ruleConfig) => {
8
+ const idPattern = ruleConfig.getOptionalString("id");
9
+ const attributesConfig = ruleConfig.getOptionalConfig("attributes");
10
+ const rule = {};
11
+ if (idPattern) {
12
+ rule.idMatcher = new minimatch.Minimatch(idPattern);
13
+ }
14
+ if (attributesConfig) {
15
+ rule.attributes = {};
16
+ for (const key of ["destructive", "readOnly", "idempotent"]) {
17
+ const value = attributesConfig.getOptionalBoolean(key);
18
+ if (value !== void 0) {
19
+ rule.attributes[key] = value;
20
+ }
21
+ }
22
+ }
23
+ return rule;
24
+ });
25
+ }
26
+ function parseServerConfigs(config) {
27
+ const serversConfig = config.getOptionalConfig("mcpActions.servers");
28
+ if (!serversConfig) {
29
+ return void 0;
30
+ }
31
+ const servers = /* @__PURE__ */ new Map();
32
+ for (const key of serversConfig.keys()) {
33
+ if (!SERVER_KEY_PATTERN.test(key)) {
34
+ throw new Error(
35
+ `Invalid MCP server key "${key}": must be lowercase alphanumeric with hyphens`
36
+ );
37
+ }
38
+ const serverConfig = serversConfig.getConfig(key);
39
+ const filterConfig = serverConfig.getOptionalConfig("filter");
40
+ const includeRules = parseFilterRules(
41
+ filterConfig?.getOptionalConfigArray("include") ?? []
42
+ );
43
+ const excludeRules = parseFilterRules(
44
+ filterConfig?.getOptionalConfigArray("exclude") ?? []
45
+ );
46
+ servers.set(key, {
47
+ name: serverConfig.getString("name"),
48
+ description: serverConfig.getOptionalString("description"),
49
+ includeRules,
50
+ excludeRules
51
+ });
52
+ }
53
+ return servers;
54
+ }
55
+
56
+ exports.parseFilterRules = parseFilterRules;
57
+ exports.parseServerConfigs = parseServerConfigs;
58
+ //# sourceMappingURL=config.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.cjs.js","sources":["../src/config.ts"],"sourcesContent":["/*\n * Copyright 2026 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 */\n\nimport { Config } from '@backstage/config';\nimport { Minimatch } from 'minimatch';\n\nexport type FilterRule = {\n idMatcher?: Minimatch;\n attributes?: Partial<\n Record<'destructive' | 'readOnly' | 'idempotent', boolean>\n >;\n};\n\nexport type McpServerConfig = {\n name: string;\n description?: string;\n includeRules: FilterRule[];\n excludeRules: FilterRule[];\n};\n\nconst SERVER_KEY_PATTERN = /^[a-z0-9][a-z0-9-]*$/;\n\nexport function parseFilterRules(configArray: Config[]): FilterRule[] {\n return configArray.map(ruleConfig => {\n const idPattern = ruleConfig.getOptionalString('id');\n const attributesConfig = ruleConfig.getOptionalConfig('attributes');\n\n const rule: FilterRule = {};\n\n if (idPattern) {\n rule.idMatcher = new Minimatch(idPattern);\n }\n\n if (attributesConfig) {\n rule.attributes = {};\n for (const key of ['destructive', 'readOnly', 'idempotent'] as const) {\n const value = attributesConfig.getOptionalBoolean(key);\n if (value !== undefined) {\n rule.attributes[key] = value;\n }\n }\n }\n\n return rule;\n });\n}\n\nexport function parseServerConfigs(\n config: Config,\n): Map<string, McpServerConfig> | undefined {\n const serversConfig = config.getOptionalConfig('mcpActions.servers');\n if (!serversConfig) {\n return undefined;\n }\n\n const servers = new Map<string, McpServerConfig>();\n\n for (const key of serversConfig.keys()) {\n if (!SERVER_KEY_PATTERN.test(key)) {\n throw new Error(\n `Invalid MCP server key \"${key}\": must be lowercase alphanumeric with hyphens`,\n );\n }\n\n const serverConfig = serversConfig.getConfig(key);\n\n const filterConfig = serverConfig.getOptionalConfig('filter');\n const includeRules = parseFilterRules(\n filterConfig?.getOptionalConfigArray('include') ?? [],\n );\n const excludeRules = parseFilterRules(\n filterConfig?.getOptionalConfigArray('exclude') ?? [],\n );\n\n servers.set(key, {\n name: serverConfig.getString('name'),\n description: serverConfig.getOptionalString('description'),\n includeRules,\n excludeRules,\n });\n }\n\n return servers;\n}\n"],"names":["Minimatch"],"mappings":";;;;AAiCA,MAAM,kBAAA,GAAqB,sBAAA;AAEpB,SAAS,iBAAiB,WAAA,EAAqC;AACpE,EAAA,OAAO,WAAA,CAAY,IAAI,CAAA,UAAA,KAAc;AACnC,IAAA,MAAM,SAAA,GAAY,UAAA,CAAW,iBAAA,CAAkB,IAAI,CAAA;AACnD,IAAA,MAAM,gBAAA,GAAmB,UAAA,CAAW,iBAAA,CAAkB,YAAY,CAAA;AAElE,IAAA,MAAM,OAAmB,EAAC;AAE1B,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,IAAA,CAAK,SAAA,GAAY,IAAIA,mBAAA,CAAU,SAAS,CAAA;AAAA,IAC1C;AAEA,IAAA,IAAI,gBAAA,EAAkB;AACpB,MAAA,IAAA,CAAK,aAAa,EAAC;AACnB,MAAA,KAAA,MAAW,GAAA,IAAO,CAAC,aAAA,EAAe,UAAA,EAAY,YAAY,CAAA,EAAY;AACpE,QAAA,MAAM,KAAA,GAAQ,gBAAA,CAAiB,kBAAA,CAAmB,GAAG,CAAA;AACrD,QAAA,IAAI,UAAU,MAAA,EAAW;AACvB,UAAA,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,GAAI,KAAA;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AAEA,IAAA,OAAO,IAAA;AAAA,EACT,CAAC,CAAA;AACH;AAEO,SAAS,mBACd,MAAA,EAC0C;AAC1C,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,iBAAA,CAAkB,oBAAoB,CAAA;AACnE,EAAA,IAAI,CAAC,aAAA,EAAe;AAClB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,uBAAc,GAAA,EAA6B;AAEjD,EAAA,KAAA,MAAW,GAAA,IAAO,aAAA,CAAc,IAAA,EAAK,EAAG;AACtC,IAAA,IAAI,CAAC,kBAAA,CAAmB,IAAA,CAAK,GAAG,CAAA,EAAG;AACjC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,2BAA2B,GAAG,CAAA,8CAAA;AAAA,OAChC;AAAA,IACF;AAEA,IAAA,MAAM,YAAA,GAAe,aAAA,CAAc,SAAA,CAAU,GAAG,CAAA;AAEhD,IAAA,MAAM,YAAA,GAAe,YAAA,CAAa,iBAAA,CAAkB,QAAQ,CAAA;AAC5D,IAAA,MAAM,YAAA,GAAe,gBAAA;AAAA,MACnB,YAAA,EAAc,sBAAA,CAAuB,SAAS,CAAA,IAAK;AAAC,KACtD;AACA,IAAA,MAAM,YAAA,GAAe,gBAAA;AAAA,MACnB,YAAA,EAAc,sBAAA,CAAuB,SAAS,CAAA,IAAK;AAAC,KACtD;AAEA,IAAA,OAAA,CAAQ,IAAI,GAAA,EAAK;AAAA,MACf,IAAA,EAAM,YAAA,CAAa,SAAA,CAAU,MAAM,CAAA;AAAA,MACnC,WAAA,EAAa,YAAA,CAAa,iBAAA,CAAkB,aAAa,CAAA;AAAA,MACzD,YAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,OAAA;AACT;;;;;"}
@@ -7,6 +7,7 @@ var McpService = require('./services/McpService.cjs.js');
7
7
  var createStreamableRouter = require('./routers/createStreamableRouter.cjs.js');
8
8
  var createSseRouter = require('./routers/createSseRouter.cjs.js');
9
9
  var alpha = require('@backstage/backend-plugin-api/alpha');
10
+ var config = require('./config.cjs.js');
10
11
 
11
12
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
12
13
 
@@ -35,31 +36,57 @@ const mcpPlugin = backendPluginApi.createBackendPlugin({
35
36
  httpAuth,
36
37
  rootRouter,
37
38
  discovery,
38
- config,
39
+ config: config$1,
39
40
  metrics
40
41
  }) {
42
+ const serverConfigs = config.parseServerConfigs(config$1);
43
+ const namespacedToolNames = config$1.getOptionalBoolean(
44
+ "mcpActions.namespacedToolNames"
45
+ );
41
46
  const mcpService = await McpService.McpService.create({
42
47
  actions,
43
- metrics
44
- });
45
- const sseRouter = createSseRouter.createSseRouter({
46
- mcpService,
47
- httpAuth
48
- });
49
- const streamableRouter = createStreamableRouter.createStreamableRouter({
50
- mcpService,
51
- httpAuth,
52
- logger,
53
- metrics
48
+ metrics,
49
+ namespacedToolNames
54
50
  });
55
51
  const router = PromiseRouter__default.default();
56
52
  router.use(express.json());
57
- router.use("/v1/sse", sseRouter);
58
- router.use("/v1", streamableRouter);
53
+ if (serverConfigs && serverConfigs.size > 0) {
54
+ for (const [key, serverConfig] of serverConfigs) {
55
+ const streamableRouter = createStreamableRouter.createStreamableRouter({
56
+ mcpService,
57
+ httpAuth,
58
+ logger,
59
+ metrics,
60
+ serverConfig
61
+ });
62
+ router.use(`/v1/${key}`, streamableRouter);
63
+ }
64
+ } else {
65
+ const serverConfig = {
66
+ name: config$1.getOptionalString("mcpActions.name") ?? "backstage",
67
+ description: config$1.getOptionalString("mcpActions.description"),
68
+ includeRules: [],
69
+ excludeRules: []
70
+ };
71
+ const sseRouter = createSseRouter.createSseRouter({
72
+ mcpService,
73
+ httpAuth,
74
+ serverConfig
75
+ });
76
+ const streamableRouter = createStreamableRouter.createStreamableRouter({
77
+ mcpService,
78
+ httpAuth,
79
+ logger,
80
+ metrics,
81
+ serverConfig
82
+ });
83
+ router.use("/v1/sse", sseRouter);
84
+ router.use("/v1", streamableRouter);
85
+ }
59
86
  httpRouter.use(router);
60
- const oauthEnabled = config.getOptionalBoolean(
87
+ const oauthEnabled = config$1.getOptionalBoolean(
61
88
  "auth.experimentalDynamicClientRegistration.enabled"
62
- ) || config.getOptionalBoolean(
89
+ ) || config$1.getOptionalBoolean(
63
90
  "auth.experimentalClientIdMetadataDocuments.enabled"
64
91
  );
65
92
  if (oauthEnabled) {
@@ -1 +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 } from 'express';\nimport Router from 'express-promise-router';\nimport { McpService } from './services/McpService';\nimport { createStreamableRouter } from './routers/createStreamableRouter';\nimport { createSseRouter } from './routers/createSseRouter';\nimport {\n actionsRegistryServiceRef,\n actionsServiceRef,\n metricsServiceRef,\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 rootRouter: coreServices.rootHttpRouter,\n discovery: coreServices.discovery,\n config: coreServices.rootConfig,\n metrics: metricsServiceRef,\n },\n async init({\n actions,\n logger,\n httpRouter,\n httpAuth,\n rootRouter,\n discovery,\n config,\n metrics,\n }) {\n const mcpService = await McpService.create({\n actions,\n metrics,\n });\n\n const sseRouter = createSseRouter({\n mcpService,\n httpAuth,\n });\n\n const streamableRouter = createStreamableRouter({\n mcpService,\n httpAuth,\n logger,\n metrics,\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 const oauthEnabled =\n config.getOptionalBoolean(\n 'auth.experimentalDynamicClientRegistration.enabled',\n ) ||\n config.getOptionalBoolean(\n 'auth.experimentalClientIdMetadataDocuments.enabled',\n );\n\n if (oauthEnabled) {\n // OAuth Authorization Server Metadata (RFC 8414)\n // This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by\n // many of the MCP clients as of yet. So this seems to be the oldest version of the spec that's implemented.\n rootRouter.use(\n '/.well-known/oauth-authorization-server',\n async (_, res) => {\n const authBaseUrl = await discovery.getBaseUrl('auth');\n const oidcResponse = await fetch(\n `${authBaseUrl}/.well-known/openid-configuration`,\n );\n res.json(await oidcResponse.json());\n },\n );\n\n // Protected Resource Metadata (RFC 9728)\n // https://datatracker.ietf.org/doc/html/rfc9728\n // This allows MCP clients to discover the authorization server for this resource\n rootRouter.use(\n '/.well-known/oauth-protected-resource',\n async (_, res) => {\n const [authBaseUrl, mcpBaseUrl] = await Promise.all([\n discovery.getBaseUrl('auth'),\n discovery.getBaseUrl('mcp-actions'),\n ]);\n res.json({\n resource: mcpBaseUrl,\n authorization_servers: [authBaseUrl],\n });\n },\n );\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","actionsServiceRef","actionsRegistryServiceRef","metricsServiceRef","McpService","createSseRouter","createStreamableRouter","Router","json"],"mappings":";;;;;;;;;;;;;;AAmCO,MAAM,YAAYA,oCAAA,CAAoB;AAAA,EAC3C,QAAA,EAAU,aAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;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,OAAA,EAASC,uBAAA;AAAA,QACT,QAAA,EAAUC,+BAAA;AAAA,QACV,YAAYF,6BAAA,CAAa,cAAA;AAAA,QACzB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,OAAA,EAASG;AAAA,OACX;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,OAAA;AAAA,QACA,MAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,SAAA;AAAA,QACA,MAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAM,UAAA,GAAa,MAAMC,qBAAA,CAAW,MAAA,CAAO;AAAA,UACzC,OAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,YAAYC,+BAAA,CAAgB;AAAA,UAChC,UAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,mBAAmBC,6CAAA,CAAuB;AAAA,UAC9C,UAAA;AAAA,UACA,QAAA;AAAA,UACA,MAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,SAASC,8BAAA,EAAO;AACtB,QAAA,MAAA,CAAO,GAAA,CAAIC,cAAM,CAAA;AAEjB,QAAA,MAAA,CAAO,GAAA,CAAI,WAAW,SAAS,CAAA;AAC/B,QAAA,MAAA,CAAO,GAAA,CAAI,OAAO,gBAAgB,CAAA;AAElC,QAAA,UAAA,CAAW,IAAI,MAAM,CAAA;AAErB,QAAA,MAAM,eACJ,MAAA,CAAO,kBAAA;AAAA,UACL;AAAA,aAEF,MAAA,CAAO,kBAAA;AAAA,UACL;AAAA,SACF;AAEF,QAAA,IAAI,YAAA,EAAc;AAIhB,UAAA,UAAA,CAAW,GAAA;AAAA,YACT,yCAAA;AAAA,YACA,OAAO,GAAG,GAAA,KAAQ;AAChB,cAAA,MAAM,WAAA,GAAc,MAAM,SAAA,CAAU,UAAA,CAAW,MAAM,CAAA;AACrD,cAAA,MAAM,eAAe,MAAM,KAAA;AAAA,gBACzB,GAAG,WAAW,CAAA,iCAAA;AAAA,eAChB;AACA,cAAA,GAAA,CAAI,IAAA,CAAK,MAAM,YAAA,CAAa,IAAA,EAAM,CAAA;AAAA,YACpC;AAAA,WACF;AAKA,UAAA,UAAA,CAAW,GAAA;AAAA,YACT,uCAAA;AAAA,YACA,OAAO,GAAG,GAAA,KAAQ;AAChB,cAAA,MAAM,CAAC,WAAA,EAAa,UAAU,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,gBAClD,SAAA,CAAU,WAAW,MAAM,CAAA;AAAA,gBAC3B,SAAA,CAAU,WAAW,aAAa;AAAA,eACnC,CAAA;AACD,cAAA,GAAA,CAAI,IAAA,CAAK;AAAA,gBACP,QAAA,EAAU,UAAA;AAAA,gBACV,qBAAA,EAAuB,CAAC,WAAW;AAAA,eACpC,CAAA;AAAA,YACH;AAAA,WACF;AAAA,QACF;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
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 } from 'express';\nimport Router from 'express-promise-router';\nimport { McpService } from './services/McpService';\nimport { createStreamableRouter } from './routers/createStreamableRouter';\nimport { createSseRouter } from './routers/createSseRouter';\nimport {\n actionsRegistryServiceRef,\n actionsServiceRef,\n metricsServiceRef,\n} from '@backstage/backend-plugin-api/alpha';\nimport { parseServerConfigs } from './config';\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 rootRouter: coreServices.rootHttpRouter,\n discovery: coreServices.discovery,\n config: coreServices.rootConfig,\n metrics: metricsServiceRef,\n },\n async init({\n actions,\n logger,\n httpRouter,\n httpAuth,\n rootRouter,\n discovery,\n config,\n metrics,\n }) {\n const serverConfigs = parseServerConfigs(config);\n const namespacedToolNames = config.getOptionalBoolean(\n 'mcpActions.namespacedToolNames',\n );\n\n const mcpService = await McpService.create({\n actions,\n metrics,\n namespacedToolNames,\n });\n\n const router = Router();\n router.use(json());\n\n if (serverConfigs && serverConfigs.size > 0) {\n for (const [key, serverConfig] of serverConfigs) {\n const streamableRouter = createStreamableRouter({\n mcpService,\n httpAuth,\n logger,\n metrics,\n serverConfig,\n });\n\n router.use(`/v1/${key}`, streamableRouter);\n }\n } else {\n const serverConfig = {\n name: config.getOptionalString('mcpActions.name') ?? 'backstage',\n description: config.getOptionalString('mcpActions.description'),\n includeRules: [],\n excludeRules: [],\n };\n\n const sseRouter = createSseRouter({\n mcpService,\n httpAuth,\n serverConfig,\n });\n\n const streamableRouter = createStreamableRouter({\n mcpService,\n httpAuth,\n logger,\n metrics,\n serverConfig,\n });\n\n router.use('/v1/sse', sseRouter);\n router.use('/v1', streamableRouter);\n }\n\n httpRouter.use(router);\n\n const oauthEnabled =\n config.getOptionalBoolean(\n 'auth.experimentalDynamicClientRegistration.enabled',\n ) ||\n config.getOptionalBoolean(\n 'auth.experimentalClientIdMetadataDocuments.enabled',\n );\n\n if (oauthEnabled) {\n // OAuth Authorization Server Metadata (RFC 8414)\n // This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by\n // many of the MCP clients as of yet. So this seems to be the oldest version of the spec that's implemented.\n rootRouter.use(\n '/.well-known/oauth-authorization-server',\n async (_, res) => {\n const authBaseUrl = await discovery.getBaseUrl('auth');\n const oidcResponse = await fetch(\n `${authBaseUrl}/.well-known/openid-configuration`,\n );\n res.json(await oidcResponse.json());\n },\n );\n\n // Protected Resource Metadata (RFC 9728)\n // https://datatracker.ietf.org/doc/html/rfc9728\n // This allows MCP clients to discover the authorization server for this resource\n rootRouter.use(\n '/.well-known/oauth-protected-resource',\n async (_, res) => {\n const [authBaseUrl, mcpBaseUrl] = await Promise.all([\n discovery.getBaseUrl('auth'),\n discovery.getBaseUrl('mcp-actions'),\n ]);\n res.json({\n resource: mcpBaseUrl,\n authorization_servers: [authBaseUrl],\n });\n },\n );\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","actionsServiceRef","actionsRegistryServiceRef","metricsServiceRef","config","parseServerConfigs","McpService","Router","json","createStreamableRouter","createSseRouter"],"mappings":"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"}
@@ -9,13 +9,15 @@ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
9
9
 
10
10
  const createSseRouter = ({
11
11
  mcpService,
12
- httpAuth
12
+ httpAuth,
13
+ serverConfig
13
14
  }) => {
14
15
  const router = PromiseRouter__default.default();
15
16
  const transportsToSessionId = /* @__PURE__ */ new Map();
16
17
  router.get("/", async (req, res) => {
17
18
  const server = mcpService.getServer({
18
- credentials: await httpAuth.credentials(req)
19
+ credentials: await httpAuth.credentials(req),
20
+ serverConfig
19
21
  });
20
22
  const transport = new sse_js.SSEServerTransport(
21
23
  `${req.originalUrl}/messages`,
@@ -1 +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,CAAA,KAGc;AACZ,EAAA,MAAM,SAASA,8BAAA,EAAc;AAC7B,EAAA,MAAM,qBAAA,uBAA4B,GAAA,EAAgC;AAElE,EAAA,MAAA,CAAO,GAAA,CAAI,GAAA,EAAK,OAAO,GAAA,EAAK,GAAA,KAAQ;AAClC,IAAA,MAAM,MAAA,GAAS,WAAW,SAAA,CAAU;AAAA,MAClC,WAAA,EAAa,MAAM,QAAA,CAAS,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,IAAA,qBAAA,CAAsB,GAAA,CAAI,SAAA,CAAU,SAAA,EAAW,SAAS,CAAA;AAExD,IAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM;AACpB,MAAA,qBAAA,CAAsB,MAAA,CAAO,UAAU,SAAS,CAAA;AAAA,IAClD,CAAC,CAAA;AAED,IAAA,MAAM,MAAA,CAAO,QAAQ,SAAS,CAAA;AAAA,EAChC,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,IAAA,CAAK,WAAA,EAAa,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC3C,IAAA,MAAM,SAAA,GAAY,IAAI,KAAA,CAAM,SAAA;AAE5B,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,YAAY,YAAY,CAAA,CAAE,MAAM,uBAAuB,CAAA;AACvE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAA,GAAY,qBAAA,CAAsB,GAAA,CAAI,SAAS,CAAA;AACrD,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,MAAM,SAAA,CAAU,iBAAA,CAAkB,GAAA,EAAK,GAAA,EAAK,IAAI,IAAI,CAAA;AAAA,IACtD,CAAA,MAAO;AACL,MAAA,GAAA,CACG,MAAA,CAAO,GAAG,CAAA,CACV,WAAA,CAAY,YAAY,CAAA,CACxB,KAAA,CAAM,CAAA,kCAAA,EAAqC,SAAS,CAAA,CAAA,CAAG,CAAA;AAAA,IAC5D;AAAA,EACF,CAAC,CAAA;AACD,EAAA,OAAO,MAAA;AACT;;;;"}
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';\nimport { McpServerConfig } from '../config';\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 serverConfig,\n}: {\n mcpService: McpService;\n httpAuth: HttpAuthService;\n serverConfig?: McpServerConfig;\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 serverConfig,\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":";;;;;;;;;AAyBO,MAAM,kBAAkB,CAAC;AAAA,EAC9B,UAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,KAIc;AACZ,EAAA,MAAM,SAASA,8BAAA,EAAc;AAC7B,EAAA,MAAM,qBAAA,uBAA4B,GAAA,EAAgC;AAElE,EAAA,MAAA,CAAO,GAAA,CAAI,GAAA,EAAK,OAAO,GAAA,EAAK,GAAA,KAAQ;AAClC,IAAA,MAAM,MAAA,GAAS,WAAW,SAAA,CAAU;AAAA,MAClC,WAAA,EAAa,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAAA,MAC3C;AAAA,KACD,CAAA;AAED,IAAA,MAAM,YAAY,IAAIC,yBAAA;AAAA,MACpB,CAAA,EAAG,IAAI,WAAW,CAAA,SAAA,CAAA;AAAA,MAClB;AAAA,KACF;AAEA,IAAA,qBAAA,CAAsB,GAAA,CAAI,SAAA,CAAU,SAAA,EAAW,SAAS,CAAA;AAExD,IAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM;AACpB,MAAA,qBAAA,CAAsB,MAAA,CAAO,UAAU,SAAS,CAAA;AAAA,IAClD,CAAC,CAAA;AAED,IAAA,MAAM,MAAA,CAAO,QAAQ,SAAS,CAAA;AAAA,EAChC,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,IAAA,CAAK,WAAA,EAAa,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC3C,IAAA,MAAM,SAAA,GAAY,IAAI,KAAA,CAAM,SAAA;AAE5B,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,YAAY,YAAY,CAAA,CAAE,MAAM,uBAAuB,CAAA;AACvE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAA,GAAY,qBAAA,CAAsB,GAAA,CAAI,SAAS,CAAA;AACrD,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,MAAM,SAAA,CAAU,iBAAA,CAAkB,GAAA,EAAK,GAAA,EAAK,IAAI,IAAI,CAAA;AAAA,IACtD,CAAA,MAAO;AACL,MAAA,GAAA,CACG,MAAA,CAAO,GAAG,CAAA,CACV,WAAA,CAAY,YAAY,CAAA,CACxB,KAAA,CAAM,CAAA,kCAAA,EAAqC,SAAS,CAAA,CAAA,CAAG,CAAA;AAAA,IAC5D;AAAA,EACF,CAAC,CAAA;AACD,EAAA,OAAO,MAAA;AACT;;;;"}
@@ -15,7 +15,8 @@ const createStreamableRouter = ({
15
15
  mcpService,
16
16
  httpAuth,
17
17
  logger,
18
- metrics: metrics$1
18
+ metrics: metrics$1,
19
+ serverConfig
19
20
  }) => {
20
21
  const router = PromiseRouter__default.default();
21
22
  const sessionDuration = metrics$1.createHistogram(
@@ -35,7 +36,8 @@ const createStreamableRouter = ({
35
36
  };
36
37
  try {
37
38
  const server = mcpService.getServer({
38
- credentials: await httpAuth.credentials(req)
39
+ credentials: await httpAuth.credentials(req),
40
+ serverConfig
39
41
  });
40
42
  const transport = new streamableHttp_js.StreamableHTTPServerTransport({
41
43
  // stateless implementation for now, so that we can support multiple
@@ -1 +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 { performance } from 'node:perf_hooks';\nimport { McpService } from '../services/McpService';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { isError } from '@backstage/errors';\nimport { MetricsService } from '@backstage/backend-plugin-api/alpha';\nimport { bucketBoundaries, McpServerSessionAttributes } from '../metrics';\n\nexport const createStreamableRouter = ({\n mcpService,\n httpAuth,\n logger,\n metrics,\n}: {\n mcpService: McpService;\n logger: LoggerService;\n httpAuth: HttpAuthService;\n metrics: MetricsService;\n}): Router => {\n const router = PromiseRouter();\n\n const sessionDuration = metrics.createHistogram<McpServerSessionAttributes>(\n 'mcp.server.session.duration',\n {\n description:\n 'The duration of the MCP session as observed on the MCP server',\n unit: 's',\n advice: { explicitBucketBoundaries: bucketBoundaries },\n },\n );\n\n router.post('/', async (req, res) => {\n const sessionStart = performance.now();\n\n const baseAttributes: McpServerSessionAttributes = {\n 'mcp.protocol.version': LATEST_PROTOCOL_VERSION,\n 'network.transport': 'tcp',\n 'network.protocol.name': 'http',\n };\n\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 const durationSeconds = (performance.now() - sessionStart) / 1000;\n\n sessionDuration.record(durationSeconds, baseAttributes);\n });\n } catch (error) {\n const errorType = isError(error) ? error.name : 'Error';\n\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 const durationSeconds = (performance.now() - sessionStart) / 1000;\n\n sessionDuration.record(durationSeconds, {\n ...baseAttributes,\n 'error.type': errorType,\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":["metrics","PromiseRouter","bucketBoundaries","performance","LATEST_PROTOCOL_VERSION","StreamableHTTPServerTransport","isError"],"mappings":"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"}
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 { performance } from 'node:perf_hooks';\nimport { McpService } from '../services/McpService';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport { isError } from '@backstage/errors';\nimport { MetricsService } from '@backstage/backend-plugin-api/alpha';\nimport { bucketBoundaries, McpServerSessionAttributes } from '../metrics';\nimport { McpServerConfig } from '../config';\n\nexport const createStreamableRouter = ({\n mcpService,\n httpAuth,\n logger,\n metrics,\n serverConfig,\n}: {\n mcpService: McpService;\n logger: LoggerService;\n httpAuth: HttpAuthService;\n metrics: MetricsService;\n serverConfig?: McpServerConfig;\n}): Router => {\n const router = PromiseRouter();\n\n const sessionDuration = metrics.createHistogram<McpServerSessionAttributes>(\n 'mcp.server.session.duration',\n {\n description:\n 'The duration of the MCP session as observed on the MCP server',\n unit: 's',\n advice: { explicitBucketBoundaries: bucketBoundaries },\n },\n );\n\n router.post('/', async (req, res) => {\n const sessionStart = performance.now();\n\n const baseAttributes: McpServerSessionAttributes = {\n 'mcp.protocol.version': LATEST_PROTOCOL_VERSION,\n 'network.transport': 'tcp',\n 'network.protocol.name': 'http',\n };\n\n try {\n const server = mcpService.getServer({\n credentials: await httpAuth.credentials(req),\n serverConfig,\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 const durationSeconds = (performance.now() - sessionStart) / 1000;\n\n sessionDuration.record(durationSeconds, baseAttributes);\n });\n } catch (error) {\n const errorType = isError(error) ? error.name : 'Error';\n\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 const durationSeconds = (performance.now() - sessionStart) / 1000;\n\n sessionDuration.record(durationSeconds, {\n ...baseAttributes,\n 'error.type': errorType,\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":["metrics","PromiseRouter","bucketBoundaries","performance","LATEST_PROTOCOL_VERSION","StreamableHTTPServerTransport","isError"],"mappings":"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"}
@@ -10,9 +10,11 @@ var metrics = require('../metrics.cjs.js');
10
10
 
11
11
  class McpService {
12
12
  actions;
13
+ namespacedToolNames;
13
14
  operationDuration;
14
- constructor(actions, metrics$1) {
15
+ constructor(actions, metrics$1, namespacedToolNames) {
15
16
  this.actions = actions;
17
+ this.namespacedToolNames = namespacedToolNames ?? true;
16
18
  this.operationDuration = metrics$1.createHistogram(
17
19
  "mcp.server.operation.duration",
18
20
  {
@@ -24,16 +26,23 @@ class McpService {
24
26
  }
25
27
  static async create({
26
28
  actions,
27
- metrics
29
+ metrics,
30
+ namespacedToolNames
28
31
  }) {
29
- return new McpService(actions, metrics);
32
+ return new McpService(actions, metrics, namespacedToolNames);
30
33
  }
31
- getServer({ credentials }) {
34
+ getServer({
35
+ credentials,
36
+ serverConfig
37
+ }) {
32
38
  const server = new index_js.Server(
33
39
  {
34
- name: "backstage",
40
+ name: serverConfig?.name ?? "backstage",
35
41
  // TODO: this version will most likely change in the future.
36
- version: package_json.version
42
+ version: package_json.version,
43
+ ...serverConfig?.description && {
44
+ description: serverConfig.description
45
+ }
37
46
  },
38
47
  { capabilities: { tools: {} } }
39
48
  );
@@ -41,14 +50,17 @@ class McpService {
41
50
  const startTime = node_perf_hooks.performance.now();
42
51
  let errorType;
43
52
  try {
44
- const { actions } = await this.actions.list({ credentials });
53
+ const { actions: allActions } = await this.actions.list({
54
+ credentials
55
+ });
56
+ const actions = serverConfig ? this.filterActions(allActions, serverConfig) : allActions;
45
57
  return {
46
58
  tools: actions.map((action) => ({
47
59
  inputSchema: action.schema.input,
48
60
  // todo(blam): this is unfortunately not supported by most clients yet.
49
61
  // When this is provided you need to provide structuredContent instead.
50
62
  // outputSchema: action.schema.output,
51
- name: action.name,
63
+ name: this.getToolName(action),
52
64
  description: action.description,
53
65
  annotations: {
54
66
  title: action.title,
@@ -76,8 +88,11 @@ class McpService {
76
88
  let isError = false;
77
89
  try {
78
90
  const result = await handleErrors.handleErrors(async () => {
79
- const { actions } = await this.actions.list({ credentials });
80
- const action = actions.find((a) => a.name === params.name);
91
+ const { actions: allActions } = await this.actions.list({
92
+ credentials
93
+ });
94
+ const actions = serverConfig ? this.filterActions(allActions, serverConfig) : allActions;
95
+ const action = actions.find((a) => this.getToolName(a) === params.name);
81
96
  if (!action) {
82
97
  throw new errors.NotFoundError(`Action "${params.name}" not found`);
83
98
  }
@@ -121,6 +136,40 @@ class McpService {
121
136
  });
122
137
  return server;
123
138
  }
139
+ filterActions(actions, serverConfig) {
140
+ const { includeRules, excludeRules } = serverConfig;
141
+ if (includeRules.length === 0 && excludeRules.length === 0) {
142
+ return actions;
143
+ }
144
+ return actions.filter((action) => {
145
+ if (excludeRules.some((rule) => this.matchesRule(action, rule))) {
146
+ return false;
147
+ }
148
+ if (includeRules.length === 0) {
149
+ return true;
150
+ }
151
+ return includeRules.some((rule) => this.matchesRule(action, rule));
152
+ });
153
+ }
154
+ getToolName(action) {
155
+ if (this.namespacedToolNames) {
156
+ return `${action.pluginId}.${action.name}`;
157
+ }
158
+ return action.name;
159
+ }
160
+ matchesRule(action, rule) {
161
+ if (rule.idMatcher && !rule.idMatcher.match(action.id)) {
162
+ return false;
163
+ }
164
+ if (rule.attributes) {
165
+ for (const [key, value] of Object.entries(rule.attributes)) {
166
+ if (action.attributes[key] !== value) {
167
+ return false;
168
+ }
169
+ }
170
+ }
171
+ return true;
172
+ }
124
173
  }
125
174
 
126
175
  exports.McpService = McpService;
@@ -1 +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 {\n ActionsService,\n MetricsServiceHistogram,\n MetricsService,\n} from '@backstage/backend-plugin-api/alpha';\nimport { version } from '@backstage/plugin-mcp-actions-backend/package.json';\nimport { NotFoundError } from '@backstage/errors';\nimport { performance } from 'node:perf_hooks';\n\nimport { handleErrors } from './handleErrors';\nimport { bucketBoundaries, McpServerOperationAttributes } from '../metrics';\n\nexport class McpService {\n private readonly actions: ActionsService;\n private readonly operationDuration: MetricsServiceHistogram<McpServerOperationAttributes>;\n\n constructor(actions: ActionsService, metrics: MetricsService) {\n this.actions = actions;\n this.operationDuration =\n metrics.createHistogram<McpServerOperationAttributes>(\n 'mcp.server.operation.duration',\n {\n description: 'MCP request duration as observed on the receiver',\n unit: 's',\n advice: { explicitBucketBoundaries: bucketBoundaries },\n },\n );\n }\n\n static async create({\n actions,\n metrics,\n }: {\n actions: ActionsService;\n metrics: MetricsService;\n }) {\n return new McpService(actions, metrics);\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 const startTime = performance.now();\n let errorType: string | undefined;\n\n try {\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 } catch (err) {\n errorType = err instanceof Error ? err.name : 'Error';\n throw err;\n } finally {\n const durationSeconds = (performance.now() - startTime) / 1000;\n\n this.operationDuration.record(durationSeconds, {\n 'mcp.method.name': 'tools/list',\n ...(errorType && { 'error.type': errorType }),\n });\n }\n });\n\n server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {\n const startTime = performance.now();\n let errorType: string | undefined;\n let isError = false;\n\n try {\n const result = await handleErrors(async () => {\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 isError = !!(result as { isError?: boolean })?.isError;\n return result;\n } catch (err) {\n errorType = err instanceof Error ? err.name : 'Error';\n throw err;\n } finally {\n const durationSeconds = (performance.now() - startTime) / 1000;\n\n // Determine error.type per OTel MCP spec:\n // - Thrown exceptions use the error name\n // - CallToolResult with isError=true uses 'tool_error'\n let errorAttribute: string | undefined = errorType;\n if (!errorAttribute && isError) {\n errorAttribute = 'tool_error';\n }\n\n this.operationDuration.record(durationSeconds, {\n 'mcp.method.name': 'tools/call',\n 'gen_ai.tool.name': params.name,\n 'gen_ai.operation.name': 'execute_tool',\n ...(errorAttribute && { 'error.type': errorAttribute }),\n });\n }\n });\n\n return server;\n }\n}\n"],"names":["metrics","bucketBoundaries","McpServer","version","ListToolsRequestSchema","performance","CallToolRequestSchema","handleErrors","NotFoundError"],"mappings":"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"}
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 {\n ActionsService,\n ActionsServiceAction,\n MetricsServiceHistogram,\n MetricsService,\n} from '@backstage/backend-plugin-api/alpha';\nimport { version } from '@backstage/plugin-mcp-actions-backend/package.json';\nimport { NotFoundError } from '@backstage/errors';\nimport { performance } from 'node:perf_hooks';\n\nimport { handleErrors } from './handleErrors';\nimport { bucketBoundaries, McpServerOperationAttributes } from '../metrics';\nimport { FilterRule, McpServerConfig } from '../config';\n\nexport class McpService {\n private readonly actions: ActionsService;\n private readonly namespacedToolNames: boolean;\n private readonly operationDuration: MetricsServiceHistogram<McpServerOperationAttributes>;\n\n constructor(\n actions: ActionsService,\n metrics: MetricsService,\n namespacedToolNames?: boolean,\n ) {\n this.actions = actions;\n this.namespacedToolNames = namespacedToolNames ?? true;\n this.operationDuration =\n metrics.createHistogram<McpServerOperationAttributes>(\n 'mcp.server.operation.duration',\n {\n description: 'MCP request duration as observed on the receiver',\n unit: 's',\n advice: { explicitBucketBoundaries: bucketBoundaries },\n },\n );\n }\n\n static async create({\n actions,\n metrics,\n namespacedToolNames,\n }: {\n actions: ActionsService;\n metrics: MetricsService;\n namespacedToolNames?: boolean;\n }) {\n return new McpService(actions, metrics, namespacedToolNames);\n }\n\n getServer({\n credentials,\n serverConfig,\n }: {\n credentials: BackstageCredentials;\n serverConfig?: McpServerConfig;\n }) {\n const server = new McpServer(\n {\n name: serverConfig?.name ?? 'backstage',\n // TODO: this version will most likely change in the future.\n version,\n ...(serverConfig?.description && {\n description: serverConfig.description,\n }),\n },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const startTime = performance.now();\n let errorType: string | undefined;\n\n try {\n const { actions: allActions } = await this.actions.list({\n credentials,\n });\n const actions = serverConfig\n ? this.filterActions(allActions, serverConfig)\n : allActions;\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: this.getToolName(action),\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 } catch (err) {\n errorType = err instanceof Error ? err.name : 'Error';\n throw err;\n } finally {\n const durationSeconds = (performance.now() - startTime) / 1000;\n\n this.operationDuration.record(durationSeconds, {\n 'mcp.method.name': 'tools/list',\n ...(errorType && { 'error.type': errorType }),\n });\n }\n });\n\n server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {\n const startTime = performance.now();\n let errorType: string | undefined;\n let isError = false;\n\n try {\n const result = await handleErrors(async () => {\n const { actions: allActions } = await this.actions.list({\n credentials,\n });\n const actions = serverConfig\n ? this.filterActions(allActions, serverConfig)\n : allActions;\n\n const action = actions.find(a => this.getToolName(a) === 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 isError = !!(result as { isError?: boolean })?.isError;\n return result;\n } catch (err) {\n errorType = err instanceof Error ? err.name : 'Error';\n throw err;\n } finally {\n const durationSeconds = (performance.now() - startTime) / 1000;\n\n // Determine error.type per OTel MCP spec:\n // - Thrown exceptions use the error name\n // - CallToolResult with isError=true uses 'tool_error'\n let errorAttribute: string | undefined = errorType;\n if (!errorAttribute && isError) {\n errorAttribute = 'tool_error';\n }\n\n this.operationDuration.record(durationSeconds, {\n 'mcp.method.name': 'tools/call',\n 'gen_ai.tool.name': params.name,\n 'gen_ai.operation.name': 'execute_tool',\n ...(errorAttribute && { 'error.type': errorAttribute }),\n });\n }\n });\n\n return server;\n }\n\n private filterActions(\n actions: ActionsServiceAction[],\n serverConfig: McpServerConfig,\n ): ActionsServiceAction[] {\n const { includeRules, excludeRules } = serverConfig;\n if (includeRules.length === 0 && excludeRules.length === 0) {\n return actions;\n }\n\n return actions.filter(action => {\n if (excludeRules.some(rule => this.matchesRule(action, rule))) {\n return false;\n }\n\n if (includeRules.length === 0) {\n return true;\n }\n\n return includeRules.some(rule => this.matchesRule(action, rule));\n });\n }\n\n private getToolName(action: ActionsServiceAction): string {\n if (this.namespacedToolNames) {\n return `${action.pluginId}.${action.name}`;\n }\n return action.name;\n }\n\n private matchesRule(action: ActionsServiceAction, rule: FilterRule): boolean {\n if (rule.idMatcher && !rule.idMatcher.match(action.id)) {\n return false;\n }\n\n if (rule.attributes) {\n for (const [key, value] of Object.entries(rule.attributes)) {\n if (\n action.attributes[\n key as 'destructive' | 'readOnly' | 'idempotent'\n ] !== value\n ) {\n return false;\n }\n }\n }\n\n return true;\n }\n}\n"],"names":["metrics","bucketBoundaries","McpServer","version","ListToolsRequestSchema","performance","CallToolRequestSchema","handleErrors","NotFoundError"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-mcp-actions-backend",
3
- "version": "0.1.10-next.1",
3
+ "version": "0.1.10",
4
4
  "backstage": {
5
5
  "role": "backend-plugin",
6
6
  "pluginId": "mcp-actions",
@@ -24,8 +24,10 @@
24
24
  "license": "Apache-2.0",
25
25
  "main": "dist/index.cjs.js",
26
26
  "types": "dist/index.d.ts",
27
+ "configSchema": "config.d.ts",
27
28
  "files": [
28
- "dist"
29
+ "dist",
30
+ "config.d.ts"
29
31
  ],
30
32
  "scripts": {
31
33
  "build": "backstage-cli package build",
@@ -37,21 +39,23 @@
37
39
  "test": "backstage-cli package test"
38
40
  },
39
41
  "dependencies": {
40
- "@backstage/backend-plugin-api": "1.7.1-next.0",
41
- "@backstage/catalog-client": "1.14.0-next.1",
42
- "@backstage/errors": "1.2.7",
43
- "@backstage/plugin-catalog-node": "2.1.0-next.1",
44
- "@backstage/types": "1.2.2",
42
+ "@backstage/backend-plugin-api": "^1.8.0",
43
+ "@backstage/catalog-client": "^1.14.0",
44
+ "@backstage/config": "^1.3.6",
45
+ "@backstage/errors": "^1.2.7",
46
+ "@backstage/plugin-catalog-node": "^2.1.0",
47
+ "@backstage/types": "^1.2.2",
45
48
  "@cfworker/json-schema": "^4.1.1",
46
49
  "@modelcontextprotocol/sdk": "^1.25.2",
47
50
  "express": "^4.22.0",
48
51
  "express-promise-router": "^4.1.0",
49
- "zod": "^3.25.76"
52
+ "minimatch": "^10.2.1",
53
+ "zod": "^3.25.76 || ^4.0.0"
50
54
  },
51
55
  "devDependencies": {
52
- "@backstage/backend-defaults": "0.16.0-next.1",
53
- "@backstage/backend-test-utils": "1.11.1-next.1",
54
- "@backstage/cli": "0.36.0-next.1",
56
+ "@backstage/backend-defaults": "^0.16.0",
57
+ "@backstage/backend-test-utils": "^1.11.1",
58
+ "@backstage/cli": "^0.36.0",
55
59
  "@types/express": "^4.17.6",
56
60
  "@types/supertest": "^2.0.8",
57
61
  "supertest": "^7.0.0"