import { McpServer, type CallToolResult, type ReadResourceResult, } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import ABCJS from "abcjs"; import { RESOURCE_MIME_TYPE, registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") : import.meta.dirname; 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. */ export 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: { ui: { 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; }