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.
Files changed (2) hide show
  1. package/dist/cli.js +575 -15
  2. 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 path3 from "node:path";
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(argv2, flag) {
98
- return argv2.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
97
+ function hasFlag(argv, flag) {
98
+ return argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
99
99
  }
100
- function buildArgv(config2, rawArgv2) {
100
+ function buildArgv(config, rawArgv2) {
101
101
  const injected = [];
102
102
  for (const { configKey, flag, isBoolean } of CONFIG_FLAG_MAP) {
103
- const value = config2[configKey];
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(binaryPath2, argv2) {
118
- const child = spawn(process.execPath, [binaryPath2, ...argv2], {
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: ${binaryPath2}`);
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 = path3.dirname(__filename);
701
+ var __dirname = path4.dirname(__filename);
152
702
  var require3 = createRequire2(import.meta.url);
153
- var pkgPath = path3.resolve(__dirname, "..", "package.json");
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 (!BINARY_MAP[subcommand]) {
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.5",
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
- "mcp-gen-ui": "^0.1.9",
32
- "mcpblox": "^0.1.1",
33
- "mcpboot": "^0.1.0"
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",