19 Commits

Author SHA1 Message Date
czlonkowski
6d95786938 2.10.2 2025-08-05 08:09:22 +02:00
czlonkowski
21d4b9b9fb chore: update n8n to v1.105.2 and dependencies
- Updated n8n from 1.104.1 to 1.105.2
- Updated n8n-core from 1.103.1 to 1.104.1
- Updated n8n-workflow from 1.101.0 to 1.102.1
- Updated @n8n/n8n-nodes-langchain from 1.103.1 to 1.104.1
- Rebuilt node database with 534 nodes
- All 1,620 tests passing
- Updated CHANGELOG.md

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-05 08:09:08 +02:00
Romuald Członkowski
035c4a349e Merge pull request #121 from czlonkowski/fix/ci-skip-docs-only-changes
fix: skip CI/CD workflows for documentation-only changes
2025-08-02 22:57:58 +02:00
czlonkowski
08f3d8120d fix: skip CI/CD workflows for documentation-only changes
- Add comprehensive paths-ignore to all workflows to skip runs when only docs are changed
- Standardize pattern ordering across all workflow files
- Fix redundant path configuration in benchmark-pr.yml
- Add support for more documentation file types (*.txt, examples/**, .gitignore, etc.)
- Ensure LICENSE* pattern covers all license file variants

This optimization saves CI/CD minutes and reduces costs by avoiding unnecessary
test runs, Docker builds, and benchmarks for documentation-only commits.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 22:23:15 +02:00
czlonkowski
4b1aaa936d update documentation 2025-08-02 21:56:50 +02:00
czlonkowski
e94bb5479c Fix deploy on Railway button 2025-08-02 21:42:00 +02:00
czlonkowski
1a99e9c6c7 fix: resolve YAML syntax error in release workflow
- Fix GitHub Actions expression in shell script by using env variable
- Prevents YAML parsing error on line 452
- Ensures workflow can execute properly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 21:34:49 +02:00
czlonkowski
7dc938065f fix: resolve YAML syntax error in release workflow
- Fix multiline commit message syntax that was breaking YAML parsing
- Add missing GITHUB_TOKEN environment variable for gh CLI commands
- Simplify commit message to avoid YAML parsing issues

The workflow was failing due to unescaped multiline string in git commit command.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 21:32:27 +02:00
czlonkowski
8022ee1f65 feat: add automated release workflow for npm publishing
- Add release.yml GitHub workflow for automated npm releases
- Add prepare-release.js script for version bumping and changelog
- Add extract-changelog.js for release notes extraction
- Add test-release-automation.js for testing the workflow
- Add documentation for automated releases

This enables automatic npm publishing when tags are pushed,
fixing the issue where releases were created but npm packages
were not published.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 21:14:00 +02:00
Romuald Członkowski
9e71c71698 Merge pull request #120 from czlonkowski/fix/issue-118-mcp-connection-loss
### Fixed
- **Memory Leak in SimpleCache**: Fixed critical memory leak causing MCP server connection loss after several hours (fixes #118)
  - Added proper timer cleanup in `SimpleCache.destroy()` method
  - Updated MCP server shutdown to clean up cache timers
  - Enhanced HTTP server error handling with transport error handlers
  - Fixed event listener cleanup to prevent accumulation
  - Added comprehensive test coverage for memory leak prevention
2025-08-02 15:24:53 +02:00
czlonkowski
df4066022f chore: bump version to 2.10.1 for memory leak fix release
- Updated version in package.json and package.runtime.json
- Updated README version badge
- Moved changelog entry from Unreleased to v2.10.1
- Added version comparison link for v2.10.1

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 15:18:58 +02:00
czlonkowski
7a71c3c3f8 fix: memory leak in SimpleCache causing MCP connection loss (fixes #118)
- Added cleanupTimer property to track setInterval timer
- Implemented destroy() method to clear timer and prevent memory leak
- Updated MCP server shutdown to call cache.destroy()
- Enhanced HTTP server error handling with transport.onerror
- Fixed event listener cleanup to prevent accumulation
- Added comprehensive test coverage for memory leak prevention

This fixes the issue where MCP server would lose connection after
several hours due to timer accumulation causing memory exhaustion.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 14:45:58 +02:00
Romuald Członkowski
3bfad51519 Merge pull request #119 from czlonkowski/ci-cd
feat: Add automated release system with CI/CD pipeline
2025-08-02 13:03:38 +02:00
czlonkowski
907d3846a9 chore: release v2.10.0
- Added automated release system with GitHub Actions
- Implemented CI/CD pipeline for zero-touch releases
- Added security fixes for deprecated actions and vulnerabilities
- Created developer tools for release preparation
- Full documentation in docs/AUTOMATED_RELEASES.md
2025-08-02 12:37:18 +02:00
Romuald Członkowski
6de82cd2b9 Merge pull request #117 from czlonkowski/bugfix/general-fixes
fix: Docker build failures and outdated pre-built images
2025-08-02 12:01:16 +02:00
czlonkowski
6856add177 fix: address code review feedback for Docker consolidation
- Improved GitHub Actions test to verify N8N_MODE environment variable
- Added explanatory comment in docker-compose.n8n.yml
- Added Docker Build Changes section to deployment documentation
- Explains the consolidation benefits and rationale for users
2025-08-02 11:54:33 +02:00
czlonkowski
3eecda4bd5 refactor: consolidate Docker builds by removing redundant Dockerfile.n8n
- Research proved n8n packages are NOT required at runtime for N8N_MODE
- The 'n8n' CMD argument was vestigial and completely ignored by code
- N8N_MODE only affects protocol negotiation, not runtime functionality
- Standard Dockerfile works perfectly with N8N_MODE=true

Benefits:
- Eliminates 500MB+ of unnecessary n8n packages from Docker images
- Reduces build time from 8+ minutes to 1-2 minutes
- Simplifies maintenance with single Dockerfile
- Improves CI/CD reliability

Updated:
- Removed Dockerfile.n8n
- Updated GitHub Actions to use standard Dockerfile
- Fixed docker-compose.n8n.yml to use standard Dockerfile
- Added missing MCP_MODE=http and AUTH_TOKEN env vars
- Updated all documentation references
2025-08-02 11:52:04 +02:00
czlonkowski
1c6bff7d42 fix: add missing axios dependency to runtime dependencies
- The Docker build was failing because axios is used by n8n-api-client.ts
- This dependency was missing from package.runtime.json causing container startup failures
- Fixes the Docker CI/CD pipeline that was stuck at v2.3.0
2025-08-02 11:15:14 +02:00
czlonkowski
8864d6fa5c fix: resolve Docker CI/CD and deployment documentation issues
- Create missing v2.9.1 git tag to trigger Docker builds
- Fix GitHub Actions workflow with proper environment variables
- Add comprehensive deployment documentation updates:
  * Add missing MCP_MODE=http environment variable requirement
  * Clarify Server URL must include /mcp endpoint
  * Add complete environment variables reference table
  * Update all Docker examples with proper variable configuration
  * Add version compatibility warnings for pre-built images
  * Document build-from-source as recommended approach
  * Add comprehensive troubleshooting section with common issues
  * Include systematic debugging steps and diagnostic commands
- Optimize package.runtime.json dependencies for Docker builds
- Ensure both MCP_AUTH_TOKEN and AUTH_TOKEN use same value

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 11:11:57 +02:00
24 changed files with 6120 additions and 473 deletions

View File

@@ -2,11 +2,19 @@ name: Benchmark PR Comparison
on:
pull_request:
branches: [main]
paths:
- 'src/**'
- 'tests/benchmarks/**'
- 'package.json'
- 'vitest.config.benchmark.ts'
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
permissions:
pull-requests: write

View File

@@ -3,8 +3,34 @@ name: Performance Benchmarks
on:
push:
branches: [main, feat/comprehensive-testing-suite]
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
pull_request:
branches: [main]
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
workflow_dispatch:
permissions:

View File

@@ -6,9 +6,35 @@ on:
- main
tags:
- 'v*'
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
pull_request:
branches:
- main
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
workflow_dispatch:
env:
@@ -53,7 +79,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.n8n
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -82,13 +108,16 @@ jobs:
- name: Test Docker image
run: |
# Test that the image starts correctly with N8N_MODE
docker run --rm \
-e N8N_MODE=true \
-e MCP_MODE=http \
-e N8N_API_URL=http://localhost:5678 \
-e N8N_API_KEY=test \
-e MCP_AUTH_TOKEN=test \
-e MCP_AUTH_TOKEN=test-token-minimum-32-chars-long \
-e AUTH_TOKEN=test-token-minimum-32-chars-long \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
node dist/index.js n8n --version
node -e "console.log('N8N_MODE:', process.env.N8N_MODE); process.exit(0);"
- name: Test health endpoint
run: |
@@ -97,9 +126,11 @@ jobs:
--name n8n-mcp-test \
-p 3000:3000 \
-e N8N_MODE=true \
-e MCP_MODE=http \
-e N8N_API_URL=http://localhost:5678 \
-e N8N_API_KEY=test \
-e MCP_AUTH_TOKEN=test \
-e MCP_AUTH_TOKEN=test-token-minimum-32-chars-long \
-e AUTH_TOKEN=test-token-minimum-32-chars-long \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# Wait for container to start
@@ -108,6 +139,9 @@ jobs:
# Test health endpoint
curl -f http://localhost:3000/health || exit 1
# Test MCP endpoint
curl -f http://localhost:3000/mcp || exit 1
# Cleanup
docker stop n8n-mcp-test
docker rm n8n-mcp-test

View File

@@ -9,23 +9,33 @@ on:
- 'v*'
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- 'LICENSE'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'docs/**'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
pull_request:
branches:
- main
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- 'LICENSE'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'docs/**'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
workflow_dispatch:
env:

526
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,526 @@
name: Automated Release
on:
push:
branches: [main]
paths:
- 'package.json'
- 'package.runtime.json'
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
permissions:
contents: write
packages: write
issues: write
pull-requests: write
# Prevent concurrent releases
concurrency:
group: release
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
detect-version-change:
name: Detect Version Change
runs-on: ubuntu-latest
outputs:
version-changed: ${{ steps.check.outputs.changed }}
new-version: ${{ steps.check.outputs.version }}
previous-version: ${{ steps.check.outputs.previous-version }}
is-prerelease: ${{ steps.check.outputs.is-prerelease }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check for version change
id: check
run: |
# Get current version from package.json
CURRENT_VERSION=$(node -e "console.log(require('./package.json').version)")
# Get previous version from git history safely
PREVIOUS_VERSION=$(git show HEAD~1:package.json 2>/dev/null | node -e "
try {
const data = require('fs').readFileSync(0, 'utf8');
const pkg = JSON.parse(data);
console.log(pkg.version || '0.0.0');
} catch (e) {
console.log('0.0.0');
}
" || echo "0.0.0")
echo "Previous version: $PREVIOUS_VERSION"
echo "Current version: $CURRENT_VERSION"
# Check if version changed
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "previous-version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT
# Check if it's a prerelease (contains alpha, beta, rc, dev)
if echo "$CURRENT_VERSION" | grep -E "(alpha|beta|rc|dev)" > /dev/null; then
echo "is-prerelease=true" >> $GITHUB_OUTPUT
else
echo "is-prerelease=false" >> $GITHUB_OUTPUT
fi
echo "🎉 Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "previous-version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT
echo "is-prerelease=false" >> $GITHUB_OUTPUT
echo " No version change detected"
fi
extract-changelog:
name: Extract Changelog
runs-on: ubuntu-latest
needs: detect-version-change
if: needs.detect-version-change.outputs.version-changed == 'true'
outputs:
release-notes: ${{ steps.extract.outputs.notes }}
has-notes: ${{ steps.extract.outputs.has-notes }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract changelog for version
id: extract
run: |
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
CHANGELOG_FILE="docs/CHANGELOG.md"
if [ ! -f "$CHANGELOG_FILE" ]; then
echo "Changelog file not found at $CHANGELOG_FILE"
echo "has-notes=false" >> $GITHUB_OUTPUT
echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT
exit 0
fi
# Use the extracted changelog script
if NOTES=$(node scripts/extract-changelog.js "$VERSION" "$CHANGELOG_FILE" 2>/dev/null); then
echo "has-notes=true" >> $GITHUB_OUTPUT
# Use heredoc to properly handle multiline content
{
echo "notes<<EOF"
echo "$NOTES"
echo "EOF"
} >> $GITHUB_OUTPUT
echo "✅ Successfully extracted changelog for version $VERSION"
else
echo "has-notes=false" >> $GITHUB_OUTPUT
echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT
echo "⚠️ Could not extract changelog for version $VERSION"
fi
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [detect-version-change, extract-changelog]
if: needs.detect-version-change.outputs.version-changed == 'true'
outputs:
release-id: ${{ steps.create.outputs.id }}
upload-url: ${{ steps.create.outputs.upload_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Create Git Tag
run: |
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Create annotated tag
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
- name: Create GitHub Release
id: create
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
IS_PRERELEASE="${{ needs.detect-version-change.outputs.is-prerelease }}"
# Create release body
cat > release_body.md << 'EOF'
# Release v${{ needs.detect-version-change.outputs.new-version }}
${{ needs.extract-changelog.outputs.release-notes }}
---
## Installation
### NPM Package
```bash
# Install globally
npm install -g n8n-mcp
# Or run directly
npx n8n-mcp
```
### Docker
```bash
# Standard image
docker run -p 3000:3000 ghcr.io/czlonkowski/n8n-mcp:v${{ needs.detect-version-change.outputs.new-version }}
# Railway optimized
docker run -p 3000:3000 ghcr.io/czlonkowski/n8n-mcp-railway:v${{ needs.detect-version-change.outputs.new-version }}
```
## Documentation
- [Installation Guide](https://github.com/czlonkowski/n8n-mcp#installation)
- [Docker Deployment](https://github.com/czlonkowski/n8n-mcp/blob/main/docs/DOCKER_README.md)
- [n8n Integration](https://github.com/czlonkowski/n8n-mcp/blob/main/docs/N8N_DEPLOYMENT.md)
- [Complete Changelog](https://github.com/czlonkowski/n8n-mcp/blob/main/docs/CHANGELOG.md)
🤖 *Generated with [Claude Code](https://claude.ai/code)*
EOF
# Create release using gh CLI
if [ "$IS_PRERELEASE" = "true" ]; then
PRERELEASE_FLAG="--prerelease"
else
PRERELEASE_FLAG=""
fi
gh release create "v$VERSION" \
--title "Release v$VERSION" \
--notes-file release_body.md \
$PRERELEASE_FLAG
# Output release info for next jobs
RELEASE_ID=$(gh release view "v$VERSION" --json id --jq '.id')
echo "id=$RELEASE_ID" >> $GITHUB_OUTPUT
echo "upload_url=https://uploads.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets{?name,label}" >> $GITHUB_OUTPUT
build-and-test:
name: Build and Test
runs-on: ubuntu-latest
needs: detect-version-change
if: needs.detect-version-change.outputs.version-changed == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Rebuild database
run: npm run rebuild
- name: Run tests
run: npm test
env:
CI: true
- name: Run type checking
run: npm run typecheck
publish-npm:
name: Publish to NPM
runs-on: ubuntu-latest
needs: [detect-version-change, build-and-test, create-release]
if: needs.detect-version-change.outputs.version-changed == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Rebuild database
run: npm run rebuild
- name: Sync runtime version
run: npm run sync:runtime-version
- name: Prepare package for publishing
run: |
# Create publish directory
PUBLISH_DIR="npm-publish-temp"
rm -rf $PUBLISH_DIR
mkdir -p $PUBLISH_DIR
# Copy necessary files
cp -r dist $PUBLISH_DIR/
cp -r data $PUBLISH_DIR/
cp README.md $PUBLISH_DIR/
cp LICENSE $PUBLISH_DIR/
cp .env.example $PUBLISH_DIR/
# Use runtime package.json as base
cp package.runtime.json $PUBLISH_DIR/package.json
cd $PUBLISH_DIR
# Update package.json with complete metadata
node -e "
const pkg = require('./package.json');
pkg.name = 'n8n-mcp';
pkg.description = 'Integration between n8n workflow automation and Model Context Protocol (MCP)';
pkg.bin = { 'n8n-mcp': './dist/mcp/index.js' };
pkg.repository = { type: 'git', url: 'git+https://github.com/czlonkowski/n8n-mcp.git' };
pkg.keywords = ['n8n', 'mcp', 'model-context-protocol', 'ai', 'workflow', 'automation'];
pkg.author = 'Romuald Czlonkowski @ www.aiadvisors.pl/en';
pkg.license = 'MIT';
pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' };
pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme';
pkg.files = ['dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE'];
delete pkg.private;
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
"
echo "Package prepared for publishing:"
echo "Name: $(node -e "console.log(require('./package.json').name)")"
echo "Version: $(node -e "console.log(require('./package.json').version)")"
- name: Publish to NPM with retry
uses: nick-invision/retry@v2
with:
timeout_minutes: 5
max_attempts: 3
command: |
cd npm-publish-temp
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Clean up
if: always()
run: rm -rf npm-publish-temp
build-docker:
name: Build and Push Docker Images
runs-on: ubuntu-latest
needs: [detect-version-change, build-and-test]
if: needs.detect-version-change.outputs.version-changed == 'true'
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true
- name: Check disk space
run: |
echo "Disk usage before Docker build:"
df -h
# Check available space (require at least 2GB)
AVAILABLE_GB=$(df / --output=avail --block-size=1G | tail -1)
if [ "$AVAILABLE_GB" -lt 2 ]; then
echo "❌ Insufficient disk space: ${AVAILABLE_GB}GB available, 2GB required"
exit 1
fi
echo "✅ Sufficient disk space: ${AVAILABLE_GB}GB available"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for standard image
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}},value=v${{ needs.detect-version-change.outputs.new-version }}
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.detect-version-change.outputs.new-version }}
type=semver,pattern={{major}},value=v${{ needs.detect-version-change.outputs.new-version }}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push standard Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata for Railway image
id: meta-railway
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-railway
tags: |
type=semver,pattern={{version}},value=v${{ needs.detect-version-change.outputs.new-version }}
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.detect-version-change.outputs.new-version }}
type=semver,pattern={{major}},value=v${{ needs.detect-version-change.outputs.new-version }}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Railway Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.railway
platforms: linux/amd64
push: true
tags: ${{ steps.meta-railway.outputs.tags }}
labels: ${{ steps.meta-railway.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
update-documentation:
name: Update Documentation
runs-on: ubuntu-latest
needs: [detect-version-change, create-release, publish-npm, build-docker]
if: needs.detect-version-change.outputs.version-changed == 'true' && !failure()
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Update version badges in README
run: |
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
# Update README version badges
if [ -f "README.md" ]; then
# Update npm version badge
sed -i.bak "s|npm/v/n8n-mcp/[^)]*|npm/v/n8n-mcp/$VERSION|g" README.md
# Update any other version references
sed -i.bak "s|version-[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*|version-$VERSION|g" README.md
# Clean up backup file
rm -f README.md.bak
echo "✅ Updated version badges in README.md to $VERSION"
fi
- name: Commit documentation updates
env:
VERSION: ${{ needs.detect-version-change.outputs.new-version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No documentation changes to commit"
else
git add README.md
git commit -m "docs: update version badges to v${VERSION}"
git push
echo "✅ Committed documentation updates"
fi
notify-completion:
name: Notify Release Completion
runs-on: ubuntu-latest
needs: [detect-version-change, create-release, publish-npm, build-docker, update-documentation]
if: always() && needs.detect-version-change.outputs.version-changed == 'true'
steps:
- name: Create release summary
run: |
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/v$VERSION"
echo "## 🎉 Release v$VERSION Published Successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### ✅ Completed Tasks:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Check job statuses
if [ "${{ needs.create-release.result }}" = "success" ]; then
echo "- ✅ GitHub Release created: [$RELEASE_URL]($RELEASE_URL)" >> $GITHUB_STEP_SUMMARY
else
echo "- ❌ GitHub Release creation failed" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.publish-npm.result }}" = "success" ]; then
echo "- ✅ NPM package published: [npmjs.com/package/n8n-mcp](https://www.npmjs.com/package/n8n-mcp)" >> $GITHUB_STEP_SUMMARY
else
echo "- ❌ NPM publishing failed" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.build-docker.result }}" = "success" ]; then
echo "- ✅ Docker images built and pushed" >> $GITHUB_STEP_SUMMARY
echo " - Standard: \`ghcr.io/czlonkowski/n8n-mcp:v$VERSION\`" >> $GITHUB_STEP_SUMMARY
echo " - Railway: \`ghcr.io/czlonkowski/n8n-mcp-railway:v$VERSION\`" >> $GITHUB_STEP_SUMMARY
else
echo "- ❌ Docker image building failed" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.update-documentation.result }}" = "success" ]; then
echo "- ✅ Documentation updated" >> $GITHUB_STEP_SUMMARY
else
echo "- ⚠️ Documentation update skipped or failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Installation:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "# NPM" >> $GITHUB_STEP_SUMMARY
echo "npx n8n-mcp" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "# Docker" >> $GITHUB_STEP_SUMMARY
echo "docker run -p 3000:3000 ghcr.io/czlonkowski/n8n-mcp:v$VERSION" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "🎉 Release automation completed for v$VERSION!"

View File

@@ -2,8 +2,34 @@ name: Test Suite
on:
push:
branches: [main, feat/comprehensive-testing-suite]
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
pull_request:
branches: [main]
paths-ignore:
- '**.md'
- '**.txt'
- 'docs/**'
- 'examples/**'
- '.github/FUNDING.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'
- '.gitignore'
- 'LICENSE*'
- 'ATTRIBUTION.md'
- 'SECURITY.md'
- 'CODE_OF_CONDUCT.md'
permissions:
contents: read

View File

@@ -1,79 +0,0 @@
# Multi-stage Dockerfile optimized for n8n integration
# Stage 1: Build stage
FROM node:20-alpine AS builder
# Install build dependencies
RUN apk add --no-cache python3 make g++ git
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install all dependencies (including dev deps for building)
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Stage 2: Production stage
FROM node:20-alpine
# Install runtime dependencies
RUN apk add --no-cache \
curl \
tini \
&& rm -rf /var/cache/apk/*
# Create non-root user with unpredictable UID/GID
# Using a hash of the build time to generate unpredictable IDs
RUN BUILD_HASH=$(date +%s | sha256sum | head -c 8) && \
UID=$((10000 + 0x${BUILD_HASH} % 50000)) && \
GID=$((10000 + 0x${BUILD_HASH} % 50000)) && \
addgroup -g ${GID} n8n-mcp && \
adduser -u ${UID} -G n8n-mcp -s /bin/sh -D n8n-mcp
# Set working directory
WORKDIR /app
# Copy package files (use runtime-only dependencies)
COPY package.runtime.json package.json
# Install production dependencies only
RUN npm install --production --no-audit --no-fund && \
npm cache clean --force
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/data ./data
# Create necessary directories and set permissions
RUN mkdir -p /app/logs /app/data && \
chown -R n8n-mcp:n8n-mcp /app
# Switch to non-root user
USER n8n-mcp
# Set environment variables for n8n mode
ENV NODE_ENV=production \
N8N_MODE=true \
N8N_API_URL="" \
N8N_API_KEY="" \
PORT=3000
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:${PORT}/health || exit 1
# Use tini for proper signal handling
ENTRYPOINT ["/sbin/tini", "--"]
# Start the application in n8n mode
CMD ["node", "dist/index.js", "n8n"]

View File

@@ -2,13 +2,13 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.9.1-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.10.1-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp)
[![codecov](https://codecov.io/gh/czlonkowski/n8n-mcp/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/czlonkowski/n8n-mcp)
[![Tests](https://img.shields.io/badge/tests-1356%20passing-brightgreen.svg)](https://github.com/czlonkowski/n8n-mcp/actions)
[![n8n version](https://img.shields.io/badge/n8n-^1.104.1-orange.svg)](https://github.com/n8n-io/n8n)
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/VY6UOG?referralCode=n8n-mcp)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 525+ workflow automation nodes.
@@ -296,7 +296,7 @@ Add to Claude Desktop config:
Deploy n8n-MCP to Railway's cloud platform with zero configuration:
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/VY6UOG?referralCode=n8n-mcp)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
**Benefits:**
- ☁️ **Instant cloud hosting** - No server setup required
@@ -781,6 +781,26 @@ Contributions are welcome! Please:
3. Run tests (`npm test`)
4. Submit a pull request
### 🚀 For Maintainers: Automated Releases
This project uses automated releases triggered by version changes:
```bash
# Guided release preparation
npm run prepare:release
# Test release automation
npm run test:release-automation
```
The system automatically handles:
- 🏷️ GitHub releases with changelog content
- 📦 NPM package publishing
- 🐳 Multi-platform Docker images
- 📚 Documentation updates
See [Automated Release Guide](./docs/AUTOMATED_RELEASES.md) for complete details.
## 👏 Acknowledgments
- [n8n](https://n8n.io) team for the workflow automation platform

Binary file not shown.

View File

@@ -32,7 +32,7 @@ services:
n8n-mcp:
build:
context: .
dockerfile: Dockerfile.n8n
dockerfile: Dockerfile # Uses standard Dockerfile with N8N_MODE=true env var
image: ghcr.io/${GITHUB_REPOSITORY:-czlonkowski/n8n-mcp}/n8n-mcp:${VERSION:-latest}
container_name: n8n-mcp
restart: unless-stopped
@@ -41,9 +41,11 @@ services:
environment:
- NODE_ENV=production
- N8N_MODE=true
- MCP_MODE=http
- N8N_API_URL=http://n8n:5678
- N8N_API_KEY=${N8N_API_KEY}
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
- AUTH_TOKEN=${MCP_AUTH_TOKEN}
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- ./data:/app/data:ro

384
docs/AUTOMATED_RELEASES.md Normal file
View File

@@ -0,0 +1,384 @@
# Automated Release Process
This document describes the automated release system for n8n-mcp, which handles version detection, changelog parsing, and multi-artifact publishing.
## Overview
The automated release system is triggered when the version in `package.json` is updated and pushed to the main branch. It handles:
- 🏷️ **GitHub Releases**: Creates releases with changelog content
- 📦 **NPM Publishing**: Publishes optimized runtime package
- 🐳 **Docker Images**: Builds and pushes multi-platform images
- 📚 **Documentation**: Updates version badges automatically
## Quick Start
### For Maintainers
Use the prepared release script for a guided experience:
```bash
npm run prepare:release
```
This script will:
1. Prompt for the new version
2. Update `package.json` and `package.runtime.json`
3. Update the changelog
4. Run tests and build
5. Create a git commit
6. Optionally push to trigger the release
### Manual Process
1. **Update the version**:
```bash
# Edit package.json version field
vim package.json
# Sync to runtime package
npm run sync:runtime-version
```
2. **Update the changelog**:
```bash
# Edit docs/CHANGELOG.md
vim docs/CHANGELOG.md
```
3. **Test and commit**:
```bash
# Ensure everything works
npm test
npm run build
npm run rebuild
# Commit changes
git add package.json package.runtime.json docs/CHANGELOG.md
git commit -m "chore: release vX.Y.Z"
git push
```
## Workflow Details
### Version Detection
The workflow monitors pushes to the main branch and detects when `package.json` version changes:
```yaml
paths:
- 'package.json'
- 'package.runtime.json'
```
### Changelog Parsing
Automatically extracts release notes from `docs/CHANGELOG.md` using the version header format:
```markdown
## [2.10.0] - 2025-08-02
### Added
- New feature descriptions
### Changed
- Changed feature descriptions
### Fixed
- Bug fix descriptions
```
### Release Artifacts
#### GitHub Release
- Created with extracted changelog content
- Tagged with `vX.Y.Z` format
- Includes installation instructions
- Links to documentation
#### NPM Package
- Published as `n8n-mcp` on npmjs.com
- Uses runtime-only dependencies (8 packages vs 50+ dev deps)
- Optimized for `npx` usage
- ~50MB vs 1GB+ with dev dependencies
#### Docker Images
- **Standard**: `ghcr.io/czlonkowski/n8n-mcp:vX.Y.Z`
- **Railway**: `ghcr.io/czlonkowski/n8n-mcp-railway:vX.Y.Z`
- Multi-platform: linux/amd64, linux/arm64
- Semantic version tags: `vX.Y.Z`, `vX.Y`, `vX`, `latest`
## Configuration
### Required Secrets
Set these in GitHub repository settings → Secrets:
| Secret | Description | Required |
|--------|-------------|----------|
| `NPM_TOKEN` | NPM authentication token for publishing | ✅ Yes |
| `GITHUB_TOKEN` | Automatically provided by GitHub Actions | ✅ Auto |
### NPM Token Setup
1. Login to [npmjs.com](https://www.npmjs.com)
2. Go to Account Settings → Access Tokens
3. Create a new **Automation** token
4. Add as `NPM_TOKEN` secret in GitHub
## Testing
### Test Release Automation
Validate the release system without triggering a release:
```bash
npm run test:release-automation
```
This checks:
- ✅ File existence and structure
- ✅ Version detection logic
- ✅ Changelog parsing
- ✅ Build process
- ✅ NPM package preparation
- ✅ Docker configuration
- ✅ Workflow syntax
- ✅ Environment setup
### Local Testing
Test individual components:
```bash
# Test version detection
node -e "console.log(require('./package.json').version)"
# Test changelog parsing
node scripts/test-release-automation.js
# Test npm package preparation
npm run prepare:publish
# Test Docker build
docker build -t test-image .
```
## Workflow Jobs
### 1. Version Detection
- Compares current vs previous version in git history
- Determines if it's a prerelease (alpha, beta, rc, dev)
- Outputs version information for other jobs
### 2. Changelog Extraction
- Parses `docs/CHANGELOG.md` for the current version
- Extracts content between version headers
- Provides formatted release notes
### 3. GitHub Release Creation
- Creates annotated git tag
- Creates GitHub release with changelog content
- Handles prerelease flag for alpha/beta versions
### 4. Build and Test
- Installs dependencies
- Runs full test suite
- Builds TypeScript
- Rebuilds node database
- Type checking
### 5. NPM Publishing
- Prepares optimized package structure
- Uses `package.runtime.json` for dependencies
- Publishes to npmjs.com registry
- Automatic cleanup
### 6. Docker Building
- Multi-platform builds (amd64, arm64)
- Two image variants (standard, railway)
- Semantic versioning tags
- GitHub Container Registry
### 7. Documentation Updates
- Updates version badges in README
- Commits documentation changes
- Automatic push back to repository
## Monitoring
### GitHub Actions
Monitor releases at: https://github.com/czlonkowski/n8n-mcp/actions
### Release Status
- **GitHub Releases**: https://github.com/czlonkowski/n8n-mcp/releases
- **NPM Package**: https://www.npmjs.com/package/n8n-mcp
- **Docker Images**: https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp
### Notifications
The workflow provides comprehensive summaries:
- ✅ Success notifications with links
- ❌ Failure notifications with error details
- 📊 Artifact information and installation commands
## Troubleshooting
### Common Issues
#### NPM Publishing Fails
```
Error: 401 Unauthorized
```
**Solution**: Check NPM_TOKEN secret is valid and has publishing permissions.
#### Docker Build Fails
```
Error: failed to solve: could not read from registry
```
**Solution**: Check GitHub Container Registry permissions and GITHUB_TOKEN.
#### Changelog Parsing Fails
```
No changelog entries found for version X.Y.Z
```
**Solution**: Ensure changelog follows the correct format:
```markdown
## [X.Y.Z] - YYYY-MM-DD
```
#### Version Detection Fails
```
Version not incremented
```
**Solution**: Ensure new version is greater than the previous version.
### Recovery Steps
#### Failed NPM Publish
1. Check if version was already published
2. If not, manually publish:
```bash
npm run prepare:publish
cd npm-publish-temp
npm publish
```
#### Failed Docker Build
1. Build locally to test:
```bash
docker build -t test-build .
```
2. Re-trigger workflow or push a fix
#### Incomplete Release
1. Delete the created tag if needed:
```bash
git tag -d vX.Y.Z
git push --delete origin vX.Y.Z
```
2. Fix issues and push again
## Security
### Secrets Management
- NPM_TOKEN has limited scope (publish only)
- GITHUB_TOKEN has automatic scoping
- No secrets are logged or exposed
### Package Security
- Runtime package excludes development dependencies
- No build tools or test frameworks in published package
- Minimal attack surface (~50MB vs 1GB+)
### Docker Security
- Multi-stage builds
- Non-root user execution
- Minimal base images
- Security scanning enabled
## Changelog Format
The automated system expects changelog entries in [Keep a Changelog](https://keepachangelog.com/) format:
```markdown
# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
### Added
- New features for next release
## [2.10.0] - 2025-08-02
### Added
- Automated release system
- Multi-platform Docker builds
### Changed
- Improved version detection
- Enhanced error handling
### Fixed
- Fixed changelog parsing edge cases
- Fixed Docker build optimization
## [2.9.1] - 2025-08-01
...
```
## Version Strategy
### Semantic Versioning
- **MAJOR** (X.0.0): Breaking changes
- **MINOR** (X.Y.0): New features, backward compatible
- **PATCH** (X.Y.Z): Bug fixes, backward compatible
### Prerelease Versions
- **Alpha**: `X.Y.Z-alpha.N` - Early development
- **Beta**: `X.Y.Z-beta.N` - Feature complete, testing
- **RC**: `X.Y.Z-rc.N` - Release candidate
Prerelease versions are automatically detected and marked appropriately.
## Best Practices
### Before Releasing
1. ✅ Run `npm run test:release-automation`
2. ✅ Update changelog with meaningful descriptions
3. ✅ Test locally with `npm test && npm run build`
4. ✅ Review breaking changes
5. ✅ Consider impact on users
### Version Bumping
- Use `npm run prepare:release` for guided process
- Follow semantic versioning strictly
- Document breaking changes clearly
- Consider backward compatibility
### Changelog Writing
- Be specific about changes
- Include migration notes for breaking changes
- Credit contributors
- Use consistent formatting
## Contributing
### For Maintainers
1. Use automated tools: `npm run prepare:release`
2. Follow semantic versioning
3. Update changelog thoroughly
4. Test before releasing
### For Contributors
- Breaking changes require MAJOR version bump
- New features require MINOR version bump
- Bug fixes require PATCH version bump
- Update changelog in PR descriptions
---
🤖 *This automated release system was designed with [Claude Code](https://claude.ai/code)*

View File

@@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [2.10.2] - 2025-08-05
### Updated
- **n8n Dependencies**: Updated to latest versions for compatibility and new features
- n8n: 1.104.1 → 1.105.2
- n8n-core: 1.103.1 → 1.104.1
- n8n-workflow: 1.101.0 → 1.102.1
- @n8n/n8n-nodes-langchain: 1.103.1 → 1.104.1
- **Node Database**: Rebuilt with 534 nodes from updated n8n packages
- **Template Library**: Fetched 499 workflow templates from the last 12 months
- Templates are filtered to include only those created or updated within the past year
- This ensures the template library contains fresh and actively maintained workflows
- All 1,620 tests passing with updated dependencies
## [2.10.1] - 2025-08-02
### Fixed
- **Memory Leak in SimpleCache**: Fixed critical memory leak causing MCP server connection loss after several hours (fixes #118)
- Added proper timer cleanup in `SimpleCache.destroy()` method
- Updated MCP server shutdown to clean up cache timers
- Enhanced HTTP server error handling with transport error handlers
- Fixed event listener cleanup to prevent accumulation
- Added comprehensive test coverage for memory leak prevention
## [2.10.0] - 2025-08-02
### Added
- **Automated Release System**: Complete CI/CD pipeline for automated releases on version bump
- GitHub Actions workflow (`.github/workflows/release.yml`) with 7 coordinated jobs
- Automatic version detection and changelog extraction
- Multi-artifact publishing: GitHub releases, NPM package, Docker images
- Interactive release preparation tool (`npm run prepare:release`)
- Comprehensive release testing tool (`npm run test:release-automation`)
- Full documentation in `docs/AUTOMATED_RELEASES.md`
- Zero-touch releases: version bump → automatic everything
### Security
- **CI/CD Security Enhancements**:
- Replaced deprecated `actions/create-release@v1` with secure `gh` CLI
- Fixed git checkout vulnerability using safe `git show` commands
- Fixed command injection risk using proper argument arrays
- Added concurrency control to prevent simultaneous releases
- Added disk space checks before resource-intensive operations
- Implemented confirmation gates for destructive operations
### Changed
- **Dockerfile Consolidation**: Removed redundant `Dockerfile.n8n` in favor of single optimized `Dockerfile`
- n8n packages are not required at runtime for N8N_MODE functionality
- Standard image works perfectly with `N8N_MODE=true` environment variable
- Reduces build complexity and maintenance overhead
- Image size reduced by 500MB+ (no unnecessary n8n packages)
- Build time improved from 8+ minutes to 1-2 minutes
### Added (CI/CD Features)
- **Developer Tools**:
- `scripts/prepare-release.js`: Interactive guided release tool
- `scripts/test-release-automation.js`: Validates entire release setup
- `scripts/extract-changelog.js`: Modular changelog extraction
- **Release Automation Features**:
- NPM publishing with 3-retry mechanism for network resilience
- Multi-platform Docker builds (amd64, arm64)
- Semantic version validation and prerelease detection
- Automatic documentation badge updates
- Runtime-optimized NPM package (8 deps vs 50+, ~50MB vs 1GB+)
### Fixed
- Fixed missing `axios` dependency in `package.runtime.json` causing Docker build failures
## [2.9.1] - 2025-08-02
### Fixed
@@ -1028,6 +1098,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Basic n8n and MCP integration
- Core workflow automation features
[2.10.2]: https://github.com/czlonkowski/n8n-mcp/compare/v2.10.1...v2.10.2
[2.10.1]: https://github.com/czlonkowski/n8n-mcp/compare/v2.10.0...v2.10.1
[2.10.0]: https://github.com/czlonkowski/n8n-mcp/compare/v2.9.1...v2.10.0
[2.9.1]: https://github.com/czlonkowski/n8n-mcp/compare/v2.9.0...v2.9.1
[2.9.0]: https://github.com/czlonkowski/n8n-mcp/compare/v2.8.3...v2.9.0
[2.8.3]: https://github.com/czlonkowski/n8n-mcp/compare/v2.8.2...v2.8.3

View File

@@ -1,6 +1,6 @@
# n8n-MCP Deployment Guide
This guide covers how to deploy n8n-MCP and connect it to your n8n instance. Whether you're testing locally or deploying to production, we'll show you how to set up n8n-MCP for use with n8n's MCP Client Tool node.
This guide covers how to deploy n8n-MCP and connect it to AI Agent nodes with the standard MCP Client Tool. Whether you're testing locally or deploying to production, we'll show you how to set it up.
## Table of Contents
- [Overview](#overview)
@@ -57,9 +57,11 @@ For development or custom testing:
```bash
# Set environment variables
export N8N_MODE=true
export MCP_MODE=http # Required for HTTP mode
export N8N_API_URL=http://localhost:5678 # Your n8n instance URL
export N8N_API_KEY=your-api-key-here # Your n8n API key
export MCP_AUTH_TOKEN=test-token-minimum-32-chars-long
export AUTH_TOKEN=test-token-minimum-32-chars-long # Same value as MCP_AUTH_TOKEN
export PORT=3001
# Start the server
@@ -71,18 +73,75 @@ npm start
# Check health
curl http://localhost:3001/health
# Check MCP protocol endpoint
# Check MCP protocol endpoint (this is the endpoint n8n connects to)
curl http://localhost:3001/mcp
# Should return: {"protocolVersion":"2024-11-05"} for n8n compatibility
```
## Environment Variables Reference
| Variable | Required | Description | Example Value |
|----------|----------|-------------|---------------|
| `N8N_MODE` | Yes | Enables n8n integration mode | `true` |
| `MCP_MODE` | Yes | Enables HTTP mode for n8n MCP Client | `http` |
| `N8N_API_URL` | Yes* | URL of your n8n instance | `http://localhost:5678` |
| `N8N_API_KEY` | Yes* | n8n API key for workflow management | `n8n_api_xxx...` |
| `MCP_AUTH_TOKEN` | Yes | Authentication token for MCP requests | `secure-random-32-char-token` |
| `AUTH_TOKEN` | Yes | Must match MCP_AUTH_TOKEN | `secure-random-32-char-token` |
| `PORT` | No | Port for the HTTP server | `3000` (default) |
| `LOG_LEVEL` | No | Logging verbosity | `info`, `debug`, `error` |
*Required only for workflow management features. Documentation tools work without these.
## Docker Build Changes (v2.9.2+)
Starting with version 2.9.2, we use a single optimized Dockerfile for all deployments:
- The previous `Dockerfile.n8n` has been removed as redundant
- N8N_MODE functionality is enabled via the `N8N_MODE=true` environment variable
- This reduces image size by 500MB+ and improves build times from 8+ minutes to 1-2 minutes
- All examples now use the standard `Dockerfile`
## Production Deployment
### Same Server as n8n
If you're running n8n-MCP on the same server as your n8n instance:
1. **Using Docker** (Recommended):
### Building from Source (Recommended)
For the latest features and bug fixes, build from source:
```bash
# Clone and build
git clone https://github.com/czlonkowski/n8n-mcp.git
cd n8n-mcp
# Build Docker image
docker build -t n8n-mcp:latest .
# Create a Docker network if n8n uses one
docker network create n8n-net
# Run n8n-MCP container
docker run -d \
--name n8n-mcp \
--network n8n-net \
-p 3000:3000 \
-e N8N_MODE=true \
-e MCP_MODE=http \
-e N8N_API_URL=http://n8n:5678 \
-e N8N_API_KEY=your-n8n-api-key \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-e AUTH_TOKEN=$(openssl rand -hex 32) \
-e LOG_LEVEL=info \
--restart unless-stopped \
n8n-mcp:latest
```
### Using Pre-built Image (May Be Outdated)
⚠️ **Warning**: Pre-built images may be outdated due to CI/CD synchronization issues. Always check the [GitHub releases](https://github.com/czlonkowski/n8n-mcp/releases) for the latest version.
```bash
# Create a Docker network if n8n uses one
docker network create n8n-net
@@ -93,15 +152,18 @@ docker run -d \
--network n8n-net \
-p 3000:3000 \
-e N8N_MODE=true \
-e MCP_MODE=http \
-e N8N_API_URL=http://n8n:5678 \
-e N8N_API_KEY=your-n8n-api-key \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-e AUTH_TOKEN=$(openssl rand -hex 32) \
-e LOG_LEVEL=info \
--restart unless-stopped \
ghcr.io/czlonkowski/n8n-mcp:latest
```
2. **Using systemd** (for native installation):
### Using systemd (for native installation)
```bash
# Create service file
sudo cat > /etc/systemd/system/n8n-mcp.service << EOF
@@ -114,9 +176,11 @@ Type=simple
User=nodejs
WorkingDirectory=/opt/n8n-mcp
Environment="N8N_MODE=true"
Environment="MCP_MODE=http"
Environment="N8N_API_URL=http://localhost:5678"
Environment="N8N_API_KEY=your-n8n-api-key"
Environment="MCP_AUTH_TOKEN=your-secure-token"
Environment="MCP_AUTH_TOKEN=your-secure-token-32-chars-min"
Environment="AUTH_TOKEN=your-secure-token-32-chars-min"
Environment="PORT=3000"
ExecStart=/usr/bin/node /opt/n8n-mcp/dist/mcp/index.js
Restart=on-failure
@@ -134,22 +198,56 @@ sudo systemctl start n8n-mcp
Deploy n8n-MCP on a separate server from your n8n instance:
#### Quick Docker Deployment
#### Quick Docker Deployment (Build from Source)
```bash
# On your cloud server (Hetzner, AWS, DigitalOcean, etc.)
# First, clone and build
git clone https://github.com/czlonkowski/n8n-mcp.git
cd n8n-mcp
docker build -t n8n-mcp:latest .
# Generate auth tokens
AUTH_TOKEN=$(openssl rand -hex 32)
echo "Save this AUTH_TOKEN: $AUTH_TOKEN"
# Run the container
docker run -d \
--name n8n-mcp \
-p 3000:3000 \
-e N8N_MODE=true \
-e MCP_MODE=http \
-e N8N_API_URL=https://your-n8n-instance.com \
-e N8N_API_KEY=your-n8n-api-key \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-e MCP_AUTH_TOKEN=$AUTH_TOKEN \
-e AUTH_TOKEN=$AUTH_TOKEN \
-e LOG_LEVEL=info \
--restart unless-stopped \
n8n-mcp:latest
```
#### Quick Docker Deployment (Pre-built Image)
⚠️ **Warning**: May be outdated. Check [releases](https://github.com/czlonkowski/n8n-mcp/releases) first.
```bash
# Generate auth tokens
AUTH_TOKEN=$(openssl rand -hex 32)
echo "Save this AUTH_TOKEN: $AUTH_TOKEN"
# Run the container
docker run -d \
--name n8n-mcp \
-p 3000:3000 \
-e N8N_MODE=true \
-e MCP_MODE=http \
-e N8N_API_URL=https://your-n8n-instance.com \
-e N8N_API_KEY=your-n8n-api-key \
-e MCP_AUTH_TOKEN=$AUTH_TOKEN \
-e AUTH_TOKEN=$AUTH_TOKEN \
-e LOG_LEVEL=info \
--restart unless-stopped \
ghcr.io/czlonkowski/n8n-mcp:latest
# Save the MCP_AUTH_TOKEN for later use!
```
#### Full Production Setup (Hetzner/AWS/DigitalOcean)
@@ -170,21 +268,32 @@ curl -fsSL https://get.docker.com | sh
```
3. **Deploy n8n-MCP with SSL** (using Caddy for automatic HTTPS):
**Option A: Build from Source (Recommended)**
```bash
# Clone and prepare
git clone https://github.com/czlonkowski/n8n-mcp.git
cd n8n-mcp
# Build local image
docker build -t n8n-mcp:latest .
# Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
n8n-mcp:
image: ghcr.io/czlonkowski/n8n-mcp:latest
image: n8n-mcp:latest # Using locally built image
container_name: n8n-mcp
restart: unless-stopped
environment:
- N8N_MODE=true
- MCP_MODE=http
- N8N_API_URL=${N8N_API_URL}
- N8N_API_KEY=${N8N_API_KEY}
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
- AUTH_TOKEN=${AUTH_TOKEN}
- PORT=3000
- LOG_LEVEL=info
networks:
@@ -212,7 +321,57 @@ volumes:
caddy_data:
caddy_config:
EOF
```
**Option B: Pre-built Image (May Be Outdated)**
```bash
# Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
n8n-mcp:
image: ghcr.io/czlonkowski/n8n-mcp:latest
container_name: n8n-mcp
restart: unless-stopped
environment:
- N8N_MODE=true
- MCP_MODE=http
- N8N_API_URL=${N8N_API_URL}
- N8N_API_KEY=${N8N_API_KEY}
- MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
- AUTH_TOKEN=${AUTH_TOKEN}
- PORT=3000
- LOG_LEVEL=info
networks:
- web
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
networks:
web:
driver: bridge
volumes:
caddy_data:
caddy_config:
EOF
```
**Complete Setup (Both Options)**
```bash
# Create Caddyfile
cat > Caddyfile << 'EOF'
mcp.yourdomain.com {
@@ -221,15 +380,17 @@ mcp.yourdomain.com {
EOF
# Create .env file
AUTH_TOKEN=$(openssl rand -hex 32)
cat > .env << EOF
N8N_API_URL=https://your-n8n-instance.com
N8N_API_KEY=your-n8n-api-key-here
MCP_AUTH_TOKEN=$(openssl rand -hex 32)
MCP_AUTH_TOKEN=$AUTH_TOKEN
AUTH_TOKEN=$AUTH_TOKEN
EOF
# Save the MCP_AUTH_TOKEN!
echo "Your MCP_AUTH_TOKEN is:"
grep MCP_AUTH_TOKEN .env
# Save the AUTH_TOKEN!
echo "Your AUTH_TOKEN is: $AUTH_TOKEN"
echo "Save this token - you'll need it in n8n MCP Client Tool configuration"
# Start services
docker compose up -d
@@ -258,15 +419,17 @@ docker compose up -d
2. **Configure the connection**:
```
Server URL:
- Same server: http://localhost:3000
- Docker network: http://n8n-mcp:3000
- Different server: https://mcp.yourdomain.com
Server URL (MUST include /mcp endpoint):
- Same server: http://localhost:3000/mcp
- Docker network: http://n8n-mcp:3000/mcp
- Different server: https://mcp.yourdomain.com/mcp
Auth Token: [Your MCP_AUTH_TOKEN]
Auth Token: [Your MCP_AUTH_TOKEN/AUTH_TOKEN value]
Transport: HTTP Streamable (SSE)
```
⚠️ **Critical**: The Server URL must include the `/mcp` endpoint path. Without this, the connection will fail.
3. **Test the connection** by selecting a simple tool like `list_nodes`
@@ -324,70 +487,255 @@ You are an n8n workflow expert. Use the MCP tools to:
## Troubleshooting
### Common Configuration Issues
**Missing `MCP_MODE=http` Environment Variable**
- **Symptom**: n8n MCP Client Tool cannot connect, server doesn't respond on `/mcp` endpoint
- **Solution**: Add `MCP_MODE=http` to your environment variables
- **Why**: Without this, the server runs in stdio mode which is incompatible with n8n
**Server URL Missing `/mcp` Endpoint**
- **Symptom**: "Connection refused" or "Invalid response" in n8n MCP Client Tool
- **Solution**: Ensure your Server URL includes `/mcp` (e.g., `http://localhost:3000/mcp`)
- **Why**: n8n connects to the `/mcp` endpoint specifically, not the root URL
**Mismatched Auth Tokens**
- **Symptom**: "Authentication failed" or "Invalid auth token"
- **Solution**: Ensure both `MCP_AUTH_TOKEN` and `AUTH_TOKEN` have the same value
- **Why**: Both variables must match for proper authentication
### Connection Issues
**"Connection refused" in n8n MCP Client Tool**
- Check n8n-MCP is running: `docker ps` or `systemctl status n8n-mcp`
- Verify port is accessible: `curl http://your-server:3000/health`
- Check firewall rules allow port 3000
1. **Check n8n-MCP is running**:
```bash
# Docker
docker ps | grep n8n-mcp
docker logs n8n-mcp --tail 20
# Systemd
systemctl status n8n-mcp
journalctl -u n8n-mcp --tail 20
```
**"Invalid auth token"**
- Ensure MCP_AUTH_TOKEN matches exactly (no extra spaces)
- Token must be at least 32 characters long
- Check for special characters that might need escaping
2. **Verify endpoints are accessible**:
```bash
# Health check (should return status info)
curl http://your-server:3000/health
# MCP endpoint (should return protocol version)
curl http://your-server:3000/mcp
```
3. **Check firewall and networking**:
```bash
# Test port accessibility from n8n server
telnet your-mcp-server 3000
# Check firewall rules (Ubuntu/Debian)
sudo ufw status
# Check if port is bound correctly
netstat -tlnp | grep :3000
```
**"Invalid auth token" or "Authentication failed"**
1. **Verify token format**:
```bash
# Check token length (should be 64 chars for hex-32)
echo $MCP_AUTH_TOKEN | wc -c
# Verify both tokens match
echo "MCP_AUTH_TOKEN: $MCP_AUTH_TOKEN"
echo "AUTH_TOKEN: $AUTH_TOKEN"
```
2. **Common token issues**:
- Token too short (minimum 32 characters)
- Extra whitespace or newlines in token
- Different values for `MCP_AUTH_TOKEN` and `AUTH_TOKEN`
- Special characters not properly escaped in environment files
**"Cannot connect to n8n API"**
- Verify N8N_API_URL is correct (include http:// or https://)
- Check n8n API key is valid and has necessary permissions
- Ensure n8n instance is accessible from n8n-MCP server
1. **Verify n8n configuration**:
```bash
# Test n8n API accessibility
curl -H "X-N8N-API-KEY: your-api-key" \
https://your-n8n-instance.com/api/v1/workflows
```
### Protocol Issues
2. **Common n8n API issues**:
- `N8N_API_URL` missing protocol (http:// or https://)
- n8n API key expired or invalid
- n8n instance not accessible from n8n-MCP server
- n8n API disabled in settings
### Version Compatibility Issues
**"Outdated Docker Image"**
- **Symptom**: Missing features, old bugs, or compatibility issues
- **Solution**: Build from source instead of using pre-built images
- **Check**: Compare your image version with [GitHub releases](https://github.com/czlonkowski/n8n-mcp/releases)
**"Protocol version mismatch"**
- n8n-MCP automatically uses version 2024-11-05 for n8n
- n8n-MCP automatically uses version 2024-11-05 for n8n compatibility
- Update to latest n8n-MCP version if issues persist
- Check `/mcp` endpoint returns correct version
- Verify `/mcp` endpoint returns correct version
**"Schema validation errors"**
- Known issue with n8n's nested output handling
- n8n-MCP includes workarounds
- Enable debug mode to see detailed errors
### Environment Variable Issues
### Debugging
1. **Enable debug mode**:
**Complete Environment Variable Checklist**:
```bash
# Required for all deployments
export N8N_MODE=true # Enables n8n integration
export MCP_MODE=http # Enables HTTP mode for n8n
export MCP_AUTH_TOKEN=your-secure-32-char-token # Auth token
export AUTH_TOKEN=your-secure-32-char-token # Same value as MCP_AUTH_TOKEN
# Required for workflow management features
export N8N_API_URL=https://your-n8n-instance.com # Your n8n URL
export N8N_API_KEY=your-n8n-api-key # Your n8n API key
# Optional
export PORT=3000 # HTTP port (default: 3000)
export LOG_LEVEL=info # Logging level
```
### Docker-Specific Issues
**Container Build Failures**
```bash
# Clear Docker cache and rebuild
docker system prune -f
docker build --no-cache -t n8n-mcp:latest .
```
**Container Runtime Issues**
```bash
# Check container logs for detailed errors
docker logs n8n-mcp -f --timestamps
# Inspect container environment
docker exec n8n-mcp env | grep -E "(N8N|MCP|AUTH)"
# Test container connectivity
docker exec n8n-mcp curl -f http://localhost:3000/health
```
### Network and SSL Issues
**HTTPS/SSL Problems**
```bash
# Test SSL certificate
openssl s_client -connect mcp.yourdomain.com:443
# Check Caddy logs
docker logs caddy -f --tail 50
```
**Docker Network Issues**
```bash
# Check if containers can communicate
docker network ls
docker network inspect bridge
# Test inter-container connectivity
docker exec n8n curl http://n8n-mcp:3000/health
```
### Debugging Steps
1. **Enable comprehensive logging**:
```bash
# For Docker
docker run -d \
--name n8n-mcp \
-e DEBUG_MCP=true \
-e LOG_LEVEL=debug \
-e N8N_MODE=true \
-e MCP_MODE=http \
# ... other settings
# For systemd, add to service file:
Environment="DEBUG_MCP=true"
Environment="LOG_LEVEL=debug"
```
2. **Check logs**:
2. **Test all endpoints systematically**:
```bash
# Docker
docker logs n8n-mcp -f --tail 100
# 1. Health check (basic server functionality)
curl -v http://localhost:3000/health
# Systemd
journalctl -u n8n-mcp -f
```
# 2. MCP protocol endpoint (what n8n connects to)
curl -v http://localhost:3000/mcp
3. **Test endpoints**:
```bash
# Health check
curl http://localhost:3000/health
# Protocol version
curl http://localhost:3000/mcp
# List tools (requires auth)
curl -X POST http://localhost:3000 \
-H "Authorization: Bearer YOUR_MCP_AUTH_TOKEN" \
# 3. Test authentication (if working, returns tools list)
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# 4. Test a simple tool (documentation only, no n8n API needed)
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_database_statistics","arguments":{}},"id":2}'
```
3. **Common log patterns to look for**:
```bash
# Success patterns
grep "Server started" /var/log/n8n-mcp.log
grep "Protocol version" /var/log/n8n-mcp.log
# Error patterns
grep -i "error\|failed\|invalid" /var/log/n8n-mcp.log
grep -i "auth\|token" /var/log/n8n-mcp.log
grep -i "connection\|network" /var/log/n8n-mcp.log
```
### Getting Help
If you're still experiencing issues:
1. **Gather diagnostic information**:
```bash
# System info
docker --version
docker-compose --version
uname -a
# n8n-MCP version
docker exec n8n-mcp node dist/index.js --version
# Environment check
docker exec n8n-mcp env | grep -E "(N8N|MCP|AUTH)" | sort
# Container status
docker ps | grep n8n-mcp
docker stats n8n-mcp --no-stream
```
2. **Create a minimal test setup**:
```bash
# Test with minimal configuration
docker run -d \
--name n8n-mcp-test \
-p 3001:3000 \
-e N8N_MODE=true \
-e MCP_MODE=http \
-e MCP_AUTH_TOKEN=test-token-minimum-32-chars-long \
-e AUTH_TOKEN=test-token-minimum-32-chars-long \
-e LOG_LEVEL=debug \
n8n-mcp:latest
# Test basic functionality
curl http://localhost:3001/health
curl http://localhost:3001/mcp
```
3. **Report issues**: Include the diagnostic information when opening an issue on [GitHub](https://github.com/czlonkowski/n8n-mcp/issues)
## Performance Tips
- **Minimal deployment**: 1 vCPU, 1GB RAM is sufficient
@@ -395,6 +743,48 @@ curl -X POST http://localhost:3000 \
- **Response time**: Average 12ms for queries
- **Caching**: Built-in 15-minute cache for repeated queries
## Railway Deployment for n8n Integration
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)
If you're using the **Deploy to Railway** button, you'll need to modify some environment variables since Railway uses a different Docker image (`Dockerfile.railway`).
### Required Environment Variable Changes
When deploying with Railway for n8n integration, add these variables in your Railway dashboard:
1. **Go to Railway dashboard** → Your service → **Variables tab**
2. **Add the following variables**:
```bash
# Required for n8n integration mode
N8N_MODE=true
# Already set by Railway template, but verify:
MCP_MODE=http # Required for HTTP mode
MCP_AUTH_TOKEN=<your-token> # Must match AUTH_TOKEN
AUTH_TOKEN=<your-token> # Same value as MCP_AUTH_TOKEN
# Optional: For workflow management features
N8N_API_URL=https://your-n8n-instance.com
N8N_API_KEY=your-n8n-api-key
```
3. **Save changes** - Railway will automatically redeploy
### Connecting n8n to Railway-deployed n8n-MCP
In your n8n workflow, configure the MCP Client Tool with:
```
Server URL: https://your-app.up.railway.app/mcp
Auth Token: [Your AUTH_TOKEN value]
Transport: HTTP Streamable (SSE)
```
> **Note**: The Railway deployment automatically includes all required dependencies and uses the optimized `Dockerfile.railway` which is compatible with both Claude Desktop and n8n integrations.
For more details on Railway deployment, see our [Railway Deployment Guide](./RAILWAY_DEPLOYMENT.md).
## Next Steps
- Test your setup with the [MCP Client Tool in n8n](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-langchain.mcpclienttool/)

View File

@@ -106,7 +106,26 @@ These are automatically set by the Railway template:
| `HOST` | `0.0.0.0` | Listen on all interfaces |
| `PORT` | (Railway provides) | Don't set manually |
### Optional: n8n API Integration
### Optional Variables
| Variable | Default Value | Description |
|----------|--------------|-------------|
| `N8N_MODE` | `false` | Enable n8n integration mode for MCP Client Tool |
| `N8N_API_URL` | - | URL of your n8n instance (for workflow management) |
| `N8N_API_KEY` | - | API key from n8n Settings → API |
### Optional: n8n Integration
#### For n8n MCP Client Tool Integration
To use n8n-MCP with n8n's MCP Client Tool node:
1. **Go to Railway dashboard** → Your service → **Variables**
2. **Add this variable**:
- `N8N_MODE`: Set to `true` to enable n8n integration mode
3. **Save changes** - Railway will redeploy automatically
#### For n8n API Integration (Workflow Management)
To enable workflow management features:

3606
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.9.1",
"version": "2.10.2",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"bin": {
@@ -77,7 +77,9 @@
"sync:runtime-version": "node scripts/sync-runtime-version.js",
"update:readme-version": "node scripts/update-readme-version.js",
"prepare:publish": "./scripts/publish-npm.sh",
"update:all": "./scripts/update-and-publish-prep.sh"
"update:all": "./scripts/update-and-publish-prep.sh",
"test:release-automation": "node scripts/test-release-automation.js",
"prepare:release": "node scripts/prepare-release.js"
},
"repository": {
"type": "git",
@@ -126,12 +128,12 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.2",
"@n8n/n8n-nodes-langchain": "^1.103.1",
"@n8n/n8n-nodes-langchain": "^1.104.1",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"n8n": "^1.104.1",
"n8n-core": "^1.103.1",
"n8n-workflow": "^1.101.0",
"n8n": "^1.105.2",
"n8n-core": "^1.104.1",
"n8n-workflow": "^1.102.1",
"sql.js": "^1.13.0",
"uuid": "^10.0.0"
},

View File

@@ -1,17 +1,15 @@
{
"name": "n8n-mcp-runtime",
"version": "2.9.1",
"version": "2.10.1",
"description": "n8n MCP Server Runtime Dependencies Only",
"private": true,
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.2",
"better-sqlite3": "^11.10.0",
"sql.js": "^1.13.0",
"express": "^5.1.0",
"dotenv": "^16.5.0",
"axios": "^1.7.2",
"zod": "^3.23.8",
"uuid": "^10.0.0"
"sql.js": "^1.13.0",
"uuid": "^10.0.0",
"axios": "^1.7.7"
},
"engines": {
"node": ">=16.0.0"

84
scripts/extract-changelog.js Executable file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* Extract changelog content for a specific version
* Used by GitHub Actions to extract release notes
*/
const fs = require('fs');
const path = require('path');
function extractChangelog(version, changelogPath) {
try {
if (!fs.existsSync(changelogPath)) {
console.error(`Changelog file not found at ${changelogPath}`);
process.exit(1);
}
const content = fs.readFileSync(changelogPath, 'utf8');
const lines = content.split('\n');
// Find the start of this version's section
const versionHeaderRegex = new RegExp(`^## \\[${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`);
let startIndex = -1;
let endIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (versionHeaderRegex.test(lines[i])) {
startIndex = i;
break;
}
}
if (startIndex === -1) {
console.error(`No changelog entries found for version ${version}`);
process.exit(1);
}
// Find the end of this version's section (next version or end of file)
for (let i = startIndex + 1; i < lines.length; i++) {
if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) {
endIndex = i;
break;
}
}
if (endIndex === -1) {
endIndex = lines.length;
}
// Extract the section content
const sectionLines = lines.slice(startIndex, endIndex);
// Remove the version header and any trailing empty lines
let contentLines = sectionLines.slice(1);
while (contentLines.length > 0 && contentLines[contentLines.length - 1].trim() === '') {
contentLines.pop();
}
if (contentLines.length === 0) {
console.error(`No content found for version ${version}`);
process.exit(1);
}
const releaseNotes = contentLines.join('\n').trim();
// Write to stdout for GitHub Actions
console.log(releaseNotes);
} catch (error) {
console.error(`Error extracting changelog: ${error.message}`);
process.exit(1);
}
}
// Parse command line arguments
const version = process.argv[2];
const changelogPath = process.argv[3];
if (!version || !changelogPath) {
console.error('Usage: extract-changelog.js <version> <changelog-path>');
process.exit(1);
}
extractChangelog(version, changelogPath);

