fix: align MCP Apps with official ext-apps spec (#574)

* feat: add MCP Apps with rich HTML UIs for tool results

Add MCP Apps infrastructure that allows MCP hosts like Claude Desktop
to render rich HTML UIs alongside tool results via `_meta.ui` and the
MCP resources protocol.

- Server-side UI module (src/mcp/ui/) with UIAppRegistry, tool-to-UI
  mapping, and _meta.ui injection into tool responses
- React + Vite build pipeline (ui-apps/) producing self-contained HTML
  per app using vite-plugin-singlefile
- Operation Result UI for workflow CRUD tools (create, update, delete,
  test, autofix, deploy)
- Validation Summary UI for validation tools (validate_node,
  validate_workflow, n8n_validate_workflow)
- Shared component library (Card, Badge, Expandable) with n8n dark theme
- MCP resources protocol support (ListResources, ReadResource handlers)
- Graceful degradation when ui-apps/dist/ is not built
- 22 unit tests across 3 test files

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: improve MCP Apps test coverage and add security hardening

- Expand test suite from 22 to 57 tests across 3 test files
- Add UIAppRegistry.reset() for proper test isolation between tests
- Replace some fs mocks with real temp directory tests in registry
- Add edge case coverage: empty strings, pre-load state, double load,
  malformed URIs, duplicate tool patterns, empty HTML files
- Add regression tests for specific tool-to-UI mappings
- Add URI format consistency validation across all configs
- Improve _meta.ui injection tests with structuredContent coexistence
- Coverage: statements 79.4% -> 80%, lines 79.4% -> 80%

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: align MCP Apps with official ext-apps spec

Update URI scheme from n8n-mcp://ui/ to ui://n8n-mcp/ per MCP spec.
Move _meta.ui.resourceUri to tool definitions (tools/list) instead of
tool call responses. Rewrite UI apps hook to use @modelcontextprotocol/ext-apps
App class instead of window.__MCP_DATA__.

Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2026-02-07 05:16:15 +01:00
committed by GitHub
parent 1f45cc6dcc
commit 23b90d01a6
12 changed files with 210 additions and 175 deletions

View File

@@ -651,6 +651,7 @@ export class N8NDocumentationMCPServer {
});
});
UIAppRegistry.injectToolMeta(tools);
return { tools };
});
@@ -781,12 +782,6 @@ export class N8NDocumentationMCPServer {
mcpResponse.structuredContent = structuredContent;
}
// Inject UI app metadata if available
const uiApp = UIAppRegistry.getAppForTool(name);
if (uiApp && uiApp.html) {
mcpResponse._meta = { ui: { app: uiApp.config.uri } };
}
return mcpResponse;
} catch (error) {
logger.error(`Error executing tool ${name}`, error);
@@ -857,8 +852,8 @@ export class N8NDocumentationMCPServer {
// Handle ReadResource for UI apps
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
// Parse n8n-mcp://ui/{id} pattern
const match = uri.match(/^n8n-mcp:\/\/ui\/(.+)$/);
// Parse ui://n8n-mcp/{id} pattern
const match = uri.match(/^ui:\/\/n8n-mcp\/(.+)$/);
if (!match) {
throw new Error(`Unknown resource URI: ${uri}`);
}

View File

@@ -5,7 +5,7 @@ export const UI_APP_CONFIGS: UIAppConfig[] = [
id: 'operation-result',
displayName: 'Operation Result',
description: 'Visual summary of workflow operations (create, update, delete, test)',
uri: 'n8n-mcp://ui/operation-result',
uri: 'ui://n8n-mcp/operation-result',
mimeType: 'text/html',
toolPatterns: [
'n8n_create_workflow',
@@ -21,7 +21,7 @@ export const UI_APP_CONFIGS: UIAppConfig[] = [
id: 'validation-summary',
displayName: 'Validation Summary',
description: 'Visual summary of node and workflow validation results',
uri: 'n8n-mcp://ui/validation-summary',
uri: 'ui://n8n-mcp/validation-summary',
mimeType: 'text/html',
toolPatterns: [
'validate_node',

View File

@@ -60,6 +60,21 @@ export class UIAppRegistry {
return Array.from(this.entries.values());
}
/**
* 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.
*/
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 } };
}
}
}
/** Reset registry state. Intended for testing only. */
static reset(): void {
this.entries.clear();

View File

@@ -13,7 +13,7 @@ export interface UIAppConfig {
export interface UIMetadata {
ui: {
app: string;
resourceUri: string;
};
}