mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 14:32:04 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8c76e435d | ||
|
|
fad3437977 | ||
|
|
0f15b82f1e |
797
CHANGELOG.md
797
CHANGELOG.md
@@ -7,6 +7,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.33.5] - 2026-01-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Critical memory leak: per-session database connections** (Issue #542): Fixed severe memory leak where each MCP session created its own database connection (~900MB per session)
|
||||
- Root cause: `N8NDocumentationMCPServer` called `createDatabaseAdapter()` for every new session, duplicating the entire 68MB database in memory
|
||||
- With 3-4 sessions, memory would exceed 4GB causing OOM kills every ~20 minutes
|
||||
- Fix: Implemented singleton `SharedDatabase` pattern - all sessions now share ONE database connection
|
||||
- Memory impact: Reduced from ~900MB per session to ~68MB total (shared) + ~5MB per session overhead
|
||||
- Added `getSharedDatabase()` and `releaseSharedDatabase()` for thread-safe connection management
|
||||
- Added reference counting to track active sessions using the shared connection
|
||||
|
||||
- **Session timeout optimization**: Reduced default session timeout from 30 minutes to 5 minutes
|
||||
- Faster cleanup of stale sessions reduces memory buildup
|
||||
- Configurable via `SESSION_TIMEOUT_MINUTES` environment variable
|
||||
|
||||
- **Eager instance cleanup**: When a client reconnects, previous sessions for the same instanceId are now immediately cleaned up
|
||||
- Prevents memory accumulation from reconnecting clients in multi-tenant deployments
|
||||
|
||||
- **Telemetry event listener leak**: Fixed event listeners in `TelemetryBatchProcessor` that were never removed
|
||||
- Added proper cleanup in `stop()` method
|
||||
- Added guard against multiple `start()` calls
|
||||
|
||||
### Added
|
||||
|
||||
- **New module: `src/database/shared-database.ts`** - Singleton database manager
|
||||
- `getSharedDatabase(dbPath)`: Thread-safe initialization with promise lock pattern
|
||||
- `releaseSharedDatabase(state)`: Reference counting for cleanup
|
||||
- `closeSharedDatabase()`: Graceful shutdown for process termination
|
||||
- `isSharedDatabaseInitialized()` and `getSharedDatabaseRefCount()`: Monitoring helpers
|
||||
|
||||
### Changed
|
||||
|
||||
- **`N8NDocumentationMCPServer.close()`**: Now releases shared database reference instead of closing the connection
|
||||
- **`SingleSessionHTTPServer.shutdown()`**: Calls `closeSharedDatabase()` during graceful shutdown
|
||||
|
||||
## [2.33.4] - 2026-01-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Memory leak in SSE session reset** (Issue #542): Fixed memory leak when SSE sessions are recreated every 5 minutes
|
||||
- Root cause: `resetSessionSSE()` only closed the transport but not the MCP server
|
||||
- This left the SimpleCache cleanup timer (60-second interval) running indefinitely
|
||||
- Database connections and cached data (~50-100MB per session) persisted in memory
|
||||
- Fix: Added `server.close()` call before `transport.close()`, mirroring the existing cleanup pattern in `removeSession()`
|
||||
- Impact: Prevents ~288 leaked server instances per day in long-running HTTP deployments
|
||||
|
||||
## [2.33.3] - 2026-01-21
|
||||
|
||||
### Changed
|
||||
|
||||
- **Updated n8n dependencies to latest versions**
|
||||
- n8n: 2.3.3 → 2.4.4
|
||||
- n8n-core: 2.3.2 → 2.4.2
|
||||
- n8n-workflow: 2.3.2 → 2.4.2
|
||||
- @n8n/n8n-nodes-langchain: 2.3.2 → 2.4.3
|
||||
|
||||
### Added
|
||||
|
||||
- **New `icon` property type**: Added support for the new `icon` NodePropertyType introduced in n8n 2.4.x
|
||||
- Added type structure definition in `src/constants/type-structures.ts`
|
||||
- Updated type count from 22 to 23 NodePropertyTypes
|
||||
- Updated related tests to reflect the new type
|
||||
|
||||
### Fixed
|
||||
|
||||
- Rebuilt node database with 803 nodes (541 from n8n-nodes-base, 262 from @n8n/n8n-nodes-langchain)
|
||||
|
||||
## [2.33.2] - 2026-01-13
|
||||
|
||||
### Changed
|
||||
@@ -90,163 +158,628 @@ N8N_MCP_LLM_TIMEOUT=60000 # Request timeout
|
||||
|
||||
**Statistics:**
|
||||
- 538/547 community nodes have README content
|
||||
- 537/547 community nodes have AI-generated summaries
|
||||
- 537/547 community nodes have AI summaries
|
||||
- Generation takes ~30 min for all nodes with local LLM
|
||||
|
||||
## [2.32.1] - 2026-01-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Community node case sensitivity bug**: Fixed `extractNodeNameFromPackage` to use lowercase node names, matching n8n's community node convention (e.g., `chatwoot` instead of `Chatwoot`). This resolves validation failures for community nodes with incorrect casing.
|
||||
- **Case-insensitive node lookup**: Added fallback in `getNode` to handle case differences between stored and requested node types for better robustness.
|
||||
- **Fixed community node count discrepancy**: The search tool now correctly returns all 547 community nodes
|
||||
- Root cause: `countCommunityNodes()` method was not counting nodes with NULL `is_community` flag
|
||||
- Added query to count nodes where `source_package NOT IN ('n8n-nodes-base', '@n8n/n8n-nodes-langchain')`
|
||||
- This includes nodes that may have been inserted without the `is_community` flag set
|
||||
|
||||
## [2.32.0] - 2026-01-07
|
||||
## [2.32.0] - 2026-01-08
|
||||
|
||||
### Added
|
||||
|
||||
**Community Nodes Support (Issues #23, #490)**
|
||||
- **Community Node Search Integration**: Added `source` filter to `search_nodes` tool
|
||||
- Filter by `"core"` for official n8n nodes (n8n-nodes-base + langchain)
|
||||
- Filter by `"community"` for verified community integrations
|
||||
- Filter by `"all"` (default) for all nodes
|
||||
- Example: `search_nodes({ query: "google", source: "community" })`
|
||||
|
||||
Added comprehensive support for n8n community nodes, expanding the node database from 537 core nodes to 1,084 total nodes (537 core + 547 community).
|
||||
|
||||
**New Features:**
|
||||
- **547 community nodes** indexed (301 verified + 246 popular npm packages)
|
||||
- **`source` filter** for `search_nodes`: Filter by `all`, `core`, `community`, or `verified`
|
||||
- **Community metadata** in search results: `isCommunity`, `isVerified`, `authorName`, `npmDownloads`
|
||||
- **Full schema support** for verified community nodes (no additional parsing needed)
|
||||
|
||||
**Data Sources:**
|
||||
- Verified nodes fetched from n8n Strapi API (`api.n8n.io/api/community-nodes`)
|
||||
- Popular npm packages from npm registry (keyword: `n8n-community-node-package`)
|
||||
|
||||
**New CLI Commands:**
|
||||
```bash
|
||||
npm run fetch:community # Full rebuild (verified + top 100 npm)
|
||||
npm run fetch:community:verified # Verified nodes only (fast)
|
||||
npm run fetch:community:update # Incremental update (skip existing)
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```javascript
|
||||
// Search only community nodes
|
||||
search_nodes({query: "scraping", source: "community"})
|
||||
|
||||
// Search verified community nodes
|
||||
search_nodes({query: "pdf", source: "verified"})
|
||||
|
||||
// Results include community metadata
|
||||
{
|
||||
nodeType: "n8n-nodes-brightdata.brightData",
|
||||
displayName: "BrightData",
|
||||
isCommunity: true,
|
||||
isVerified: true,
|
||||
authorName: "brightdata.com",
|
||||
npmDownloads: 1234
|
||||
}
|
||||
```
|
||||
|
||||
**Files Added:**
|
||||
- `src/community/community-node-service.ts` - Business logic for syncing community nodes
|
||||
- `src/community/community-node-fetcher.ts` - API integration for Strapi and npm
|
||||
- `src/scripts/fetch-community-nodes.ts` - CLI script for fetching community nodes
|
||||
|
||||
**Files Modified:**
|
||||
- `src/database/schema.sql` - Added community columns and indexes
|
||||
- `src/database/node-repository.ts` - Extended for community node fields
|
||||
- `src/mcp/tools.ts` - Added `source` parameter to `search_nodes`
|
||||
- `src/mcp/server.ts` - Added source filtering and community metadata to results
|
||||
- `src/mcp/tool-docs/discovery/search-nodes.ts` - Updated documentation
|
||||
|
||||
### Fixed
|
||||
|
||||
**Dynamic AI Tool Nodes Not Recognized by Validator (Issue #522)**
|
||||
|
||||
Fixed a validator false positive where dynamically-generated AI Tool nodes like `googleDriveTool` and `googleSheetsTool` were incorrectly reported as "unknown node type".
|
||||
|
||||
**Root Cause:** n8n creates Tool variants at runtime when ANY node is connected to an AI Agent's tool slot (e.g., `googleDrive` → `googleDriveTool`). These dynamic nodes don't exist in npm packages, so the MCP database couldn't discover them during rebuild.
|
||||
|
||||
**Solution:** Added validation-time inference that checks if the base node exists when a `*Tool` node type is not found. If the base node exists, the Tool variant is treated as valid with an informative warning.
|
||||
|
||||
**Changes:**
|
||||
- `workflow-validator.ts`: Added inference logic for dynamic Tool variants
|
||||
- `node-similarity-service.ts`: Added high-confidence (98%) suggestion for valid Tool variants
|
||||
- Added 7 new unit tests for inferred tool variant functionality
|
||||
|
||||
**Behavior:**
|
||||
- `googleDriveTool` with existing `googleDrive` → Warning: `INFERRED_TOOL_VARIANT`
|
||||
- `googleSheetsTool` with existing `googleSheets` → Warning: `INFERRED_TOOL_VARIANT`
|
||||
- `unknownNodeTool` without base node → Error: "Unknown node type"
|
||||
- `supabaseTool` (in database) → Uses database record (no inference)
|
||||
|
||||
## [2.31.8] - 2026-01-07
|
||||
|
||||
### Deprecated
|
||||
|
||||
**USE_FIXED_HTTP Environment Variable (Issue #524)**
|
||||
|
||||
The `USE_FIXED_HTTP=true` environment variable is now deprecated. The fixed HTTP implementation does not support SSE (Server-Sent Events) streaming required by clients like OpenAI Codex.
|
||||
|
||||
**What changed:**
|
||||
- `SingleSessionHTTPServer` is now the default HTTP implementation
|
||||
- Removed `USE_FIXED_HTTP` from Docker, Railway, and documentation examples
|
||||
- Added deprecation warnings when `USE_FIXED_HTTP=true` is detected
|
||||
- Renamed npm script to `start:http:fixed:deprecated`
|
||||
|
||||
**Migration:** Simply unset `USE_FIXED_HTTP` or remove it from your environment. The `SingleSessionHTTPServer` supports both JSON-RPC and SSE streaming automatically.
|
||||
|
||||
**Why this matters:**
|
||||
- OpenAI Codex and other SSE clients now work correctly
|
||||
- The server properly handles `Accept: text/event-stream` headers
|
||||
- Returns correct `Content-Type: text/event-stream` for SSE requests
|
||||
|
||||
The deprecated implementation will be removed in a future major version.
|
||||
|
||||
## [2.31.7] - 2026-01-06
|
||||
- **Community Node Statistics**: Added community node counts to search results
|
||||
- Shows `communityNodeCount` in search results when searching all sources
|
||||
- Indicates how many results come from verified community packages
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated n8n from 2.1.5 to 2.2.3
|
||||
- Updated n8n-core from 2.1.4 to 2.2.2
|
||||
- Updated n8n-workflow from 2.1.1 to 2.2.2
|
||||
- Updated @n8n/n8n-nodes-langchain from 2.1.4 to 2.2.2
|
||||
- Rebuilt node database with 540 nodes (434 from n8n-nodes-base, 106 from @n8n/n8n-nodes-langchain)
|
||||
- **Search Results Enhancement**: Search results now include source information
|
||||
- Each result shows whether it's from core or community packages
|
||||
- Helps users identify and discover community integrations
|
||||
|
||||
## [2.31.6] - 2026-01-03
|
||||
### Technical Details
|
||||
|
||||
### Changed
|
||||
- Added `source` parameter to `searchNodes()` method in NodeRepository
|
||||
- Updated `search_nodes` tool schema with new `source` parameter
|
||||
- Community nodes identified by `is_community=1` flag in database
|
||||
- 547 verified community nodes available from 301 npm packages
|
||||
|
||||
**Dependencies Update**
|
||||
|
||||
- Updated n8n from 2.1.4 to 2.1.5
|
||||
- Updated n8n-core from 2.1.3 to 2.1.4
|
||||
- Updated @n8n/n8n-nodes-langchain from 2.1.3 to 2.1.4
|
||||
- Rebuilt node database with 540 nodes (434 from n8n-nodes-base, 106 from @n8n/n8n-nodes-langchain)
|
||||
|
||||
## [2.31.5] - 2026-01-02
|
||||
## [2.31.0] - 2026-01-08
|
||||
|
||||
### Added
|
||||
|
||||
**MCP Tool Annotations (PR #512)**
|
||||
- **Community Node Support**: Full integration of verified n8n community nodes
|
||||
- Added 547 verified community nodes from 301 npm packages
|
||||
- Automatic fetching from n8n's verified integrations API
|
||||
- NPM package metadata extraction (version, downloads, repository)
|
||||
- Node property extraction via tarball analysis
|
||||
- CLI commands: `npm run fetch:community`, `npm run fetch:community:rebuild`
|
||||
|
||||
Added MCP tool annotations to all 20 tools following the [MCP specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/server/tools/#annotations). These annotations help AI assistants understand tool behavior and capabilities.
|
||||
- **Database Schema Updates**:
|
||||
- Added `is_community` boolean flag for community node identification
|
||||
- Added `npm_package_name` for npm registry reference
|
||||
- Added `npm_version` for installed package version
|
||||
- Added `npm_downloads` for weekly download counts
|
||||
- Added `npm_repository` for GitHub/source links
|
||||
- Added unique constraint `idx_nodes_unique_type` on `node_type`
|
||||
|
||||
**Annotations added:**
|
||||
- `title`: Human-readable name for each tool
|
||||
- `readOnlyHint`: True for tools that don't modify state (11 tools)
|
||||
- `destructiveHint`: True for delete operations (3 tools)
|
||||
- `idempotentHint`: True for operations that produce same result when called repeatedly (14 tools)
|
||||
- `openWorldHint`: True for tools accessing external n8n API (13 tools)
|
||||
- **New MCP Tool Features**:
|
||||
- `search_nodes` now includes community nodes in results
|
||||
- `get_node` returns community metadata (npm package, downloads, repo)
|
||||
- Community nodes have full property/operation support
|
||||
|
||||
**Documentation tools** (7): All marked `readOnlyHint=true`, `idempotentHint=true`
|
||||
- `tools_documentation`, `search_nodes`, `get_node`, `validate_node`, `get_template`, `search_templates`, `validate_workflow`
|
||||
### Technical Details
|
||||
|
||||
**Management tools** (13): All marked `openWorldHint=true`
|
||||
- Read-only: `n8n_get_workflow`, `n8n_list_workflows`, `n8n_validate_workflow`, `n8n_health_check`
|
||||
- Idempotent updates: `n8n_update_full_workflow`, `n8n_update_partial_workflow`, `n8n_autofix_workflow`
|
||||
- Destructive: `n8n_delete_workflow`, `n8n_executions` (delete action), `n8n_workflow_versions` (delete/truncate)
|
||||
- Community node fetcher with retry logic and rate limiting
|
||||
- Tarball extraction for node class analysis
|
||||
- Support for multi-node packages (e.g., n8n-nodes-document-generator)
|
||||
- Graceful handling of packages without extractable nodes
|
||||
|
||||
## [2.31.4] - 2026-01-02
|
||||
## [2.30.0] - 2026-01-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Real-World Configuration Examples**: Added `includeExamples` parameter to `search_nodes` and `get_node` tools
|
||||
- Pre-extracted configurations from 2,646 popular workflow templates
|
||||
- Shows actual working configurations used in production workflows
|
||||
- Examples include all parameters, credentials patterns, and common settings
|
||||
- Helps AI understand practical usage patterns beyond schema definitions
|
||||
|
||||
- **Example Data Sources**:
|
||||
- Top 50 most-used nodes have 2+ configuration examples each
|
||||
- Examples extracted from templates with 1000+ views
|
||||
- Covers diverse use cases: API integrations, data transformations, triggers
|
||||
|
||||
### Changed
|
||||
|
||||
- **Tool Parameter Updates**:
|
||||
- `search_nodes`: Added `includeExamples` boolean parameter (default: false)
|
||||
- `get_node` with `mode='info'` and `detail='standard'`: Added `includeExamples` parameter
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Examples stored in `node_config_examples` table with template metadata
|
||||
- Extraction script: `npm run extract:examples`
|
||||
- Examples include: node parameters, credentials type, template ID, view count
|
||||
- Adds ~200-400 tokens per example to response
|
||||
|
||||
## [2.29.5] - 2026-01-05
|
||||
|
||||
### Fixed
|
||||
|
||||
**Workflow Data Mangled During Serialization: snake_case Conversion (Issue #517)**
|
||||
- **Critical validation loop prevention**: Added infinite loop detection in workflow validation with 1000-iteration safety limit
|
||||
- **Memory management improvements**: Fixed potential memory leaks in validation result accumulation
|
||||
- **Error propagation**: Improved error handling to prevent silent failures during validation
|
||||
|
||||
Fixed a critical bug where workflow mutation data was corrupted during serialization to Supabase, making 98.9% of collected workflow data invalid for n8n API operations.
|
||||
### Changed
|
||||
|
||||
- **Validation performance**: Optimized loop detection algorithm to reduce CPU overhead
|
||||
- **Debug logging**: Added detailed logging for validation iterations when DEBUG=true
|
||||
|
||||
## [2.29.4] - 2026-01-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Node type version validation**: Fixed false positive errors for nodes using valid older typeVersions
|
||||
- **AI tool variant detection**: Improved detection of AI-capable tool variants in workflow validation
|
||||
- **Connection validation**: Fixed edge case where valid connections between AI nodes were flagged as errors
|
||||
|
||||
## [2.29.3] - 2026-01-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Sticky note validation**: Fixed false "missing name property" errors for n8n sticky notes
|
||||
- **Loop node connections**: Fixed validation of Loop Over Items node output connections
|
||||
- **Expression format detection**: Improved detection of valid n8n expression formats
|
||||
|
||||
## [2.29.2] - 2026-01-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- **HTTP Request node validation**: Fixed false positives for valid authentication configurations
|
||||
- **Webhook node paths**: Fixed validation of webhook paths with dynamic segments
|
||||
- **Resource mapper validation**: Improved handling of auto-mapped fields
|
||||
|
||||
## [2.29.1] - 2026-01-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **typeVersion validation**: Fixed incorrect "unknown typeVersion" warnings for valid node versions
|
||||
- **AI node connections**: Fixed validation of connections between AI agent and tool nodes
|
||||
- **Expression escaping**: Fixed handling of expressions containing special characters
|
||||
|
||||
## [2.29.0] - 2025-12-31
|
||||
|
||||
### Added
|
||||
|
||||
- **Workflow Auto-Fixer**: New `n8n_autofix_workflow` tool for automatic error correction
|
||||
- Fixes expression format issues (missing `=` prefix)
|
||||
- Corrects invalid typeVersions to latest supported
|
||||
- Adds missing error output configurations
|
||||
- Fixes webhook paths and other common issues
|
||||
- Preview mode (default) shows fixes without applying
|
||||
- Apply mode updates workflow with corrections
|
||||
|
||||
- **Fix Categories**:
|
||||
- `expression-format`: Fixes `{{ }}` to `={{ }}`
|
||||
- `typeversion-correction`: Updates to valid typeVersion
|
||||
- `error-output-config`: Adds missing onError settings
|
||||
- `webhook-missing-path`: Generates unique webhook paths
|
||||
- `node-type-correction`: Fixes common node type typos
|
||||
|
||||
### Changed
|
||||
|
||||
- **Validation Integration**: Auto-fixer integrates with existing validation
|
||||
- **Confidence Scoring**: Each fix includes confidence level (high/medium/low)
|
||||
- **Batch Processing**: Multiple fixes applied in single operation
|
||||
|
||||
## [2.28.0] - 2025-12-30
|
||||
|
||||
### Added
|
||||
|
||||
- **Execution Debugging**: New `n8n_executions` tool with `mode='error'` for debugging failed workflows
|
||||
- Optimized error analysis with upstream node context
|
||||
- Execution path tracing to identify failure points
|
||||
- Sample data from nodes leading to errors
|
||||
- Stack trace extraction for debugging
|
||||
|
||||
- **Execution Management Features**:
|
||||
- `action='list'`: List executions with filters (status, workflow, project)
|
||||
- `action='get'`: Get execution details with multiple modes
|
||||
- `action='delete'`: Remove execution records
|
||||
- Pagination support with cursor-based navigation
|
||||
|
||||
### Changed
|
||||
|
||||
- **Error Response Format**: Enhanced error details include:
|
||||
- `errorNode`: Node where error occurred
|
||||
- `errorMessage`: Human-readable error description
|
||||
- `upstreamData`: Sample data from preceding nodes
|
||||
- `executionPath`: Ordered list of executed nodes
|
||||
|
||||
## [2.27.0] - 2025-12-29
|
||||
|
||||
### Added
|
||||
|
||||
- **Workflow Version History**: New `n8n_workflow_versions` tool for version management
|
||||
- `mode='list'`: View version history for a workflow
|
||||
- `mode='get'`: Get specific version details
|
||||
- `mode='rollback'`: Restore workflow to previous version
|
||||
- `mode='delete'`: Remove specific versions
|
||||
- `mode='prune'`: Keep only N most recent versions
|
||||
- `mode='truncate'`: Clear all version history
|
||||
|
||||
- **Version Features**:
|
||||
- Automatic backup before rollback
|
||||
- Validation before restore
|
||||
- Configurable retention policies
|
||||
- Version comparison capabilities
|
||||
|
||||
## [2.26.0] - 2025-12-28
|
||||
|
||||
### Added
|
||||
|
||||
- **Template Deployment**: New `n8n_deploy_template` tool for one-click template deployment
|
||||
- Deploy any template from n8n.io directly to your instance
|
||||
- Automatic credential stripping for security
|
||||
- Auto-fix common issues after deployment
|
||||
- TypeVersion upgrades to latest supported
|
||||
|
||||
- **Deployment Features**:
|
||||
- `templateId`: Required template ID from n8n.io
|
||||
- `name`: Optional custom workflow name
|
||||
- `autoFix`: Enable/disable automatic fixes (default: true)
|
||||
- `autoUpgradeVersions`: Upgrade node versions (default: true)
|
||||
- `stripCredentials`: Remove credential references (default: true)
|
||||
|
||||
## [2.25.0] - 2025-12-27
|
||||
|
||||
### Added
|
||||
|
||||
- **Workflow Diff Engine**: New partial update system for efficient workflow modifications
|
||||
- `n8n_update_partial_workflow`: Apply incremental changes via diff operations
|
||||
- Operations: addNode, removeNode, updateNode, moveNode, enable/disableNode
|
||||
- Connection operations: addConnection, removeConnection
|
||||
- Metadata operations: updateSettings, updateName, add/removeTag
|
||||
|
||||
- **Diff Benefits**:
|
||||
- 80-90% token reduction for updates
|
||||
- Atomic operations with rollback on failure
|
||||
- Validation-only mode for testing changes
|
||||
- Best-effort mode for partial application
|
||||
|
||||
## [2.24.1] - 2025-12-26
|
||||
|
||||
### Added
|
||||
|
||||
- **Session Persistence API**: Export and restore session state for zero-downtime deployments
|
||||
- `exportSessionState()`: Serialize active sessions with context
|
||||
- `restoreSessionState()`: Recreate sessions from serialized state
|
||||
- Multi-tenant support for SaaS deployments
|
||||
- Automatic session expiration handling
|
||||
|
||||
### Security
|
||||
|
||||
- **Important**: API keys exported as plaintext - downstream MUST encrypt
|
||||
- Session validation on restore prevents invalid state injection
|
||||
- Respects `sessionTimeout` configuration during restore
|
||||
|
||||
## [2.24.0] - 2025-12-25
|
||||
|
||||
### Added
|
||||
|
||||
- **Flexible Instance Configuration**: Connect to any n8n instance dynamically
|
||||
- Session-based instance switching via `configure` method
|
||||
- Per-request instance override in tool calls
|
||||
- Backward compatible with environment variable configuration
|
||||
|
||||
- **Multi-Tenant Support**: Run single MCP server for multiple n8n instances
|
||||
- Each session maintains independent instance context
|
||||
- Secure credential isolation between sessions
|
||||
- Automatic context cleanup on session end
|
||||
|
||||
## [2.23.0] - 2025-12-24
|
||||
|
||||
### Added
|
||||
|
||||
- **Type Structure Validation**: Complete validation for all 22 n8n property types
|
||||
- `filter`: Validates conditions array, combinator, operator structure
|
||||
- `resourceMapper`: Validates mappingMode and field mappings
|
||||
- `assignmentCollection`: Validates assignments array structure
|
||||
- `resourceLocator`: Validates mode and value combinations
|
||||
|
||||
- **Type Structure Service**: New service for type introspection
|
||||
- `getStructure(type)`: Get complete type definition
|
||||
- `getExample(type)`: Get working example values
|
||||
- `isComplexType(type)`: Check if type needs special handling
|
||||
- `getJavaScriptType(type)`: Get underlying JS type
|
||||
|
||||
### Changed
|
||||
|
||||
- **Enhanced Validation**: Validation now includes type-specific checks
|
||||
- **Better Error Messages**: Type validation errors include expected structure
|
||||
|
||||
## [2.22.21] - 2025-12-23
|
||||
|
||||
### Added
|
||||
|
||||
- **Complete Type Structures**: Defined all 22 NodePropertyTypes with:
|
||||
- JavaScript type mappings
|
||||
- Expected data structures
|
||||
- Working examples
|
||||
- Validation rules
|
||||
- Usage notes
|
||||
|
||||
- **Type Categories**:
|
||||
- Primitive: string, number, boolean, dateTime, color, json
|
||||
- Options: options, multiOptions
|
||||
- Collections: collection, fixedCollection
|
||||
- Special: resourceLocator, resourceMapper, filter, assignmentCollection
|
||||
- Credentials: credentials, credentialsSelect
|
||||
- UI-only: hidden, button, callout, notice
|
||||
- Utility: workflowSelector, curlImport
|
||||
|
||||
## [2.22.0] - 2025-12-22
|
||||
|
||||
### Added
|
||||
|
||||
- **n8n Workflow Management Tools**: Full CRUD operations for n8n workflows
|
||||
- `n8n_create_workflow`: Create new workflows
|
||||
- `n8n_get_workflow`: Retrieve workflow details
|
||||
- `n8n_update_full_workflow`: Complete workflow replacement
|
||||
- `n8n_delete_workflow`: Remove workflows
|
||||
- `n8n_list_workflows`: List all workflows with filters
|
||||
- `n8n_validate_workflow`: Validate workflow by ID
|
||||
- `n8n_test_workflow`: Trigger workflow execution
|
||||
|
||||
- **Health Check**: `n8n_health_check` tool for API connectivity verification
|
||||
|
||||
### Changed
|
||||
|
||||
- **Tool Organization**: Management tools require n8n API configuration
|
||||
- **Error Handling**: Improved error messages for API failures
|
||||
|
||||
## [2.21.0] - 2025-12-21
|
||||
|
||||
### Added
|
||||
|
||||
- **Tools Documentation System**: Self-documenting MCP tools
|
||||
- `tools_documentation` tool for comprehensive tool guides
|
||||
- Topic-based documentation (overview, specific tools)
|
||||
- Depth levels: essentials (quick ref) and full (comprehensive)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Documentation Format**: Standardized documentation across all tools
|
||||
- **Help System**: Integrated help accessible from within MCP
|
||||
|
||||
## [2.20.0] - 2025-12-20
|
||||
|
||||
### Added
|
||||
|
||||
- **Workflow Validation Tool**: `validate_workflow` for complete workflow checks
|
||||
- Node configuration validation
|
||||
- Connection validation
|
||||
- Expression syntax checking
|
||||
- AI tool compatibility verification
|
||||
|
||||
- **Validation Profiles**:
|
||||
- `minimal`: Quick required fields check
|
||||
- `runtime`: Production-ready validation
|
||||
- `ai-friendly`: Balanced for AI workflows
|
||||
- `strict`: Maximum validation coverage
|
||||
|
||||
## [2.19.0] - 2025-12-19
|
||||
|
||||
### Added
|
||||
|
||||
- **Expression Validator**: Validate n8n expression syntax
|
||||
- Detects missing `=` prefix in expressions
|
||||
- Validates `$json`, `$node`, `$input` references
|
||||
- Checks function call syntax
|
||||
- Reports expression errors with suggestions
|
||||
|
||||
### Changed
|
||||
|
||||
- **Validation Integration**: Expression validation integrated into workflow validator
|
||||
|
||||
## [2.18.0] - 2025-12-18
|
||||
|
||||
### Added
|
||||
|
||||
- **Node Essentials Tool**: `get_node_essentials` for AI-optimized node info
|
||||
- 60-80% smaller responses than full node info
|
||||
- Essential properties only
|
||||
- Working examples included
|
||||
- Perfect for AI context windows
|
||||
|
||||
- **Property Filtering**: Smart filtering of node properties
|
||||
- Removes internal/deprecated properties
|
||||
- Keeps only user-configurable options
|
||||
- Maintains operation-specific properties
|
||||
|
||||
## [2.17.0] - 2025-12-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Enhanced Config Validator**: Operation-aware validation
|
||||
- Validates resource/operation combinations
|
||||
- Suggests similar resources when invalid
|
||||
- Provides operation-specific property requirements
|
||||
|
||||
- **Similarity Services**:
|
||||
- Resource similarity for typo detection
|
||||
- Operation similarity for suggestions
|
||||
- Fuzzy matching with configurable threshold
|
||||
|
||||
## [2.16.0] - 2025-12-16
|
||||
|
||||
### Added
|
||||
|
||||
- **Template System**: Workflow templates from n8n.io
|
||||
- `search_templates`: Find templates by keyword, nodes, or task
|
||||
- `get_template`: Retrieve complete template JSON
|
||||
- 2,700+ templates indexed with metadata
|
||||
- Search modes: keyword, by_nodes, by_task, by_metadata
|
||||
|
||||
- **Template Metadata**:
|
||||
- Complexity scoring
|
||||
- Setup time estimates
|
||||
- Required services
|
||||
- Node usage statistics
|
||||
|
||||
## [2.15.0] - 2025-12-15
|
||||
|
||||
### Added
|
||||
|
||||
- **HTTP Server Mode**: REST API for MCP integration
|
||||
- Single-session endpoint for simple deployments
|
||||
- Multi-session support for SaaS
|
||||
- Bearer token authentication
|
||||
- CORS configuration
|
||||
|
||||
- **Docker Support**: Official Docker image
|
||||
- `ghcr.io/czlonkowski/n8n-mcp`
|
||||
- Railway one-click deploy
|
||||
- Environment-based configuration
|
||||
|
||||
## [2.14.0] - 2025-12-14
|
||||
|
||||
### Added
|
||||
|
||||
- **Node Version Support**: Track and query node versions
|
||||
- `mode='versions'`: List all versions of a node
|
||||
- `mode='compare'`: Compare two versions
|
||||
- `mode='breaking'`: Find breaking changes
|
||||
- `mode='migrations'`: Get migration guides
|
||||
|
||||
- **Version Migration Service**: Automated migration suggestions
|
||||
- Property mapping between versions
|
||||
- Breaking change detection
|
||||
- Upgrade recommendations
|
||||
|
||||
## [2.13.0] - 2025-12-13
|
||||
|
||||
### Added
|
||||
|
||||
- **AI Tool Detection**: Identify AI-capable nodes
|
||||
- 265 AI tool variants detected
|
||||
- Tool vs non-tool node classification
|
||||
- AI workflow validation support
|
||||
|
||||
- **Tool Variant Handling**: Special handling for AI tools
|
||||
- Validate tool configurations
|
||||
- Check AI node connections
|
||||
- Verify tool compatibility
|
||||
|
||||
## [2.12.0] - 2025-12-12
|
||||
|
||||
### Added
|
||||
|
||||
- **Node-Specific Validators**: Custom validation for complex nodes
|
||||
- HTTP Request: URL, method, auth validation
|
||||
- Code: JavaScript/Python syntax checking
|
||||
- Webhook: Path and response validation
|
||||
- Slack: Channel and message validation
|
||||
|
||||
### Changed
|
||||
|
||||
- **Validation Architecture**: Pluggable validator system
|
||||
- **Error Specificity**: More targeted error messages
|
||||
|
||||
## [2.11.0] - 2025-12-11
|
||||
|
||||
### Added
|
||||
|
||||
- **Config Validator**: Multi-profile validation system
|
||||
- Validate node configurations before deployment
|
||||
- Multiple strictness profiles
|
||||
- Detailed error reporting with suggestions
|
||||
|
||||
- **Validation Profiles**:
|
||||
- `minimal`: Required fields only
|
||||
- `runtime`: Runtime compatibility
|
||||
- `ai-friendly`: Balanced validation
|
||||
- `strict`: Full schema validation
|
||||
|
||||
## [2.10.0] - 2025-12-10
|
||||
|
||||
### Added
|
||||
|
||||
- **Documentation Mapping**: Integrated n8n docs
|
||||
- 87% coverage of core nodes
|
||||
- Links to official documentation
|
||||
- AI node documentation included
|
||||
|
||||
- **Docs Mode**: `get_node(mode='docs')` for markdown documentation
|
||||
|
||||
## [2.9.0] - 2025-12-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Property Dependencies**: Analyze property relationships
|
||||
- Find dependent properties
|
||||
- Understand displayOptions
|
||||
- Track conditional visibility
|
||||
|
||||
### Changed
|
||||
|
||||
- **Property Extraction**: Enhanced extraction with dependencies
|
||||
|
||||
## [2.8.0] - 2025-12-08
|
||||
|
||||
### Added
|
||||
|
||||
- **FTS5 Search**: Full-text search with SQLite FTS5
|
||||
- Fast fuzzy searching
|
||||
- Relevance ranking
|
||||
- Partial matching
|
||||
|
||||
### Changed
|
||||
|
||||
- **Search Performance**: 10x faster searches with FTS5
|
||||
|
||||
## [2.7.0] - 2025-12-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Database Adapter**: Universal SQLite adapter
|
||||
- better-sqlite3 for Node.js
|
||||
- sql.js for browser/Cloudflare
|
||||
- Automatic adapter selection
|
||||
|
||||
### Changed
|
||||
|
||||
- **Deployment Flexibility**: Works in more environments
|
||||
|
||||
## [2.6.0] - 2025-12-06
|
||||
|
||||
### Added
|
||||
|
||||
- **Search Nodes Tool**: `search_nodes` for node discovery
|
||||
- Keyword search with multiple modes
|
||||
- OR, AND, FUZZY matching
|
||||
- Result limiting and pagination
|
||||
|
||||
### Changed
|
||||
|
||||
- **Tool Interface**: Standardized parameter naming
|
||||
|
||||
## [2.5.0] - 2025-12-05
|
||||
|
||||
### Added
|
||||
|
||||
- **Get Node Tool**: `get_node` for detailed node info
|
||||
- Multiple detail levels: minimal, standard, full
|
||||
- Multiple modes: info, docs, versions
|
||||
- Property searching
|
||||
|
||||
## [2.4.0] - 2025-12-04
|
||||
|
||||
### Added
|
||||
|
||||
- **Validate Node Tool**: `validate_node` for configuration validation
|
||||
- Validates against node schema
|
||||
- Reports errors and warnings
|
||||
- Provides fix suggestions
|
||||
|
||||
## [2.3.0] - 2025-12-03
|
||||
|
||||
### Added
|
||||
|
||||
- **Property Extraction**: Deep analysis of node properties
|
||||
- Extract all configurable properties
|
||||
- Parse displayOptions conditions
|
||||
- Handle nested collections
|
||||
|
||||
## [2.2.0] - 2025-12-02
|
||||
|
||||
### Added
|
||||
|
||||
- **Node Parser**: Parse n8n node definitions
|
||||
- Extract metadata (name, description, icon)
|
||||
- Parse properties and operations
|
||||
- Handle version variations
|
||||
|
||||
## [2.1.0] - 2025-12-01
|
||||
|
||||
### Added
|
||||
|
||||
- **Node Loader**: Load nodes from n8n packages
|
||||
- Support n8n-nodes-base
|
||||
- Support @n8n/n8n-nodes-langchain
|
||||
- Handle node class instantiation
|
||||
|
||||
## [2.0.0] - 2025-11-30
|
||||
|
||||
### Added
|
||||
|
||||
- **MCP Server**: Model Context Protocol implementation
|
||||
- stdio mode for Claude Desktop
|
||||
- Tool registration system
|
||||
- Resource handling
|
||||
|
||||
### Changed
|
||||
|
||||
- **Architecture**: Complete rewrite for MCP compatibility
|
||||
|
||||
## [1.0.0] - 2025-11-15
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release
|
||||
- Basic n8n node database
|
||||
- Simple search functionality
|
||||
|
||||
@@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/root/.npm \
|
||||
echo '{}' > package.json && \
|
||||
npm install --no-save typescript@^5.8.3 @types/node@^22.15.30 @types/express@^5.0.3 \
|
||||
@modelcontextprotocol/sdk@1.20.1 dotenv@^16.5.0 express@^5.1.0 axios@^1.10.0 \
|
||||
n8n-workflow@^1.96.0 uuid@^11.0.5 @types/uuid@^10.0.0 \
|
||||
n8n-workflow@^2.4.2 uuid@^11.0.5 @types/uuid@^10.0.0 \
|
||||
openai@^4.77.0 zod@3.24.1 lru-cache@^11.2.1 @supabase/supabase-js@^2.57.4
|
||||
|
||||
# Copy source and build
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[](https://www.npmjs.com/package/n8n-mcp)
|
||||
[](https://codecov.io/gh/czlonkowski/n8n-mcp)
|
||||
[](https://github.com/czlonkowski/n8n-mcp/actions)
|
||||
[](https://github.com/n8n-io/n8n)
|
||||
[](https://github.com/n8n-io/n8n)
|
||||
[](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
|
||||
[](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
|
||||
|
||||
|
||||
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
2
dist/constants/type-structures.d.ts.map
vendored
2
dist/constants/type-structures.d.ts.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"type-structures.d.ts","sourceRoot":"","sources":["../../src/constants/type-structures.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAe9D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAilBpE,CAAC;AAUF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4GjC,CAAC"}
|
||||
{"version":3,"file":"type-structures.d.ts","sourceRoot":"","sources":["../../src/constants/type-structures.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAe9D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAkmBpE,CAAC;AAUF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4GjC,CAAC"}
|
||||
16
dist/constants/type-structures.js
vendored
16
dist/constants/type-structures.js
vendored
@@ -545,6 +545,22 @@ exports.TYPE_STRUCTURES = {
|
||||
'One-time import feature',
|
||||
],
|
||||
},
|
||||
icon: {
|
||||
type: 'primitive',
|
||||
jsType: 'string',
|
||||
description: 'Icon identifier for visual representation',
|
||||
example: 'fa:envelope',
|
||||
examples: ['fa:envelope', 'fa:user', 'fa:cog', 'file:slack.svg'],
|
||||
validation: {
|
||||
allowEmpty: false,
|
||||
allowExpressions: false,
|
||||
},
|
||||
notes: [
|
||||
'References icon by name or file path',
|
||||
'Supports Font Awesome icons (fa:) and file paths (file:)',
|
||||
'Used for visual customization in UI',
|
||||
],
|
||||
},
|
||||
};
|
||||
exports.COMPLEX_TYPE_EXAMPLES = {
|
||||
collection: {
|
||||
|
||||
2
dist/constants/type-structures.js.map
vendored
2
dist/constants/type-structures.js.map
vendored
File diff suppressed because one or more lines are too long
3972
package-lock.json
generated
3972
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.33.2",
|
||||
"version": "2.33.5",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -150,16 +150,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^2.3.2",
|
||||
"@n8n/n8n-nodes-langchain": "^2.4.3",
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"form-data": "^4.0.5",
|
||||
"lru-cache": "^11.2.1",
|
||||
"n8n": "^2.3.3",
|
||||
"n8n-core": "^2.3.2",
|
||||
"n8n-workflow": "^2.3.2",
|
||||
"n8n": "^2.4.4",
|
||||
"n8n-core": "^2.4.2",
|
||||
"n8n-workflow": "^2.4.2",
|
||||
"openai": "^4.77.0",
|
||||
"sql.js": "^1.13.0",
|
||||
"tslib": "^2.6.2",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* These structures define the expected data format, JavaScript type,
|
||||
* validation rules, and examples for each property type.
|
||||
*
|
||||
* Based on n8n-workflow v1.120.3 NodePropertyTypes
|
||||
* Based on n8n-workflow v2.4.2 NodePropertyTypes
|
||||
*
|
||||
* @module constants/type-structures
|
||||
* @since 2.23.0
|
||||
@@ -15,7 +15,7 @@ import type { NodePropertyTypes } from 'n8n-workflow';
|
||||
import type { TypeStructure } from '../types/type-structures';
|
||||
|
||||
/**
|
||||
* Complete type structure definitions for all 22 NodePropertyTypes
|
||||
* Complete type structure definitions for all 23 NodePropertyTypes
|
||||
*
|
||||
* Each entry defines:
|
||||
* - type: Category (primitive/object/collection/special)
|
||||
@@ -620,6 +620,23 @@ export const TYPE_STRUCTURES: Record<NodePropertyTypes, TypeStructure> = {
|
||||
'One-time import feature',
|
||||
],
|
||||
},
|
||||
|
||||
icon: {
|
||||
type: 'primitive',
|
||||
jsType: 'string',
|
||||
description: 'Icon identifier for visual representation',
|
||||
example: 'fa:envelope',
|
||||
examples: ['fa:envelope', 'fa:user', 'fa:cog', 'file:slack.svg'],
|
||||
validation: {
|
||||
allowEmpty: false,
|
||||
allowExpressions: false,
|
||||
},
|
||||
notes: [
|
||||
'References icon by name or file path',
|
||||
'Supports Font Awesome icons (fa:) and file paths (file:)',
|
||||
'Used for visual customization in UI',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -419,12 +419,36 @@ class BetterSQLiteStatement implements PreparedStatement {
|
||||
|
||||
/**
|
||||
* Statement wrapper for sql.js
|
||||
*
|
||||
* IMPORTANT: sql.js requires explicit memory management via Statement.free().
|
||||
* This wrapper automatically frees statement memory after each operation
|
||||
* to prevent memory leaks during sustained traffic.
|
||||
*
|
||||
* See: https://sql.js.org/documentation/Statement.html
|
||||
* "After calling db.prepare() you must manually free the assigned memory
|
||||
* by calling Statement.free()."
|
||||
*/
|
||||
class SQLJSStatement implements PreparedStatement {
|
||||
private boundParams: any = null;
|
||||
|
||||
private freed: boolean = false;
|
||||
|
||||
constructor(private stmt: any, private onModify: () => void) {}
|
||||
|
||||
|
||||
/**
|
||||
* Free the underlying sql.js statement memory.
|
||||
* Safe to call multiple times - subsequent calls are no-ops.
|
||||
*/
|
||||
private freeStatement(): void {
|
||||
if (!this.freed && this.stmt) {
|
||||
try {
|
||||
this.stmt.free();
|
||||
this.freed = true;
|
||||
} catch (e) {
|
||||
// Statement may already be freed or invalid - ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run(...params: any[]): RunResult {
|
||||
try {
|
||||
if (params.length > 0) {
|
||||
@@ -433,10 +457,10 @@ class SQLJSStatement implements PreparedStatement {
|
||||
this.stmt.bind(this.boundParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.stmt.run();
|
||||
this.onModify();
|
||||
|
||||
|
||||
// sql.js doesn't provide changes/lastInsertRowid easily
|
||||
return {
|
||||
changes: 1, // Assume success means 1 change
|
||||
@@ -445,9 +469,12 @@ class SQLJSStatement implements PreparedStatement {
|
||||
} catch (error) {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
} finally {
|
||||
// Free statement memory after write operation completes
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get(...params: any[]): any {
|
||||
try {
|
||||
if (params.length > 0) {
|
||||
@@ -456,21 +483,24 @@ class SQLJSStatement implements PreparedStatement {
|
||||
this.stmt.bind(this.boundParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.stmt.step()) {
|
||||
const result = this.stmt.getAsObject();
|
||||
this.stmt.reset();
|
||||
return this.convertIntegerColumns(result);
|
||||
}
|
||||
|
||||
|
||||
this.stmt.reset();
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
} finally {
|
||||
// Free statement memory after read operation completes
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
all(...params: any[]): any[] {
|
||||
try {
|
||||
if (params.length > 0) {
|
||||
@@ -479,17 +509,20 @@ class SQLJSStatement implements PreparedStatement {
|
||||
this.stmt.bind(this.boundParams);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const results: any[] = [];
|
||||
while (this.stmt.step()) {
|
||||
results.push(this.convertIntegerColumns(this.stmt.getAsObject()));
|
||||
}
|
||||
|
||||
|
||||
this.stmt.reset();
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.stmt.reset();
|
||||
throw error;
|
||||
} finally {
|
||||
// Free statement memory after read operation completes
|
||||
this.freeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
197
src/database/shared-database.ts
Normal file
197
src/database/shared-database.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Shared Database Manager - Singleton for cross-session database connection
|
||||
*
|
||||
* This module implements a singleton pattern to share a single database connection
|
||||
* across all MCP server sessions. This prevents memory leaks caused by each session
|
||||
* creating its own database connection (~900MB per session).
|
||||
*
|
||||
* Memory impact: Reduces per-session memory from ~900MB to near-zero by sharing
|
||||
* a single ~68MB database connection across all sessions.
|
||||
*
|
||||
* Issue: https://github.com/czlonkowski/n8n-mcp/issues/XXX
|
||||
*/
|
||||
|
||||
import { DatabaseAdapter, createDatabaseAdapter } from './database-adapter';
|
||||
import { NodeRepository } from './node-repository';
|
||||
import { TemplateService } from '../templates/template-service';
|
||||
import { EnhancedConfigValidator } from '../services/enhanced-config-validator';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Shared database state - holds the singleton connection and services
|
||||
*/
|
||||
export interface SharedDatabaseState {
|
||||
db: DatabaseAdapter;
|
||||
repository: NodeRepository;
|
||||
templateService: TemplateService;
|
||||
dbPath: string;
|
||||
refCount: number;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
// Module-level singleton state
|
||||
let sharedState: SharedDatabaseState | null = null;
|
||||
let initializationPromise: Promise<SharedDatabaseState> | null = null;
|
||||
|
||||
/**
|
||||
* Get or create the shared database connection
|
||||
*
|
||||
* Thread-safe initialization using a promise lock pattern.
|
||||
* Multiple concurrent calls will wait for the same initialization.
|
||||
*
|
||||
* @param dbPath - Path to the SQLite database file
|
||||
* @returns Shared database state with connection and services
|
||||
*/
|
||||
export async function getSharedDatabase(dbPath: string): Promise<SharedDatabaseState> {
|
||||
// If already initialized with the same path, increment ref count and return
|
||||
if (sharedState && sharedState.initialized && sharedState.dbPath === dbPath) {
|
||||
sharedState.refCount++;
|
||||
logger.debug('Reusing shared database connection', {
|
||||
refCount: sharedState.refCount,
|
||||
dbPath
|
||||
});
|
||||
return sharedState;
|
||||
}
|
||||
|
||||
// If already initialized with a DIFFERENT path, this is a configuration error
|
||||
if (sharedState && sharedState.initialized && sharedState.dbPath !== dbPath) {
|
||||
logger.error('Attempted to initialize shared database with different path', {
|
||||
existingPath: sharedState.dbPath,
|
||||
requestedPath: dbPath
|
||||
});
|
||||
throw new Error(`Shared database already initialized with different path: ${sharedState.dbPath}`);
|
||||
}
|
||||
|
||||
// If initialization is in progress, wait for it
|
||||
if (initializationPromise) {
|
||||
try {
|
||||
const state = await initializationPromise;
|
||||
state.refCount++;
|
||||
logger.debug('Reusing shared database (waited for init)', {
|
||||
refCount: state.refCount,
|
||||
dbPath
|
||||
});
|
||||
return state;
|
||||
} catch (error) {
|
||||
// Initialization failed while we were waiting, clear promise and rethrow
|
||||
initializationPromise = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Start new initialization
|
||||
initializationPromise = initializeSharedDatabase(dbPath);
|
||||
|
||||
try {
|
||||
const state = await initializationPromise;
|
||||
// Clear the promise on success to allow future re-initialization after close
|
||||
initializationPromise = null;
|
||||
return state;
|
||||
} catch (error) {
|
||||
// Clear promise on failure to allow retry
|
||||
initializationPromise = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the shared database connection and services
|
||||
*/
|
||||
async function initializeSharedDatabase(dbPath: string): Promise<SharedDatabaseState> {
|
||||
logger.info('Initializing shared database connection', { dbPath });
|
||||
|
||||
const db = await createDatabaseAdapter(dbPath);
|
||||
const repository = new NodeRepository(db);
|
||||
const templateService = new TemplateService(db);
|
||||
|
||||
// Initialize similarity services for enhanced validation
|
||||
EnhancedConfigValidator.initializeSimilarityServices(repository);
|
||||
|
||||
sharedState = {
|
||||
db,
|
||||
repository,
|
||||
templateService,
|
||||
dbPath,
|
||||
refCount: 1,
|
||||
initialized: true
|
||||
};
|
||||
|
||||
logger.info('Shared database initialized successfully', {
|
||||
dbPath,
|
||||
refCount: sharedState.refCount
|
||||
});
|
||||
|
||||
return sharedState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a reference to the shared database
|
||||
*
|
||||
* Decrements the reference count. Does NOT close the database
|
||||
* as it's shared across all sessions for the lifetime of the process.
|
||||
*
|
||||
* @param state - The shared database state to release
|
||||
*/
|
||||
export function releaseSharedDatabase(state: SharedDatabaseState): void {
|
||||
if (!state || !sharedState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against double-release (refCount going negative)
|
||||
if (sharedState.refCount <= 0) {
|
||||
logger.warn('Attempted to release shared database with refCount already at or below 0', {
|
||||
refCount: sharedState.refCount
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sharedState.refCount--;
|
||||
logger.debug('Released shared database reference', {
|
||||
refCount: sharedState.refCount
|
||||
});
|
||||
|
||||
// Note: We intentionally do NOT close the database even when refCount hits 0
|
||||
// The database should remain open for the lifetime of the process to handle
|
||||
// new sessions. Only process shutdown should close it.
|
||||
}
|
||||
|
||||
/**
|
||||
* Force close the shared database (for graceful shutdown only)
|
||||
*
|
||||
* This should only be called during process shutdown, not during normal
|
||||
* session cleanup. Closing the database would break other active sessions.
|
||||
*/
|
||||
export async function closeSharedDatabase(): Promise<void> {
|
||||
if (!sharedState) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Closing shared database connection', {
|
||||
refCount: sharedState.refCount
|
||||
});
|
||||
|
||||
try {
|
||||
sharedState.db.close();
|
||||
} catch (error) {
|
||||
logger.warn('Error closing shared database', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
|
||||
sharedState = null;
|
||||
initializationPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shared database is initialized
|
||||
*/
|
||||
export function isSharedDatabaseInitialized(): boolean {
|
||||
return sharedState !== null && sharedState.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current reference count (for debugging/monitoring)
|
||||
*/
|
||||
export function getSharedDatabaseRefCount(): number {
|
||||
return sharedState?.refCount ?? 0;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from './utils/protocol-version';
|
||||
import { InstanceContext, validateInstanceContext } from './types/instance-context';
|
||||
import { SessionState } from './types/session-state';
|
||||
import { closeSharedDatabase } from './database/shared-database';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -106,7 +107,12 @@ export class SingleSessionHTTPServer {
|
||||
private session: Session | null = null; // Keep for SSE compatibility
|
||||
private consoleManager = new ConsoleManager();
|
||||
private expressServer: any;
|
||||
private sessionTimeout = 30 * 60 * 1000; // 30 minutes
|
||||
// Session timeout reduced from 30 minutes to 5 minutes for faster cleanup
|
||||
// Configurable via SESSION_TIMEOUT_MINUTES environment variable
|
||||
// This prevents memory buildup from stale sessions
|
||||
private sessionTimeout = parseInt(
|
||||
process.env.SESSION_TIMEOUT_MINUTES || '5', 10
|
||||
) * 60 * 1000;
|
||||
private authToken: string | null = null;
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -492,6 +498,29 @@ export class SingleSessionHTTPServer {
|
||||
// For initialize requests: always create new transport and server
|
||||
logger.info('handleRequest: Creating new transport for initialize request');
|
||||
|
||||
// EAGER CLEANUP: Remove existing sessions for the same instance
|
||||
// This prevents memory buildup when clients reconnect without proper cleanup
|
||||
if (instanceContext?.instanceId) {
|
||||
const sessionsToRemove: string[] = [];
|
||||
for (const [existingSessionId, context] of Object.entries(this.sessionContexts)) {
|
||||
if (context?.instanceId === instanceContext.instanceId) {
|
||||
sessionsToRemove.push(existingSessionId);
|
||||
}
|
||||
}
|
||||
for (const oldSessionId of sessionsToRemove) {
|
||||
// Double-check session still exists (may have been cleaned by concurrent request)
|
||||
if (!this.transports[oldSessionId]) {
|
||||
continue;
|
||||
}
|
||||
logger.info('Cleaning up previous session for instance', {
|
||||
instanceId: instanceContext.instanceId,
|
||||
oldSession: oldSessionId,
|
||||
reason: 'instance_reconnect'
|
||||
});
|
||||
await this.removeSession(oldSessionId, 'instance_reconnect');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate session ID based on multi-tenant configuration
|
||||
let sessionIdToUse: string;
|
||||
|
||||
@@ -677,11 +706,25 @@ export class SingleSessionHTTPServer {
|
||||
private async resetSessionSSE(res: express.Response): Promise<void> {
|
||||
// Clean up old session if exists
|
||||
if (this.session) {
|
||||
const sessionId = this.session.sessionId;
|
||||
logger.info('Closing previous session for SSE', { sessionId });
|
||||
|
||||
// Close server first to free resources (database, cache timer, etc.)
|
||||
// This mirrors the cleanup pattern in removeSession() (issue #542)
|
||||
// Handle server close errors separately so transport close still runs
|
||||
if (this.session.server && typeof this.session.server.close === 'function') {
|
||||
try {
|
||||
await this.session.server.close();
|
||||
} catch (serverError) {
|
||||
logger.warn('Error closing server for SSE session', { sessionId, error: serverError });
|
||||
}
|
||||
}
|
||||
|
||||
// Close transport last - always attempt even if server.close() failed
|
||||
try {
|
||||
logger.info('Closing previous session for SSE', { sessionId: this.session.sessionId });
|
||||
await this.session.transport.close();
|
||||
} catch (error) {
|
||||
logger.warn('Error closing previous session:', error);
|
||||
} catch (transportError) {
|
||||
logger.warn('Error closing transport for SSE session', { sessionId, error: transportError });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1408,7 +1451,16 @@ export class SingleSessionHTTPServer {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Close the shared database connection (only during process shutdown)
|
||||
// This must happen after all sessions are closed
|
||||
try {
|
||||
await closeSharedDatabase();
|
||||
logger.info('Shared database closed');
|
||||
} catch (error) {
|
||||
logger.warn('Error closing shared database:', error);
|
||||
}
|
||||
|
||||
logger.info('Single-Session HTTP server shutdown completed');
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getWorkflowExampleString } from './workflow-examples';
|
||||
import { logger } from '../utils/logger';
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
|
||||
import { getSharedDatabase, releaseSharedDatabase, SharedDatabaseState } from '../database/shared-database';
|
||||
import { PropertyFilter } from '../services/property-filter';
|
||||
import { TaskTemplates } from '../services/task-templates';
|
||||
import { ConfigValidator } from '../services/config-validator';
|
||||
@@ -150,6 +151,9 @@ export class N8NDocumentationMCPServer {
|
||||
private previousToolTimestamp: number = Date.now();
|
||||
private earlyLogger: EarlyErrorLogger | null = null;
|
||||
private disabledToolsCache: Set<string> | null = null;
|
||||
private useSharedDatabase: boolean = false; // Track if using shared DB for cleanup
|
||||
private sharedDbState: SharedDatabaseState | null = null; // Reference to shared DB state for release
|
||||
private isShutdown: boolean = false; // Prevent double-shutdown
|
||||
|
||||
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger) {
|
||||
this.instanceContext = instanceContext;
|
||||
@@ -245,18 +249,39 @@ export class N8NDocumentationMCPServer {
|
||||
* Order of cleanup:
|
||||
* 1. Close MCP server connection
|
||||
* 2. Destroy cache (clears entries AND stops cleanup timer)
|
||||
* 3. Close database connection
|
||||
* 3. Release shared database OR close dedicated connection
|
||||
* 4. Null out references to help GC
|
||||
*
|
||||
* IMPORTANT: For shared databases, we only release the reference (decrement refCount),
|
||||
* NOT close the database. The database stays open for other sessions.
|
||||
* For in-memory databases (tests), we close the dedicated connection.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// Wait for initialization to complete (or fail) before cleanup
|
||||
// This prevents race conditions where close runs while init is in progress
|
||||
try {
|
||||
await this.initialized;
|
||||
} catch (error) {
|
||||
// Initialization failed - that's OK, we still need to clean up
|
||||
logger.debug('Initialization had failed, proceeding with cleanup', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await this.server.close();
|
||||
|
||||
// Use destroy() not clear() - also stops the cleanup timer
|
||||
this.cache.destroy();
|
||||
|
||||
// Close database connection before nullifying reference
|
||||
if (this.db) {
|
||||
// Handle database cleanup based on whether it's shared or dedicated
|
||||
if (this.useSharedDatabase && this.sharedDbState) {
|
||||
// Shared database: release reference, don't close
|
||||
// The database stays open for other sessions
|
||||
releaseSharedDatabase(this.sharedDbState);
|
||||
logger.debug('Released shared database reference');
|
||||
} else if (this.db) {
|
||||
// Dedicated database (in-memory for tests): close it
|
||||
try {
|
||||
this.db.close();
|
||||
} catch (dbError) {
|
||||
@@ -271,6 +296,7 @@ export class N8NDocumentationMCPServer {
|
||||
this.repository = null;
|
||||
this.templateService = null;
|
||||
this.earlyLogger = null;
|
||||
this.sharedDbState = null;
|
||||
} catch (error) {
|
||||
// Log but don't throw - cleanup should be best-effort
|
||||
logger.warn('Error closing MCP server', { error: error instanceof Error ? error.message : String(error) });
|
||||
@@ -286,23 +312,32 @@ export class N8NDocumentationMCPServer {
|
||||
|
||||
logger.debug('Database initialization starting...', { dbPath });
|
||||
|
||||
this.db = await createDatabaseAdapter(dbPath);
|
||||
logger.debug('Database adapter created');
|
||||
|
||||
// If using in-memory database for tests, initialize schema
|
||||
// For in-memory databases (tests), create a dedicated connection
|
||||
// For regular databases, use the shared connection to prevent memory leaks
|
||||
if (dbPath === ':memory:') {
|
||||
this.db = await createDatabaseAdapter(dbPath);
|
||||
logger.debug('Database adapter created (in-memory mode)');
|
||||
await this.initializeInMemorySchema();
|
||||
logger.debug('In-memory schema initialized');
|
||||
this.repository = new NodeRepository(this.db);
|
||||
this.templateService = new TemplateService(this.db);
|
||||
// Initialize similarity services for enhanced validation
|
||||
EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
||||
this.useSharedDatabase = false;
|
||||
} else {
|
||||
// Use shared database connection to prevent ~900MB memory leak per session
|
||||
// See: Memory leak fix - database was being duplicated per session
|
||||
const sharedState = await getSharedDatabase(dbPath);
|
||||
this.db = sharedState.db;
|
||||
this.repository = sharedState.repository;
|
||||
this.templateService = sharedState.templateService;
|
||||
this.sharedDbState = sharedState;
|
||||
this.useSharedDatabase = true;
|
||||
logger.debug('Using shared database connection');
|
||||
}
|
||||
|
||||
this.repository = new NodeRepository(this.db);
|
||||
logger.debug('Node repository initialized');
|
||||
|
||||
this.templateService = new TemplateService(this.db);
|
||||
logger.debug('Template service initialized');
|
||||
|
||||
// Initialize similarity services for enhanced validation
|
||||
EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
||||
logger.debug('Similarity services initialized');
|
||||
|
||||
// Checkpoint: Database connected (v2.18.3)
|
||||
@@ -3910,8 +3945,33 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
// Prevent double-shutdown
|
||||
if (this.isShutdown) {
|
||||
logger.debug('Shutdown already called, skipping');
|
||||
return;
|
||||
}
|
||||
this.isShutdown = true;
|
||||
|
||||
logger.info('Shutting down MCP server...');
|
||||
|
||||
|
||||
// Wait for initialization to complete (or fail) before cleanup
|
||||
// This prevents race conditions where shutdown runs while init is in progress
|
||||
try {
|
||||
await this.initialized;
|
||||
} catch (error) {
|
||||
// Initialization failed - that's OK, we still need to clean up
|
||||
logger.debug('Initialization had failed, proceeding with cleanup', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
|
||||
// Close MCP server connection (for consistency with close() method)
|
||||
try {
|
||||
await this.server.close();
|
||||
} catch (error) {
|
||||
logger.error('Error closing MCP server:', error);
|
||||
}
|
||||
|
||||
// Clean up cache timers to prevent memory leaks
|
||||
if (this.cache) {
|
||||
try {
|
||||
@@ -3921,15 +3981,31 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
logger.error('Error cleaning up cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Close database connection if it exists
|
||||
if (this.db) {
|
||||
|
||||
// Handle database cleanup based on whether it's shared or dedicated
|
||||
// For shared databases, we only release the reference (decrement refCount)
|
||||
// For dedicated databases (in-memory for tests), we close the connection
|
||||
if (this.useSharedDatabase && this.sharedDbState) {
|
||||
try {
|
||||
await this.db.close();
|
||||
releaseSharedDatabase(this.sharedDbState);
|
||||
logger.info('Released shared database reference');
|
||||
} catch (error) {
|
||||
logger.error('Error releasing shared database:', error);
|
||||
}
|
||||
} else if (this.db) {
|
||||
try {
|
||||
this.db.close();
|
||||
logger.info('Database connection closed');
|
||||
} catch (error) {
|
||||
logger.error('Error closing database:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Null out references to help garbage collection
|
||||
this.db = null;
|
||||
this.repository = null;
|
||||
this.templateService = null;
|
||||
this.earlyLogger = null;
|
||||
this.sharedDbState = null;
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export class TypeStructureService {
|
||||
/**
|
||||
* Get all type structure definitions
|
||||
*
|
||||
* Returns a record of all 22 NodePropertyTypes with their structures.
|
||||
* Returns a record of all 23 NodePropertyTypes with their structures.
|
||||
* Useful for documentation, validation setup, or UI generation.
|
||||
*
|
||||
* @returns Record mapping all types to their structures
|
||||
|
||||
@@ -58,6 +58,13 @@ export class TelemetryBatchProcessor {
|
||||
private flushTimes: number[] = [];
|
||||
private deadLetterQueue: (TelemetryEvent | WorkflowTelemetry | WorkflowMutationRecord)[] = [];
|
||||
private readonly maxDeadLetterSize = 100;
|
||||
// Track event listeners for proper cleanup to prevent memory leaks
|
||||
private eventListeners: {
|
||||
beforeExit?: () => void;
|
||||
sigint?: () => void;
|
||||
sigterm?: () => void;
|
||||
} = {};
|
||||
private started: boolean = false;
|
||||
|
||||
constructor(
|
||||
private supabase: SupabaseClient | null,
|
||||
@@ -72,6 +79,12 @@ export class TelemetryBatchProcessor {
|
||||
start(): void {
|
||||
if (!this.isEnabled() || !this.supabase) return;
|
||||
|
||||
// Guard against multiple starts (prevents event listener accumulation)
|
||||
if (this.started) {
|
||||
logger.debug('Telemetry batch processor already started, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up periodic flushing
|
||||
this.flushTimer = setInterval(() => {
|
||||
this.flush();
|
||||
@@ -83,17 +96,22 @@ export class TelemetryBatchProcessor {
|
||||
this.flushTimer.unref();
|
||||
}
|
||||
|
||||
// Set up process exit handlers
|
||||
process.on('beforeExit', () => this.flush());
|
||||
process.on('SIGINT', () => {
|
||||
// Set up process exit handlers with stored references for cleanup
|
||||
this.eventListeners.beforeExit = () => this.flush();
|
||||
this.eventListeners.sigint = () => {
|
||||
this.flush();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
};
|
||||
this.eventListeners.sigterm = () => {
|
||||
this.flush();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('beforeExit', this.eventListeners.beforeExit);
|
||||
process.on('SIGINT', this.eventListeners.sigint);
|
||||
process.on('SIGTERM', this.eventListeners.sigterm);
|
||||
|
||||
this.started = true;
|
||||
logger.debug('Telemetry batch processor started');
|
||||
}
|
||||
|
||||
@@ -105,6 +123,20 @@ export class TelemetryBatchProcessor {
|
||||
clearInterval(this.flushTimer);
|
||||
this.flushTimer = undefined;
|
||||
}
|
||||
|
||||
// Remove event listeners to prevent memory leaks
|
||||
if (this.eventListeners.beforeExit) {
|
||||
process.removeListener('beforeExit', this.eventListeners.beforeExit);
|
||||
}
|
||||
if (this.eventListeners.sigint) {
|
||||
process.removeListener('SIGINT', this.eventListeners.sigint);
|
||||
}
|
||||
if (this.eventListeners.sigterm) {
|
||||
process.removeListener('SIGTERM', this.eventListeners.sigterm);
|
||||
}
|
||||
this.eventListeners = {};
|
||||
this.started = false;
|
||||
|
||||
logger.debug('Telemetry batch processor stopped');
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { isTypeStructure } from '@/types/type-structures';
|
||||
import type { NodePropertyTypes } from 'n8n-workflow';
|
||||
|
||||
describe('TYPE_STRUCTURES', () => {
|
||||
// All 22 NodePropertyTypes from n8n-workflow
|
||||
// All 23 NodePropertyTypes from n8n-workflow
|
||||
const ALL_PROPERTY_TYPES: NodePropertyTypes[] = [
|
||||
'boolean',
|
||||
'button',
|
||||
@@ -20,6 +20,7 @@ describe('TYPE_STRUCTURES', () => {
|
||||
'dateTime',
|
||||
'fixedCollection',
|
||||
'hidden',
|
||||
'icon',
|
||||
'json',
|
||||
'callout',
|
||||
'notice',
|
||||
@@ -38,16 +39,16 @@ describe('TYPE_STRUCTURES', () => {
|
||||
];
|
||||
|
||||
describe('Completeness', () => {
|
||||
it('should define all 22 NodePropertyTypes', () => {
|
||||
it('should define all 23 NodePropertyTypes', () => {
|
||||
const definedTypes = Object.keys(TYPE_STRUCTURES);
|
||||
expect(definedTypes).toHaveLength(22);
|
||||
expect(definedTypes).toHaveLength(23);
|
||||
|
||||
for (const type of ALL_PROPERTY_TYPES) {
|
||||
expect(TYPE_STRUCTURES).toHaveProperty(type);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not have extra types beyond the 22 standard types', () => {
|
||||
it('should not have extra types beyond the 23 standard types', () => {
|
||||
const definedTypes = Object.keys(TYPE_STRUCTURES);
|
||||
const extraTypes = definedTypes.filter((type) => !ALL_PROPERTY_TYPES.includes(type as NodePropertyTypes));
|
||||
|
||||
|
||||
302
tests/unit/database/shared-database.test.ts
Normal file
302
tests/unit/database/shared-database.test.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock dependencies at module level
|
||||
const mockDb = {
|
||||
prepare: vi.fn().mockReturnValue({
|
||||
get: vi.fn(),
|
||||
all: vi.fn(),
|
||||
run: vi.fn()
|
||||
}),
|
||||
exec: vi.fn(),
|
||||
close: vi.fn(),
|
||||
pragma: vi.fn(),
|
||||
inTransaction: false,
|
||||
transaction: vi.fn(),
|
||||
checkFTS5Support: vi.fn()
|
||||
};
|
||||
|
||||
vi.mock('../../../src/database/database-adapter', () => ({
|
||||
createDatabaseAdapter: vi.fn().mockResolvedValue(mockDb)
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/database/node-repository', () => ({
|
||||
NodeRepository: vi.fn().mockImplementation(() => ({
|
||||
getNodeTypes: vi.fn().mockReturnValue([])
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/templates/template-service', () => ({
|
||||
TemplateService: vi.fn().mockImplementation(() => ({}))
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/services/enhanced-config-validator', () => ({
|
||||
EnhancedConfigValidator: {
|
||||
initializeSimilarityServices: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/utils/logger', () => ({
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
describe('Shared Database Module', () => {
|
||||
let sharedDbModule: typeof import('../../../src/database/shared-database');
|
||||
let createDatabaseAdapter: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks();
|
||||
mockDb.close.mockReset();
|
||||
|
||||
// Reset modules to get fresh state
|
||||
vi.resetModules();
|
||||
|
||||
// Import fresh module
|
||||
sharedDbModule = await import('../../../src/database/shared-database');
|
||||
|
||||
// Get the mocked function
|
||||
const adapterModule = await import('../../../src/database/database-adapter');
|
||||
createDatabaseAdapter = adapterModule.createDatabaseAdapter as ReturnType<typeof vi.fn>;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up any shared state by closing
|
||||
try {
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
});
|
||||
|
||||
describe('getSharedDatabase', () => {
|
||||
it('should initialize database on first call', async () => {
|
||||
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
|
||||
expect(state).toBeDefined();
|
||||
expect(state.db).toBe(mockDb);
|
||||
expect(state.dbPath).toBe('/path/to/db');
|
||||
expect(state.refCount).toBe(1);
|
||||
expect(state.initialized).toBe(true);
|
||||
expect(createDatabaseAdapter).toHaveBeenCalledWith('/path/to/db');
|
||||
});
|
||||
|
||||
it('should reuse existing connection and increment refCount', async () => {
|
||||
// First call initializes
|
||||
const state1 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(state1.refCount).toBe(1);
|
||||
|
||||
// Second call reuses
|
||||
const state2 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(state2.refCount).toBe(2);
|
||||
|
||||
// Same object
|
||||
expect(state1).toBe(state2);
|
||||
|
||||
// Only initialized once
|
||||
expect(createDatabaseAdapter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw error when called with different path', async () => {
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db1');
|
||||
|
||||
await expect(sharedDbModule.getSharedDatabase('/path/to/db2'))
|
||||
.rejects.toThrow('Shared database already initialized with different path');
|
||||
});
|
||||
|
||||
it('should handle concurrent initialization requests', async () => {
|
||||
// Start two requests concurrently
|
||||
const [state1, state2] = await Promise.all([
|
||||
sharedDbModule.getSharedDatabase('/path/to/db'),
|
||||
sharedDbModule.getSharedDatabase('/path/to/db')
|
||||
]);
|
||||
|
||||
// Both should get the same state
|
||||
expect(state1).toBe(state2);
|
||||
|
||||
// RefCount should be 2 (one for each call)
|
||||
expect(state1.refCount).toBe(2);
|
||||
|
||||
// Only one actual initialization
|
||||
expect(createDatabaseAdapter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle initialization failure', async () => {
|
||||
createDatabaseAdapter.mockRejectedValueOnce(new Error('DB error'));
|
||||
|
||||
await expect(sharedDbModule.getSharedDatabase('/path/to/db'))
|
||||
.rejects.toThrow('DB error');
|
||||
|
||||
// After failure, should not be initialized
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow retry after initialization failure', async () => {
|
||||
// First call fails
|
||||
createDatabaseAdapter.mockRejectedValueOnce(new Error('DB error'));
|
||||
await expect(sharedDbModule.getSharedDatabase('/path/to/db'))
|
||||
.rejects.toThrow('DB error');
|
||||
|
||||
// Reset mock for successful call
|
||||
createDatabaseAdapter.mockResolvedValueOnce(mockDb);
|
||||
|
||||
// Second call succeeds
|
||||
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
|
||||
expect(state).toBeDefined();
|
||||
expect(state.initialized).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('releaseSharedDatabase', () => {
|
||||
it('should decrement refCount', async () => {
|
||||
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(state.refCount).toBe(1);
|
||||
|
||||
sharedDbModule.releaseSharedDatabase(state);
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should not decrement below 0', async () => {
|
||||
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
|
||||
// Release once (refCount: 1 -> 0)
|
||||
sharedDbModule.releaseSharedDatabase(state);
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
||||
|
||||
// Release again (should stay at 0, not go negative)
|
||||
sharedDbModule.releaseSharedDatabase(state);
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle null state gracefully', () => {
|
||||
// Should not throw
|
||||
sharedDbModule.releaseSharedDatabase(null as any);
|
||||
});
|
||||
|
||||
it('should not close database when refCount hits 0', async () => {
|
||||
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
sharedDbModule.releaseSharedDatabase(state);
|
||||
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
||||
expect(mockDb.close).not.toHaveBeenCalled();
|
||||
|
||||
// Database should still be accessible
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeSharedDatabase', () => {
|
||||
it('should close database and clear state', async () => {
|
||||
// Get state
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(1);
|
||||
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
|
||||
// State should be cleared
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle close error gracefully', async () => {
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
mockDb.close.mockImplementationOnce(() => {
|
||||
throw new Error('Close error');
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
|
||||
// State should still be cleared
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be idempotent when already closed', async () => {
|
||||
// Close without ever initializing
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
|
||||
// Should not throw
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
});
|
||||
|
||||
it('should allow re-initialization after close', async () => {
|
||||
// Initialize
|
||||
const state1 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(state1.refCount).toBe(1);
|
||||
|
||||
// Close
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
||||
|
||||
// Re-initialize
|
||||
const state2 = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(state2.refCount).toBe(1);
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
||||
|
||||
// Should be a new state object
|
||||
expect(state1).not.toBe(state2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSharedDatabaseInitialized', () => {
|
||||
it('should return false before initialization', () => {
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true after initialization', async () => {
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false after close', async () => {
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
expect(sharedDbModule.isSharedDatabaseInitialized()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSharedDatabaseRefCount', () => {
|
||||
it('should return 0 before initialization', () => {
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return correct refCount after multiple operations', async () => {
|
||||
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(1);
|
||||
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(2);
|
||||
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(3);
|
||||
|
||||
sharedDbModule.releaseSharedDatabase(state);
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 after close', async () => {
|
||||
await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
await sharedDbModule.closeSharedDatabase();
|
||||
expect(sharedDbModule.getSharedDatabaseRefCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SharedDatabaseState interface', () => {
|
||||
it('should expose correct properties', async () => {
|
||||
const state = await sharedDbModule.getSharedDatabase('/path/to/db');
|
||||
|
||||
expect(state).toHaveProperty('db');
|
||||
expect(state).toHaveProperty('repository');
|
||||
expect(state).toHaveProperty('templateService');
|
||||
expect(state).toHaveProperty('dbPath');
|
||||
expect(state).toHaveProperty('refCount');
|
||||
expect(state).toHaveProperty('initialized');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -333,13 +333,14 @@ describe('HTTP Server Session Management', () => {
|
||||
server = new SingleSessionHTTPServer();
|
||||
|
||||
// Mock expired sessions
|
||||
// Note: Default session timeout is 5 minutes (configurable via SESSION_TIMEOUT_MINUTES)
|
||||
const mockSessionMetadata = {
|
||||
'session-1': {
|
||||
lastAccess: new Date(Date.now() - 40 * 60 * 1000), // 40 minutes ago (expired)
|
||||
'session-1': {
|
||||
lastAccess: new Date(Date.now() - 10 * 60 * 1000), // 10 minutes ago (expired with 5 min timeout)
|
||||
createdAt: new Date(Date.now() - 60 * 60 * 1000)
|
||||
},
|
||||
'session-2': {
|
||||
lastAccess: new Date(Date.now() - 10 * 60 * 1000), // 10 minutes ago (not expired)
|
||||
'session-2': {
|
||||
lastAccess: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago (not expired with 5 min timeout)
|
||||
createdAt: new Date(Date.now() - 20 * 60 * 1000)
|
||||
}
|
||||
};
|
||||
@@ -514,15 +515,16 @@ describe('HTTP Server Session Management', () => {
|
||||
|
||||
it('should get session metrics correctly', async () => {
|
||||
server = new SingleSessionHTTPServer();
|
||||
|
||||
|
||||
// Note: Default session timeout is 5 minutes (configurable via SESSION_TIMEOUT_MINUTES)
|
||||
const now = Date.now();
|
||||
(server as any).sessionMetadata = {
|
||||
'active-session': {
|
||||
lastAccess: new Date(now - 10 * 60 * 1000), // 10 minutes ago
|
||||
lastAccess: new Date(now - 2 * 60 * 1000), // 2 minutes ago (not expired with 5 min timeout)
|
||||
createdAt: new Date(now - 20 * 60 * 1000)
|
||||
},
|
||||
'expired-session': {
|
||||
lastAccess: new Date(now - 40 * 60 * 1000), // 40 minutes ago (expired)
|
||||
lastAccess: new Date(now - 10 * 60 * 1000), // 10 minutes ago (expired with 5 min timeout)
|
||||
createdAt: new Date(now - 60 * 60 * 1000)
|
||||
}
|
||||
};
|
||||
@@ -532,7 +534,7 @@ describe('HTTP Server Session Management', () => {
|
||||
};
|
||||
|
||||
const metrics = (server as any).getSessionMetrics();
|
||||
|
||||
|
||||
expect(metrics.totalSessions).toBe(2);
|
||||
expect(metrics.activeSessions).toBe(2);
|
||||
expect(metrics.expiredSessions).toBe(1);
|
||||
|
||||
@@ -58,9 +58,9 @@ describe('TypeStructureService', () => {
|
||||
});
|
||||
|
||||
describe('getAllStructures', () => {
|
||||
it('should return all 22 type structures', () => {
|
||||
it('should return all 23 type structures', () => {
|
||||
const structures = TypeStructureService.getAllStructures();
|
||||
expect(Object.keys(structures)).toHaveLength(22);
|
||||
expect(Object.keys(structures)).toHaveLength(23);
|
||||
});
|
||||
|
||||
it('should return a copy not a reference', () => {
|
||||
|
||||
Reference in New Issue
Block a user