fa-mcp-sdk 0.4.5 → 0.4.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 (153) hide show
  1. package/bin/fa-mcp.js +1040 -1039
  2. package/cli-template/eslint.config.js +16 -136
  3. package/cli-template/package.json +3 -8
  4. package/cli-template/tsconfig.json +1 -0
  5. package/dist/core/_types_/active-directory-config.d.ts.map +1 -1
  6. package/dist/core/_types_/config.d.ts +1 -1
  7. package/dist/core/_types_/config.d.ts.map +1 -1
  8. package/dist/core/_types_/types.d.ts.map +1 -1
  9. package/dist/core/ad/group-checker.d.ts.map +1 -1
  10. package/dist/core/ad/group-checker.js.map +1 -1
  11. package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
  12. package/dist/core/agent-tester/agent-tester-router.js +8 -8
  13. package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
  14. package/dist/core/agent-tester/check-llm.d.ts.map +1 -1
  15. package/dist/core/agent-tester/check-llm.js +1 -1
  16. package/dist/core/agent-tester/check-llm.js.map +1 -1
  17. package/dist/core/agent-tester/services/TesterAgentService.d.ts.map +1 -1
  18. package/dist/core/agent-tester/services/TesterAgentService.js +53 -53
  19. package/dist/core/agent-tester/services/TesterAgentService.js.map +1 -1
  20. package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
  21. package/dist/core/agent-tester/services/TesterMcpClientService.js +2 -2
  22. package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
  23. package/dist/core/auth/admin-auth.d.ts.map +1 -1
  24. package/dist/core/auth/admin-auth.js +3 -3
  25. package/dist/core/auth/admin-auth.js.map +1 -1
  26. package/dist/core/auth/basic.d.ts.map +1 -1
  27. package/dist/core/auth/basic.js.map +1 -1
  28. package/dist/core/auth/jwt.d.ts.map +1 -1
  29. package/dist/core/auth/jwt.js +6 -16
  30. package/dist/core/auth/jwt.js.map +1 -1
  31. package/dist/core/auth/middleware.d.ts.map +1 -1
  32. package/dist/core/auth/middleware.js +3 -2
  33. package/dist/core/auth/middleware.js.map +1 -1
  34. package/dist/core/auth/multi-auth.d.ts +0 -3
  35. package/dist/core/auth/multi-auth.d.ts.map +1 -1
  36. package/dist/core/auth/multi-auth.js +10 -7
  37. package/dist/core/auth/multi-auth.js.map +1 -1
  38. package/dist/core/auth/permanent.d.ts.map +1 -1
  39. package/dist/core/auth/permanent.js +1 -1
  40. package/dist/core/auth/permanent.js.map +1 -1
  41. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.d.ts.map +1 -1
  42. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js +2 -2
  43. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js.map +1 -1
  44. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts.map +1 -1
  45. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js +1 -1
  46. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js.map +1 -1
  47. package/dist/core/auth/token-generator/ntlm/ntlm-integration.d.ts.map +1 -1
  48. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js +1 -1
  49. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js.map +1 -1
  50. package/dist/core/auth/token-generator/ntlm/ntlm-templates.d.ts.map +1 -1
  51. package/dist/core/auth/token-generator/ntlm/ntlm-templates.js +222 -221
  52. package/dist/core/auth/token-generator/ntlm/ntlm-templates.js.map +1 -1
  53. package/dist/core/auth/token-generator/server.d.ts.map +1 -1
  54. package/dist/core/auth/token-generator/server.js +8 -8
  55. package/dist/core/auth/token-generator/server.js.map +1 -1
  56. package/dist/core/bootstrap/init-config.d.ts.map +1 -1
  57. package/dist/core/bootstrap/init-config.js +4 -4
  58. package/dist/core/bootstrap/init-config.js.map +1 -1
  59. package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
  60. package/dist/core/bootstrap/startup-info.js +4 -4
  61. package/dist/core/bootstrap/startup-info.js.map +1 -1
  62. package/dist/core/cache/cache.d.ts.map +1 -1
  63. package/dist/core/cache/cache.js +3 -3
  64. package/dist/core/cache/cache.js.map +1 -1
  65. package/dist/core/consul/access-points-updater.d.ts.map +1 -1
  66. package/dist/core/consul/access-points-updater.js +3 -3
  67. package/dist/core/consul/access-points-updater.js.map +1 -1
  68. package/dist/core/consul/deregister.d.ts.map +1 -1
  69. package/dist/core/consul/deregister.js +1 -1
  70. package/dist/core/consul/deregister.js.map +1 -1
  71. package/dist/core/consul/get-consul-api.d.ts.map +1 -1
  72. package/dist/core/consul/get-consul-api.js +3 -3
  73. package/dist/core/consul/get-consul-api.js.map +1 -1
  74. package/dist/core/db/pg-db.d.ts +1 -1
  75. package/dist/core/db/pg-db.d.ts.map +1 -1
  76. package/dist/core/db/pg-db.js +2 -2
  77. package/dist/core/db/pg-db.js.map +1 -1
  78. package/dist/core/debug.js +1 -1
  79. package/dist/core/debug.js.map +1 -1
  80. package/dist/core/init-mcp-server.d.ts.map +1 -1
  81. package/dist/core/init-mcp-server.js +9 -9
  82. package/dist/core/init-mcp-server.js.map +1 -1
  83. package/dist/core/logger.d.ts.map +1 -1
  84. package/dist/core/logger.js +3 -3
  85. package/dist/core/logger.js.map +1 -1
  86. package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
  87. package/dist/core/mcp/create-mcp-server.js +1 -1
  88. package/dist/core/mcp/create-mcp-server.js.map +1 -1
  89. package/dist/core/mcp/prompts.d.ts.map +1 -1
  90. package/dist/core/mcp/prompts.js +1 -3
  91. package/dist/core/mcp/prompts.js.map +1 -1
  92. package/dist/core/mcp/resources.d.ts.map +1 -1
  93. package/dist/core/mcp/resources.js +8 -10
  94. package/dist/core/mcp/resources.js.map +1 -1
  95. package/dist/core/mcp/server-stdio.d.ts.map +1 -1
  96. package/dist/core/mcp/server-stdio.js.map +1 -1
  97. package/dist/core/utils/formatToolResult.d.ts.map +1 -1
  98. package/dist/core/utils/formatToolResult.js +1 -3
  99. package/dist/core/utils/formatToolResult.js.map +1 -1
  100. package/dist/core/utils/port-checker.d.ts.map +1 -1
  101. package/dist/core/utils/port-checker.js +1 -1
  102. package/dist/core/utils/port-checker.js.map +1 -1
  103. package/dist/core/utils/rate-limit.js +2 -2
  104. package/dist/core/utils/testing/McpSseClient.d.ts.map +1 -1
  105. package/dist/core/utils/testing/McpSseClient.js.map +1 -1
  106. package/dist/core/utils/testing/McpStdioClient.d.ts.map +1 -1
  107. package/dist/core/utils/testing/McpStdioClient.js.map +1 -1
  108. package/dist/core/utils/utils.d.ts.map +1 -1
  109. package/dist/core/utils/utils.js.map +1 -1
  110. package/dist/core/web/admin-router.d.ts.map +1 -1
  111. package/dist/core/web/admin-router.js +4 -4
  112. package/dist/core/web/admin-router.js.map +1 -1
  113. package/dist/core/web/cors.d.ts.map +1 -1
  114. package/dist/core/web/cors.js.map +1 -1
  115. package/dist/core/web/favicon-svg.d.ts.map +1 -1
  116. package/dist/core/web/favicon-svg.js.map +1 -1
  117. package/dist/core/web/home-api.d.ts.map +1 -1
  118. package/dist/core/web/home-api.js +4 -4
  119. package/dist/core/web/home-api.js.map +1 -1
  120. package/dist/core/web/openapi.d.ts.map +1 -1
  121. package/dist/core/web/openapi.js.map +1 -1
  122. package/dist/core/web/server-http.d.ts.map +1 -1
  123. package/dist/core/web/server-http.js +20 -22
  124. package/dist/core/web/server-http.js.map +1 -1
  125. package/dist/core/web/static/agent-tester/script.js +1503 -1513
  126. package/dist/core/web/static/home/script.js +646 -646
  127. package/dist/core/web/static/token-gen/script.js +561 -561
  128. package/dist/core/web/svg-icons.d.ts.map +1 -1
  129. package/dist/core/web/svg-icons.js +1 -1
  130. package/dist/core/web/svg-icons.js.map +1 -1
  131. package/package.json +2 -6
  132. package/scripts/copy-static.js +31 -31
  133. package/scripts/kill-port.js +107 -107
  134. package/scripts/npm/patch_node_modules.js +8 -8
  135. package/scripts/npm/run.js +31 -31
  136. package/scripts/remove-nul.js +53 -53
  137. package/scripts/update-doc.js +18 -18
  138. package/src/template/_types_/custom-config.ts +83 -83
  139. package/src/template/api/router.ts +86 -89
  140. package/src/template/custom-resources.ts +11 -11
  141. package/src/template/prompts/agent-brief.ts +8 -8
  142. package/src/template/prompts/agent-prompt.ts +10 -10
  143. package/src/template/prompts/custom-prompts.ts +12 -12
  144. package/src/template/start.ts +71 -72
  145. package/src/template/tools/handle-tool-call.ts +57 -56
  146. package/src/template/tools/tools.ts +89 -88
  147. package/src/tests/jest-simple-reporter.js +10 -10
  148. package/src/tests/mcp/sse/test-sse-npm-package.js +96 -96
  149. package/src/tests/mcp/test-cases.js +143 -143
  150. package/src/tests/mcp/test-http.js +76 -75
  151. package/src/tests/mcp/test-sse.js +80 -79
  152. package/src/tests/mcp/test-stdio.js +83 -81
  153. package/src/tests/utils.ts +157 -156
