ext-apps/examples/sheet-music-server/server.ts
Jerome c25d464d18
feat: publish example servers to npm (#184)
* feat: publish example servers to npm

Make example MCP App servers publishable to npm under the
@modelcontextprotocol scope:

- server-basic-react
- server-basic-vanillajs
- server-budget-allocator
- server-cohort-heatmap
- server-customer-segmentation
- server-scenario-modeler
- server-system-monitor
- server-threejs
- server-wiki-explorer

Changes:
- Remove `private: true` from example package.json files
- Add proper npm metadata (description, repository, license, files)
- Update ext-apps dependency from relative to ^0.2.2
- Copy server-utils.ts into each package for standalone use
- Add publish-examples job to npm-publish.yml workflow

The examples will be published automatically when a GitHub release
is created, after the main SDK package is published.

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

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

* feat: publish examples with pkg-pr-new for PR testing

* feat: export createServer from example servers

* style: format integration-server/server.ts

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

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

* fix: update e2e snapshots for basic-server examples

Update golden screenshots for basic-react and basic-vanillajs after
the tool output was simplified from JSON to plain text in #182.

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

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

* merge

* refactor: update sheet-music-server and video-resource-server to new publishing pattern

- Move server-utils.ts from src/ to root level
- Add npm publishing metadata (scoped name, repository, license, files)
- Change ext-apps dependency from local to versioned ^0.2.2
- Update server.ts to handle stdio mode inline

* update package-lock

* fix: update package-lock.json for renamed server packages

* feat: add NASA public domain video to video-resource-server

Added 'nasa-earth' option - NASA ISS Earth observations (6.4MB).
Source: archive.org/details/NASA-Ultra-High-Definition (public domain)

* fix: add default videoId for video-resource-server

* fix: use existing bunny-1mb as default video, remove nasa-earth

* refactor: update new framework examples to publishing pattern

Add publishing metadata for:
- @modelcontextprotocol/server-basic-preact
- @modelcontextprotocol/server-basic-solid
- @modelcontextprotocol/server-basic-svelte
- @modelcontextprotocol/server-basic-vue

Changes:
- Move server-utils.ts from src/ to root
- Add npm publishing metadata
- Update ext-apps dep to versioned ^0.2.2
- Add stdio mode support in main()

* fix: add missing @rollup/rollup-win32-arm64-msvc to optionalDependencies

This fixes the Windows ARM64 CI build failure caused by npm's optional
dependency bug. The package.json was missing this platform-specific
rollup binding.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
2026-01-08 20:08:50 +00:00

132 lines
3.9 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,
RESOURCE_URI_META_KEY,
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { startServer } from "./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;
}
async function main() {
if (process.argv.includes("--stdio")) {
await createServer().connect(new StdioServerTransport());
} else {
const port = parseInt(process.env.PORT ?? "3001", 10);
await startServer(createServer, { port, name: "Sheet Music Server" });
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});