mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-09 14:53:07 +00:00
Add workflow-list, execution-history, and health-dashboard apps. Redesign operation-result with operation-aware headers, detail panels, and copy-to-clipboard. Fix React hooks violations in validation-summary and execution-history (useMemo after early returns). Add local preview harness for development. Update tests for 5-app config. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
358 lines
16 KiB
HTML
358 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>MCP App Preview</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 24px; transition: background 0.3s, color 0.3s; }
|
|
body.dark { background: #111; color: #e0e0e0; }
|
|
body.light { background: #f5f5f5; color: #1f2937; }
|
|
h1 { font-size: 18px; margin-bottom: 8px; }
|
|
.theme-toggle { margin-bottom: 16px; }
|
|
.theme-toggle button { padding: 6px 14px; border: 1px solid #666; border-radius: 6px; cursor: pointer; font-size: 12px; margin-right: 8px; }
|
|
body.dark .theme-toggle button { background: #252540; color: #e0e0e0; border-color: #444; }
|
|
body.light .theme-toggle button { background: #fff; color: #1f2937; border-color: #ccc; }
|
|
.theme-toggle button.active { border-color: #ff6d5a; color: #ff6d5a; }
|
|
.section-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 16px 0 8px; opacity: 0.6; }
|
|
.controls { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
button { padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; }
|
|
body.dark button { border: 1px solid #444; background: #252540; color: #e0e0e0; }
|
|
body.light button { border: 1px solid #d1d5db; background: #fff; color: #1f2937; }
|
|
button:hover { opacity: 0.85; }
|
|
button.active { border-color: #ff6d5a; color: #ff6d5a; }
|
|
.preview-frame { border-radius: 8px; overflow: hidden; max-width: 520px; transition: background 0.3s, border-color 0.3s; }
|
|
body.dark .preview-frame { border: 1px solid #333; background: #1a1a2e; }
|
|
body.light .preview-frame { border: 1px solid #e5e7eb; background: #ffffff; }
|
|
iframe { border: none; width: 100%; height: 600px; }
|
|
.info { font-size: 12px; opacity: 0.4; margin-top: 12px; }
|
|
</style>
|
|
</head>
|
|
<body class="dark">
|
|
<h1>MCP App Local Preview</h1>
|
|
<div class="theme-toggle">
|
|
<button onclick="setTheme('dark')" class="active" id="btn-dark">Dark</button>
|
|
<button onclick="setTheme('light')" id="btn-light">Light</button>
|
|
</div>
|
|
|
|
<div class="section-label">Operation Result</div>
|
|
<div class="controls">
|
|
<button onclick="load('operation-result', mockCreateSuccess, 'n8n_create_workflow', this)" class="active">Create (success)</button>
|
|
<button onclick="load('operation-result', mockCreateError, 'n8n_create_workflow', this)">Create (error)</button>
|
|
<button onclick="load('operation-result', mockDelete, 'n8n_delete_workflow', this)">Delete</button>
|
|
<button onclick="load('operation-result', mockPartialUpdate, 'n8n_update_partial_workflow', this)">Partial Update</button>
|
|
<button onclick="load('operation-result', mockFullUpdate, 'n8n_update_full_workflow', this)">Full Update</button>
|
|
<button onclick="load('operation-result', mockAutofix, 'n8n_autofix_workflow', this)">Autofix</button>
|
|
<button onclick="load('operation-result', mockAutofixPreview, 'n8n_autofix_workflow', this)">Autofix (preview)</button>
|
|
<button onclick="load('operation-result', mockDeploy, 'n8n_deploy_template', this)">Deploy Template</button>
|
|
<button onclick="load('operation-result', mockTest, 'n8n_test_workflow', this)">Test Workflow</button>
|
|
</div>
|
|
|
|
<div class="section-label">Validation Summary</div>
|
|
<div class="controls">
|
|
<button onclick="load('validation-summary', mockValidValid, 'validate_node', this)">Valid Node</button>
|
|
<button onclick="load('validation-summary', mockValidInvalid, 'validate_node', this)">Invalid Node</button>
|
|
<button onclick="load('validation-summary', mockN8nValidate, 'n8n_validate_workflow', this)">Workflow (multi-node)</button>
|
|
</div>
|
|
|
|
<div class="section-label">Workflow List</div>
|
|
<div class="controls">
|
|
<button onclick="load('workflow-list', mockWorkflowList, 'n8n_list_workflows', this)">Workflow List</button>
|
|
<button onclick="load('workflow-list', mockWorkflowListEmpty, 'n8n_list_workflows', this)">Empty List</button>
|
|
</div>
|
|
|
|
<div class="section-label">Execution History</div>
|
|
<div class="controls">
|
|
<button onclick="load('execution-history', mockExecutions, 'n8n_executions', this)">Executions</button>
|
|
<button onclick="load('execution-history', mockExecutionsEmpty, 'n8n_executions', this)">Empty</button>
|
|
</div>
|
|
|
|
<div class="section-label">Health Dashboard</div>
|
|
<div class="controls">
|
|
<button onclick="load('health-dashboard', mockHealthOk, 'n8n_health_check', this)">Healthy</button>
|
|
<button onclick="load('health-dashboard', mockHealthOutdated, 'n8n_health_check', this)">Outdated</button>
|
|
<button onclick="load('health-dashboard', mockHealthError, 'n8n_health_check', this)">Error</button>
|
|
</div>
|
|
|
|
<div class="preview-frame">
|
|
<iframe id="app"></iframe>
|
|
</div>
|
|
<div class="info">This preview simulates the Claude host postMessage protocol to push mock tool result data into the MCP App iframe.</div>
|
|
|
|
<script>
|
|
const iframe = document.getElementById('app');
|
|
let pendingData = null;
|
|
let pendingToolName = null;
|
|
let currentTheme = 'dark';
|
|
|
|
// --- Theme ---
|
|
function setTheme(theme) {
|
|
currentTheme = theme;
|
|
document.body.className = theme;
|
|
document.getElementById('btn-dark').classList.toggle('active', theme === 'dark');
|
|
document.getElementById('btn-light').classList.toggle('active', theme === 'light');
|
|
// Reload current iframe to re-initialize with new theme
|
|
if (iframe.src) iframe.src = iframe.src;
|
|
}
|
|
|
|
// --- Mock Data: Operation Result ---
|
|
const mockCreateSuccess = {
|
|
success: true,
|
|
data: { id: 'abc123XYZ', name: 'Webhook with Set Node', active: false, nodeCount: 2 },
|
|
message: 'Workflow "Webhook with Set Node" created successfully.'
|
|
};
|
|
|
|
const mockCreateError = {
|
|
success: false,
|
|
error: 'Node type format error: n8n API requires FULL form node types',
|
|
details: { errors: ['Node 0 ("HTTP Request") uses SHORT form "nodes-base.httpRequest". Change to "n8n-nodes-base.httpRequest"'] }
|
|
};
|
|
|
|
const mockDelete = {
|
|
success: true,
|
|
data: { id: 'wf_456', name: 'Old Workflow', deleted: true },
|
|
message: 'Workflow "Old Workflow" deleted successfully.'
|
|
};
|
|
|
|
const mockPartialUpdate = {
|
|
success: true,
|
|
data: { id: 'wf_789', name: 'My API Workflow', active: true, nodeCount: 5, operationsApplied: 3 },
|
|
message: 'Workflow "My API Workflow" updated successfully.',
|
|
details: {
|
|
applied: ['add_node:Set', 'modify_node:HTTP Request', 'add_connection:Set->HTTP Request'],
|
|
failed: [],
|
|
warnings: ['Node "Set" has deprecated property "keepOnlySet"']
|
|
}
|
|
};
|
|
|
|
const mockFullUpdate = {
|
|
success: true,
|
|
data: { id: 'wf_101', name: 'Updated Workflow', active: true, nodeCount: 8 },
|
|
message: 'Workflow updated.'
|
|
};
|
|
|
|
const mockAutofix = {
|
|
success: true,
|
|
data: {
|
|
id: 'wf_auto', name: 'Fixed Workflow', nodeCount: 4, fixesApplied: 3,
|
|
fixes: [
|
|
{ description: 'Changed node type from short to full form', confidence: 'HIGH' },
|
|
{ description: 'Added missing authentication parameter', confidence: 'HIGH' },
|
|
{ description: 'Replaced deprecated property channelId with channel', confidence: 'MEDIUM' }
|
|
]
|
|
}
|
|
};
|
|
|
|
const mockAutofixPreview = {
|
|
success: true,
|
|
data: {
|
|
id: 'wf_preview', name: 'Preview Workflow', nodeCount: 3, fixesApplied: 2, preview: true,
|
|
fixes: [
|
|
{ description: 'Would change node type from short to full form', confidence: 'HIGH' },
|
|
{ description: 'Would add missing required field "resource"', confidence: 'MEDIUM' }
|
|
]
|
|
}
|
|
};
|
|
|
|
const mockDeploy = {
|
|
success: true,
|
|
data: {
|
|
id: 'wf_deployed', name: 'Email Notification Flow', active: false, nodeCount: 5,
|
|
templateId: 1234, triggerType: 'webhook',
|
|
requiredCredentials: ['gmailOAuth2Api (Gmail node)', 'slackOAuth2Api (Slack node)'],
|
|
autoFixStatus: 'success'
|
|
}
|
|
};
|
|
|
|
const mockTest = {
|
|
success: true,
|
|
data: {
|
|
id: 'wf_test', name: 'Test Workflow', executionId: 'exec_98765',
|
|
triggerType: 'manual'
|
|
}
|
|
};
|
|
|
|
// --- Mock Data: Validation Summary ---
|
|
const mockValidValid = {
|
|
nodeType: 'n8n-nodes-base.httpRequest',
|
|
displayName: 'HTTP Request',
|
|
valid: true,
|
|
errors: [],
|
|
warnings: [],
|
|
suggestions: ['Consider adding error handling with an Error Trigger node'],
|
|
summary: { hasErrors: false, errorCount: 0, warningCount: 0, suggestionCount: 1 }
|
|
};
|
|
|
|
const mockValidInvalid = {
|
|
nodeType: 'n8n-nodes-base.slack',
|
|
displayName: 'Slack',
|
|
valid: false,
|
|
errors: [
|
|
{ type: 'missing_required', property: 'authentication', message: 'Required field "authentication" is missing', fix: 'Set authentication to "oAuth2" or "accessToken"' },
|
|
{ type: 'missing_required', property: 'resource', message: 'Required field "resource" is missing', fix: 'Set resource to "channel", "message", "user", etc.' }
|
|
],
|
|
warnings: [
|
|
{ type: 'deprecated_property', property: 'channelId', message: 'Property "channelId" is deprecated, use "channel" instead' }
|
|
],
|
|
suggestions: ['Use OAuth2 authentication for production workflows', 'Add error handling for rate limits'],
|
|
summary: { hasErrors: true, errorCount: 2, warningCount: 1, suggestionCount: 2 }
|
|
};
|
|
|
|
const mockN8nValidate = {
|
|
success: true,
|
|
data: {
|
|
valid: false,
|
|
workflowId: 'wf_abc',
|
|
workflowName: 'My Production Workflow',
|
|
errors: [
|
|
{ node: 'HTTP Request', message: 'Missing URL parameter', property: 'url', fix: 'Set the url field' },
|
|
{ node: 'HTTP Request', message: 'Invalid method "PATCH" for this endpoint', property: 'method' },
|
|
{ node: 'Slack', message: 'Missing authentication configuration', property: 'authentication', fix: 'Set authentication to oAuth2' }
|
|
],
|
|
warnings: [
|
|
{ node: 'Set', message: 'Unused output field "oldField"', property: 'oldField' },
|
|
{ node: 'HTTP Request', message: 'Consider using retry on failure', property: 'options.retry' }
|
|
],
|
|
suggestions: ['Add error handling between HTTP Request and Set nodes'],
|
|
summary: { errorCount: 3, warningCount: 2, totalNodes: 4, enabledNodes: 4 }
|
|
}
|
|
};
|
|
|
|
// --- Mock Data: Workflow List ---
|
|
const mockWorkflowList = {
|
|
success: true,
|
|
data: {
|
|
workflows: [
|
|
{ id: 'wf_001', name: 'Customer Onboarding', active: true, nodeCount: 12, tags: ['production', 'crm'], updatedAt: '2026-02-07T14:30:00Z' },
|
|
{ id: 'wf_002', name: 'Slack Notifications', active: true, nodeCount: 5, tags: ['notifications'], updatedAt: '2026-02-06T09:15:00Z' },
|
|
{ id: 'wf_003', name: 'Data Backup (weekly)', active: false, nodeCount: 8, tags: ['maintenance', 'backup'], updatedAt: '2026-01-28T22:00:00Z' },
|
|
{ id: 'wf_004', name: 'Invoice Processing', active: true, nodeCount: 15, tags: ['finance', 'production', 'critical'], updatedAt: '2026-02-08T01:00:00Z' },
|
|
{ id: 'wf_005', name: 'Old Integration Test', active: false, isArchived: true, nodeCount: 3, tags: [], updatedAt: '2025-11-01T10:00:00Z' },
|
|
{ id: 'wf_006', name: 'Email Campaign Drip', active: true, nodeCount: 9, tags: ['marketing'], updatedAt: '2026-02-05T16:45:00Z' },
|
|
],
|
|
returned: 6,
|
|
hasMore: true,
|
|
nextCursor: 'cursor_abc'
|
|
}
|
|
};
|
|
|
|
const mockWorkflowListEmpty = {
|
|
success: true,
|
|
data: { workflows: [], returned: 0, hasMore: false }
|
|
};
|
|
|
|
// --- Mock Data: Execution History ---
|
|
const mockExecutions = {
|
|
success: true,
|
|
data: {
|
|
executions: [
|
|
{ id: 'exec_001', workflowName: 'Customer Onboarding', status: 'success', startedAt: '2026-02-08T10:30:00Z', stoppedAt: '2026-02-08T10:30:02Z', mode: 'webhook' },
|
|
{ id: 'exec_002', workflowName: 'Slack Notifications', status: 'success', startedAt: '2026-02-08T10:28:00Z', stoppedAt: '2026-02-08T10:28:01Z', mode: 'trigger' },
|
|
{ id: 'exec_003', workflowName: 'Invoice Processing', status: 'error', startedAt: '2026-02-08T10:25:00Z', stoppedAt: '2026-02-08T10:25:15Z', mode: 'webhook' },
|
|
{ id: 'exec_004', workflowName: 'Customer Onboarding', status: 'success', startedAt: '2026-02-08T10:20:00Z', stoppedAt: '2026-02-08T10:20:03Z', mode: 'webhook' },
|
|
{ id: 'exec_005', workflowName: 'Email Campaign Drip', status: 'waiting', startedAt: '2026-02-08T10:15:00Z', mode: 'manual' },
|
|
{ id: 'exec_006', workflowName: 'Data Backup', status: 'success', startedAt: '2026-02-08T09:00:00Z', stoppedAt: '2026-02-08T09:02:30Z', mode: 'cron' },
|
|
{ id: 'exec_007', workflowName: 'Invoice Processing', status: 'error', startedAt: '2026-02-07T23:00:00Z', stoppedAt: '2026-02-07T23:00:08Z', mode: 'webhook' },
|
|
],
|
|
returned: 7,
|
|
hasMore: true
|
|
}
|
|
};
|
|
|
|
const mockExecutionsEmpty = {
|
|
success: true,
|
|
data: { executions: [], returned: 0, hasMore: false }
|
|
};
|
|
|
|
// --- Mock Data: Health Dashboard ---
|
|
const mockHealthOk = {
|
|
success: true,
|
|
data: {
|
|
status: 'connected',
|
|
instanceId: 'inst_abc123',
|
|
n8nVersion: '1.72.1',
|
|
mcpVersion: '2.24.1',
|
|
apiUrl: 'https://n8n.example.com/api/v1',
|
|
versionCheck: { current: '1.72.1', latest: '1.72.1', upToDate: true },
|
|
performance: { responseTimeMs: 142, cacheHitRate: 0.87 },
|
|
nextSteps: ['Your n8n instance is up to date', 'Try creating a workflow with n8n_create_workflow']
|
|
}
|
|
};
|
|
|
|
const mockHealthOutdated = {
|
|
success: true,
|
|
data: {
|
|
status: 'connected',
|
|
instanceId: 'inst_xyz789',
|
|
n8nVersion: '1.68.0',
|
|
mcpVersion: '2.24.1',
|
|
apiUrl: 'https://n8n.company.io/api/v1',
|
|
versionCheck: { current: '1.68.0', latest: '1.72.1', upToDate: false, updateCommand: 'npm update n8n -g' },
|
|
performance: { responseTimeMs: 1850, cacheHitRate: 0.45 },
|
|
nextSteps: ['Update n8n to version 1.72.1 for latest features and fixes', 'Cache hit rate is low - consider warming the cache']
|
|
}
|
|
};
|
|
|
|
const mockHealthError = {
|
|
success: false,
|
|
error: 'Connection failed: Unable to reach n8n instance at https://n8n.offline.com/api/v1'
|
|
};
|
|
|
|
// --- Host Protocol ---
|
|
window.addEventListener('message', (event) => {
|
|
if (!event.data || typeof event.data !== 'object') return;
|
|
const msg = event.data;
|
|
if (!msg.jsonrpc) return;
|
|
|
|
if (msg.jsonrpc === '2.0' && msg.method === 'ui/initialize' && msg.id != null) {
|
|
iframe.contentWindow.postMessage({
|
|
jsonrpc: '2.0',
|
|
id: msg.id,
|
|
result: {
|
|
protocolVersion: '2026-01-26',
|
|
hostCapabilities: {},
|
|
hostInfo: { name: 'Local Preview', version: '1.0.0' },
|
|
hostContext: {
|
|
theme: currentTheme,
|
|
toolInfo: pendingToolName ? { tool: { name: pendingToolName, inputSchema: { type: 'object', properties: {} } } } : undefined,
|
|
}
|
|
}
|
|
}, '*');
|
|
|
|
setTimeout(() => {
|
|
if (pendingData) {
|
|
iframe.contentWindow.postMessage({
|
|
jsonrpc: '2.0',
|
|
method: 'ui/notifications/tool-result',
|
|
params: {
|
|
content: [{ type: 'text', text: JSON.stringify(pendingData) }]
|
|
}
|
|
}, '*');
|
|
}
|
|
}, 500);
|
|
}
|
|
// Respond to any other JSON-RPC request (with id) with empty result
|
|
else if (msg.jsonrpc === '2.0' && msg.id != null && msg.method) {
|
|
iframe.contentWindow.postMessage({
|
|
jsonrpc: '2.0',
|
|
id: msg.id,
|
|
result: {}
|
|
}, '*');
|
|
}
|
|
});
|
|
|
|
function load(appName, data, toolName, btn) {
|
|
pendingData = data;
|
|
pendingToolName = toolName || null;
|
|
document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));
|
|
if (btn) btn.classList.add('active');
|
|
iframe.src = `dist/${appName}/index.html`;
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
load('operation-result', mockCreateSuccess, 'n8n_create_workflow', document.querySelector('.controls button'));
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|