mcp-music-studio 0.1.1 → 0.1.2

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Forked from [`@modelcontextprotocol/server-sheet-music`](https://github.com/mode
8
8
 
9
9
  - **8 style presets** — rock, jazz, bossa, waltz, march, reggae, folk, classical. One parameter adds drums + bass + chord accompaniment.
10
10
  - **30 instruments** — selectable from UI or via tool parameter, with fuzzy matching
11
- - **Browser fallback** — `openInBrowser: true` launches a standalone player for CLI environments (Claude Code, Codex, Gemini CLI) that don't support ext-apps UI
11
+ - **Three rendering modes** — ext-apps inline UI for Claude Desktop/VS Code, browser fallback for CLI environments, configurable via `--render-mode`
12
12
  - **`get-music-guide` tool** — on-demand reference for AI systems (instruments, drums, ABC syntax, arrangements, genre templates, MIDI directives)
13
13
  - **7 `music://guide/*` resources** — same content for resource-capable clients
14
14
  - **Note highlighting** — currently playing notes light up during playback
@@ -148,7 +148,6 @@ Creates and plays music with visual sheet music and multi-instrument audio.
148
148
  | `tempo` | number? | BPM (40-240) |
149
149
  | `swing` | number? | Swing feel (0-100) |
150
150
  | `transpose` | number? | Semitones (-12 to 12) |
151
- | `openInBrowser` | boolean? | Open standalone browser player (for CLI environments without UI) |
152
151
 
153
152
  **Example — jazz arrangement:**
154
153
  ```json
@@ -174,6 +173,31 @@ Returns reference material for composition. Topics:
174
173
  | `styles` | What each style preset does and when to use it |
175
174
  | `midi-directives` | Full `%%MIDI` reference for ABCJS |
176
175
 
176
+ ## Render Modes
177
+
178
+ The server auto-detects whether the client supports ext-apps UI. For clients that don't, use `--render-mode` to control how music is delivered:
179
+
180
+ | Mode | Flag | Behavior |
181
+ |------|------|----------|
182
+ | `auto` | (default) | Inline ext-apps UI for Claude Desktop, VS Code |
183
+ | `browser` | `--render-mode browser` | Saves HTML player and opens in system browser |
184
+ | `html` | `--render-mode html` | Returns HTML as embedded resource in response |
185
+
186
+ Use `--output-dir` to control where HTML player files are saved (default: `~/Desktop/mcp-music-studio`).
187
+
188
+ **Example — Cherry Studio, CLI environments, or other non-ext-apps clients:**
189
+
190
+ ```json
191
+ {
192
+ "mcpServers": {
193
+ "music-studio": {
194
+ "command": "npx",
195
+ "args": ["-y", "mcp-music-studio", "--stdio", "--render-mode", "browser", "--output-dir", "/path/to/output"]
196
+ }
197
+ }
198
+ }
199
+ ```
200
+
177
201
  ## Development
178
202
 
179
203
  ```bash
package/dist/index.js CHANGED
@@ -30624,13 +30624,13 @@ async function startStreamableHTTPServer(createServer2) {
30624
30624
  }
30625
30625
  }
30626
30626
  });
