diff --git a/examples/sheet-music-server/README.md b/examples/sheet-music-server/README.md new file mode 100644 index 00000000..0eba9ff8 --- /dev/null +++ b/examples/sheet-music-server/README.md @@ -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. + + + + + + +
Twinkle, Twinkle Little StarPlaying on repeat
+ +## 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()`) diff --git a/examples/sheet-music-server/mcp-app.html b/examples/sheet-music-server/mcp-app.html new file mode 100644 index 00000000..f9b53022 --- /dev/null +++ b/examples/sheet-music-server/mcp-app.html @@ -0,0 +1,27 @@ + + + + + + + Sheet Music Viewer + + +
+
+
+ +
+ Waiting for notation... +
+ +
+
+ +
+
+
+ + + + diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json new file mode 100644 index 00000000..a2cd1ca0 --- /dev/null +++ b/examples/sheet-music-server/package.json @@ -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" + } +} diff --git a/examples/sheet-music-server/server.ts b/examples/sheet-music-server/server.ts new file mode 100644 index 00000000..d6da53f2 --- /dev/null +++ b/examples/sheet-music-server/server.ts @@ -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 => { + // 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 => { + 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); diff --git a/examples/sheet-music-server/src/global.css b/examples/sheet-music-server/src/global.css new file mode 100644 index 00000000..97cda440 --- /dev/null +++ b/examples/sheet-music-server/src/global.css @@ -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; +} diff --git a/examples/sheet-music-server/src/mcp-app.css b/examples/sheet-music-server/src/mcp-app.css new file mode 100644 index 00000000..48435e34 --- /dev/null +++ b/examples/sheet-music-server/src/mcp-app.css @@ -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; + } +} diff --git a/examples/sheet-music-server/src/mcp-app.ts b/examples/sheet-music-server/src/mcp-app.ts new file mode 100644 index 00000000..a6efadeb --- /dev/null +++ b/examples/sheet-music-server/src/mcp-app.ts @@ -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 { + 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); + } +}); diff --git a/examples/sheet-music-server/src/server-utils.ts b/examples/sheet-music-server/src/server-utils.ts new file mode 100644 index 00000000..40524237 --- /dev/null +++ b/examples/sheet-music-server/src/server-utils.ts @@ -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 { + 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 { + 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 { + 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(); + + 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; +} diff --git a/examples/sheet-music-server/tsconfig.json b/examples/sheet-music-server/tsconfig.json new file mode 100644 index 00000000..535267b2 --- /dev/null +++ b/examples/sheet-music-server/tsconfig.json @@ -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"] +} diff --git a/examples/sheet-music-server/vite.config.ts b/examples/sheet-music-server/vite.config.ts new file mode 100644 index 00000000..6ff6d997 --- /dev/null +++ b/examples/sheet-music-server/vite.config.ts @@ -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, + }, +}); diff --git a/package-lock.json b/package-lock.json index 00ad1e77..93d25950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -306,7 +306,7 @@ "version": "1.0.0", "dependencies": { "@modelcontextprotocol/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.22.0", + "@modelcontextprotocol/sdk": "^1.24.0", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" @@ -318,7 +318,6 @@ "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^4.3.4", - "bun": "^1.3.2", "concurrently": "^9.2.1", "cors": "^2.8.5", "express": "^5.1.0", @@ -386,6 +385,63 @@ "dev": true, "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": { "version": "1.0.0", "dependencies": { @@ -2394,6 +2450,16 @@ "dev": true, "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2685,40 +2751,6 @@ "resolved": "examples/budget-allocator-server", "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": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.3.tgz", @@ -5656,6 +5688,10 @@ "node": ">=8" } }, + "node_modules/sheet-music-server": { + "resolved": "examples/sheet-music-server", + "link": true + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", diff --git a/tests/e2e/servers.spec.ts b/tests/e2e/servers.spec.ts index eaf83da7..e4b1bb52 100644 --- a/tests/e2e/servers.spec.ts +++ b/tests/e2e/servers.spec.ts @@ -30,6 +30,7 @@ const SERVERS = [ { key: "cohort-heatmap", name: "Cohort Heatmap Server" }, { key: "customer-segmentation", name: "Customer Segmentation Server" }, { key: "scenario-modeler", name: "SaaS Scenario Modeler" }, + { key: "sheet-music", name: "Sheet Music Server" }, { key: "system-monitor", name: "System Monitor Server" }, { key: "threejs", name: "Three.js Server" }, { key: "wiki-explorer", name: "Wiki Explorer" }, diff --git a/tests/e2e/servers.spec.ts-snapshots/sheet-music.png b/tests/e2e/servers.spec.ts-snapshots/sheet-music.png new file mode 100644 index 00000000..54d0a96f Binary files /dev/null and b/tests/e2e/servers.spec.ts-snapshots/sheet-music.png differ