Files
m2pack-secure/mcp_server.mjs
2026-04-14 12:26:22 +02:00

237 lines
5.4 KiB
JavaScript

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