mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-09 23:03:12 +00:00
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 <noreply@anthropic.com>
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { UIAppRegistry } from '@/mcp/ui/registry';
|
|
|
|
vi.mock('fs', () => ({
|
|
existsSync: vi.fn(),
|
|
readFileSync: vi.fn(),
|
|
}));
|
|
|
|
import { existsSync, readFileSync } from 'fs';
|
|
|
|
const mockExistsSync = vi.mocked(existsSync);
|
|
const mockReadFileSync = vi.mocked(readFileSync);
|
|
|
|
describe('UI Meta Injection on Tool Definitions', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
UIAppRegistry.reset();
|
|
});
|
|
|
|
describe('when HTML is loaded', () => {
|
|
beforeEach(() => {
|
|
mockExistsSync.mockReturnValue(true);
|
|
mockReadFileSync.mockReturnValue('<html>ui content</html>');
|
|
UIAppRegistry.load();
|
|
});
|
|
|
|
it('should add _meta.ui.resourceUri to matching tool definitions', () => {
|
|
const tools: any[] = [
|
|
{ name: 'n8n_create_workflow', description: 'Create workflow', inputSchema: { type: 'object', properties: {} } },
|
|
];
|
|
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
|
|
expect(tools[0]._meta).toBeDefined();
|
|
expect(tools[0]._meta.ui.resourceUri).toBe('ui://n8n-mcp/operation-result');
|
|
});
|
|
|
|
it('should add _meta.ui.resourceUri to validation tool definitions', () => {
|
|
const tools: any[] = [
|
|
{ name: 'validate_workflow', description: 'Validate', inputSchema: { type: 'object', properties: {} } },
|
|
];
|
|
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
|
|
expect(tools[0]._meta).toBeDefined();
|
|
expect(tools[0]._meta.ui.resourceUri).toBe('ui://n8n-mcp/validation-summary');
|
|
});
|
|
|
|
it('should NOT add _meta to non-matching tool definitions', () => {
|
|
const tools: any[] = [
|
|
{ name: 'get_node_info', description: 'Get info', inputSchema: { type: 'object', properties: {} } },
|
|
];
|
|
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
|
|
expect(tools[0]._meta).toBeUndefined();
|
|
});
|
|
|
|
it('should inject _meta on matching tools and skip non-matching in a mixed list', () => {
|
|
const tools: any[] = [
|
|
{ name: 'n8n_create_workflow', description: 'Create', inputSchema: { type: 'object', properties: {} } },
|
|
{ name: 'get_node_info', description: 'Info', inputSchema: { type: 'object', properties: {} } },
|
|
{ name: 'validate_node', description: 'Validate', inputSchema: { type: 'object', properties: {} } },
|
|
];
|
|
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
|
|
expect(tools[0]._meta).toBeDefined();
|
|
expect(tools[0]._meta.ui.resourceUri).toBe('ui://n8n-mcp/operation-result');
|
|
expect(tools[1]._meta).toBeUndefined();
|
|
expect(tools[2]._meta).toBeDefined();
|
|
expect(tools[2]._meta.ui.resourceUri).toBe('ui://n8n-mcp/validation-summary');
|
|
});
|
|
|
|
it('should produce _meta with both nested and flat resourceUri keys', () => {
|
|
const tools: any[] = [
|
|
{ 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',
|
|
},
|
|
'ui/resourceUri': 'ui://n8n-mcp/operation-result',
|
|
});
|
|
expect(tools[0]._meta.ui.resourceUri).toBe('ui://n8n-mcp/operation-result');
|
|
expect(tools[0]._meta['ui/resourceUri']).toBe('ui://n8n-mcp/operation-result');
|
|
});
|
|
});
|
|
|
|
describe('when HTML is not loaded', () => {
|
|
beforeEach(() => {
|
|
mockExistsSync.mockReturnValue(false);
|
|
UIAppRegistry.load();
|
|
});
|
|
|
|
it('should NOT add _meta even for matching tools', () => {
|
|
const tools: any[] = [
|
|
{ name: 'n8n_create_workflow', description: 'Create', inputSchema: { type: 'object', properties: {} } },
|
|
];
|
|
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
|
|
expect(tools[0]._meta).toBeUndefined();
|
|
});
|
|
|
|
it('should NOT add _meta for validation tools without HTML', () => {
|
|
const tools: any[] = [
|
|
{ name: 'validate_node', description: 'Validate', inputSchema: { type: 'object', properties: {} } },
|
|
];
|
|
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
|
|
expect(tools[0]._meta).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('when registry has not been loaded at all', () => {
|
|
it('should NOT add _meta because registry is not loaded', () => {
|
|
const tools: any[] = [
|
|
{ name: 'n8n_create_workflow', description: 'Create', inputSchema: { type: 'object', properties: {} } },
|
|
];
|
|
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
|
|
expect(tools[0]._meta).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('empty tool list', () => {
|
|
beforeEach(() => {
|
|
mockExistsSync.mockReturnValue(true);
|
|
mockReadFileSync.mockReturnValue('<html>ui</html>');
|
|
UIAppRegistry.load();
|
|
});
|
|
|
|
it('should handle an empty tools array without error', () => {
|
|
const tools: any[] = [];
|
|
UIAppRegistry.injectToolMeta(tools);
|
|
expect(tools.length).toBe(0);
|
|
});
|
|
});
|
|
});
|