feat: MCP Apps - rich HTML UIs for tool results (#573)

* 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>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2026-02-07 04:11:21 +01:00
committed by GitHub
parent 6814880410
commit 1f45cc6dcc
37 changed files with 3306 additions and 9 deletions

View File

@@ -1,13 +1,16 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
import {
CallToolRequestSchema,
ListToolsRequestSchema,
InitializeRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { existsSync, promises as fs } from 'fs';
import path from 'path';
import { n8nDocumentationToolsFinal } from './tools';
import { UIAppRegistry } from './ui';
import { n8nManagementTools } from './tools-n8n-manager';
import { makeToolsN8nFriendly } from './tools-n8n-friendly';
import { getWorkflowExampleString } from './workflow-examples';
@@ -235,10 +238,12 @@ export class N8NDocumentationMCPServer {
{
capabilities: {
tools: {},
resources: {},
},
}
);
UIAppRegistry.load();
this.setupHandlers();
}
@@ -563,6 +568,7 @@ export class N8NDocumentationMCPServer {
protocolVersion: negotiationResult.version,
capabilities: {
tools: {},
resources: {},
},
serverInfo: {
name: 'n8n-documentation-mcp',
@@ -774,7 +780,13 @@ export class N8NDocumentationMCPServer {
if (name.startsWith('validate_') && structuredContent !== null) {
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);
@@ -826,6 +838,46 @@ export class N8NDocumentationMCPServer {
};
}
});
// Handle ListResources for UI apps
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const apps = UIAppRegistry.getAllApps();
return {
resources: apps
.filter(app => app.html !== null)
.map(app => ({
uri: app.config.uri,
name: app.config.displayName,
description: app.config.description,
mimeType: app.config.mimeType,
})),
};
});
// 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\/(.+)$/);
if (!match) {
throw new Error(`Unknown resource URI: ${uri}`);
}
const app = UIAppRegistry.getAppById(match[1]);
if (!app || !app.html) {
throw new Error(`UI app not found or not built: ${match[1]}`);
}
return {
contents: [
{
uri: app.config.uri,
mimeType: app.config.mimeType,
text: app.html,
},
],
};
});
}
/**