iflow-mcp_ghchen99_mcp-musescore 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/METADATA +282 -0
- iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/RECORD +18 -0
- iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/WHEEL +4 -0
- iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/licenses/LICENSE +21 -0
- server.py +44 -0
- src/__init__.py +3 -0
- src/client/__init__.py +5 -0
- src/client/websocket_client.py +55 -0
- src/tools/__init__.py +17 -0
- src/tools/connection.py +23 -0
- src/tools/navigation.py +52 -0
- src/tools/notes_measures.py +87 -0
- src/tools/sequences.py +16 -0
- src/tools/staff_instruments.py +44 -0
- src/tools/time_tempo.py +20 -0
- src/types/__init__.py +29 -0
- src/types/action_types.py +12 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iflow-mcp_ghchen99_mcp-musescore
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for MuseScore control through WebSocket
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Requires-Dist: mcp>=1.0.0
|
|
8
|
+
Requires-Dist: websockets>=13.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# MuseScore MCP Server
|
|
12
|
+
|
|
13
|
+
A Model Context Protocol (MCP) server that provides programmatic control over MuseScore through a WebSocket-based plugin system. This allows AI assistants like Claude to compose music, add lyrics, navigate scores, and control MuseScore directly.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- MuseScore 3.x or 4.x
|
|
20
|
+
- Python 3.8+
|
|
21
|
+
- Claude Desktop or compatible MCP client
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
### 1. Install the MuseScore Plugin
|
|
26
|
+
|
|
27
|
+
First, save the QML plugin code to your MuseScore plugins directory:
|
|
28
|
+
|
|
29
|
+
**macOS**: `~/Documents/MuseScore4/Plugins/musescore-mcp-websocket.qml`
|
|
30
|
+
**Windows**: `%USERPROFILE%\Documents\MuseScore4\Plugins\musescore-mcp-websocket.qml`
|
|
31
|
+
**Linux**: `~/Documents/MuseScore4/Plugins/musescore-mcp-websocket.qml`
|
|
32
|
+
|
|
33
|
+
### 2. Enable the Plugin in MuseScore
|
|
34
|
+
|
|
35
|
+
1. Open MuseScore
|
|
36
|
+
2. Go to **Plugins → Plugin Manager**
|
|
37
|
+
3. Find "MuseScore API Server" and check the box to enable it
|
|
38
|
+
4. Click **OK**
|
|
39
|
+
|
|
40
|
+
### 3. Setup Python Environment
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone <your-repo>
|
|
44
|
+
cd mcp-agents-demo
|
|
45
|
+
python -m venv .venv
|
|
46
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
47
|
+
pip install fastmcp websockets
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 4. Configure Claude Desktop
|
|
51
|
+
|
|
52
|
+
Add to your Claude Desktop configuration file:
|
|
53
|
+
|
|
54
|
+
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
55
|
+
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"musescore": {
|
|
61
|
+
"command": "/path/to/your/project/.venv/bin/python",
|
|
62
|
+
"args": [
|
|
63
|
+
"/path/to/your/project/server.py"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Note**: Update the paths to match your actual project location.
|
|
71
|
+
|
|
72
|
+
## Running the System
|
|
73
|
+
|
|
74
|
+
### Order of Operations (Important!)
|
|
75
|
+
|
|
76
|
+
1. **Start MuseScore first** with a score open
|
|
77
|
+
2. **Run the MuseScore plugin**: Go to **Plugins → MuseScore API Server**
|
|
78
|
+
- You should see console output: `"Starting MuseScore API Server on port 8765"`
|
|
79
|
+
3. **Then start the Python MCP server** or restart Claude Desktop
|
|
80
|
+
|
|
81
|
+
[insert screenshot of different functionality, harmonisation, melodywriting, as zoomed in GIFs]
|
|
82
|
+
|
|
83
|
+
### Development and Testing
|
|
84
|
+
|
|
85
|
+
For development, use the MCP development tools:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Install MCP dev tools
|
|
89
|
+
pip install mcp
|
|
90
|
+
|
|
91
|
+
# Test your server
|
|
92
|
+
mcp dev server.py
|
|
93
|
+
|
|
94
|
+
# Check connection status
|
|
95
|
+
mcp dev server.py --inspect
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Viewing Console Output
|
|
99
|
+
|
|
100
|
+
To see MuseScore plugin console output, run MuseScore from terminal:
|
|
101
|
+
|
|
102
|
+
**macOS**:
|
|
103
|
+
```bash
|
|
104
|
+
/Applications/MuseScore\ 4.app/Contents/MacOS/mscore
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Windows**:
|
|
108
|
+
```cmd
|
|
109
|
+
cd "C:\Program Files\MuseScore 4\bin"
|
|
110
|
+
MuseScore.exe
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Linux**:
|
|
114
|
+
```bash
|
|
115
|
+
musescore4
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Features
|
|
119
|
+
|
|
120
|
+
This MCP server provides comprehensive MuseScore control:
|
|
121
|
+
|
|
122
|
+
### **Navigation & Cursor Control**
|
|
123
|
+
- `get_cursor_info()` - Get current cursor position and selection info
|
|
124
|
+
- `go_to_measure(measure)` - Navigate to specific measure
|
|
125
|
+
- `go_to_beginning_of_score()` / `go_to_final_measure()` - Navigate to start/end
|
|
126
|
+
- `next_element()` / `prev_element()` - Move cursor element by element
|
|
127
|
+
- `next_staff()` / `prev_staff()` - Move between staves
|
|
128
|
+
- `select_current_measure()` - Select entire current measure
|
|
129
|
+
|
|
130
|
+
### **Note & Rest Creation**
|
|
131
|
+
- `add_note(pitch, duration, advance_cursor_after_action)` - Add notes with MIDI pitch
|
|
132
|
+
- `add_rest(duration, advance_cursor_after_action)` - Add rests
|
|
133
|
+
- `add_tuplet(duration, ratio, advance_cursor_after_action)` - Add tuplets (triplets, etc.)
|
|
134
|
+
|
|
135
|
+
### **Measure Management**
|
|
136
|
+
- `insert_measure()` - Insert measure at current position
|
|
137
|
+
- `append_measure(count)` - Add measures to end of score
|
|
138
|
+
- `delete_selection(measure)` - Delete current selection or specific measure
|
|
139
|
+
|
|
140
|
+
### **Lyrics & Text**
|
|
141
|
+
- `add_lyrics_to_current_note(text)` - Add lyrics to current note
|
|
142
|
+
- `add_lyrics(lyrics_list)` - Batch add lyrics to multiple notes
|
|
143
|
+
- `set_title(title)` - Set score title
|
|
144
|
+
|
|
145
|
+
### **Score Information**
|
|
146
|
+
- `get_score()` - Get complete score analysis and structure
|
|
147
|
+
- `ping_musescore()` - Test connection to MuseScore
|
|
148
|
+
- `connect_to_musescore()` - Establish WebSocket connection
|
|
149
|
+
|
|
150
|
+
### **Utilities**
|
|
151
|
+
- `undo()` - Undo last action
|
|
152
|
+
- `set_time_signature(numerator, denominator)` - Change time signature
|
|
153
|
+
- `processSequence(sequence)` - Execute multiple commands in batch
|
|
154
|
+
|
|
155
|
+
## Sample Music
|
|
156
|
+
|
|
157
|
+
Check out the `/examples` folder for sample MuseScore files demonstrating various musical styles:
|
|
158
|
+
|
|
159
|
+
- **Asian Instrumental** - Traditional Asian-inspired instrumental piece
|
|
160
|
+
- **String Quartet** - Classical string quartet arrangement
|
|
161
|
+
|
|
162
|
+
Each example includes:
|
|
163
|
+
- `.mscz` - MuseScore file (editable)
|
|
164
|
+
- `.pdf` - Sheet music
|
|
165
|
+
- `.mp3` - Audio preview
|
|
166
|
+
|
|
167
|
+
## Usage Examples
|
|
168
|
+
|
|
169
|
+
### Creating a Simple Melody
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# Set up the score
|
|
173
|
+
await set_title("My First Song")
|
|
174
|
+
await go_to_beginning_of_score()
|
|
175
|
+
|
|
176
|
+
# Add notes (MIDI pitch: 60=C, 62=D, 64=E, etc.)
|
|
177
|
+
await add_note(60, {"numerator": 1, "denominator": 4}, True) # Quarter note C
|
|
178
|
+
await add_note(64, {"numerator": 1, "denominator": 4}, True) # Quarter note E
|
|
179
|
+
await add_note(67, {"numerator": 1, "denominator": 4}, True) # Quarter note G
|
|
180
|
+
await add_note(72, {"numerator": 1, "denominator": 2}, True) # Half note C
|
|
181
|
+
|
|
182
|
+
# Add lyrics
|
|
183
|
+
await go_to_beginning_of_score()
|
|
184
|
+
await add_lyrics_to_current_note("Do")
|
|
185
|
+
await next_element()
|
|
186
|
+
await add_lyrics_to_current_note("Mi")
|
|
187
|
+
await next_element()
|
|
188
|
+
await add_lyrics_to_current_note("Sol")
|
|
189
|
+
await next_element()
|
|
190
|
+
await add_lyrics_to_current_note("Do")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Batch Operations
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# Add multiple lyrics at once
|
|
197
|
+
await add_lyrics(["Twin-", "kle", "twin-", "kle", "lit-", "tle", "star"])
|
|
198
|
+
|
|
199
|
+
# Use sequence processing for complex operations
|
|
200
|
+
sequence = [
|
|
201
|
+
{"action": "goToBeginningOfScore", "params": {}},
|
|
202
|
+
{"action": "addNote", "params": {"pitch": 60, "duration": {"numerator": 1, "denominator": 4}, "advanceCursorAfterAction": True}},
|
|
203
|
+
{"action": "addNote", "params": {"pitch": 64, "duration": {"numerator": 1, "denominator": 4}, "advanceCursorAfterAction": True}},
|
|
204
|
+
{"action": "addRest", "params": {"duration": {"numerator": 1, "denominator": 4}, "advanceCursorAfterAction": True}}
|
|
205
|
+
]
|
|
206
|
+
await processSequence(sequence)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Troubleshooting
|
|
210
|
+
|
|
211
|
+
### Connection Issues
|
|
212
|
+
- **"Not connected to MuseScore"**:
|
|
213
|
+
- Ensure MuseScore is running with a score open
|
|
214
|
+
- Run the MuseScore plugin (Plugins → MuseScore API Server)
|
|
215
|
+
- Check that port 8765 isn't blocked by firewall
|
|
216
|
+
|
|
217
|
+
### Plugin Issues
|
|
218
|
+
- **Plugin not appearing**: Check the `.qml` file is in the correct plugins directory
|
|
219
|
+
- **Plugin won't enable**: Restart MuseScore after placing the plugin file
|
|
220
|
+
- **No console output**: Run MuseScore from terminal to see debug messages
|
|
221
|
+
|
|
222
|
+
### Python Server Issues
|
|
223
|
+
- **"No server object found"**: The server object must be named `mcp`, `server`, or `app` at module level
|
|
224
|
+
- **WebSocket errors**: Make sure MuseScore plugin is running before starting Python server
|
|
225
|
+
- **Connection timeout**: The MuseScore plugin must be actively running, not just enabled
|
|
226
|
+
|
|
227
|
+
### API Limitations
|
|
228
|
+
- **Lyrics**: Only first verse supported in MuseScore 3.x plugin API
|
|
229
|
+
- **Title setting**: Uses multiple fallback methods due to frame access limitations
|
|
230
|
+
- **Selection persistence**: Some operations may affect current selection
|
|
231
|
+
|
|
232
|
+
## File Structure
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
mcp-agents-demo/
|
|
236
|
+
├── .venv/
|
|
237
|
+
├── server.py # Python MCP server entry point
|
|
238
|
+
├── musescore-mcp-websocket.qml # MuseScore plugin
|
|
239
|
+
├── requirements.txt
|
|
240
|
+
├── README.md
|
|
241
|
+
└── src/ # Source code modules
|
|
242
|
+
├── __init__.py
|
|
243
|
+
├── client/ # WebSocket client functionality
|
|
244
|
+
│ ├── __init__.py
|
|
245
|
+
│ └── websocket_client.py
|
|
246
|
+
├── tools/ # MCP tool implementations
|
|
247
|
+
│ ├── __init__.py
|
|
248
|
+
│ ├── connection.py # Connection management tools
|
|
249
|
+
│ ├── navigation.py # Score navigation tools
|
|
250
|
+
│ ├── notes_measures.py # Note and measure manipulation
|
|
251
|
+
│ ├── sequences.py # Batch operation tools
|
|
252
|
+
│ ├── staff_instruments.py # Staff and instrument tools
|
|
253
|
+
│ └── time_tempo.py # Timing and tempo tools
|
|
254
|
+
└── types/ # Type definitions
|
|
255
|
+
├── __init__.py
|
|
256
|
+
└── action_types.py # WebSocket action type definitions
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Requirements
|
|
260
|
+
|
|
261
|
+
Create a `requirements.txt` file with:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
fastmcp
|
|
265
|
+
websockets
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## MIDI Pitch Reference
|
|
269
|
+
|
|
270
|
+
Common MIDI pitch values for reference:
|
|
271
|
+
- **Middle C**: 60
|
|
272
|
+
- **C Major Scale**: 60, 62, 64, 65, 67, 69, 71, 72
|
|
273
|
+
- **Chromatic**: C=60, C#=61, D=62, D#=63, E=64, F=65, F#=66, G=67, G#=68, A=69, A#=70, B=71
|
|
274
|
+
|
|
275
|
+
## Duration Reference
|
|
276
|
+
|
|
277
|
+
Duration format: `{"numerator": int, "denominator": int}`
|
|
278
|
+
- **Whole note**: `{"numerator": 1, "denominator": 1}`
|
|
279
|
+
- **Half note**: `{"numerator": 1, "denominator": 2}`
|
|
280
|
+
- **Quarter note**: `{"numerator": 1, "denominator": 4}`
|
|
281
|
+
- **Eighth note**: `{"numerator": 1, "denominator": 8}`
|
|
282
|
+
- **Dotted quarter**: `{"numerator": 3, "denominator": 8}`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
server.py,sha256=9dIrI669T4c8VCAF4iP7h_5v_o-NX1H2AG9K6mBt3uo,1130
|
|
2
|
+
src/__init__.py,sha256=-jib-i1HuFU7ElFWBWohFghvPOE7AveBCFjeie62eGE,110
|
|
3
|
+
src/client/__init__.py,sha256=MNyf4k-4MdWouGG0P1ConsLs6UnNvOGjXCEWmOTWsD8,129
|
|
4
|
+
src/client/websocket_client.py,sha256=3FzE9dbrDMWb1LZ3-uG1FFWPAyIYXk6KMfvHRtC1vSI,1944
|
|
5
|
+
src/tools/__init__.py,sha256=bJbvf3pA5bFypW0MIC-QeaS822n5LrO8af0eAY2KYoI,547
|
|
6
|
+
src/tools/connection.py,sha256=0HrzZJDb0er9t-roIP5za_8C7uv4Gxg0RBYAom8-J4g,703
|
|
7
|
+
src/tools/navigation.py,sha256=R4GI4Oal97y6EZbFa6NEwzsTdgDevAIFCfhjEEWjIVE,1667
|
|
8
|
+
src/tools/notes_measures.py,sha256=HzjBcX-JaWAr1FwJI61WPVWTtsHDXn6GE84FuerPwIM,3676
|
|
9
|
+
src/tools/sequences.py,sha256=QEeCVc6D9gqSzTN8DnoEg0yhjC5pIhXAI1muNnBwQp4,491
|
|
10
|
+
src/tools/staff_instruments.py,sha256=3dv6ab2lDt7jYYBANZ2YTMhp4K7YzKEnHf7T7UucXyM,1303
|
|
11
|
+
src/tools/time_tempo.py,sha256=mHpyits3jIfjNJAMypcNHkR39-8-F1hHT7cqT41eloQ,678
|
|
12
|
+
src/types/__init__.py,sha256=zu6KFs8c4Ex6dw_Re1xok3EjUmVF7KPoY4LVKEStFxY,679
|
|
13
|
+
src/types/action_types.py,sha256=t-24oOqlQan3UA0-VLT1D3Gy_rpZaguumh8JTYWbnEo,277
|
|
14
|
+
iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/METADATA,sha256=9Ns82Sb6z7lnQJcAOlNKyeztdd-XtH8tOhbGmWCg5Ac,9196
|
|
15
|
+
iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/entry_points.txt,sha256=jbMwCNQtTWK1AFnvDssHEwY1a99FqLTWxSxvYCWMAzo,46
|
|
17
|
+
iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/licenses/LICENSE,sha256=mc8W3ZAoSXw3qToh-9CfxDqZ8XRyykACU5kBlqdlBuI,1068
|
|
18
|
+
iflow_mcp_ghchen99_mcp_musescore-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 George Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
server.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from mcp.server.fastmcp import FastMCP
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
# Import modular components
|
|
6
|
+
from src.client import MuseScoreClient
|
|
7
|
+
from src.tools import (
|
|
8
|
+
setup_connection_tools,
|
|
9
|
+
setup_navigation_tools,
|
|
10
|
+
setup_notes_measures_tools,
|
|
11
|
+
setup_staff_instruments_tools,
|
|
12
|
+
setup_time_tempo_tools,
|
|
13
|
+
setup_sequence_tools
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Set up logging
|
|
17
|
+
logging.basicConfig(
|
|
18
|
+
level=logging.INFO,
|
|
19
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
20
|
+
handlers=[logging.StreamHandler(sys.stderr)]
|
|
21
|
+
)
|
|
22
|
+
logger = logging.getLogger("MuseScoreMCP")
|
|
23
|
+
|
|
24
|
+
# Create the MCP app and client
|
|
25
|
+
mcp = FastMCP("MuseScore Assistant")
|
|
26
|
+
client = MuseScoreClient()
|
|
27
|
+
|
|
28
|
+
# Setup all tool categories
|
|
29
|
+
setup_connection_tools(mcp, client)
|
|
30
|
+
setup_navigation_tools(mcp, client)
|
|
31
|
+
setup_notes_measures_tools(mcp, client)
|
|
32
|
+
setup_staff_instruments_tools(mcp, client)
|
|
33
|
+
setup_time_tempo_tools(mcp, client)
|
|
34
|
+
setup_sequence_tools(mcp, client)
|
|
35
|
+
|
|
36
|
+
# Main entry point
|
|
37
|
+
def main():
|
|
38
|
+
sys.stderr.write("MuseScore MCP Server starting up...\n")
|
|
39
|
+
sys.stderr.flush()
|
|
40
|
+
logger.info("MuseScore MCP Server is running")
|
|
41
|
+
mcp.run()
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
src/__init__.py
ADDED
src/client/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""WebSocket client for communicating with MuseScore."""
|
|
2
|
+
|
|
3
|
+
import websockets
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("MuseScoreMCP.Client")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MuseScoreClient:
|
|
12
|
+
"""Client to communicate with MuseScore WebSocket API."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, host: str = "localhost", port: int = 8765):
|
|
15
|
+
self.uri = f"ws://{host}:{port}"
|
|
16
|
+
self.websocket = None
|
|
17
|
+
|
|
18
|
+
async def connect(self):
|
|
19
|
+
"""Connect to the MuseScore WebSocket API."""
|
|
20
|
+
try:
|
|
21
|
+
self.websocket = await websockets.connect(self.uri)
|
|
22
|
+
logger.info(f"Connected to MuseScore API at {self.uri}")
|
|
23
|
+
return True
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.error(f"Failed to connect to MuseScore API: {str(e)}")
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
async def send_command(self, action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
29
|
+
"""Send a command to MuseScore and wait for response."""
|
|
30
|
+
if not self.websocket:
|
|
31
|
+
connected = await self.connect()
|
|
32
|
+
if not connected:
|
|
33
|
+
return {"error": "Not connected to MuseScore"}
|
|
34
|
+
|
|
35
|
+
if params is None:
|
|
36
|
+
params = {}
|
|
37
|
+
|
|
38
|
+
command = {"action": action, "params": params}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
logger.info(f"Sending command: {json.dumps(command)}")
|
|
42
|
+
await self.websocket.send(json.dumps(command))
|
|
43
|
+
response = await self.websocket.recv()
|
|
44
|
+
logger.info(f"Received response: {response}")
|
|
45
|
+
return json.loads(response)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Error sending command: {str(e)}")
|
|
48
|
+
return {"error": str(e)}
|
|
49
|
+
|
|
50
|
+
async def close(self):
|
|
51
|
+
"""Close the WebSocket connection."""
|
|
52
|
+
if self.websocket:
|
|
53
|
+
await self.websocket.close()
|
|
54
|
+
self.websocket = None
|
|
55
|
+
logger.info("Disconnected from MuseScore API")
|
src/tools/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""MCP tools for MuseScore operations."""
|
|
2
|
+
|
|
3
|
+
from .connection import setup_connection_tools
|
|
4
|
+
from .navigation import setup_navigation_tools
|
|
5
|
+
from .notes_measures import setup_notes_measures_tools
|
|
6
|
+
from .staff_instruments import setup_staff_instruments_tools
|
|
7
|
+
from .time_tempo import setup_time_tempo_tools
|
|
8
|
+
from .sequences import setup_sequence_tools
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"setup_connection_tools",
|
|
12
|
+
"setup_navigation_tools",
|
|
13
|
+
"setup_notes_measures_tools",
|
|
14
|
+
"setup_staff_instruments_tools",
|
|
15
|
+
"setup_time_tempo_tools",
|
|
16
|
+
"setup_sequence_tools"
|
|
17
|
+
]
|
src/tools/connection.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Connection and utility tools for MuseScore MCP."""
|
|
2
|
+
|
|
3
|
+
from ..client import MuseScoreClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_connection_tools(mcp, client: MuseScoreClient):
|
|
7
|
+
"""Setup connection and utility tools."""
|
|
8
|
+
|
|
9
|
+
@mcp.tool()
|
|
10
|
+
async def connect_to_musescore():
|
|
11
|
+
"""Connect to the MuseScore WebSocket API."""
|
|
12
|
+
result = await client.connect()
|
|
13
|
+
return {"success": result}
|
|
14
|
+
|
|
15
|
+
@mcp.tool()
|
|
16
|
+
async def ping_musescore():
|
|
17
|
+
"""Ping the MuseScore WebSocket API to check connection."""
|
|
18
|
+
return await client.send_command("ping")
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
async def get_score():
|
|
22
|
+
"""Get information about the current score."""
|
|
23
|
+
return await client.send_command("getScore")
|
src/tools/navigation.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Cursor and navigation tools for MuseScore MCP."""
|
|
2
|
+
|
|
3
|
+
from ..client import MuseScoreClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_navigation_tools(mcp, client: MuseScoreClient):
|
|
7
|
+
"""Setup cursor and navigation tools."""
|
|
8
|
+
|
|
9
|
+
@mcp.tool()
|
|
10
|
+
async def get_cursor_info():
|
|
11
|
+
"""Get information about the current cursor position."""
|
|
12
|
+
return await client.send_command("getCursorInfo")
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
async def go_to_measure(measure: int):
|
|
16
|
+
"""Navigate to a specific measure."""
|
|
17
|
+
return await client.send_command("goToMeasure", {"measure": measure})
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
async def go_to_final_measure():
|
|
21
|
+
"""Navigate to the final measure of the score."""
|
|
22
|
+
return await client.send_command("goToFinalMeasure")
|
|
23
|
+
|
|
24
|
+
@mcp.tool()
|
|
25
|
+
async def go_to_beginning_of_score():
|
|
26
|
+
"""Navigate to the beginning of the score."""
|
|
27
|
+
return await client.send_command("goToBeginningOfScore")
|
|
28
|
+
|
|
29
|
+
@mcp.tool()
|
|
30
|
+
async def next_element():
|
|
31
|
+
"""Move cursor to the next element."""
|
|
32
|
+
return await client.send_command("nextElement")
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
async def prev_element():
|
|
36
|
+
"""Move cursor to the previous element."""
|
|
37
|
+
return await client.send_command("prevElement")
|
|
38
|
+
|
|
39
|
+
@mcp.tool()
|
|
40
|
+
async def next_staff():
|
|
41
|
+
"""Move cursor to the next staff."""
|
|
42
|
+
return await client.send_command("nextStaff")
|
|
43
|
+
|
|
44
|
+
@mcp.tool()
|
|
45
|
+
async def prev_staff():
|
|
46
|
+
"""Move cursor to the previous staff."""
|
|
47
|
+
return await client.send_command("prevStaff")
|
|
48
|
+
|
|
49
|
+
@mcp.tool()
|
|
50
|
+
async def select_current_measure():
|
|
51
|
+
"""Select the current measure."""
|
|
52
|
+
return await client.send_command("selectCurrentMeasure")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Notes and measures tools for MuseScore MCP."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from ..client import MuseScoreClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def setup_notes_measures_tools(mcp, client: MuseScoreClient):
|
|
8
|
+
"""Setup notes and measures tools."""
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
async def add_note(pitch: int = 64, duration: dict = {"numerator": 1, "denominator": 4}, advance_cursor_after_action: bool = True):
|
|
12
|
+
"""Add a note at the current cursor position with the specified pitch and duration.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
pitch: MIDI pitch value (0-127, where 60 is middle C)
|
|
16
|
+
duration: Duration as {"numerator": int, "denominator": int} (e.g., {"numerator": 1, "denominator": 4} for quarter note)
|
|
17
|
+
advance_cursor_after_action: Whether to move cursor to next position after adding note
|
|
18
|
+
"""
|
|
19
|
+
return await client.send_command("addNote", {
|
|
20
|
+
"pitch": pitch,
|
|
21
|
+
"duration": duration,
|
|
22
|
+
"advanceCursorAfterAction": advance_cursor_after_action
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
async def add_rest(duration: dict = {"numerator": 1, "denominator": 4}, advance_cursor_after_action: bool = True):
|
|
27
|
+
"""Add a rest at the current cursor position.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
duration: Duration as {"numerator": int, "denominator": int} (e.g., {"numerator": 1, "denominator": 4} for quarter rest)
|
|
31
|
+
advance_cursor_after_action: Whether to move cursor to next position after adding rest
|
|
32
|
+
"""
|
|
33
|
+
return await client.send_command("addRest", {
|
|
34
|
+
"duration": duration,
|
|
35
|
+
"advanceCursorAfterAction": advance_cursor_after_action
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
@mcp.tool()
|
|
39
|
+
async def add_tuplet(duration: dict = {"numerator": 1, "denominator": 4}, ratio: dict = {"numerator": 3, "denominator": 2}, advance_cursor_after_action: bool = True):
|
|
40
|
+
"""Add a tuplet at the current cursor position.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
duration: Base duration as {"numerator": int, "denominator": int}
|
|
44
|
+
ratio: Tuplet ratio as {"numerator": int, "denominator": int} (e.g., {"numerator": 3, "denominator": 2} for triplet)
|
|
45
|
+
advance_cursor_after_action: Whether to move cursor to next position after adding tuplet
|
|
46
|
+
"""
|
|
47
|
+
return await client.send_command("addTuplet", {
|
|
48
|
+
"duration": duration,
|
|
49
|
+
"ratio": ratio,
|
|
50
|
+
"advanceCursorAfterAction": advance_cursor_after_action
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
@mcp.tool()
|
|
54
|
+
async def add_lyrics(lyrics: List[str], verse: int = 0):
|
|
55
|
+
"""Add lyrics to consecutive notes starting from the current cursor position.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
lyrics: List of lyric syllables to add (e.g., ["Hel", "lo", "world"])
|
|
59
|
+
verse: Verse number (0-based, default is 0 for first verse)
|
|
60
|
+
"""
|
|
61
|
+
return await client.send_command("addLyrics", {
|
|
62
|
+
"lyrics": lyrics,
|
|
63
|
+
"verse": verse
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
@mcp.tool()
|
|
67
|
+
async def insert_measure():
|
|
68
|
+
"""Insert a measure at the current position."""
|
|
69
|
+
return await client.send_command("insertMeasure")
|
|
70
|
+
|
|
71
|
+
@mcp.tool()
|
|
72
|
+
async def append_measure(count: int = 1):
|
|
73
|
+
"""Append measures to the end of the score."""
|
|
74
|
+
return await client.send_command("appendMeasure", {"count": count})
|
|
75
|
+
|
|
76
|
+
@mcp.tool()
|
|
77
|
+
async def delete_selection(measure: Optional[int] = None):
|
|
78
|
+
"""Delete the current selection or specified measure."""
|
|
79
|
+
params = {}
|
|
80
|
+
if measure is not None:
|
|
81
|
+
params["measure"] = measure
|
|
82
|
+
return await client.send_command("deleteSelection", params)
|
|
83
|
+
|
|
84
|
+
@mcp.tool()
|
|
85
|
+
async def undo():
|
|
86
|
+
"""Undo the last action."""
|
|
87
|
+
return await client.send_command("undo")
|
src/tools/sequences.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Sequence processing tools for MuseScore MCP."""
|
|
2
|
+
|
|
3
|
+
from ..client import MuseScoreClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_sequence_tools(mcp, client: MuseScoreClient):
|
|
7
|
+
"""Setup sequence processing tools."""
|
|
8
|
+
|
|
9
|
+
@mcp.tool()
|
|
10
|
+
async def processSequence(sequence: list):
|
|
11
|
+
"""Process a sequence of commands.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
sequence: A list of action dictionaries with 'action' and 'params' keys
|
|
15
|
+
"""
|
|
16
|
+
return await client.send_command("processSequence", {"sequence": sequence})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Staff and instrument tools for MuseScore MCP."""
|
|
2
|
+
|
|
3
|
+
from ..client import MuseScoreClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_staff_instruments_tools(mcp, client: MuseScoreClient):
|
|
7
|
+
"""Setup staff and instrument tools."""
|
|
8
|
+
|
|
9
|
+
@mcp.tool()
|
|
10
|
+
async def add_instrument(instrument_id: str):
|
|
11
|
+
"""Add a new staff/instrument to the score.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
instrument_id: ID of the instrument to add
|
|
15
|
+
"""
|
|
16
|
+
return await client.send_command("addInstrument", {
|
|
17
|
+
"instrumentId": instrument_id
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
async def set_staff_mute(staff: int, mute: bool):
|
|
22
|
+
"""Mute or unmute a staff.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
staff: Staff number (0-based)
|
|
26
|
+
mute: True to mute, False to unmute
|
|
27
|
+
"""
|
|
28
|
+
return await client.send_command("setStaffMute", {
|
|
29
|
+
"staff": staff,
|
|
30
|
+
"mute": mute
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
async def set_instrument_sound(staff: int, instrument_id: str):
|
|
35
|
+
"""Change the sound of an instrument on a staff.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
staff: Staff number (0-based)
|
|
39
|
+
instrument_id: ID of the new instrument sound
|
|
40
|
+
"""
|
|
41
|
+
return await client.send_command("setInstrumentSound", {
|
|
42
|
+
"staff": staff,
|
|
43
|
+
"instrumentId": instrument_id
|
|
44
|
+
})
|
src/tools/time_tempo.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Time signature and tempo tools for MuseScore MCP."""
|
|
2
|
+
|
|
3
|
+
from ..client import MuseScoreClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_time_tempo_tools(mcp, client: MuseScoreClient):
|
|
7
|
+
"""Setup time signature and tempo tools."""
|
|
8
|
+
|
|
9
|
+
@mcp.tool()
|
|
10
|
+
async def set_time_signature(numerator: int = 4, denominator: int = 4):
|
|
11
|
+
"""Set the time signature.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
numerator: Top number of time signature (beats per measure)
|
|
15
|
+
denominator: Bottom number of time signature (note value that gets the beat)
|
|
16
|
+
"""
|
|
17
|
+
return await client.send_command("setTimeSignature", {
|
|
18
|
+
"numerator": numerator,
|
|
19
|
+
"denominator": denominator
|
|
20
|
+
})
|
src/types/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Type definitions for MuseScore MCP."""
|
|
2
|
+
|
|
3
|
+
from .action_types import *
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"ActionSequence",
|
|
7
|
+
"getScoreAction",
|
|
8
|
+
"addNoteAction",
|
|
9
|
+
"addRestAction",
|
|
10
|
+
"addTupletAction",
|
|
11
|
+
"addLyricsAction",
|
|
12
|
+
"addInstrumentAction",
|
|
13
|
+
"setStaffMuteAction",
|
|
14
|
+
"setInstrumentSoundAction",
|
|
15
|
+
"appendMeasureAction",
|
|
16
|
+
"deleteSelectionAction",
|
|
17
|
+
"getCursorInfoAction",
|
|
18
|
+
"goToMeasureAction",
|
|
19
|
+
"nextElementAction",
|
|
20
|
+
"prevElementAction",
|
|
21
|
+
"selectCurrentMeasureAction",
|
|
22
|
+
"insertMeasureAction",
|
|
23
|
+
"goToFinalMeasureAction",
|
|
24
|
+
"goToBeginningOfScoreAction",
|
|
25
|
+
"setTimeSignatureAction",
|
|
26
|
+
"undoAction",
|
|
27
|
+
"nextStaffAction",
|
|
28
|
+
"prevStaffAction"
|
|
29
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""TypedDict definitions for MuseScore MCP action sequences."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, List, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ActionSequenceItem(TypedDict):
|
|
7
|
+
"""A single action in a sequence."""
|
|
8
|
+
action: str
|
|
9
|
+
params: Dict[str, Any]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ActionSequence = List[ActionSequenceItem]
|