ext-apps/examples/sheet-music-server/server.ts
Olivier Chafik 89a684546b
fix(examples): resolve DIST_DIR path for npm package execution (#277)
* fix(examples): resolve DIST_DIR path for npm package execution

When servers are compiled to dist/server.js and published to npm,
`import.meta.dirname` already points to dist/, so joining with "dist"
results in dist/dist/ which doesn't exist.

This fix detects whether we're running from TypeScript source or
compiled JavaScript and adjusts the path accordingly:
- From source (server.ts): use dist/ subdirectory
- From compiled (dist/server.js): use current directory

Fixes ENOENT errors like:
"ENOENT: no such file or directory, open '.../dist/dist/mcp-app.html'"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(pdf-server): mark pdfjs-dist as external in bundle

The pdf.worker.mjs file wasn't being found when running from npm
because bun bundled pdfjs-dist but didn't include the worker file.

By marking pdfjs-dist as external, it uses the installed package
from node_modules at runtime where the worker is available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 12:23:13 +00:00

117 lines
3.5 KiB
TypeScript

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,
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<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;
}