Guide · Release Engineering

MCP server release automation

Manually deciding when to bump a version number, writing a CHANGELOG entry, tagging the commit, and running npm publish is error-prone — version bumps get missed, CHANGELOG entries are inconsistent, and patches ship without tags. Release automation makes the process reliable: every merge to main that changes user-visible behavior produces a release with a correct semver bump, a CHANGELOG entry describing what changed, and an npm publish — without human intervention. The two main tools are semantic-release (fully automated, reads commit messages) and changesets (semi-automated, requires explicit changeset files in PRs).

TL;DR

For MCP servers published as npm packages: use semantic-release if your team writes conventional commits consistently, or changesets if you want explicit version bump decisions per PR. Both produce a CHANGELOG and npm publish. Map conventional commit types to MCP tool schema changes: feat: for new tools, fix: for bug fixes, BREAKING CHANGE: in the commit footer for removed tools or breaking inputSchema changes. For deployed services (not npm packages), release automation is simpler — just tag and deploy.

semantic-release vs changesets

Propertysemantic-releasechangesets
How version bump is decidedReads conventional commit messages automaticallyDeveloper adds a changeset file to the PR explicitly
Developer workflow changeMust write conventional commitsMust run changeset add in each PR
Mono repo supportPlugin-based, complex setupFirst-class — designed for monorepos
Release timingOn every merge to mainOn "version" PR merge (batches multiple changesets)
CHANGELOG styleAuto-generated from commitsWritten by developer in changeset file
Prerelease supportYes (pre-release branches)Yes (pre-mode)
Best forSingle-package repos, teams with commit disciplineMonorepos, teams who want human-written changelogs

Option A: semantic-release

semantic-release reads commit messages since the last release, determines the version bump (patch/minor/major), generates a CHANGELOG entry, creates a GitHub release, and publishes to npm — all on every merge to main.

// .releaserc.json
{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    ["@semantic-release/changelog", {
      "changelogFile": "CHANGELOG.md"
    }],
    ["@semantic-release/npm", {
      "npmPublish": true
    }],
    ["@semantic-release/github", {
      "assets": []
    }],
    ["@semantic-release/git", {
      "assets": ["CHANGELOG.md", "package.json"],
      "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
    }]
  ]
}
name: Release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write       # Create tags and releases
      issues: write         # Comment on issues when closed by release
      pull-requests: write  # Comment on PRs
      id-token: write       # npm provenance

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0     # Full history required by semantic-release
          persist-credentials: false

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'

      - run: npm ci

      - name: Run tests before release
        run: npm test

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release

Conventional commits for MCP server changes

semantic-release maps commit message prefixes to version bumps. Here's how to use them for MCP tool changes:

Commit typeVersion bumpMCP example
fix:patchfix: get_document now returns isError:true when document not found instead of throwing
perf:patchperf: cache search_documents results in Redis with 5 minute TTL
feat:minorfeat: add delete_document tool with soft-delete and undo window
feat: + optional paramsminorfeat: add optional format parameter to get_document (markdown|plain|html)
BREAKING CHANGE: in footermajorSee below

Breaking changes require the BREAKING CHANGE: footer in the commit body (not just the subject line):

feat!: rename get_document to fetch_document

BREAKING CHANGE: The tool name `get_document` has been renamed to
`fetch_document` to align with the new naming convention across all
MCP servers. Update your AI client configurations to use the new name.
Clients using `get_document` will receive a MethodNotFound error.

Closes #47

The ! after the type is shorthand for a breaking change and triggers a major bump. The BREAKING CHANGE: footer provides the migration message that ends up in CHANGELOG.md and the GitHub release notes.

Option B: changesets

Changesets is better suited for monorepos with multiple packages. Developers add a changeset file to each PR that describes what changed and at what severity level. A periodic "version" PR batches all changesets into version bumps and CHANGELOG entries.

# Install changesets
npm install -D @changesets/cli

# Initialize (creates .changeset/config.json)
npx changeset init
// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

Developer workflow for each PR:

# 1. Make your changes
# 2. Add a changeset describing what changed
npx changeset add

# This prompts:
# ? Which packages would you like to include?
#   ◉ @yourscope/mcp-server-documents
# ? What kind of change is this for mcp-server-documents?
#   minor
# ? Please enter a summary for this change:
#   Add delete_document tool with 30-day soft-delete and undo_delete tool

# Creates .changeset/purple-tigers-sing.md with your summary
git add .changeset/
git commit -m "feat: add delete_document and undo_delete tools"

The changeset file sits in the PR alongside the code change:

---
"@yourscope/mcp-server-documents": minor
---

Add `delete_document` tool that soft-deletes documents with a 30-day recovery window,
and `undo_delete` tool that restores documents within that window. Both tools require
the `documents:write` scope. See the README for the updated tool schema.

When you're ready to release, run changeset version (usually via a bot PR) and then changeset publish:

name: Changesets release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'

      - run: npm ci

      - name: Create release PR or publish
        uses: changesets/action@v1
        with:
          publish: npm run release  # Your script running changeset publish
          version: npm run version  # Your script running changeset version
          commit: "chore: version packages"
          title: "chore: release packages"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

