fa-mcp-sdk 0.2.38 → 0.2.78

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 (121) hide show
  1. package/bin/fa-mcp.js +781 -0
  2. package/cli-template/.editorconfig +13 -0
  3. package/cli-template/.env.example +29 -0
  4. package/cli-template/.envrc +3 -0
  5. package/cli-template/.run/== START ==.run.xml +14 -0
  6. package/cli-template/.run/TEST HTTP.run.xml +5 -0
  7. package/cli-template/.run/TEST SSE.run.xml +5 -0
  8. package/cli-template/.run/TEST STDIO.run.xml +5 -0
  9. package/cli-template/.run/TEST search.run.xml +11 -0
  10. package/cli-template/.run/cb.run.xml +12 -0
  11. package/cli-template/.run/ci.run.xml +12 -0
  12. package/cli-template/.run/kill-port 3030.run.xml +5 -0
  13. package/cli-template/.run/lint.run.xml +12 -0
  14. package/cli-template/.run/lint_fix.run.xml +12 -0
  15. package/cli-template/.run/reinstall.run.xml +12 -0
  16. package/cli-template/.run/remove-nul.js.run.xml +5 -0
  17. package/cli-template/LICENSE +21 -0
  18. package/cli-template/config/_local.yaml +64 -0
  19. package/cli-template/config/custom-environment-variables.yaml +33 -0
  20. package/cli-template/config/default.yaml +101 -0
  21. package/cli-template/config/development.yaml +4 -0
  22. package/cli-template/config/production.yaml +4 -0
  23. package/cli-template/config/test.yaml +26 -0
  24. package/cli-template/deploy/.gitkeep +0 -0
  25. package/cli-template/deploy/config.example.yml +3 -0
  26. package/cli-template/deploy/mcp-template.com.conf +58 -0
  27. package/cli-template/deploy/pm2.config.js +30 -0
  28. package/cli-template/deploy/pm2reg.sh +49 -0
  29. package/cli-template/deploy/srv.sh +359 -0
  30. package/cli-template/deploy/srv.sh.readme.md +347 -0
  31. package/cli-template/eslint.config.js +139 -0
  32. package/cli-template/jest.config.js +30 -0
  33. package/cli-template/package.json +73 -0
  34. package/cli-template/scripts/kill-port.js +107 -0
  35. package/cli-template/scripts/npm/patch_node_modules.js +9 -0
  36. package/cli-template/scripts/npm/run.js +31 -0
  37. package/cli-template/scripts/npm/yarn-ci.ps1 +16 -0
  38. package/cli-template/scripts/npm/yarn-ci.sh +8 -0
  39. package/cli-template/scripts/npm/yarn-reinstall.ps1 +54 -0
  40. package/cli-template/scripts/npm/yarn-reinstall.sh +10 -0
  41. package/cli-template/scripts/pre-commit +58 -0
  42. package/cli-template/scripts/remove-nul.js +53 -0
  43. package/cli-template/src/_types_/common.d.ts +27 -0
  44. package/cli-template/src/api/router.ts +35 -0
  45. package/cli-template/src/api/swagger.ts +167 -0
  46. package/cli-template/src/asset/favicon.svg +4 -0
  47. package/cli-template/src/custom-resources.ts +11 -0
  48. package/cli-template/src/prompts/agent-brief.ts +8 -0
  49. package/cli-template/src/prompts/agent-prompt.ts +1 -0
  50. package/cli-template/src/prompts/custom-prompts.ts +12 -0
  51. package/cli-template/src/start.ts +84 -0
  52. package/cli-template/src/tools/handle-tool-call.ts +55 -0
  53. package/cli-template/src/tools/tools.ts +88 -0
  54. package/cli-template/tests/jest-simple-reporter.js +10 -0
  55. package/cli-template/tests/mcp/sse/mcp-sse-client-handling.md +111 -0
  56. package/cli-template/tests/mcp/sse/test-sse-npm-package.js +96 -0
  57. package/cli-template/tests/mcp/test-cases.js +143 -0
  58. package/cli-template/tests/mcp/test-http.js +63 -0
  59. package/cli-template/tests/mcp/test-sse.js +67 -0
  60. package/cli-template/tests/mcp/test-stdio.js +78 -0
  61. package/cli-template/tests/utils.ts +154 -0
  62. package/cli-template/tsconfig.json +48 -0
  63. package/cli-template/update.cjs +631 -0
  64. package/dist/core/_types_/active-directory-config.d.ts +24 -0
  65. package/dist/core/_types_/active-directory-config.d.ts.map +1 -0
  66. package/dist/core/_types_/active-directory-config.js +2 -0
  67. package/dist/core/_types_/active-directory-config.js.map +1 -0
  68. package/dist/core/bootstrap/init-config.d.ts.map +1 -1
  69. package/dist/core/bootstrap/init-config.js +14 -3
  70. package/dist/core/bootstrap/init-config.js.map +1 -1
  71. package/dist/core/bootstrap/startup-info.js +1 -1
  72. package/dist/core/bootstrap/startup-info.js.map +1 -1
  73. package/dist/core/index.d.ts +3 -2
  74. package/dist/core/index.d.ts.map +1 -1
  75. package/dist/core/index.js +5 -2
  76. package/dist/core/index.js.map +1 -1
  77. package/dist/core/init-mcp-server.js +1 -1
  78. package/dist/core/init-mcp-server.js.map +1 -1
  79. package/dist/core/token/gen-token-app/gen-token-server.d.ts.map +1 -1
  80. package/dist/core/token/gen-token-app/gen-token-server.js +85 -9
  81. package/dist/core/token/gen-token-app/gen-token-server.js.map +1 -1
  82. package/dist/core/token/gen-token-app/html.d.ts +8 -1
  83. package/dist/core/token/gen-token-app/html.d.ts.map +1 -1
  84. package/dist/core/token/gen-token-app/html.js +98 -2
  85. package/dist/core/token/gen-token-app/html.js.map +1 -1
  86. package/dist/core/token/gen-token-app/ntlm-auth-options.d.ts +4 -0
  87. package/dist/core/token/gen-token-app/ntlm-auth-options.d.ts.map +1 -0
  88. package/dist/core/token/gen-token-app/ntlm-auth-options.js +94 -0
  89. package/dist/core/token/gen-token-app/ntlm-auth-options.js.map +1 -0
  90. package/dist/core/token/gen-token-app/ntlm-domain-config.d.ts +16 -0
  91. package/dist/core/token/gen-token-app/ntlm-domain-config.d.ts.map +1 -0
  92. package/dist/core/token/gen-token-app/ntlm-domain-config.js +71 -0
  93. package/dist/core/token/gen-token-app/ntlm-domain-config.js.map +1 -0
  94. package/dist/core/token/gen-token-app/ntlm-integration.d.ts +3 -0
  95. package/dist/core/token/gen-token-app/ntlm-integration.d.ts.map +1 -0
  96. package/dist/core/token/gen-token-app/ntlm-integration.js +69 -0
  97. package/dist/core/token/gen-token-app/ntlm-integration.js.map +1 -0
  98. package/dist/core/token/gen-token-app/ntlm-session-storage.d.ts +16 -0
  99. package/dist/core/token/gen-token-app/ntlm-session-storage.d.ts.map +1 -0
  100. package/dist/core/token/gen-token-app/ntlm-session-storage.js +74 -0
  101. package/dist/core/token/gen-token-app/ntlm-session-storage.js.map +1 -0
  102. package/dist/core/token/gen-token-app/ntlm-templates.d.ts +21 -0
  103. package/dist/core/token/gen-token-app/ntlm-templates.d.ts.map +1 -0
  104. package/dist/core/token/gen-token-app/ntlm-templates.js +246 -0
  105. package/dist/core/token/gen-token-app/ntlm-templates.js.map +1 -0
  106. package/dist/core/token/{token.d.ts → token-auth.d.ts} +1 -1
  107. package/dist/core/token/token-auth.d.ts.map +1 -0
  108. package/dist/core/token/{token.js → token-auth.js} +4 -6
  109. package/dist/core/token/token-auth.js.map +1 -0
  110. package/dist/core/token/token-core.d.ts +5 -1
  111. package/dist/core/token/token-core.d.ts.map +1 -1
  112. package/dist/core/token/token-core.js +13 -3
  113. package/dist/core/token/token-core.js.map +1 -1
  114. package/dist/core/web/about-page/render.d.ts.map +1 -1
  115. package/dist/core/web/about-page/render.js +26 -3
  116. package/dist/core/web/about-page/render.js.map +1 -1
  117. package/dist/core/web/server-http.js +1 -1
  118. package/dist/core/web/server-http.js.map +1 -1
  119. package/package.json +10 -3
  120. package/dist/core/token/token.d.ts.map +0 -1
  121. package/dist/core/token/token.js.map +0 -1
