Files
n8n-mcp/ui-apps/src/apps/operation-result/App.tsx
Romuald Członkowski 1f45cc6dcc 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>
2026-02-07 04:11:21 +01:00

85 lines
3.2 KiB
TypeScript

import React from 'react';
import '@shared/styles/theme.css';
import { Card, Badge, Expandable } from '@shared/components';
import { useToolData } from '@shared/hooks/useToolData';
import type { OperationResultData } from '@shared/types';
export default function App() {
const data = useToolData<OperationResultData>();
if (!data) {
return <div style={{ padding: '16px', color: 'var(--n8n-text-muted)' }}>Loading...</div>;
}
const isSuccess = data.status === 'success';
return (
<div style={{ maxWidth: '480px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
<Badge variant={isSuccess ? 'success' : 'error'}>
{isSuccess ? 'Success' : 'Error'}
</Badge>
<h2 style={{ fontSize: '16px', fontWeight: 600 }}>{data.operation}</h2>
</div>
<Card title="Workflow">
<div style={{ fontSize: '14px' }}>
{data.workflowName && <div><strong>Name:</strong> {data.workflowName}</div>}
{data.workflowId && <div><strong>ID:</strong> {data.workflowId}</div>}
{data.timestamp && (
<div style={{ color: 'var(--n8n-text-muted)', fontSize: '12px', marginTop: '4px' }}>
{data.timestamp}
</div>
)}
</div>
</Card>
{data.message && (
<Card>
<div style={{ fontSize: '13px' }}>{data.message}</div>
</Card>
)}
{data.changes && (
<>
{data.changes.nodesAdded && data.changes.nodesAdded.length > 0 && (
<Expandable title="Nodes Added" count={data.changes.nodesAdded.length} defaultOpen>
<ul style={{ listStyle: 'none', fontSize: '13px' }}>
{data.changes.nodesAdded.map((node, i) => (
<li key={i} style={{ padding: '4px 0', borderBottom: '1px solid var(--n8n-border)' }}>
<span style={{ color: 'var(--n8n-success)' }}>+</span> {node}
</li>
))}
</ul>
</Expandable>
)}
{data.changes.nodesModified && data.changes.nodesModified.length > 0 && (
<Expandable title="Nodes Modified" count={data.changes.nodesModified.length}>
<ul style={{ listStyle: 'none', fontSize: '13px' }}>
{data.changes.nodesModified.map((node, i) => (
<li key={i} style={{ padding: '4px 0', borderBottom: '1px solid var(--n8n-border)' }}>
<span style={{ color: 'var(--n8n-warning)' }}>~</span> {node}
</li>
))}
</ul>
</Expandable>
)}
{data.changes.nodesRemoved && data.changes.nodesRemoved.length > 0 && (
<Expandable title="Nodes Removed" count={data.changes.nodesRemoved.length}>
<ul style={{ listStyle: 'none', fontSize: '13px' }}>
{data.changes.nodesRemoved.map((node, i) => (
<li key={i} style={{ padding: '4px 0', borderBottom: '1px solid var(--n8n-border)' }}>
<span style={{ color: 'var(--n8n-error)' }}>-</span> {node}
</li>
))}
</ul>
</Expandable>
)}
</>
)}
</div>
);
}