Skip to main content
Glama

FM8 MCP Server

server.ts6.9 kB
import fs from "fs"; import path from "path"; import crypto from "crypto"; import express from "express"; import morgan from "morgan"; import cors from "cors"; import * as easymidi from "easymidi"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; const args = Object.fromEntries( process.argv.slice(2).map((a) => { const [k, v] = a.replace(/^--/, "").split("="); return [k, v ?? "true"]; }) ); const MIDI_PORT = (args["midi-port-name"] as string) || "fm8"; const MIDI_CHANNEL = Math.max(1, Math.min(16, parseInt((args["channel"] as string) || "1", 10))); const TRANSPORT = (args["transport"] as string) || "stdio"; const PORT = parseInt((args["port"] as string) || "3333", 10); // Try to find mappings.json - first in cwd, then relative to this script let mappingPath = path.resolve(process.cwd(), "mappings.json"); if (!fs.existsSync(mappingPath)) { // If running from dist/server.js, go up to project root mappingPath = path.resolve(path.dirname(process.argv[1]), "..", "mappings.json"); } if (!fs.existsSync(mappingPath)) { throw new Error(`Could not find mappings.json. Tried: ${process.cwd()}/mappings.json and ${path.dirname(process.argv[1])}/../mappings.json`); } const mapping = JSON.parse(fs.readFileSync(mappingPath, "utf-8")) as { instrument: string; section: string; cc_map: Array<{ cc: number; label: string; source?: string; dest?: string; type?: string; }>; }; const byCC = new Map<number, (typeof mapping)["cc_map"][number]>(); const byRoute = new Map<string, (typeof mapping)["cc_map"][number]>(); for (const row of mapping.cc_map) { byCC.set(row.cc, row); if (row.source && row.dest) { byRoute.set(`${row.source}->${row.dest}`.toUpperCase(), row); } } let midi: easymidi.Output | null = null; let midiError: string | null = null; function openMidi() { if (midi) return midi; if (midiError) throw new Error(midiError); try { midi = new easymidi.Output(MIDI_PORT); return midi; } catch (error) { midiError = `Failed to open MIDI port "${MIDI_PORT}". Make sure loopMIDI is running and the port exists. Error: ${error}`; throw new Error(midiError); } } function sendCC(cc: number, value: number, channel = MIDI_CHANNEL) { const out = openMidi(); const clamped = Math.max(0, Math.min(127, Math.round(value))); const midiChannel = (channel - 1) as easymidi.Channel; // Log what we're about to send console.error(`[MIDI SEND] CC=${cc} Value=${clamped} Channel=${midiChannel} (arg was ${channel})`); (out as any).send("cc", { controller: cc, value: clamped, channel: midiChannel }); } function panic(channel = MIDI_CHANNEL) { const out = openMidi(); const ch = (channel - 1) as easymidi.Channel; (out as any).send("cc", { controller: 121, value: 0, channel: ch }); (out as any).send("cc", { controller: 120, value: 0, channel: ch }); (out as any).send("cc", { controller: 123, value: 0, channel: ch }); } const server = new McpServer({ name: "fm8-mcp-server", version: "0.1.1", description: "MCP server that maps ChatGPT tool calls to FM8 matrix CCs via loopMIDI" }); server.registerTool( "send_by_cc", { title: "Send raw CC", description: "Send a raw MIDI CC (0-127) on the configured channel.", inputSchema: { cc: z.number().int().min(0).max(127), value: z.number().int().min(0).max(127) } }, async ({ cc, value }) => { const row = byCC.get(cc); sendCC(cc, value); const output = { ok: true, cc, value, mappedLabel: row?.label ?? null }; return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output }; } ); server.registerTool( "send_by_route", { title: "Send by route", description: "Send a FM8 matrix route like A->B with a value (0-127).", inputSchema: { source: z.string(), dest: z.string(), value: z.number().int().min(0).max(127) } }, async ({ source, dest, value }) => { console.error(`[TOOL CALL] send_by_route: source=${source}, dest=${dest}, value=${value}`); const key = `${source}->${dest}`.toUpperCase(); const row = byRoute.get(key); if (!row) throw new Error(`No mapping for route ${source}->${dest}`); sendCC(row.cc, value); const output = { ok: true, cc: row.cc, label: row.label }; return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output }; } ); server.registerTool( "list_mappings", { title: "List mappings", description: "List all available FM8 mappings (route, label, cc).", inputSchema: {} }, async () => { const output = mapping.cc_map.map((r) => ({ cc: r.cc, label: r.label, source: r.source, dest: r.dest, type: r.type })); return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: { mappings: output } }; } ); server.registerTool( "panic", { title: "Panic", description: "Sends Reset All Controllers, All Sound Off, and All Notes Off.", inputSchema: { channel: z.number().int().min(1).max(16).optional() } }, async ({ channel }) => { panic(channel ?? MIDI_CHANNEL); const output = { ok: true }; return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output }; } ); server.registerTool( "status", { title: "Status", description: "Report server status.", inputSchema: {} }, async () => { const output = { transport: TRANSPORT, midiPort: MIDI_PORT, channel: MIDI_CHANNEL, instrument: mapping.instrument, section: mapping.section, routes: mapping.cc_map.length }; return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output }; } ); async function start() { if (TRANSPORT === "stdio") { const transport = new StdioServerTransport(); await server.connect(transport); process.stdin.resume(); } else if (TRANSPORT === "http") { const app = express(); app.use(cors()); app.use(morgan("dev")); app.use(express.json()); app.post("/mcp", async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() }); res.on("close", () => transport.close()); await server.connect(transport); await transport.handleRequest(req as any, res as any, req.body); }); app.get("/health", (_req, res) => res.json({ ok: true })); app.listen(PORT, () => console.log(`MCP HTTP on http://localhost:${PORT}/mcp`)); } else { throw new Error(`Unknown transport: ${TRANSPORT}`); } } start().catch((e) => { console.error(e); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/thesigma1receptor/fm8MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server