frontmcp 1.1.0 → 1.1.1

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 (49) hide show
  1. package/package.json +5 -5
  2. package/src/commands/build/adapters/cloudflare.js +29 -1
  3. package/src/commands/build/adapters/cloudflare.js.map +1 -1
  4. package/src/commands/build/adapters/distributed.js +4 -1
  5. package/src/commands/build/adapters/distributed.js.map +1 -1
  6. package/src/commands/build/exec/cli-runtime/extract-public-message.snippet.d.ts +18 -0
  7. package/src/commands/build/exec/cli-runtime/extract-public-message.snippet.js +58 -0
  8. package/src/commands/build/exec/cli-runtime/extract-public-message.snippet.js.map +1 -0
  9. package/src/commands/build/exec/cli-runtime/generate-cli-entry.d.ts +2 -2
  10. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +165 -47
  11. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
  12. package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +7 -0
  13. package/src/commands/build/exec/cli-runtime/schema-extractor.js +17 -1
  14. package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
  15. package/src/commands/build/exec/index.d.ts +11 -0
  16. package/src/commands/build/exec/index.js +68 -20
  17. package/src/commands/build/exec/index.js.map +1 -1
  18. package/src/commands/build/exec/installer-script.d.ts +8 -2
  19. package/src/commands/build/exec/installer-script.js +34 -15
  20. package/src/commands/build/exec/installer-script.js.map +1 -1
  21. package/src/commands/build/exec/manifest.d.ts +16 -3
  22. package/src/commands/build/exec/manifest.js +17 -5
  23. package/src/commands/build/exec/manifest.js.map +1 -1
  24. package/src/commands/build/exec/runner-script.d.ts +9 -1
  25. package/src/commands/build/exec/runner-script.js +60 -2
  26. package/src/commands/build/exec/runner-script.js.map +1 -1
  27. package/src/commands/build/index.js +80 -18
  28. package/src/commands/build/index.js.map +1 -1
  29. package/src/commands/build/load-entry-config.d.ts +5 -0
  30. package/src/commands/build/load-entry-config.js +96 -0
  31. package/src/commands/build/load-entry-config.js.map +1 -0
  32. package/src/commands/build/mcpb/manifest.d.ts +14 -0
  33. package/src/commands/build/mcpb/manifest.js +29 -0
  34. package/src/commands/build/mcpb/manifest.js.map +1 -1
  35. package/src/commands/build/types.d.ts +19 -0
  36. package/src/commands/build/types.js.map +1 -1
  37. package/src/commands/dev/doctor.js +7 -3
  38. package/src/commands/dev/doctor.js.map +1 -1
  39. package/src/commands/package/install.d.ts +1 -1
  40. package/src/commands/package/install.js +10 -8
  41. package/src/commands/package/install.js.map +1 -1
  42. package/src/commands/package/types.d.ts +2 -1
  43. package/src/commands/package/types.js.map +1 -1
  44. package/src/config/frontmcp-config.loader.d.ts +20 -0
  45. package/src/config/frontmcp-config.loader.js +124 -5
  46. package/src/config/frontmcp-config.loader.js.map +1 -1
  47. package/src/config/index.d.ts +1 -1
  48. package/src/config/index.js +2 -1
  49. package/src/config/index.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontmcp",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "FrontMCP command line interface",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -22,6 +22,7 @@
22
22
  "url": "https://github.com/agentfront/frontmcp/issues"
23
23
  },
24
24
  "main": "./src/index.js",
25
+ "types": "./index.d.js",
25
26
  "bin": {
26
27
  "frontmcp": "./src/core/cli.js"
27
28
  },
@@ -30,9 +31,9 @@
30
31
  },
31
32
  "dependencies": {
32
33
  "@clack/prompts": "^0.10.0",
33
- "@frontmcp/lazy-zod": "1.1.0",
34
- "@frontmcp/utils": "1.1.0",
35
- "@frontmcp/skills": "1.1.0",
34
+ "@frontmcp/lazy-zod": "1.1.1",
35
+ "@frontmcp/utils": "1.1.1",
36
+ "@frontmcp/skills": "1.1.1",
36
37
  "commander": "^13.0.0",
37
38
  "tslib": "^2.3.0",
38
39
  "vectoriadb": "^2.2.0",
@@ -48,6 +49,5 @@
48
49
  "@types/yauzl": "^2.10.3",
49
50
  "@types/yazl": "^2.4.5"
50
51
  },
51
- "types": "./index.d.js",
52
52
  "type": "commonjs"
53
53
  }
@@ -72,8 +72,36 @@ module.exports = {
72
72
  }
73
73
  };
74
74
  `,
75
+ // #375 — fail the build when the user's @FrontMcp config references
76
+ // Node-only storage providers that can't run on Workers. Conditional
77
+ // (env-gated) usage still passes; only unconditional declarations throw.
78
+ validate: (decoratorConfig) => {
79
+ if (!decoratorConfig)
80
+ return;
81
+ const sqlite = decoratorConfig['sqlite'];
82
+ const redis = decoratorConfig['redis'];
83
+ const errors = [];
84
+ if (sqlite && typeof sqlite === 'object') {
85
+ errors.push('sqlite storage is not supported on --target cloudflare (no fs / native modules on Workers). ' +
86
+ 'Use Cloudflare KV / Durable Objects, or gate the sqlite branch behind a build-time `define` ' +
87
+ 'so the bundler can dead-code-eliminate it.');
88
+ }
89
+ if (redis && typeof redis === 'object') {
90
+ errors.push('ioredis-style `redis` storage is not supported on --target cloudflare (no Node net). ' +
91
+ 'Use Vercel KV / Upstash Redis (HTTP), or move the redis config to a runtime branch ' +
92
+ 'gated on `globalThis.process?.env`.');
93
+ }
94
+ if (errors.length) {
95
+ throw new Error(`[--target cloudflare] config incompatible with Cloudflare Workers:\n - ${errors.join('\n - ')}`);
96
+ }
97
+ },
98
+ // #374 — always write wrangler.toml from the build output. Skipping when
99
+ // the file already exists left users with a wrangler.toml that pointed at
100
+ // dist/index.js while the build emitted dist/cloudflare/index.js, and
101
+ // wrangler deploy silently failed.
102
+ alwaysWriteConfig: true,
75
103
  getConfig: (_cwd) => `name = "frontmcp-worker"
