mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-07 05:53:07 +00:00
* 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>
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { existsSync, readFileSync } from 'fs';
|
|
import path from 'path';
|
|
import { logger } from '../../utils/logger';
|
|
import type { UIAppConfig, UIAppEntry } from './types';
|
|
import { UI_APP_CONFIGS } from './app-configs';
|
|
|
|
export class UIAppRegistry {
|
|
private static entries: Map<string, UIAppEntry> = new Map();
|
|
private static toolIndex: Map<string, UIAppEntry> = new Map();
|
|
private static loaded = false;
|
|
|
|
static load(): void {
|
|
// Resolve dist directory relative to package root
|
|
// In production: package-root/ui-apps/dist/
|
|
// __dirname will be src/mcp/ui or dist/mcp/ui
|
|
const packageRoot = path.resolve(__dirname, '..', '..', '..');
|
|
const distDir = path.join(packageRoot, 'ui-apps', 'dist');
|
|
|
|
this.entries.clear();
|
|
this.toolIndex.clear();
|
|
|
|
for (const config of UI_APP_CONFIGS) {
|
|
let html: string | null = null;
|
|
const htmlPath = path.join(distDir, config.id, 'index.html');
|
|
|
|
if (existsSync(htmlPath)) {
|
|
try {
|
|
html = readFileSync(htmlPath, 'utf-8');
|
|
logger.info(`Loaded UI app: ${config.id}`);
|
|
} catch (err) {
|
|
logger.warn(`Failed to read UI app HTML: ${config.id}`, err);
|
|
}
|
|
}
|
|
|
|
const entry: UIAppEntry = { config, html };
|
|
this.entries.set(config.id, entry);
|
|
|
|
// Build tool -> entry index
|
|
for (const pattern of config.toolPatterns) {
|
|
this.toolIndex.set(pattern, entry);
|
|
}
|
|
}
|
|
|
|
this.loaded = true;
|
|
logger.info(`UI App Registry loaded: ${this.entries.size} apps, ${this.toolIndex.size} tool mappings`);
|
|
}
|
|
|
|
static getAppForTool(toolName: string): UIAppEntry | null {
|
|
if (!this.loaded) return null;
|
|
return this.toolIndex.get(toolName) ?? null;
|
|
}
|
|
|
|
static getAppById(id: string): UIAppEntry | null {
|
|
if (!this.loaded) return null;
|
|
return this.entries.get(id) ?? null;
|
|
}
|
|
|
|
static getAllApps(): UIAppEntry[] {
|
|
if (!this.loaded) return [];
|
|
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();
|
|
this.toolIndex.clear();
|
|
this.loaded = false;
|
|
}
|
|
}
|