mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-09 06:43:08 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c601581714 | ||
|
|
020bc3d43d | ||
|
|
a57b400bd0 |
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.34.5] - 2026-02-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
## [2.34.3] - 2026-02-07
|
## [2.34.3] - 2026-02-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.34.3",
|
"version": "2.34.5",
|
||||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -5,13 +5,26 @@ import { useToolData } from '@shared/hooks/useToolData';
|
|||||||
import type { OperationResultData } from '@shared/types';
|
import type { OperationResultData } from '@shared/types';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const data = useToolData<OperationResultData>();
|
const { data, error, isConnected } = useToolData<OperationResultData>();
|
||||||
|
|
||||||
if (!data) {
|
if (error) {
|
||||||
return <div style={{ padding: '16px', color: 'var(--n8n-text-muted)' }}>Loading...</div>;
|
return <div style={{ padding: '16px', color: '#ef4444' }}>Error: {error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSuccess = data.status === 'success';
|
if (!isConnected) {
|
||||||
|
return <div style={{ padding: '16px', color: '#9ca3af' }}>Connecting...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <div style={{ padding: '16px', color: '#9ca3af' }}>Waiting for data...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div style={{ maxWidth: '480px' }}>
|
<div style={{ maxWidth: '480px' }}>
|
||||||
@@ -19,65 +32,34 @@ export default function App() {
|
|||||||
<Badge variant={isSuccess ? 'success' : 'error'}>
|
<Badge variant={isSuccess ? 'success' : 'error'}>
|
||||||
{isSuccess ? 'Success' : 'Error'}
|
{isSuccess ? 'Success' : 'Error'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<h2 style={{ fontSize: '16px', fontWeight: 600 }}>{data.operation}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card title="Workflow">
|
{(workflowName || workflowId) && (
|
||||||
<div style={{ fontSize: '14px' }}>
|
<Card title="Workflow">
|
||||||
{data.workflowName && <div><strong>Name:</strong> {data.workflowName}</div>}
|
<div style={{ fontSize: '14px' }}>
|
||||||
{data.workflowId && <div><strong>ID:</strong> {data.workflowId}</div>}
|
{workflowName && <div><strong>Name:</strong> {workflowName}</div>}
|
||||||
{data.timestamp && (
|
{workflowId && <div><strong>ID:</strong> {workflowId}</div>}
|
||||||
<div style={{ color: 'var(--n8n-text-muted)', fontSize: '12px', marginTop: '4px' }}>
|
{nodeCount !== undefined && <div><strong>Nodes:</strong> {nodeCount}</div>}
|
||||||
{data.timestamp}
|
{isDeleted && <div style={{ color: 'var(--n8n-warning)', marginTop: '4px' }}>Deleted</div>}
|
||||||
</div>
|
{operationsApplied !== undefined && (
|
||||||
)}
|
<div><strong>Operations applied:</strong> {operationsApplied}</div>
|
||||||
</div>
|
)}
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
{data.message && (
|
|
||||||
<Card>
|
|
||||||
<div style={{ fontSize: '13px' }}>{data.message}</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.changes && (
|
{(data.message || data.error) && (
|
||||||
<>
|
<Card>
|
||||||
{data.changes.nodesAdded && data.changes.nodesAdded.length > 0 && (
|
<div style={{ fontSize: '13px' }}>{data.message || data.error}</div>
|
||||||
<Expandable title="Nodes Added" count={data.changes.nodesAdded.length} defaultOpen>
|
</Card>
|
||||||
<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 && (
|
{data.details && (
|
||||||
<Expandable title="Nodes Modified" count={data.changes.nodesModified.length}>
|
<Expandable title="Details">
|
||||||
<ul style={{ listStyle: 'none', fontSize: '13px' }}>
|
<pre style={{ fontSize: '11px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
{data.changes.nodesModified.map((node, i) => (
|
{JSON.stringify(data.details, null, 2)}
|
||||||
<li key={i} style={{ padding: '4px 0', borderBottom: '1px solid var(--n8n-border)' }}>
|
</pre>
|
||||||
<span style={{ color: 'var(--n8n-warning)' }}>~</span> {node}
|
</Expandable>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,40 +2,59 @@ import React from 'react';
|
|||||||
import '@shared/styles/theme.css';
|
import '@shared/styles/theme.css';
|
||||||
import { Card, Badge, Expandable } from '@shared/components';
|
import { Card, Badge, Expandable } from '@shared/components';
|
||||||
import { useToolData } from '@shared/hooks/useToolData';
|
import { useToolData } from '@shared/hooks/useToolData';
|
||||||
import type { ValidationSummaryData } from '@shared/types';
|
import type { ValidationSummaryData, ValidationError, ValidationWarning } from '@shared/types';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const data = useToolData<ValidationSummaryData>();
|
const { data: raw, error, isConnected } = useToolData<ValidationSummaryData>();
|
||||||
|
|
||||||
if (!data) {
|
if (error) {
|
||||||
return <div style={{ padding: '16px', color: 'var(--n8n-text-muted)' }}>Loading...</div>;
|
return <div style={{ padding: '16px', color: '#ef4444' }}>Error: {error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return <div style={{ padding: '16px', color: '#9ca3af' }}>Connecting...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div style={{ maxWidth: '480px' }}>
|
<div style={{ maxWidth: '480px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
|
||||||
<Badge variant={data.valid ? 'success' : 'error'}>
|
<Badge variant={valid ? 'success' : 'error'}>
|
||||||
{data.valid ? 'Valid' : 'Invalid'}
|
{valid ? 'Valid' : 'Invalid'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{data.displayName && (
|
{displayName && (
|
||||||
<span style={{ fontSize: '14px', color: 'var(--n8n-text-muted)' }}>{data.displayName}</span>
|
<span style={{ fontSize: '14px', color: 'var(--n8n-text-muted)' }}>{displayName}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<div style={{ display: 'flex', gap: '16px', fontSize: '13px' }}>
|
<div style={{ display: 'flex', gap: '16px', fontSize: '13px' }}>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: 'var(--n8n-error)' }}>{data.errorCount}</span> errors
|
<span style={{ color: 'var(--n8n-error)' }}>{errorCount}</span> errors
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: 'var(--n8n-warning)' }}>{data.warningCount}</span> warnings
|
<span style={{ color: 'var(--n8n-warning)' }}>{warningCount}</span> warnings
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{data.errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<Expandable title="Errors" count={data.errors.length} defaultOpen>
|
<Expandable title="Errors" count={errors.length} defaultOpen>
|
||||||
{data.errors.map((err, i) => (
|
{errors.map((err, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
marginBottom: '6px',
|
marginBottom: '6px',
|
||||||
@@ -44,7 +63,9 @@ export default function App() {
|
|||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: 'var(--n8n-error)',
|
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>}
|
{err.property && <div style={{ opacity: 0.8 }}>Property: {err.property}</div>}
|
||||||
<div>{err.message}</div>
|
<div>{err.message}</div>
|
||||||
{err.fix && (
|
{err.fix && (
|
||||||
@@ -55,9 +76,9 @@ export default function App() {
|
|||||||
</Expandable>
|
</Expandable>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.warnings.length > 0 && (
|
{warnings.length > 0 && (
|
||||||
<Expandable title="Warnings" count={data.warnings.length}>
|
<Expandable title="Warnings" count={warnings.length}>
|
||||||
{data.warnings.map((warn, i) => (
|
{warnings.map((warn, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
marginBottom: '6px',
|
marginBottom: '6px',
|
||||||
@@ -66,7 +87,9 @@ export default function App() {
|
|||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: 'var(--n8n-warning)',
|
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>}
|
{warn.property && <div style={{ opacity: 0.8 }}>Property: {warn.property}</div>}
|
||||||
<div>{warn.message}</div>
|
<div>{warn.message}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,10 +97,10 @@ export default function App() {
|
|||||||
</Expandable>
|
</Expandable>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.suggestions && data.suggestions.length > 0 && (
|
{suggestions.length > 0 && (
|
||||||
<Expandable title="Suggestions" count={data.suggestions.length}>
|
<Expandable title="Suggestions" count={suggestions.length}>
|
||||||
<ul style={{ paddingLeft: '16px', fontSize: '12px' }}>
|
<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>
|
<li key={i} style={{ padding: '2px 0', color: 'var(--n8n-info)' }}>{suggestion}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { App } from '@modelcontextprotocol/ext-apps';
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
|
||||||
export function useToolData<T>(): T | null {
|
interface UseToolDataResult<T> {
|
||||||
|
data: T | null;
|
||||||
|
error: string | null;
|
||||||
|
isConnected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToolData<T>(): UseToolDataResult<T> {
|
||||||
const [data, setData] = useState<T | null>(null);
|
const [data, setData] = useState<T | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const onAppCreated = useCallback((app: any) => {
|
||||||
const app = new App();
|
|
||||||
|
|
||||||
app.ontoolresult = (result: any) => {
|
app.ontoolresult = (result: any) => {
|
||||||
// The host pushes tool result content; parse the first text item as JSON
|
|
||||||
if (result?.content) {
|
if (result?.content) {
|
||||||
const textItem = Array.isArray(result.content)
|
const textItem = Array.isArray(result.content)
|
||||||
? result.content.find((c: any) => c.type === 'text')
|
? result.content.find((c: any) => c.type === 'text')
|
||||||
@@ -17,19 +20,22 @@ export function useToolData<T>(): T | null {
|
|||||||
try {
|
try {
|
||||||
setData(JSON.parse(textItem.text) as T);
|
setData(JSON.parse(textItem.text) as T);
|
||||||
} catch {
|
} catch {
|
||||||
// Not JSON — use raw text as-is
|
|
||||||
setData(textItem.text as unknown as T);
|
setData(textItem.text as unknown as T);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.connect();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
app.close();
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return data;
|
const { isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'n8n-mcp-ui', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error: error?.message ?? null,
|
||||||
|
isConnected,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,67 @@
|
|||||||
|
// Matches the McpToolResponse format from handlers-n8n-manager.ts
|
||||||
export interface OperationResultData {
|
export interface OperationResultData {
|
||||||
status: 'success' | 'error';
|
success: boolean;
|
||||||
operation: string;
|
data?: {
|
||||||
workflowName?: string;
|
id?: string;
|
||||||
workflowId?: string;
|
name?: string;
|
||||||
timestamp?: string;
|
active?: boolean;
|
||||||
message?: string;
|
nodeCount?: number;
|
||||||
changes?: {
|
workflowId?: string;
|
||||||
nodesAdded?: string[];
|
workflowName?: string;
|
||||||
nodesModified?: string[];
|
deleted?: boolean;
|
||||||
nodesRemoved?: string[];
|
operationsApplied?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
details?: Record<string, unknown>;
|
details?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationError {
|
export interface ValidationError {
|
||||||
type: string;
|
type?: string;
|
||||||
property?: string;
|
property?: string;
|
||||||
message: string;
|
message: string;
|
||||||
fix?: string;
|
fix?: string;
|
||||||
|
node?: string;
|
||||||
|
details?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationWarning {
|
export interface ValidationWarning {
|
||||||
type: string;
|
type?: string;
|
||||||
property?: string;
|
property?: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
node?: string;
|
||||||
|
details?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matches the validate_node / validate_workflow response format from server.ts
|
||||||
export interface ValidationSummaryData {
|
export interface ValidationSummaryData {
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
errorCount: number;
|
nodeType?: string;
|
||||||
warningCount: number;
|
displayName?: string;
|
||||||
errors: ValidationError[];
|
errors: ValidationError[];
|
||||||
warnings: ValidationWarning[];
|
warnings: ValidationWarning[];
|
||||||
suggestions?: string[];
|
suggestions?: string[];
|
||||||
nodeType?: string;
|
summary?: {
|
||||||
displayName?: string;
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user