fix: resolve multiple n8n_update_partial_workflow bugs (#635)

* fix: use correct MCP SDK API for server capabilities in test

getServerVersion() returns Implementation (name/version only), not the
full init result. Use client.getServerCapabilities() instead to access
server capabilities, fixing the CI typecheck failure.

Concieved by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve multiple n8n_update_partial_workflow bugs (#592, #599, #610, #623, #624, #625, #629, #630, #633)

Phase 1 - Data loss prevention:
- Add missing unary operators (empty, notEmpty, exists, notExists) to sanitizer (#592)
- Preserve positional empty arrays in connections during removeNode/cleanStale (#610)
- Scope sanitization to modified nodes only, preventing unrelated node corruption
- Add empty body {} to activate/deactivate POST calls to fix 415 errors (#633)

Phase 2 - Error handling & response clarity:
- Serialize Zod errors to readable "path: message" strings (#630)
- Add saved:true/false field to all response paths (#625)
- Improve updateNode error hint with correct structure example (#623)
- Track removed node names for better removeConnection errors (#624)

Phase 3 - Connection & type fixes:
- Coerce sourceOutput/targetInput to String() consistently (#629)
- Accept numeric sourceOutput/targetInput at Zod schema level via transform

Phase 4 - Tag operations via dedicated API (#599):
- Track tags as tagsToAdd/tagsToRemove instead of mutating workflow.tags
- Orchestrate tag creation and association via listTags/createTag/updateWorkflowTags
- Reconcile conflicting add/remove for same tag (last operation wins)
- Tag failures produce warnings, not hard errors

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add v2.37.0 changelog entry

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve pre-existing integration test failures in CI

- Create new MCP Server instance per connection in test helpers (SDK 1.27+
  requires separate Protocol instance per connection)
- Normalize database paths with path.resolve() in shared-database singleton
  to prevent path mismatch errors across test files
- Add no-op catch handler to deferred initialization promise in server.ts
  to prevent unhandled rejection warnings
- Properly call mcpServer.shutdown() in test helper close() to release
  shared database references

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2026-03-14 17:27:33 +01:00
committed by GitHub
parent 248f859c49
commit 9590f751d2
15 changed files with 504 additions and 225 deletions

View File

@@ -11,6 +11,7 @@
* Issue: https://github.com/czlonkowski/n8n-mcp/issues/XXX
*/
import path from 'path';
import { DatabaseAdapter, createDatabaseAdapter } from './database-adapter';
import { NodeRepository } from './node-repository';
import { TemplateService } from '../templates/template-service';
@@ -43,21 +44,26 @@ let initializationPromise: Promise<SharedDatabaseState> | null = null;
* @returns Shared database state with connection and services
*/
export async function getSharedDatabase(dbPath: string): Promise<SharedDatabaseState> {
// Normalize to a canonical absolute path so that callers using different
// relative or join-based paths (e.g. "./data/nodes.db" vs an absolute path)
// resolve to the same string and do not trigger a false "different path" error.
const normalizedPath = dbPath === ':memory:' ? dbPath : path.resolve(dbPath);
// If already initialized with the same path, increment ref count and return
if (sharedState && sharedState.initialized && sharedState.dbPath === dbPath) {
if (sharedState && sharedState.initialized && sharedState.dbPath === normalizedPath) {
sharedState.refCount++;
logger.debug('Reusing shared database connection', {
refCount: sharedState.refCount,
dbPath
dbPath: normalizedPath
});
return sharedState;
}
// If already initialized with a DIFFERENT path, this is a configuration error
if (sharedState && sharedState.initialized && sharedState.dbPath !== dbPath) {
if (sharedState && sharedState.initialized && sharedState.dbPath !== normalizedPath) {
logger.error('Attempted to initialize shared database with different path', {
existingPath: sharedState.dbPath,
requestedPath: dbPath
requestedPath: normalizedPath
});
throw new Error(`Shared database already initialized with different path: ${sharedState.dbPath}`);
}
@@ -69,7 +75,7 @@ export async function getSharedDatabase(dbPath: string): Promise<SharedDatabaseS
state.refCount++;
logger.debug('Reusing shared database (waited for init)', {
refCount: state.refCount,
dbPath
dbPath: normalizedPath
});
return state;
} catch (error) {
@@ -80,7 +86,7 @@ export async function getSharedDatabase(dbPath: string): Promise<SharedDatabaseS
}
// Start new initialization
initializationPromise = initializeSharedDatabase(dbPath);
initializationPromise = initializeSharedDatabase(normalizedPath);
try {
const state = await initializationPromise;