76
- main = "dist/index.js"
104
+ main = "dist/cloudflare/index.js"
77
105
  compatibility_date = "2024-01-01"
78
106
  `,
79
107
  configFileName: 'wrangler.toml',
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../../../../src/commands/build/adapters/cloudflare.ts"],"names":[],"mappings":";;;AAEA;;;;;GAKG;AACU,QAAA,iBAAiB,GAAoB;IAChD,YAAY,EAAE,UAAU;IAExB,gBAAgB,EAAE,CAAC,cAAsB,EAAE,EAAE,CAAC;;;;;WAKrC,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyDxB;IAEC,SAAS,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC;;;CAG9B;IAEC,cAAc,EAAE,eAAe;CAChC,CAAC","sourcesContent":["import type { AdapterTemplate } from '../types';\n\n/**\n * Cloudflare Workers adapter - edge deployment on Cloudflare.\n * Compiles to CommonJS and adapts the Express app to Cloudflare's fetch API.\n *\n * @see https://developers.cloudflare.com/workers/\n */\nexport const cloudflareAdapter: AdapterTemplate = {\n moduleFormat: 'commonjs',\n\n getEntryTemplate: (mainModulePath: string) => `// Auto-generated Cloudflare Workers entry point\n// Generated by: frontmcp build --target cloudflare\nprocess.env.FRONTMCP_SERVERLESS = '1';\nprocess.env.FRONTMCP_DEPLOYMENT_MODE = 'serverless';\n\nrequire('${mainModulePath}');\nconst { getServerlessHandlerAsync } = require('@frontmcp/sdk');\n\nlet app = null;\n\nasync function handleRequest(request) {\n if (!app) {\n app = await getServerlessHandlerAsync();\n }\n\n // Convert Cloudflare Request to Node.js format\n const url = new URL(request.url);\n const req = {\n method: request.method,\n url: url.pathname + url.search,\n headers: Object.fromEntries(request.headers),\n body: request.body,\n };\n\n return new Promise((resolve) => {\n const res = {\n statusCode: 200,\n headers: {},\n body: '',\n status(code) { this.statusCode = code; return this; },\n setHeader(key, value) { this.headers[key] = value; },\n json(data) {\n this.headers['Content-Type'] = 'application/json';\n this.body = JSON.stringify(data);\n resolve(new Response(this.body, {\n status: this.statusCode,\n headers: this.headers,\n }));\n },\n send(data) {\n this.body = data;\n resolve(new Response(this.body, {\n status: this.statusCode,\n headers: this.headers,\n }));\n },\n end() {\n resolve(new Response(this.body, {\n status: this.statusCode,\n headers: this.headers,\n }));\n },\n };\n app(req, res);\n });\n}\n\nmodule.exports = {\n async fetch(request, env, ctx) {\n return handleRequest(request);\n }\n};\n`,\n\n getConfig: (_cwd: string) => `name = \"frontmcp-worker\"\nmain = \"dist/index.js\"\ncompatibility_date = \"2024-01-01\"\n`,\n\n configFileName: 'wrangler.toml',\n};\n"]}
1
+ {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../../../../src/commands/build/adapters/cloudflare.ts"],"names":[],"mappings":";;;AAEA;;;;;GAKG;AACU,QAAA,iBAAiB,GAAoB;IAChD,YAAY,EAAE,UAAU;IAExB,gBAAgB,EAAE,CAAC,cAAsB,EAAE,EAAE,CAAC;;;;;WAKrC,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyDxB;IAEC,oEAAoE;IACpE,qEAAqE;IACrE,yEAAyE;IACzE,QAAQ,EAAE,CAAC,eAAe,EAAE,EAAE;QAC5B,IAAI,CAAC,eAAe;YAAE,OAAO;QAC7B,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YACzC,MAAM,CAAC,IAAI,CACT,8FAA8F;gBAC5F,8FAA8F;gBAC9F,4CAA4C,CAC/C,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,CAAC,IAAI,CACT,uFAAuF;gBACrF,qFAAqF;gBACrF,qCAAqC,CACxC,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CACb,2EAA2E,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CACnG,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,sEAAsE;IACtE,mCAAmC;IACnC,iBAAiB,EAAE,IAAI;IAEvB,SAAS,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC;;;CAG9B;IAEC,cAAc,EAAE,eAAe;CAChC,CAAC","sourcesContent":["import type { AdapterTemplate } from '../types';\n\n/**\n * Cloudflare Workers adapter - edge deployment on Cloudflare.\n * Compiles to CommonJS and adapts the Express app to Cloudflare's fetch API.\n *\n * @see https://developers.cloudflare.com/workers/\n */\nexport const cloudflareAdapter: AdapterTemplate = {\n moduleFormat: 'commonjs',\n\n getEntryTemplate: (mainModulePath: string) => `// Auto-generated Cloudflare Workers entry point\n// Generated by: frontmcp build --target cloudflare\nprocess.env.FRONTMCP_SERVERLESS = '1';\nprocess.env.FRONTMCP_DEPLOYMENT_MODE = 'serverless';\n\nrequire('${mainModulePath}');\nconst { getServerlessHandlerAsync } = require('@frontmcp/sdk');\n\nlet app = null;\n\nasync function handleRequest(request) {\n if (!app) {\n app = await getServerlessHandlerAsync();\n }\n\n // Convert Cloudflare Request to Node.js format\n const url = new URL(request.url);\n const req = {\n method: request.method,\n url: url.pathname + url.search,\n headers: Object.fromEntries(request.headers),\n body: request.body,\n };\n\n return new Promise((resolve) => {\n const res = {\n statusCode: 200,\n headers: {},\n body: '',\n status(code) { this.statusCode = code; return this; },\n setHeader(key, value) { this.headers[key] = value; },\n json(data) {\n this.headers['Content-Type'] = 'application/json';\n this.body = JSON.stringify(data);\n resolve(new Response(this.body, {\n status: this.statusCode,\n headers: this.headers,\n }));\n },\n send(data) {\n this.body = data;\n resolve(new Response(this.body, {\n status: this.statusCode,\n headers: this.headers,\n }));\n },\n end() {\n resolve(new Response(this.body, {\n status: this.statusCode,\n headers: this.headers,\n }));\n },\n };\n app(req, res);\n });\n}\n\nmodule.exports = {\n async fetch(request, env, ctx) {\n return handleRequest(request);\n }\n};\n`,\n\n // #375 — fail the build when the user's @FrontMcp config references\n // Node-only storage providers that can't run on Workers. Conditional\n // (env-gated) usage still passes; only unconditional declarations throw.\n validate: (decoratorConfig) => {\n if (!decoratorConfig) return;\n const sqlite = decoratorConfig['sqlite'];\n const redis = decoratorConfig['redis'];\n const errors: string[] = [];\n if (sqlite && typeof sqlite === 'object') {\n errors.push(\n 'sqlite storage is not supported on --target cloudflare (no fs / native modules on Workers). ' +\n 'Use Cloudflare KV / Durable Objects, or gate the sqlite branch behind a build-time `define` ' +\n 'so the bundler can dead-code-eliminate it.',\n );\n }\n if (redis && typeof redis === 'object') {\n errors.push(\n 'ioredis-style `redis` storage is not supported on --target cloudflare (no Node net). ' +\n 'Use Vercel KV / Upstash Redis (HTTP), or move the redis config to a runtime branch ' +\n 'gated on `globalThis.process?.env`.',\n );\n }\n if (errors.length) {\n throw new Error(\n `[--target cloudflare] config incompatible with Cloudflare Workers:\\n - ${errors.join('\\n - ')}`,\n );\n }\n },\n\n // #374 — always write wrangler.toml from the build output. Skipping when\n // the file already exists left users with a wrangler.toml that pointed at\n // dist/index.js while the build emitted dist/cloudflare/index.js, and\n // wrangler deploy silently failed.\n alwaysWriteConfig: true,\n\n getConfig: (_cwd: string) => `name = \"frontmcp-worker\"\nmain = \"dist/cloudflare/index.js\"\ncompatibility_date = \"2024-01-01\"\n`,\n\n configFileName: 'wrangler.toml',\n};\n"]}
@@ -23,7 +23,10 @@ process.env.FRONTMCP_DEPLOYMENT_MODE = 'distributed';
23
23
  `,
24
24
  getEntryTemplate: (mainModulePath) => `// Auto-generated distributed entry point
25
25
  // Generated by: frontmcp build --target distributed
26
- require('./ha-setup.js');
26
+ // The build emits the HA bootstrap as serverless-setup.js (shared name across
27
+ // all serverless/distributed adapters); requiring ha-setup.js used to crash at
28
+ // runtime because that filename was never written.
29
+ require('./serverless-setup.js');
27
30
  require('${mainModulePath}');
28
31
  `,
29
32
  };
@@ -1 +1 @@
1
- {"version":3,"file":"distributed.js","sourceRoot":"","sources":["../../../../../src/commands/build/adapters/distributed.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;GAYG;AACU,QAAA,kBAAkB,GAAoB;IACjD,YAAY,EAAE,UAAU;IAExB,gBAAgB,EAAE,GAAG,EAAE,CAAC;;;;CAIzB;IAEC,gBAAgB,EAAE,CAAC,cAAsB,EAAE,EAAE,CAAC;;;WAGrC,cAAc;CACxB;CACA,CAAC","sourcesContent":["import type { AdapterTemplate } from '../types';\n\n/**\n * Distributed adapter - multi-pod Node.js deployment with HA.\n * Compiles to CommonJS with an HA setup file that sets FRONTMCP_DEPLOYMENT_MODE.\n *\n * The setup file runs before any imports to ensure the deployment mode\n * is detected before @FrontMcp decorators execute. This enables:\n * - Session heartbeat and takeover\n * - Cross-pod notification relay via Redis pub/sub\n * - LB affinity via __frontmcp_node cookie\n * - Machine ID from K8s HOSTNAME or os.hostname()\n *\n * Requires Redis configuration in @FrontMcp() for session persistence.\n */\nexport const distributedAdapter: AdapterTemplate = {\n moduleFormat: 'commonjs',\n\n getSetupTemplate: () => `// Distributed HA setup - MUST be imported first\n// Sets FRONTMCP_DEPLOYMENT_MODE before any decorators run\n// Enables heartbeat, session takeover, and notification relay\nprocess.env.FRONTMCP_DEPLOYMENT_MODE = 'distributed';\n`,\n\n getEntryTemplate: (mainModulePath: string) => `// Auto-generated distributed entry point\n// Generated by: frontmcp build --target distributed\nrequire('./ha-setup.js');\nrequire('${mainModulePath}');\n`,\n};\n"]}
1
+ {"version":3,"file":"distributed.js","sourceRoot":"","sources":["../../../../../src/commands/build/adapters/distributed.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;GAYG;AACU,QAAA,kBAAkB,GAAoB;IACjD,YAAY,EAAE,UAAU;IAExB,gBAAgB,EAAE,GAAG,EAAE,CAAC;;;;CAIzB;IAEC,gBAAgB,EAAE,CAAC,cAAsB,EAAE,EAAE,CAAC;;;;;;WAMrC,cAAc;CACxB;CACA,CAAC","sourcesContent":["import type { AdapterTemplate } from '../types';\n\n/**\n * Distributed adapter - multi-pod Node.js deployment with HA.\n * Compiles to CommonJS with an HA setup file that sets FRONTMCP_DEPLOYMENT_MODE.\n *\n * The setup file runs before any imports to ensure the deployment mode\n * is detected before @FrontMcp decorators execute. This enables:\n * - Session heartbeat and takeover\n * - Cross-pod notification relay via Redis pub/sub\n * - LB affinity via __frontmcp_node cookie\n * - Machine ID from K8s HOSTNAME or os.hostname()\n *\n * Requires Redis configuration in @FrontMcp() for session persistence.\n */\nexport const distributedAdapter: AdapterTemplate = {\n moduleFormat: 'commonjs',\n\n getSetupTemplate: () => `// Distributed HA setup - MUST be imported first\n// Sets FRONTMCP_DEPLOYMENT_MODE before any decorators run\n// Enables heartbeat, session takeover, and notification relay\nprocess.env.FRONTMCP_DEPLOYMENT_MODE = 'distributed';\n`,\n\n getEntryTemplate: (mainModulePath: string) => `// Auto-generated distributed entry point\n// Generated by: frontmcp build --target distributed\n// The build emits the HA bootstrap as serverless-setup.js (shared name across\n// all serverless/distributed adapters); requiring ha-setup.js used to crash at\n// runtime because that filename was never written.\nrequire('./serverless-setup.js');\nrequire('${mainModulePath}');\n`,\n};\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * CLI-bundle source for `_extractPublicMessage` / `_exitWithError`.
3
+ *
4
+ * The SEA CLI bundle has to be self-contained — it can't reach into
5
+ * `@frontmcp/sdk` at runtime — so we duplicate the SDK's
6
+ * `extractPublicMessage` walker into the generated source. To prevent the
7
+ * two implementations from drifting, the duplicated source lives in this
8
+ * file as a string constant and is shared by:
9
+ *
10
+ * - `generateCliEntry.ts` (embeds it in the generated CLI source)
11
+ * - `extract-public-message-parity.spec.ts` (parity-tests it against
12
+ * the SDK's TS implementation using shared fixtures)
13
+ *
14
+ * Keep the function bodies free of TS / ESM-only syntax — the resulting
15
+ * string is concatenated into a CommonJS bundle and `Function('return …')`
16
+ * compiles it as plain ES2020.
17
+ */
18
+ export declare const EXTRACT_PUBLIC_MESSAGE_SNIPPET: string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXTRACT_PUBLIC_MESSAGE_SNIPPET = void 0;
4
+ /**
5
+ * CLI-bundle source for `_extractPublicMessage` / `_exitWithError`.
6
+ *
7
+ * The SEA CLI bundle has to be self-contained — it can't reach into
8
+ * `@frontmcp/sdk` at runtime — so we duplicate the SDK's
9
+ * `extractPublicMessage` walker into the generated source. To prevent the
10
+ * two implementations from drifting, the duplicated source lives in this
11
+ * file as a string constant and is shared by:
12
+ *
13
+ * - `generateCliEntry.ts` (embeds it in the generated CLI source)
14
+ * - `extract-public-message-parity.spec.ts` (parity-tests it against
15
+ * the SDK's TS implementation using shared fixtures)
16
+ *
17
+ * Keep the function bodies free of TS / ESM-only syntax — the resulting
18
+ * string is concatenated into a CommonJS bundle and `Function('return …')`
19
+ * compiles it as plain ES2020.
20
+ */
21
+ exports.EXTRACT_PUBLIC_MESSAGE_SNIPPET = `
22
+ // Walk an error chain (cause / originalError) and return the most user-friendly
23
+ // message — prefers PublicMcpError.getPublicMessage() over wrapped wrappers like
24
+ // "Tool 'X' execution failed: Unknown error". Mirrors @frontmcp/sdk's
25
+ // extractPublicMessage so the SEA bundle stays self-contained. Cycles in the
26
+ // chain are short-circuited via a WeakSet of already-visited error objects.
27
+ function _extractPublicMessage(err) {
28
+ return _extractPublicMessageImpl(err, new WeakSet());
29
+ }
30
+ function _extractPublicMessageImpl(err, visited) {
31
+ if (err && typeof err === 'object') {
32
+ if (visited.has(err)) return 'Unknown error';
33
+ visited.add(err);
34
+ }
35
+ if (err == null) return 'Unknown error';
36
+ if (typeof err === 'string') return err;
37
+ // Direct PublicMcpError (has isPublic === true and a non-default message)
38
+ if (err && err.isPublic === true && err.message) return err.message;
39
+ // Wrapped: try originalError, then cause.
40
+ var inner = err && (err.originalError || err.cause);
41
+ if (inner) {
42
+ var innerMsg = _extractPublicMessageImpl(inner, visited);
43
+ if (innerMsg && innerMsg !== 'Unknown error') return innerMsg;
44
+ }
45
+ // Fallback: own .message, but skip generic wrappers when we have nothing better.
46
+ if (err.message) return err.message;
47
+ return String(err);
48
+ }
49
+ // Print an error with the best available public message and set the appropriate
50
+ // exit code (1 = runtime error, 2 = usage error). Centralized so every action
51
+ // handler reports the same shape.
52
+ function _exitWithError(err, code) {
53
+ var msg = _extractPublicMessage(err);
54
+ console.error('Error: ' + msg);
55
+ process.exitCode = code || 1;
56
+ }
57
+ `.trim();
58
+ //# sourceMappingURL=extract-public-message.snippet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-public-message.snippet.js","sourceRoot":"","sources":["../../../../../../src/commands/build/exec/cli-runtime/extract-public-message.snippet.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;GAgBG;AACU,QAAA,8BAA8B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoC7C,CAAC,IAAI,EAAE,CAAC","sourcesContent":["/**\n * CLI-bundle source for `_extractPublicMessage` / `_exitWithError`.\n *\n * The SEA CLI bundle has to be self-contained — it can't reach into\n * `@frontmcp/sdk` at runtime — so we duplicate the SDK's\n * `extractPublicMessage` walker into the generated source. To prevent the\n * two implementations from drifting, the duplicated source lives in this\n * file as a string constant and is shared by:\n *\n * - `generateCliEntry.ts` (embeds it in the generated CLI source)\n * - `extract-public-message-parity.spec.ts` (parity-tests it against\n * the SDK's TS implementation using shared fixtures)\n *\n * Keep the function bodies free of TS / ESM-only syntax — the resulting\n * string is concatenated into a CommonJS bundle and `Function('return …')`\n * compiles it as plain ES2020.\n */\nexport const EXTRACT_PUBLIC_MESSAGE_SNIPPET = `\n// Walk an error chain (cause / originalError) and return the most user-friendly\n// message — prefers PublicMcpError.getPublicMessage() over wrapped wrappers like\n// \"Tool 'X' execution failed: Unknown error\". Mirrors @frontmcp/sdk's\n// extractPublicMessage so the SEA bundle stays self-contained. Cycles in the\n// chain are short-circuited via a WeakSet of already-visited error objects.\nfunction _extractPublicMessage(err) {\n return _extractPublicMessageImpl(err, new WeakSet());\n}\nfunction _extractPublicMessageImpl(err, visited) {\n if (err && typeof err === 'object') {\n if (visited.has(err)) return 'Unknown error';\n visited.add(err);\n }\n if (err == null) return 'Unknown error';\n if (typeof err === 'string') return err;\n // Direct PublicMcpError (has isPublic === true and a non-default message)\n if (err && err.isPublic === true && err.message) return err.message;\n // Wrapped: try originalError, then cause.\n var inner = err && (err.originalError || err.cause);\n if (inner) {\n var innerMsg = _extractPublicMessageImpl(inner, visited);\n if (innerMsg && innerMsg !== 'Unknown error') return innerMsg;\n }\n // Fallback: own .message, but skip generic wrappers when we have nothing better.\n if (err.message) return err.message;\n return String(err);\n}\n// Print an error with the best available public message and set the appropriate\n// exit code (1 = runtime error, 2 = usage error). Centralized so every action\n// handler reports the same shape.\nfunction _exitWithError(err, code) {\n var msg = _extractPublicMessage(err);\n console.error('Error: ' + msg);\n process.exitCode = code || 1;\n}\n`.trim();\n"]}
@@ -2,8 +2,8 @@
2
2
  * Generates the CLI entry point TypeScript/JavaScript source code.
3
3
  * This creates a commander.js-based CLI where each MCP tool is a subcommand.
4
4
  */
5
- import { CliConfig, OAuthConfig } from '../config';
6
- import { ExtractedSchema } from './schema-extractor';
5
+ import { type CliConfig, type OAuthConfig } from '../config';
6
+ import { type ExtractedSchema } from './schema-extractor';
7
7
  export declare const RESERVED_COMMANDS: Set<string>;
8
8
  export interface CliEntryOptions {
9
9
  appName: string;
@@ -8,6 +8,7 @@ exports.RESERVED_COMMANDS = void 0;
8
8
  exports.resolveToolCommandName = resolveToolCommandName;
9
9
  exports.generateCliEntry = generateCliEntry;
10
10
  exports.extractTemplateParams = extractTemplateParams;
11
+ const extract_public_message_snippet_1 = require("./extract-public-message.snippet");
11
12
  const schema_extractor_1 = require("./schema-extractor");
12
13
  const schema_to_commander_1 = require("./schema-to-commander");
13
14
  exports.RESERVED_COMMANDS = new Set([
@@ -15,6 +16,9 @@ exports.RESERVED_COMMANDS = new Set([
15
16
  'login', 'logout', 'connect', 'serve', 'daemon',
16
17
  'doctor', 'install', 'uninstall', 'sessions', 'help', 'version',
17
18
  'skills', 'job', 'workflow',
19
+ // Reserved by the symmetric `prompt get <name>` / `resource read <uri>`
20
+ // sub-API; included so a user-defined entry can never shadow them.
21
+ 'get', 'list', 'read',
18
22
  ]);
19
23
  /**
20
24
  * Resolve tool command name, appending '-tool' suffix if it conflicts with a built-in command.
@@ -193,7 +197,31 @@ async function closeClient() {
193
197
  // Flag set by long-running commands (serve, daemon) to prevent the footer from calling process.exit().
194
198
  var _isLongRunning = false;
195
199
 
200
+ ${extract_public_message_snippet_1.EXTRACT_PUBLIC_MESSAGE_SNIPPET}
201
+
196
202
  var program = new Command();
203
+ // Make Commander's own usage errors (unknown subcommand, missing required option,
204
+ // invalid value) exit with code 2 instead of the default 0 — matches POSIX
205
+ // convention and lets shell scripts/CI distinguish runtime failures from usage.
206
+ //
207
+ // Sets process.exitCode and re-throws so the parseAsync() footer can run
208
+ // closeClient() / native-addon teardown before the actual exit. Calling
209
+ // process.exit() directly here would skip that and could leave better-sqlite3
210
+ // / ONNX / file handles in a corrupt state for short-lived runs.
211
+ program.exitOverride(function(err) {
212
+ if (err.code === 'commander.helpDisplayed' || err.code === 'commander.help' || err.code === 'commander.version') {
213
+ process.exitCode = 0;
214
+ } else if (err.code === 'commander.unknownCommand' || err.code === 'commander.unknownOption' ||
215
+ err.code === 'commander.missingArgument' || err.code === 'commander.missingMandatoryOptionValue' ||
216
+ err.code === 'commander.invalidArgument' || err.code === 'commander.optionMissingArgument' ||
217
+ err.code === 'commander.invalidOptionArgument' || err.code === 'commander.excessArguments') {
218
+ process.exitCode = 2;
219
+ } else {
220
+ process.exitCode = 1;
221
+ }
222
+ // Re-throw so parseAsync().catch in the footer runs cleanup before exit.
223
+ throw err;
224
+ });
197
225
  program
198
226
  .name(${JSON.stringify(appName)})
199
227
  .version(${JSON.stringify(appVersion)})
@@ -280,16 +308,28 @@ ${optionLines}
280
308
  var result = await client.callTool(${JSON.stringify(tool.name)}, args);
281
309
  var mode = program.opts().output || ${JSON.stringify('text')};
282
310
  console.log(fmt.formatToolResult(result, mode));
311
+ // The SDK converts thrown errors into a CallToolResult with isError:true
312
+ // (so HTTP/JSON-RPC clients still get a structured response). Detect
313
+ // that here and map to a non-zero exit code so shell scripts can gate
314
+ // on success — Zod input validation errors → exit 2 (usage), all
315
+ // other tool errors → exit 1 (runtime).
316
+ if (result && result.isError === true) {
317
+ var rmeta = (result && result._meta) || {};
318
+ process.exitCode = (rmeta.code === 'INVALID_INPUT') ? 2 : 1;
319
+ }
283
320
  } catch (err) {
284
321
  var meta = err && err._meta ? err._meta : (err && err.data && err.data._meta ? err.data._meta : null);
285
322
  if (meta && meta.authorization_required) {
286
323
  console.error('Authorization required' + (meta.app ? ' for ' + meta.app : ''));
287
324
  if (meta.auth_url) console.error('Authorize at: ' + meta.auth_url);
288
325
  console.error('Or run: ' + ${JSON.stringify(appName)} + ' login');
326
+ process.exitCode = 1;
289
327
  } else {
290
- console.error('Error:', err.message || err);
328
+ // Thrown error path (transport / DI / pre-flow failure) — same
329
+ // mapping as the isError result path above.
330
+ var isUsage = err && err.code === 'INVALID_INPUT';
331
+ _exitWithError(err, isUsage ? 2 : 1);
291
332
  }
292
- process.exitCode = 1;
293
333
  }
294
334
  });`;
295
335
  });
@@ -362,8 +402,7 @@ resourceCmd
362
402
  });
363
403
  }
364
404
  } catch (err) {
365
- console.error('Error:', err.message || err);
366
- process.exitCode = 1;
405
+ _exitWithError(err, 1);
367
406
  }
368
407
  });
369
408
 
@@ -377,8 +416,7 @@ resourceCmd
377
416
  var mode = program.opts().output || 'text';
378
417
  console.log(fmt.formatResourceResult(result, mode));
379
418
  } catch (err) {
380
- console.error('Error:', err.message || err);
381
- process.exitCode = 1;
419
+ _exitWithError(err, 1);
382
420
  }
383
421
  });`;
384
422
  }
@@ -412,8 +450,7 @@ ${optionLines}
412
450
  var mode = program.opts().output || 'text';
413
451
  console.log(fmt.formatResourceResult(result, mode));
414
452
  } catch (err) {
415
- console.error('Error:', err.message || err);
416
- process.exitCode = 1;
453
+ _exitWithError(err, 1);
417
454
  }
418
455
  });`;
419
456
  });
@@ -437,14 +474,25 @@ templateCmd
437
474
  });
438
475
  }
439
476
  } catch (err) {
440
- console.error('Error:', err.message || err);
441
- process.exitCode = 1;
477
+ _exitWithError(err, 1);
442
478
  }
443
479
  });
444
480
 
445
481
  ${subcommands.join('\n\n')}`;
446
482
  }
447
483
  function generatePromptCommands(prompts) {
484
+ // Map known prompts → option specs so `prompt get <name>` knows which flags
485
+ // to accept per prompt. Unknown prompt names still call getPrompt() and
486
+ // surface the server's error to the user.
487
+ const promptArgsMap = prompts.map((p) => {
488
+ const args = (p.arguments || []).map((a) => ({
489
+ name: a.name,
490
+ kebab: (0, schema_to_commander_1.camelToKebab)(a.name),
491
+ camel: kebabToCamel((0, schema_to_commander_1.camelToKebab)(a.name)),
492
+ required: !!a.required,
493
+ }));
494
+ return `${JSON.stringify(p.name)}: ${JSON.stringify(args)}`;
495
+ }).join(',\n ');
448
496
  const subcommands = prompts.map((prompt) => {
449
497
  const cmdName = (0, schema_to_commander_1.camelToKebab)(prompt.name).replace(/_/g, '-');
450
498
  const argOptions = (prompt.arguments || [])
@@ -458,7 +506,7 @@ function generatePromptCommands(prompts) {
458
506
  .join('\n');
459
507
  return `promptCmd
460
508
  .command(${JSON.stringify(cmdName)})
461
- .description(${JSON.stringify(prompt.description || '')})
509
+ .description(${JSON.stringify((prompt.description || '') + ' (deprecated alias — use `prompt get ' + cmdName + '`)')})
462
510
  ${argOptions}
463
511
  .action(async function(opts) {
464
512
  try {
@@ -473,8 +521,7 @@ ${argOptions}
473
521
  var mode = program.opts().output || 'text';
474
522
  console.log(fmt.formatPromptResult(result, mode));
475
523
  } catch (err) {
476
- console.error('Error:', err.message || err);
477
- process.exitCode = 1;
524
+ _exitWithError(err, 1);
478
525
  }
479
526
  });`;
480
527
  });
@@ -498,8 +545,83 @@ promptCmd
498
545
  });
499
546
  }
500
547
  } catch (err) {
501
- console.error('Error:', err.message || err);
502
- process.exitCode = 1;
548
+ _exitWithError(err, 1);
549
+ }
550
+ });
551
+
552
+ // Symmetric \`prompt get <name>\` to mirror \`resource read <uri>\`. Looks up
553
+ // per-prompt argument metadata from \`promptArgs\` and forwards as MCP
554
+ // \`prompts/get\` request arguments. Unknown / missing required flags exit 2.
555
+ //
556
+ // Robust flag parser supports:
557
+ // --key value (canonical form)
558
+ // --key=value (single-token form)
559
+ // --bool (boolean — value defaults to "true")
560
+ // -- (end-of-options marker; everything after is ignored)
561
+ // Unknown flags fail-fast with exit 2 instead of being silently dropped.
562
+ var promptArgs = {
563
+ ${promptArgsMap}
564
+ };
565
+ var _getCmd = promptCmd
566
+ .command('get <name>')
567
+ .description('Render a prompt by name')
568
+ .allowUnknownOption(true)
569
+ .action(async function(name) {
570
+ try {
571
+ var spec = promptArgs[name];
572
+ if (!spec) {
573
+ console.error('Error: Unknown prompt: ' + name);
574
+ process.exitCode = 1;
575
+ return;
576
+ }
577
+ var rawTokens = this.args.slice(1);
578
+ var args = {};
579
+ var unknown = [];
580
+ var byKebab = {};
581
+ for (var s = 0; s < spec.length; s++) byKebab[spec[s].kebab] = spec[s];
582
+ for (var i = 0; i < rawTokens.length; i++) {
583
+ var tok = rawTokens[i];
584
+ if (tok === '--') break;
585
+ if (typeof tok !== 'string' || tok.indexOf('--') !== 0) continue;
586
+ var keyAndVal = tok.slice(2);
587
+ var key, val;
588
+ var eq = keyAndVal.indexOf('=');
589
+ if (eq >= 0) {
590
+ key = keyAndVal.slice(0, eq);
591
+ val = keyAndVal.slice(eq + 1);
592
+ } else {
593
+ key = keyAndVal;
594
+ var next = (i + 1 < rawTokens.length) ? rawTokens[i + 1] : undefined;
595
+ if (typeof next === 'string' && next.indexOf('--') !== 0) {
596
+ val = next;
597
+ i++;
598
+ } else {
599
+ val = 'true';
600
+ }
601
+ }
602
+ var match = byKebab[key];
603
+ if (!match) { unknown.push('--' + key); continue; }
604
+ args[match.name] = val;
605
+ }
606
+ if (unknown.length > 0) {
607
+ console.error('Error: unknown option(s) for prompt "' + name + '": ' + unknown.join(', '));
608
+ process.exitCode = 2;
609
+ return;
610
+ }
611
+ // Validate required args
612
+ for (var r = 0; r < spec.length; r++) {
613
+ if (spec[r].required && args[spec[r].name] === undefined) {
614
+ console.error('Error: missing required option --' + spec[r].kebab);
615
+ process.exitCode = 2;
616
+ return;
617
+ }
618
+ }
619
+ var client = await getClient();
620
+ var result = await client.getPrompt(name, args);
621
+ var mode = program.opts().output || 'text';
622
+ console.log(fmt.formatPromptResult(result, mode));
623
+ } catch (err) {
624
+ _exitWithError(err, 1);
503
625
  }
504
626
  });
505
627
 
@@ -535,8 +657,7 @@ skillsCmd
535
657
  console.log(" Use '" + program.name() + " skills load <name>' to load a skill.\\n");
536
658
  }
537
659
  } catch (err) {
538
- console.error('Error:', err.message || err);
539
- process.exitCode = 1;
660
+ _exitWithError(err, 1);
540
661
  }
541
662
  });
542
663
 
@@ -557,8 +678,7 @@ skillsCmd
557
678
  }
558
679
  }
559
680
  } catch (err) {
560
- console.error('Error:', err.message || err);
561
- process.exitCode = 1;
681
+ _exitWithError(err, 1);
562
682
  }
563
683
  });
564
684
 
@@ -594,8 +714,7 @@ skillsCmd
594
714
  console.log(" Load: " + program.name() + " skills load " + name + '\\n');
595
715
  }
596
716
  } catch (err) {
597
- console.error('Error:', err.message || err);
598
- process.exitCode = 1;
717
+ _exitWithError(err, 1);
599
718
  }
600
719
  });
601
720
 
@@ -623,8 +742,7 @@ skillsCmd
623
742
  console.log(" Use '" + program.name() + " skills read <name>' for full details.\\n");
624
743
  }
625
744
  } catch (err) {
626
- console.error('Error:', err.message || err);
627
- process.exitCode = 1;
745
+ _exitWithError(err, 1);
628
746
  }
629
747
  });`;
630
748
  }
@@ -659,8 +777,7 @@ ${optionLines}
659
777
  }
660
778
  }
661
779
  } catch (err) {
662
- console.error('Error:', err.message || err);
663
- process.exitCode = 1;
780
+ _exitWithError(err, 1);
664
781
  }
665
782
  });`;
666
783
  }
@@ -690,8 +807,7 @@ ${optionLines}
690
807
  }
691
808
  }
692
809
  } catch (err) {
693
- console.error('Error:', err.message || err);
694
- process.exitCode = 1;
810
+ _exitWithError(err, 1);
695
811
  }
696
812
  });`;
697
813
  });
@@ -721,8 +837,7 @@ ${optionLines}
721
837
  }
722
838
  }
723
839
  } catch (err) {
724
- console.error('Error:', err.message || err);
725
- process.exitCode = 1;
840
+ _exitWithError(err, 1);
726
841
  }
727
842
  });`;
728
843
  return `var jobCmd = program.command('job').description('Job operations');
@@ -749,8 +864,7 @@ jobCmd
749
864
  }
750
865
  }
751
866
  } catch (err) {
752
- console.error('Error:', err.message || err);
753
- process.exitCode = 1;
867
+ _exitWithError(err, 1);
754
868
  }
755
869
  });
756
870
 
@@ -782,8 +896,7 @@ ${jobs.length > 0 ? genericRun : `jobRunCmd
782
896
  }
783
897
  }
784
898
  } catch (err) {
785
- console.error('Error:', err.message || err);
786
- process.exitCode = 1;
899
+ _exitWithError(err, 1);
787
900
  }
788
901
  });`}
789
902
 
@@ -801,8 +914,7 @@ jobCmd
801
914
  console.log('Status: ' + (result.status || JSON.stringify(result)));
802
915
  }
803
916
  } catch (err) {
804
- console.error('Error:', err.message || err);
805
- process.exitCode = 1;
917
+ _exitWithError(err, 1);
806
918
  }
807
919
  });`;
808
920
  }
@@ -831,8 +943,7 @@ workflowCmd
831
943
  }
832
944
  }
833
945
  } catch (err) {
834
- console.error('Error:', err.message || err);
835
- process.exitCode = 1;
946
+ _exitWithError(err, 1);
836
947
  }
837
948
  });
838
949
 
@@ -861,8 +972,7 @@ workflowCmd
861
972
  }
862
973
  }
863
974
  } catch (err) {
864
- console.error('Error:', err.message || err);
865
- process.exitCode = 1;
975
+ _exitWithError(err, 1);
866
976
  }
867
977
  });
868
978
 
@@ -880,8 +990,7 @@ workflowCmd
880
990
  console.log('Status: ' + (result.status || JSON.stringify(result)));
881
991
  }
882
992
  } catch (err) {
883
- console.error('Error:', err.message || err);
884
- process.exitCode = 1;
993
+ _exitWithError(err, 1);
885
994
  }
886
995
  });`;
887
996
  }
@@ -933,8 +1042,7 @@ subscribeCmd
933
1042
  setInterval(function() {}, 2147483647);
934
1043
  await new Promise(function() {});
935
1044
  } catch (err) {
936
- console.error('Error:', err.message || err);
937
- process.exitCode = 1;
1045
+ _exitWithError(err, 1);
938
1046
  }
939
1047
  });
940
1048
 
@@ -962,8 +1070,7 @@ subscribeCmd
962
1070
  setInterval(function() {}, 2147483647);
963
1071
  await new Promise(function() {});
964
1072
  } catch (err) {
965
- console.error('Error:', err.message || err);
966
- process.exitCode = 1;
1073
+ _exitWithError(err, 1);
967
1074
  }
968
1075
  });`;
969
1076
  }
@@ -1521,9 +1628,20 @@ program.parseAsync(process.argv).then(async function() {
1521
1628
  // (ONNX runtime, etc.) can release mutexes before V8 tears down.
1522
1629
  setImmediate(function() { process.exit(process.exitCode || 0); });
1523
1630
  }).catch(async function(err) {
1524
- console.error('Fatal:', err.message || err);
1631
+ // Commander errors come through exitOverride with the code already set on
1632
+ // process.exitCode. They are user-facing usage errors, not fatals — don't
1633
+ // re-print "Fatal:" / "Unknown error" for them.
1634
+ var isCommanderErr = err && typeof err.code === 'string' && err.code.indexOf('commander.') === 0;
1635
+ if (!isCommanderErr) {
1636
+ console.error('Fatal:', err.message || err);
1637
+ }
1525
1638
  await closeClient();
1526
- process.exit(1);
1639
+ // Use the exit code set by exitOverride (which can legitimately be 0 for
1640
+ // --help / --version). Only fall back to 1 when no code was set.
1641
+ setImmediate(function() {
1642
+ var code = (typeof process.exitCode === 'number') ? process.exitCode : 1;
1643
+ process.exit(code);
1644
+ });
1527
1645
  });`;
1528
1646
  }
1529
1647
  /**