Add Node MCP server
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/build/
|
||||
/.idea/
|
||||
/.vscode/
|
||||
/node_modules/
|
||||
*.tmp
|
||||
*.swp
|
||||
keys/
|
||||
|
||||
38
README.md
38
README.md
@@ -24,6 +24,44 @@ with.
|
||||
- `m2pack extract`
|
||||
- `m2pack export-client-config`
|
||||
|
||||
## MCP Server
|
||||
|
||||
The repository also ships a Linux-friendly MCP server that wraps the `m2pack`
|
||||
CLI for AI agents and automation.
|
||||
|
||||
Files:
|
||||
|
||||
- `mcp_server.mjs`
|
||||
- `package.json`
|
||||
|
||||
Install:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node mcp_server.mjs
|
||||
```
|
||||
|
||||
If the `m2pack` binary is not at `build/m2pack`, set:
|
||||
|
||||
```bash
|
||||
export M2PACK_BINARY=/absolute/path/to/m2pack
|
||||
```
|
||||
|
||||
Exposed tools:
|
||||
|
||||
- `pack_keygen`
|
||||
- `pack_build`
|
||||
- `pack_list`
|
||||
- `pack_verify`
|
||||
- `pack_extract`
|
||||
- `pack_export_client_config`
|
||||
- `pack_binary_info`
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
|
||||
95
docs/mcp.md
Normal file
95
docs/mcp.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# MCP server
|
||||
|
||||
`mcp_server.mjs` exposes the `m2pack` CLI as MCP tools over stdio.
|
||||
|
||||
This keeps the packing logic in the compiled CLI and uses MCP only as an
|
||||
automation layer for bots and local tooling.
|
||||
|
||||
## Why this layout
|
||||
|
||||
- the C++ binary remains the single source of truth
|
||||
- no duplicate archive logic in Python
|
||||
- Linux-native development workflow
|
||||
- works well with Codex, Claude Desktop, Inspector, and other MCP hosts
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd /path/to/m2pack-secure
|
||||
npm install
|
||||
cmake -S . -B build
|
||||
cmake --build build -j
|
||||
node mcp_server.mjs
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
- `M2PACK_BINARY`
|
||||
Use this when `m2pack` is not located at `build/m2pack`.
|
||||
|
||||
## Tool contract
|
||||
|
||||
### `pack_keygen`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `out_dir`
|
||||
|
||||
### `pack_build`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `input_dir`
|
||||
- `output_archive`
|
||||
- `key_file`
|
||||
- `signing_secret_key_file`
|
||||
|
||||
### `pack_list`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `archive`
|
||||
|
||||
### `pack_verify`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `archive`
|
||||
- `public_key_file` optional
|
||||
- `key_file` optional
|
||||
|
||||
### `pack_extract`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `archive`
|
||||
- `output_dir`
|
||||
- `key_file`
|
||||
|
||||
### `pack_export_client_config`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `key_file`
|
||||
- `public_key_file`
|
||||
- `output_header`
|
||||
|
||||
### `pack_binary_info`
|
||||
|
||||
No input. Returns the active `m2pack` binary path.
|
||||
|
||||
## Claude Desktop style config example
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"m2pack-secure": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/m2pack-secure/mcp_server.mjs"],
|
||||
"env": {
|
||||
"M2PACK_BINARY": "/absolute/path/to/m2pack-secure/build/m2pack"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
192
mcp_server.mjs
Normal file
192
mcp_server.mjs
Normal file
@@ -0,0 +1,192 @@
|
||||
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(),
|
||||
},
|
||||
async ({ input_dir, output_archive, key_file, signing_secret_key_file }) =>
|
||||
toolResult(
|
||||
runCli([
|
||||
"build",
|
||||
"--input",
|
||||
input_dir,
|
||||
"--output",
|
||||
output_archive,
|
||||
"--key",
|
||||
key_file,
|
||||
"--sign-secret-key",
|
||||
signing_secret_key_file,
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
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(),
|
||||
},
|
||||
async ({ key_file, public_key_file, output_header }) =>
|
||||
toolResult(
|
||||
runCli([
|
||||
"export-client-config",
|
||||
"--key",
|
||||
key_file,
|
||||
"--public-key",
|
||||
public_key_file,
|
||||
"--output",
|
||||
output_header,
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
1048
package-lock.json
generated
Normal file
1048
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
package.json
Normal file
10
package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "m2pack-secure-mcp",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user