@backstage/plugin-mcp-actions-backend 0.1.13-next.0 → 0.1.13
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 +13 -0
- package/README.md +4 -0
- package/config.d.ts +15 -0
- package/dist/plugin.cjs.js +11 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/routers/createSseRouter.cjs.js +9 -1
- package/dist/routers/createSseRouter.cjs.js.map +1 -1
- package/dist/routers/createStreamableRouter.cjs.js +9 -1
- package/dist/routers/createStreamableRouter.cjs.js.map +1 -1
- package/dist/services/McpService.cjs.js +97 -36
- package/dist/services/McpService.cjs.js.map +1 -1
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @backstage/plugin-mcp-actions-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ca8951a: Fixed an issue where actions returned Markdown-formatted JSON instead of plain JSON and a `structuredContent` field for model context protocol responses.
|
|
8
|
+
- 8916f83: Trace spans are now emitted for MCP `tools/call` invocations, following OpenTelemetry server-side MCP semantic conventions.
|
|
9
|
+
- Updated dependencies
|
|
10
|
+
- @backstage/errors@1.3.1
|
|
11
|
+
- @backstage/backend-plugin-api@1.9.1
|
|
12
|
+
- @backstage/plugin-catalog-node@2.2.1
|
|
13
|
+
- @backstage/catalog-client@1.15.1
|
|
14
|
+
- @backstage/config@1.3.8
|
|
15
|
+
|
|
3
16
|
## 0.1.13-next.0
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -135,6 +135,10 @@ When errors are thrown from MCP actions, the backend will handle and surface err
|
|
|
135
135
|
|
|
136
136
|
See [Backstage Errors](https://backstage.io/docs/reference/errors/) for a full list of supported errors.
|
|
137
137
|
|
|
138
|
+
### Response Format
|
|
139
|
+
|
|
140
|
+
Tool execution results are returned in a format compliant with the MCP specification, including both a plain-text representation in the `content` array and the raw JSON result in the `structuredContent` field. This ensures that AI clients can process the data either as text or as structured data for more precise tool use.
|
|
141
|
+
|
|
138
142
|
When writing MCP tools, use the appropriate error from `@backstage/errors` when applicable:
|
|
139
143
|
|
|
140
144
|
```ts
|
package/config.d.ts
CHANGED
|
@@ -36,6 +36,21 @@ export interface Config {
|
|
|
36
36
|
*/
|
|
37
37
|
namespacedToolNames?: boolean;
|
|
38
38
|
|
|
39
|
+
tracing?: {
|
|
40
|
+
capture?: {
|
|
41
|
+
/**
|
|
42
|
+
* When true, the MCP tool call's input arguments and output result
|
|
43
|
+
* are included on the MCP `tools/call` server span as
|
|
44
|
+
* `gen_ai.tool.call.arguments` and `gen_ai.tool.call.result`.
|
|
45
|
+
* These attributes are marked Opt-In by the OpenTelemetry GenAI
|
|
46
|
+
* semantic conventions because they may contain sensitive
|
|
47
|
+
* information (entity payloads, scaffolder inputs, free-form
|
|
48
|
+
* text). Defaults to false.
|
|
49
|
+
*/
|
|
50
|
+
toolPayload?: boolean;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
39
54
|
/**
|
|
40
55
|
* Named MCP servers, each exposed at /api/mcp-actions/v1/{key}.
|
|
41
56
|
* When not configured, the plugin serves a single server at /api/mcp-actions/v1.
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -27,7 +27,8 @@ const mcpPlugin = backendPluginApi.createBackendPlugin({
|
|
|
27
27
|
rootRouter: backendPluginApi.coreServices.rootHttpRouter,
|
|
28
28
|
discovery: backendPluginApi.coreServices.discovery,
|
|
29
29
|
config: backendPluginApi.coreServices.rootConfig,
|
|
30
|
-
metrics: alpha.metricsServiceRef
|
|
30
|
+
metrics: alpha.metricsServiceRef,
|
|
31
|
+
tracing: alpha.tracingServiceRef
|
|
31
32
|
},
|
|
32
33
|
async init({
|
|
33
34
|
actions,
|
|
@@ -37,16 +38,20 @@ const mcpPlugin = backendPluginApi.createBackendPlugin({
|
|
|
37
38
|
rootRouter,
|
|
38
39
|
discovery,
|
|
39
40
|
config: config$1,
|
|
40
|
-
metrics
|
|
41
|
+
metrics,
|
|
42
|
+
tracing
|
|
41
43
|
}) {
|
|
42
44
|
const serverConfigs = config.parseServerConfigs(config$1);
|
|
43
45
|
const namespacedToolNames = config$1.getOptionalBoolean(
|
|
44
46
|
"mcpActions.namespacedToolNames"
|
|
45
47
|
);
|
|
48
|
+
const captureToolPayloads = config$1.getOptionalBoolean("mcpActions.tracing.capture.toolPayload") ?? false;
|
|
46
49
|
const mcpService = await McpService.McpService.create({
|
|
47
50
|
actions,
|
|
48
51
|
metrics,
|
|
49
|
-
namespacedToolNames
|
|
52
|
+
namespacedToolNames,
|
|
53
|
+
tracingService: tracing,
|
|
54
|
+
captureToolPayloads
|
|
50
55
|
});
|
|
51
56
|
const router = PromiseRouter__default.default();
|
|
52
57
|
router.use(express.json());
|
|
@@ -57,6 +62,7 @@ const mcpPlugin = backendPluginApi.createBackendPlugin({
|
|
|
57
62
|
httpAuth,
|
|
58
63
|
logger,
|
|
59
64
|
metrics,
|
|
65
|
+
tracing,
|
|
60
66
|
serverConfig
|
|
61
67
|
});
|
|
62
68
|
router.use(`/v1/${key}`, streamableRouter);
|
|
@@ -71,6 +77,7 @@ const mcpPlugin = backendPluginApi.createBackendPlugin({
|
|
|
71
77
|
const sseRouter = createSseRouter.createSseRouter({
|
|
72
78
|
mcpService,
|
|
73
79
|
httpAuth,
|
|
80
|
+
tracing,
|
|
74
81
|
serverConfig
|
|
75
82
|
});
|
|
76
83
|
const streamableRouter = createStreamableRouter.createStreamableRouter({
|
|
@@ -78,6 +85,7 @@ const mcpPlugin = backendPluginApi.createBackendPlugin({
|
|
|
78
85
|
httpAuth,
|
|
79
86
|
logger,
|
|
80
87
|
metrics,
|
|
88
|
+
tracing,
|
|
81
89
|
serverConfig
|
|
82
90
|
});
|
|
83
91
|
router.use("/v1/sse", sseRouter);
|
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';\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 const serverSuffixes = serverConfigs?.size\n ? [...serverConfigs.keys()].map(key => `/v1/${key}`)\n : ['/v1'];\n\n for (const suffix of serverSuffixes) {\n const mcpBasePath = `/api/mcp-actions${suffix}`;\n\n rootRouter.use(\n `/.well-known/oauth-protected-resource${mcpBasePath}`,\n async (_req, res) => {\n const [authBaseUrl, mcpBaseUrl] = await Promise.all([\n discovery.getExternalBaseUrl('auth'),\n discovery.getExternalBaseUrl('mcp-actions'),\n ]);\n\n res.json({\n resource: `${mcpBaseUrl}${suffix}`,\n authorization_servers: [authBaseUrl],\n });\n },\n );\n }\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","actionsServiceRef","actionsRegistryServiceRef","metricsServiceRef","config","parseServerConfigs","McpService","Router","json","createStreamableRouter","createSseRouter"],"mappings":";;;;;;;;;;;;;;;
|
|
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 tracingServiceRef,\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 tracing: tracingServiceRef,\n },\n async init({\n actions,\n logger,\n httpRouter,\n httpAuth,\n rootRouter,\n discovery,\n config,\n metrics,\n tracing,\n }) {\n const serverConfigs = parseServerConfigs(config);\n const namespacedToolNames = config.getOptionalBoolean(\n 'mcpActions.namespacedToolNames',\n );\n const captureToolPayloads =\n config.getOptionalBoolean('mcpActions.tracing.capture.toolPayload') ??\n false;\n\n const mcpService = await McpService.create({\n actions,\n metrics,\n namespacedToolNames,\n tracingService: tracing,\n captureToolPayloads,\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 tracing,\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 tracing,\n serverConfig,\n });\n\n const streamableRouter = createStreamableRouter({\n mcpService,\n httpAuth,\n logger,\n metrics,\n tracing,\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 const serverSuffixes = serverConfigs?.size\n ? [...serverConfigs.keys()].map(key => `/v1/${key}`)\n : ['/v1'];\n\n for (const suffix of serverSuffixes) {\n const mcpBasePath = `/api/mcp-actions${suffix}`;\n\n rootRouter.use(\n `/.well-known/oauth-protected-resource${mcpBasePath}`,\n async (_req, res) => {\n const [authBaseUrl, mcpBaseUrl] = await Promise.all([\n discovery.getExternalBaseUrl('auth'),\n discovery.getExternalBaseUrl('mcp-actions'),\n ]);\n\n res.json({\n resource: `${mcpBaseUrl}${suffix}`,\n authorization_servers: [authBaseUrl],\n });\n },\n );\n }\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","actionsServiceRef","actionsRegistryServiceRef","metricsServiceRef","tracingServiceRef","config","parseServerConfigs","McpService","Router","json","createStreamableRouter","createSseRouter"],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
|
|
|
10
10
|
const createSseRouter = ({
|
|
11
11
|
mcpService,
|
|
12
12
|
httpAuth,
|
|
13
|
+
tracing,
|
|
13
14
|
serverConfig
|
|
14
15
|
}) => {
|
|
15
16
|
const router = PromiseRouter__default.default();
|
|
@@ -37,7 +38,14 @@ const createSseRouter = ({
|
|
|
37
38
|
}
|
|
38
39
|
const transport = transportsToSessionId.get(sessionId);
|
|
39
40
|
if (transport) {
|
|
40
|
-
|
|
41
|
+
const ctx = tracing.propagation.extract(
|
|
42
|
+
tracing.context.active(),
|
|
43
|
+
req.headers
|
|
44
|
+
);
|
|
45
|
+
await tracing.context.with(
|
|
46
|
+
ctx,
|
|
47
|
+
() => transport.handlePostMessage(req, res, req.body)
|
|
48
|
+
);
|
|
41
49
|
} else {
|
|
42
50
|
res.status(400).contentType("text/plain").write(`No transport found for sessionId "${sessionId}"`);
|
|
43
51
|
}
|
|
@@ -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';\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":";;;;;;;;;
|
|
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 { TracingService } from '@backstage/backend-plugin-api/alpha';\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 tracing,\n serverConfig,\n}: {\n mcpService: McpService;\n httpAuth: HttpAuthService;\n tracing: TracingService;\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 const ctx = tracing.propagation.extract(\n tracing.context.active(),\n req.headers,\n );\n await tracing.context.with(ctx, () =>\n transport.handlePostMessage(req, res, req.body),\n );\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":";;;;;;;;;AA0BO,MAAM,kBAAkB,CAAC;AAAA,EAC9B,UAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,KAKc;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,GAAA,GAAM,QAAQ,WAAA,CAAY,OAAA;AAAA,QAC9B,OAAA,CAAQ,QAAQ,MAAA,EAAO;AAAA,QACvB,GAAA,CAAI;AAAA,OACN;AACA,MAAA,MAAM,QAAQ,OAAA,CAAQ,IAAA;AAAA,QAAK,GAAA;AAAA,QAAK,MAC9B,SAAA,CAAU,iBAAA,CAAkB,GAAA,EAAK,GAAA,EAAK,IAAI,IAAI;AAAA,OAChD;AAAA,IACF,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;;;;"}
|
|
@@ -16,6 +16,7 @@ const createStreamableRouter = ({
|
|
|
16
16
|
httpAuth,
|
|
17
17
|
logger,
|
|
18
18
|
metrics: metrics$1,
|
|
19
|
+
tracing,
|
|
19
20
|
serverConfig
|
|
20
21
|
}) => {
|
|
21
22
|
const router = PromiseRouter__default.default();
|
|
@@ -45,7 +46,14 @@ const createStreamableRouter = ({
|
|
|
45
46
|
sessionIdGenerator: void 0
|
|
46
47
|
});
|
|
47
48
|
await server.connect(transport);
|
|
48
|
-
|
|
49
|
+
const ctx = tracing.propagation.extract(
|
|
50
|
+
tracing.context.active(),
|
|
51
|
+
req.headers
|
|
52
|
+
);
|
|
53
|
+
await tracing.context.with(
|
|
54
|
+
ctx,
|
|
55
|
+
() => transport.handleRequest(req, res, req.body)
|
|
56
|
+
);
|
|
49
57
|
res.on("close", () => {
|
|
50
58
|
transport.close();
|
|
51
59
|
server.close();
|
|
@@ -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 { toError } from '@backstage/errors';\nimport {
|
|
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 { toError } from '@backstage/errors';\nimport {\n MetricsService,\n TracingService,\n} 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 tracing,\n serverConfig,\n}: {\n mcpService: McpService;\n logger: LoggerService;\n httpAuth: HttpAuthService;\n metrics: MetricsService;\n tracing: TracingService;\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 const ctx = tracing.propagation.extract(\n tracing.context.active(),\n req.headers,\n );\n await tracing.context.with(ctx, () =>\n transport.handleRequest(req, res, req.body),\n );\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 err = toError(error);\n const errorType = err.name;\n\n logger.error(err.message);\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","toError"],"mappings":"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"}
|
|
@@ -8,13 +8,43 @@ var node_perf_hooks = require('node:perf_hooks');
|
|
|
8
8
|
var handleErrors = require('./handleErrors.cjs.js');
|
|
9
9
|
var metrics = require('../metrics.cjs.js');
|
|
10
10
|
|
|
11
|
+
function safeStringify(value) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.stringify(value) ?? String(value);
|
|
14
|
+
} catch {
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const PROPAGATED_BAGGAGE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
19
|
+
"gen_ai.agent.id",
|
|
20
|
+
"gen_ai.agent.name",
|
|
21
|
+
"gen_ai.conversation.id",
|
|
22
|
+
"gen_ai.provider.name",
|
|
23
|
+
"gen_ai.request.model"
|
|
24
|
+
]);
|
|
25
|
+
const BAGGAGE_ATTRIBUTE_VALUE_MAX_LENGTH = 256;
|
|
26
|
+
function baggageAttributes(tracingService) {
|
|
27
|
+
const baggage = tracingService.propagation.getActiveBaggage();
|
|
28
|
+
if (!baggage) return {};
|
|
29
|
+
const attrs = {};
|
|
30
|
+
for (const [key, entry] of baggage.getAllEntries()) {
|
|
31
|
+
if (PROPAGATED_BAGGAGE_ATTRIBUTES.has(key)) {
|
|
32
|
+
attrs[key] = entry.value.slice(0, BAGGAGE_ATTRIBUTE_VALUE_MAX_LENGTH);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return attrs;
|
|
36
|
+
}
|
|
11
37
|
class McpService {
|
|
12
38
|
actions;
|
|
13
39
|
namespacedToolNames;
|
|
40
|
+
tracingService;
|
|
41
|
+
captureToolPayloads;
|
|
14
42
|
operationDuration;
|
|
15
|
-
constructor(actions, metrics$1, namespacedToolNames) {
|
|
43
|
+
constructor(actions, metrics$1, tracingService, namespacedToolNames, captureToolPayloads) {
|
|
16
44
|
this.actions = actions;
|
|
17
45
|
this.namespacedToolNames = namespacedToolNames ?? true;
|
|
46
|
+
this.tracingService = tracingService;
|
|
47
|
+
this.captureToolPayloads = captureToolPayloads ?? false;
|
|
18
48
|
this.operationDuration = metrics$1.createHistogram(
|
|
19
49
|
"mcp.server.operation.duration",
|
|
20
50
|
{
|
|
@@ -27,9 +57,17 @@ class McpService {
|
|
|
27
57
|
static async create({
|
|
28
58
|
actions,
|
|
29
59
|
metrics,
|
|
30
|
-
|
|
60
|
+
tracingService,
|
|
61
|
+
namespacedToolNames,
|
|
62
|
+
captureToolPayloads
|
|
31
63
|
}) {
|
|
32
|
-
return new McpService(
|
|
64
|
+
return new McpService(
|
|
65
|
+
actions,
|
|
66
|
+
metrics,
|
|
67
|
+
tracingService,
|
|
68
|
+
namespacedToolNames,
|
|
69
|
+
captureToolPayloads
|
|
70
|
+
);
|
|
33
71
|
}
|
|
34
72
|
getServer({
|
|
35
73
|
credentials,
|
|
@@ -38,7 +76,6 @@ class McpService {
|
|
|
38
76
|
const server = new index_js.Server(
|
|
39
77
|
{
|
|
40
78
|
name: serverConfig?.name ?? "backstage",
|
|
41
|
-
// TODO: this version will most likely change in the future.
|
|
42
79
|
version: package_json.version,
|
|
43
80
|
...serverConfig?.description && {
|
|
44
81
|
description: serverConfig.description
|
|
@@ -57,9 +94,6 @@ class McpService {
|
|
|
57
94
|
return {
|
|
58
95
|
tools: actions.map((action) => ({
|
|
59
96
|
inputSchema: action.schema.input,
|
|
60
|
-
// todo(blam): this is unfortunately not supported by most clients yet.
|
|
61
|
-
// When this is provided you need to provide structuredContent instead.
|
|
62
|
-
// outputSchema: action.schema.output,
|
|
63
97
|
name: this.getToolName(action),
|
|
64
98
|
description: action.description,
|
|
65
99
|
annotations: {
|
|
@@ -87,36 +121,63 @@ class McpService {
|
|
|
87
121
|
let errorType;
|
|
88
122
|
let isError = false;
|
|
89
123
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
credentials
|
|
103
|
-
});
|
|
104
|
-
return {
|
|
105
|
-
// todo(blam): unfortunately structuredContent is not supported by most clients yet.
|
|
106
|
-
// so the validation for the output happens in the default actions registry
|
|
107
|
-
// and we return it as json text instead for now.
|
|
108
|
-
content: [
|
|
109
|
-
{
|
|
110
|
-
type: "text",
|
|
111
|
-
text: ["```json", JSON.stringify(output, null, 2), "```"].join(
|
|
112
|
-
"\n"
|
|
113
|
-
)
|
|
124
|
+
return await this.tracingService.startActiveSpan(
|
|
125
|
+
`tools/call ${params.name}`,
|
|
126
|
+
{
|
|
127
|
+
kind: "server",
|
|
128
|
+
credentials,
|
|
129
|
+
attributes: {
|
|
130
|
+
...baggageAttributes(this.tracingService),
|
|
131
|
+
"mcp.method.name": "tools/call",
|
|
132
|
+
"gen_ai.tool.name": params.name,
|
|
133
|
+
"gen_ai.operation.name": "execute_tool",
|
|
134
|
+
...this.captureToolPayloads && {
|
|
135
|
+
"gen_ai.tool.call.arguments": safeStringify(params.arguments)
|
|
114
136
|
}
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async (span) => {
|
|
140
|
+
const result = await handleErrors.handleErrors(async () => {
|
|
141
|
+
const { actions: allActions } = await this.actions.list({
|
|
142
|
+
credentials
|
|
143
|
+
});
|
|
144
|
+
const actions = serverConfig ? this.filterActions(allActions, serverConfig) : allActions;
|
|
145
|
+
const action = actions.find(
|
|
146
|
+
(a) => this.getToolName(a) === params.name
|
|
147
|
+
);
|
|
148
|
+
if (!action) {
|
|
149
|
+
throw new errors.NotFoundError(`Action "${params.name}" not found`);
|
|
150
|
+
}
|
|
151
|
+
span.setAttribute("backstage.plugin.id", action.pluginId);
|
|
152
|
+
const { output } = await this.actions.invoke({
|
|
153
|
+
id: action.id,
|
|
154
|
+
input: params.arguments,
|
|
155
|
+
credentials
|
|
156
|
+
});
|
|
157
|
+
if (this.captureToolPayloads) {
|
|
158
|
+
span.setAttribute(
|
|
159
|
+
"gen_ai.tool.call.result",
|
|
160
|
+
safeStringify(output)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: safeStringify(output)
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
structuredContent: output
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
isError = !!result?.isError;
|
|
174
|
+
if (isError) {
|
|
175
|
+
span.setAttribute("error.type", "tool_error");
|
|
176
|
+
span.setStatus({ code: "error", message: "tool_error" });
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
);
|
|
120
181
|
} catch (err) {
|
|
121
182
|
errorType = err instanceof Error ? err.name : "Error";
|
|
122
183
|
throw err;
|
|
@@ -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 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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"}
|
|
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 TracingService,\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\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n// Baggage is propagated from untrusted callers, so we forward only an\n// explicit allowlist of low-cardinality identifier keys from the OTel\n// `gen_ai.*` registry.\nconst PROPAGATED_BAGGAGE_ATTRIBUTES: ReadonlySet<string> = new Set([\n 'gen_ai.agent.id',\n 'gen_ai.agent.name',\n 'gen_ai.conversation.id',\n 'gen_ai.provider.name',\n 'gen_ai.request.model',\n]);\n\n// Cap each forwarded baggage value before it lands on a span attribute.\n// Baggage values are caller-controlled strings of unbounded length;\n// allowlisting keys protects against arbitrary attribute names but not\n// against pathologically large values inflating exported span sizes.\nconst BAGGAGE_ATTRIBUTE_VALUE_MAX_LENGTH = 256;\n\nfunction baggageAttributes(\n tracingService: TracingService,\n): Record<string, string> {\n const baggage = tracingService.propagation.getActiveBaggage();\n if (!baggage) return {};\n const attrs: Record<string, string> = {};\n for (const [key, entry] of baggage.getAllEntries()) {\n if (PROPAGATED_BAGGAGE_ATTRIBUTES.has(key)) {\n attrs[key] = entry.value.slice(0, BAGGAGE_ATTRIBUTE_VALUE_MAX_LENGTH);\n }\n }\n return attrs;\n}\n\nexport class McpService {\n private readonly actions: ActionsService;\n private readonly namespacedToolNames: boolean;\n private readonly tracingService: TracingService;\n private readonly captureToolPayloads: boolean;\n private readonly operationDuration: MetricsServiceHistogram<McpServerOperationAttributes>;\n\n constructor(\n actions: ActionsService,\n metrics: MetricsService,\n tracingService: TracingService,\n namespacedToolNames?: boolean,\n captureToolPayloads?: boolean,\n ) {\n this.actions = actions;\n this.namespacedToolNames = namespacedToolNames ?? true;\n this.tracingService = tracingService;\n this.captureToolPayloads = captureToolPayloads ?? false;\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 tracingService,\n namespacedToolNames,\n captureToolPayloads,\n }: {\n actions: ActionsService;\n metrics: MetricsService;\n tracingService: TracingService;\n namespacedToolNames?: boolean;\n captureToolPayloads?: boolean;\n }) {\n return new McpService(\n actions,\n metrics,\n tracingService,\n namespacedToolNames,\n captureToolPayloads,\n );\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 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 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 return await this.tracingService.startActiveSpan(\n `tools/call ${params.name}`,\n {\n kind: 'server',\n credentials,\n attributes: {\n ...baggageAttributes(this.tracingService),\n 'mcp.method.name': 'tools/call',\n 'gen_ai.tool.name': params.name,\n 'gen_ai.operation.name': 'execute_tool',\n ...(this.captureToolPayloads && {\n 'gen_ai.tool.call.arguments': safeStringify(params.arguments),\n }),\n },\n },\n async span => {\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(\n a => this.getToolName(a) === params.name,\n );\n\n if (!action) {\n throw new NotFoundError(`Action \"${params.name}\" not found`);\n }\n\n // Re-attribute the span to the plugin that owns the action.\n // This runs after the span has started, so head-based samplers\n // still see the default `mcp-actions` value when deciding\n // whether to record the span. The pluginId is only known after\n // resolving the action via `actions.list`, so the reattribution\n // is unavoidable.\n span.setAttribute('backstage.plugin.id', action.pluginId);\n\n const { output } = await this.actions.invoke({\n id: action.id,\n input: params.arguments as JsonObject,\n credentials,\n });\n\n if (this.captureToolPayloads) {\n span.setAttribute(\n 'gen_ai.tool.call.result',\n safeStringify(output),\n );\n }\n\n return {\n content: [\n {\n type: 'text',\n text: safeStringify(output),\n },\n ],\n structuredContent: output,\n };\n });\n\n isError = !!(result as { isError?: boolean })?.isError;\n if (isError) {\n span.setAttribute('error.type', 'tool_error');\n span.setStatus({ code: 'error', message: 'tool_error' });\n }\n return result;\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 // 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.13
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"backstage": {
|
|
5
5
|
"role": "backend-plugin",
|
|
6
6
|
"pluginId": "mcp-actions",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"test": "backstage-cli package test"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@backstage/backend-plugin-api": "1.9.1
|
|
43
|
-
"@backstage/catalog-client": "1.15.1
|
|
44
|
-
"@backstage/config": "1.3.8
|
|
45
|
-
"@backstage/errors": "1.3.1
|
|
46
|
-
"@backstage/plugin-catalog-node": "2.2.1
|
|
47
|
-
"@backstage/types": "1.2.2",
|
|
42
|
+
"@backstage/backend-plugin-api": "^1.9.1",
|
|
43
|
+
"@backstage/catalog-client": "^1.15.1",
|
|
44
|
+
"@backstage/config": "^1.3.8",
|
|
45
|
+
"@backstage/errors": "^1.3.1",
|
|
46
|
+
"@backstage/plugin-catalog-node": "^2.2.1",
|
|
47
|
+
"@backstage/types": "^1.2.2",
|
|
48
48
|
"@cfworker/json-schema": "^4.1.1",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
50
50
|
"express": "^4.22.0",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"zod": "^3.25.76 || ^4.0.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@backstage/backend-defaults": "0.17.1
|
|
57
|
-
"@backstage/backend-test-utils": "1.11.3
|
|
58
|
-
"@backstage/cli": "0.36.2
|
|
56
|
+
"@backstage/backend-defaults": "^0.17.1",
|
|
57
|
+
"@backstage/backend-test-utils": "^1.11.3",
|
|
58
|
+
"@backstage/cli": "^0.36.2",
|
|
59
59
|
"@types/express": "^4.17.6",
|
|
60
60
|
"@types/supertest": "^2.0.8",
|
|
61
61
|
"supertest": "^7.0.0"
|