Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions examples/sheet-music-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Example: Sheet Music Server

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.

<table>
<tr>
<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>
<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>
</tr>
</table>

## Features

- **Audio Playback**: Built-in audio player with play/pause and loop controls
- **Sheet Music Rendering**: Displays ABC notation as properly formatted sheet music

## Running

1. Install dependencies:

```bash
npm install
```

2. Build and start the server:

```bash
npm run start:http # for Streamable HTTP transport
# OR
npm run start:stdio # for stdio transport
```

3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.

### Tool Input

When calling the `play-sheet-music` tool, provide ABC notation:

```json
{
"abcNotation": "X:1\nT:C Major Scale\nM:4/4\nL:1/4\nK:C\nC D E F | G A B c |"
}
```

#### ABC Notation Examples

**C Major Scale:**

```abc
X:1
T:C Major Scale
M:4/4
L:1/4
K:C
C D E F | G A B c |
```

**Twinkle, Twinkle Little Star:**

```abc
X:1
T:Twinkle, Twinkle Little Star
M:4/4
L:1/4
K:C
C C G G | A A G2 | F F E E | D D C2 |
G G F F | E E D2 | G G F F | E E D2 |
C C G G | A A G2 | F F E E | D D C2 |
```

## Architecture

### Server (`server.ts`)

Exposes a single `play-sheet-music` tool that accepts:

- `abcNotation`: ABC notation string to render

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.

### App (`src/mcp-app.ts`)

- Receives ABC notation via `ontoolinput` handler
- Uses abcjs for audio playback controls and sheet music rendering (in `renderAbc()`)
27 changes: 27 additions & 0 deletions examples/sheet-music-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>Sheet Music Viewer</title>
</head>
<body>
<main class="main">
<header class="header">
<div id="audio-controls" class="audio-controls abcjs-large">
<!-- SynthController will inject audio controls here -->
</div>
<span id="status" class="status">Waiting for notation...</span>
</header>

<section class="sheet-section">
<div id="sheet-music" class="sheet-music-container">
<!-- ABC notation will be rendered here by abcjs -->
</div>
</section>
</main>

<script type="module" src="./src/mcp-app.ts"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions examples/sheet-music-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "sheet-music-server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve:http": "bun server.ts",
"serve:stdio": "bun server.ts --stdio",
"start": "npm run start:http",
"start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http",
"start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio",
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve:http'"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "../..",
"@modelcontextprotocol/sdk": "^1.24.0",
"abcjs": "^6.4.4",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"express": "^5.1.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
}
}
111 changes: 111 additions & 0 deletions examples/sheet-music-server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type {
CallToolResult,
ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
import ABCJS from "abcjs";
import {
RESOURCE_MIME_TYPE,
RESOURCE_URI_META_KEY,
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
import { startServer } from "./src/server-utils.js";

const DIST_DIR = path.join(import.meta.dirname, "dist");

const DEFAULT_ABC_NOTATION_INPUT = `X:1
T:Twinkle, Twinkle Little Star
M:4/4
L:1/4
K:C
C C G G | A A G2 | F F E E | D D C2 |
G G F F | E E D2 | G G F F | E E D2 |
C C G G | A A G2 | F F E E | D D C2 |`;

/**
* Creates a new MCP server instance with the sheet music tool and resource.
*/
function createServer(): McpServer {
const server = new McpServer({
name: "Sheet Music Server",
version: "1.0.0",
});

const resourceUri = "ui://sheet-music/mcp-app.html";

// Register the play-sheet-music tool.
// Validates ABC notation server-side, then the client renders via ontoolinput.
registerAppTool(
server,
"play-sheet-music",
{
title: "Play Sheet Music",
description:
"Plays music from ABC notation with audio playback and visual sheet music. " +
"Use this to compose original songs (for birthdays, holidays, or any occasion) " +
"or perform well-known tunes (folk songs, nursery rhymes, hymns, classical melodies). " +
"For accurate renditions of well-known tunes, look up the ABC notation from " +
"abcnotation.com or thesession.org rather than recalling from memory.",
inputSchema: z.object({
abcNotation: z
.string()
.default(DEFAULT_ABC_NOTATION_INPUT)
.describe(
"ABC notation string to render as sheet music with audio playback",
),
}),
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async ({ abcNotation }): Promise<CallToolResult> => {
// Validate ABC notation using abcjs parser
const [{ warnings }] = ABCJS.parseOnly(abcNotation);

// Check for parse warnings (abcjs reports errors as warnings)
if (warnings && warnings.length > 0) {
// Strip HTML markup from warning messages
const messages = warnings.map((w) => w.replace(/<[^>]*>/g, ""));
const error = `Invalid ABC notation:\n${messages.join("\n")}`;
return {
isError: true,
content: [{ type: "text", text: error }],
};
}

return {
content: [{ type: "text", text: "Input parsed successfully." }],
};
},
);

// Register the UI resource that serves the bundled HTML/JS/CSS.
registerAppResource(
server,
resourceUri,
resourceUri,
{ mimeType: RESOURCE_MIME_TYPE, description: "Sheet Music Viewer UI" },
async (): Promise<ReadResourceResult> => {
const html = await fs.readFile(
path.join(DIST_DIR, "mcp-app.html"),
"utf-8",
);

return {
contents: [
{
uri: resourceUri,
mimeType: RESOURCE_MIME_TYPE,
text: html,
},
],
};
},
);

return server;
}

startServer(createServer);
12 changes: 12 additions & 0 deletions examples/sheet-music-server/src/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
* {
box-sizing: border-box;
}

html, body {
font-family: system-ui, -apple-system, sans-serif;
font-size: 1rem;
}

code {
font-size: 1em;
}
87 changes: 87 additions & 0 deletions examples/sheet-music-server/src/mcp-app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
: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 */
#audio-controls:empty {
display: none;
}

#audio-controls:not(:empty) ~ #status {
display: none;
}

.audio-controls {
width: 100%;

.abcjs-inline-audio {
border-radius: 8px;

/* Make loop button active state more visible */
.abcjs-midi-loop.abcjs-pushed {
background-color: var(--color-success) !important;
border-color: var(--color-success) !important;
}
}
}

.status {
font-size: 0.875rem;
color: var(--color-text-muted);
}

.status.error {
color: var(--color-danger);
}


/* Sheet Music Section */
.sheet-section {
background: var(--color-card-bg);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--color-border);
min-height: 500px;

.sheet-music-container {
width: 100%;
overflow-x: auto;
}
}
Loading
Loading