400
scripts/prepare-release.js Executable file
View File

@@ -0,0 +1,400 @@
#!/usr/bin/env node
/**
* Pre-release preparation script
* Validates and prepares everything needed for a successful release
*/
const fs = require('fs');
const path = require('path');
const { execSync, spawnSync } = require('child_process');
const readline = require('readline');
// Color codes
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function success(message) {
log(`${message}`, 'green');
}
function warning(message) {
log(`⚠️ ${message}`, 'yellow');
}
function error(message) {
log(`${message}`, 'red');
}
function info(message) {
log(` ${message}`, 'blue');
}
function header(title) {
log(`\n${'='.repeat(60)}`, 'cyan');
log(`🚀 ${title}`, 'cyan');
log(`${'='.repeat(60)}`, 'cyan');
}
class ReleasePreparation {
constructor() {
this.rootDir = path.resolve(__dirname, '..');
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
}
async askQuestion(question) {
return new Promise((resolve) => {
this.rl.question(question, resolve);
});
}
/**
* Get current version and ask for new version
*/
async getVersionInfo() {
const packageJson = require(path.join(this.rootDir, 'package.json'));
const currentVersion = packageJson.version;
log(`\nCurrent version: ${currentVersion}`, 'blue');
const newVersion = await this.askQuestion('\nEnter new version (e.g., 2.10.0): ');
if (!newVersion || !this.isValidSemver(newVersion)) {
error('Invalid semantic version format');
throw new Error('Invalid version');
}
if (this.compareVersions(newVersion, currentVersion) <= 0) {
error('New version must be greater than current version');
throw new Error('Version not incremented');
}
return { currentVersion, newVersion };
}
/**
* Validate semantic version format (strict semver compliance)
*/
isValidSemver(version) {
// Strict semantic versioning regex
const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
return semverRegex.test(version);
}
/**
* Compare two semantic versions
*/
compareVersions(v1, v2) {
const parseVersion = (v) => v.split('-')[0].split('.').map(Number);
const [v1Parts, v2Parts] = [parseVersion(v1), parseVersion(v2)];
for (let i = 0; i < 3; i++) {
if (v1Parts[i] > v2Parts[i]) return 1;
if (v1Parts[i] < v2Parts[i]) return -1;
}
return 0;
}
/**
* Update version in package files
*/
updateVersions(newVersion) {
log('\n📝 Updating version in package files...', 'blue');
// Update package.json
const packageJsonPath = path.join(this.rootDir, 'package.json');
const packageJson = require(packageJsonPath);
packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
success('Updated package.json');
// Sync to runtime package
try {
execSync('npm run sync:runtime-version', { cwd: this.rootDir, stdio: 'pipe' });
success('Synced package.runtime.json');
} catch (err) {
warning('Could not sync runtime version automatically');
// Manual sync
const runtimeJsonPath = path.join(this.rootDir, 'package.runtime.json');
if (fs.existsSync(runtimeJsonPath)) {
const runtimeJson = require(runtimeJsonPath);
runtimeJson.version = newVersion;
fs.writeFileSync(runtimeJsonPath, JSON.stringify(runtimeJson, null, 2) + '\n');
success('Manually synced package.runtime.json');
}
}
}
/**
* Update changelog
*/
async updateChangelog(newVersion) {
const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
if (!fs.existsSync(changelogPath)) {
warning('Changelog file not found, skipping update');
return;
}
log('\n📋 Updating changelog...', 'blue');
const content = fs.readFileSync(changelogPath, 'utf8');
const today = new Date().toISOString().split('T')[0];
// Check if version already exists in changelog
const versionRegex = new RegExp(`^## \\[${newVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
if (versionRegex.test(content)) {
info(`Version ${newVersion} already exists in changelog`);
return;
}
// Find the Unreleased section
const unreleasedMatch = content.match(/^## \[Unreleased\]\s*\n([\s\S]*?)(?=\n## \[|$)/m);
if (unreleasedMatch) {
const unreleasedContent = unreleasedMatch[1].trim();
if (unreleasedContent) {
log('\nFound content in Unreleased section:', 'blue');
log(unreleasedContent.substring(0, 200) + '...', 'yellow');
const moveContent = await this.askQuestion('\nMove this content to the new version? (y/n): ');
if (moveContent.toLowerCase() === 'y') {
// Move unreleased content to new version
const newVersionSection = `## [${newVersion}] - ${today}\n\n${unreleasedContent}\n\n`;
const updatedContent = content.replace(
/^## \[Unreleased\]\s*\n[\s\S]*?(?=\n## \[)/m,
`## [Unreleased]\n\n${newVersionSection}## [`
);
fs.writeFileSync(changelogPath, updatedContent);
success(`Moved unreleased content to version ${newVersion}`);
} else {
// Just add empty version section
const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
const updatedContent = content.replace(
/^## \[Unreleased\]\s*\n/m,
`## [Unreleased]\n\n${newVersionSection}`
);
fs.writeFileSync(changelogPath, updatedContent);
warning(`Added empty version section for ${newVersion} - please fill in the changes`);
}
} else {
// Add empty version section
const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
const updatedContent = content.replace(
/^## \[Unreleased\]\s*\n/m,
`## [Unreleased]\n\n${newVersionSection}`
);
fs.writeFileSync(changelogPath, updatedContent);
warning(`Added empty version section for ${newVersion} - please fill in the changes`);
}
} else {
warning('Could not find Unreleased section in changelog');
}
info('Please review and edit the changelog before committing');
}
/**
* Run tests and build
*/
async runChecks() {
log('\n🧪 Running pre-release checks...', 'blue');
try {
// Run tests
log('Running tests...', 'blue');
execSync('npm test', { cwd: this.rootDir, stdio: 'inherit' });
success('All tests passed');
// Run build
log('Building project...', 'blue');
execSync('npm run build', { cwd: this.rootDir, stdio: 'inherit' });
success('Build completed');
// Rebuild database
log('Rebuilding database...', 'blue');
execSync('npm run rebuild', { cwd: this.rootDir, stdio: 'inherit' });
success('Database rebuilt');
// Run type checking
log('Type checking...', 'blue');
execSync('npm run typecheck', { cwd: this.rootDir, stdio: 'inherit' });
success('Type checking passed');
} catch (err) {
error('Pre-release checks failed');
throw err;
}
}
/**
* Create git commit
*/
async createCommit(newVersion) {
log('\n📝 Creating git commit...', 'blue');
try {
// Check git status
const status = execSync('git status --porcelain', {
cwd: this.rootDir,
encoding: 'utf8'
});
if (!status.trim()) {
info('No changes to commit');
return;
}
// Show what will be committed
log('\nFiles to be committed:', 'blue');
execSync('git diff --name-only', { cwd: this.rootDir, stdio: 'inherit' });
const commit = await this.askQuestion('\nCreate commit for release? (y/n): ');
if (commit.toLowerCase() === 'y') {
// Add files
execSync('git add package.json package.runtime.json docs/CHANGELOG.md', {
cwd: this.rootDir,
stdio: 'pipe'
});
// Create commit
const commitMessage = `chore: release v${newVersion}
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>`;
const result = spawnSync('git', ['commit', '-m', commitMessage], {
cwd: this.rootDir,
stdio: 'pipe',
encoding: 'utf8'
});
if (result.error || result.status !== 0) {
throw new Error(`Git commit failed: ${result.stderr || result.error?.message}`);
}
success(`Created commit for v${newVersion}`);
const push = await this.askQuestion('\nPush to trigger release workflow? (y/n): ');
if (push.toLowerCase() === 'y') {
// Add confirmation for destructive operation
warning('\n⚠ DESTRUCTIVE OPERATION WARNING ⚠️');
warning('This will trigger a PUBLIC RELEASE that cannot be undone!');
warning('The following will happen automatically:');
warning('• Create GitHub release with tag');
warning('• Publish package to NPM registry');
warning('• Build and push Docker images');
warning('• Update documentation');
const confirmation = await this.askQuestion('\nType "RELEASE" (all caps) to confirm: ');
if (confirmation === 'RELEASE') {
execSync('git push', { cwd: this.rootDir, stdio: 'inherit' });
success('Pushed to remote repository');
log('\n🎉 Release workflow will be triggered automatically!', 'green');
log('Monitor progress at: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
} else {
warning('Release cancelled. Commit created but not pushed.');
info('You can push manually later to trigger the release.');
}
} else {
info('Commit created but not pushed. Push manually to trigger release.');
}
}
} catch (err) {
error(`Git operations failed: ${err.message}`);
throw err;
}
}
/**
* Display final instructions
*/
displayInstructions(newVersion) {
header('Release Preparation Complete');
log('📋 What happens next:', 'blue');
log(`1. The GitHub Actions workflow will detect the version change to v${newVersion}`, 'green');
log('2. It will automatically:', 'green');
log(' • Create a GitHub release with changelog content', 'green');
log(' • Publish the npm package', 'green');
log(' • Build and push Docker images', 'green');
log(' • Update documentation badges', 'green');
log('\n🔍 Monitor the release at:', 'blue');
log(' • GitHub Actions: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
log(' • NPM Package: https://www.npmjs.com/package/n8n-mcp', 'blue');
log(' • Docker Images: https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp', 'blue');
log('\n✅ Release preparation completed successfully!', 'green');
}
/**
* Main execution flow
*/
async run() {
try {
header('n8n-MCP Release Preparation');
// Get version information
const { currentVersion, newVersion } = await this.getVersionInfo();
log(`\n🔄 Preparing release: ${currentVersion}${newVersion}`, 'magenta');
// Update versions
this.updateVersions(newVersion);
// Update changelog
await this.updateChangelog(newVersion);
// Run pre-release checks
await this.runChecks();
// Create git commit
await this.createCommit(newVersion);
// Display final instructions
this.displayInstructions(newVersion);
} catch (err) {
error(`Release preparation failed: ${err.message}`);
process.exit(1);
} finally {
this.rl.close();
}
}
}
// Run the script
if (require.main === module) {
const preparation = new ReleasePreparation();
preparation.run().catch(err => {
console.error('Release preparation failed:', err);
process.exit(1);
});
}
module.exports = ReleasePreparation;

View File

@@ -0,0 +1,560 @@
#!/usr/bin/env node
/**
* Test script for release automation
* Validates the release workflow components locally
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Color codes for output
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function header(title) {
log(`\n${'='.repeat(60)}`, 'cyan');
log(`🧪 ${title}`, 'cyan');
log(`${'='.repeat(60)}`, 'cyan');
}
function section(title) {
log(`\n📋 ${title}`, 'blue');
log(`${'-'.repeat(40)}`, 'blue');
}
function success(message) {
log(`${message}`, 'green');
}
function warning(message) {
log(`⚠️ ${message}`, 'yellow');
}
function error(message) {
log(`${message}`, 'red');
}
function info(message) {
log(` ${message}`, 'blue');
}
class ReleaseAutomationTester {
constructor() {
this.rootDir = path.resolve(__dirname, '..');
this.errors = [];
this.warnings = [];
}
/**
* Test if required files exist
*/
testFileExistence() {
section('Testing File Existence');
const requiredFiles = [
'package.json',
'package.runtime.json',
'docs/CHANGELOG.md',
'.github/workflows/release.yml',
'scripts/sync-runtime-version.js',
'scripts/publish-npm.sh'
];
for (const file of requiredFiles) {
const filePath = path.join(this.rootDir, file);
if (fs.existsSync(filePath)) {
success(`Found: ${file}`);
} else {
error(`Missing: ${file}`);
this.errors.push(`Missing required file: ${file}`);
}
}
}
/**
* Test version detection logic
*/
testVersionDetection() {
section('Testing Version Detection');
try {
const packageJson = require(path.join(this.rootDir, 'package.json'));
const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
success(`Package.json version: ${packageJson.version}`);
success(`Runtime package version: ${runtimeJson.version}`);
if (packageJson.version === runtimeJson.version) {
success('Version sync: Both versions match');
} else {
warning('Version sync: Versions do not match - run sync:runtime-version');
this.warnings.push('Package versions are not synchronized');
}
// Test semantic version format
const semverRegex = /^\d+\.\d+\.\d+(?:-[\w\.-]+)?(?:\+[\w\.-]+)?$/;
if (semverRegex.test(packageJson.version)) {
success(`Version format: Valid semantic version (${packageJson.version})`);
} else {
error(`Version format: Invalid semantic version (${packageJson.version})`);
this.errors.push('Invalid semantic version format');
}
} catch (err) {
error(`Version detection failed: ${err.message}`);
this.errors.push(`Version detection error: ${err.message}`);
}
}
/**
* Test changelog parsing
*/
testChangelogParsing() {
section('Testing Changelog Parsing');
try {
const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
if (!fs.existsSync(changelogPath)) {
error('Changelog file not found');
this.errors.push('Missing changelog file');
return;
}
const changelogContent = fs.readFileSync(changelogPath, 'utf8');
const packageJson = require(path.join(this.rootDir, 'package.json'));
const currentVersion = packageJson.version;
// Check if current version exists in changelog
const versionRegex = new RegExp(`^## \\[${currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
if (versionRegex.test(changelogContent)) {
success(`Changelog entry found for version ${currentVersion}`);
// Test extraction logic (simplified version of the GitHub Actions script)
const lines = changelogContent.split('\n');
let startIndex = -1;
let endIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (versionRegex.test(lines[i])) {
startIndex = i;
break;
}
}
if (startIndex !== -1) {
// Find the end of this version's section
for (let i = startIndex + 1; i < lines.length; i++) {
if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) {
endIndex = i;
break;
}
}
if (endIndex === -1) {
endIndex = lines.length;
}
const sectionLines = lines.slice(startIndex + 1, endIndex);
const contentLines = sectionLines.filter(line => line.trim() !== '');
if (contentLines.length > 0) {
success(`Changelog content extracted: ${contentLines.length} lines`);
info(`Preview: ${contentLines[0].substring(0, 100)}...`);
} else {
warning('Changelog section appears to be empty');
this.warnings.push(`Empty changelog section for version ${currentVersion}`);
}
}
} else {
warning(`No changelog entry found for current version ${currentVersion}`);
this.warnings.push(`Missing changelog entry for version ${currentVersion}`);
}
// Check changelog format
if (changelogContent.includes('## [Unreleased]')) {
success('Changelog format: Contains Unreleased section');
} else {
warning('Changelog format: Missing Unreleased section');
}
if (changelogContent.includes('Keep a Changelog')) {
success('Changelog format: Follows Keep a Changelog format');
} else {
warning('Changelog format: Does not reference Keep a Changelog');
}
} catch (err) {
error(`Changelog parsing failed: ${err.message}`);
this.errors.push(`Changelog parsing error: ${err.message}`);
}
}
/**
* Test build process
*/
testBuildProcess() {
section('Testing Build Process');
try {
// Check if dist directory exists
const distPath = path.join(this.rootDir, 'dist');
if (fs.existsSync(distPath)) {
success('Build output: dist directory exists');
// Check for key build files
const keyFiles = [
'dist/index.js',
'dist/mcp/index.js',
'dist/mcp/server.js'
];
for (const file of keyFiles) {
const filePath = path.join(this.rootDir, file);
if (fs.existsSync(filePath)) {
success(`Build file: ${file} exists`);
} else {
warning(`Build file: ${file} missing - run 'npm run build'`);
this.warnings.push(`Missing build file: ${file}`);
}
}
} else {
warning('Build output: dist directory missing - run "npm run build"');
this.warnings.push('Missing build output');
}
// Check database
const dbPath = path.join(this.rootDir, 'data/nodes.db');
if (fs.existsSync(dbPath)) {
const stats = fs.statSync(dbPath);
success(`Database: nodes.db exists (${Math.round(stats.size / 1024 / 1024)}MB)`);
} else {
warning('Database: nodes.db missing - run "npm run rebuild"');
this.warnings.push('Missing database file');
}
} catch (err) {
error(`Build process test failed: ${err.message}`);
this.errors.push(`Build process error: ${err.message}`);
}
}
/**
* Test npm publish preparation
*/
testNpmPublishPrep() {
section('Testing NPM Publish Preparation');
try {
const packageJson = require(path.join(this.rootDir, 'package.json'));
const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
// Check package.json fields
const requiredFields = ['name', 'version', 'description', 'main', 'bin'];
for (const field of requiredFields) {
if (packageJson[field]) {
success(`Package field: ${field} is present`);
} else {
error(`Package field: ${field} is missing`);
this.errors.push(`Missing package.json field: ${field}`);
}
}
// Check runtime dependencies
if (runtimeJson.dependencies) {
const depCount = Object.keys(runtimeJson.dependencies).length;
success(`Runtime dependencies: ${depCount} packages`);
// List key dependencies
const keyDeps = ['@modelcontextprotocol/sdk', 'express', 'sql.js'];
for (const dep of keyDeps) {
if (runtimeJson.dependencies[dep]) {
success(`Key dependency: ${dep} (${runtimeJson.dependencies[dep]})`);
} else {
warning(`Key dependency: ${dep} is missing`);
this.warnings.push(`Missing key dependency: ${dep}`);
}
}
} else {
error('Runtime package has no dependencies');
this.errors.push('Missing runtime dependencies');
}
// Check files array
if (packageJson.files && Array.isArray(packageJson.files)) {
success(`Package files: ${packageJson.files.length} patterns specified`);
info(`Files: ${packageJson.files.join(', ')}`);
} else {
warning('Package files: No files array specified');
this.warnings.push('No files array in package.json');
}
} catch (err) {
error(`NPM publish prep test failed: ${err.message}`);
this.errors.push(`NPM publish prep error: ${err.message}`);
}
}
/**
* Test Docker configuration
*/
testDockerConfig() {
section('Testing Docker Configuration');
try {
const dockerfiles = ['Dockerfile', 'Dockerfile.railway'];
for (const dockerfile of dockerfiles) {
const dockerfilePath = path.join(this.rootDir, dockerfile);
if (fs.existsSync(dockerfilePath)) {
success(`Dockerfile: ${dockerfile} exists`);
const content = fs.readFileSync(dockerfilePath, 'utf8');
// Check for key instructions
if (content.includes('FROM node:')) {
success(`${dockerfile}: Uses Node.js base image`);
} else {
warning(`${dockerfile}: Does not use standard Node.js base image`);
}
if (content.includes('COPY dist')) {
success(`${dockerfile}: Copies build output`);
} else {
warning(`${dockerfile}: May not copy build output correctly`);
}
} else {
warning(`Dockerfile: ${dockerfile} not found`);
this.warnings.push(`Missing Dockerfile: ${dockerfile}`);
}
}
// Check docker-compose files
const composeFiles = ['docker-compose.yml', 'docker-compose.n8n.yml'];
for (const composeFile of composeFiles) {
const composePath = path.join(this.rootDir, composeFile);
if (fs.existsSync(composePath)) {
success(`Docker Compose: ${composeFile} exists`);
} else {
info(`Docker Compose: ${composeFile} not found (optional)`);
}
}
} catch (err) {
error(`Docker config test failed: ${err.message}`);
this.errors.push(`Docker config error: ${err.message}`);
}
}
/**
* Test workflow file syntax
*/
testWorkflowSyntax() {
section('Testing Workflow Syntax');
try {
const workflowPath = path.join(this.rootDir, '.github/workflows/release.yml');
if (!fs.existsSync(workflowPath)) {
error('Release workflow file not found');
this.errors.push('Missing release workflow file');
return;
}
const workflowContent = fs.readFileSync(workflowPath, 'utf8');
// Basic YAML structure checks
if (workflowContent.includes('name: Automated Release')) {
success('Workflow: Has correct name');
} else {
warning('Workflow: Name may be incorrect');
}
if (workflowContent.includes('on:') && workflowContent.includes('push:')) {
success('Workflow: Has push trigger');
} else {
error('Workflow: Missing push trigger');
this.errors.push('Workflow missing push trigger');
}
if (workflowContent.includes('branches: [main]')) {
success('Workflow: Configured for main branch');
} else {
warning('Workflow: May not be configured for main branch');
}
// Check for required jobs
const requiredJobs = [
'detect-version-change',
'extract-changelog',
'create-release',
'publish-npm',
'build-docker'
];
for (const job of requiredJobs) {
if (workflowContent.includes(`${job}:`)) {
success(`Workflow job: ${job} defined`);
} else {
error(`Workflow job: ${job} missing`);
this.errors.push(`Missing workflow job: ${job}`);
}
}
// Check for secrets usage
if (workflowContent.includes('${{ secrets.NPM_TOKEN }}')) {
success('Workflow: NPM_TOKEN secret configured');
} else {
warning('Workflow: NPM_TOKEN secret may be missing');
this.warnings.push('NPM_TOKEN secret may need to be configured');
}
if (workflowContent.includes('${{ secrets.GITHUB_TOKEN }}')) {
success('Workflow: GITHUB_TOKEN secret configured');
} else {
warning('Workflow: GITHUB_TOKEN secret may be missing');
}
} catch (err) {
error(`Workflow syntax test failed: ${err.message}`);
this.errors.push(`Workflow syntax error: ${err.message}`);
}
}
/**
* Test environment and dependencies
*/
testEnvironment() {
section('Testing Environment');
try {
// Check Node.js version
const nodeVersion = process.version;
success(`Node.js version: ${nodeVersion}`);
// Check if npm is available
try {
const npmVersion = execSync('npm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
success(`NPM version: ${npmVersion}`);
} catch (err) {
error('NPM not available');
this.errors.push('NPM not available');
}
// Check if git is available
try {
const gitVersion = execSync('git --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
success(`Git available: ${gitVersion}`);
} catch (err) {
error('Git not available');
this.errors.push('Git not available');
}
// Check if we're in a git repository
try {
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
success('Git repository: Detected');
// Check current branch
try {
const branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: 'pipe' }).trim();
info(`Current branch: ${branch}`);
} catch (err) {
info('Could not determine current branch');
}
} catch (err) {
warning('Not in a git repository');
this.warnings.push('Not in a git repository');
}
} catch (err) {
error(`Environment test failed: ${err.message}`);
this.errors.push(`Environment error: ${err.message}`);
}
}
/**
* Run all tests
*/
async runAllTests() {
header('Release Automation Test Suite');
info('Testing release automation components...');
this.testFileExistence();
this.testVersionDetection();
this.testChangelogParsing();
this.testBuildProcess();
this.testNpmPublishPrep();
this.testDockerConfig();
this.testWorkflowSyntax();
this.testEnvironment();
// Summary
header('Test Summary');
if (this.errors.length === 0 && this.warnings.length === 0) {
log('🎉 All tests passed! Release automation is ready.', 'green');
} else {
if (this.errors.length > 0) {
log(`\n${this.errors.length} Error(s):`, 'red');
this.errors.forEach(err => log(`${err}`, 'red'));
}
if (this.warnings.length > 0) {
log(`\n⚠️ ${this.warnings.length} Warning(s):`, 'yellow');
this.warnings.forEach(warn => log(`${warn}`, 'yellow'));
}
if (this.errors.length > 0) {
log('\n🔧 Please fix the errors before running the release workflow.', 'red');
process.exit(1);
} else {
log('\n✅ No critical errors found. Warnings should be reviewed but won\'t prevent releases.', 'yellow');
}
}
// Next steps
log('\n📋 Next Steps:', 'cyan');
log('1. Ensure all secrets are configured in GitHub repository settings:', 'cyan');
log(' • NPM_TOKEN (required for npm publishing)', 'cyan');
log(' • GITHUB_TOKEN (automatically available)', 'cyan');
log('\n2. To trigger a release:', 'cyan');
log(' • Update version in package.json', 'cyan');
log(' • Update changelog in docs/CHANGELOG.md', 'cyan');
log(' • Commit and push to main branch', 'cyan');
log('\n3. Monitor the release workflow in GitHub Actions', 'cyan');
return this.errors.length === 0;
}
}
// Run the tests
if (require.main === module) {
const tester = new ReleaseAutomationTester();
tester.runAllTests().catch(err => {
console.error('Test suite failed:', err);
process.exit(1);
});
}
module.exports = ReleaseAutomationTester;

View File

@@ -369,7 +369,7 @@ export class SingleSessionHTTPServer {
}
});
// Set up cleanup handler
// Set up cleanup handlers
transport.onclose = () => {
const sid = transport.sessionId;
if (sid) {
@@ -378,6 +378,17 @@ export class SingleSessionHTTPServer {
}
};
// Handle transport errors to prevent connection drops
transport.onerror = (error: Error) => {
const sid = transport.sessionId;
logger.error('Transport error', { sessionId: sid, error: error.message });
if (sid) {
this.removeSession(sid, 'transport_error').catch(err => {
logger.error('Error during transport error cleanup', { error: err });
});
}
};
// Connect the server to the transport BEFORE handling the request
logger.info('handleRequest: Connecting server to new transport');
await server.connect(transport);
@@ -873,7 +884,7 @@ export class SingleSessionHTTPServer {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
// Only add event listener if the request object supports it (not in test mocks)
if (typeof req.on === 'function') {
req.on('close', () => {
const closeHandler = () => {
if (!res.headersSent && sessionId) {
logger.info('Connection closed before response sent', { sessionId });
// Schedule immediate cleanup if connection closes unexpectedly
@@ -883,11 +894,20 @@ export class SingleSessionHTTPServer {
const timeSinceAccess = Date.now() - metadata.lastAccess.getTime();
// Only remove if it's been inactive for a bit to avoid race conditions
if (timeSinceAccess > 60000) { // 1 minute
this.removeSession(sessionId, 'connection_closed');
this.removeSession(sessionId, 'connection_closed').catch(err => {
logger.error('Error during connection close cleanup', { error: err });
});
}
}
});
}
};
req.on('close', closeHandler);
// Clean up event listener when response ends to prevent memory leaks
res.on('finish', () => {
req.removeListener('close', closeHandler);
});
}

View File

@@ -2538,6 +2538,16 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
async shutdown(): Promise<void> {
logger.info('Shutting down MCP server...');
// Clean up cache timers to prevent memory leaks
if (this.cache) {
try {
this.cache.destroy();
logger.info('Cache timers cleaned up');
} catch (error) {
logger.error('Error cleaning up cache:', error);
}
}
// Close database connection if it exists
if (this.db) {
try {

View File

@@ -4,10 +4,11 @@
*/
export class SimpleCache {
private cache = new Map<string, { data: any; expires: number }>();
private cleanupTimer: NodeJS.Timeout | null = null;
constructor() {
// Clean up expired entries every minute
setInterval(() => {
this.cleanupTimer = setInterval(() => {
const now = Date.now();
for (const [key, item] of this.cache.entries()) {
if (item.expires < now) this.cache.delete(key);
@@ -34,4 +35,16 @@ export class SimpleCache {
clear(): void {
this.cache.clear();
}
/**
* Clean up the cache and stop the cleanup timer
* Essential for preventing memory leaks in long-running servers
*/
destroy(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
this.cache.clear();
}
}

View File

@@ -0,0 +1,123 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SimpleCache } from '../../../src/utils/simple-cache';
describe('SimpleCache Memory Leak Fix', () => {
let cache: SimpleCache;
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
if (cache && typeof cache.destroy === 'function') {
cache.destroy();
}
vi.restoreAllMocks();
});
it('should track cleanup timer', () => {
cache = new SimpleCache();
// Access private property for testing
expect((cache as any).cleanupTimer).toBeDefined();
expect((cache as any).cleanupTimer).not.toBeNull();
});
it('should clear timer on destroy', () => {
cache = new SimpleCache();
const timer = (cache as any).cleanupTimer;
cache.destroy();
expect((cache as any).cleanupTimer).toBeNull();
// Verify timer was cleared
expect(() => clearInterval(timer)).not.toThrow();
});
it('should clear cache on destroy', () => {
cache = new SimpleCache();
cache.set('test-key', 'test-value', 300);
expect(cache.get('test-key')).toBe('test-value');
cache.destroy();
expect(cache.get('test-key')).toBeNull();
});
it('should handle multiple destroy calls safely', () => {
cache = new SimpleCache();
expect(() => {
cache.destroy();
cache.destroy();
cache.destroy();
}).not.toThrow();
expect((cache as any).cleanupTimer).toBeNull();
});
it('should not create new timers after destroy', () => {
cache = new SimpleCache();
const originalTimer = (cache as any).cleanupTimer;
cache.destroy();
// Try to use the cache after destroy
cache.set('key', 'value');
cache.get('key');
cache.clear();
// Timer should still be null
expect((cache as any).cleanupTimer).toBeNull();
expect((cache as any).cleanupTimer).not.toBe(originalTimer);
});
it('should clean up expired entries periodically', () => {
cache = new SimpleCache();
// Set items with different TTLs
cache.set('short', 'value1', 1); // 1 second
cache.set('long', 'value2', 300); // 300 seconds
// Advance time by 2 seconds
vi.advanceTimersByTime(2000);
// Advance time to trigger cleanup (60 seconds)
vi.advanceTimersByTime(58000);
// Short-lived item should be gone
expect(cache.get('short')).toBeNull();
// Long-lived item should still exist
expect(cache.get('long')).toBe('value2');
});
it('should prevent memory leak by clearing timer', () => {
const timers: NodeJS.Timeout[] = [];
const originalSetInterval = global.setInterval;
// Mock setInterval to track created timers
global.setInterval = vi.fn((callback, delay) => {
const timer = originalSetInterval(callback, delay);
timers.push(timer);
return timer;
});
// Create and destroy multiple caches
for (let i = 0; i < 5; i++) {
const tempCache = new SimpleCache();
tempCache.set(`key${i}`, `value${i}`);
tempCache.destroy();
}
// All timers should have been cleared
expect(timers.length).toBe(5);
// Restore original setInterval
global.setInterval = originalSetInterval;
});
it('should have destroy method defined', () => {
cache = new SimpleCache();
expect(typeof cache.destroy).toBe('function');
});
});