mcp-musescore-extended
Controls a live MuseScore Studio (4.6.x) score through a QML plugin that opens a WebSocket server inside MuseScore. Exposes tools for adding and editing notes, rests, tuplets, lyrics, measures, staff text and instruments; navigating and selecting ranges; and a full notation layer (slurs, dynamics, articulations, hairpins, key signatures, clefs, tempo, ties, read-back and deletion of notations). Also provides transposition tools, a compact passage read/write format, in-place saving, and command-line rendering/export of scores to PDF, PNG, SVG, MIDI, MP3, WAV, MusicXML or mscz/mscx, plus score metadata inspection.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-musescore-extendedadd a crescendo from bar 4 to 8 in the violin and slur bars 4–6"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-musescore-extended
Control a live MuseScore Studio score from an AI assistant over MCP. A QML plugin inside MuseScore opens a WebSocket server; a Python MCP server connects to it and exposes the score as tools the model can call.
An extended fork of ghchen99/mcp-musescore: it adds the notation layer upstream does not have, plus a passage format for reading and writing music compactly. Upstream is the base, not a competitor — this fork was 0 commits behind it when these changes were made.
CHANGES.md— exactly what differs from upstream, with measurements and the failure each fix addresses.SPEC.md— the design behind the passage format.
Where this is going:
SPEC-2.md— the round-2 spec covering MuseScore version support, universal write verification, packaging and the gaps against the closest competitor. Background survey of 89 music MCP projects inNOTES-mcp-landscape.md.
⚠️ Read this first: MuseScore version
This bridge requires MuseScore Studio 4.6.x or earlier. It does not work on 4.7.x.
MuseScore 4.7 removed the WebSocket plugin API. The API object still appears in
the plugin context, but it resolves to undefined. MuseScore's own log says:
ERROR | ApiRegister::createApi | not registered creator for api: MuseApi.WebsocketServer
WARN | ExtApiV1::api | Not allowed api: MuseApi.WebsocketServer
INFO | Api | "typeof api.websocketserver = undefined"Verified on 4.7.4 and 4.7.5. Verified working on 4.6.5:
INFO | Api | "typeof api.websocketserver = object"
INFO | Api | "LISTEN-OK port 8765"If you are on 4.7.x, install 4.6.5 alongside it and run the bridge there. Both
versions can coexist, but note they share
~/Library/Application Support/MuseScore/MuseScore4/ and
~/Library/Preferences/org.musescore.MuseScore4.plist — back those up before
first use.
Related MCP server: MuseScore MCP Server
What this fork adds
Upstream covers notes, rests, tuplets, lyrics, measures, navigation and
selection. This fork adds the notation layer and fixes a class of silent
misdirected writes. Full detail, with measurements and the failure behind each
fix, is in CHANGES.md.
New notation tools
Tool | Element |
| Slur (spanner) |
| Dynamic marking, with correct SMuFL markup |
| Staccato, tenuto, accent, marcato, portato, and combinations |
| Crescendo / diminuendo (spanner) |
| Key signature, per-staff or score-wide |
| Clef, including mid-piece changes on a barline |
| Tie — via MuseScore's own |
| Tempo text |
| Read back dynamics, articulations and spanners |
| Remove one notation kind at a tick |
New passage layer — read_passage, write_passage, probe_action. Reading
a 117-measure score as compact text costs 9.4 KB where get_score costs 134 KB
(~33k tokens). write_passage validates everything before writing and then reads
the bar back and compares.
New transposition — transpose_chromatic, transpose_diatonic,
transpose_to_key, plus save (in-place, via MuseScore's own action).
New rendering — render_score exports PDF, PNG, SVG, MIDI, MP3, WAV,
MusicXML or .mscz/.mscx through the MuseScore command line, and
score_metadata reads a file's title, duration, measure count and key. Neither
needs a running MuseScore, the plugin, or a bridge connection — which also makes
this the one part of the server with no MuseScore version constraint.
Also added: add_tuplet_run (atomic tuplet runs on an explicit staff) and
add_staff_text.
Cursor fixes. The upstream cursor used INPUT_STATE_SYNC_WITH_SCORE, so an
explicit staffIdx did not reliably stick and writes could land on the wrong
staff — intermittently, and drifting mid-run. Explicit-staff calls now use
INPUT_STATE_INDEPENDENT, and two fallback paths that hardcoded staff 0 were
fixed.
Dynamic fix. A Dynamic created through the API starts with empty custom
text; MuseScore saves that as <text/> and draws nothing while still
playing back correctly. add_dynamic now sets the SMuFL markup explicitly
(p → <sym>dynamicPiano</sym>), and repairs a same-type dynamic in place
rather than stacking duplicates.
Architecture
AI client (Claude Desktop, DSH, ...)
│ MCP over stdio
▼
server.py ─ FastMCP, tool schemas live here
│ ws://localhost:8765
▼
musescore-mcp-websocket.qml ─ runs inside MuseScore
│
▼
MuseScore Studio 4.6.xSetup
1. Install the plugin
Copy musescore-mcp-websocket.qml into MuseScore's plugin folder:
OS | Path |
macOS |
|
Linux |
|
Windows |
|
2. Enable the plugin
Plugins are disabled by default. Enable it once:
Open MuseScore → Home → Plugins
Find MuseScore API Server → Enable
If you run it while still disabled, MuseScore shows a modal:
The plugin "musescore-mcp-websocket" is currently disabled. Do you want to enable it now?
It is easy to miss. The enabled list lives in
<appdata>/extensions/config.json.
3. Python environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt # mcp[cli]<2, websocketsmcp must be 1.x. Version 2.x moved to typing_extensions.TypedDict, which
breaks the typed action definitions on Python < 3.12.
4. Point your MCP client at server.py
{
"mcpServers": {
"musescore": {
"command": "/path/to/.venv/bin/python",
"args": ["/path/to/server.py"]
}
}
}5. Run
Start MuseScore 4.6.x with a score open
Plugins → MuseScore API Server
Start your MCP client
The connection is made lazily and reconnects on failure, so the Python server may be started before or after the plugin.
Tools
Connection
ping_musescore · connect_to_musescore · get_score
get_score dumps the entire score and is expensive on a large file. Use it to
learn the score's shape, then prefer select_custom_range over a narrow tick
range for anything else.
Navigation and selection
get_cursor_info · go_to_measure · go_to_beginning_of_score ·
go_to_final_measure · next_element · prev_element · next_staff ·
prev_staff · select_current_measure · select_custom_range
Notes and rests
add_note · add_rest · add_tuplet · add_tuplet_run · add_lyrics ·
add_staff_text
add_note replaces the chord at the cursor rather than stacking a second
one, so re-voicing a passage in place is safe and needs no delete.
Notation
add_slur · add_dynamic · add_articulation · add_hairpin ·
add_key_signature · add_clef · set_tempo · get_notations ·
delete_notation
Structure
insert_measure · append_measure · delete_selection · undo
Staves and instruments
add_instrument · set_staff_mute · set_instrument_sound
Time
set_time_signature
Batch
processSequence — run a list of actions inside a single undo group.
Concepts
Ticks and durations
Division is 480 per quarter note, so a whole note is 1920 ticks and a
measure of 4/4 must total exactly 1920. Durations are written as fractions:
{"numerator": 1, "denominator": 4} // quarter note
{"numerator": 3, "denominator": 8} // dotted quarterSpanner ticks are onset-to-onset
add_slur and add_hairpin take ticks measured to the onset of the last
note — not past its end. An eighth pair 240 ticks apart needs ticks = 240,
not 480. Passing the full span makes the spanner swallow the following note.
ticks = lastNoteOnset − firstNoteOnsetStaff indices are 0-based
On a cello + harp score: 0 = cello, 1 = harp upper, 2 = harp lower.
Confirm before writing — the live selection decides the staff.
select_custom_range reads and writes want different arguments
endStaff is exclusive:
Intent | Arguments |
Read staff 1 only |
|
Write to staff 1 |
|
With start_staff == end_staff for a write, the selection is empty and the
cursor falls back to staff 0. A read also destroys the write selection, so
never read and then write without re-selecting.
Articulations attach to chords, dynamics to segments
add_articulation must land on a note. Aimed at a rest it returns an error
rather than reporting success.
Known limitations
Ties work, but not where you would look for them. add_tie runs MuseScore's
own tie action over a range; the application builds the tie, so nothing is
hand-built. Do not construct a Tie through newElement(Element.TIE) — the
plugin API has no TIE case and a hand-built Tie with no start or end note
crashes MuseScore outright with no crash report. Two properties matter: the
action is per-range (it cannot tie one pitch of a chord and leave another,
which is why write_passage refuses the grammar's per-pitch ~) and it
toggles (run it on an already-tied note and the tie is removed). Read the
passage back and tie only what is untied; see CHANGES.md §3.
Saving and export. save dispatches MuseScore's own file-save and is
measured working, but it saves in place — a score that has never been saved
has nowhere to go and MuseScore opens a Save dialog, which stalls the bridge
until dismissed. The plugin API gives no reliable way to detect that case
(Score.path reads null even for a score opened from disk), so know whether the
score has been saved once already. save_score, which called writeScore() and
could never work, has been removed.
Notation is invisible to the ordinary reads. get_score,
get_cursor_info and select_custom_range only ever report Chord and
Rest. Use get_notations for dynamics, articulations and spanners.
A range delete_selection fails on tuplet content — it reports success and
changes nothing. Select the measure first, then delete.
addTuplet mid-bar can snap backwards and swallow the preceding note,
because its cursor rewinds to the last existing segment. Prefer a plain rhythm;
if a mid-bar tuplet is essential, write a rest up to the target tick first and
verify immediately.
Plugin console.log() goes nowhere. It is not written to MuseScore's log
or to stdout. Use api.log.info(...) — output lands in
<appdata>/logs/MuseScore_*.log under the Api context.
Other gaps: barlines/repeats, rehearsal marks, ornaments, tremolo,
arpeggio, glissando, 8va and pedal lines, grace notes, and explicit accidentals
are not exposed. newElement(Element.X) + cursor.add() can create them — the
plugin just does not wrap them yet.
Development
The running plugin does not reload
Editing the .qml does nothing to a running instance. Re-running the plugin
does not help either — if port 8765 is already held, MuseScore logs
QTcpServer::listen() called when already listening and the old instance
keeps serving.
To load plugin changes: quit MuseScore completely, reopen it, run the plugin.
Python changes are different: src/tools/*.py and src/types/*.py define the
tool schema, so the MCP server process must restart. Killing it is enough if
your client reconnects.
Keep both plugin copies identical
~/Documents/MuseScore4/Plugins/musescore-mcp-websocket.qml # what MuseScore loads
./musescore-mcp-websocket.qml # what git trackscp musescore-mcp-websocket.qml ~/Documents/MuseScore4/Plugins/
shasum -a 256 musescore-mcp-websocket.qml ~/Documents/MuseScore4/Plugins/musescore-mcp-websocket.qmlWriting to the plugin API
Methods must start with a lowercase letter —
function logLine(), notfunction LogLine(). QML rejects the file otherwise.Set spanner ticks before
cursor.add();spannerTick/spannerTicksare the only handles, sincestartElement/endElementare read-only.For an explicit staff, build the cursor yourself:
cursor.inputStateMode = Cursor.INPUT_STATE_INDEPENDENTand setstaffIdx.
Debugging
ls -t ~/Library/Application\ Support/MuseScore/MuseScore4/logs/ | head -1Load errors appear as ExtPluginRunner::run | Failed to load QML file. A
success: true from a tool is not evidence the write landed — read it back.
Fallback: MusicXML round-trip
When the bridge cannot express something, write MusicXML and convert it with MuseScore's own reader:
"/Applications/MuseScore 4.6.5.app/Contents/MacOS/mscore" -o out.mscz in.musicxmlImporter quirks worth knowing: <type> wins over <duration> when they
disagree; <dot> on a rest is dropped; a leading gap split across two rests
loses the second one. Write leading gaps as a single un-dotted rest, no longer
than two beats, then verify the tick sum per measure.
File structure
├── server.py # MCP entry point
├── musescore-mcp-websocket.qml # MuseScore plugin (the bridge)
├── requirements.txt
└── src/
├── client/
│ └── websocket_client.py # WebSocket transport, reconnect logic
├── tools/ # MCP tool definitions
│ ├── connection.py
│ ├── navigation.py
│ ├── notes_measures.py # notes, rests, tuplets, and all notation
│ ├── sequences.py # processSequence
│ ├── staff_instruments.py
│ └── time_tempo.py
└── types/
└── action_types.py # typed action paramsReference
MIDI pitch: middle C = 60; C major scale = 60, 62, 64, 65, 67, 69, 71, 72.
Clefs: treble bass tenor alto soprano mezzo baritone treble8vb bass8vb
Keys: C G D A E B F# C# F Bb Eb Ab Db Gb Cb
Modes: major minor dorian phrygian lydian mixolydian aeolian ionian locrian
Hairpins: cresc dim crescline dimline
Dynamics: pppppp … ffffff, p mp mf f, fp pf sf sfz sff sffz sfp,
rf rfz fz m r
Articulations: staccato staccatissimo tenuto accent marcato tenutoaccent accentstaccato marcatostaccato marcatotenuto portato staccatissimostroke staccatissimowedges
Credits
Built on ghchen99/mcp-musescore by
George Chen and contributors, which builds the bridge and the note-level tooling
this fork extends. Licensed MIT under the same terms as upstream; LICENSE is
upstream's, unmodified, and the full commit history is preserved so every
upstream contribution stays attributed.
This server cannot be deployed
Maintenance
Related MCP Connectors
Generate AI music via the Lacuna Music API from MCP clients like Claude Desktop & Code.
Turn recordings, videos and sheet music into playable, editable scores. Transcribe audio or video (files or links) to sheet music, read sheet-music photos and PDFs, convert MIDI, MusicXML and ABC, edit notes in conversation with previews and undo, and export PDF, MIDI, MusicXML, guitar TAB, jianpu and audio. Hosted remote server with OAuth sign-in; one instrument, voice or solo piano is free.
Create, co-edit, analyze, publish, and export collaborative step-sequencer sessions through MCP.
MCP server for Producer/Riffusion AI music generation
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceConnects Ableton Live to AI assistants through Model Context Protocol (MCP), enabling natural language control of music production tasks like track creation, MIDI editing, instrument loading, and playback control.15MIT
- AlicenseNot gradedqualityBmaintenanceA Model Context Protocol server that provides programmatic control over MuseScore through a WebSocket-based plugin system, allowing AI assistants to compose music, add lyrics, navigate scores, and control MuseScore directly.108MIT
- AlicenseAqualityAmaintenanceAI-powered music notation server that lets you create and edit scores using natural language, integrating with MuseScore for live manipulation.2354 PyPI26MIT
- AlicenseBqualityDmaintenanceAn MCP server that enables natural language control of Steinberg Dorico music notation software through Claude Desktop or ChatGPT, offering tools for score creation, note input, notation, harmony analysis, and orchestration.5414MIT