Compare commits

..

4 Commits

Author SHA1 Message Date
Romuald Członkowski
c601581714 Fix/mcp app blank UI (#580)
* 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>

* chore: bump version to 2.34.5 for npm publish

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 03:23:16 +01:00
Romuald Członkowski
020bc3d43d 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>
2026-02-08 02:08:50 +01:00
Romuald Członkowski
a57b400bd0 fix: use official ext-apps useApp hook to fix blank MCP App rendering (#578)
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>
2026-02-07 16:25:27 +01:00
Romuald Członkowski
38aa70261a fix: use text/html;profile=mcp-app MIME type for MCP Apps resources (#577)
The ext-apps spec requires RESOURCE_MIME_TYPE (text/html;profile=mcp-app)
for hosts to recognize resources as MCP Apps. Without the profile parameter,
Claude Desktop/web fails with "Failed to load MCP App: the resource may
exceed the 5 MB size limit."

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:18:50 +01:00
5 changed files with 111 additions and 97 deletions

View File

@@ -7,15 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.34.4] - 2026-02-07
## [2.34.5] - 2026-02-08
### Fixed
- **MCP Apps: Fix blank UI rendering in Claude**: Rewrote `useToolData` hook to use the official `useApp` hook from `@modelcontextprotocol/ext-apps/react` instead of manually managing `App` lifecycle
- Proper initialization handshake with host via `appInfo` and `capabilities`
- Handlers registered via `onAppCreated` callback (before `connect()`) to avoid race conditions
- Removed `app.close()` on unmount which caused issues with React Strict Mode
- Added visible error and connection states with inline colors for debugging
- **MCP Apps: Fix blank UI and wrong status badge in Claude**: Rewrote `useToolData` hook to use the official `useApp` hook from `@modelcontextprotocol/ext-apps/react` for proper lifecycle management. Updated UI types and components to match actual server response format (`success: boolean` instead of `status: string`, nested `data` object for workflow details). Validation summary now handles both direct and wrapped (`n8n_validate_workflow`) response shapes.
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.34.4",
"version": "2.34.5",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",

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>
);

View File

@@ -2,10 +2,10 @@ import React from 'react';
import '@shared/styles/theme.css';
import { Card, Badge, Expandable } from '@shared/components';
import { useToolData } from '@shared/hooks/useToolData';
import type { ValidationSummaryData } from '@shared/types';
import type { ValidationSummaryData, ValidationError, ValidationWarning } from '@shared/types';
export default function App() {
const { data, error, isConnected } = useToolData<ValidationSummaryData>();
const { data: raw, error, isConnected } = useToolData<ValidationSummaryData>();
if (error) {
return <div style={{ padding: '16px', color: '#ef4444' }}>Error: {error}</div>;
@@ -15,35 +15,46 @@ export default function App() {
return <div style={{ padding: '16px', color: '#9ca3af' }}>Connecting...</div>;
}
if (!data) {
if (!raw) {
return <div style={{ padding: '16px', color: '#9ca3af' }}>Waiting for data...</div>;
}
// n8n_validate_workflow wraps result in { success, data: {...} }
// validate_node and validate_workflow return data directly
const inner = raw.data || raw;
const valid = inner.valid ?? raw.valid ?? false;
const displayName = raw.displayName || raw.data?.workflowName;
const errors: ValidationError[] = inner.errors || raw.errors || [];
const warnings: ValidationWarning[] = inner.warnings || raw.warnings || [];
const suggestions: string[] = inner.suggestions || raw.suggestions || [];
const errorCount = raw.summary?.errorCount ?? inner.summary?.errorCount ?? errors.length;
const warningCount = raw.summary?.warningCount ?? inner.summary?.warningCount ?? warnings.length;
return (
<div style={{ maxWidth: '480px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
<Badge variant={data.valid ? 'success' : 'error'}>
{data.valid ? 'Valid' : 'Invalid'}
<Badge variant={valid ? 'success' : 'error'}>
{valid ? 'Valid' : 'Invalid'}
</Badge>
{data.displayName && (
<span style={{ fontSize: '14px', color: 'var(--n8n-text-muted)' }}>{data.displayName}</span>
{displayName && (
<span style={{ fontSize: '14px', color: 'var(--n8n-text-muted)' }}>{displayName}</span>
)}
</div>
<Card>
<div style={{ display: 'flex', gap: '16px', fontSize: '13px' }}>
<div>
<span style={{ color: 'var(--n8n-error)' }}>{data.errorCount}</span> errors
<span style={{ color: 'var(--n8n-error)' }}>{errorCount}</span> errors
</div>
<div>
<span style={{ color: 'var(--n8n-warning)' }}>{data.warningCount}</span> warnings
<span style={{ color: 'var(--n8n-warning)' }}>{warningCount}</span> warnings
</div>
</div>
</Card>
{data.errors.length > 0 && (
<Expandable title="Errors" count={data.errors.length} defaultOpen>
{data.errors.map((err, i) => (
{errors.length > 0 && (
<Expandable title="Errors" count={errors.length} defaultOpen>
{errors.map((err, i) => (
<div key={i} style={{
padding: '8px',
marginBottom: '6px',
@@ -52,7 +63,9 @@ export default function App() {
fontSize: '12px',
color: 'var(--n8n-error)',
}}>
<div style={{ fontWeight: 600 }}>{err.type}</div>
{(err.type || err.node) && (
<div style={{ fontWeight: 600 }}>{err.type || err.node}</div>
)}
{err.property && <div style={{ opacity: 0.8 }}>Property: {err.property}</div>}
<div>{err.message}</div>
{err.fix && (
@@ -63,9 +76,9 @@ export default function App() {
</Expandable>
)}
{data.warnings.length > 0 && (
<Expandable title="Warnings" count={data.warnings.length}>
{data.warnings.map((warn, i) => (
{warnings.length > 0 && (
<Expandable title="Warnings" count={warnings.length}>
{warnings.map((warn, i) => (
<div key={i} style={{
padding: '8px',
marginBottom: '6px',
@@ -74,7 +87,9 @@ export default function App() {
fontSize: '12px',
color: 'var(--n8n-warning)',
}}>
<div style={{ fontWeight: 600 }}>{warn.type}</div>
{(warn.type || warn.node) && (
<div style={{ fontWeight: 600 }}>{warn.type || warn.node}</div>
)}
{warn.property && <div style={{ opacity: 0.8 }}>Property: {warn.property}</div>}
<div>{warn.message}</div>
</div>
@@ -82,10 +97,10 @@ export default function App() {
</Expandable>
)}
{data.suggestions && data.suggestions.length > 0 && (
<Expandable title="Suggestions" count={data.suggestions.length}>
{suggestions.length > 0 && (
<Expandable title="Suggestions" count={suggestions.length}>
<ul style={{ paddingLeft: '16px', fontSize: '12px' }}>
{data.suggestions.map((suggestion, i) => (
{suggestions.map((suggestion, i) => (
<li key={i} style={{ padding: '2px 0', color: 'var(--n8n-info)' }}>{suggestion}</li>
))}
</ul>

View File

@@ -1,38 +1,67 @@
// Matches the McpToolResponse format from handlers-n8n-manager.ts
export interface OperationResultData {
status: 'success' | 'error';
operation: string;
workflowName?: string;
workflowId?: string;
timestamp?: string;
message?: string;
changes?: {
nodesAdded?: string[];
nodesModified?: string[];
nodesRemoved?: string[];
success: boolean;
data?: {
id?: string;
name?: string;
active?: boolean;
nodeCount?: number;
workflowId?: string;
workflowName?: string;
deleted?: boolean;
operationsApplied?: number;
[key: string]: unknown;
};
message?: string;
error?: string;
details?: Record<string, unknown>;
}
export interface ValidationError {
type: string;
type?: string;
property?: string;
message: string;
fix?: string;
node?: string;
details?: unknown;
}
export interface ValidationWarning {
type: string;
type?: string;
property?: string;
message: string;
node?: string;
details?: unknown;
}
// Matches the validate_node / validate_workflow response format from server.ts
export interface ValidationSummaryData {
valid: boolean;
errorCount: number;
warningCount: number;
nodeType?: string;
displayName?: string;
errors: ValidationError[];
warnings: ValidationWarning[];
suggestions?: string[];
nodeType?: string;
displayName?: string;
summary?: {
errorCount?: number;
warningCount?: number;
hasErrors?: boolean;
suggestionCount?: number;
[key: string]: unknown;
};
// n8n_validate_workflow wraps result in success/data
success?: boolean;
data?: {
valid?: boolean;
workflowId?: string;
workflowName?: string;
errors?: ValidationError[];
warnings?: ValidationWarning[];
suggestions?: string[];
summary?: {
errorCount?: number;
warningCount?: number;
[key: string]: unknown;
};
};
}