Skip to content

Commit d925af5

Browse files
authored
Merge pull request #196 from jonathanhefner/sheet-music-example
Add `sheet-music-server` example
2 parents 38126f7 + 012fb32 commit d925af5

13 files changed

Lines changed: 709 additions & 36 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Example: Sheet Music Server
2+
3+
A demo MCP App that renders [ABC notation](https://en.wikipedia.org/wiki/ABC_notation) as sheet music with interactive audio playback using the [abcjs](https://www.abcjs.net/) library.
4+
5+
<table>
6+
<tr>
7+
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/sheet-music-server/01-twinkle-twinkle-little-star.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/sheet-music-server/01-twinkle-twinkle-little-star.png" alt="Twinkle, Twinkle Little Star" width="100%"></a></td>
8+
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/sheet-music-server/02-playing-on-repeat.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/sheet-music-server/02-playing-on-repeat.png" alt="Playing on repeat" width="100%"></a></td>
9+
</tr>
10+
</table>
11+
12+
## Features
13+
14+
- **Audio Playback**: Built-in audio player with play/pause and loop controls
15+
- **Sheet Music Rendering**: Displays ABC notation as properly formatted sheet music
16+
17+
## Running
18+
19+
1. Install dependencies:
20+
21+
```bash
22+
npm install
23+
```
24+
25+
2. Build and start the server:
26+
27+
```bash
28+
npm run start:http # for Streamable HTTP transport
29+
# OR
30+
npm run start:stdio # for stdio transport
31+
```
32+
33+
3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
34+
35+
### Tool Input
36+
37+
When calling the `play-sheet-music` tool, provide ABC notation:
38+
39+
```json
40+
{
41+
"abcNotation": "X:1\nT:C Major Scale\nM:4/4\nL:1/4\nK:C\nC D E F | G A B c |"
42+
}
43+
```
44+
45+
#### ABC Notation Examples
46+
47+
**C Major Scale:**
48+
49+
```abc
50+
X:1
51+
T:C Major Scale
52+
M:4/4
53+
L:1/4
54+
K:C
55+
C D E F | G A B c |
56+
```
57+
58+
**Twinkle, Twinkle Little Star:**
59+
60+
```abc
61+
X:1
62+
T:Twinkle, Twinkle Little Star
63+
M:4/4
64+
L:1/4
65+
K:C
66+
C C G G | A A G2 | F F E E | D D C2 |
67+
G G F F | E E D2 | G G F F | E E D2 |
68+
C C G G | A A G2 | F F E E | D D C2 |
69+
```
70+
71+
## Architecture
72+
73+
### Server (`server.ts`)
74+
75+
Exposes a single `play-sheet-music` tool that accepts:
76+
77+
- `abcNotation`: ABC notation string to render
78+
79+
The tool validates the ABC notation server-side using the abcjs parser and returns any parse errors. The actual rendering happens client-side when the UI receives the tool input.
80+
81+
### App (`src/mcp-app.ts`)
82+
83+
- Receives ABC notation via `ontoolinput` handler
84+
- Uses abcjs for audio playback controls and sheet music rendering (in `renderAbc()`)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="color-scheme" content="light dark">
7+
<title>Sheet Music Viewer</title>
8+
</head>
9+
<body>
10+
<main class="main">
11+
<header class="header">
12+
<div id="audio-controls" class="audio-controls abcjs-large">
13+
<!-- SynthController will inject audio controls here -->
14+
</div>
15+
<span id="status" class="status">Waiting for notation...</span>
16+
</header>
17+
18+
<section class="sheet-section">
19+
<div id="sheet-music" class="sheet-music-container">
20+
<!-- ABC notation will be rendered here by abcjs -->
21+
</div>
22+
</section>
23+
</main>
24+
25+
<script type="module" src="./src/mcp-app.ts"></script>
26+
</body>
27+
</html>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "sheet-music-server",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
8+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
9+
"serve:http": "bun server.ts",
10+
"serve:stdio": "bun server.ts --stdio",
11+
"start": "npm run start:http",
12+
"start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http",
13+
"start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio",
14+
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve:http'"
15+
},
16+
"dependencies": {
17+
"@modelcontextprotocol/ext-apps": "../..",
18+
"@modelcontextprotocol/sdk": "^1.24.0",
19+
"abcjs": "^6.4.4",
20+
"zod": "^4.1.13"
21+
},
22+
"devDependencies": {
23+
"@types/cors": "^2.8.19",
24+
"@types/express": "^5.0.0",
25+
"@types/node": "^22.0.0",
26+
"concurrently": "^9.2.1",
27+
"cors": "^2.8.5",
28+
"cross-env": "^7.0.3",
29+
"express": "^5.1.0",
30+
"typescript": "^5.9.3",
31+
"vite": "^6.0.0",
32+
"vite-plugin-singlefile": "^2.3.0"
33+
}
34+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import type {
3+
CallToolResult,
4+
ReadResourceResult,
5+
} from "@modelcontextprotocol/sdk/types.js";
6+
import fs from "node:fs/promises";
7+
import path from "node:path";
8+
import { z } from "zod";
9+
import ABCJS from "abcjs";
10+
import {
11+
RESOURCE_MIME_TYPE,
12+
RESOURCE_URI_META_KEY,
13+
registerAppResource,
14+
registerAppTool,
15+
} from "@modelcontextprotocol/ext-apps/server";
16+
import { startServer } from "./src/server-utils.js";
17+
18+
const DIST_DIR = path.join(import.meta.dirname, "dist");
19+
20+
const DEFAULT_ABC_NOTATION_INPUT = `X:1
21+
T:Twinkle, Twinkle Little Star
22+
M:4/4
23+
L:1/4
24+
K:C
25+
C C G G | A A G2 | F F E E | D D C2 |
26+
G G F F | E E D2 | G G F F | E E D2 |
27+
C C G G | A A G2 | F F E E | D D C2 |`;
28+
29+
/**
30+
* Creates a new MCP server instance with the sheet music tool and resource.
31+
*/
32+
function createServer(): McpServer {
33+
const server = new McpServer({
34+
name: "Sheet Music Server",
35+
version: "1.0.0",
36+
});
37+
38+
const resourceUri = "ui://sheet-music/mcp-app.html";
39+
40+
// Register the play-sheet-music tool.
41+
// Validates ABC notation server-side, then the client renders via ontoolinput.
42+
registerAppTool(
43+
server,
44+
"play-sheet-music",
45+
{
46+
title: "Play Sheet Music",
47+
description:
48+
"Plays music from ABC notation with audio playback and visual sheet music. " +
49+
"Use this to compose original songs (for birthdays, holidays, or any occasion) " +
50+
"or perform well-known tunes (folk songs, nursery rhymes, hymns, classical melodies). " +
51+
"For accurate renditions of well-known tunes, look up the ABC notation from " +
52+
"abcnotation.com or thesession.org rather than recalling from memory.",
53+
inputSchema: z.object({
54+
abcNotation: z
55+
.string()
56+
.default(DEFAULT_ABC_NOTATION_INPUT)
57+
.describe(
58+
"ABC notation string to render as sheet music with audio playback",
59+
),
60+
}),
61+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
62+
},
63+
async ({ abcNotation }): Promise<CallToolResult> => {
64+
// Validate ABC notation using abcjs parser
65+
const [{ warnings }] = ABCJS.parseOnly(abcNotation);
66+
67+
// Check for parse warnings (abcjs reports errors as warnings)
68+
if (warnings && warnings.length > 0) {
69+
// Strip HTML markup from warning messages
70+
const messages = warnings.map((w) => w.replace(/<[^>]*>/g, ""));
71+
const error = `Invalid ABC notation:\n${messages.join("\n")}`;
72+
return {
73+
isError: true,
74+
content: [{ type: "text", text: error }],
75+
};
76+
}
77+
78+
return {
79+
content: [{ type: "text", text: "Input parsed successfully." }],
80+
};
81+
},
82+
);
83+
84+
// Register the UI resource that serves the bundled HTML/JS/CSS.
85+
registerAppResource(
86+
server,
87+
resourceUri,
88+
resourceUri,
89+
{ mimeType: RESOURCE_MIME_TYPE, description: "Sheet Music Viewer UI" },
90+
async (): Promise<ReadResourceResult> => {
91+
const html = await fs.readFile(
92+
path.join(DIST_DIR, "mcp-app.html"),
93+
"utf-8",
94+
);
95+
96+
return {
97+
contents: [
98+
{
99+
uri: resourceUri,
100+
mimeType: RESOURCE_MIME_TYPE,
101+
text: html,
102+
_meta: {
103+
ui: {
104+
csp: {
105+
// Allow loading soundfonts for audio playback
106+
connectDomains: ["https://paulrosen.github.io"],
107+
},
108+
},
109+
},
110+
},
111+
],
112+
};
113+
},
114+
);
115+
116+
return server;
117+
}
118+
119+
startServer(createServer);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
html, body {
6+
font-family: system-ui, -apple-system, sans-serif;
7+
font-size: 1rem;
8+
}
9+
10+
code {
11+
font-size: 1em;
12+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
:root {
2+
--color-bg: #ffffff;
3+
--color-text: #1f2937;
4+
--color-text-muted: #6b7280;
5+
--color-primary: #2563eb;
6+
--color-success: #10b981;
7+
--color-danger: #ef4444;
8+
--color-card-bg: #f9fafb;
9+
--color-border: #e5e7eb;
10+
}
11+
12+
@media (prefers-color-scheme: dark) {
13+
:root {
14+
--color-bg: #111827;
15+
--color-text: #f9fafb;
16+
--color-text-muted: #9ca3af;
17+
--color-primary: #3b82f6;
18+
--color-success: #34d399;
19+
--color-danger: #f87171;
20+
--color-card-bg: #1f2937;
21+
--color-border: #374151;
22+
}
23+
}
24+
25+
html,
26+
body {
27+
margin: 0;
28+
padding: 0;
29+
background: var(--color-bg);
30+
color: var(--color-text);
31+
}
32+
33+
.main {
34+
width: 100%;
35+
margin: 0 auto;
36+
padding: 8px;
37+
display: flex;
38+
flex-direction: column;
39+
gap: 8px;
40+
}
41+
42+
/* Header */
43+
#audio-controls:empty {
44+
display: none;
45+
}
46+
47+
#audio-controls:not(:empty) ~ #status {
48+
display: none;
49+
}
50+
51+
.audio-controls {
52+
width: 100%;
53+
54+
.abcjs-inline-audio {
55+
border-radius: 8px;
56+
57+
/* Make loop button active state more visible */
58+
.abcjs-midi-loop.abcjs-pushed {
59+
background-color: var(--color-success) !important;
60+
border-color: var(--color-success) !important;
61+
}
62+
}
63+
}
64+
65+
.status {
66+
font-size: 0.875rem;
67+
color: var(--color-text-muted);
68+
}
69+
70+
.status.error {
71+
color: var(--color-danger);
72+
}
73+
74+
75+
/* Sheet Music Section */
76+
.sheet-section {
77+
background: var(--color-card-bg);
78+
border-radius: 8px;
79+
padding: 16px;
80+
border: 1px solid var(--color-border);
81+
min-height: 500px;
82+
83+
.sheet-music-container {
84+
width: 100%;
85+
overflow-x: auto;
86+
}
87+
}

0 commit comments

Comments
 (0)