@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 +28 -0
- package/README.md +86 -3
- package/config.d.ts +75 -0
- package/dist/config.cjs.js +58 -0
- package/dist/config.cjs.js.map +1 -0
- package/dist/plugin.cjs.js +43 -16
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/routers/createSseRouter.cjs.js +4 -2
- package/dist/routers/createSseRouter.cjs.js.map +1 -1
- package/dist/routers/createStreamableRouter.cjs.js +4 -2
- package/dist/routers/createStreamableRouter.cjs.js.map +1 -1
- package/dist/services/McpService.cjs.js +59 -10
- package/dist/services/McpService.cjs.js.map +1 -1
- package/package.json +15 -11
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;;;;;"}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
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) {
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -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
|
|
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":";;;;;;;;;;;;;;;AAoCO,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,gBACAC,QAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAM,aAAA,GAAgBC,0BAAmBD,QAAM,CAAA;AAC/C,QAAA,MAAM,sBAAsBA,QAAA,CAAO,kBAAA;AAAA,UACjC;AAAA,SACF;AAEA,QAAA,MAAM,UAAA,GAAa,MAAME,qBAAA,CAAW,MAAA,CAAO;AAAA,UACzC,OAAA;AAAA,UACA,OAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,MAAM,SAASC,8BAAA,EAAO;AACtB,QAAA,MAAA,CAAO,GAAA,CAAIC,cAAM,CAAA;AAEjB,QAAA,IAAI,aAAA,IAAiB,aAAA,CAAc,IAAA,GAAO,CAAA,EAAG;AAC3C,UAAA,KAAA,MAAW,CAAC,GAAA,EAAK,YAAY,CAAA,IAAK,aAAA,EAAe;AAC/C,YAAA,MAAM,mBAAmBC,6CAAA,CAAuB;AAAA,cAC9C,UAAA;AAAA,cACA,QAAA;AAAA,cACA,MAAA;AAAA,cACA,OAAA;AAAA,cACA;AAAA,aACD,CAAA;AAED,YAAA,MAAA,CAAO,GAAA,CAAI,CAAA,IAAA,EAAO,GAAG,CAAA,CAAA,EAAI,gBAAgB,CAAA;AAAA,UAC3C;AAAA,QACF,CAAA,MAAO;AACL,UAAA,MAAM,YAAA,GAAe;AAAA,YACnB,IAAA,EAAML,QAAA,CAAO,iBAAA,CAAkB,iBAAiB,CAAA,IAAK,WAAA;AAAA,YACrD,WAAA,EAAaA,QAAA,CAAO,iBAAA,CAAkB,wBAAwB,CAAA;AAAA,YAC9D,cAAc,EAAC;AAAA,YACf,cAAc;AAAC,WACjB;AAEA,UAAA,MAAM,YAAYM,+BAAA,CAAgB;AAAA,YAChC,UAAA;AAAA,YACA,QAAA;AAAA,YACA;AAAA,WACD,CAAA;AAED,UAAA,MAAM,mBAAmBD,6CAAA,CAAuB;AAAA,YAC9C,UAAA;AAAA,YACA,QAAA;AAAA,YACA,MAAA;AAAA,YACA,OAAA;AAAA,YACA;AAAA,WACD,CAAA;AAED,UAAA,MAAA,CAAO,GAAA,CAAI,WAAW,SAAS,CAAA;AAC/B,UAAA,MAAA,CAAO,GAAA,CAAI,OAAO,gBAAgB,CAAA;AAAA,QACpC;AAEA,QAAA,UAAA,CAAW,IAAI,MAAM,CAAA;AAErB,QAAA,MAAM,eACJL,QAAA,CAAO,kBAAA;AAAA,UACL;AAAA,aAEFA,QAAA,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;;;;"}
|
|
@@ -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":";;;;;;;;;
|
|
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":";;;;;;;;;;;;;
|
|
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({
|
|
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({
|
|
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
|
|
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({
|
|
80
|
-
|
|
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":";;;;;;;;;;AAoCO,MAAM,UAAA,CAAW;AAAA,EACL,OAAA;AAAA,EACA,mBAAA;AAAA,EACA,iBAAA;AAAA,EAEjB,WAAA,CACE,OAAA,EACAA,SAAA,EACA,mBAAA,EACA;AACA,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAA,CAAK,sBAAsB,mBAAA,IAAuB,IAAA;AAClD,IAAA,IAAA,CAAK,oBACHA,SAAA,CAAQ,eAAA;AAAA,MACN,+BAAA;AAAA,MACA;AAAA,QACE,WAAA,EAAa,kDAAA;AAAA,QACb,IAAA,EAAM,GAAA;AAAA,QACN,MAAA,EAAQ,EAAE,wBAAA,EAA0BC,wBAAA;AAAiB;AACvD,KACF;AAAA,EACJ;AAAA,EAEA,aAAa,MAAA,CAAO;AAAA,IAClB,OAAA;AAAA,IACA,OAAA;AAAA,IACA;AAAA,GACF,EAIG;AACD,IAAA,OAAO,IAAI,UAAA,CAAW,OAAA,EAAS,OAAA,EAAS,mBAAmB,CAAA;AAAA,EAC7D;AAAA,EAEA,SAAA,CAAU;AAAA,IACR,WAAA;AAAA,IACA;AAAA,GACF,EAGG;AACD,IAAA,MAAM,SAAS,IAAIC,eAAA;AAAA,MACjB;AAAA,QACE,IAAA,EAAM,cAAc,IAAA,IAAQ,WAAA;AAAA;AAAA,iBAE5BC,oBAAA;AAAA,QACA,GAAI,cAAc,WAAA,IAAe;AAAA,UAC/B,aAAa,YAAA,CAAa;AAAA;AAC5B,OACF;AAAA,MACA,EAAE,YAAA,EAAc,EAAE,KAAA,EAAO,IAAG;AAAE,KAChC;AAEA,IAAA,MAAA,CAAO,iBAAA,CAAkBC,iCAAwB,YAAY;AAC3D,MAAA,MAAM,SAAA,GAAYC,4BAAY,GAAA,EAAI;AAClC,MAAA,IAAI,SAAA;AAEJ,MAAA,IAAI;AACF,QAAA,MAAM,EAAE,OAAA,EAAS,UAAA,KAAe,MAAM,IAAA,CAAK,QAAQ,IAAA,CAAK;AAAA,UACtD;AAAA,SACD,CAAA;AACD,QAAA,MAAM,UAAU,YAAA,GACZ,IAAA,CAAK,aAAA,CAAc,UAAA,EAAY,YAAY,CAAA,GAC3C,UAAA;AAEJ,QAAA,OAAO;AAAA,UACL,KAAA,EAAO,OAAA,CAAQ,GAAA,CAAI,CAAA,MAAA,MAAW;AAAA,YAC5B,WAAA,EAAa,OAAO,MAAA,CAAO,KAAA;AAAA;AAAA;AAAA;AAAA,YAI3B,IAAA,EAAM,IAAA,CAAK,WAAA,CAAY,MAAM,CAAA;AAAA,YAC7B,aAAa,MAAA,CAAO,WAAA;AAAA,YACpB,WAAA,EAAa;AAAA,cACX,OAAO,MAAA,CAAO,KAAA;AAAA,cACd,eAAA,EAAiB,OAAO,UAAA,CAAW,WAAA;AAAA,cACnC,cAAA,EAAgB,OAAO,UAAA,CAAW,UAAA;AAAA,cAClC,YAAA,EAAc,OAAO,UAAA,CAAW,QAAA;AAAA,cAChC,aAAA,EAAe;AAAA;AACjB,WACF,CAAE;AAAA,SACJ;AAAA,MACF,SAAS,GAAA,EAAK;AACZ,QAAA,SAAA,GAAY,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,IAAA,GAAO,OAAA;AAC9C,QAAA,MAAM,GAAA;AAAA,MACR,CAAA,SAAE;AACA,QAAA,MAAM,eAAA,GAAA,CAAmBA,2BAAA,CAAY,GAAA,EAAI,GAAI,SAAA,IAAa,GAAA;AAE1D,QAAA,IAAA,CAAK,iBAAA,CAAkB,OAAO,eAAA,EAAiB;AAAA,UAC7C,iBAAA,EAAmB,YAAA;AAAA,UACnB,GAAI,SAAA,IAAa,EAAE,YAAA,EAAc,SAAA;AAAU,SAC5C,CAAA;AAAA,MACH;AAAA,IACF,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,iBAAA,CAAkBC,8BAAA,EAAuB,OAAO,EAAE,QAAO,KAAM;AACpE,MAAA,MAAM,SAAA,GAAYD,4BAAY,GAAA,EAAI;AAClC,MAAA,IAAI,SAAA;AACJ,MAAA,IAAI,OAAA,GAAU,KAAA;AAEd,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,GAAS,MAAME,yBAAA,CAAa,YAAY;AAC5C,UAAA,MAAM,EAAE,OAAA,EAAS,UAAA,KAAe,MAAM,IAAA,CAAK,QAAQ,IAAA,CAAK;AAAA,YACtD;AAAA,WACD,CAAA;AACD,UAAA,MAAM,UAAU,YAAA,GACZ,IAAA,CAAK,aAAA,CAAc,UAAA,EAAY,YAAY,CAAA,GAC3C,UAAA;AAEJ,UAAA,MAAM,MAAA,GAAS,QAAQ,IAAA,CAAK,CAAA,CAAA,KAAK,KAAK,WAAA,CAAY,CAAC,CAAA,KAAM,MAAA,CAAO,IAAI,CAAA;AAEpE,UAAA,IAAI,CAAC,MAAA,EAAQ;AACX,YAAA,MAAM,IAAIC,oBAAA,CAAc,CAAA,QAAA,EAAW,MAAA,CAAO,IAAI,CAAA,WAAA,CAAa,CAAA;AAAA,UAC7D;AAEA,UAAA,MAAM,EAAE,MAAA,EAAO,GAAI,MAAM,IAAA,CAAK,QAAQ,MAAA,CAAO;AAAA,YAC3C,IAAI,MAAA,CAAO,EAAA;AAAA,YACX,OAAO,MAAA,CAAO,SAAA;AAAA,YACd;AAAA,WACD,CAAA;AAED,UAAA,OAAO;AAAA;AAAA;AAAA;AAAA,YAIL,OAAA,EAAS;AAAA,cACP;AAAA,gBACE,IAAA,EAAM,MAAA;AAAA,gBACN,IAAA,EAAM,CAAC,SAAA,EAAW,IAAA,CAAK,SAAA,CAAU,QAAQ,IAAA,EAAM,CAAC,CAAA,EAAG,KAAK,CAAA,CAAE,IAAA;AAAA,kBACxD;AAAA;AACF;AACF;AACF,WACF;AAAA,QACF,CAAC,CAAA;AAED,QAAA,OAAA,GAAU,CAAC,CAAE,MAAA,EAAkC,OAAA;AAC/C,QAAA,OAAO,MAAA;AAAA,MACT,SAAS,GAAA,EAAK;AACZ,QAAA,SAAA,GAAY,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,IAAA,GAAO,OAAA;AAC9C,QAAA,MAAM,GAAA;AAAA,MACR,CAAA,SAAE;AACA,QAAA,MAAM,eAAA,GAAA,CAAmBH,2BAAA,CAAY,GAAA,EAAI,GAAI,SAAA,IAAa,GAAA;AAK1D,QAAA,IAAI,cAAA,GAAqC,SAAA;AACzC,QAAA,IAAI,CAAC,kBAAkB,OAAA,EAAS;AAC9B,UAAA,cAAA,GAAiB,YAAA;AAAA,QACnB;AAEA,QAAA,IAAA,CAAK,iBAAA,CAAkB,OAAO,eAAA,EAAiB;AAAA,UAC7C,iBAAA,EAAmB,YAAA;AAAA,UACnB,oBAAoB,MAAA,CAAO,IAAA;AAAA,UAC3B,uBAAA,EAAyB,cAAA;AAAA,UACzB,GAAI,cAAA,IAAkB,EAAE,YAAA,EAAc,cAAA;AAAe,SACtD,CAAA;AAAA,MACH;AAAA,IACF,CAAC,CAAA;AAED,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEQ,aAAA,CACN,SACA,YAAA,EACwB;AACxB,IAAA,MAAM,EAAE,YAAA,EAAc,YAAA,EAAa,GAAI,YAAA;AACvC,IAAA,IAAI,YAAA,CAAa,MAAA,KAAW,CAAA,IAAK,YAAA,CAAa,WAAW,CAAA,EAAG;AAC1D,MAAA,OAAO,OAAA;AAAA,IACT;AAEA,IAAA,OAAO,OAAA,CAAQ,OAAO,CAAA,MAAA,KAAU;AAC9B,MAAA,IAAI,YAAA,CAAa,KAAK,CAAA,IAAA,KAAQ,IAAA,CAAK,YAAY,MAAA,EAAQ,IAAI,CAAC,CAAA,EAAG;AAC7D,QAAA,OAAO,KAAA;AAAA,MACT;AAEA,MAAA,IAAI,YAAA,CAAa,WAAW,CAAA,EAAG;AAC7B,QAAA,OAAO,IAAA;AAAA,MACT;AAEA,MAAA,OAAO,aAAa,IAAA,CAAK,CAAA,IAAA,KAAQ,KAAK,WAAA,CAAY,MAAA,EAAQ,IAAI,CAAC,CAAA;AAAA,IACjE,CAAC,CAAA;AAAA,EACH;AAAA,EAEQ,YAAY,MAAA,EAAsC;AACxD,IAAA,IAAI,KAAK,mBAAA,EAAqB;AAC5B,MAAA,OAAO,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,CAAA,EAAI,OAAO,IAAI,CAAA,CAAA;AAAA,IAC1C;AACA,IAAA,OAAO,MAAA,CAAO,IAAA;AAAA,EAChB;AAAA,EAEQ,WAAA,CAAY,QAA8B,IAAA,EAA2B;AAC3E,IAAA,IAAI,IAAA,CAAK,aAAa,CAAC,IAAA,CAAK,UAAU,KAAA,CAAM,MAAA,CAAO,EAAE,CAAA,EAAG;AACtD,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,KAAK,UAAA,EAAY;AACnB,MAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,IAAA,CAAK,UAAU,CAAA,EAAG;AAC1D,QAAA,IACE,MAAA,CAAO,UAAA,CACL,GACF,CAAA,KAAM,KAAA,EACN;AACA,UAAA,OAAO,KAAA;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-mcp-actions-backend",
|
|
3
|
-
"version": "0.1.10
|
|
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.
|
|
41
|
-
"@backstage/catalog-client": "1.14.0
|
|
42
|
-
"@backstage/
|
|
43
|
-
"@backstage/
|
|
44
|
-
"@backstage/
|
|
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
|
-
"
|
|
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
|
|
53
|
-
"@backstage/backend-test-utils": "1.11.1
|
|
54
|
-
"@backstage/cli": "0.36.0
|
|
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"
|