Files
n8n-mcp/tests/unit/mcp/ui/app-configs.test.ts
Romuald Członkowski 23b90d01a6 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>
2026-02-07 05:16:15 +01:00

111 lines
3.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { UI_APP_CONFIGS } from '@/mcp/ui/app-configs';
describe('UI_APP_CONFIGS', () => {
it('should have all required fields for every config', () => {
for (const config of UI_APP_CONFIGS) {
expect(config.id).toBeDefined();
expect(typeof config.id).toBe('string');
expect(config.id.length).toBeGreaterThan(0);
expect(config.displayName).toBeDefined();
expect(typeof config.displayName).toBe('string');
expect(config.displayName.length).toBeGreaterThan(0);
expect(config.description).toBeDefined();
expect(typeof config.description).toBe('string');
expect(config.description.length).toBeGreaterThan(0);
expect(config.uri).toBeDefined();
expect(typeof config.uri).toBe('string');
expect(config.mimeType).toBeDefined();
expect(typeof config.mimeType).toBe('string');
expect(config.toolPatterns).toBeDefined();
expect(Array.isArray(config.toolPatterns)).toBe(true);
}
});
it('should have URIs following ui://n8n-mcp/{id} pattern', () => {
for (const config of UI_APP_CONFIGS) {
expect(config.uri).toBe(`ui://n8n-mcp/${config.id}`);
}
});
it('should have unique IDs', () => {
const ids = UI_APP_CONFIGS.map(c => c.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(ids.length);
});
it('should have non-empty toolPatterns arrays', () => {
for (const config of UI_APP_CONFIGS) {
expect(config.toolPatterns.length).toBeGreaterThan(0);
for (const pattern of config.toolPatterns) {
expect(typeof pattern).toBe('string');
expect(pattern.length).toBeGreaterThan(0);
}
}
});
it('should not have duplicate tool patterns across configs', () => {
const allPatterns: string[] = [];
for (const config of UI_APP_CONFIGS) {
allPatterns.push(...config.toolPatterns);
}
const uniquePatterns = new Set(allPatterns);
expect(uniquePatterns.size).toBe(allPatterns.length);
});
it('should not have duplicate tool patterns within a single config', () => {
for (const config of UI_APP_CONFIGS) {
const unique = new Set(config.toolPatterns);
expect(unique.size).toBe(config.toolPatterns.length);
}
});
it('should have consistent mimeType of text/html', () => {
for (const config of UI_APP_CONFIGS) {
expect(config.mimeType).toBe('text/html');
}
});
it('should have URIs that start with the ui://n8n-mcp/ scheme', () => {
for (const config of UI_APP_CONFIGS) {
expect(config.uri).toMatch(/^ui:\/\/n8n-mcp\//);
}
});
// Regression: verify expected configs are present
it('should contain the operation-result config', () => {
const config = UI_APP_CONFIGS.find(c => c.id === 'operation-result');
expect(config).toBeDefined();
expect(config!.displayName).toBe('Operation Result');
expect(config!.toolPatterns).toContain('n8n_create_workflow');
expect(config!.toolPatterns).toContain('n8n_update_full_workflow');
expect(config!.toolPatterns).toContain('n8n_delete_workflow');
expect(config!.toolPatterns).toContain('n8n_test_workflow');
expect(config!.toolPatterns).toContain('n8n_deploy_template');
});
it('should contain the validation-summary config', () => {
const config = UI_APP_CONFIGS.find(c => c.id === 'validation-summary');
expect(config).toBeDefined();
expect(config!.displayName).toBe('Validation Summary');
expect(config!.toolPatterns).toContain('validate_node');
expect(config!.toolPatterns).toContain('validate_workflow');
expect(config!.toolPatterns).toContain('n8n_validate_workflow');
});
it('should have exactly 2 configs', () => {
expect(UI_APP_CONFIGS.length).toBe(2);
});
it('should have IDs that are valid URI path segments (no spaces or special chars)', () => {
for (const config of UI_APP_CONFIGS) {
expect(config.id).toMatch(/^[a-z0-9-]+$/);
}
});
});