Compare commits

...

3 Commits

Author SHA1 Message Date
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
Romuald Członkowski
1b328d8168 fix: include UI apps build in CI release pipeline (#575)
The release workflow only ran `npm run build` (TypeScript), skipping the
UI apps build. This meant ui-apps/dist/ was missing from npm packages.

- Change `npm run build` to `npm run build:all` in build-and-verify and
  publish-npm jobs
- Copy ui-apps/dist into the npm publish directory
- Add ui-apps/dist/**/* to the published package files list
- Bump version to 2.34.2

Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 05:40:21 +01:00
8 changed files with 84 additions and 29 deletions

View File

@@ -283,8 +283,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build project - name: Build project (server + UI apps)
run: npm run build run: npm run build:all
# Database is already built and committed during development # Database is already built and committed during development
# Rebuilding here causes segfault due to memory pressure (exit code 139) # Rebuilding here causes segfault due to memory pressure (exit code 139)
@@ -322,8 +322,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build project - name: Build project (server + UI apps)
run: npm run build run: npm run build:all
# Database is already built and committed during development # Database is already built and committed during development
- name: Verify database exists - name: Verify database exists
@@ -347,6 +347,8 @@ jobs:
# Copy necessary files # Copy necessary files
cp -r dist $PUBLISH_DIR/ cp -r dist $PUBLISH_DIR/
cp -r data $PUBLISH_DIR/ cp -r data $PUBLISH_DIR/
mkdir -p $PUBLISH_DIR/ui-apps
cp -r ui-apps/dist $PUBLISH_DIR/ui-apps/
cp README.md $PUBLISH_DIR/ cp README.md $PUBLISH_DIR/
cp LICENSE $PUBLISH_DIR/ cp LICENSE $PUBLISH_DIR/
cp .env.example $PUBLISH_DIR/ cp .env.example $PUBLISH_DIR/
@@ -377,7 +379,7 @@ jobs:
pkg.license = 'MIT'; pkg.license = 'MIT';
pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' }; pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' };
pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme'; pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme';
pkg.files = ['dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE']; pkg.files = ['dist/**/*', 'ui-apps/dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE'];
delete pkg.private; delete pkg.private;
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
" "

View File

@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.34.4] - 2026-02-07
### 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
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
## [2.34.3] - 2026-02-07
### Fixed
- **MCP Apps: Use correct MIME type for ext-apps spec**: Changed resource MIME type from `text/html` to `text/html;profile=mcp-app` (the `RESOURCE_MIME_TYPE` constant from `@modelcontextprotocol/ext-apps`). Without this profile parameter, Claude Desktop/web fails to recognize resources as MCP Apps and shows "Failed to load MCP App: the resource may exceed the 5 MB size limit."
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
## [2.34.2] - 2026-02-07
### Fixed
- **CI: UI apps missing from npm package**: Release pipeline only ran `npm run build` (TypeScript), so `ui-apps/dist/` was never built and excluded from published packages
- Changed build step to `npm run build:all` in `build-and-verify` and `publish-npm` jobs
- Added `ui-apps/dist/` to npm publish staging directory
- Added `ui-apps/dist/**/*` to published package files list
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
## [2.34.1] - 2026-02-07 ## [2.34.1] - 2026-02-07
### Changed ### Changed

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.34.1", "version": "2.34.4",
"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",

View File

@@ -6,7 +6,7 @@ export const UI_APP_CONFIGS: UIAppConfig[] = [
displayName: 'Operation Result', displayName: 'Operation Result',
description: 'Visual summary of workflow operations (create, update, delete, test)', description: 'Visual summary of workflow operations (create, update, delete, test)',
uri: 'ui://n8n-mcp/operation-result', uri: 'ui://n8n-mcp/operation-result',
mimeType: 'text/html', mimeType: 'text/html;profile=mcp-app',
toolPatterns: [ toolPatterns: [
'n8n_create_workflow', 'n8n_create_workflow',
'n8n_update_full_workflow', 'n8n_update_full_workflow',
@@ -22,7 +22,7 @@ export const UI_APP_CONFIGS: UIAppConfig[] = [
displayName: 'Validation Summary', displayName: 'Validation Summary',
description: 'Visual summary of node and workflow validation results', description: 'Visual summary of node and workflow validation results',
uri: 'ui://n8n-mcp/validation-summary', uri: 'ui://n8n-mcp/validation-summary',
mimeType: 'text/html', mimeType: 'text/html;profile=mcp-app',
toolPatterns: [ toolPatterns: [
'validate_node', 'validate_node',
'validate_workflow', 'validate_workflow',

View File

@@ -65,9 +65,9 @@ describe('UI_APP_CONFIGS', () => {
} }
}); });
it('should have consistent mimeType of text/html', () => { it('should have consistent mimeType of text/html;profile=mcp-app', () => {
for (const config of UI_APP_CONFIGS) { for (const config of UI_APP_CONFIGS) {
expect(config.mimeType).toBe('text/html'); expect(config.mimeType).toBe('text/html;profile=mcp-app');
} }
}); });

View File

@@ -5,10 +5,18 @@ 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 (error) {
return <div style={{ padding: '16px', color: '#ef4444' }}>Error: {error}</div>;
}
if (!isConnected) {
return <div style={{ padding: '16px', color: '#9ca3af' }}>Connecting...</div>;
}
if (!data) { if (!data) {
return <div style={{ padding: '16px', color: 'var(--n8n-text-muted)' }}>Loading...</div>; return <div style={{ padding: '16px', color: '#9ca3af' }}>Waiting for data...</div>;
} }
const isSuccess = data.status === 'success'; const isSuccess = data.status === 'success';

View File

@@ -5,10 +5,18 @@ import { useToolData } from '@shared/hooks/useToolData';
import type { ValidationSummaryData } from '@shared/types'; import type { ValidationSummaryData } from '@shared/types';
export default function App() { export default function App() {
const data = useToolData<ValidationSummaryData>(); const { data, error, isConnected } = useToolData<ValidationSummaryData>();
if (error) {
return <div style={{ padding: '16px', color: '#ef4444' }}>Error: {error}</div>;
}
if (!isConnected) {
return <div style={{ padding: '16px', color: '#9ca3af' }}>Connecting...</div>;
}
if (!data) { if (!data) {
return <div style={{ padding: '16px', color: 'var(--n8n-text-muted)' }}>Loading...</div>; return <div style={{ padding: '16px', color: '#9ca3af' }}>Waiting for data...</div>;
} }
return ( return (

View File

@@ -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,
};
} }