Add sheet-music-server example
Demo MCP App showcasing ABC notation rendering and audio playback with: - Server-side ABC notation validation - Built-in audio player with play/pause and loop controls - Sheet music visualization using abcjs library 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e514e6c745
commit
012fb3260c
13 changed files with 709 additions and 36 deletions
84
examples/sheet-music-server/README.md
Normal file
84
examples/sheet-music-server/README.md
Normal file
|
|
@ -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
examples/sheet-music-server/mcp-app.html
Normal file
27
examples/sheet-music-server/mcp-app.html
Normal file
|
|
@ -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
examples/sheet-music-server/package.json
Normal file
34
examples/sheet-music-server/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
119
examples/sheet-music-server/server.ts
Normal file
119
examples/sheet-music-server/server.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
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,
|
||||||
|
_meta: {
|
||||||
|
ui: {
|
||||||
|
csp: {
|
||||||
|
// Allow loading soundfonts for audio playback
|
||||||
|
connectDomains: ["https://paulrosen.github.io"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer(createServer);
|
||||||
12
examples/sheet-music-server/src/global.css
Normal file
12
examples/sheet-music-server/src/global.css
Normal file
|
|
@ -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
examples/sheet-music-server/src/mcp-app.css
Normal file
87
examples/sheet-music-server/src/mcp-app.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
examples/sheet-music-server/src/mcp-app.ts
Normal file
120
examples/sheet-music-server/src/mcp-app.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* @file Sheet Music App - renders ABC notation with abcjs and provides audio playback
|
||||||
|
*/
|
||||||
|
import { App } from "@modelcontextprotocol/ext-apps";
|
||||||
|
import ABCJS from "abcjs";
|
||||||
|
import "abcjs/abcjs-audio.css";
|
||||||
|
import "./global.css";
|
||||||
|
import "./mcp-app.css";
|
||||||
|
|
||||||
|
const log = {
|
||||||
|
info: console.log.bind(console, "[APP]"),
|
||||||
|
error: console.error.bind(console, "[APP]"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// State management
|
||||||
|
interface AppState {
|
||||||
|
visualObj: ABCJS.TuneObject[] | null;
|
||||||
|
synthControl: ABCJS.SynthObjectController | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state: AppState = {
|
||||||
|
visualObj: null,
|
||||||
|
synthControl: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM element references
|
||||||
|
const mainEl = document.querySelector(".main") as HTMLElement;
|
||||||
|
const statusEl = document.getElementById("status")!;
|
||||||
|
const sheetMusicEl = document.getElementById("sheet-music")!;
|
||||||
|
const audioControlsEl = document.getElementById("audio-controls")!;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the status display.
|
||||||
|
*/
|
||||||
|
function setStatus(text: string, isError = false): void {
|
||||||
|
statusEl.textContent = text;
|
||||||
|
statusEl.classList.toggle("error", isError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders ABC notation to sheet music and sets up audio playback
|
||||||
|
*/
|
||||||
|
async function renderAbc(abcNotation: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
setStatus("Rendering...");
|
||||||
|
|
||||||
|
// Clear previous content
|
||||||
|
sheetMusicEl.innerHTML = "";
|
||||||
|
audioControlsEl.innerHTML = "";
|
||||||
|
|
||||||
|
// Render the sheet music visually
|
||||||
|
state.visualObj = ABCJS.renderAbc(sheetMusicEl, abcNotation, {
|
||||||
|
responsive: "resize",
|
||||||
|
add_classes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state.visualObj || state.visualObj.length === 0) {
|
||||||
|
throw new Error("Failed to parse music notation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ABCJS.synth.supportsAudio()) {
|
||||||
|
throw new Error("Audio not supported in this browser");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create synth controller with UI controls
|
||||||
|
state.synthControl = new ABCJS.synth.SynthController();
|
||||||
|
state.synthControl.load(audioControlsEl, null, {
|
||||||
|
displayLoop: true,
|
||||||
|
// displayRestart: true,
|
||||||
|
displayPlay: true,
|
||||||
|
displayProgress: true,
|
||||||
|
// displayWarp: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect synth to the rendered tune
|
||||||
|
await state.synthControl.setTune(state.visualObj[0], false, {});
|
||||||
|
|
||||||
|
setStatus("Ready to play!");
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Render error:", error);
|
||||||
|
setStatus(`Error: ${(error as Error).message}`, true);
|
||||||
|
audioControlsEl.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create app instance
|
||||||
|
const app = new App({ name: "Sheet Music App", version: "1.0.0" });
|
||||||
|
|
||||||
|
// Handle tool input - receives ABC notation from the host
|
||||||
|
app.ontoolinput = (params) => {
|
||||||
|
log.info("Received tool input:", params);
|
||||||
|
|
||||||
|
const abcNotation = params.arguments?.abcNotation as string | undefined;
|
||||||
|
|
||||||
|
if (abcNotation) {
|
||||||
|
renderAbc(abcNotation);
|
||||||
|
} else {
|
||||||
|
setStatus("No ABC notation provided", true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
app.onerror = log.error;
|
||||||
|
|
||||||
|
app.onhostcontextchanged = (params) => {
|
||||||
|
if (params.safeAreaInsets) {
|
||||||
|
mainEl.style.paddingTop = `${params.safeAreaInsets.top}px`;
|
||||||
|
mainEl.style.paddingRight = `${params.safeAreaInsets.right}px`;
|
||||||
|
mainEl.style.paddingBottom = `${params.safeAreaInsets.bottom}px`;
|
||||||
|
mainEl.style.paddingLeft = `${params.safeAreaInsets.left}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect to host
|
||||||
|
app.connect().then(() => {
|
||||||
|
const ctx = app.getHostContext();
|
||||||
|
if (ctx) {
|
||||||
|
app.onhostcontextchanged(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
110
examples/sheet-music-server/src/server-utils.ts
Normal file
110
examples/sheet-music-server/src/server-utils.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
/**
|
||||||
|
* Shared utilities for running MCP servers with various transports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||||
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
|
import cors from "cors";
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an MCP server using the appropriate transport based on command-line arguments.
|
||||||
|
*
|
||||||
|
* If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport.
|
||||||
|
*
|
||||||
|
* @param createServer - Factory function that creates a new McpServer instance.
|
||||||
|
*/
|
||||||
|
export async function startServer(
|
||||||
|
createServer: () => McpServer,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (process.argv.includes("--stdio")) {
|
||||||
|
await startStdioServer(createServer);
|
||||||
|
} else {
|
||||||
|
await startStreamableHttpServer(createServer);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an MCP server with stdio transport.
|
||||||
|
*
|
||||||
|
* @param createServer - Factory function that creates a new McpServer instance.
|
||||||
|
*/
|
||||||
|
export async function startStdioServer(
|
||||||
|
createServer: () => McpServer,
|
||||||
|
): Promise<void> {
|
||||||
|
await createServer().connect(new StdioServerTransport());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
||||||
|
*
|
||||||
|
* Each request creates a fresh server and transport instance, which are
|
||||||
|
* closed when the response ends (no session tracking).
|
||||||
|
*
|
||||||
|
* The server listens on the port specified by the PORT environment variable,
|
||||||
|
* defaulting to 3001 if not set.
|
||||||
|
*
|
||||||
|
* @param createServer - Factory function that creates a new McpServer instance per request.
|
||||||
|
*/
|
||||||
|
export async function startStreamableHttpServer(
|
||||||
|
createServer: () => McpServer,
|
||||||
|
): Promise<void> {
|
||||||
|
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||||
|
|
||||||
|
// Express app - bind to all interfaces for development/testing
|
||||||
|
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
|
||||||
|
expressApp.use(cors());
|
||||||
|
|
||||||
|
expressApp.all("/mcp", async (req: Request, res: Response) => {
|
||||||
|
// Create fresh server and transport for each request (stateless mode)
|
||||||
|
const server = createServer();
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up when response ends
|
||||||
|
res.on("close", () => {
|
||||||
|
transport.close().catch(() => {});
|
||||||
|
server.close().catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.connect(transport);
|
||||||
|
await transport.handleRequest(req, res, req.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MCP error:", error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: { code: -32603, message: "Internal server error" },
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||||
|
|
||||||
|
const httpServer = expressApp.listen(port, (err?: Error) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
console.log(`Server listening on http://localhost:${port}/mcp`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const shutdown = () => {
|
||||||
|
console.log("\nShutting down...");
|
||||||
|
httpServer.close(() => process.exit(0));
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGINT", shutdown);
|
||||||
|
process.on("SIGTERM", shutdown);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
19
examples/sheet-music-server/tsconfig.json
Normal file
19
examples/sheet-music-server/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src", "server.ts"]
|
||||||
|
}
|
||||||
24
examples/sheet-music-server/vite.config.ts
Normal file
24
examples/sheet-music-server/vite.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
|
||||||
|
const INPUT = process.env.INPUT;
|
||||||
|
if (!INPUT) {
|
||||||
|
throw new Error("INPUT environment variable is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [viteSingleFile()],
|
||||||
|
build: {
|
||||||
|
sourcemap: isDevelopment ? "inline" : undefined,
|
||||||
|
cssMinify: !isDevelopment,
|
||||||
|
minify: !isDevelopment,
|
||||||
|
|
||||||
|
rollupOptions: {
|
||||||
|
input: INPUT,
|
||||||
|
},
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
108
package-lock.json
generated
108
package-lock.json
generated
|
|
@ -306,7 +306,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/ext-apps": "../..",
|
"@modelcontextprotocol/ext-apps": "../..",
|
||||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
|
|
@ -318,7 +318,6 @@
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"bun": "^1.3.2",
|
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
|
@ -386,6 +385,63 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"examples/sheet-music-server": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples/sheet-music-server/node_modules/@types/node": {
|
||||||
|
"version": "22.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||||
|
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples/sheet-music-server/node_modules/cross-env": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "src/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples/sheet-music-server/node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"examples/system-monitor-server": {
|
"examples/system-monitor-server": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -2394,6 +2450,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/abcjs": {
|
||||||
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/abcjs/-/abcjs-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-XLDZPy/4TZbOqPsLwuu0Umsl79NTAcObEkboPxdYZXI8/fU6PNh59SAnkZOnEPVbyT8EXfBUjgNoe/uKd3T0xQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/paulrosen"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
|
@ -2685,40 +2751,6 @@
|
||||||
"resolved": "examples/budget-allocator-server",
|
"resolved": "examples/budget-allocator-server",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/bun": {
|
|
||||||
"version": "1.3.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/bun/-/bun-1.3.5.tgz",
|
|
||||||
"integrity": "sha512-c1YHIGUfgvYPJmLug5QiLzNWlX2Dg7X/67JWu1Va+AmMXNXzC/KQn2lgQ7rD+n1u1UqDpJMowVGGxTNpbPydNw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64",
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"os": [
|
|
||||||
"darwin",
|
|
||||||
"linux",
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"bin": {
|
|
||||||
"bun": "bin/bun.exe",
|
|
||||||
"bunx": "bin/bunx.exe"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@oven/bun-darwin-aarch64": "1.3.5",
|
|
||||||
"@oven/bun-darwin-x64": "1.3.5",
|
|
||||||
"@oven/bun-darwin-x64-baseline": "1.3.5",
|
|
||||||
"@oven/bun-linux-aarch64": "1.3.5",
|
|
||||||
"@oven/bun-linux-aarch64-musl": "1.3.5",
|
|
||||||
"@oven/bun-linux-x64": "1.3.5",
|
|
||||||
"@oven/bun-linux-x64-baseline": "1.3.5",
|
|
||||||
"@oven/bun-linux-x64-musl": "1.3.5",
|
|
||||||
"@oven/bun-linux-x64-musl-baseline": "1.3.5",
|
|
||||||
"@oven/bun-windows-x64": "1.3.5",
|
|
||||||
"@oven/bun-windows-x64-baseline": "1.3.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bun-types": {
|
"node_modules/bun-types": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.3.tgz",
|
||||||
|
|
@ -5656,6 +5688,10 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sheet-music-server": {
|
||||||
|
"resolved": "examples/sheet-music-server",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/shell-quote": {
|
"node_modules/shell-quote": {
|
||||||
"version": "1.8.3",
|
"version": "1.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ const SERVERS = [
|
||||||
{ key: "cohort-heatmap", name: "Cohort Heatmap Server" },
|
{ key: "cohort-heatmap", name: "Cohort Heatmap Server" },
|
||||||
{ key: "customer-segmentation", name: "Customer Segmentation Server" },
|
{ key: "customer-segmentation", name: "Customer Segmentation Server" },
|
||||||
{ key: "scenario-modeler", name: "SaaS Scenario Modeler" },
|
{ key: "scenario-modeler", name: "SaaS Scenario Modeler" },
|
||||||
|
{ key: "sheet-music", name: "Sheet Music Server" },
|
||||||
{ key: "system-monitor", name: "System Monitor Server" },
|
{ key: "system-monitor", name: "System Monitor Server" },
|
||||||
{ key: "threejs", name: "Three.js Server" },
|
{ key: "threejs", name: "Three.js Server" },
|
||||||
{ key: "wiki-explorer", name: "Wiki Explorer" },
|
{ key: "wiki-explorer", name: "Wiki Explorer" },
|
||||||
|
|
|
||||||
BIN
tests/e2e/servers.spec.ts-snapshots/sheet-music.png
Normal file
BIN
tests/e2e/servers.spec.ts-snapshots/sheet-music.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Loading…
Reference in a new issue