Skip to content

Commit be44f25

Browse files
bitwize-musicclaude
andcommitted
feat: run the MCP server on mcp 2.x while keeping 1.x working (#537)
mcp 2.0.0 moved `mcp.server.fastmcp`'s FastMCP to `mcp.server.mcpserver`'s MCPServer and shipped no compat shim, so server.py's single `from mcp` import raised and the server exited 1 before registering a tool. #542 mitigated the three paths that led users to 2.x; this migrates onto it. server.py imports whichever class is present, preferring 2.x, and requirements.txt pins mcp[cli]==2.0.0. Supporting both is the point rather than a hedge: the plugin updates through the marketplace but the venv is updated by hand, so a hard cutover would have killed the server on every existing install the moment the plugin updated, with no action by the user. The migration is far smaller than #537 anticipated, and that was measured rather than assumed. Every registration site is a bare `@mcp.tool()` with no kwargs, both SDKs build schemas via `inspect.signature(fn, eval_str=True)`, and MCPServer takes the same constructor name, `.tool()` decorator and `.run(transport="stdio")`. All 91 tools produce byte-identical `tools/list` wire schemas on 1.28.1 and 2.0.0; a real stdio handshake returns the same protocol version and capabilities on both. The issue's central worry — that install_error_boundary could keep working while silently changing all 91 generated schemas — does not occur. - serverInfo.version now reports the plugin version on 2.x. 1.x has no such parameter and hardcodes the SDK's own version, so it is passed only where accepted and 1.x behaviour is unchanged. - A golden file locks the 91 schemas (tests/fixtures/tool_schemas.json). This is the schema-parity check #537 asked for, and it keeps earning its place: drop the @functools.wraps out of the error boundary and every tool collapses to one async_wrapper taking (*args, **kwargs) — handlers keep working, the suite stays green, clients lose every parameter. That is #443's failure mode and nothing else sees it. Verified by mutation. - A `MCP Server Boot (mcp 1.x fallback)` CI job covers the fallback branch, which every other job misses since they all install requirements.txt. It boots on the 1.28.1 floor and re-checks the golden, holding the cross-SDK guarantee. - The dependabot mcp-major ignore comes out, as its own comment specified. - Install advice and all four readiness probes accept either SDK line; a probe naming one module calls a working install broken. - `_MCPServer` is annotated Any so mypy's verdict does not depend on which SDK the machine running it has installed. Verified: full suite 4514 passed on both 1.28.1 and 2.0.0; ruff/bandit/mypy clean; mypy clean in all three states (no mcp, 1.x, 2.x); e2e mcp_boot_check passes through the real launcher on both lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4b8b4ed commit be44f25

38 files changed

Lines changed: 3814 additions & 125 deletions

.github/dependabot.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,13 @@ updates:
1515
# upgrades here. They do not merely fail tests — pip cannot resolve them at all
1616
# ("No matching distribution found"), which takes down every job that installs
1717
# requirements.txt, pip-audit included.
18-
# mcp 2.x removed mcp.server.fastmcp, which server.py is built on, with no compat
19-
# shim — the server cannot boot, and since pip-all groups every dependency, one
20-
# un-mergeable major takes the whole group PR down with it (#537).
2118
ignore:
2219
- dependency-name: "scipy"
2320
versions: [">=1.18.0"]
2421
- dependency-name: "numpy"
2522
versions: [">=2.5.0"]
2623
- dependency-name: "librosa"
2724
versions: [">=1.0.0"]
28-
- dependency-name: "mcp"
29-
update-types: ["version-update:semver-major"]
3025
groups:
3126
pip-all:
3227
patterns:

.github/workflows/test.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,65 @@ jobs:
431431
python tests/e2e/mcp_boot_check.py --timeout 240 --call-tool health_check --scenario state-workflow -- \
432432
"$GITHUB_WORKSPACE/servers/bitwize-music-server/mcp-launch.cmd"
433433
434+
# requirements.txt pins mcp 2.x, so every other job exercises only that line.
435+
# server.py also supports 1.x (#537) so a plugin upgrade does not break users
436+
# whose hand-managed venv has not been updated yet — and an untested fallback
437+
# branch is one that quietly rots. This job is that branch's only coverage:
438+
# it boots the server on the 1.x floor and re-checks the schema golden, which
439+
# is what proves the two SDK lines really do generate identical tool schemas.
440+
# Linux-only on purpose: the variable under test is the SDK, not the OS.
441+
mcp-boot-legacy-sdk:
442+
name: MCP Server Boot (mcp 1.x fallback)
443+
runs-on: ubuntu-latest
444+
# Skip for fork PRs — untrusted code could modify requirements/test files
445+
if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
446+
timeout-minutes: 20
447+
defaults:
448+
run:
449+
shell: bash
450+
451+
steps:
452+
- name: Checkout code
453+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
454+
455+
- name: Set up Python
456+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
457+
with:
458+
python-version: '3.11'
459+
cache: 'pip'
460+
461+
- name: Install system dependencies
462+
run: |
463+
sudo apt-get update
464+
sudo apt-get install -y ffmpeg
465+
466+
# Install the pinned set, then downgrade mcp alone to the documented floor.
467+
# mcp 2.x's extra distributions (mcp-types, httpx2) stay installed and unused,
468+
# which is exactly the state a user lands in when they downgrade by hand.
469+
- name: Install requirements, then pin mcp to the 1.x floor
470+
run: |
471+
python -m pip install --upgrade pip
472+
python -m pip install -r requirements.txt -r requirements-test.txt
473+
python -m pip install "mcp[cli]==1.28.1"
474+
python -c "import mcp.server.fastmcp; print('fastmcp import OK — on the 1.x line')"
475+
if python -c "import mcp.server.mcpserver" 2>/dev/null; then
476+
echo "::error::mcp.server.mcpserver is still importable — the downgrade did not take, so this job would retest 2.x"
477+
exit 1
478+
fi
479+
480+
- name: Boot check via mcp-launch on the 1.x fallback branch
481+
env:
482+
CLAUDE_PLUGIN_ROOT: ${{ github.workspace }}
483+
run: |
484+
python tests/e2e/mcp_boot_check.py --timeout 120 --call-tool health_check --scenario state-workflow -- \
485+
"$GITHUB_WORKSPACE/servers/bitwize-music-server/mcp-launch"
486+
487+
# The cross-SDK guarantee: the golden was generated on one line and must
488+
# reproduce byte-for-byte on the other.
489+
- name: Verify tool schemas match the golden on mcp 1.x
490+
run: |
491+
python -m pytest tests/unit/state/test_tool_schema_parity.py tests/unit/shared/test_pinned_dependencies.py -v
492+
434493
lint:
435494
name: Lint
436495
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ This project uses [Conventional Commits](https://conventionalcommits.org/) and [
1111
- Frontmatter `genre:` on a track is optional and absent by default; nothing changes for albums that do not use it. It is a **musical descriptor only** — the album's `genre` stays the parent directory name, because ~7 handler sites resolve album paths from it, so it has to keep matching the directory. `templates/track.md` carries the key commented out and `reference/state-schema.md` documents it on the track table. Both indexing paths carry it — the full scan and the incremental re-parse — held together by a parity test, since a field written by one path and dropped by the other is the drift [#523](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/523) fixed for `tracks_completed`.
1212

1313
### Changed
14+
- **The MCP server runs on `mcp` 2.x, and still runs on 1.x** ([#537](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/537)) — `mcp` 2.0.0 moved `mcp.server.fastmcp`'s `FastMCP` to `mcp.server.mcpserver`'s `MCPServer` with no compat shim. `server.py` now imports whichever is present, preferring 2.x, and `requirements.txt` pins `mcp[cli]==2.0.0`. Supporting both is the point rather than a hedge: the plugin updates through the marketplace but the venv is updated by hand, so a hard cutover would have killed the server on every existing install the moment the plugin updated, with no action by the user. `check_venv_health` reports the pin drift and people upgrade on their own schedule. The install advice and all four readiness probes accept either line — a probe naming only one module calls a perfectly working install broken, which is the same false verdict in the opposite direction from the one #542 fixed.
15+
- **The migration is smaller than it looked, and that was measured rather than assumed.** The concern on the issue was that `_shared.install_error_boundary` monkey-patches `mcp.tool` across all 91 tools and could keep working while silently changing every generated schema. It does not: both SDKs build schemas from the handler signature via `inspect.signature(fn, eval_str=True)`, every registration site is a bare `@mcp.tool()` with no keyword arguments, and `MCPServer` takes the same constructor name, `.tool()` decorator and `.run(transport="stdio")`. Dumped side by side, all 91 tools produce byte-identical `tools/list` wire schemas on 1.28.1 and 2.0.0, and a real stdio handshake returns the same protocol version and capabilities on both.
16+
- **`serverInfo.version` now reports the plugin version on 2.x.** 1.x has no `version` parameter and hardcodes the SDK's own version — telling a client `1.28.1`, which describes the SDK rather than this server — while 2.x would otherwise default it to the empty string. It is passed only where it is accepted, so 1.x behaviour is unchanged.
17+
- **A golden file locks the 91 generated schemas** (`tests/fixtures/tool_schemas.json`, checked by `tests/unit/state/test_tool_schema_parity.py`). This is the schema-parity check the issue asked for, and it keeps earning its place after the migration: drop the `@functools.wraps` out of the error boundary and every tool collapses to a single `async_wrapper` taking `(*args, **kwargs)` — handlers keep working, the rest of the suite stays green, and clients lose every parameter. That is [#443](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/443)'s failure mode, and nothing else in the suite sees it. Verified by mutation: removing the decorator turns the parity test red. Tool *descriptions* are deliberately outside the golden so ordinary docstring edits do not force a regeneration.
18+
- **A `MCP Server Boot (mcp 1.x fallback)` CI job covers the fallback branch.** Every other job installs `requirements.txt` and therefore only ever exercises 2.x, which would leave the 1.x path uncovered from the day it was written — the way compat shims rot. The job installs the pinned set, downgrades mcp alone to the 1.28.1 floor, fails loudly if the downgrade did not take, boots the server through the same `mcp-launch` launcher `.mcp.json` uses, and re-checks the schema golden. That last step is what holds the cross-SDK guarantee: the golden is generated on one line and must reproduce byte-for-byte on the other. Linux-only, since the variable under test is the SDK and not the OS.
1419
- **Dependency bumps that the grouped PR could not deliver: `ruff` 0.16.0 → 0.16.2, `boto3` 1.43.56 → 1.43.69, `playwright` 1.61.0 → 1.62.0** — these three were safe the whole time, but rode in a `pip-all` group PR alongside two upgrades that cannot land, so all three sat unmerged. Split out and verified on their own. `ruff` matters most of the three: it is one of the exactly-pinned gate tools from [#532](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/532), so its verdict changes on unchanged code — 0.16.2 was run against the full tree (`tools/`, `servers/`, `hooks/`, plus the scoped `PLW1514` preview pass) and reports no new findings.
1520
- **`librosa` 1.0+ is blocked in `.github/dependabot.yml`, on the same Python-floor grounds as `scipy` and `numpy`** — `librosa` 1.0.0 declares `Requires-Python >=3.12` and the plugin supports 3.11, so pip cannot resolve it at all: `No matching distribution found for librosa==1.0.0`. That is worse than a failing test. Every job that installs `requirements.txt` dies at the install step, `pip-audit` included, which is why the group PR carrying it failed 11 checks rather than the 6 that mcp alone accounts for. It joins the existing ignore block, whose comment now also records *why* this class of upgrade is blocked rather than merely which packages are affected. ([#532](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/532)) — `requirements.txt` pinned all 16 runtime deps with `==`, but every entry in `requirements-test.txt` used `>=`, so `ruff`, `mypy` and `bandit` resolved to whatever was newest on PyPI at the moment CI ran. Those three decide the Lint and Security Scan verdicts, and unlike a test runner they change their answer on unchanged code — a new rule or a widened check reddens a commit nobody touched, and re-running an old green build no longer reproduces it. The drift was already visible: the file read `ruff>=0.15.21` while CI had been installing `0.16.0`, which is why Dependabot closed #522 as redundant. `ruff`, `mypy` and `bandit` are now `==` pins (`cache: 'pip'` never mitigated this — it caches wheels, but pip still resolves to newest). The `pytest` stack stays on `>=`: it changes what runs, not what counts as a violation. A parametrized test in `tests/unit/shared/test_pinned_dependencies.py` keeps the three from silently loosening again.
1621

@@ -22,7 +27,7 @@ This project uses [Conventional Commits](https://conventionalcommits.org/) and [
2227
- The lexical guard is now a single `_reject_unsafe_segments` helper, and the two truncations of the same layout — `_albums_dir` and `_genre_dir` — call it too. Both interpolated their segments straight in; `_albums_dir` deliberately, on the documented grounds that `artist` is trusted config. That is true, and it is also the same component `_album_dir` guards, so which of the two helpers a caller happened to reach for decided whether the guard applied — the failure mode #529 was opened about. No call site changes behaviour, since both are reached only from config values; a bad value now returns the structured JSON error the MCP boundary produces rather than a wrong path.
2328
- **`PATH_ESCAPES_ROOT` now reads `Path escapes root directory`** rather than `Resolved path escapes root directory`. #534 made it the message for lexical rejections too, where nothing has been resolved, so the first word described one of the ways to trigger it and misdescribed the rest. User-visible wording only — it surfaces through the MCP error boundary, and nothing branches on the string.
2429
- **mcp 2.x can no longer reach the plugin through Dependabot, the printed install advice, or the readiness probe** ([#537](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/537)) — `mcp` 2.0.0 restructured `mcp.server.fastmcp`'s `FastMCP` into `mcp.server.mcpserver`'s `MCPServer` and shipped no compat shim, so `server.py`'s single `from mcp` import raises and the server exits 1 before it registers a tool. `requirements.txt` pins `mcp[cli]==1.28.1`, so no correctly-installed user was ever affected — but three unpinned paths led straight to 2.x anyway:
25-
- **The weekly `pip-all` group PR.** Three of them ([#536](https://github.com/bitwize-music-studio/claude-ai-music-skills/pull/536), [#540](https://github.com/bitwize-music-studio/claude-ai-music-skills/pull/540), [#541](https://github.com/bitwize-music-studio/claude-ai-music-skills/pull/541)) failed Tests and MCP Server Boot on all three runners, and since the group is `patterns: ["*"]`, one un-mergeable major held every unrelated bump hostage with it — #541 was carrying ruff, pypdf, boto3 and playwright updates that had nothing to do with mcp. `.github/dependabot.yml` now ignores mcp majors, scoped to the major so 1.x patches keep flowing. It comes out together with the 2.0 migration, not before.
30+
- **The weekly `pip-all` group PR.** Three of them ([#536](https://github.com/bitwize-music-studio/claude-ai-music-skills/pull/536), [#540](https://github.com/bitwize-music-studio/claude-ai-music-skills/pull/540), [#541](https://github.com/bitwize-music-studio/claude-ai-music-skills/pull/541)) failed Tests and MCP Server Boot on all three runners, and since the group is `patterns: ["*"]`, one un-mergeable major held every unrelated bump hostage with it — #541 was carrying ruff, pypdf, boto3 and playwright updates that had nothing to do with mcp. `.github/dependabot.yml` ignored mcp majors as a stopgap, scoped to the major so 1.x patches kept flowing. That ignore came back out in the same release, once the 2.0 migration below landed and made the major mergeable.
2631
- **The install advice printed by the ImportError handler itself.** Five strings across `server.py` and the server README read `mcp[cli]>=1.2.0` or a bare `pipx install mcp`, all of which resolve to 2.0.0 today. Two of them are printed *by the handler that fires when the import fails* — so someone whose server would not start followed the instructions on screen, installed the one version that cannot start, and got the same message back. A setup loop, shown precisely when the user is already stuck. All five are now bounded `<2`, with the floor moved to the version actually pinned and tested rather than the `>=1.2.0` the server has carried since the MCP server first shipped.
2732
- **The session-start readiness probe.** `python3 -c "import mcp"` succeeds on 2.x — the removed module is the *submodule* — so the gate `CLAUDE.md` says must halt the session reported `✅ MCP ready` on exactly the install where the server was dead, while the server's own stderr said `ERROR: MCP SDK not installed`. All four probes (`CLAUDE.md`, `skills/session-start`, `skills/setup`, `reference/workflows/error-recovery.md`) now import `mcp.server.fastmcp`, which is what `server.py` imports, and report `MCP unusable` rather than `MCP missing` — the remedy is the same `/bitwize-music:setup mcp` either way, but "missing" is wrong about a package that is installed.
2833

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ At the beginning of a fresh session:
7171

7272
1. **Verify setup** — Quick dependency check:
7373
```bash
74-
~/.bitwize-music/venv/bin/python3 -c "import mcp.server.fastmcp" 2>&1 >/dev/null && echo "✅ MCP ready" || echo "❌ MCP unusable" # macOS/Linux/WSL
75-
~/.bitwize-music/venv/Scripts/python.exe -c "import mcp.server.fastmcp" 2>&1 >/dev/null && echo "✅ MCP ready" || echo "❌ MCP unusable" # Windows (Git Bash; cmd/PowerShell: %USERPROFILE%\.bitwize-music\venv\Scripts\python.exe)
74+
~/.bitwize-music/venv/bin/python3 -c "import mcp.server.mcpserver" 2>/dev/null || ~/.bitwize-music/venv/bin/python3 -c "import mcp.server.fastmcp" 2>/dev/null && echo "✅ MCP ready" || echo "❌ MCP unusable" # macOS/Linux/WSL
75+
~/.bitwize-music/venv/Scripts/python.exe -c "import mcp.server.mcpserver" 2>/dev/null || ~/.bitwize-music/venv/Scripts/python.exe -c "import mcp.server.fastmcp" 2>/dev/null && echo "✅ MCP ready" || echo "❌ MCP unusable" # Windows (Git Bash; cmd/PowerShell: %USERPROFILE%\.bitwize-music\venv\Scripts\python.exe)
7676
```
77-
- If MCP unusable → **Stop immediately** and suggest: `/bitwize-music:setup mcp` (either the SDK is missing, or mcp 2.x is installed — it dropped `mcp.server.fastmcp`, so bare `import mcp` would report healthy on an install the server cannot boot on)
77+
- If MCP unusable → **Stop immediately** and suggest: `/bitwize-music:setup mcp` (the SDK is missing or predates 1.28.1). Probe both server modules, never bare `import mcp`: 2.x serves `MCPServer` from `mcp.server.mcpserver` and 1.x serves `FastMCP` from `mcp.server.fastmcp`, and the server accepts either — but a bare `import mcp` succeeds even when neither module is present, reporting healthy on an install the server cannot boot on
7878
- If config missing → suggest: `/bitwize-music:configure`
7979
- Don't proceed with session start until setup is complete
8080
1.5. **Health check** — Use `health_check` MCP tool (checks venv + skill registration):

reference/workflows/error-recovery.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,13 @@ This document covers edge cases and recovery procedures for common workflow issu
316316
**Prevention**: Install and upgrade via the marketplace (`claude plugin update bitwize-music`) rather than editing the cached plugin. Keep the venv in sync (session-start venv check / `check_venv_health`).
317317

318318
**Recovery Steps**:
319-
1. If MCP tools are entirely unavailable, the server didn't start — run `/bitwize-music:setup` (or `/bitwize-music:setup mcp`) to detect the Python environment and reinstall dependencies. Quick check: `~/.bitwize-music/venv/bin/python3 -c "import mcp.server.fastmcp"` (macOS/Linux/WSL) or `~/.bitwize-music/venv/Scripts/python.exe -c "import mcp.server.fastmcp"` (Windows; cmd/PowerShell: `%USERPROFILE%\.bitwize-music\venv\Scripts\python.exe`). Probe the submodule, not bare `mcp` — mcp 2.x imports fine but dropped `mcp.server.fastmcp`, so a bare `import mcp` reports healthy on an install the server cannot boot on ([#537](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/537))
319+
1. If MCP tools are entirely unavailable, the server didn't start — run `/bitwize-music:setup` (or `/bitwize-music:setup mcp`) to detect the Python environment and reinstall dependencies. Quick check (macOS/Linux/WSL; on Windows swap in `~/.bitwize-music/venv/Scripts/python.exe`, or `%USERPROFILE%\.bitwize-music\venv\Scripts\python.exe` for cmd/PowerShell):
320+
321+
```bash
322+
~/.bitwize-music/venv/bin/python3 -c "import mcp.server.mcpserver" 2>/dev/null || ~/.bitwize-music/venv/bin/python3 -c "import mcp.server.fastmcp"
323+
```
324+
325+
Probe both server modules, never bare `mcp`. The server takes either SDK line — 2.x serves `MCPServer` from `mcp.server.mcpserver`, 1.x serves `FastMCP` from `mcp.server.fastmcp` — but a bare `import mcp` succeeds even when neither is present, reporting healthy on an install the server cannot boot on ([#537](https://github.com/bitwize-music-studio/claude-ai-music-skills/issues/537))
320326
2. If the server runs but skills are missing/ghost, `health_check` will say so — run `claude plugin update bitwize-music` to refresh the plugin cache, then restart the session
321327
3. If `health_check` reports skills `no_cache`, the plugin isn't installed via the marketplace — reinstall it (or use `--plugin-dir` for local development)
322328
4. If the venv is stale or missing (`check_venv_health``stale`/`no_venv`), run the reported `pip install -r requirements.txt` fix, or `/bitwize-music:setup` to rebuild the venv

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# =============================================================================
1212
# MCP SERVER (required for plugin core functionality)
1313
# =============================================================================
14-
mcp[cli]==1.28.1
14+
mcp[cli]==2.0.0
1515
pyyaml==6.0.3
1616

1717
# =============================================================================

0 commit comments

Comments
 (0)