fix: MCP App UI - use official ext-apps hook + align types with server responses (#579)

* fix: use official ext-apps useApp hook to fix blank MCP App rendering

The custom useToolData hook had lifecycle issues that prevented the UI
from rendering in Claude Desktop/web: no appInfo in App constructor,
unhandled connect() Promise, app.close() on unmount conflicting with
React Strict Mode. Switched to the official useApp hook from
@modelcontextprotocol/ext-apps/react which handles initialization
handshake, handler registration, and cleanup correctly.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: align MCP App UI types with actual server response format

- useToolData hook now uses official useApp from ext-apps/react
- OperationResultData uses success:boolean + data.id/name (matching
  McpToolResponse from handlers-n8n-manager.ts)
- ValidationSummaryData handles both direct results (validate_node,
  validate_workflow) and wrapped results (n8n_validate_workflow)
- Added visible error/connection states for debugging

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>
This commit is contained in:
Romuald Członkowski
2026-02-08 02:08:50 +01:00
committed by GitHub
parent a57b400bd0
commit 020bc3d43d
4 changed files with 109 additions and 95 deletions

View File

@@ -19,7 +19,12 @@ export default function App() {
return <div style={{ padding: '16px', color: '#9ca3af' }}>Waiting for data...</div>;
}
const isSuccess = data.status === 'success';
const isSuccess = data.success === true;
const workflowName = data.data?.name || data.data?.workflowName;
const workflowId = data.data?.id || data.data?.workflowId;
const nodeCount = data.data?.nodeCount;
const isDeleted = data.data?.deleted === true;
const operationsApplied = data.data?.operationsApplied;
return (
<div style={{ maxWidth: '480px' }}>
@@ -27,65 +32,34 @@ export default function App() {
<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>
{(workflowName || workflowId) && (
<Card title="Workflow">
<div style={{ fontSize: '14px' }}>
{workflowName && <div><strong>Name:</strong> {workflowName}</div>}
{workflowId && <div><strong>ID:</strong> {workflowId}</div>}
{nodeCount !== undefined && <div><strong>Nodes:</strong> {nodeCount}</div>}
{isDeleted && <div style={{ color: 'var(--n8n-warning)', marginTop: '4px' }}>Deleted</div>}
{operationsApplied !== undefined && (
<div><strong>Operations applied:</strong> {operationsApplied}</div>
)}
</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.message || data.error) && (
<Card>
<div style={{ fontSize: '13px' }}>{data.message || data.error}</div>
</Card>
)}
{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>
)}
</>
{data.details && (
<Expandable title="Details">
<pre style={{ fontSize: '11px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{JSON.stringify(data.details, null, 2)}
</pre>
</Expandable>
)}
</div>
);