package/bin/fa-mcp.js CHANGED
@@ -1,1039 +1,1040 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs/promises';
4
- import fss from 'fs';
5
- import path from 'path';
6
- import { fileURLToPath } from 'url';
7
- import readline from 'readline';
8
- import { v4 as uuidv4 } from 'uuid';
9
- import chalk from 'chalk';
10
- import yaml from 'js-yaml';
11
-
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = path.dirname(__filename);
14
- const PRINT_FILLED = true;
15
- const PROJ_ROOT = path.join(__dirname, '..');
16
-
17
- const hl = (v) => chalk.bgGreen.black(v);
18
- const hly = (v) => chalk.bgYellow.black(v);
19
- const hp = (paramName) => ` [${chalk.magenta(paramName)}]`;
20
- const formatDefaultValue = (v) => (v ? ` (default: ${hl(v)})` : '');
21
- const OPTIONAL = chalk.gray(' (optional)');
22
- const FROM_CONFIG = chalk.gray(' (from config)');
23
- const trim = (s) => String(s || '').trim();
24
-
25
- const printFilled = (paramName, paramValue) => {
26
- if (PRINT_FILLED) {
27
- console.log(` ${hp(paramName)}: ${hl(paramValue)}`);
28
- }
29
- };
30
-
31
- const pjContent = fss.readFileSync(path.join(PROJ_ROOT, 'package.json'));
32
-
33
- const faMcpSdkVersion = JSON.parse(pjContent).version;
34
-
35
- // Print version and exit on -V or --version
36
- const argv = process.argv.slice(2);
37
- if (argv.includes('-V') || argv.includes('--version')) {
38
- console.log(faMcpSdkVersion);
39
- process.exit(0);
40
- }
41
-
42
- const ALLOWED_FILES = [
43
- '.git',
44
- '.idea',
45
- '.vscode',
46
- '.swp',
47
- '.swo',
48
- '.DS_Store',
49
- '.sublime-project',
50
- '.sublime-workspace',
51
- 'node_modules',
52
- 'dist',
53
- '__misc',
54
- '_tmp',
55
- '~last-cli-config.json',
56
- 'yarn.lock',
57
- ];
58
-
59
- const getAsk = () => {
60
- const rl = readline.createInterface({
61
- input: process.stdin,
62
- output: process.stdout,
63
- });
64
-
65
- const yn_ = (prompt, defaultAnswer = 'y') => new Promise((resolve) => {
66
- rl.question(prompt, (v) => {
67
- resolve((trim(v) || defaultAnswer).toLowerCase());
68
- });
69
- });
70
-
71
- return {
72
- close: rl.close.bind(rl),
73
-
74
- question: (prompt) => new Promise(resolve => {
75
- rl.question(prompt, resolve);
76
- }),
77
-
78
- optional: (title, paramName, defaultValue, example = undefined) => new Promise(resolve => {
79
- const defaultText = formatDefaultValue(defaultValue);
80
- example = example ? ` (example: ${example})` : '';
81
- const prompt = `${title}${hp(paramName)}${defaultText}${example}${OPTIONAL}: `;
82
- rl.question(prompt, (v) => {
83
- resolve(trim(v) || trim(defaultValue));
84
- });
85
- }),
86
-
87
- yn: async (title, paramName, defaultValue = 'false') => {
88
- const isTrue = /^(true|y)$/i.test(trim(defaultValue));
89
- const y = isTrue ? `${hl('y')}` : 'y';
90
- const n = isTrue ? 'n' : `${hl('n')}`;
91
-
92
- const hpn = paramName ? hp(paramName) : '';
93
- const prompt = `${title}${hpn} (${y}/${n}): `;
94
- while (true) {
95
- const answer = await yn_(prompt, defaultValue === 'true' ? 'y' : 'n');
96
- if (answer === 'y' || answer === 'n') {
97
- return answer === 'y';
98
- }
99
- console.log(chalk.red('⚠️ Please enter "y" for yes or "n" for no.'));
100
- }
101
- },
102
- };
103
- };
104
-
105
- /**
106
- * Parse configuration file (JSON or YAML)
107
- * @param {string} filePath - Path to the configuration file
108
- * @param {string} content - Content of the file
109
- * @returns {object} Parsed configuration object
110
- */
111
- const parseConfigFile = (filePath, content) => {
112
- const ext = path.extname(filePath).toLowerCase();
113
-
114
- try {
115
- if (ext === '.json') {
116
- return JSON.parse(content);
117
- } else if (ext === '.yaml' || ext === '.yml') {
118
- return yaml.load(content, { schema: yaml.DEFAULT_SCHEMA });
119
- } else {
120
- // Try to detect format by content
121
- const trimmed = content.trim();
122
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
123
- return JSON.parse(content);
124
- } else {
125
- return yaml.load(content, { schema: yaml.DEFAULT_SCHEMA });
126
- }
127
- }
128
- } catch (error) {
129
- throw new Error(`Failed to parse configuration file ${filePath}: ${error.message}`);
130
- }
131
- };
132
-
133
- const removeIfExists = async (targetPath, relPath, options = {}) => {
134
- const fullPath = path.join(targetPath, relPath);
135
-
136
- try {
137
- let finalOptions = { force: true, ...options };
138
-
139
- try {
140
- const stat = await fs.lstat(fullPath);
141
- if (stat.isDirectory() && finalOptions.recursive === undefined) {
142
- finalOptions = { ...finalOptions, recursive: true };
143
- }
144
- } catch {
145
- // lstat will crash if there is no file/folder – that's ok, just go to rm with the same options
146
- }
147
-
148
- await fs.rm(fullPath, finalOptions);
149
- } catch {
150
- // ignore any deletion errors
151
- }
152
- };
153
-
154
- class MCPGenerator {
155
- constructor () {
156
- this.lastConfigPath = path.join(process.cwd(), '~last-cli-config.json');
157
- this.requiredParams = [
158
- {
159
- name: 'project.name',
160
- defaultValue: '',
161
- title: 'Project name for package.json and MCP server identification',
162
- },
163
- {
164
- name: 'project.description',
165
- defaultValue: '',
166
- title: 'Project description for package.json',
167
- },
168
- {
169
- name: 'project.productName',
170
- defaultValue: '',
171
- title: 'Product name displayed in UI and documentation',
172
- },
173
- {
174
- name: 'port',
175
- defaultValue: '3000',
176
- title: 'Web server port for HTTP endpoints and MCP protocol',
177
- },
178
- ];
179
-
180
- this.optionalParams = [
181
- {
182
- name: 'author.name',
183
- defaultValue: '',
184
- title: 'Author name for package.json',
185
- },
186
- {
187
- name: 'author.email',
188
- defaultValue: '',
189
- title: 'Author email for package.json',
190
- },
191
-
192
- {
193
- name: 'git-base-url',
194
- defaultValue: 'github.com/username',
195
- title: 'Git repository base URL',
196
- },
197
- {
198
- name: 'consul.agent.dev.dc',
199
- defaultValue: '',
200
- title: 'Development Consul Datacenter to search for services',
201
- },
202
- {
203
- name: 'consul.agent.dev.host',
204
- defaultValue: 'consul.my.ui',
205
- title: 'Development Consul UI host',
206
- },
207
- {
208
- name: 'consul.agent.dev.token',
209
- defaultValue: '***',
210
- title: 'Token for accessing Development Consul Datacenter',
211
- },
212
- {
213
- name: 'consul.agent.prd.dc',
214
- defaultValue: '',
215
- title: 'Production Consul Datacenter to search for services',
216
- },
217
- {
218
- name: 'consul.agent.prd.host',
219
- defaultValue: 'consul.my.ui',
220
- title: 'Production Consul UI host',
221
- },
222
- {
223
- name: 'consul.agent.prd.token',
224
- defaultValue: '***',
225
- title: 'Token for accessing Production Consul Datacenter',
226
- },
227
- // Register in Consul
228
- {
229
- name: 'consul.service.enable',
230
- defaultValue: 'false',
231
- title: 'Whether to register service in Consul',
232
- },
233
- {
234
- name: 'consul.agent.reg.token',
235
- defaultValue: '***',
236
- title: 'Token for registering service with Consul agent',
237
- },
238
- {
239
- name: 'consul.agent.reg.host',
240
- defaultValue: '',
241
- title: 'The host of the consul agent where the service will be registered',
242
- },
243
- {
244
- name: 'consul.envCode.dev',
245
- defaultValue: '<envCode.dev>',
246
- title: 'Development environment code for Consul service ID generation',
247
- },
248
- {
249
- name: 'consul.envCode.prod',
250
- defaultValue: '<envCode.prod>',
251
- title: 'Production environment code for Consul service ID generation',
252
- },
253
- {
254
- name: 'NODE_CONSUL_ENV',
255
- defaultValue: '',
256
- title: 'Affects how the Consul service ID is formed - as a product or development ID. Valid values: "" | "development" | "production"',
257
- },
258
-
259
- {
260
- name: 'mcp.domain',
261
- defaultValue: '',
262
- title: 'Domain name for nginx configuration',
263
- },
264
- {
265
- name: 'ssl-wildcard.conf.rel.path',
266
- defaultValue: 'snippets/ssl-wildcard.conf',
267
- title: `The relative path to the nginx configuration file
268
- in the /etc/nginx folder that specifies the SSL
269
- certificate's public and private keys`,
270
- },
271
-
272
- {
273
- name: 'webServer.auth.enabled',
274
- defaultValue: 'false',
275
- title: 'Whether to enable authorization by token in the MCP server',
276
- },
277
- {
278
- skip: true,
279
- name: 'webServer.auth.token.encryptKey',
280
- defaultValue: '***',
281
- title: 'Encryption key for MCP tokens',
282
- },
283
- {
284
- name: 'webServer.auth.token.checkMCPName',
285
- defaultValue: 'false',
286
- title: 'Whether to check MCP name in the token',
287
- },
288
- {
289
- skip: true,
290
- name: 'projectAbsPath',
291
- },
292
- {
293
- title: 'Is it Production mode',
294
- defaultValue: 'false',
295
- name: 'isProduction',
296
- },
297
- {
298
- skip: true,
299
- name: 'NODE_ENV',
300
- },
301
- {
302
- name: 'SERVICE_INSTANCE',
303
- defaultValue: '',
304
- title: 'Suffix of the service name in Consul and process manager',
305
- },
306
- {
307
- skip: true,
308
- name: 'PM2_NAMESPACE',
309
- },
310
- {
311
- name: 'maintainerUrl',
312
- defaultValue: '',
313
- title: 'Maintainer url',
314
- },
315
- {
316
- name: 'logger.useFileLogger',
317
- defaultValue: '',
318
- title: 'Whether to use file logger',
319
- },
320
- {
321
- skip: true,
322
- name: 'logger.dir',
323
- defaultValue: '',
324
- title: 'Absolute path to the folder where logs will be written',
325
- },
326
- {
327
- name: 'claude.isBypassPermissions',
328
- defaultValue: 'false',
329
- title: 'Enable GOD Mode for Claude Code',
330
- },
331
- ];
332
- }
333
-
334
- createConfigProxy (config) {
335
- const lastConfigPath = this.lastConfigPath; // Capture this in closure
336
-
337
- return new Proxy(config, {
338
- set (target, prop, value, receiver) {
339
- // Check if the value is actually changing
340
- const currentValue = target[prop];
341
- if (currentValue === value) {
342
- return Reflect.set(target, prop, value, receiver);
343
- }
344
- // Regular assignment behavior first
345
- const result = Reflect.set(target, prop, value, receiver);
346
- // Save to file asynchronously without blocking
347
- fs.writeFile(lastConfigPath, JSON.stringify(target, null, 2), 'utf8')
348
- .catch(error => console.warn('⚠️ Warning: Could not save config to file:', error.message));
349
- return result;
350
- },
351
- });
352
- }
353
-
354
- async collectConfigData (config, isRetry = false) {
355
- const ask = getAsk();
356
- // Collect required parameters
357
- for (const param of this.requiredParams) {
358
- const { title, name } = param;
359
- const currentValue = config[name];
360
- const defaultValue = currentValue || param.defaultValue;
361
-
362
- // Skip if already has value and not in retry mode
363
- if (currentValue && !isRetry) {
364
- printFilled(name, currentValue);
365
- continue;
366
- }
367
-
368
- let value;
369
- const defaultText = formatDefaultValue(defaultValue);
370
-
371
- // Keep asking until we get a valid value for required parameters
372
- while (true) {
373
- switch (name) {
374
- case 'port':
375
- value = await ask.question(`Enter port${defaultText}: `);
376
- value = value || defaultValue;
377
- break;
378
- default:
379
- value = await ask.question(`Enter ${title} [${name}]${defaultText}: `);
380
- value = value.trim() || defaultValue;
381
- }
382
-
383
- // Check if we have a valid value
384
- if (value && value.trim()) {
385
- break; // Exit the loop - we have a valid value
386
- }
387
-
388
- // If no default value and no input, continue asking
389
- if (!defaultValue) {
390
- console.error(chalk.red(`⚠️ ${name} is required. Please enter a value.`));
391
- continue;
392
- }
393
-
394
- // Should not reach here, but just in case
395
- break;
396
- }
397
-
398
- config[name] = value;
399
- }
400
-
401
- // Self register service in consul
402
- {
403
- const param = this.optionalParams.find((p) => p.name === 'consul.service.enable');
404
- const { title, name } = param;
405
- const currentValue = config[name];
406
- const defaultValue = currentValue || param.defaultValue;
407
- let shouldSkipConsulRegisterParams = currentValue === 'false';
408
-
409
- if (currentValue && !isRetry) {
410
- printFilled(name, currentValue);
411
- } else {
412
- const enabled = await ask.yn(title, name, defaultValue);
413
- config[name] = String(enabled);
414
- shouldSkipConsulRegisterParams = !enabled;
415
- }
416
-
417
- // If consul registration is enabled, collect consul parameters immediately
418
- if (!shouldSkipConsulRegisterParams) {
419
- const consulParams = this.optionalParams.filter(({ name: n }) => n === 'consul.agent.reg.token' || n.startsWith('consul.envCode.'));
420
- for (const param of consulParams) {
421
- const { title, name } = param;
422
- const currentValue = config[name];
423
- const defaultValue = currentValue || param.defaultValue;
424
-
425
- // Skip if already has value and not in retry mode
426
- if (currentValue && !isRetry) {
427
- printFilled(name, currentValue);
428
- continue;
429
- }
430
-
431
- const value = await ask.question(`${title} [${name}]${formatDefaultValue(defaultValue)}${OPTIONAL}: `);
432
- config[name] = trim(value) || defaultValue;
433
- }
434
- }
435
- }
436
- // Other consul parameters
437
- const consulParams = this.optionalParams.filter(({ name: n }) => n.startsWith('consul.agent.dev') || n.startsWith('consul.agent.prd'));
438
- for (const param of consulParams) {
439
- const { title, name } = param;
440
- const currentValue = config[name];
441
- if (currentValue && !isRetry) {
442
- printFilled(name, currentValue);
443
- continue;
444
- }
445
- const defaultValue = currentValue || param.defaultValue;
446
- config[name] = await ask.optional(title, name, defaultValue);
447
- }
448
-
449
- // Handle webServer.auth.enabled to determine if auth parameters are needed
450
- {
451
- const param = this.optionalParams.find((p) => p.name === 'webServer.auth.enabled');
452
- const { title, name } = param;
453
- const currentValue = config[name];
454
- let shouldSkipAuthParams = currentValue === 'false';
455
-
456
- if (currentValue && !isRetry) {
457
- printFilled(name, currentValue);
458
- } else {
459
- const enabled = await ask.yn(title, name, currentValue || param.defaultValue);
460
- config[name] = String(enabled);
461
- shouldSkipAuthParams = !enabled;
462
- }
463
-
464
- // Generate encrypt key if auth is enabled
465
- if (!shouldSkipAuthParams) {
466
- config['webServer.auth.token.encryptKey'] = uuidv4();
467
- }
468
-
469
- // If authentication is enabled, collect auth parameters immediately
470
- if (!shouldSkipAuthParams) {
471
- const authParams = this.optionalParams.filter((p) => p.name.startsWith('webServer.auth.') && p.name !== 'webServer.auth.enabled');
472
-
473
- for (const param of authParams) {
474
- const { title, name, skip } = param;
475
- const currentValue = config[name];
476
- const defaultValue = currentValue || param.defaultValue;
477
-
478
- // Skip if already has value and not in retry mode
479
- if (currentValue && !isRetry) {
480
- printFilled(name, currentValue);
481
- continue;
482
- }
483
- if (skip) {
484
- if (name === 'webServer.auth.token.encryptKey') {
485
- config[name] = uuidv4();
486
- }
487
- continue;
488
- }
489
- switch (name) {
490
- case 'webServer.auth.token.checkMCPName':
491
- config[name] = String(await ask.yn(title, name, defaultValue));
492
- break;
493
- default:
494
- config[name] = await ask.optional(title, name, defaultValue);
495
- }
496
- }
497
- }
498
- }
499
-
500
- // Collect optional parameters
501
- for (const param of this.optionalParams) {
502
- const { title, name, skip } = param;
503
- // Skip already processed parameters
504
- if (name.startsWith('consul.') || name.startsWith('webServer.auth.')) {
505
- continue;
506
- }
507
-
508
- const currentValue = config[name];
509
- const defaultValue = currentValue || param.defaultValue;
510
-
511
- // Skip if already has value and not in retry mode
512
- if (currentValue && !isRetry) {
513
- printFilled(name, currentValue);
514
-
515
- if (name === 'mcp.domain' && !config['upstream']) {
516
- config['upstream'] = currentValue.replace(/\./g, '-');
517
- }
518
-
519
- continue;
520
- }
521
- if (skip) {
522
- continue;
523
- }
524
-
525
- let value;
526
- switch (name) {
527
- case 'git-base-url':
528
- value = await ask.optional(title, name, defaultValue, 'github.com/username OR gitlab.company.com/PROJECT');
529
- value = value.trim() || defaultValue;
530
- break;
531
- case 'author.email': {
532
- let go = true;
533
- while (go) {
534
- value = await ask.optional(title, name, defaultValue);
535
- if (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/im.test(value)) {
536
- go = false;
537
- } else {
538
- console.log(chalk.red('⚠️ Please enter valid email or leave empty.'));
539
- }
540
- }
541
- break;
542
- }
543
-
544
- case 'mcp.domain':
545
- value = await ask.optional(title, name, defaultValue);
546
- if (value) {
547
- // Auto-generate upstream from mcp.domain by replacing dots with dashes
548
- config.upstream = value.replace(/\./g, '-');
549
- }
550
- continue;
551
-
552
- case 'maintainerUrl':
553
- value = await ask.optional(title, name, defaultValue);
554
- if (value) {
555
- config.maintainerHtml = `<a href="${value}" target="_blank" rel="noopener" class="clickable">Support</a>`;
556
- }
557
- continue;
558
- case 'logger.useFileLogger': {
559
- const enabled = await ask.yn(title, name, defaultValue);
560
- config[name] = String(enabled);
561
- const nm = 'logger.dir';
562
- if (enabled) {
563
- const p = this.optionalParams.find(({ name: n }) => n === nm);
564
- value = await ask.optional(p.title, nm, config[nm] || p.defaultValue);
565
- if (value) {
566
- config[nm] = value;
567
- }
568
- } else {
569
- config[nm] = '';
570
- }
571
- continue;
572
- }
573
- case 'isProduction':
574
- case 'claude.isBypassPermissions': {
575
- const enabled = await ask.yn(title, name, defaultValue);
576
- config[name] = String(enabled);
577
- continue;
578
- }
579
- case 'NODE_CONSUL_ENV': {
580
- if (currentValue === '') {
581
- continue;
582
- }
583
- value = await ask.optional(title, name, defaultValue);
584
- if (value === '' || value === 'development' || value === 'production') {
585
- config[name] = value;
586
- } else {
587
- config[name] = '';
588
- }
589
- continue;
590
- }
591
-
592
- default:
593
- value = await ask.optional(title, name, defaultValue);
594
- }
595
-
596
- if (value) {
597
- config[name] = value;
598
- }
599
- }
600
- ask.close();
601
- }
602
-
603
- async confirmConfiguration (config) {
604
- console.log('\n📋 Configuration Summary:');
605
- console.log('========================');
606
-
607
- // Show all parameters
608
- const allParams = [...this.requiredParams, ...this.optionalParams];
609
- for (const param of allParams) {
610
- const value = config[param.name];
611
- if (value !== undefined) {
612
- console.log(` ${param.name}: ${hl(value)}`);
613
- }
614
- }
615
-
616
- const ask = getAsk();
617
- let confirmed;
618
- const use = config.forceAcceptConfig;
619
- // Check for automatic answer from config
620
- if (use === 'y' || use === 'n') {
621
- confirmed = use === 'y';
622
- console.log(`\nUse this configuration: ${hl('y')}es${FROM_CONFIG}`);
623
- } else {
624
- confirmed = await ask.yn('\nUse this configuration?', '', 'y');
625
- }
626
- if (confirmed) {
627
- config.forceAcceptConfig = 'y';
628
- } else {
629
- delete config.forceAcceptConfig;
630
- }
631
- ask.close();
632
-
633
- return confirmed;
634
- }
635
-
636
- async collectConfiguration () {
637
- const config = {};
638
- const configFile = process.argv.find((arg) => arg.endsWith('.json') || arg.endsWith('.yaml') || arg.endsWith('.yml')) ||
639
- process.argv.find((arg) => arg.startsWith('--config='))?.split('=')[1];
640
-
641
- if (configFile) {
642
- try {
643
- const configData = await fs.readFile(configFile, 'utf8');
644
- const parsedConfig = parseConfigFile(configFile, configData);
645
- Object.assign(config, parsedConfig);
646
- console.log(`📋 Loaded configuration from: ${hly(configFile)}`);
647
- } catch (error) {
648
- console.warn(`⚠️ Warning: Could not load config file ${configFile}: ${error.message}`);
649
- }
650
- }
651
-
652
- // Create proxy for automatic saving before starting data collection
653
- const configProxy = this.createConfigProxy(config);
654
-
655
- // Save initial state if there's any pre-loaded config
656
- if (Object.keys(config).length > 0) {
657
- try {
658
- await fs.writeFile(this.lastConfigPath, JSON.stringify(config, null, 2), 'utf8');
659
- } catch (error) {
660
- console.warn('⚠️ Warning: Could not save initial config to file:', error.message);
661
- }
662
- }
663
-
664
- if (configProxy.NODE_ENV === 'development') {
665
- configProxy.isProduction = 'false';
666
- } else if (configProxy.NODE_ENV === 'production') {
667
- configProxy.isProduction = 'true';
668
- }
669
- if (config['logger.useFileLogger'] !== 'true') {
670
- config['logger.dir'] = '';
671
- }
672
- let confirmed = false;
673
- let isRetry = false;
674
-
675
- // Loop until configuration is confirmed
676
- while (!confirmed) {
677
- await this.collectConfigData(configProxy, isRetry);
678
-
679
- // Set NODE_ENV and PM2_NAMESPACE based on isProduction
680
- config.NODE_ENV = config.isProduction === 'true' ? 'production' : 'development';
681
- config.PM2_NAMESPACE = config.isProduction === 'true' ? 'prod' : 'dev';
682
- config.SERVICE_INSTANCE = config.PM2_NAMESPACE;
683
-
684
- confirmed = await this.confirmConfiguration(config);
685
-
686
- if (!confirmed) {
687
- console.log('\n🔄 Let\'s re-enter the configuration:\n');
688
- isRetry = true;
689
- }
690
- }
691
-
692
- return config;
693
- }
694
-
695
- async getTargetPath (config = {}) {
696
- const ask = getAsk();
697
-
698
- let tp = process.cwd();
699
- let createInCurrent;
700
- let pPath = trim(config.projectAbsPath);
701
- if (pPath) {
702
- tp = path.resolve(pPath);
703
- console.log(`Create project in: ${hl(tp)}${FROM_CONFIG}`);
704
- } else {
705
- createInCurrent = await ask.yn(`Create project in current directory? (${hl(tp)})`, '', 'n');
706
- if (!createInCurrent) {
707
- tp = await ask.question('Enter absolute path for project: ');
708
- tp = path.resolve(tp);
709
- }
710
- }
711
-
712
- config.projectAbsPath = tp;
713
- // Create directory if it doesn't exist
714
- try {
715
- await fs.access(tp);
716
- } catch {
717
- console.log('Creating directory recursively...');
718
- await fs.mkdir(tp, { recursive: true });
719
- }
720
-
721
- const errMsg = `❌ Directory ${hl(tp)} not empty - cannot create project here. Use an empty directory or specify a different path.`;
722
-
723
- // Check if directory is empty
724
- try {
725
- const files = await fs.readdir(tp);
726
- const firstDeprecatedFile = files.find((file) => !ALLOWED_FILES.includes(file));
727
-
728
- if (firstDeprecatedFile) {
729
- console.error(errMsg);
730
- console.error(` First deprecated file: ${hl(firstDeprecatedFile)}`);
731
- process.exit(1);
732
- }
733
- } catch (error) {
734
- if (error.message.includes('Directory not empty')) {
735
- console.error(errMsg);
736
- process.exit(1);
737
- }
738
- throw new Error(`Cannot access directory: ${error.message}`);
739
- }
740
-
741
- ask.close();
742
- return tp;
743
- }
744
-
745
- async copyDirectory (source, target) {
746
- const entries = await fs.readdir(source, { withFileTypes: true });
747
- if (!fss.existsSync(target)) {
748
- await fs.mkdir(target, { recursive: true });
749
- }
750
-
751
- for (const entry of entries) {
752
- if (entry.name === 'node_modules' || entry.name === 'dist') {
753
- continue; // Skip node_modules & dist directories
754
- }
755
-
756
- const sourcePath = path.join(source, entry.name);
757
- const targetPath = path.join(target, entry.name);
758
-
759
- if (entry.isDirectory()) {
760
- await fs.mkdir(targetPath, { recursive: true });
761
- await this.copyDirectory(sourcePath, targetPath);
762
- } else {
763
- await fs.copyFile(sourcePath, targetPath);
764
- }
765
- }
766
- }
767
-
768
- async handlePackageJson (content, config) {
769
- try {
770
- content = content
771
- .replace(/"project\.name"/g, '"{{project.name}}"')
772
- .replace(/"node \.\.\/scripts/g, '"node ./scripts');
773
- // First replace all template parameters in the content string
774
- let updatedContent = content;
775
- for (const [param, value] of Object.entries(config)) {
776
- const template = `{{${param}}}`;
777
- if (updatedContent.includes(template)) {
778
- updatedContent = updatedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
779
- }
780
- }
781
-
782
- // Now parse the updated content and handle author fields
783
- const packageJson = JSON.parse(updatedContent);
784
- const authorName = config['author.name'];
785
- const authorEmail = config['author.email'];
786
- // Handle optional author fields
787
- if (!authorName && !authorEmail) {
788
- delete packageJson.author;
789
- } else {
790
- if (!packageJson.author) {packageJson.author = {};}
791
- if (authorName) {
792
- packageJson.author.name = authorName;
793
- }
794
- if (authorEmail) {
795
- packageJson.author.email = authorEmail;
796
- }
797
- // Remove empty author object if no fields
798
- if (Object.keys(packageJson.author).length === 0) {
799
- delete packageJson.author;
800
- }
801
- }
802
-
803
- packageJson.dependencies['fa-mcp-sdk'] = `^${faMcpSdkVersion}`;
804
-
805
- if (!config.keepPostinstall) {
806
- delete packageJson.scripts.postinstall;
807
- }
808
-
809
- return JSON.stringify(packageJson, null, 2);
810
- } catch (error) {
811
- throw new Error(`Error processing package.json: ${error.message}`);
812
- }
813
- }
814
-
815
- async getAllFiles (dir, skipRootDirs) {
816
- const files = [];
817
- const entries = await fs.readdir(dir, { withFileTypes: true });
818
-
819
- for (const entry of entries) {
820
- if (skipRootDirs && skipRootDirs.includes(entry.name)) {
821
- continue;
822
- }
823
- const fullPath = path.join(dir, entry.name);
824
-
825
- if (entry.isDirectory()) {
826
- files.push(...await this.getAllFiles(fullPath));
827
- } else {
828
- files.push(fullPath);
829
- }
830
- }
831
-
832
- return files;
833
- }
834
-
835
- async transformTargetFile (config, targetRelPath, transformFn) {
836
- const targetPath = config.projectAbsPath;
837
- const targetFullPath = path.join(targetPath, targetRelPath);
838
- const content = await fs.readFile(targetFullPath, 'utf8');
839
- const transformedContent = transformFn(content, config);
840
- await fs.writeFile(targetFullPath, transformedContent, 'utf8');
841
- }
842
-
843
- async replaceTemplateParameters (config) {
844
- const targetPath = config.projectAbsPath;
845
- const files = await this.getAllFiles(targetPath, ALLOWED_FILES);
846
- const importRe = /'[^']+\/core\/index.js'/ig;
847
- for (const filePath of files) {
848
- let content = await fs.readFile(filePath, 'utf8');
849
- let modified = false;
850
-
851
- // Special handling for package.json
852
- if (filePath.endsWith('package.json')) {
853
- content = await this.handlePackageJson(content, config);
854
- modified = true;
855
- } else {
856
- // Replace all template parameters
857
- for (const [param, value] of Object.entries(config)) {
858
- const template = `{{${param}}}`;
859
- if (content.includes(template)) {
860
- content = content.replace(new RegExp(escapeRegExp(template), 'g'), value);
861
- modified = true;
862
- }
863
- }
864
- if (importRe.test(content)) {
865
- content = content.replace(importRe, '\'fa-mcp-sdk\'');
866
- modified = true;
867
- }
868
- }
869
- if (filePath.endsWith('test-sse-npm-package.js')) {
870
- content = content.replace(/http:\/\/localhost:9876/g, `http://localhost:${config.port}`);
871
- modified = true;
872
- }
873
- if (filePath.endsWith('test-stdio.js')) {
874
- content = content.replace('../dist/template/start.js', 'dist/src/start.js');
875
- modified = true;
876
- }
877
-
878
- if (modified) {
879
- await fs.writeFile(filePath, content, 'utf8');
880
- }
881
- }
882
- if (config['NODE_CONSUL_ENV'] === '') {
883
- await this.transformTargetFile(config, '.env', (c) => c.replace(/^(NODE_CONSUL_ENV)=([^\r\n]*)/m, '#$1=$2'));
884
- }
885
- if (config['claude.isBypassPermissions'] === 'true') {
886
- const c1 = ['sudo cp', 'sudo', 'bash', 'chmod', 'curl', 'dir', 'echo', 'git', 'find', 'grep', 'jest',
887
- 'mkdir', 'node', 'npm install', 'npm run', 'npm test', 'npm', 'npx', 'pkill', 'set', 'playwright', 'powershell',
888
- 'rm', 'taskkill', 'tasklist', 'timeout', 'turbo run', 'wc'];
889
- const c2 = ['jobs', 'npm start', 'unset http_proxy'];
890
- const i = ' '.repeat(8);
891
- const allowBashLines = [...c1.map((c) => `${i}"Bash(${c}:*)",`), ...c2.map((c) => `${i}"Bash(${c})",`)].join('\n');
892
- const transformFn = (c) => c.replace('"acceptEdits"', '"bypassPermissions"')
893
- .replace(/"allow": \[\s+"Edit",/, `"allow": [\n${allowBashLines}\n${i}"Edit",`);
894
- await this.transformTargetFile(config, '.claude/settings.json', transformFn);
895
- }
896
- }
897
-
898
- async createProject (config) {
899
- const targetPath = config.projectAbsPath;
900
- // Copy template files
901
- await this.copyDirectory(path.join(PROJ_ROOT, 'cli-template'), targetPath);
902
- await this.copyDirectory(path.join(PROJ_ROOT, 'src/template'), path.join(targetPath, 'src'));
903
-
904
- const testsTargetPath = path.join(targetPath, 'tests');
905
-
906
- await this.copyDirectory(path.join(PROJ_ROOT, 'src/tests'), testsTargetPath);
907
- await fs.copyFile(path.join(targetPath, '.env.example'), path.join(targetPath, '.env'));
908
- await fs.rename(path.join(targetPath, 'gitignore'), path.join(targetPath, '.gitignore'));
909
- await fs.rename(path.join(targetPath, 'r'), path.join(targetPath, '.run'));
910
-
911
- await this.copyDirectory(path.join(PROJ_ROOT, 'config'), path.join(targetPath, 'config'));
912
-
913
- const scriptsTargetPath = path.join(targetPath, 'scripts');
914
- await this.copyDirectory(path.join(PROJ_ROOT, 'scripts'), scriptsTargetPath);
915
- await fs.rm(path.join(targetPath, 'scripts/copy-static.js'), { force: true });
916
- await fs.rm(path.join(targetPath, 'scripts/publish.sh'), { force: true });
917
-
918
- // Rename all .xml files in .run directory to .run.xml
919
- const runDirPath = path.join(targetPath, '.run');
920
- const files = await fs.readdir(runDirPath);
921
-
922
- for (const file of files) {
923
- if (file.endsWith('.xml')) {
924
- const oldFilePath = path.join(runDirPath, file);
925
- const newFileName = file.slice(0, -4) + '.run.xml';
926
- const newFilePath = path.join(runDirPath, newFileName);
927
- await fs.rename(oldFilePath, newFilePath);
928
- }
929
- }
930
-
931
- // Rename mcp-template.com.conf if mcp.domain is provided
932
- const mcpDomain = config['mcp.domain'];
933
- if (mcpDomain) {
934
- try {
935
- const oldConfigPath = path.join(targetPath, 'deploy/NGINX/sites-enabled/mcp-template.com.conf');
936
- const newConfigPath = path.join(targetPath, 'deploy/NGINX/sites-enabled', `${mcpDomain}.conf`);
937
-
938
- await fs.access(oldConfigPath);
939
- await fs.rename(oldConfigPath, newConfigPath);
940
- } catch (error) {
941
- // File doesn't exist or rename failed, which is not critical
942
- console.log('⚠️ Warning: Could not rename mcp-template.com.conf file', error);
943
- }
944
- }
945
-
946
- // Read _local.yaml into memory and rename it to local.yaml
947
- let localYamlExampleContent = '';
948
- const localYamlExamplePath = path.join(targetPath, 'config', '_local.yaml');
949
- const localYamlPath = path.join(targetPath, 'config', 'local.yaml');
950
- try {
951
-
952
- localYamlExampleContent = await fs.readFile(localYamlExamplePath, 'utf8');
953
- } catch (error) {
954
- console.log('⚠️ Warning: Could not process config/_local.yaml file:', error.message);
955
- }
956
-
957
- // Replace template parameters
958
- await this.replaceTemplateParameters(config);
959
-
960
- // Replace template placeholders with defaultValue from optionalParams and save as _local.yaml
961
- if (localYamlExampleContent) {
962
- try {
963
- let localYamlExampleModifiedContent = localYamlExampleContent;
964
- let localYamlModifiedContent = localYamlExampleContent;
965
- // Replace with defaultValue from optionalParams
966
- for (const param of this.optionalParams) {
967
- const template = `{{${param.name}}}`;
968
- if (localYamlExampleModifiedContent.includes(template)) {
969
- const defaultValue = param.defaultValue || '';
970
- localYamlExampleModifiedContent = localYamlExampleModifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), defaultValue);
971
- }
972
- }
973
-
974
- // Replacement of the remaining substitution places with what is in the config
975
- for (const [paramName, value] of Object.entries(config)) {
976
- const template = `{{${paramName}}}`;
977
- if (localYamlExampleModifiedContent.includes(template)) {
978
- localYamlExampleModifiedContent = localYamlExampleModifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
979
- }
980
- if (localYamlModifiedContent.includes(template)) {
981
- localYamlModifiedContent = localYamlModifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
982
- }
983
- }
984
- if (!config['consul.agent.reg.host']) {
985
- localYamlModifiedContent = localYamlModifiedContent.replace(/(\n +)host: '[^']*'( # The host of the consul agent)/, '$1# host: \'\'$2');
986
- }
987
-
988
- await fs.writeFile(localYamlPath, localYamlModifiedContent, 'utf8');
989
- await fs.writeFile(localYamlExamplePath, localYamlExampleModifiedContent, 'utf8');
990
- } catch (error) {
991
- console.log('⚠️ Warning: Could not create config/_local.yaml file:', error.message);
992
- }
993
- }
994
- const pathsToRemove = [
995
- { rel: 'package-lock.json' },
996
- ];
997
-
998
- await Promise.all(
999
- pathsToRemove.map(({ rel, options }) => removeIfExists(targetPath, rel, options)),
1000
- );
1001
- }
1002
-
1003
- async run () {
1004
- console.log('MCP Server Template Generator');
1005
- console.log('==================================\n');
1006
-
1007
- try {
1008
- const config = await this.collectConfiguration();
1009
- const targetPath = await this.getTargetPath(config);
1010
-
1011
- console.log(`\n📁 Creating project in: ${targetPath}`);
1012
- await this.createProject(config);
1013
-
1014
- console.log('\n✅ MCP Server template created successfully!');
1015
- console.log('\n📋 Next steps:');
1016
- console.log(` cd ${targetPath}`);
1017
- console.log(' npm install');
1018
- console.log(' npm run build');
1019
- console.log(' npm start');
1020
-
1021
- process.exit(0);
1022
-
1023
- } catch (error) {
1024
- if (error.message && !(error.stack || '').includes(String(error.message))) {
1025
- console.error('\n❌ Error:', error.message);
1026
- }
1027
- console.error(error.stack);
1028
- process.exit(1);
1029
- }
1030
- }
1031
- }
1032
-
1033
- function escapeRegExp (string) {
1034
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1035
- }
1036
-
1037
- // Run the generator
1038
- const generator = new MCPGenerator();
1039
- generator.run().catch(console.error);
1
+ #!/usr/bin/env node
2
+
3
+ import fss from 'fs';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import readline from 'readline';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ import chalk from 'chalk';
10
+ import yaml from 'js-yaml';
11
+ import { v4 as uuidv4 } from 'uuid';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const PRINT_FILLED = true;
16
+ const PROJ_ROOT = path.join(__dirname, '..');
17
+
18
+ const hl = (v) => chalk.bgGreen.black(v);
19
+ const hly = (v) => chalk.bgYellow.black(v);
20
+ const hp = (paramName) => ` [${chalk.magenta(paramName)}]`;
21
+ const formatDefaultValue = (v) => (v ? ` (default: ${hl(v)})` : '');
22
+ const OPTIONAL = chalk.gray(' (optional)');
23
+ const FROM_CONFIG = chalk.gray(' (from config)');
24
+ const trim = (s) => String(s || '').trim();
25
+
26
+ const printFilled = (paramName, paramValue) => {
27
+ if (PRINT_FILLED) {
28
+ console.log(` ${hp(paramName)}: ${hl(paramValue)}`);
29
+ }
30
+ };
31
+
32
+ const pjContent = fss.readFileSync(path.join(PROJ_ROOT, 'package.json'));
33
+
34
+ const faMcpSdkVersion = JSON.parse(pjContent).version;
35
+
36
+ // Print version and exit on -V or --version
37
+ const argv = process.argv.slice(2);
38
+ if (argv.includes('-V') || argv.includes('--version')) {
39
+ console.log(faMcpSdkVersion);
40
+ process.exit(0);
41
+ }
42
+
43
+ const ALLOWED_FILES = [
44
+ '.git',
45
+ '.idea',
46
+ '.vscode',
47
+ '.swp',
48
+ '.swo',
49
+ '.DS_Store',
50
+ '.sublime-project',
51
+ '.sublime-workspace',
52
+ 'node_modules',
53
+ 'dist',
54
+ '__misc',
55
+ '_tmp',
56
+ '~last-cli-config.json',
57
+ 'yarn.lock',
58
+ ];
59
+
60
+ const getAsk = () => {
61
+ const rl = readline.createInterface({
62
+ input: process.stdin,
63
+ output: process.stdout,
64
+ });
65
+
66
+ const yn_ = (prompt, defaultAnswer = 'y') => new Promise((resolve) => {
67
+ rl.question(prompt, (v) => {
68
+ resolve((trim(v) || defaultAnswer).toLowerCase());
69
+ });
70
+ });
71
+
72
+ return {
73
+ close: rl.close.bind(rl),
74
+
75
+ question: (prompt) => new Promise(resolve => {
76
+ rl.question(prompt, resolve);
77
+ }),
78
+
79
+ optional: (title, paramName, defaultValue, example = undefined) => new Promise(resolve => {
80
+ const defaultText = formatDefaultValue(defaultValue);
81
+ example = example ? ` (example: ${example})` : '';
82
+ const prompt = `${title}${hp(paramName)}${defaultText}${example}${OPTIONAL}: `;
83
+ rl.question(prompt, (v) => {
84
+ resolve(trim(v) || trim(defaultValue));
85
+ });
86
+ }),
87
+
88
+ yn: async (title, paramName, defaultValue = 'false') => {
89
+ const isTrue = /^(true|y)$/i.test(trim(defaultValue));
90
+ const y = isTrue ? `${hl('y')}` : 'y';
91
+ const n = isTrue ? 'n' : `${hl('n')}`;
92
+
93
+ const hpn = paramName ? hp(paramName) : '';
94
+ const prompt = `${title}${hpn} (${y}/${n}): `;
95
+ while (true) {
96
+ const answer = await yn_(prompt, defaultValue === 'true' ? 'y' : 'n');
97
+ if (answer === 'y' || answer === 'n') {
98
+ return answer === 'y';
99
+ }
100
+ console.log(chalk.red('⚠️ Please enter "y" for yes or "n" for no.'));
101
+ }
102
+ },
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Parse configuration file (JSON or YAML)
108
+ * @param {string} filePath - Path to the configuration file
109
+ * @param {string} content - Content of the file
110
+ * @returns {object} Parsed configuration object
111
+ */
112
+ const parseConfigFile = (filePath, content) => {
113
+ const ext = path.extname(filePath).toLowerCase();
114
+
115
+ try {
116
+ if (ext === '.json') {
117
+ return JSON.parse(content);
118
+ } else if (ext === '.yaml' || ext === '.yml') {
119
+ return yaml.load(content, { schema: yaml.DEFAULT_SCHEMA });
120
+ } else {
121
+ // Try to detect format by content
122
+ const trimmed = content.trim();
123
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
124
+ return JSON.parse(content);
125
+ } else {
126
+ return yaml.load(content, { schema: yaml.DEFAULT_SCHEMA });
127
+ }
128
+ }
129
+ } catch (error) {
130
+ throw new Error(`Failed to parse configuration file ${filePath}: ${error.message}`);
131
+ }
132
+ };
133
+
134
+ const removeIfExists = async (targetPath, relPath, options = {}) => {
135
+ const fullPath = path.join(targetPath, relPath);
136
+
137
+ try {
138
+ let finalOptions = { force: true, ...options };
139
+
140
+ try {
141
+ const stat = await fs.lstat(fullPath);
142
+ if (stat.isDirectory() && finalOptions.recursive === undefined) {
143
+ finalOptions = { ...finalOptions, recursive: true };
144
+ }
145
+ } catch {
146
+ // lstat will crash if there is no file/folder – that's ok, just go to rm with the same options
147
+ }
148
+
149
+ await fs.rm(fullPath, finalOptions);
150
+ } catch {
151
+ // ignore any deletion errors
152
+ }
153
+ };
154
+
155
+ class MCPGenerator {
156
+ constructor () {
157
+ this.lastConfigPath = path.join(process.cwd(), '~last-cli-config.json');
158
+ this.requiredParams = [
159
+ {
160
+ name: 'project.name',
161
+ defaultValue: '',
162
+ title: 'Project name for package.json and MCP server identification',
163
+ },
164
+ {
165
+ name: 'project.description',
166
+ defaultValue: '',
167
+ title: 'Project description for package.json',
168
+ },
169
+ {
170
+ name: 'project.productName',
171
+ defaultValue: '',
172
+ title: 'Product name displayed in UI and documentation',
173
+ },
174
+ {
175
+ name: 'port',
176
+ defaultValue: '3000',
177
+ title: 'Web server port for HTTP endpoints and MCP protocol',
178
+ },
179
+ ];
180
+
181
+ this.optionalParams = [
182
+ {
183
+ name: 'author.name',
184
+ defaultValue: '',
185
+ title: 'Author name for package.json',
186
+ },
187
+ {
188
+ name: 'author.email',
189
+ defaultValue: '',
190
+ title: 'Author email for package.json',
191
+ },
192
+
193
+ {
194
+ name: 'git-base-url',
195
+ defaultValue: 'github.com/username',
196
+ title: 'Git repository base URL',
197
+ },
198
+ {
199
+ name: 'consul.agent.dev.dc',
200
+ defaultValue: '',
201
+ title: 'Development Consul Datacenter to search for services',
202
+ },
203
+ {
204
+ name: 'consul.agent.dev.host',
205
+ defaultValue: 'consul.my.ui',
206
+ title: 'Development Consul UI host',
207
+ },
208
+ {
209
+ name: 'consul.agent.dev.token',
210
+ defaultValue: '***',
211
+ title: 'Token for accessing Development Consul Datacenter',
212
+ },
213
+ {
214
+ name: 'consul.agent.prd.dc',
215
+ defaultValue: '',
216
+ title: 'Production Consul Datacenter to search for services',
217
+ },
218
+ {
219
+ name: 'consul.agent.prd.host',
220
+ defaultValue: 'consul.my.ui',
221
+ title: 'Production Consul UI host',
222
+ },
223
+ {
224
+ name: 'consul.agent.prd.token',
225
+ defaultValue: '***',
226
+ title: 'Token for accessing Production Consul Datacenter',
227
+ },
228
+ // Register in Consul
229
+ {
230
+ name: 'consul.service.enable',
231
+ defaultValue: 'false',
232
+ title: 'Whether to register service in Consul',
233
+ },
234
+ {
235
+ name: 'consul.agent.reg.token',
236
+ defaultValue: '***',
237
+ title: 'Token for registering service with Consul agent',
238
+ },
239
+ {
240
+ name: 'consul.agent.reg.host',
241
+ defaultValue: '',
242
+ title: 'The host of the consul agent where the service will be registered',
243
+ },
244
+ {
245
+ name: 'consul.envCode.dev',
246
+ defaultValue: '<envCode.dev>',
247
+ title: 'Development environment code for Consul service ID generation',
248
+ },
249
+ {
250
+ name: 'consul.envCode.prod',
251
+ defaultValue: '<envCode.prod>',
252
+ title: 'Production environment code for Consul service ID generation',
253
+ },
254
+ {
255
+ name: 'NODE_CONSUL_ENV',
256
+ defaultValue: '',
257
+ title: 'Affects how the Consul service ID is formed - as a product or development ID. Valid values: "" | "development" | "production"',
258
+ },
259
+
260
+ {
261
+ name: 'mcp.domain',
262
+ defaultValue: '',
263
+ title: 'Domain name for nginx configuration',
264
+ },
265
+ {
266
+ name: 'ssl-wildcard.conf.rel.path',
267
+ defaultValue: 'snippets/ssl-wildcard.conf',
268
+ title: `The relative path to the nginx configuration file
269
+ in the /etc/nginx folder that specifies the SSL
270
+ certificate's public and private keys`,
271
+ },
272
+
273
+ {
274
+ name: 'webServer.auth.enabled',
275
+ defaultValue: 'false',
276
+ title: 'Whether to enable authorization by token in the MCP server',
277
+ },
278
+ {
279
+ skip: true,
280
+ name: 'webServer.auth.token.encryptKey',
281
+ defaultValue: '***',
282
+ title: 'Encryption key for MCP tokens',
283
+ },
284
+ {
285
+ name: 'webServer.auth.token.checkMCPName',
286
+ defaultValue: 'false',
287
+ title: 'Whether to check MCP name in the token',
288
+ },
289
+ {
290
+ skip: true,
291
+ name: 'projectAbsPath',
292
+ },
293
+ {
294
+ title: 'Is it Production mode',
295
+ defaultValue: 'false',
296
+ name: 'isProduction',
297
+ },
298
+ {
299
+ skip: true,
300
+ name: 'NODE_ENV',
301
+ },
302
+ {
303
+ name: 'SERVICE_INSTANCE',
304
+ defaultValue: '',
305
+ title: 'Suffix of the service name in Consul and process manager',
306
+ },
307
+ {
308
+ skip: true,
309
+ name: 'PM2_NAMESPACE',
310
+ },
311
+ {
312
+ name: 'maintainerUrl',
313
+ defaultValue: '',
314
+ title: 'Maintainer url',
315
+ },
316
+ {
317
+ name: 'logger.useFileLogger',
318
+ defaultValue: '',
319
+ title: 'Whether to use file logger',
320
+ },
321
+ {
322
+ skip: true,
323
+ name: 'logger.dir',
324
+ defaultValue: '',
325
+ title: 'Absolute path to the folder where logs will be written',
326
+ },
327
+ {
328
+ name: 'claude.isBypassPermissions',
329
+ defaultValue: 'false',
330
+ title: 'Enable GOD Mode for Claude Code',
331
+ },
332
+ ];
333
+ }
334
+
335
+ createConfigProxy (config) {
336
+ const { lastConfigPath } = this; // Capture this in closure
337
+
338
+ return new Proxy(config, {
339
+ set (target, prop, value, receiver) {
340
+ // Check if the value is actually changing
341
+ const currentValue = target[prop];
342
+ if (currentValue === value) {
343
+ return Reflect.set(target, prop, value, receiver);
344
+ }
345
+ // Regular assignment behavior first
346
+ const result = Reflect.set(target, prop, value, receiver);
347
+ // Save to file asynchronously without blocking
348
+ fs.writeFile(lastConfigPath, JSON.stringify(target, null, 2), 'utf8')
349
+ .catch(error => console.warn('⚠️ Warning: Could not save config to file:', error.message));
350
+ return result;
351
+ },
352
+ });
353
+ }
354
+
355
+ async collectConfigData (config, isRetry = false) {
356
+ const ask = getAsk();
357
+ // Collect required parameters
358
+ for (const param of this.requiredParams) {
359
+ const { title, name } = param;
360
+ const currentValue = config[name];
361
+ const defaultValue = currentValue || param.defaultValue;
362
+
363
+ // Skip if already has value and not in retry mode
364
+ if (currentValue && !isRetry) {
365
+ printFilled(name, currentValue);
366
+ continue;
367
+ }
368
+
369
+ let value;
370
+ const defaultText = formatDefaultValue(defaultValue);
371
+
372
+ // Keep asking until we get a valid value for required parameters
373
+ while (true) {
374
+ switch (name) {
375
+ case 'port':
376
+ value = await ask.question(`Enter port${defaultText}: `);
377
+ value = value || defaultValue;
378
+ break;
379
+ default:
380
+ value = await ask.question(`Enter ${title} [${name}]${defaultText}: `);
381
+ value = value.trim() || defaultValue;
382
+ }
383
+
384
+ // Check if we have a valid value
385
+ if (value && value.trim()) {
386
+ break; // Exit the loop - we have a valid value
387
+ }
388
+
389
+ // If no default value and no input, continue asking
390
+ if (!defaultValue) {
391
+ console.error(chalk.red(`⚠️ ${name} is required. Please enter a value.`));
392
+ continue;
393
+ }
394
+
395
+ // Should not reach here, but just in case
396
+ break;
397
+ }
398
+
399
+ config[name] = value;
400
+ }
401
+
402
+ // Self register service in consul
403
+ {
404
+ const param = this.optionalParams.find((p) => p.name === 'consul.service.enable');
405
+ const { title, name } = param;
406
+ const currentValue = config[name];
407
+ const defaultValue = currentValue || param.defaultValue;
408
+ let shouldSkipConsulRegisterParams = currentValue === 'false';
409
+
410
+ if (currentValue && !isRetry) {
411
+ printFilled(name, currentValue);
412
+ } else {
413
+ const enabled = await ask.yn(title, name, defaultValue);
414
+ config[name] = String(enabled);
415
+ shouldSkipConsulRegisterParams = !enabled;
416
+ }
417
+
418
+ // If consul registration is enabled, collect consul parameters immediately
419
+ if (!shouldSkipConsulRegisterParams) {
420
+ const consulParams = this.optionalParams.filter(({ name: n }) => n === 'consul.agent.reg.token' || n.startsWith('consul.envCode.'));
421
+ for (const param of consulParams) {
422
+ const { title, name } = param;
423
+ const currentValue = config[name];
424
+ const defaultValue = currentValue || param.defaultValue;
425
+
426
+ // Skip if already has value and not in retry mode
427
+ if (currentValue && !isRetry) {
428
+ printFilled(name, currentValue);
429
+ continue;
430
+ }
431
+
432
+ const value = await ask.question(`${title} [${name}]${formatDefaultValue(defaultValue)}${OPTIONAL}: `);
433
+ config[name] = trim(value) || defaultValue;
434
+ }
435
+ }
436
+ }
437
+ // Other consul parameters
438
+ const consulParams = this.optionalParams.filter(({ name: n }) => n.startsWith('consul.agent.dev') || n.startsWith('consul.agent.prd'));
439
+ for (const param of consulParams) {
440
+ const { title, name } = param;
441
+ const currentValue = config[name];
442
+ if (currentValue && !isRetry) {
443
+ printFilled(name, currentValue);
444
+ continue;
445
+ }
446
+ const defaultValue = currentValue || param.defaultValue;
447
+ config[name] = await ask.optional(title, name, defaultValue);
448
+ }
449
+
450
+ // Handle webServer.auth.enabled to determine if auth parameters are needed
451
+ {
452
+ const param = this.optionalParams.find((p) => p.name === 'webServer.auth.enabled');
453
+ const { title, name } = param;
454
+ const currentValue = config[name];
455
+ let shouldSkipAuthParams = currentValue === 'false';
456
+
457
+ if (currentValue && !isRetry) {
458
+ printFilled(name, currentValue);
459
+ } else {
460
+ const enabled = await ask.yn(title, name, currentValue || param.defaultValue);
461
+ config[name] = String(enabled);
462
+ shouldSkipAuthParams = !enabled;
463
+ }
464
+
465
+ // Generate encrypt key if auth is enabled
466
+ if (!shouldSkipAuthParams) {
467
+ config['webServer.auth.token.encryptKey'] = uuidv4();
468
+ }
469
+
470
+ // If authentication is enabled, collect auth parameters immediately
471
+ if (!shouldSkipAuthParams) {
472
+ const authParams = this.optionalParams.filter((p) => p.name.startsWith('webServer.auth.') && p.name !== 'webServer.auth.enabled');
473
+
474
+ for (const param of authParams) {
475
+ const { title, name, skip } = param;
476
+ const currentValue = config[name];
477
+ const defaultValue = currentValue || param.defaultValue;
478
+
479
+ // Skip if already has value and not in retry mode
480
+ if (currentValue && !isRetry) {
481
+ printFilled(name, currentValue);
482
+ continue;
483
+ }
484
+ if (skip) {
485
+ if (name === 'webServer.auth.token.encryptKey') {
486
+ config[name] = uuidv4();
487
+ }
488
+ continue;
489
+ }
490
+ switch (name) {
491
+ case 'webServer.auth.token.checkMCPName':
492
+ config[name] = String(await ask.yn(title, name, defaultValue));
493
+ break;
494
+ default:
495
+ config[name] = await ask.optional(title, name, defaultValue);
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ // Collect optional parameters
502
+ for (const param of this.optionalParams) {
503
+ const { title, name, skip } = param;
504
+ // Skip already processed parameters
505
+ if (name.startsWith('consul.') || name.startsWith('webServer.auth.')) {
506
+ continue;
507
+ }
508
+
509
+ const currentValue = config[name];
510
+ const defaultValue = currentValue || param.defaultValue;
511
+
512
+ // Skip if already has value and not in retry mode
513
+ if (currentValue && !isRetry) {
514
+ printFilled(name, currentValue);
515
+
516
+ if (name === 'mcp.domain' && !config['upstream']) {
517
+ config['upstream'] = currentValue.replace(/\./g, '-');
518
+ }
519
+
520
+ continue;
521
+ }
522
+ if (skip) {
523
+ continue;
524
+ }
525
+
526
+ let value;
527
+ switch (name) {
528
+ case 'git-base-url':
529
+ value = await ask.optional(title, name, defaultValue, 'github.com/username OR gitlab.company.com/PROJECT');
530
+ value = value.trim() || defaultValue;
531
+ break;
532
+ case 'author.email': {
533
+ let go = true;
534
+ while (go) {
535
+ value = await ask.optional(title, name, defaultValue);
536
+ if (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/im.test(value)) {
537
+ go = false;
538
+ } else {
539
+ console.log(chalk.red('⚠️ Please enter valid email or leave empty.'));
540
+ }
541
+ }
542
+ break;
543
+ }
544
+
545
+ case 'mcp.domain':
546
+ value = await ask.optional(title, name, defaultValue);
547
+ if (value) {
548
+ // Auto-generate upstream from mcp.domain by replacing dots with dashes
549
+ config.upstream = value.replace(/\./g, '-');
550
+ }
551
+ continue;
552
+
553
+ case 'maintainerUrl':
554
+ value = await ask.optional(title, name, defaultValue);
555
+ if (value) {
556
+ config.maintainerHtml = `<a href="${value}" target="_blank" rel="noopener" class="clickable">Support</a>`;
557
+ }
558
+ continue;
559
+ case 'logger.useFileLogger': {
560
+ const enabled = await ask.yn(title, name, defaultValue);
561
+ config[name] = String(enabled);
562
+ const nm = 'logger.dir';
563
+ if (enabled) {
564
+ const p = this.optionalParams.find(({ name: n }) => n === nm);
565
+ value = await ask.optional(p.title, nm, config[nm] || p.defaultValue);
566
+ if (value) {
567
+ config[nm] = value;
568
+ }
569
+ } else {
570
+ config[nm] = '';
571
+ }
572
+ continue;
573
+ }
574
+ case 'isProduction':
575
+ case 'claude.isBypassPermissions': {
576
+ const enabled = await ask.yn(title, name, defaultValue);
577
+ config[name] = String(enabled);
578
+ continue;
579
+ }
580
+ case 'NODE_CONSUL_ENV': {
581
+ if (currentValue === '') {
582
+ continue;
583
+ }
584
+ value = await ask.optional(title, name, defaultValue);
585
+ if (value === '' || value === 'development' || value === 'production') {
586
+ config[name] = value;
587
+ } else {
588
+ config[name] = '';
589
+ }
590
+ continue;
591
+ }
592
+
593
+ default:
594
+ value = await ask.optional(title, name, defaultValue);
595
+ }
596
+
597
+ if (value) {
598
+ config[name] = value;
599
+ }
600
+ }
601
+ ask.close();
602
+ }
603
+
604
+ async confirmConfiguration (config) {
605
+ console.log('\n📋 Configuration Summary:');
606
+ console.log('========================');
607
+
608
+ // Show all parameters
609
+ const allParams = [...this.requiredParams, ...this.optionalParams];
610
+ for (const param of allParams) {
611
+ const value = config[param.name];
612
+ if (value !== undefined) {
613
+ console.log(` ${param.name}: ${hl(value)}`);
614
+ }
615
+ }
616
+
617
+ const ask = getAsk();
618
+ let confirmed;
619
+ const use = config.forceAcceptConfig;
620
+ // Check for automatic answer from config
621
+ if (use === 'y' || use === 'n') {
622
+ confirmed = use === 'y';
623
+ console.log(`\nUse this configuration: ${hl('y')}es${FROM_CONFIG}`);
624
+ } else {
625
+ confirmed = await ask.yn('\nUse this configuration?', '', 'y');
626
+ }
627
+ if (confirmed) {
628
+ config.forceAcceptConfig = 'y';
629
+ } else {
630
+ delete config.forceAcceptConfig;
631
+ }
632
+ ask.close();
633
+
634
+ return confirmed;
635
+ }
636
+
637
+ async collectConfiguration () {
638
+ const config = {};
639
+ const configFile = process.argv.find((arg) => arg.endsWith('.json') || arg.endsWith('.yaml') || arg.endsWith('.yml')) ||
640
+ process.argv.find((arg) => arg.startsWith('--config='))?.split('=')[1];
641
+
642
+ if (configFile) {
643
+ try {
644
+ const configData = await fs.readFile(configFile, 'utf8');
645
+ const parsedConfig = parseConfigFile(configFile, configData);
646
+ Object.assign(config, parsedConfig);
647
+ console.log(`📋 Loaded configuration from: ${hly(configFile)}`);
648
+ } catch (error) {
649
+ console.warn(`⚠️ Warning: Could not load config file ${configFile}: ${error.message}`);
650
+ }
651
+ }
652
+
653
+ // Create proxy for automatic saving before starting data collection
654
+ const configProxy = this.createConfigProxy(config);
655
+
656
+ // Save initial state if there's any pre-loaded config
657
+ if (Object.keys(config).length > 0) {
658
+ try {
659
+ await fs.writeFile(this.lastConfigPath, JSON.stringify(config, null, 2), 'utf8');
660
+ } catch (error) {
661
+ console.warn('⚠️ Warning: Could not save initial config to file:', error.message);
662
+ }
663
+ }
664
+
665
+ if (configProxy.NODE_ENV === 'development') {
666
+ configProxy.isProduction = 'false';
667
+ } else if (configProxy.NODE_ENV === 'production') {
668
+ configProxy.isProduction = 'true';
669
+ }
670
+ if (config['logger.useFileLogger'] !== 'true') {
671
+ config['logger.dir'] = '';
672
+ }
673
+ let confirmed = false;
674
+ let isRetry = false;
675
+
676
+ // Loop until configuration is confirmed
677
+ while (!confirmed) {
678
+ await this.collectConfigData(configProxy, isRetry);
679
+
680
+ // Set NODE_ENV and PM2_NAMESPACE based on isProduction
681
+ config.NODE_ENV = config.isProduction === 'true' ? 'production' : 'development';
682
+ config.PM2_NAMESPACE = config.isProduction === 'true' ? 'prod' : 'dev';
683
+ config.SERVICE_INSTANCE = config.PM2_NAMESPACE;
684
+
685
+ confirmed = await this.confirmConfiguration(config);
686
+
687
+ if (!confirmed) {
688
+ console.log('\n🔄 Let\'s re-enter the configuration:\n');
689
+ isRetry = true;
690
+ }
691
+ }
692
+
693
+ return config;
694
+ }
695
+
696
+ async getTargetPath (config = {}) {
697
+ const ask = getAsk();
698
+
699
+ let tp = process.cwd();
700
+ let createInCurrent;
701
+ let pPath = trim(config.projectAbsPath);
702
+ if (pPath) {
703
+ tp = path.resolve(pPath);
704
+ console.log(`Create project in: ${hl(tp)}${FROM_CONFIG}`);
705
+ } else {
706
+ createInCurrent = await ask.yn(`Create project in current directory? (${hl(tp)})`, '', 'n');
707
+ if (!createInCurrent) {
708
+ tp = await ask.question('Enter absolute path for project: ');
709
+ tp = path.resolve(tp);
710
+ }
711
+ }
712
+
713
+ config.projectAbsPath = tp;
714
+ // Create directory if it doesn't exist
715
+ try {
716
+ await fs.access(tp);
717
+ } catch {
718
+ console.log('Creating directory recursively...');
719
+ await fs.mkdir(tp, { recursive: true });
720
+ }
721
+
722
+ const errMsg = `❌ Directory ${hl(tp)} not empty - cannot create project here. Use an empty directory or specify a different path.`;
723
+
724
+ // Check if directory is empty
725
+ try {
726
+ const files = await fs.readdir(tp);
727
+ const firstDeprecatedFile = files.find((file) => !ALLOWED_FILES.includes(file));
728
+
729
+ if (firstDeprecatedFile) {
730
+ console.error(errMsg);
731
+ console.error(` First deprecated file: ${hl(firstDeprecatedFile)}`);
732
+ process.exit(1);
733
+ }
734
+ } catch (error) {
735
+ if (error.message.includes('Directory not empty')) {
736
+ console.error(errMsg);
737
+ process.exit(1);
738
+ }
739
+ throw new Error(`Cannot access directory: ${error.message}`);
740
+ }
741
+
742
+ ask.close();
743
+ return tp;
744
+ }
745
+
746
+ async copyDirectory (source, target) {
747
+ const entries = await fs.readdir(source, { withFileTypes: true });
748
+ if (!fss.existsSync(target)) {
749
+ await fs.mkdir(target, { recursive: true });
750
+ }
751
+
752
+ for (const entry of entries) {
753
+ if (entry.name === 'node_modules' || entry.name === 'dist') {
754
+ continue; // Skip node_modules & dist directories
755
+ }
756
+
757
+ const sourcePath = path.join(source, entry.name);
758
+ const targetPath = path.join(target, entry.name);
759
+
760
+ if (entry.isDirectory()) {
761
+ await fs.mkdir(targetPath, { recursive: true });
762
+ await this.copyDirectory(sourcePath, targetPath);
763
+ } else {
764
+ await fs.copyFile(sourcePath, targetPath);
765
+ }
766
+ }
767
+ }
768
+
769
+ async handlePackageJson (content, config) {
770
+ try {
771
+ content = content
772
+ .replace(/"project\.name"/g, '"{{project.name}}"')
773
+ .replace(/"node \.\.\/scripts/g, '"node ./scripts');
774
+ // First replace all template parameters in the content string
775
+ let updatedContent = content;
776
+ for (const [param, value] of Object.entries(config)) {
777
+ const template = `{{${param}}}`;
778
+ if (updatedContent.includes(template)) {
779
+ updatedContent = updatedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
780
+ }
781
+ }
782
+
783
+ // Now parse the updated content and handle author fields
784
+ const packageJson = JSON.parse(updatedContent);
785
+ const authorName = config['author.name'];
786
+ const authorEmail = config['author.email'];
787
+ // Handle optional author fields
788
+ if (!authorName && !authorEmail) {
789
+ delete packageJson.author;
790
+ } else {
791
+ if (!packageJson.author) {packageJson.author = {};}
792
+ if (authorName) {
793
+ packageJson.author.name = authorName;
794
+ }
795
+ if (authorEmail) {
796
+ packageJson.author.email = authorEmail;
797
+ }
798
+ // Remove empty author object if no fields
799
+ if (Object.keys(packageJson.author).length === 0) {
800
+ delete packageJson.author;
801
+ }
802
+ }
803
+
804
+ packageJson.dependencies['fa-mcp-sdk'] = `^${faMcpSdkVersion}`;
805
+
806
+ if (!config.keepPostinstall) {
807
+ delete packageJson.scripts.postinstall;
808
+ }
809
+
810
+ return JSON.stringify(packageJson, null, 2);
811
+ } catch (error) {
812
+ throw new Error(`Error processing package.json: ${error.message}`);
813
+ }
814
+ }
815
+
816
+ async getAllFiles (dir, skipRootDirs) {
817
+ const files = [];
818
+ const entries = await fs.readdir(dir, { withFileTypes: true });
819
+
820
+ for (const entry of entries) {
821
+ if (skipRootDirs && skipRootDirs.includes(entry.name)) {
822
+ continue;
823
+ }
824
+ const fullPath = path.join(dir, entry.name);
825
+
826
+ if (entry.isDirectory()) {
827
+ files.push(...await this.getAllFiles(fullPath));
828
+ } else {
829
+ files.push(fullPath);
830
+ }
831
+ }
832
+
833
+ return files;
834
+ }
835
+
836
+ async transformTargetFile (config, targetRelPath, transformFn) {
837
+ const targetPath = config.projectAbsPath;
838
+ const targetFullPath = path.join(targetPath, targetRelPath);
839
+ const content = await fs.readFile(targetFullPath, 'utf8');
840
+ const transformedContent = transformFn(content, config);
841
+ await fs.writeFile(targetFullPath, transformedContent, 'utf8');
842
+ }
843
+
844
+ async replaceTemplateParameters (config) {
845
+ const targetPath = config.projectAbsPath;
846
+ const files = await this.getAllFiles(targetPath, ALLOWED_FILES);
847
+ const importRe = /'[^']+\/core\/index.js'/ig;
848
+ for (const filePath of files) {
849
+ let content = await fs.readFile(filePath, 'utf8');
850
+ let modified = false;
851
+
852
+ // Special handling for package.json
853
+ if (filePath.endsWith('package.json')) {
854
+ content = await this.handlePackageJson(content, config);
855
+ modified = true;
856
+ } else {
857
+ // Replace all template parameters
858
+ for (const [param, value] of Object.entries(config)) {
859
+ const template = `{{${param}}}`;
860
+ if (content.includes(template)) {
861
+ content = content.replace(new RegExp(escapeRegExp(template), 'g'), value);
862
+ modified = true;
863
+ }
864
+ }
865
+ if (importRe.test(content)) {
866
+ content = content.replace(importRe, '\'fa-mcp-sdk\'');
867
+ modified = true;
868
+ }
869
+ }
870
+ if (filePath.endsWith('test-sse-npm-package.js')) {
871
+ content = content.replace(/http:\/\/localhost:9876/g, `http://localhost:${config.port}`);
872
+ modified = true;
873
+ }
874
+ if (filePath.endsWith('test-stdio.js')) {
875
+ content = content.replace('../dist/template/start.js', 'dist/src/start.js');
876
+ modified = true;
877
+ }
878
+
879
+ if (modified) {
880
+ await fs.writeFile(filePath, content, 'utf8');
881
+ }
882
+ }
883
+ if (config['NODE_CONSUL_ENV'] === '') {
884
+ await this.transformTargetFile(config, '.env', (c) => c.replace(/^(NODE_CONSUL_ENV)=([^\r\n]*)/m, '#$1=$2'));
885
+ }
886
+ if (config['claude.isBypassPermissions'] === 'true') {
887
+ const c1 = ['sudo cp', 'sudo', 'bash', 'chmod', 'curl', 'dir', 'echo', 'git', 'find', 'grep', 'jest',
888
+ 'mkdir', 'node', 'npm install', 'npm run', 'npm test', 'npm', 'npx', 'pkill', 'set', 'playwright', 'powershell',
889
+ 'rm', 'taskkill', 'tasklist', 'timeout', 'turbo run', 'wc'];
890
+ const c2 = ['jobs', 'npm start', 'unset http_proxy'];
891
+ const i = ' '.repeat(8);
892
+ const allowBashLines = [...c1.map((c) => `${i}"Bash(${c}:*)",`), ...c2.map((c) => `${i}"Bash(${c})",`)].join('\n');
893
+ const transformFn = (c) => c.replace('"acceptEdits"', '"bypassPermissions"')
894
+ .replace(/"allow": \[\s+"Edit",/, `"allow": [\n${allowBashLines}\n${i}"Edit",`);
895
+ await this.transformTargetFile(config, '.claude/settings.json', transformFn);
896
+ }
897
+ }
898
+
899
+ async createProject (config) {
900
+ const targetPath = config.projectAbsPath;
901
+ // Copy template files
902
+ await this.copyDirectory(path.join(PROJ_ROOT, 'cli-template'), targetPath);
903
+ await this.copyDirectory(path.join(PROJ_ROOT, 'src/template'), path.join(targetPath, 'src'));
904
+
905
+ const testsTargetPath = path.join(targetPath, 'tests');
906
+
907
+ await this.copyDirectory(path.join(PROJ_ROOT, 'src/tests'), testsTargetPath);
908
+ await fs.copyFile(path.join(targetPath, '.env.example'), path.join(targetPath, '.env'));
909
+ await fs.rename(path.join(targetPath, 'gitignore'), path.join(targetPath, '.gitignore'));
910
+ await fs.rename(path.join(targetPath, 'r'), path.join(targetPath, '.run'));
911
+
912
+ await this.copyDirectory(path.join(PROJ_ROOT, 'config'), path.join(targetPath, 'config'));
913
+
914
+ const scriptsTargetPath = path.join(targetPath, 'scripts');
915
+ await this.copyDirectory(path.join(PROJ_ROOT, 'scripts'), scriptsTargetPath);
916
+ await fs.rm(path.join(targetPath, 'scripts/copy-static.js'), { force: true });
917
+ await fs.rm(path.join(targetPath, 'scripts/publish.sh'), { force: true });
918
+
919
+ // Rename all .xml files in .run directory to .run.xml
920
+ const runDirPath = path.join(targetPath, '.run');
921
+ const files = await fs.readdir(runDirPath);
922
+
923
+ for (const file of files) {
924
+ if (file.endsWith('.xml')) {
925
+ const oldFilePath = path.join(runDirPath, file);
926
+ const newFileName = file.slice(0, -4) + '.run.xml';
927
+ const newFilePath = path.join(runDirPath, newFileName);
928
+ await fs.rename(oldFilePath, newFilePath);
929
+ }
930
+ }
931
+
932
+ // Rename mcp-template.com.conf if mcp.domain is provided
933
+ const mcpDomain = config['mcp.domain'];
934
+ if (mcpDomain) {
935
+ try {
936
+ const oldConfigPath = path.join(targetPath, 'deploy/NGINX/sites-enabled/mcp-template.com.conf');
937
+ const newConfigPath = path.join(targetPath, 'deploy/NGINX/sites-enabled', `${mcpDomain}.conf`);
938
+
939
+ await fs.access(oldConfigPath);
940
+ await fs.rename(oldConfigPath, newConfigPath);
941
+ } catch (error) {
942
+ // File doesn't exist or rename failed, which is not critical
943
+ console.log('⚠️ Warning: Could not rename mcp-template.com.conf file', error);
944
+ }
945
+ }
946
+
947
+ // Read _local.yaml into memory and rename it to local.yaml
948
+ let localYamlExampleContent = '';
949
+ const localYamlExamplePath = path.join(targetPath, 'config', '_local.yaml');
950
+ const localYamlPath = path.join(targetPath, 'config', 'local.yaml');
951
+ try {
952
+
953
+ localYamlExampleContent = await fs.readFile(localYamlExamplePath, 'utf8');
954
+ } catch (error) {
955
+ console.log('⚠️ Warning: Could not process config/_local.yaml file:', error.message);
956
+ }
957
+
958
+ // Replace template parameters
959
+ await this.replaceTemplateParameters(config);
960
+
961
+ // Replace template placeholders with defaultValue from optionalParams and save as _local.yaml
962
+ if (localYamlExampleContent) {
963
+ try {
964
+ let localYamlExampleModifiedContent = localYamlExampleContent;
965
+ let localYamlModifiedContent = localYamlExampleContent;
966
+ // Replace with defaultValue from optionalParams
967
+ for (const param of this.optionalParams) {
968
+ const template = `{{${param.name}}}`;
969
+ if (localYamlExampleModifiedContent.includes(template)) {
970
+ const defaultValue = param.defaultValue || '';
971
+ localYamlExampleModifiedContent = localYamlExampleModifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), defaultValue);
972
+ }
973
+ }
974
+
975
+ // Replacement of the remaining substitution places with what is in the config
976
+ for (const [paramName, value] of Object.entries(config)) {
977
+ const template = `{{${paramName}}}`;
978
+ if (localYamlExampleModifiedContent.includes(template)) {
979
+ localYamlExampleModifiedContent = localYamlExampleModifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
980
+ }
981
+ if (localYamlModifiedContent.includes(template)) {
982
+ localYamlModifiedContent = localYamlModifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
983
+ }
984
+ }
985
+ if (!config['consul.agent.reg.host']) {
986
+ localYamlModifiedContent = localYamlModifiedContent.replace(/(\n +)host: '[^']*'( # The host of the consul agent)/, '$1# host: \'\'$2');
987
+ }
988
+
989
+ await fs.writeFile(localYamlPath, localYamlModifiedContent, 'utf8');
990
+ await fs.writeFile(localYamlExamplePath, localYamlExampleModifiedContent, 'utf8');
991
+ } catch (error) {
992
+ console.log('⚠️ Warning: Could not create config/_local.yaml file:', error.message);
993
+ }
994
+ }
995
+ const pathsToRemove = [
996
+ { rel: 'package-lock.json' },
997
+ ];
998
+
999
+ await Promise.all(
1000
+ pathsToRemove.map(({ rel, options }) => removeIfExists(targetPath, rel, options)),
1001
+ );
1002
+ }
1003
+
1004
+ async run () {
1005
+ console.log('MCP Server Template Generator');
1006
+ console.log('==================================\n');
1007
+
1008
+ try {
1009
+ const config = await this.collectConfiguration();
1010
+ const targetPath = await this.getTargetPath(config);
1011
+
1012
+ console.log(`\n📁 Creating project in: ${targetPath}`);
1013
+ await this.createProject(config);
1014
+
1015
+ console.log('\n✅ MCP Server template created successfully!');
1016
+ console.log('\n📋 Next steps:');
1017
+ console.log(` cd ${targetPath}`);
1018
+ console.log(' npm install');
1019
+ console.log(' npm run build');
1020
+ console.log(' npm start');
1021
+
1022
+ process.exit(0);
1023
+
1024
+ } catch (error) {
1025
+ if (error.message && !(error.stack || '').includes(String(error.message))) {
1026
+ console.error('\n❌ Error:', error.message);
1027
+ }
1028
+ console.error(error.stack);
1029
+ process.exit(1);
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ function escapeRegExp (string) {
1035
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1036
+ }
1037
+
1038
+ // Run the generator
1039
+ const generator = new MCPGenerator();
1040
+ generator.run().catch(console.error);