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
| Property | semantic-release | changesets |
|---|---|---|
| How version bump is decided | Reads conventional commit messages automatically | Developer adds a changeset file to the PR explicitly |
| Developer workflow change | Must write conventional commits | Must run changeset add in each PR |
| Mono repo support | Plugin-based, complex setup | First-class — designed for monorepos |
| Release timing | On every merge to main | On "version" PR merge (batches multiple changesets) |
| CHANGELOG style | Auto-generated from commits | Written by developer in changeset file |
| Prerelease support | Yes (pre-release branches) | Yes (pre-mode) |
| Best for | Single-package repos, teams with commit discipline | Monorepos, 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 type | Version bump | MCP example |
|---|---|---|
fix: | patch | fix: get_document now returns isError:true when document not found instead of throwing |
perf: | patch | perf: cache search_documents results in Redis with 5 minute TTL |
feat: | minor | feat: add delete_document tool with soft-delete and undo window |
feat: + optional params | minor | feat: add optional format parameter to get_document (markdown|plain|html) |
BREAKING CHANGE: in footer | major | See 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.