The changesets GitHub Action creates a "Version packages" PR that accumulates all pending changesets. Merging that PR triggers the actual publish.

CHANGELOG format for MCP servers

Whether you use semantic-release or changesets, write CHANGELOG entries that communicate tool-level changes clearly — your users are developers who care about tool names and schema changes, not internal code changes:

## [2.0.0] - 2026-06-27

### BREAKING CHANGES
- **`get_document` renamed to `fetch_document`** — Update your AI client MCP config and any
  code that references this tool name. The inputSchema is unchanged.
- **`document_id` parameter type changed from `string` to `string (UUID format)`** — The
  server now validates UUID format and returns `isError: true` for non-UUID values.
  Previously any string was accepted.

## [1.3.0] - 2026-06-15

### New Tools
- **`delete_document`** — Soft-delete a document with a 30-day recovery window.
- **`undo_delete`** — Restore a soft-deleted document within its recovery window.

## [1.2.1] - 2026-06-10

### Bug Fixes
- **`search_documents`** — Fixed: tool returned `isError: true` when the query contained
  apostrophes. The query is now properly parameterized. (#52)

Deployed services (not npm packages)

If your MCP server is a deployed service (not an npm package), release automation is simpler. You don't need npm publish — you just need tagging and deployment:

name: Tag and deploy

on:
  workflow_dispatch:
    inputs:
      version_bump:
        description: 'Version bump type'
        required: true
        type: choice
        options: [patch, minor, major]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Bump version
        run: |
          npm version ${{ inputs.version_bump }} -m "chore(release): %s"

      - name: Push tag
        run: git push && git push --tags

      - name: Deploy
        run: |
          # Your deployment command
          # e.g., fly deploy
          # e.g., railway deploy

For deployed services, the key release artifact is the deployment itself — the version in package.json is informational. Use CI/CD to automate the deploy, and use the git tag for rollback (check out the previous tag and redeploy).

Snapshot testing as the release gate

Wire your tool schema snapshot tests as a required check before any release. If a tool name, description, or inputSchema changes unexpectedly, the snapshot test fails and blocks the release. This prevents accidental breaking changes from shipping without a major version bump:

// tool-schema.snapshot.test.ts
import { describe, it, expect } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { createServer } from './index.js';

describe('Tool schema snapshot', () => {
  it('tools/list output matches snapshot', async () => {
    const [st, ct] = InMemoryTransport.createLinkedPair();
    const server = createServer({ db: mockDb });
    await server.connect(st);
    const client = new Client({ name: 'snap', version: '1' }, { capabilities: {} });
    await client.connect(ct);
    const { tools } = await client.listTools();
    // Snapshot fails if any tool name, description, or inputSchema changes
    expect(tools).toMatchSnapshot();
    await client.close();
  });
});

If the snapshot fails, the developer must run vitest --update-snapshots to accept the new schema — a deliberate action that forces them to think about whether the change is a patch, minor, or major version bump before committing.

Related pages

FAQ

Should I use semantic-release or changesets for a single MCP server package?

If your team writes commits consistently and is willing to follow the conventional commits format, semantic-release is lower-friction — there's no extra step per PR. If you want human-written CHANGELOG entries that explain the "why" behind tool changes (not just what changed), or if you batch releases and don't want a publish on every merge, changesets is better. For monorepos, changesets wins clearly — it understands cross-package dependencies and handles version propagation automatically.

How do I handle a breaking tool change without immediately releasing a major version?

Use a deprecation window: ship the breaking change behind a feature flag or as a parallel tool (e.g., add fetch_document_v2 alongside get_document), then remove the old tool in a later major version. With changesets, mark the changeset as minor for the new tool addition and leave a note that the old tool is deprecated. With semantic-release, use feat: for the new tool and docs: for the deprecation notice — the major bump only happens when you actually remove the old tool.

How do I prevent release automation from publishing a broken package?

The prepublishOnly script in package.json is your last line of defense. Set it to "prepublishOnly": "npm run build && npm test" — npm runs this before every publish, including automated ones. If tests fail, the publish is aborted. Additionally, run tests in CI before semantic-release or changesets action, and make tests a required status check on the main branch so broken code can't merge in the first place.

What happens if I forget to add a changeset to a PR?

The changeset bot will comment on the PR reminding you to add one. Configure the bot to mark the PR check as "pending" (not failed) so PRs without changesets can still merge — useful for PRs that fix documentation or internal refactors that don't affect the published package behavior. For PRs where no version bump is needed, run npx changeset add --empty to create an empty changeset that signals "this change is intentionally not versioned."

How does release automation interact with AliveMCP monitoring?

For deployed MCP services, add a post-deploy step to the release workflow that verifies AliveMCP is green within five minutes of the deploy completing. If AliveMCP reports the server down after the release deploy, trigger an automatic rollback to the previous version tag. This closes the loop between the release pipeline and the external probe — you know within minutes if a release broke the production MCP server, not when a user reports it.