From ca8951ae87be4945dfbdffdb4b4d732783516dc3 Mon Sep 17 00:00:00 2001 From: Love Kumar Chauhan Date: Sat, 25 Apr 2026 23:52:49 +0530 Subject: [PATCH] fix(mcp-actions-backend): return structured content and remove markdown formatting in tool responses This change aligns the MCP tool execution responses with the specification by returning plain text in the 'content' array and providing the raw JSON in a new 'structuredContent' field. It also removes the unnecessary Markdown code block formatting that was previously added to tool results. Fixes #34052 Signed-off-by: Love Kumar Chauhan Co-authored-by: benjdlambert <3645856+benjdlambert@users.noreply.github.com> Signed-off-by: benjdlambert --- .changeset/mcp-actions-backend-md-result-fix.md | 5 +++++ docs-ui/src/app/components/accordion/page.mdx | 2 +- plugins/mcp-actions-backend/README.md | 4 ++++ .../src/services/McpService.test.ts | 7 ++----- .../src/services/McpService.ts | 17 ++--------------- 5 files changed, 14 insertions(+), 21 deletions(-) create mode 100644 .changeset/mcp-actions-backend-md-result-fix.md diff --git a/.changeset/mcp-actions-backend-md-result-fix.md b/.changeset/mcp-actions-backend-md-result-fix.md new file mode 100644 index 0000000000..63b651bf34 --- /dev/null +++ b/.changeset/mcp-actions-backend-md-result-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Fixed an issue where actions returned Markdown-formatted JSON instead of plain JSON and a `structuredContent` field for model context protocol responses. diff --git a/docs-ui/src/app/components/accordion/page.mdx b/docs-ui/src/app/components/accordion/page.mdx index 90c573b087..dc9b87b55d 100644 --- a/docs-ui/src/app/components/accordion/page.mdx +++ b/docs-ui/src/app/components/accordion/page.mdx @@ -140,7 +140,7 @@ Allows multiple panels to be open simultaneously. } code={groupMultipleOpenSnippet} /> diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index d496cf0f03..b3f6895078 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -135,6 +135,10 @@ When errors are thrown from MCP actions, the backend will handle and surface err See [Backstage Errors](https://backstage.io/docs/reference/errors/) for a full list of supported errors. +### Response Format + +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. + When writing MCP tools, use the appropriate error from `@backstage/errors` when applicable: ```ts diff --git a/plugins/mcp-actions-backend/src/services/McpService.test.ts b/plugins/mcp-actions-backend/src/services/McpService.test.ts index d96e9c7285..8aa1bcb22f 100644 --- a/plugins/mcp-actions-backend/src/services/McpService.test.ts +++ b/plugins/mcp-actions-backend/src/services/McpService.test.ts @@ -216,13 +216,10 @@ describe('McpService', () => { expect(result.content).toEqual([ { type: 'text', - text: [ - '```json', - JSON.stringify({ output: 'test' }, null, 2), - '```', - ].join('\n'), + text: JSON.stringify({ output: 'test' }), }, ]); + expect((result as any).structuredContent).toEqual({ output: 'test' }); const histogram = mockMetrics.createHistogram.mock.results[0]?.value; expect(histogram.record).toHaveBeenCalledTimes(1); diff --git a/plugins/mcp-actions-backend/src/services/McpService.ts b/plugins/mcp-actions-backend/src/services/McpService.ts index 9d907f8258..735bce6f55 100644 --- a/plugins/mcp-actions-backend/src/services/McpService.ts +++ b/plugins/mcp-actions-backend/src/services/McpService.ts @@ -135,7 +135,6 @@ export class McpService { const server = new McpServer( { name: serverConfig?.name ?? 'backstage', - // TODO: this version will most likely change in the future. version, ...(serverConfig?.description && { description: serverConfig.description, @@ -159,9 +158,6 @@ export class McpService { return { tools: actions.map(action => ({ inputSchema: action.schema.input, - // todo(blam): this is unfortunately not supported by most clients yet. - // When this is provided you need to provide structuredContent instead. - // outputSchema: action.schema.output, name: this.getToolName(action), description: action.description, annotations: { @@ -238,9 +234,6 @@ export class McpService { credentials, }); - // Record the structured action output directly rather than the - // CallToolResult envelope below, which wraps an already- - // stringified markdown-fenced JSON block. if (this.captureToolPayloads) { span.setAttribute( 'gen_ai.tool.call.result', @@ -249,19 +242,13 @@ export class McpService { } return { - // todo(blam): unfortunately structuredContent is not supported by most clients yet. - // so the validation for the output happens in the default actions registry - // and we return it as json text instead for now. content: [ { type: 'text', - text: [ - '```json', - JSON.stringify(output, null, 2), - '```', - ].join('\n'), + text: JSON.stringify(output), }, ], + structuredContent: output, }; });