mcpknife 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +575 -15
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { createRequire as createRequire2 } from "node:module";
|
|
5
5
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import
|
|
7
|
+
import path4 from "node:path";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
@@ -94,13 +94,13 @@ var CONFIG_FLAG_MAP = [
|
|
|
94
94
|
{ configKey: "apiKey", flag: "--api-key", isBoolean: false },
|
|
95
95
|
{ configKey: "verbose", flag: "--verbose", isBoolean: true }
|
|
96
96
|
];
|
|
97
|
-
function hasFlag(
|
|
98
|
-
return
|
|
97
|
+
function hasFlag(argv, flag) {
|
|
98
|
+
return argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
99
99
|
}
|
|
100
|
-
function buildArgv(
|
|
100
|
+
function buildArgv(config, rawArgv2) {
|
|
101
101
|
const injected = [];
|
|
102
102
|
for (const { configKey, flag, isBoolean } of CONFIG_FLAG_MAP) {
|
|
103
|
-
const value =
|
|
103
|
+
const value = config[configKey];
|
|
104
104
|
if (value === void 0 || value === false) continue;
|
|
105
105
|
if (hasFlag(rawArgv2, flag)) continue;
|
|
106
106
|
if (isBoolean) {
|
|
@@ -114,8 +114,8 @@ function buildArgv(config2, rawArgv2) {
|
|
|
114
114
|
|
|
115
115
|
// src/spawn.ts
|
|
116
116
|
import { spawn } from "node:child_process";
|
|
117
|
-
function spawnTool(
|
|
118
|
-
const child = spawn(process.execPath, [
|
|
117
|
+
function spawnTool(binaryPath, argv) {
|
|
118
|
+
const child = spawn(process.execPath, [binaryPath, ...argv], {
|
|
119
119
|
stdio: "inherit",
|
|
120
120
|
env: process.env
|
|
121
121
|
});
|
|
@@ -130,7 +130,7 @@ function spawnTool(binaryPath2, argv2) {
|
|
|
130
130
|
forwardSignal("SIGTERM");
|
|
131
131
|
child.on("error", (err) => {
|
|
132
132
|
if (err.code === "ENOENT") {
|
|
133
|
-
console.error(`mcpknife: binary not found: ${
|
|
133
|
+
console.error(`mcpknife: binary not found: ${binaryPath}`);
|
|
134
134
|
console.error(`Try reinstalling: npm install -g mcpknife`);
|
|
135
135
|
} else {
|
|
136
136
|
console.error(`mcpknife: failed to start: ${err.message}`);
|
|
@@ -146,11 +146,561 @@ function spawnTool(binaryPath2, argv2) {
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// src/export.ts
|
|
150
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
151
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
152
|
+
|
|
153
|
+
// src/codegen.ts
|
|
154
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
155
|
+
import path3 from "node:path";
|
|
156
|
+
function ensureDir(dir) {
|
|
157
|
+
mkdirSync(dir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
function writeJSON(filePath, data) {
|
|
160
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
161
|
+
}
|
|
162
|
+
function writeText(filePath, content) {
|
|
163
|
+
writeFileSync(filePath, content);
|
|
164
|
+
}
|
|
165
|
+
function generatePackageJson(stages) {
|
|
166
|
+
const stageNames = stages.map((s) => s.stage).join("+");
|
|
167
|
+
return {
|
|
168
|
+
name: "exported-mcp-server",
|
|
169
|
+
version: "1.0.0",
|
|
170
|
+
description: `Standalone MCP server exported from mcpknife pipeline (${stageNames})`,
|
|
171
|
+
type: "module",
|
|
172
|
+
main: "server.js",
|
|
173
|
+
scripts: {
|
|
174
|
+
start: "node server.js"
|
|
175
|
+
},
|
|
176
|
+
dependencies: {
|
|
177
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function generateReadme(stages) {
|
|
182
|
+
const lines = [
|
|
183
|
+
"# Exported MCP Server",
|
|
184
|
+
"",
|
|
185
|
+
`Standalone server exported from a mcpknife pipeline with ${stages.length} stage(s).`,
|
|
186
|
+
"",
|
|
187
|
+
"## Quick Start",
|
|
188
|
+
"",
|
|
189
|
+
"```bash",
|
|
190
|
+
"npm install",
|
|
191
|
+
"node server.js",
|
|
192
|
+
"```",
|
|
193
|
+
"",
|
|
194
|
+
`The server listens on \`http://localhost:\${PORT}/mcp\` (default PORT=8000).`,
|
|
195
|
+
"",
|
|
196
|
+
"## Pipeline Stages",
|
|
197
|
+
""
|
|
198
|
+
];
|
|
199
|
+
for (const stage of stages) {
|
|
200
|
+
lines.push(`### ${stage.stage} (v${stage.version})`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
if (stage.stage === "boot") {
|
|
203
|
+
const boot = stage;
|
|
204
|
+
lines.push(`Tools: ${boot.tools.map((t) => t.name).join(", ")}`);
|
|
205
|
+
if (boot.whitelist_domains.length > 0) {
|
|
206
|
+
lines.push(`Network domains: ${boot.whitelist_domains.join(", ")}`);
|
|
207
|
+
}
|
|
208
|
+
} else if (stage.stage === "mod") {
|
|
209
|
+
const mod = stage;
|
|
210
|
+
if (mod.pass_through_tools.length > 0) {
|
|
211
|
+
lines.push(`Pass-through: ${mod.pass_through_tools.map((t) => t.exposed_name).join(", ")}`);
|
|
212
|
+
}
|
|
213
|
+
if (mod.synthetic_tools.length > 0) {
|
|
214
|
+
lines.push(`Synthetic: ${mod.synthetic_tools.map((t) => t.name).join(", ")}`);
|
|
215
|
+
}
|
|
216
|
+
if (mod.hidden_tools.length > 0) {
|
|
217
|
+
lines.push(`Hidden: ${mod.hidden_tools.join(", ")}`);
|
|
218
|
+
}
|
|
219
|
+
} else if (stage.stage === "ui") {
|
|
220
|
+
const ui = stage;
|
|
221
|
+
lines.push(`UI resources: ${ui.ui_resources.map((r) => r.tool_name).join(", ")}`);
|
|
222
|
+
}
|
|
223
|
+
lines.push("");
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
function getExposedTools(stages) {
|
|
228
|
+
const boot = stages.find((s) => s.stage === "boot");
|
|
229
|
+
const mod = stages.find((s) => s.stage === "mod");
|
|
230
|
+
if (mod) {
|
|
231
|
+
const tools = [];
|
|
232
|
+
for (const t of mod.pass_through_tools) {
|
|
233
|
+
tools.push({
|
|
234
|
+
name: t.exposed_name,
|
|
235
|
+
description: t.description || "",
|
|
236
|
+
inputSchema: t.exposed_schema
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
for (const t of mod.synthetic_tools) {
|
|
240
|
+
tools.push({
|
|
241
|
+
name: t.name,
|
|
242
|
+
description: t.description,
|
|
243
|
+
inputSchema: t.input_schema
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return tools;
|
|
247
|
+
}
|
|
248
|
+
if (boot) {
|
|
249
|
+
return boot.tools.map((t) => ({
|
|
250
|
+
name: t.name,
|
|
251
|
+
description: t.description,
|
|
252
|
+
inputSchema: t.input_schema
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
function generateServerJs(stages) {
|
|
258
|
+
const boot = stages.find((s) => s.stage === "boot");
|
|
259
|
+
const mod = stages.find((s) => s.stage === "mod");
|
|
260
|
+
const ui = stages.find((s) => s.stage === "ui");
|
|
261
|
+
const hasNetwork = boot?.tools.some((t) => t.needs_network) ?? false;
|
|
262
|
+
const whitelistDomains = boot?.whitelist_domains ?? [];
|
|
263
|
+
const exposedTools = getExposedTools(stages);
|
|
264
|
+
const importLines = [
|
|
265
|
+
'import { Server } from "@modelcontextprotocol/sdk/server/index.js";',
|
|
266
|
+
'import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";',
|
|
267
|
+
'import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";',
|
|
268
|
+
'import http from "node:http";',
|
|
269
|
+
'import vm from "node:vm";',
|
|
270
|
+
'import { readFileSync } from "node:fs";',
|
|
271
|
+
'import { fileURLToPath } from "node:url";',
|
|
272
|
+
'import path from "node:path";'
|
|
273
|
+
];
|
|
274
|
+
if (ui) {
|
|
275
|
+
importLines.push(
|
|
276
|
+
'import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";'
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const parts = [];
|
|
280
|
+
parts.push(importLines.join("\n"));
|
|
281
|
+
parts.push("");
|
|
282
|
+
parts.push("const __filename = fileURLToPath(import.meta.url);");
|
|
283
|
+
parts.push("const __dirname = path.dirname(__filename);");
|
|
284
|
+
parts.push("");
|
|
285
|
+
if (hasNetwork) {
|
|
286
|
+
parts.push(`const WHITELIST_DOMAINS = ${JSON.stringify(whitelistDomains)};`);
|
|
287
|
+
parts.push("");
|
|
288
|
+
parts.push(`function isAllowed(url) {
|
|
289
|
+
try {
|
|
290
|
+
const hostname = new URL(url).hostname;
|
|
291
|
+
return WHITELIST_DOMAINS.some(d => hostname === d || hostname.endsWith("." + d));
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const whitelistedFetch = (url, opts) => {
|
|
298
|
+
if (!isAllowed(url)) {
|
|
299
|
+
throw new Error("Network access denied: " + url);
|
|
300
|
+
}
|
|
301
|
+
return fetch(url, opts);
|
|
302
|
+
};`);
|
|
303
|
+
parts.push("");
|
|
304
|
+
}
|
|
305
|
+
parts.push(`const SANDBOX_TIMEOUT_MS = 30000;
|
|
306
|
+
|
|
307
|
+
function runHandler(code, args) {
|
|
308
|
+
const sandbox = {
|
|
309
|
+
args,
|
|
310
|
+
JSON, Math, String, Number, Boolean, Array, Object, Map, Set,
|
|
311
|
+
Date, RegExp, parseInt, parseFloat, isNaN, isFinite,
|
|
312
|
+
structuredClone, console: { log: console.log },
|
|
313
|
+
Promise,${hasNetwork ? "\n fetch: whitelistedFetch," : ""}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const context = vm.createContext(sandbox);
|
|
317
|
+
const wrappedCode = "(async function(args) { " + code + " })(args)";
|
|
318
|
+
const script = new vm.Script(wrappedCode);
|
|
319
|
+
return script.runInContext(context, { timeout: SANDBOX_TIMEOUT_MS });
|
|
320
|
+
}`);
|
|
321
|
+
parts.push("");
|
|
322
|
+
if (boot) {
|
|
323
|
+
parts.push(`// Boot tool handlers
|
|
324
|
+
const bootHandlers = new Map();
|
|
325
|
+
${boot.tools.map(
|
|
326
|
+
(t) => `bootHandlers.set(${JSON.stringify(t.name)}, readFileSync(path.join(__dirname, "handlers", ${JSON.stringify(t.name + ".js")}), "utf-8"));`
|
|
327
|
+
).join("\n")}`);
|
|
328
|
+
parts.push("");
|
|
329
|
+
parts.push(`async function callBootTool(name, args) {
|
|
330
|
+
const code = bootHandlers.get(name);
|
|
331
|
+
if (!code) throw new Error("Unknown boot tool: " + name);
|
|
332
|
+
return runHandler(code, args);
|
|
333
|
+
}`);
|
|
334
|
+
parts.push("");
|
|
335
|
+
}
|
|
336
|
+
if (mod) {
|
|
337
|
+
if (mod.pass_through_tools.some((t) => t.input_transform_code || t.output_transform_code)) {
|
|
338
|
+
parts.push(`// Mod transforms
|
|
339
|
+
const transforms = new Map();
|
|
340
|
+
${mod.pass_through_tools.filter((t) => t.input_transform_code || t.output_transform_code).map(
|
|
341
|
+
(t) => `transforms.set(${JSON.stringify(t.exposed_name)}, JSON.parse(readFileSync(path.join(__dirname, "transforms", ${JSON.stringify(t.exposed_name + ".json")}), "utf-8")));`
|
|
342
|
+
).join("\n")}`);
|
|
343
|
+
parts.push("");
|
|
344
|
+
}
|
|
345
|
+
if (mod.synthetic_tools.length > 0) {
|
|
346
|
+
parts.push(`// Synthetic tool orchestrations
|
|
347
|
+
const orchestrations = new Map();
|
|
348
|
+
${mod.synthetic_tools.map(
|
|
349
|
+
(t) => `orchestrations.set(${JSON.stringify(t.name)}, readFileSync(path.join(__dirname, "orchestrations", ${JSON.stringify(t.name + ".js")}), "utf-8"));`
|
|
350
|
+
).join("\n")}`);
|
|
351
|
+
parts.push("");
|
|
352
|
+
}
|
|
353
|
+
parts.push(`const toolRouting = new Map();`);
|
|
354
|
+
for (const t of mod.pass_through_tools) {
|
|
355
|
+
parts.push(`toolRouting.set(${JSON.stringify(t.exposed_name)}, ${JSON.stringify(t.upstream_name)});`);
|
|
356
|
+
}
|
|
357
|
+
parts.push("");
|
|
358
|
+
}
|
|
359
|
+
parts.push(`async function dispatchTool(name, args) {`);
|
|
360
|
+
if (mod) {
|
|
361
|
+
parts.push(` // Synthetic tools
|
|
362
|
+
if (orchestrations && orchestrations.has(name)) {
|
|
363
|
+
const code = orchestrations.get(name);
|
|
364
|
+
const callTool = async (n, a) => callBootTool(n, a);
|
|
365
|
+
const sandbox = {
|
|
366
|
+
args, callTool,
|
|
367
|
+
JSON, Math, String, Number, Boolean, Array, Object, Map, Set,
|
|
368
|
+
Date, RegExp, parseInt, parseFloat, isNaN, isFinite,
|
|
369
|
+
structuredClone, console: { log: console.log },
|
|
370
|
+
Promise,${hasNetwork ? "\n fetch: whitelistedFetch," : ""}
|
|
371
|
+
};
|
|
372
|
+
const context = vm.createContext(sandbox);
|
|
373
|
+
const wrappedCode = "(async function(args, callTool) { " + code + " })(args, callTool)";
|
|
374
|
+
const script = new vm.Script(wrappedCode);
|
|
375
|
+
return script.runInContext(context, { timeout: SANDBOX_TIMEOUT_MS });
|
|
376
|
+
}`);
|
|
377
|
+
parts.push("");
|
|
378
|
+
parts.push(` // Pass-through and modified tools
|
|
379
|
+
const upstreamName = toolRouting.get(name);
|
|
380
|
+
if (upstreamName !== undefined) {
|
|
381
|
+
let callArgs = args;
|
|
382
|
+
|
|
383
|
+
// Apply input transform if present
|
|
384
|
+
const transform = transforms ? transforms.get(name) : undefined;
|
|
385
|
+
if (transform && transform.input_transform_code) {
|
|
386
|
+
try {
|
|
387
|
+
const fn = new Function("args", transform.input_transform_code);
|
|
388
|
+
callArgs = fn(args);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error("Input transform error for " + name + ": " + err);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Call boot handler
|
|
395
|
+
let result = await callBootTool(upstreamName, callArgs);
|
|
396
|
+
|
|
397
|
+
// Apply output transform if present
|
|
398
|
+
if (transform && transform.output_transform_code) {
|
|
399
|
+
try {
|
|
400
|
+
const fn = new Function("result", transform.output_transform_code);
|
|
401
|
+
result = fn(result);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error("Output transform error for " + name + ": " + err);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
}`);
|
|
409
|
+
parts.push("");
|
|
410
|
+
}
|
|
411
|
+
if (boot && !mod) {
|
|
412
|
+
parts.push(` return callBootTool(name, args);`);
|
|
413
|
+
} else {
|
|
414
|
+
parts.push(` return { content: [{ type: "text", text: "Unknown tool: " + name }], isError: true };`);
|
|
415
|
+
}
|
|
416
|
+
parts.push(`}`);
|
|
417
|
+
parts.push("");
|
|
418
|
+
parts.push(`const TOOLS = ${JSON.stringify(exposedTools, null, 2)};`);
|
|
419
|
+
parts.push("");
|
|
420
|
+
if (ui && ui.ui_resources.length > 0) {
|
|
421
|
+
parts.push(`// UI resources
|
|
422
|
+
const uiResources = new Map();
|
|
423
|
+
${ui.ui_resources.map(
|
|
424
|
+
(r) => `uiResources.set(${JSON.stringify(r.resource_uri)}, {
|
|
425
|
+
toolName: ${JSON.stringify(r.tool_name)},
|
|
426
|
+
html: readFileSync(path.join(__dirname, "ui", ${JSON.stringify(r.tool_name + ".html")}), "utf-8"),
|
|
427
|
+
});`
|
|
428
|
+
).join("\n")}`);
|
|
429
|
+
parts.push("");
|
|
430
|
+
}
|
|
431
|
+
parts.push(`const PORT = parseInt(process.env.PORT || "8000", 10);
|
|
432
|
+
|
|
433
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
434
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
435
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
436
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
437
|
+
|
|
438
|
+
if (req.method === "OPTIONS") {
|
|
439
|
+
res.writeHead(204);
|
|
440
|
+
res.end();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (req.method === "POST" && (req.url === "/mcp" || req.url === "/")) {
|
|
445
|
+
const mcpServer = new Server(
|
|
446
|
+
{ name: "exported-mcp-server", version: "1.0.0" },
|
|
447
|
+
{ capabilities: { tools: {}${ui ? ", resources: {}" : ""} } },
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
451
|
+
tools: TOOLS.map(t => ({
|
|
452
|
+
name: t.name,
|
|
453
|
+
description: t.description,
|
|
454
|
+
inputSchema: t.inputSchema,
|
|
455
|
+
})),
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
459
|
+
const { name, arguments: args } = request.params;
|
|
460
|
+
return dispatchTool(name, args || {});
|
|
461
|
+
});`);
|
|
462
|
+
if (ui && ui.ui_resources.length > 0) {
|
|
463
|
+
parts.push(`
|
|
464
|
+
mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
465
|
+
resources: Array.from(uiResources.entries()).map(([uri, r]) => ({
|
|
466
|
+
uri,
|
|
467
|
+
name: r.toolName + " UI",
|
|
468
|
+
description: "Generated interactive UI for " + r.toolName,
|
|
469
|
+
mimeType: "text/html",
|
|
470
|
+
})),
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
474
|
+
const uri = request.params.uri;
|
|
475
|
+
const resource = uiResources.get(uri);
|
|
476
|
+
if (!resource) throw new Error("Unknown resource: " + uri);
|
|
477
|
+
return {
|
|
478
|
+
contents: [{ uri, mimeType: "text/html", text: resource.html }],
|
|
479
|
+
};
|
|
480
|
+
});`);
|
|
481
|
+
}
|
|
482
|
+
parts.push(`
|
|
483
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
484
|
+
try {
|
|
485
|
+
const chunks = [];
|
|
486
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
487
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
488
|
+
await mcpServer.connect(transport);
|
|
489
|
+
await transport.handleRequest(req, res, body);
|
|
490
|
+
res.on("close", () => {
|
|
491
|
+
transport.close();
|
|
492
|
+
mcpServer.close();
|
|
493
|
+
});
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (!res.headersSent) {
|
|
496
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
497
|
+
res.end(JSON.stringify({
|
|
498
|
+
jsonrpc: "2.0",
|
|
499
|
+
error: { code: -32603, message: String(error) },
|
|
500
|
+
id: null,
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} else if (req.method === "GET" && req.url === "/health") {
|
|
505
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
506
|
+
res.end(JSON.stringify({ status: "ok", tools: TOOLS.length }));
|
|
507
|
+
} else {
|
|
508
|
+
res.writeHead(404);
|
|
509
|
+
res.end("Not found");
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
httpServer.listen(PORT, () => {
|
|
514
|
+
console.log("Exported MCP server listening on http://localhost:" + PORT + "/mcp");
|
|
515
|
+
console.log("Serving " + TOOLS.length + " tool(s)");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
process.on("SIGINT", () => { httpServer.close(); process.exit(0); });
|
|
519
|
+
process.on("SIGTERM", () => { httpServer.close(); process.exit(0); });`);
|
|
520
|
+
return parts.join("\n");
|
|
521
|
+
}
|
|
522
|
+
async function generateProject(stages, outputDir) {
|
|
523
|
+
const absDir = path3.resolve(outputDir);
|
|
524
|
+
ensureDir(absDir);
|
|
525
|
+
const boot = stages.find((s) => s.stage === "boot");
|
|
526
|
+
const mod = stages.find((s) => s.stage === "mod");
|
|
527
|
+
const ui = stages.find((s) => s.stage === "ui");
|
|
528
|
+
writeJSON(path3.join(absDir, "package.json"), generatePackageJson(stages));
|
|
529
|
+
if (boot) {
|
|
530
|
+
const handlersDir = path3.join(absDir, "handlers");
|
|
531
|
+
ensureDir(handlersDir);
|
|
532
|
+
for (const tool of boot.tools) {
|
|
533
|
+
writeText(path3.join(handlersDir, `${tool.name}.js`), tool.handler_code);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (mod) {
|
|
537
|
+
const modifiedTools = mod.pass_through_tools.filter(
|
|
538
|
+
(t) => t.input_transform_code || t.output_transform_code
|
|
539
|
+
);
|
|
540
|
+
if (modifiedTools.length > 0) {
|
|
541
|
+
const transformsDir = path3.join(absDir, "transforms");
|
|
542
|
+
ensureDir(transformsDir);
|
|
543
|
+
for (const tool of modifiedTools) {
|
|
544
|
+
writeJSON(path3.join(transformsDir, `${tool.exposed_name}.json`), {
|
|
545
|
+
input_transform_code: tool.input_transform_code,
|
|
546
|
+
output_transform_code: tool.output_transform_code
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (mod.synthetic_tools.length > 0) {
|
|
551
|
+
const orchDir = path3.join(absDir, "orchestrations");
|
|
552
|
+
ensureDir(orchDir);
|
|
553
|
+
for (const tool of mod.synthetic_tools) {
|
|
554
|
+
writeText(path3.join(orchDir, `${tool.name}.js`), tool.orchestration_code);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (ui && ui.ui_resources.length > 0) {
|
|
559
|
+
const uiDir = path3.join(absDir, "ui");
|
|
560
|
+
ensureDir(uiDir);
|
|
561
|
+
for (const resource of ui.ui_resources) {
|
|
562
|
+
writeText(path3.join(uiDir, `${resource.tool_name}.html`), resource.html);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
writeText(path3.join(absDir, "server.js"), generateServerJs(stages));
|
|
566
|
+
writeText(path3.join(absDir, "README.md"), generateReadme(stages));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/export.ts
|
|
570
|
+
function parseExportArgs(argv) {
|
|
571
|
+
let outputDir = "./exported_mcp";
|
|
572
|
+
let help = false;
|
|
573
|
+
for (let i = 0; i < argv.length; i++) {
|
|
574
|
+
const arg = argv[i];
|
|
575
|
+
if (arg === "--help" || arg === "-h") {
|
|
576
|
+
help = true;
|
|
577
|
+
} else if (arg === "--output-dir" && i + 1 < argv.length) {
|
|
578
|
+
outputDir = argv[++i];
|
|
579
|
+
} else if (arg.startsWith("--output-dir=")) {
|
|
580
|
+
outputDir = arg.slice("--output-dir=".length);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { outputDir, help };
|
|
584
|
+
}
|
|
585
|
+
function printExportHelp() {
|
|
586
|
+
console.log(`mcpknife export \u2014 dump a self-contained MCP server project to disk
|
|
587
|
+
|
|
588
|
+
Usage:
|
|
589
|
+
mcpknife export [--output-dir <dir>]
|
|
590
|
+
|
|
591
|
+
Options:
|
|
592
|
+
--output-dir <dir> Output directory (default: ./exported_mcp)
|
|
593
|
+
--help Show this help message
|
|
594
|
+
|
|
595
|
+
The export command reads an upstream MCP server URL from stdin (pipe protocol)
|
|
596
|
+
and recursively walks the _mcp_metadata chain to collect all implementation code,
|
|
597
|
+
then combines it into a standalone Node.js project.
|
|
598
|
+
|
|
599
|
+
Examples:
|
|
600
|
+
mcpknife boot --prompt "Dictionary API" | mcpknife export
|
|
601
|
+
mcpknife boot ... | mcpknife mod ... | mcpknife export --output-dir ./my-server
|
|
602
|
+
mcpknife boot ... | mcpknife mod ... | mcpknife ui | mcpknife export`);
|
|
603
|
+
}
|
|
604
|
+
function readUrlFromStdin(timeoutMs = 1e4) {
|
|
605
|
+
return new Promise((resolve, reject) => {
|
|
606
|
+
if (process.stdin.isTTY) {
|
|
607
|
+
reject(new Error(
|
|
608
|
+
"export reads upstream URL from stdin pipe.\nUsage: mcpknife boot ... | mcpknife export\n or: mcpknife boot ... | mcpknife mod ... | mcpknife export"
|
|
609
|
+
));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
let data = "";
|
|
613
|
+
const timer = setTimeout(() => {
|
|
614
|
+
process.stdin.removeAllListeners();
|
|
615
|
+
process.stdin.destroy();
|
|
616
|
+
reject(new Error("Timed out waiting for upstream URL on stdin"));
|
|
617
|
+
}, timeoutMs);
|
|
618
|
+
process.stdin.setEncoding("utf-8");
|
|
619
|
+
process.stdin.on("data", (chunk) => {
|
|
620
|
+
data += chunk;
|
|
621
|
+
const lines = data.split("\n");
|
|
622
|
+
for (const line of lines) {
|
|
623
|
+
const trimmed = line.trim();
|
|
624
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
process.stdin.removeAllListeners();
|
|
627
|
+
process.stdin.destroy();
|
|
628
|
+
resolve(trimmed);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
process.stdin.on("end", () => {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
const trimmed = data.trim();
|
|
636
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
637
|
+
resolve(trimmed);
|
|
638
|
+
} else {
|
|
639
|
+
reject(new Error(`Invalid upstream URL on stdin: "${trimmed.slice(0, 100)}"`));
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
process.stdin.on("error", (err) => {
|
|
643
|
+
clearTimeout(timer);
|
|
644
|
+
reject(err);
|
|
645
|
+
});
|
|
646
|
+
process.stdin.resume();
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
async function fetchMetadata(url) {
|
|
650
|
+
const client = new Client(
|
|
651
|
+
{ name: "mcpknife-export", version: "0.1.0" },
|
|
652
|
+
{ capabilities: {} }
|
|
653
|
+
);
|
|
654
|
+
const transport = new StreamableHTTPClientTransport(new URL(url));
|
|
655
|
+
await client.connect(transport);
|
|
656
|
+
try {
|
|
657
|
+
const result = await client.callTool({
|
|
658
|
+
name: "_mcp_metadata",
|
|
659
|
+
arguments: {}
|
|
660
|
+
});
|
|
661
|
+
const content = result.content;
|
|
662
|
+
if (!content || content.length === 0 || !content[0].text) {
|
|
663
|
+
throw new Error(`Empty _mcp_metadata response from ${url}`);
|
|
664
|
+
}
|
|
665
|
+
return JSON.parse(content[0].text);
|
|
666
|
+
} finally {
|
|
667
|
+
await client.close();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async function crawlPipeline(url) {
|
|
671
|
+
const stages = [];
|
|
672
|
+
let currentUrl = url;
|
|
673
|
+
while (currentUrl) {
|
|
674
|
+
const metadata = await fetchMetadata(currentUrl);
|
|
675
|
+
stages.push(metadata);
|
|
676
|
+
currentUrl = metadata.upstream_url ?? null;
|
|
677
|
+
}
|
|
678
|
+
stages.reverse();
|
|
679
|
+
return stages;
|
|
680
|
+
}
|
|
681
|
+
async function runExport(argv) {
|
|
682
|
+
const { outputDir, help } = parseExportArgs(argv);
|
|
683
|
+
if (help) {
|
|
684
|
+
printExportHelp();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
console.error("[export] Reading upstream URL from stdin...");
|
|
688
|
+
const url = await readUrlFromStdin();
|
|
689
|
+
console.error(`[export] Upstream URL: ${url}`);
|
|
690
|
+
console.error("[export] Crawling pipeline metadata...");
|
|
691
|
+
const stages = await crawlPipeline(url);
|
|
692
|
+
console.error(`[export] Found ${stages.length} stage(s): ${stages.map((s) => s.stage).join(" \u2192 ")}`);
|
|
693
|
+
console.error(`[export] Generating project in ${outputDir}...`);
|
|
694
|
+
await generateProject(stages, outputDir);
|
|
695
|
+
console.error(`[export] Done! Project written to ${outputDir}`);
|
|
696
|
+
console.error(`[export] To run: cd ${outputDir} && npm install && node server.js`);
|
|
697
|
+
}
|
|
698
|
+
|
|
149
699
|
// src/cli.ts
|
|
150
700
|
var __filename = fileURLToPath(import.meta.url);
|
|
151
|
-
var __dirname =
|
|
701
|
+
var __dirname = path4.dirname(__filename);
|
|
152
702
|
var require3 = createRequire2(import.meta.url);
|
|
153
|
-
var pkgPath =
|
|
703
|
+
var pkgPath = path4.resolve(__dirname, "..", "package.json");
|
|
154
704
|
var version = "0.1.0";
|
|
155
705
|
try {
|
|
156
706
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
@@ -167,6 +717,7 @@ Commands:
|
|
|
167
717
|
boot Generate an MCP server from a prompt and API docs
|
|
168
718
|
mod Transform tools on an existing MCP server
|
|
169
719
|
ui Add interactive UI to an MCP server
|
|
720
|
+
export Dump a self-contained MCP server project to disk
|
|
170
721
|
|
|
171
722
|
Options:
|
|
172
723
|
--help Show this help message
|
|
@@ -185,6 +736,9 @@ Examples:
|
|
|
185
736
|
# Full pipeline
|
|
186
737
|
mcpknife boot --prompt "Yahoo Finance" | mcpknife mod --prompt "combine tools" | mcpknife ui
|
|
187
738
|
|
|
739
|
+
# Export standalone server
|
|
740
|
+
mcpknife boot --prompt "Dictionary API" | mcpknife mod --prompt "synonyms" | mcpknife export
|
|
741
|
+
|
|
188
742
|
Run 'mcpknife <command> --help' for command-specific options.`);
|
|
189
743
|
}
|
|
190
744
|
var args = process.argv.slice(2);
|
|
@@ -201,12 +755,18 @@ if (args[0] === "--version" || args[0] === "-V") {
|
|
|
201
755
|
}
|
|
202
756
|
var subcommand = args[0];
|
|
203
757
|
var rawArgv = args.slice(1);
|
|
204
|
-
if (
|
|
758
|
+
if (subcommand === "export") {
|
|
759
|
+
runExport(rawArgv).then(() => process.exit(0)).catch((err) => {
|
|
760
|
+
console.error(`mcpknife export: ${err.message}`);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
});
|
|
763
|
+
} else if (!BINARY_MAP[subcommand]) {
|
|
205
764
|
console.error(`mcpknife: unknown subcommand '${subcommand}'`);
|
|
206
765
|
console.error(`Run 'mcpknife --help' for usage`);
|
|
207
766
|
process.exit(1);
|
|
767
|
+
} else {
|
|
768
|
+
const config = loadConfig();
|
|
769
|
+
const binaryPath = resolveBinary(subcommand);
|
|
770
|
+
const argv = buildArgv(config, rawArgv);
|
|
771
|
+
spawnTool(binaryPath, argv);
|
|
208
772
|
}
|
|
209
|
-
var config = loadConfig();
|
|
210
|
-
var binaryPath = resolveBinary(subcommand);
|
|
211
|
-
var argv = buildArgv(config, rawArgv);
|
|
212
|
-
spawnTool(binaryPath, argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpknife",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Swiss Army knife for MCP servers — generate, transform, and add UIs with Unix pipes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"author": "Vivek Haldar",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
32
|
+
"mcp-gen-ui": "^0.1.11",
|
|
33
|
+
"mcpblox": "^0.1.3",
|
|
34
|
+
"mcpboot": "^0.1.3"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"esbuild": "^0.25.0",
|