import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const defaultBinary = path.join(__dirname, "build", "m2pack"); function resolveBinary() { const envBinary = process.env.M2PACK_BINARY; if (envBinary && existsSync(envBinary)) { return envBinary; } if (existsSync(defaultBinary)) { return defaultBinary; } return "m2pack"; } function runCli(args) { const binary = resolveBinary(); const proc = spawnSync(binary, [...args, "--json"], { cwd: __dirname, encoding: "utf-8", }); if (proc.error) { throw new Error(`m2pack failed to start: ${proc.error.message}`); } if (proc.status !== 0) { const detail = (proc.stderr || proc.stdout || `exit code ${proc.status}`).trim(); throw new Error(`m2pack failed: ${detail}`); } const stdout = proc.stdout.trim(); if (!stdout) { return { ok: true }; } try { return JSON.parse(stdout); } catch (error) { throw new Error(`m2pack returned non-JSON output: ${stdout}`); } } function toolResult(payload) { return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], structuredContent: payload, }; } const server = new McpServer({ name: "m2pack-secure", version: "0.1.0", }); server.tool( "pack_keygen", "Generate a new master content key and Ed25519 signing keypair.", { out_dir: z.string(), }, async ({ out_dir }) => toolResult(runCli(["keygen", "--out-dir", out_dir])), ); server.tool( "pack_build", "Build an .m2p archive from a source directory.", { input_dir: z.string(), output_archive: z.string(), key_file: z.string(), signing_secret_key_file: z.string(), key_id: z.number().int().positive().optional(), }, async ({ input_dir, output_archive, key_file, signing_secret_key_file, key_id }) => toolResult( runCli([ "build", "--input", input_dir, "--output", output_archive, "--key", key_file, "--sign-secret-key", signing_secret_key_file, "--key-id", String(key_id ?? 1), ]), ), ); server.tool( "pack_diff", "Diff two directories and/or .m2p archives using normalized paths and plaintext hashes.", { left: z.string(), right: z.string(), }, async ({ left, right }) => toolResult(runCli(["diff", "--left", left, "--right", right])), ); server.tool( "pack_list", "List entries in an .m2p archive.", { archive: z.string(), }, async ({ archive }) => toolResult(runCli(["list", "--archive", archive])), ); server.tool( "pack_verify", "Verify archive manifest integrity and optionally decrypt all entries.", { archive: z.string(), public_key_file: z.string().optional(), key_file: z.string().optional(), }, async ({ archive, public_key_file, key_file }) => { const args = ["verify", "--archive", archive]; if (public_key_file) { args.push("--public-key", public_key_file); } if (key_file) { args.push("--key", key_file); } return toolResult(runCli(args)); }, ); server.tool( "pack_extract", "Extract an .m2p archive into a directory.", { archive: z.string(), output_dir: z.string(), key_file: z.string(), }, async ({ archive, output_dir, key_file }) => toolResult( runCli([ "extract", "--archive", archive, "--output", output_dir, "--key", key_file, ]), ), ); server.tool( "pack_export_client_config", "Generate M2PackKeys.h for the Windows client tree.", { key_file: z.string(), public_key_file: z.string(), output_header: z.string(), key_id: z.number().int().positive().optional(), }, async ({ key_file, public_key_file, output_header, key_id }) => toolResult( runCli([ "export-client-config", "--key", key_file, "--public-key", public_key_file, "--key-id", String(key_id ?? 1), "--output", output_header, ]), ), ); server.tool( "pack_export_runtime_key", "Generate a launcher runtime key payload in json or blob form.", { key_file: z.string(), public_key_file: z.string(), output_file: z.string(), key_id: z.number().int().positive().optional(), format: z.enum(["json", "blob"]).optional(), }, async ({ key_file, public_key_file, output_file, key_id, format }) => toolResult( runCli([ "export-runtime-key", "--key", key_file, "--public-key", public_key_file, "--key-id", String(key_id ?? 1), "--format", format ?? "json", "--output", output_file, ]), ), ); server.tool( "pack_binary_info", "Report which m2pack binary the server will execute.", {}, async () => toolResult({ ok: true, binary: resolveBinary(), repo_root: __dirname, }), ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((error) => { console.error(error); process.exit(1); });