Files
046719aabc feat(transport): Streamable HTTP mode — one shared MCP process for all sessions (v1.2.0) (#13)
* chore(ci): release fires on workflow_dispatch, not on every CI pass

Replaced workflow_run trigger (fired automatically when CI completed
on main) with workflow_dispatch. The full semantic-release automation
is preserved — bump detection, version commit, tag, GitHub Release,
NPM + Docker publish — but now runs only when explicitly triggered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(transport): add Streamable HTTP mode — one shared process for all MCP clients (v1.2.0)

Add MCP_TRANSPORT=http mode backed by StreamableHTTPServerTransport. Each
Claude Code session connects to the shared long-lived process via HTTP (port
3031 by default) instead of spawning a new stdio process per session, eliminating
per-session process multiplication. Sessions are isolated by mcp-session-id header.

Also refactor src/index.ts to extract createMcpServer()/registerHandlers() for
clean per-session server instantiation, and upgrade fs.writeFileSync/readFileSync
calls to fs.promises async variants in export/import tool handlers.

9 new tests cover transport resolution, session isolation, teardown, and
startMcpHttpServer. All 528 tests pass. Bumps v1.1.0 → v1.2.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: newblacc <newblacc@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:51:08 +02:00

271 lines
9.8 KiB
YAML

name: Release & Publish
on:
workflow_dispatch:
concurrency:
group: release-main
cancel-in-progress: true
permissions:
contents: write
id-token: write
jobs:
check:
name: Check for releasable commits
runs-on: ubuntu-latest
if: always()
outputs:
bump: ${{ steps.bump.outputs.bump }}
new_version: ${{ steps.bump.outputs.new_version }}
changelog: ${{ steps.bump.outputs.changelog }}
should_release: ${{ steps.bump.outputs.should_release }}
prev_tag: ${{ steps.bump.outputs.prev_tag }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Determine version bump from conventional commits
id: bump
run: |
# Find the latest semver tag
LATEST_TAG=$(git tag --list 'v*' --sort=-version:refname | head -n1)
PREV_TAG="${LATEST_TAG:-}"
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG=$(git rev-list --max-parents=0 HEAD)
echo "No tags found, using first commit"
fi
echo "Latest tag: $LATEST_TAG"
# Get commit messages since last tag (subject + body for BREAKING CHANGE footers)
SUBJECTS=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%s" 2>/dev/null || git log --pretty=format:"%s")
FULL_LOG=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%B---END---" 2>/dev/null || git log --pretty=format:"%B---END---")
if [ -z "$SUBJECTS" ]; then
echo "No new commits since $LATEST_TAG"
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Skip if the only commit is a version bump
NON_RELEASE_COMMITS=$(echo "$SUBJECTS" | grep -v "^chore(release):" || true)
if [ -z "$NON_RELEASE_COMMITS" ]; then
echo "Only release commits found, skipping"
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Commits since $LATEST_TAG:"
echo "$SUBJECTS"
# Determine bump type from conventional commit prefixes and footers
BUMP="patch"
if echo "$SUBJECTS" | grep -qiE "^.*(BREAKING[ -]CHANGE|!)(\(.+\))?:"; then
BUMP="major"
elif echo "$FULL_LOG" | grep -qiE "^BREAKING[ -]CHANGE:"; then
BUMP="major"
elif echo "$SUBJECTS" | grep -qiE "^(feat|feature)(\(.+\))?:"; then
BUMP="minor"
elif echo "$SUBJECTS" | grep -qiE "^✨"; then
BUMP="minor"
fi
# Read current version and compute new one
CURRENT=$(node -p "require('./package.json').version")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "$BUMP" in
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
esac
echo "Current: $CURRENT → New: $NEW_VERSION ($BUMP)"
# Build changelog from conventional commits
CHANGELOG=""
BREAKING=$(echo "$SUBJECTS" | grep -iE "^.*(BREAKING[ -]CHANGE|!)(\(.+\))?:" || true)
FEATURES=$(echo "$SUBJECTS" | grep -iE "^(feat|feature|✨)" || true)
FIXES=$(echo "$SUBJECTS" | grep -iE "^(fix|🐛)" || true)
OTHERS=$(echo "$SUBJECTS" | grep -viE "^(feat|feature|✨|fix|🐛|chore\(release\))" | grep -viE "BREAKING" || true)
add_section() {
local header="$1"
local items="$2"
if [ -n "$items" ]; then
CHANGELOG="$(printf '%s\n\n%s\n%s' "$CHANGELOG" "$header" "$(echo "$items" | sed 's/^/- /')")"
fi
}
add_section "### ⚠️ Breaking Changes" "$BREAKING"
add_section "### ✨ Features" "$FEATURES"
add_section "### 🐛 Bug Fixes" "$FIXES"
add_section "### 📦 Other Changes" "$OTHERS"
echo "bump=$BUMP" >> "$GITHUB_OUTPUT"
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
# Multi-line output for changelog
{
echo "changelog<<CHANGELOG_EOF"
echo "$CHANGELOG"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
release:
name: Version bump & release
needs: check
if: needs.check.outputs.should_release == 'true'
runs-on: ubuntu-latest
outputs:
version: ${{ needs.check.outputs.new_version }}
steps:
- name: Generate release bot token
id: app-token
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c550f5b5e3e8cd3f9 # v1.11.6
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Configure git
run: |
git config user.name "excalidraw-sentinel-release-bot[bot]"
git config user.email "${{ secrets.APP_ID }}+excalidraw-sentinel-release-bot[bot]@users.noreply.github.com"
- name: Bump version in package.json
run: |
npm version ${{ needs.check.outputs.new_version }} --no-git-tag-version
git add package.json package-lock.json
git commit -m "chore(release): v${{ needs.check.outputs.new_version }}"
git tag "v${{ needs.check.outputs.new_version }}"
- name: Push version commit and tag
run: |
git push origin main
git push origin "v${{ needs.check.outputs.new_version }}"
- name: Create GitHub Release
uses: softprops/action-gh-release@da05d552573ad5aba36ea0be2ddfef1a7e5c4d12 # v2.2.2
with:
token: ${{ steps.app-token.outputs.token }}
tag_name: v${{ needs.check.outputs.new_version }}
name: v${{ needs.check.outputs.new_version }}
generate_release_notes: true
body: |
## What's Changed in v${{ needs.check.outputs.new_version }}
${{ needs.check.outputs.changelog }}
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ needs.check.outputs.prev_tag && needs.check.outputs.prev_tag || 'initial' }}...v${{ needs.check.outputs.new_version }}
---
```
npm install excalidraw-mcp-sentinel@${{ needs.check.outputs.new_version }}
```
draft: false
prerelease: false
publish-npm:
name: Publish to NPM
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Cache node_modules
id: cache-nm
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: node_modules
key: node-modules-${{ runner.os }}-node20.x-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache-nm.outputs.cache-hit != 'true'
run: npm ci
- name: Build
run: npm run build
- name: Check if version already on NPM
id: check-npm
run: |
VERSION=$(node -p "require('./package.json').version")
if npm view excalidraw-mcp-sentinel@$VERSION version 2>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to NPM
if: steps.check-npm.outputs.exists == 'false'
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Published
if: steps.check-npm.outputs.exists == 'false'
run: echo "✅ Published v$(node -p "require('./package.json').version") to NPM"
- name: Skipped (already exists)
if: steps.check-npm.outputs.exists == 'true'
run: echo "⚠️ Version already on NPM, skipping"
publish-docker:
name: Build & push Docker images
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: v${{ needs.release.outputs.version }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Log in to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push MCP Server image
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v5.5.0
with:
context: .
file: ./Dockerfile
push: true
tags: |
celstnblacc/excalidraw-mcp-sentinel:latest
celstnblacc/excalidraw-mcp-sentinel:v${{ needs.release.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
- name: Build and push Canvas Server image
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v5.5.0
with:
context: .
file: ./Dockerfile.canvas
push: true
tags: |
celstnblacc/excalidraw-mcp-sentinel-canvas:latest
celstnblacc/excalidraw-mcp-sentinel-canvas:v${{ needs.release.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64