30627
- const httpServer = app.listen(port, (err) => {
30628
- if (err) {
30629
- console.error("Failed to start server:", err);
30630
- process.exit(1);
30631
- }
30627
+ const httpServer = app.listen(port, () => {
30632
30628
  console.log(`MCP server listening on http://localhost:${port}/mcp`);
30633
30629
  });
30630
+ httpServer.on("error", (err) => {
30631
+ console.error("Failed to start server:", err);
30632
+ process.exit(1);
30633
+ });
30634
30634
  const shutdown = () => {
30635
30635
  console.log(`
30636
30636
  Shutting down...`);
@@ -30642,11 +30642,38 @@ Shutting down...`);
30642
30642
  async function startStdioServer(createServer2) {
30643
30643
  await createServer2().connect(new StdioServerTransport);
30644
30644
  }
30645
+ function parseArg(name) {
30646
+ for (let i = 0;i < process.argv.length; i++) {
30647
+ const arg = process.argv[i].trim();
30648
+ if (arg === name && i + 1 < process.argv.length)
30649
+ return process.argv[i + 1].trim();
30650
+ if (arg.startsWith(name + "="))
30651
+ return arg.slice(name.length + 1).trim();
30652
+ if (arg.startsWith(name + " "))
30653
+ return arg.slice(name.length).trim();
30654
+ }
30655
+ return;
30656
+ }
30657
+ function parseRenderMode() {
30658
+ const val = parseArg("--render-mode");
30659
+ if (!val)
30660
+ return "auto";
30661
+ if (val === "html" || val === "browser" || val === "auto")
30662
+ return val;
30663
+ console.error(`Unknown render mode "${val}", using "auto"`);
30664
+ return "auto";
30665
+ }
30666
+ function parseOutputDir() {
30667
+ return parseArg("--output-dir");
30668
+ }
30645
30669
  async function main() {
30670
+ const defaultRenderMode = parseRenderMode();
30671
+ const outputDir = parseOutputDir();
30672
+ const factory = () => createServer({ defaultRenderMode, outputDir });
30646
30673
  if (process.argv.includes("--stdio")) {
30647
- await startStdioServer(createServer);
30674
+ await startStdioServer(factory);
30648
30675
  } else {
30649
- await startStreamableHTTPServer(createServer);
30676
+ await startStreamableHTTPServer(factory);
30650
30677
  }
30651
30678
  }
30652
30679
  main().catch((e) => {
package/dist/mcp-app.html CHANGED
@@ -228,7 +228,7 @@ X:`),l=o.length;return l===0&&(l=1),l};var r=a.TuneBook=function(s){var o=t(s);t
228
228
  `)}
229
229
  `):`${a.join(`
230
230
  `)}
231
- ${n}`}function f3(e){const t=u3(e),n={};return e.swing!==void 0&&(n.swing=e.swing),{...t,abcNotation:e.abcNotation?h3(e.abcNotation,e):void 0,synthOptions:n}}const ye={visualObj:null,synthControl:null,currentInstrument:ou,currentStyle:"",currentAbc:null,highlightedEls:[]},nr=document.querySelector(".main"),Tc=document.getElementById("status"),At=document.getElementById("sheet-music"),pa=document.getElementById("audio-controls"),lu=document.getElementById("instrument-selector"),uu=document.getElementById("style-selector"),hu=document.getElementById("toolbar");"audioSession"in navigator&&(navigator.audioSession.type="playback");const d3={onEvent(e){if(ye.highlightedEls.forEach(t=>t.classList.remove("note-playing")),ye.highlightedEls=[],!!e.elements){for(const t of e.elements)for(const n of t)n.classList.add("note-playing"),ye.highlightedEls.push(n);ye.highlightedEls.length>0&&ye.highlightedEls[0].scrollIntoView({behavior:"smooth",block:"nearest"})}},onFinished(){ye.highlightedEls.forEach(e=>e.classList.remove("note-playing")),ye.highlightedEls=[]}},_t=document.createElement("select");_t.id="instrument-select";_t.className="control-select";for(const e of Object.keys($t)){const t=document.createElement("option");t.value=e,t.textContent=e,e===ye.currentInstrument&&(t.selected=!0),_t.appendChild(t)}_t.addEventListener("change",()=>{ye.currentInstrument=_t.value,ye.visualObj&&ye.synthControl&&p3()});async function p3(){var e;if(!(!ye.synthControl||!((e=ye.visualObj)!=null&&e[0])))try{const n={program:$t[ye.currentInstrument]??0};await ye.synthControl.setTune(ye.visualObj[0],!1,n)}catch(t){console.error("Failed to apply settings:",t)}}const kr=document.createElement("label");kr.className="control-label";kr.htmlFor="instrument-select";kr.textContent="Instrument";lu.appendChild(kr);lu.appendChild(_t);const ot=document.createElement("select");ot.id="style-select";ot.className="control-select";const hi=document.createElement("option");hi.value="";hi.textContent="No style (melody only)";ot.appendChild(hi);for(const e of Object.keys(li)){const t=document.createElement("option");t.value=e,t.textContent=e.charAt(0).toUpperCase()+e.slice(1),ot.appendChild(t)}ot.addEventListener("change",()=>{ye.currentStyle=ot.value,ye.currentAbc&&fu(ye.currentAbc)});const xr=document.createElement("label");xr.className="control-label";xr.htmlFor="style-select";xr.textContent="Style";uu.appendChild(xr);uu.appendChild(ot);let ir=null;const Jt=document.createElement("button");Jt.className="toolbar-btn";Jt.textContent="⛶";Jt.title="Toggle fullscreen";Jt.addEventListener("click",()=>{ir==null||ir.requestDisplayMode({mode:"fullscreen"})});hu.appendChild(Jt);function It(e,t=!1){Tc.textContent=e,Tc.classList.toggle("error",t)}async function fu(e,t){try{It("Rendering..."),ye.synthControl&&ye.synthControl.pause(),ye.currentAbc=e,At.innerHTML="",pa.innerHTML="";const n=cu(e,ye.currentStyle);if(ye.visualObj=ar.renderAbc(At,n,{responsive:"resize",add_classes:!0}),!ye.visualObj||ye.visualObj.length===0)throw new Error("Failed to parse music notation");if(!ar.synth.supportsAudio())throw new Error("Audio not supported in this browser");ye.synthControl=new ar.synth.SynthController,ye.synthControl.load(pa,d3,{displayLoop:!0,displayPlay:!0,displayProgress:!0,displayWarp:!0});const r={program:$t[ye.currentInstrument]??0,...t};await ye.synthControl.setTune(ye.visualObj[0],!1,r),hu.classList.add("visible"),It("Ready to play!")}catch(n){console.error("Render error:",n),It(`Error: ${n.message}`,!0),pa.innerHTML=""}}const kt=new yg({name:"Music Studio",version:"0.1.0"});ir=kt;kt.ontoolinput=e=>{console.info("Received tool input:",e);const t=f3(e.arguments??{});ye.currentInstrument=t.instrument,_t.value=t.instrument,ye.currentStyle=t.style,ot.value=t.style,t.abcNotation?fu(t.abcNotation,t.synthOptions).catch(console.error):It("No ABC notation provided",!0)};let ga=null,Ec="";kt.ontoolinputpartial=e=>{var a,r;const t=(a=e.arguments)==null?void 0:a.abcNotation;if(!t)return;ye.currentAbc=t;const n=(r=e.arguments)==null?void 0:r.style;if(n&&ui(n)&&ye.currentStyle!==n&&(ye.currentStyle=n,ot.value=n),!t.match(/K:[^\n]+/)){It("Composing...");return}t!==Ec&&(Ec=t,ga&&clearTimeout(ga),ga=setTimeout(()=>{try{const s=cu(t,ye.currentStyle);ar.renderAbc(At,s,{responsive:"resize",add_classes:!0}),At.scrollTop=At.scrollHeight;const o=At.closest(".sheet-section");o&&(o.scrollTop=o.scrollHeight),It("Composing...")}catch{}},150))};kt.onerror=console.error;function du(e){e.safeAreaInsets&&(nr.style.paddingTop=`${e.safeAreaInsets.top}px`,nr.style.paddingRight=`${e.safeAreaInsets.right}px`,nr.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,nr.style.paddingLeft=`${e.safeAreaInsets.left}px`)}kt.onhostcontextchanged=du;kt.connect().then(()=>{const e=kt.getHostContext();e&&du(e)});</script>
231
+ ${n}`}function f3(e){const t=u3(e),n={};return e.swing!==void 0&&(n.swing=e.swing),{...t,abcNotation:e.abcNotation?h3(e.abcNotation,e):void 0,synthOptions:n}}const ye={visualObj:null,synthControl:null,currentInstrument:ou,currentStyle:"",currentAbc:null,highlightedEls:[]},nr=document.querySelector(".main"),Tc=document.getElementById("status"),At=document.getElementById("sheet-music"),pa=document.getElementById("audio-controls"),lu=document.getElementById("instrument-selector"),uu=document.getElementById("style-selector"),hu=document.getElementById("toolbar");"audioSession"in navigator&&(navigator.audioSession.type="playback");const d3={onEvent(e){if(ye.highlightedEls.forEach(t=>t.classList.remove("note-playing")),ye.highlightedEls=[],!!e.elements){for(const t of e.elements)for(const n of t)n.classList.add("note-playing"),ye.highlightedEls.push(n);ye.highlightedEls.length>0&&ye.highlightedEls[0].scrollIntoView({behavior:"smooth",block:"nearest"})}},onFinished(){ye.highlightedEls.forEach(e=>e.classList.remove("note-playing")),ye.highlightedEls=[]}},_t=document.createElement("select");_t.id="instrument-select";_t.className="control-select";for(const e of Object.keys($t)){const t=document.createElement("option");t.value=e,t.textContent=e,e===ye.currentInstrument&&(t.selected=!0),_t.appendChild(t)}_t.addEventListener("change",()=>{ye.currentInstrument=_t.value,ye.visualObj&&ye.synthControl&&p3()});async function p3(){var e;if(!(!ye.synthControl||!((e=ye.visualObj)!=null&&e[0])))try{const n={program:$t[ye.currentInstrument]??0};await ye.synthControl.setTune(ye.visualObj[0],!1,n)}catch(t){console.error("Failed to apply settings:",t)}}const kr=document.createElement("label");kr.className="control-label";kr.htmlFor="instrument-select";kr.textContent="Instrument";lu.appendChild(kr);lu.appendChild(_t);const ot=document.createElement("select");ot.id="style-select";ot.className="control-select";const hi=document.createElement("option");hi.value="";hi.textContent="No style (melody only)";ot.appendChild(hi);for(const e of Object.keys(li)){const t=document.createElement("option");t.value=e,t.textContent=e.charAt(0).toUpperCase()+e.slice(1),ot.appendChild(t)}ot.addEventListener("change",()=>{ye.currentStyle=ot.value,ye.currentAbc&&fu(ye.currentAbc)});const xr=document.createElement("label");xr.className="control-label";xr.htmlFor="style-select";xr.textContent="Style";uu.appendChild(xr);uu.appendChild(ot);let ir=null;const Jt=document.createElement("button");Jt.className="toolbar-btn";Jt.textContent="⛶";Jt.title="Toggle fullscreen";Jt.addEventListener("click",()=>{ir==null||ir.requestDisplayMode({mode:"fullscreen"})});hu.appendChild(Jt);function It(e,t=!1){Tc.textContent=e,Tc.classList.toggle("error",t)}async function fu(e,t){try{It("Rendering..."),ye.synthControl&&ye.synthControl.pause(),ye.currentAbc=e,At.innerHTML="",pa.innerHTML="";const n=cu(e,ye.currentStyle);if(ye.visualObj=ar.renderAbc(At,n,{responsive:"resize",add_classes:!0}),!ye.visualObj||ye.visualObj.length===0)throw new Error("Failed to parse music notation");if(!ar.synth.supportsAudio())throw new Error("Audio not supported in this browser");ye.synthControl=new ar.synth.SynthController,ye.synthControl.load(pa,d3,{displayLoop:!0,displayPlay:!0,displayProgress:!0,displayWarp:!0});const r={program:$t[ye.currentInstrument]??0,...t};await ye.synthControl.setTune(ye.visualObj[0],!1,r),hu.classList.add("visible"),It("Ready to play!")}catch(n){console.error("Render error:",n),It(`Error: ${n.message}`,!0),pa.innerHTML=""}}const kt=new yg({name:"Music Studio",version:"0.1.2"});ir=kt;kt.ontoolinput=e=>{console.info("Received tool input:",e);const t=f3(e.arguments??{});ye.currentInstrument=t.instrument,_t.value=t.instrument,ye.currentStyle=t.style,ot.value=t.style,t.abcNotation?fu(t.abcNotation,t.synthOptions).catch(console.error):It("No ABC notation provided",!0)};let ga=null,Ec="";kt.ontoolinputpartial=e=>{var a,r;const t=(a=e.arguments)==null?void 0:a.abcNotation;if(!t)return;ye.currentAbc=t;const n=(r=e.arguments)==null?void 0:r.style;if(n&&ui(n)&&ye.currentStyle!==n&&(ye.currentStyle=n,ot.value=n),!t.match(/K:[^\n]+/)){It("Composing...");return}t!==Ec&&(Ec=t,ga&&clearTimeout(ga),ga=setTimeout(()=>{try{const s=cu(t,ye.currentStyle);ar.renderAbc(At,s,{responsive:"resize",add_classes:!0}),At.scrollTop=At.scrollHeight;const o=At.closest(".sheet-section");o&&(o.scrollTop=o.scrollHeight),It("Composing...")}catch{}},150))};kt.onerror=console.error;function du(e){e.safeAreaInsets&&(nr.style.paddingTop=`${e.safeAreaInsets.top}px`,nr.style.paddingRight=`${e.safeAreaInsets.right}px`,nr.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,nr.style.paddingLeft=`${e.safeAreaInsets.left}px`)}kt.onhostcontextchanged=du;kt.connect().then(()=>{const e=kt.getHostContext();e&&du(e)});</script>
232
232
  <style rel="stylesheet" crossorigin>.abcjs-inline-audio{height:26px;padding:0 5px;border-radius:3px;background-color:#424242;display:flex;align-items:center;box-sizing:border-box}.abcjs-inline-audio.abcjs-disabled{opacity:.5}.abcjs-inline-audio .abcjs-btn{display:block;width:28px;height:34px;margin-right:2px;padding:7px 4px;background:none!important;border:1px solid transparent;box-sizing:border-box;line-height:1}.abcjs-btn g{fill:#f4f4f4;stroke:#f4f4f4}.abcjs-inline-audio .abcjs-btn:hover g{fill:#ccc;stroke:#ccc}.abcjs-inline-audio .abcjs-midi-selection.abcjs-pushed,.abcjs-inline-audio .abcjs-midi-loop.abcjs-pushed,.abcjs-inline-audio .abcjs-midi-reset.abcjs-pushed{border:1px solid #cccccc;background-color:#666;box-sizing:border-box}.abcjs-inline-audio .abcjs-midi-start .abcjs-pause-svg,.abcjs-inline-audio .abcjs-midi-start .abcjs-loading-svg,.abcjs-inline-audio .abcjs-midi-start.abcjs-pushed .abcjs-play-svg,.abcjs-inline-audio .abcjs-midi-start.abcjs-loading .abcjs-play-svg{display:none}.abcjs-inline-audio .abcjs-midi-start.abcjs-pushed .abcjs-pause-svg{display:block}.abcjs-inline-audio .abcjs-midi-progress-background{background-color:#424242;height:10px;border-radius:5px;border:2px solid #cccccc;margin:0 8px 0 15px;position:relative;flex:1;padding:0;box-sizing:border-box}.abcjs-inline-audio .abcjs-midi-progress-indicator{width:20px;margin-left:-10px;height:14px;background-color:#f4f4f4;position:absolute;display:inline-block;border-radius:6px;top:-4px;left:0;box-sizing:border-box}.abcjs-inline-audio .abcjs-midi-clock{margin-left:4px;margin-top:1px;margin-right:2px;display:inline-block;font-family:sans-serif;font-size:16px;box-sizing:border-box;color:#f4f4f4}.abcjs-inline-audio .abcjs-tempo-wrapper{font-size:10px;color:#f4f4f4;box-sizing:border-box;display:flex;align-items:center}.abcjs-inline-audio .abcjs-midi-tempo{border-radius:2px;border:none;margin:0 2px 0 4px;width:42px;padding-left:2px;box-sizing:border-box}.abcjs-inline-audio .abcjs-loading .abcjs-loading-svg{display:inherit}.abcjs-inline-audio .abcjs-loading{outline:none;animation-name:abcjs-spin;animation-duration:1s;animation-iteration-count:infinite;animation-timing-function:linear}.abcjs-inline-audio .abcjs-loading-svg circle{stroke:#f4f4f4}@keyframes abcjs-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.abcjs-large .abcjs-inline-audio{height:52px}.abcjs-large .abcjs-btn{width:56px;height:52px;font-size:28px;padding:6px 8px}.abcjs-large .abcjs-midi-progress-background{height:20px;border:4px solid #cccccc}.abcjs-large .abcjs-midi-progress-indicator{height:28px;top:-8px;width:40px}.abcjs-large .abcjs-midi-clock{font-size:32px;margin-right:10px;margin-left:10px;margin-top:-1px}.abcjs-large .abcjs-midi-tempo{font-size:20px;width:50px}.abcjs-large .abcjs-tempo-wrapper{font-size:20px}.abcjs-css-warning{display:none}*{box-sizing:border-box}html,body{font-family:system-ui,-apple-system,sans-serif;font-size:1rem}code{font-size:1em}:root{--color-bg: #ffffff;--color-text: #1f2937;--color-text-muted: #6b7280;--color-primary: #2563eb;--color-success: #10b981;--color-danger: #ef4444;--color-card-bg: #f9fafb;--color-border: #e5e7eb}@media(prefers-color-scheme:dark){:root{--color-bg: #111827;--color-text: #f9fafb;--color-text-muted: #9ca3af;--color-primary: #3b82f6;--color-success: #34d399;--color-danger: #f87171;--color-card-bg: #1f2937;--color-border: #374151}}html,body{margin:0;padding:0;background:var(--color-bg);color:var(--color-text)}.main{width:100%;margin:0 auto;padding:8px;display:flex;flex-direction:column;gap:8px}.header{display:flex;flex-direction:column;gap:6px}#audio-controls:empty{display:none}#audio-controls:not(:empty)~#status{display:none}.audio-controls{width:100%}.audio-controls .abcjs-inline-audio{border-radius:8px}.audio-controls .abcjs-inline-audio .abcjs-midi-loop.abcjs-pushed{background-color:var(--color-success)!important;border-color:var(--color-success)!important}.status{font-size:.875rem;color:var(--color-text-muted)}.status.error{color:var(--color-danger)}.toolbar{display:none;flex-wrap:wrap;gap:8px}.toolbar.visible{display:flex}.toolbar-group{display:flex;align-items:center;gap:6px;flex:1;min-width:140px}.control-label{font-size:.75rem;color:var(--color-text-muted);white-space:nowrap;text-transform:uppercase;letter-spacing:.05em}.control-select{flex:1;padding:5px 8px;border:1px solid var(--color-border);border-radius:6px;background:var(--color-card-bg);color:var(--color-text);font-size:.8125rem;cursor:pointer;min-width:0}.control-select:focus{outline:2px solid var(--color-primary);outline-offset:-1px}.toolbar-btn{padding:5px 10px;border:1px solid var(--color-border);border-radius:6px;background:var(--color-card-bg);color:var(--color-text);font-size:1rem;cursor:pointer;line-height:1}.toolbar-btn:hover{background:var(--color-border)}.note-playing path,.note-playing circle,.note-playing ellipse,.note-playing polygon{fill:var(--color-primary)!important;transition:fill .08s ease}.note-playing path[stroke]{stroke:var(--color-primary)!important}.sheet-section{background:var(--color-card-bg);border-radius:8px;padding:16px;border:1px solid var(--color-border);min-height:200px;max-height:80vh;overflow-y:auto;scroll-behavior:smooth}.sheet-section .sheet-music-container{width:100%;overflow-x:auto}</style>
233
233
  </head>
234
234
  <body>
package/dist/server.d.ts CHANGED
@@ -8,5 +8,10 @@ export declare function handlePlaySheetMusic({ abcNotation, }: {
8
8
  export declare function handleGetMusicGuide({ topic, }: {
9
9
  topic: (typeof GUIDE_TOPICS)[number];
10
10
  }): Promise<CallToolResult>;
11
- export declare function createServer(): McpServer;
11
+ export type RenderMode = "auto" | "html" | "browser";
12
+ export interface ServerOptions {
13
+ defaultRenderMode?: RenderMode;
14
+ outputDir?: string;
15
+ }
16
+ export declare function createServer(options?: ServerOptions): McpServer;
12
17
  export {};
package/dist/server.js CHANGED
@@ -52118,6 +52118,12 @@ function uZ(Z, $, J, X) {
52118
52118
  function fZ(Z, $, J, X, D) {
52119
52119
  return Z.registerResource($, J, { mimeType: d, ...X }, D);
52120
52120
  }
52121
+ var OQ = "io.modelcontextprotocol/ui";
52122
+ function dZ(Z) {
52123
+ if (!Z)
52124
+ return;
52125
+ return Z.extensions?.[OQ];
52126
+ }
52121
52127
 
52122
52128
  // src/music-logic.ts
52123
52129
  var INSTRUMENTS = {
@@ -52343,8 +52349,11 @@ function formatKey(raw) {
52343
52349
  return "";
52344
52350
  return raw.replace(/([A-G])b/g, "$1♭").replace(/([A-G])#/g, "$1♯");
52345
52351
  }
52352
+ function stripVoiceNameQuotes(abc) {
52353
+ return abc.replace(/^(V:\S+.*?\bname=)"([^"]*)"(.*)$/gm, "$1$2$3");
52354
+ }
52346
52355
  function generatePlayerHtml(options) {
52347
- let abc = options.abcNotation;
52356
+ let abc = stripVoiceNameQuotes(options.abcNotation);
52348
52357
  if (options.tempo !== undefined || options.transpose !== undefined) {
52349
52358
  abc = injectTempoAndTranspose(abc, {
52350
52359
  tempo: options.tempo,
@@ -52673,12 +52682,12 @@ function generatePlayerHtml(options) {
52673
52682
  </body>
52674
52683
  </html>`;