package/bin/fa-mcp.js ADDED
@@ -0,0 +1,781 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import readline from 'readline';
7
+ import { v4 as uuidv4 } from 'uuid';
8
+ import chalk from 'chalk';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const PRINT_FILLED = true;
13
+
14
+ const hl = (v) => chalk.bgGreen.black(v);
15
+ const hly = (v) => chalk.bgYellow.black(v);
16
+ const formatDefaultValue = (v) => (v ? ` (default: ${hl(v)})` : '');
17
+ const OPTIONAL = chalk.gray(' (optional)');
18
+ const FROM_CONFIG = chalk.gray(' (from config)');
19
+ const trim = (s) => String(s || '').trim();
20
+
21
+ const printFilled = (paramName, paramValue) => {
22
+ if (PRINT_FILLED) {
23
+ console.log(` ${paramName}: ${hl(paramValue)}`);
24
+ }
25
+ };
26
+
27
+ const getAsk = () => {
28
+ const rl = readline.createInterface({
29
+ input: process.stdin,
30
+ output: process.stdout,
31
+ });
32
+
33
+ const yn_ = (prompt, defaultAnswer = 'y') => new Promise((resolve) => {
34
+ rl.question(prompt, (v) => {
35
+ resolve((trim(v) || defaultAnswer).toLowerCase());
36
+ });
37
+ });
38
+
39
+ return {
40
+ close: rl.close.bind(rl),
41
+
42
+ question: (prompt) => new Promise(resolve => {
43
+ rl.question(prompt, resolve);
44
+ }),
45
+
46
+ optional: (title, paramName, defaultValue, example = undefined) => new Promise(resolve => {
47
+ const defaultText = formatDefaultValue(defaultValue);
48
+ example = example ? ` (example: ${example})` : '';
49
+ const prompt = `${title} [${paramName}]${defaultText}${example}${OPTIONAL}: `;
50
+ rl.question(prompt, (v) => {
51
+ resolve(trim(v) || trim(defaultValue));
52
+ });
53
+ }),
54
+
55
+ yn: async (title, paramName, defaultValue = 'false') => {
56
+ const isTrue = /^(true|y)$/i.test(trim(defaultValue));
57
+ const y = isTrue ? `${hl('y')}` : 'y';
58
+ const n = isTrue ? 'n' : `${hl('n')}`;
59
+
60
+ paramName = paramName ? ` [${paramName}]` : '';
61
+ const prompt = `${title}${paramName} (${y}/${n}): `;
62
+ while (true) {
63
+ const answer = await yn_(prompt, defaultValue === 'true' ? 'y' : 'n');
64
+ if (answer === 'y' || answer === 'n') {
65
+ return answer === 'y';
66
+ }
67
+ console.log(chalk.red('⚠️ Please enter "y" for yes or "n" for no.'));
68
+ }
69
+ },
70
+ };
71
+ };
72
+
73
+ class MCPGenerator {
74
+ constructor () {
75
+ this.templateDir = path.join(__dirname, '..', 'cli-template');
76
+ this.lastConfigPath = path.join(process.cwd(), '~last-cli-config.json');
77
+ this.requiredParams = [
78
+ {
79
+ name: 'project.name',
80
+ defaultValue: '',
81
+ title: 'Project name for package.json and MCP server identification',
82
+ },
83
+ {
84
+ name: 'project.description',
85
+ defaultValue: '',
86
+ title: 'Project description for package.json',
87
+ },
88
+ {
89
+ name: 'project.productName',
90
+ defaultValue: '',
91
+ title: 'Product name displayed in UI and documentation',
92
+ },
93
+ {
94
+ name: 'port',
95
+ defaultValue: '3000',
96
+ title: 'Web server port for HTTP endpoints and MCP protocol',
97
+ },
98
+ ];
99
+
100
+ this.optionalParams = [
101
+ {
102
+ name: 'author.name',
103
+ defaultValue: '',
104
+ title: 'Author name for package.json',
105
+ },
106
+ {
107
+ name: 'author.email',
108
+ defaultValue: '',
109
+ title: 'Author email for package.json',
110
+ },
111
+
112
+ {
113
+ name: 'git-base-url',
114
+ defaultValue: 'github.com/username',
115
+ title: 'Git repository base URL',
116
+ },
117
+ {
118
+ name: 'consul.agent.dev.dc',
119
+ defaultValue: '',
120
+ title: 'Development Consul Datacenter to search for services',
121
+ },
122
+ {
123
+ name: 'consul.agent.dev.host',
124
+ defaultValue: 'consul.my.ui',
125
+ title: 'Development Consul UI host',
126
+ },
127
+ {
128
+ name: 'consul.agent.dev.token',
129
+ defaultValue: '***',
130
+ title: 'Token for accessing Development Consul Datacenter',
131
+ },
132
+ {
133
+ name: 'consul.agent.prd.dc',
134
+ defaultValue: '',
135
+ title: 'Production Consul Datacenter to search for services',
136
+ },
137
+ {
138
+ name: 'consul.agent.prd.host',
139
+ defaultValue: 'consul.my.ui',
140
+ title: 'Production Consul UI host',
141
+ },
142
+ {
143
+ name: 'consul.agent.prd.token',
144
+ defaultValue: '***',
145
+ title: 'Token for accessing Production Consul Datacenter',
146
+ },
147
+ // Register in Consul
148
+ {
149
+ name: 'consul.service.enable',
150
+ defaultValue: 'false',
151
+ title: 'Whether to register service in Consul',
152
+ },
153
+ {
154
+ name: 'consul.agent.reg.token',
155
+ defaultValue: '***',
156
+ title: 'Token for registering service with Consul agent',
157
+ },
158
+ {
159
+ name: 'consul.envCode.dev',
160
+ defaultValue: '<envCode.dev>',
161
+ title: 'Development environment code for Consul service ID generation',
162
+ },
163
+ {
164
+ name: 'consul.envCode.prod',
165
+ defaultValue: '<envCode.prod>',
166
+ title: 'Production environment code for Consul service ID generation',
167
+ },
168
+
169
+ {
170
+ name: 'mcp.domain',
171
+ defaultValue: '',
172
+ title: 'Domain name for nginx configuration',
173
+ },
174
+
175
+ {
176
+ name: 'webServer.auth.enabled',
177
+ defaultValue: 'false',
178
+ title: 'Whether to enable authorization by token in the MCP server',
179
+ },
180
+ {
181
+ skip: true,
182
+ name: 'webServer.auth.token.encryptKey',
183
+ defaultValue: '***',
184
+ title: 'Encryption key for MCP tokens',
185
+ },
186
+ {
187
+ name: 'webServer.auth.token.checkMCPName',
188
+ defaultValue: 'false',
189
+ title: 'Whether to check MCP name in the token',
190
+ },
191
+ {
192
+ skip: true,
193
+ name: 'projectAbsPath',
194
+ },
195
+ {
196
+ title: 'Is it Production mode',
197
+ name: 'isProduction',
198
+ },
199
+ {
200
+ skip: true,
201
+ name: 'NODE_ENV',
202
+ },
203
+ {
204
+ skip: true,
205
+ name: 'SERVICE_INSTANCE',
206
+ },
207
+ {
208
+ skip: true,
209
+ name: 'PM2_NAMESPACE',
210
+ },
211
+ ];
212
+ }
213
+
214
+ createConfigProxy (config) {
215
+ const lastConfigPath = this.lastConfigPath; // Capture this in closure
216
+
217
+ return new Proxy(config, {
218
+ set (target, prop, value, receiver) {
219
+ // Check if the value is actually changing
220
+ const currentValue = target[prop];
221
+ if (currentValue === value) {
222
+ return Reflect.set(target, prop, value, receiver);
223
+ }
224
+ // Regular assignment behavior first
225
+ const result = Reflect.set(target, prop, value, receiver);
226
+ // Save to file asynchronously without blocking
227
+ fs.writeFile(lastConfigPath, JSON.stringify(target, null, 2), 'utf8')
228
+ .catch(error => console.warn('⚠️ Warning: Could not save config to file:', error.message));
229
+ return result;
230
+ },
231
+ });
232
+ }
233
+
234
+ async collectConfigData (config, isRetry = false) {
235
+ const ask = getAsk();
236
+
237
+ // Collect required parameters
238
+ for (const param of this.requiredParams) {
239
+ const { title, name } = param;
240
+ const currentValue = config[name];
241
+ const defaultValue = currentValue || param.defaultValue;
242
+
243
+ // Skip if already has value and not in retry mode
244
+ if (currentValue && !isRetry) {
245
+ printFilled(name, currentValue);
246
+ continue;
247
+ }
248
+
249
+ let value;
250
+ const defaultText = formatDefaultValue(defaultValue);
251
+
252
+ // Keep asking until we get a valid value for required parameters
253
+ while (true) {
254
+ switch (name) {
255
+ case 'port':
256
+ value = await ask.question(`Enter port${defaultText}: `);
257
+ value = value || defaultValue;
258
+ break;
259
+ default:
260
+ value = await ask.question(`Enter ${title} [${name}]${defaultText}: `);
261
+ value = value.trim() || defaultValue;
262
+ }
263
+
264
+ // Check if we have a valid value
265
+ if (value && value.trim()) {
266
+ break; // Exit the loop - we have a valid value
267
+ }
268
+
269
+ // If no default value and no input, continue asking
270
+ if (!defaultValue) {
271
+ console.error(chalk.red(`⚠️ ${name} is required. Please enter a value.`));
272
+ continue;
273
+ }
274
+
275
+ // Should not reach here, but just in case
276
+ break;
277
+ }
278
+
279
+ config[name] = value;
280
+ }
281
+
282
+ // Self register service in consul
283
+ {
284
+ const param = this.optionalParams.find((p) => p.name === 'consul.service.enable');
285
+ const { title, name } = param;
286
+ const currentValue = config[name];
287
+ const defaultValue = currentValue || param.defaultValue;
288
+ let shouldSkipConsulRegisterParams = currentValue === 'false';
289
+
290
+ if (currentValue && !isRetry) {
291
+ printFilled(name, currentValue);
292
+ } else {
293
+ const enabled = await ask.yn(title, name, defaultValue);
294
+ config[name] = String(enabled);
295
+ shouldSkipConsulRegisterParams = !enabled;
296
+ }
297
+
298
+ // If consul registration is enabled, collect consul parameters immediately
299
+ if (!shouldSkipConsulRegisterParams) {
300
+ const consulParams = this.optionalParams.filter(({ name: n }) => n === 'consul.agent.reg.token' || n.startsWith('consul.envCode.'));
301
+ for (const param of consulParams) {
302
+ const { title, name } = param;
303
+ const currentValue = config[name];
304
+ const defaultValue = currentValue || param.defaultValue;
305
+
306
+ // Skip if already has value and not in retry mode
307
+ if (currentValue && !isRetry) {
308
+ printFilled(name, currentValue);
309
+ continue;
310
+ }
311
+
312
+ const value = await ask.question(`${title} [${name}]${formatDefaultValue(defaultValue)}${OPTIONAL}: `);
313
+ config[name] = trim(value) || defaultValue;
314
+ }
315
+ }
316
+ }
317
+ // Other consul parameters
318
+ const consulParams = this.optionalParams.filter(({ name: n }) => n.startsWith('consul.agent.dev') || n.startsWith('consul.agent.prd'));
319
+ for (const param of consulParams) {
320
+ const { title, name } = param;
321
+ const currentValue = config[name];
322
+ if (currentValue && !isRetry) {
323
+ printFilled(name, currentValue);
324
+ continue;
325
+ }
326
+ const defaultValue = currentValue || param.defaultValue;
327
+ config[name] = await ask.optional(title, name, defaultValue);
328
+ }
329
+
330
+ // Handle webServer.auth.enabled to determine if auth parameters are needed
331
+ {
332
+ const param = this.optionalParams.find((p) => p.name === 'webServer.auth.enabled');
333
+ const { title, name } = param;
334
+ const currentValue = config[name];
335
+ let shouldSkipAuthParams = currentValue === 'false';
336
+
337
+ if (currentValue && !isRetry) {
338
+ printFilled(name, currentValue);
339
+ } else {
340
+ const enabled = await ask.yn(title, name, currentValue || param.defaultValue);
341
+ config[name] = String(enabled);
342
+ shouldSkipAuthParams = !enabled;
343
+ }
344
+
345
+ // Generate encrypt key if auth is enabled
346
+ if (!shouldSkipAuthParams) {
347
+ config['webServer.auth.token.encryptKey'] = uuidv4();
348
+ }
349
+
350
+ // If authentication is enabled, collect auth parameters immediately
351
+ if (!shouldSkipAuthParams) {
352
+ const authParams = this.optionalParams.filter((p) => p.name.startsWith('webServer.auth.') && p.name !== 'webServer.auth.enabled');
353
+
354
+ for (const param of authParams) {
355
+ const { title, name, skip } = param;
356
+ const currentValue = config[name];
357
+ const defaultValue = currentValue || param.defaultValue;
358
+
359
+ // Skip if already has value and not in retry mode
360
+ if (currentValue && !isRetry) {
361
+ printFilled(name, currentValue);
362
+ continue;
363
+ }
364
+ if (skip) {
365
+ if (name === 'webServer.auth.token.encryptKey') {
366
+ config[name] = uuidv4();
367
+ }
368
+ continue;
369
+ }
370
+ switch (name) {
371
+ case 'webServer.auth.token.checkMCPName':
372
+ config[name] = String(await ask.yn(title, name, defaultValue));
373
+ break;
374
+ default:
375
+ config[name] = await ask.optional(title, name, defaultValue);
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ // Collect optional parameters
382
+ for (const param of this.optionalParams) {
383
+ const { title, name, skip } = param;
384
+ // Skip already processed parameters
385
+ if (name.startsWith('consul.') || name.startsWith('webServer.auth.')) {
386
+ continue;
387
+ }
388
+
389
+ const currentValue = config[name];
390
+ const defaultValue = currentValue || param.defaultValue;
391
+
392
+ // Skip if already has value and not in retry mode
393
+ if (currentValue && !isRetry) {
394
+ printFilled(name, currentValue);
395
+
396
+ if (name === 'mcp.domain' && !config['upstream']) {
397
+ config['upstream'] = currentValue.replace(/\./g, '-');
398
+ }
399
+
400
+ continue;
401
+ }
402
+ if (skip) {
403
+ continue;
404
+ }
405
+ let value;
406
+ switch (name) {
407
+ case 'git-base-url':
408
+ value = await ask.optional(title, name, defaultValue, 'github.com/username OR gitlab.company.com/PROJECT');
409
+ value = value.trim() || defaultValue;
410
+ break;
411
+ case 'author.email': {
412
+ let go = true;
413
+ while (go) {
414
+ value = await ask.optional(title, name, defaultValue);
415
+ if (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/im.test(value)) {
416
+ go = false;
417
+ } else {
418
+ console.log(chalk.red('⚠️ Please enter valid email or leave empty.'));
419
+ }
420
+ }
421
+ break;
422
+ }
423
+
424
+ case 'mcp.domain':
425
+ value = await ask.optional(title, name, defaultValue);
426
+ if (value) {
427
+ // Auto-generate upstream from mcp.domain by replacing dots with dashes
428
+ config['upstream'] = value.replace(/\./g, '-');
429
+ }
430
+ continue; // Skip the generic assignment at the end
431
+ default:
432
+ value = await ask.optional(title, name, defaultValue);
433
+ }
434
+
435
+ if (value) {
436
+ config[name] = value;
437
+ }
438
+ }
439
+ ask.close();
440
+ }
441
+
442
+ async confirmConfiguration (config) {
443
+ console.log('\n📋 Configuration Summary:');
444
+ console.log('========================');
445
+
446
+ // Show all parameters
447
+ const allParams = [...this.requiredParams, ...this.optionalParams];
448
+ for (const param of allParams) {
449
+ const value = config[param.name];
450
+ if (value !== undefined) {
451
+ console.log(` ${param.name}: ${hl(value)}`);
452
+ }
453
+ }
454
+
455
+ const ask = getAsk();
456
+ let confirmed;
457
+ const use = config.forceAcceptConfig;
458
+ // Check for automatic answer from config
459
+ if (use === 'y' || use === 'n') {
460
+ confirmed = use === 'y';
461
+ console.log(`\nUse this configuration: ${hl('y')}es${FROM_CONFIG}`);
462
+ } else {
463
+ confirmed = await ask.yn('\nUse this configuration?', '', 'y');
464
+ }
465
+ if (confirmed) {
466
+ config.forceAcceptConfig = 'y';
467
+ } else {
468
+ delete config.forceAcceptConfig;
469
+ }
470
+ ask.close();
471
+
472
+ return confirmed;
473
+ }
474
+
475
+ async collectConfiguration () {
476
+ const config = {};
477
+ const configFile = process.argv.find((arg) => arg.endsWith('.json')) ||
478
+ process.argv.find((arg) => arg.startsWith('--config='))?.split('=')[1];
479
+
480
+ if (configFile) {
481
+ try {
482
+ const configData = await fs.readFile(configFile, 'utf8');
483
+ const parsedConfig = JSON.parse(configData);
484
+ Object.assign(config, parsedConfig);
485
+ console.log(`📋 Loaded configuration from: ${hly(configFile)}`);
486
+ } catch (error) {
487
+ console.warn(`⚠️ Warning: Could not load config file ${configFile}`);
488
+ }
489
+ }
490
+
491
+ // Create proxy for automatic saving before starting data collection
492
+ const configProxy = this.createConfigProxy(config);
493
+
494
+ // Save initial state if there's any pre-loaded config
495
+ if (Object.keys(config).length > 0) {
496
+ try {
497
+ await fs.writeFile(this.lastConfigPath, JSON.stringify(config, null, 2), 'utf8');
498
+ } catch (error) {
499
+ console.warn('⚠️ Warning: Could not save initial config to file:', error.message);
500
+ }
501
+ }
502
+
503
+ let confirmed = false;
504
+ let isRetry = false;
505
+
506
+ // Loop until configuration is confirmed
507
+ while (!confirmed) {
508
+ await this.collectConfigData(configProxy, isRetry);
509
+ confirmed = await this.confirmConfiguration(config);
510
+
511
+ if (!confirmed) {
512
+ console.log('\n🔄 Let\'s re-enter the configuration:\n');
513
+ isRetry = true;
514
+ }
515
+ }
516
+
517
+ return config;
518
+ }
519
+
520
+ async getTargetPath (config = {}) {
521
+ const ask = getAsk();
522
+
523
+ let targetPath = process.cwd();
524
+ let createInCurrent;
525
+ let pPath = trim(config.projectAbsPath);
526
+ if (pPath) {
527
+ targetPath = path.resolve(pPath);
528
+ console.log(`Create project in: ${hl(targetPath)}${FROM_CONFIG}`);
529
+ } else {
530
+ createInCurrent = await ask.yn(`Create project in current directory? (${hl(targetPath)})`, '', 'n');
531
+ if (!createInCurrent) {
532
+ targetPath = await ask.question('Enter absolute path for project: ');
533
+ targetPath = path.resolve(targetPath);
534
+ }
535
+ }
536
+
537
+ config.projectAbsPath = targetPath;
538
+ // Create directory if it doesn't exist
539
+ try {
540
+ await fs.access(targetPath);
541
+ } catch {
542
+ console.log('Creating directory recursively...');
543
+ await fs.mkdir(targetPath, { recursive: true });
544
+ }
545
+
546
+ const errMsg = `❌ Directory ${hl(targetPath)} not empty - cannot create project here. Use an empty directory or specify a different path.`;
547
+
548
+ // Check if directory is empty
549
+ try {
550
+ const files = await fs.readdir(targetPath);
551
+ const allowedFiles = ['.git', '.idea', '.vscode', '.swp', '.swo', '.DS_Store', '.sublime-project', '.sublime-workspace', 'node_modules', 'dist'];
552
+ const hasOtherFiles = files.some(file => !allowedFiles.includes(file));
553
+
554
+ if (hasOtherFiles) {
555
+ console.error(errMsg);
556
+ process.exit(1);
557
+ }
558
+ } catch (error) {
559
+ if (error.message.includes('Directory not empty')) {
560
+ console.error(errMsg);
561
+ process.exit(1);
562
+ }
563
+ throw new Error(`Cannot access directory: ${error.message}`);
564
+ }
565
+
566
+ ask.close();
567
+ return targetPath;
568
+ }
569
+
570
+ async copyDirectory (source, target) {
571
+ const entries = await fs.readdir(source, { withFileTypes: true });
572
+
573
+ for (const entry of entries) {
574
+ if (entry.name === 'node_modules' || entry.name === 'dist') {
575
+ continue; // Skip node_modules & dist directories
576
+ }
577
+
578
+ const sourcePath = path.join(source, entry.name);
579
+ const targetPath = path.join(target, entry.name);
580
+
581
+ if (entry.isDirectory()) {
582
+ await fs.mkdir(targetPath, { recursive: true });
583
+ await this.copyDirectory(sourcePath, targetPath);
584
+ } else {
585
+ await fs.copyFile(sourcePath, targetPath);
586
+ }
587
+ }
588
+ }
589
+
590
+ async handlePackageJson (content, config) {
591
+ try {
592
+ // First replace all template parameters in the content string
593
+ let updatedContent = content;
594
+ for (const [param, value] of Object.entries(config)) {
595
+ const template = `{{${param}}}`;
596
+ if (updatedContent.includes(template)) {
597
+ updatedContent = updatedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
598
+ }
599
+ }
600
+
601
+ // Now parse the updated content and handle author fields
602
+ const packageJson = JSON.parse(updatedContent);
603
+ const authorName = config['author.name'];
604
+ const authorEmail = config['author.email'];
605
+ // Handle optional author fields
606
+ if (!authorName && !authorEmail) {
607
+ delete packageJson.author;
608
+ } else {
609
+ if (!packageJson.author) {packageJson.author = {};}
610
+ if (authorName) {
611
+ packageJson.author.name = authorName;
612
+ }
613
+ if (authorEmail) {
614
+ packageJson.author.email = authorEmail;
615
+ }
616
+ // Remove empty author object if no fields
617
+ if (Object.keys(packageJson.author).length === 0) {
618
+ delete packageJson.author;
619
+ }
620
+ }
621
+
622
+ return JSON.stringify(packageJson, null, 2);
623
+ } catch (error) {
624
+ throw new Error(`Error processing package.json: ${error.message}`);
625
+ }
626
+ }
627
+
628
+ async getAllFiles (dir) {
629
+ const files = [];
630
+ const entries = await fs.readdir(dir, { withFileTypes: true });
631
+
632
+ for (const entry of entries) {
633
+ const fullPath = path.join(dir, entry.name);
634
+
635
+ if (entry.isDirectory()) {
636
+ files.push(...await this.getAllFiles(fullPath));
637
+ } else {
638
+ files.push(fullPath);
639
+ }
640
+ }
641
+
642
+ return files;
643
+ }
644
+
645
+ async replaceTemplateParameters (targetPath, config) {
646
+ const files = await this.getAllFiles(targetPath);
647
+
648
+ for (const filePath of files) {
649
+ let content = await fs.readFile(filePath, 'utf8');
650
+ let modified = false;
651
+
652
+ // Special handling for package.json
653
+ if (filePath.endsWith('package.json')) {
654
+ content = await this.handlePackageJson(content, config);
655
+ modified = true;
656
+ } else {
657
+ // Replace all template parameters
658
+ for (const [param, value] of Object.entries(config)) {
659
+ const template = `{{${param}}}`;
660
+ if (content.includes(template)) {
661
+ content = content.replace(new RegExp(escapeRegExp(template), 'g'), value);
662
+ modified = true;
663
+ }
664
+ }
665
+ }
666
+
667
+ if (modified) {
668
+ await fs.writeFile(filePath, content, 'utf8');
669
+ }
670
+ }
671
+ }
672
+
673
+ async createProject (targetPath, config) {
674
+ // Copy template files
675
+ await this.copyDirectory(this.templateDir, targetPath);
676
+ await fs.copyFile(path.join(targetPath, '.env.example'), path.join(targetPath, '.env')); // VVT
677
+
678
+ // Rename mcp-template.com.conf if mcp.domain is provided
679
+ const mcpDomain = config['mcp.domain'];
680
+ if (mcpDomain) {
681
+ try {
682
+ const oldConfigPath = path.join(targetPath, 'deploy', 'mcp-template.com.conf');
683
+ const newConfigPath = path.join(targetPath, 'deploy', `${mcpDomain}.conf`);
684
+
685
+ await fs.access(oldConfigPath);
686
+ await fs.rename(oldConfigPath, newConfigPath);
687
+ } catch (error) {
688
+ // File doesn't exist or rename failed, which is not critical
689
+ console.log('⚠️ Warning: Could not rename mcp-template.com.conf file', error);
690
+ }
691
+ }
692
+
693
+ // Read _local.yaml into memory and rename it to local.yaml
694
+ let localYamlContent = '';
695
+ try {
696
+ const localYamlPath = path.join(targetPath, 'config', '_local.yaml');
697
+ const localYamlNewPath = path.join(targetPath, 'config', 'local.yaml');
698
+
699
+ localYamlContent = await fs.readFile(localYamlPath, 'utf8');
700
+ await fs.rename(localYamlPath, localYamlNewPath);
701
+ } catch (error) {
702
+ // _local.yaml doesn't exist, which might be fine
703
+ console.log('⚠️ Warning: Could not process config/_local.yaml file:', error.message);
704
+ }
705
+
706
+ // Replace template parameters
707
+ await this.replaceTemplateParameters(targetPath, config);
708
+
709
+ // Replace template placeholders with defaultValue from optionalParams and save as _local.yaml
710
+ if (localYamlContent) {
711
+ try {
712
+ let modifiedContent = localYamlContent;
713
+
714
+ // Replace with defaultValue from optionalParams
715
+ for (const param of this.optionalParams) {
716
+ const template = `{{${param.name}}}`;
717
+ if (modifiedContent.includes(template)) {
718
+ const defaultValue = param.defaultValue || '';
719
+ modifiedContent = modifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), defaultValue);
720
+ }
721
+ }
722
+ // Replacement of the remaining substitution places with what is in the config
723
+ for (const [paramName, value] of Object.entries(config)) {
724
+ const template = `{{${paramName}}}`;
725
+ if (modifiedContent.includes(template)) {
726
+ modifiedContent = modifiedContent.replace(new RegExp(escapeRegExp(template), 'g'), value);
727
+ }
728
+ }
729
+
730
+ const newLocalYamlPath = path.join(targetPath, 'config', '_local.yaml');
731
+ await fs.writeFile(newLocalYamlPath, modifiedContent, 'utf8');
732
+ } catch (error) {
733
+ console.log('⚠️ Warning: Could not create config/_local.yaml file:', error.message);
734
+ }
735
+ }
736
+
737
+ // Remove node_modules from project if it exists
738
+ try {
739
+ const nodeModulesPath = path.join(targetPath, 'node_modules');
740
+ await fs.access(nodeModulesPath);
741
+ await fs.rm(nodeModulesPath, { recursive: true, force: true });
742
+ } catch {
743
+ // node_modules doesn't exist, which is fine
744
+ }
745
+ }
746
+
747
+ async run () {
748
+ console.log('MCP Server Template Generator');
749
+ console.log('==================================\n');
750
+
751
+ try {
752
+ const config = await this.collectConfiguration();
753
+ const targetPath = await this.getTargetPath(config);
754
+
755
+ console.log(`\n📁 Creating project in: ${targetPath}`);
756
+ await this.createProject(targetPath, config);
757
+
758
+ console.log('\n✅ MCP Server template created successfully!');
759
+ console.log('\n📋 Next steps:');
760
+ console.log(` cd ${targetPath}`);
761
+ console.log(' npm install');
762
+ console.log(' npm run build');
763
+ console.log(' npm start');
764
+
765
+ process.exit(0);
766
+
767
+ } catch (error) {
768
+ console.error('\n❌ Error:', error.message);
769
+ console.error(error.stack);
770
+ process.exit(1);
771
+ }
772
+ }
773
+ }
774
+
775
+ function escapeRegExp (string) {
776
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
777
+ }
778
+
779
+ // Run the generator
780
+ const generator = new MCPGenerator();
781
+ generator.run().catch(console.error);