From 34159f4ece6f345ead8f2c551fa391f621073f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romuald=20Cz=C5=82onkowski?= <56956555+czlonkowski@users.noreply.github.com> Date: Mon, 9 Feb 2026 06:40:52 +0100 Subject: [PATCH] fix: add legacy flat _meta key for MCP App rendering in Claude (#585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude.ai reads the flat `_meta["ui/resourceUri"]` key to discover UI apps, not the nested `_meta.ui.resourceUri`. Without the flat key, tools like n8n_health_check and n8n_list_workflows showed as collapsed accordions instead of rendering rich UI. Now sets both keys, matching the behavior of the official registerAppTool helper from @modelcontextprotocol/ext-apps. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-authored-by: Claude Opus 4.6 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- src/mcp/ui/registry.ts | 8 +++++++- tests/unit/mcp/ui/meta-injection.test.ts | 8 ++++---- tests/unit/mcp/ui/registry.test.ts | 4 ++-- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a23af6e..d579fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.35.1] - 2026-02-09 + +### Fixed + +- **MCP Apps: Fix UI not rendering for some tools in Claude**: Added legacy flat `_meta["ui/resourceUri"]` key alongside the nested `_meta.ui.resourceUri` in tool definitions. Claude.ai reads the flat key format; without it, tools like `n8n_health_check` and `n8n_list_workflows` showed as collapsed accordions instead of rendering their rich UI apps. Both key formats are now set by `injectToolMeta()`, matching the behavior of the official `registerAppTool` helper from `@modelcontextprotocol/ext-apps/server`. + +Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en + ## [2.35.0] - 2026-02-09 ### Added diff --git a/package.json b/package.json index fdbd5d2..d20d3f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.35.0", + "version": "2.35.1", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/mcp/ui/registry.ts b/src/mcp/ui/registry.ts index 846891b..4698e29 100644 --- a/src/mcp/ui/registry.ts +++ b/src/mcp/ui/registry.ts @@ -64,13 +64,19 @@ export class UIAppRegistry { * Enrich tool definitions with _meta.ui.resourceUri for tools that have * a matching UI app. Per MCP ext-apps spec, this goes on the tool * definition (tools/list), not the tool call response. + * + * Sets both nested (_meta.ui.resourceUri) and flat (_meta["ui/resourceUri"]) + * keys for compatibility with hosts that read either format. */ static injectToolMeta(tools: Array<{ name: string; [key: string]: any }>): void { if (!this.loaded) return; for (const tool of tools) { const entry = this.toolIndex.get(tool.name); if (entry && entry.html) { - tool._meta = { ui: { resourceUri: entry.config.uri } }; + tool._meta = { + ui: { resourceUri: entry.config.uri }, + 'ui/resourceUri': entry.config.uri, + }; } } } diff --git a/tests/unit/mcp/ui/meta-injection.test.ts b/tests/unit/mcp/ui/meta-injection.test.ts index 54956ee..f626f3d 100644 --- a/tests/unit/mcp/ui/meta-injection.test.ts +++ b/tests/unit/mcp/ui/meta-injection.test.ts @@ -72,7 +72,7 @@ describe('UI Meta Injection on Tool Definitions', () => { expect(tools[2]._meta.ui.resourceUri).toBe('ui://n8n-mcp/validation-summary'); }); - it('should produce _meta with exact shape { ui: { resourceUri: string } }', () => { + it('should produce _meta with both nested and flat resourceUri keys', () => { const tools: any[] = [ { name: 'n8n_create_workflow', description: 'Create', inputSchema: { type: 'object', properties: {} } }, ]; @@ -83,10 +83,10 @@ describe('UI Meta Injection on Tool Definitions', () => { ui: { resourceUri: 'ui://n8n-mcp/operation-result', }, + 'ui/resourceUri': 'ui://n8n-mcp/operation-result', }); - expect(Object.keys(tools[0]._meta)).toEqual(['ui']); - expect(Object.keys(tools[0]._meta.ui)).toEqual(['resourceUri']); - expect(typeof tools[0]._meta.ui.resourceUri).toBe('string'); + expect(tools[0]._meta.ui.resourceUri).toBe('ui://n8n-mcp/operation-result'); + expect(tools[0]._meta['ui/resourceUri']).toBe('ui://n8n-mcp/operation-result'); }); }); diff --git a/tests/unit/mcp/ui/registry.test.ts b/tests/unit/mcp/ui/registry.test.ts index 7bb1cc8..3d85167 100644 --- a/tests/unit/mcp/ui/registry.test.ts +++ b/tests/unit/mcp/ui/registry.test.ts @@ -308,7 +308,7 @@ describe('UIAppRegistry', () => { { name: 'n8n_create_workflow', description: 'Create', inputSchema: { type: 'object', properties: {} } }, ]; UIAppRegistry.injectToolMeta(tools); - expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/operation-result' } }); + expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/operation-result' }, 'ui/resourceUri': 'ui://n8n-mcp/operation-result' }); }); it('should set _meta.ui.resourceUri on matching validation tools', () => { @@ -316,7 +316,7 @@ describe('UIAppRegistry', () => { { name: 'validate_node', description: 'Validate', inputSchema: { type: 'object', properties: {} } }, ]; UIAppRegistry.injectToolMeta(tools); - expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/validation-summary' } }); + expect(tools[0]._meta).toEqual({ ui: { resourceUri: 'ui://n8n-mcp/validation-summary' }, 'ui/resourceUri': 'ui://n8n-mcp/validation-summary' }); }); it('should not set _meta on tools without a matching UI app', () => {