52675
52684
  }
52676
- async function openPlayerInBrowser(options) {
52685
+ async function openPlayerInBrowser(options, outputDir) {
52677
52686
  const html = generatePlayerHtml(options);
52678
- const tmpDir = path.join(os.tmpdir(), "mcp-music-studio");
52679
- await fs.mkdir(tmpDir, { recursive: true });
52687
+ const outDir = outputDir ?? path.join(os.homedir(), "Desktop", "mcp-music-studio");
52688
+ await fs.mkdir(outDir, { recursive: true });
52680
52689
  const filename = `player-${Date.now()}.html`;
52681
- const filepath = path.join(tmpDir, filename);
52690
+ const filepath = path.join(outDir, filename);
52682
52691
  await fs.writeFile(filepath, html, "utf-8");
52683
52692
  const platform = process.platform;
52684
52693
  let cmd;
@@ -53180,62 +53189,90 @@ async function handleGetMusicGuide({
53180
53189
  content: [{ type: "text", text: GUIDES[topic] }]
53181
53190
  };
53182
53191
  }
53183
- function createServer() {
53192
+ function createServer(options) {
53193
+ const defaultRenderMode = options?.defaultRenderMode ?? "auto";
53194
+ const outputDir = options?.outputDir;
53195
+ let clientSupportsExtApps = false;
53184
53196
  const server = new McpServer({
53185
53197
  name: "Music Studio",
53186
- version: "0.1.0"
53198
+ version: "0.1.2"
53187
53199
  });
53188
53200
  const resourceUri = "ui://sheet-music/mcp-app.html";
53189
- uZ(server, "play-sheet-music", {
53190
- title: "Play Sheet Music",
53191
- description: "Creates and plays music with visual sheet music and multi-instrument audio. " + "Use this to compose original songs, demonstrate music theory, set a mood, " + `celebrate occasions, or play well-known tunes.
53201
+ const playInputSchema = exports_external.object({
53202
+ abcNotation: exports_external.string().default(DEFAULT_ABC_NOTATION_INPUT).describe('ABC notation string. Include chord symbols ("C", "Am7") above notes for auto-accompaniment with style presets.'),
53203
+ instrument: exports_external.string().optional().describe("Default instrument. Options: Acoustic Grand Piano, Electric Piano, Harpsichord, " + "Music Box, Vibraphone, Marimba, Xylophone, Church Organ, Accordion, Harmonica, " + "Acoustic Guitar (Nylon), Acoustic Guitar (Steel), Electric Guitar (Clean), " + "Acoustic Bass, Violin, Viola, Cello, String Ensemble, Trumpet, Trombone, " + "French Horn, Alto Sax, Tenor Sax, Oboe, Clarinet, Flute, Pan Flute, Steel Drums."),
53204
+ style: exports_external.enum(STYLE_NAMES).optional().describe("Accompaniment style. Adds drums, bass, and chord patterns automatically. " + 'Your ABC needs chord symbols ("C", "Am") for accompaniment to work. ' + "Options: rock, jazz, bossa, waltz, march, reggae, folk, classical."),
53205
+ tempo: exports_external.number().min(40).max(240).optional().describe("Tempo in BPM (40-240). Overrides Q: in ABC notation."),
53206
+ swing: exports_external.number().min(0).max(100).optional().describe("Swing percentage (0-100). 0=straight, 33=light swing, 66=heavy swing. Great for jazz and blues."),
53207
+ transpose: exports_external.number().min(-12).max(12).optional().describe("Transpose by semitones (-12 to 12). Positive=higher, negative=lower.")
53208
+ });
53209
+ const BASE_DESCRIPTION = "Creates and plays music with visual sheet music and multi-instrument audio. " + "Use this to compose original songs, demonstrate music theory, set a mood, " + `celebrate occasions, or play well-known tunes.
53192
53210
 
53193
- ` + `WHAT YOU CAN CREATE:
53194
- ` + `- Original compositions for any occasion (birthdays, lullabies, celebrations)
53195
- ` + `- Genre pieces: set the style parameter for automatic drums + bass + chord accompaniment
53196
- ` + `- Educational demos: scales, modes, chord progressions, rhythms
53197
- ` + `- Multi-voice arrangements with different instruments per voice
53211
+ ` + "BEFORE COMPOSING: Call get-music-guide first to learn available instruments, " + "styles, drum patterns, and ABC syntax. Start with topic 'genres' for complete " + `examples, 'styles' for preset details, or 'instruments' for the full list.
53198
53212
 
53199
53213
  ` + `HOW IT WORKS:
53200
- ` + 'Write ABC notation with chord symbols ("C", "Am7", "G7") above the melody. ' + "Set a style to get automatic accompaniment. For known tunes, look up ABC from " + `abcnotation.com or thesession.org.
53214
+ ` + 'Write ABC notation with chord symbols ("C", "Am7", "G7") above the melody. ' + "Set a style to get automatic drums + bass + chord accompaniment. " + `For known tunes, look up ABC from abcnotation.com or thesession.org.
53201
53215
 
53202
53216
  ` + `QUICK ABC REFERENCE:
53203
53217
  ` + `Notes: C D E F G A B c d e (lowercase=octave up) | Rests: z z2 z4
53204
53218
  ` + `Lengths: C2(half) C/2(eighth) C3/2(dotted) | Bars: | |: :| [1 [2
53205
53219
  ` + `Chords: [CEG](simultaneous) "Am"(guitar chord) | Ties: C-C | Slurs: (CDE)
53206
53220
  ` + `Multi-voice: V:1 ... V:2 ... with %%MIDI program N per voice
53207
- ` + `Dynamics: !p! !f! !ff! !mf! | Tempo: Q:1/4=120
53221
+ ` + "Dynamics: !p! !f! !ff! !mf! | Tempo: Q:1/4=120";
53222
+ const EXT_APPS_SUFFIX = `
53208
53223
 
53209
- ` + "Use the get-music-guide tool for detailed reference on instruments, drums, genres, and arrangements.",
53210
- inputSchema: exports_external.object({
53211
- abcNotation: exports_external.string().default(DEFAULT_ABC_NOTATION_INPUT).describe('ABC notation string. Include chord symbols ("C", "Am7") above notes for auto-accompaniment with style presets.'),
53212
- instrument: exports_external.string().optional().describe("Default instrument. Options: Acoustic Grand Piano, Electric Piano, Harpsichord, " + "Music Box, Vibraphone, Marimba, Xylophone, Church Organ, Accordion, Harmonica, " + "Acoustic Guitar (Nylon), Acoustic Guitar (Steel), Electric Guitar (Clean), " + "Acoustic Bass, Violin, Viola, Cello, String Ensemble, Trumpet, Trombone, " + "French Horn, Alto Sax, Tenor Sax, Oboe, Clarinet, Flute, Pan Flute, Steel Drums."),
53213
- style: exports_external.enum(STYLE_NAMES).optional().describe("Accompaniment style. Adds drums, bass, and chord patterns automatically. " + 'Your ABC needs chord symbols ("C", "Am") for accompaniment to work. ' + "Options: rock, jazz, bossa, waltz, march, reggae, folk, classical."),
53214
- tempo: exports_external.number().min(40).max(240).optional().describe("Tempo in BPM (40-240). Overrides Q: in ABC notation."),
53215
- swing: exports_external.number().min(0).max(100).optional().describe("Swing percentage (0-100). 0=straight, 33=light swing, 66=heavy swing. Great for jazz and blues."),
53216
- transpose: exports_external.number().min(-12).max(12).optional().describe("Transpose by semitones (-12 to 12). Positive=higher, negative=lower."),
53217
- openInBrowser: exports_external.boolean().optional().describe("Open a standalone browser player with full playback, note highlighting, and controls. " + "Use this in CLI environments (Claude Code, Codex, Gemini CLI) where the MCP client doesn't render UI.")
53218
- }),
53219
- _meta: { ui: { resourceUri } }
53220
- }, async (args) => {
53224
+ The music player renders inline with interactive playback controls.`;
53225
+ const FALLBACK_SUFFIX = `
53226
+
53227
+ The music player is delivered as HTML or opened in the browser automatically.`;
53228
+ const playHandler = async (args) => {
53221
53229
  const result = await handlePlaySheetMusic(args);
53222
- if (args.openInBrowser && !result.isError) {
53230
+ if (result.isError)
53231
+ return result;
53232
+ if (defaultRenderMode === "auto")
53233
+ return result;
53234
+ const playerOpts = {
53235
+ abcNotation: args.abcNotation,
53236
+ style: args.style,
53237
+ instrument: args.instrument,
53238
+ tempo: args.tempo,
53239
+ swing: args.swing,
53240
+ transpose: args.transpose
53241
+ };
53242
+ if (defaultRenderMode === "html") {
53223
53243
  try {
53224
- const filepath = await openPlayerInBrowser({
53225
- abcNotation: args.abcNotation,
53226
- style: args.style,
53227
- instrument: args.instrument,
53228
- tempo: args.tempo,
53229
- swing: args.swing,
53230
- transpose: args.transpose
53244
+ const html = generatePlayerHtml(playerOpts);
53245
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "";
53246
+ result.content = [
53247
+ { type: "text", text },
53248
+ {
53249
+ type: "resource",
53250
+ resource: {
53251
+ uri: `music://player/${Date.now()}.html`,
53252
+ mimeType: "text/html",
53253
+ text: html
53254
+ }
53255
+ }
53256
+ ];
53257
+ } catch (err) {
53258
+ result.content.push({
53259
+ type: "text",
53260
+ text: `
53261
+ Failed to generate HTML player: ${err.message}`
53231
53262
  });
53263
+ }
53264
+ } else if (defaultRenderMode === "browser") {
53265
+ try {
53266
+ const filepath = await openPlayerInBrowser(playerOpts, outputDir);
53267
+ const fileUrl = `file://${filepath}`;
53232
53268
  const text = result.content[0]?.type === "text" ? result.content[0].text : "";
53233
53269
  result.content = [
53234
53270
  {
53235
53271
  type: "text",
53236
53272
  text: `${text}
53237
53273
 
53238
- Opened music player in browser: ${filepath}`
53274
+ Music player saved and opening in browser.
53275
+ File: ${fileUrl}`
53239
53276
  }
53240
53277
  ];
53241
53278
  } catch (err) {
@@ -53247,7 +53284,26 @@ Failed to open browser: ${err.message}`
53247
53284
  }
53248
53285
  }
53249
53286
  return result;
53250
- });
53287
+ };
53288
+ uZ(server, "play-sheet-music", {
53289
+ title: "Play Sheet Music",
53290
+ description: BASE_DESCRIPTION + FALLBACK_SUFFIX,
53291
+ inputSchema: playInputSchema,
53292
+ _meta: { ui: { resourceUri } }
53293
+ }, playHandler);
53294
+ server.server.oninitialized = () => {
53295
+ const caps = server.server.getClientCapabilities();
53296
+ const uiCap = dZ(caps);
53297
+ clientSupportsExtApps = !!uiCap?.mimeTypes?.includes(d);
53298
+ if (clientSupportsExtApps) {
53299
+ uZ(server, "play-sheet-music", {
53300
+ title: "Play Sheet Music",
53301
+ description: BASE_DESCRIPTION + EXT_APPS_SUFFIX,
53302
+ inputSchema: playInputSchema,
53303
+ _meta: { ui: { resourceUri } }
53304
+ }, playHandler);
53305
+ }
53306
+ };
53251
53307
  server.registerTool("get-music-guide", {
53252
53308
  title: "Music Reference Guide",
53253
53309
  description: "Returns detailed reference material for music composition. " + "Topics: instruments (GM instrument list + combos), drums (patterns + percussion notes), " + "abc-syntax (notation reference), arrangements (multi-voice patterns), " + "genres (complete ABC templates for jazz/blues/folk/rock/bossa/classical), " + "styles (what each style preset does), midi-directives (%%MIDI reference).",
@@ -7,4 +7,4 @@ export interface BrowserPlayerOptions {
7
7
  transpose?: number;
8
8
  }
9
9
  export declare function generatePlayerHtml(options: BrowserPlayerOptions): string;
10
- export declare function openPlayerInBrowser(options: BrowserPlayerOptions): Promise<string>;
10
+ export declare function openPlayerInBrowser(options: BrowserPlayerOptions, outputDir?: string): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-music-studio",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "MCP music composition server — multi-instrument playback, style presets, note highlighting, and AI-friendly reference guides. Fork of @modelcontextprotocol/server-sheet-music.",
6
6
  "keywords": [