fa-mcp-sdk 0.2.38 → 0.2.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fa-mcp.js +781 -0
- package/cli-template/.editorconfig +13 -0
- package/cli-template/.env.example +29 -0
- package/cli-template/.envrc +3 -0
- package/cli-template/.run/== START ==.run.xml +14 -0
- package/cli-template/.run/TEST HTTP.run.xml +5 -0
- package/cli-template/.run/TEST SSE.run.xml +5 -0
- package/cli-template/.run/TEST STDIO.run.xml +5 -0
- package/cli-template/.run/TEST search.run.xml +11 -0
- package/cli-template/.run/cb.run.xml +12 -0
- package/cli-template/.run/ci.run.xml +12 -0
- package/cli-template/.run/kill-port 3030.run.xml +5 -0
- package/cli-template/.run/lint.run.xml +12 -0
- package/cli-template/.run/lint_fix.run.xml +12 -0
- package/cli-template/.run/reinstall.run.xml +12 -0
- package/cli-template/.run/remove-nul.js.run.xml +5 -0
- package/cli-template/LICENSE +21 -0
- package/cli-template/config/_local.yaml +64 -0
- package/cli-template/config/custom-environment-variables.yaml +33 -0
- package/cli-template/config/default.yaml +101 -0
- package/cli-template/config/development.yaml +4 -0
- package/cli-template/config/production.yaml +4 -0
- package/cli-template/config/test.yaml +26 -0
- package/cli-template/deploy/.gitkeep +0 -0
- package/cli-template/deploy/config.example.yml +3 -0
- package/cli-template/deploy/mcp-template.com.conf +58 -0
- package/cli-template/deploy/pm2.config.js +30 -0
- package/cli-template/deploy/pm2reg.sh +49 -0
- package/cli-template/deploy/srv.sh +359 -0
- package/cli-template/deploy/srv.sh.readme.md +347 -0
- package/cli-template/eslint.config.js +139 -0
- package/cli-template/jest.config.js +30 -0
- package/cli-template/package.json +73 -0
- package/cli-template/scripts/kill-port.js +107 -0
- package/cli-template/scripts/npm/patch_node_modules.js +9 -0
- package/cli-template/scripts/npm/run.js +31 -0
- package/cli-template/scripts/npm/yarn-ci.ps1 +16 -0
- package/cli-template/scripts/npm/yarn-ci.sh +8 -0
- package/cli-template/scripts/npm/yarn-reinstall.ps1 +54 -0
- package/cli-template/scripts/npm/yarn-reinstall.sh +10 -0
- package/cli-template/scripts/pre-commit +58 -0
- package/cli-template/scripts/remove-nul.js +53 -0
- package/cli-template/src/_types_/common.d.ts +27 -0
- package/cli-template/src/api/router.ts +35 -0
- package/cli-template/src/api/swagger.ts +167 -0
- package/cli-template/src/asset/favicon.svg +4 -0
- package/cli-template/src/custom-resources.ts +11 -0
- package/cli-template/src/prompts/agent-brief.ts +8 -0
- package/cli-template/src/prompts/agent-prompt.ts +1 -0
- package/cli-template/src/prompts/custom-prompts.ts +12 -0
- package/cli-template/src/start.ts +84 -0
- package/cli-template/src/tools/handle-tool-call.ts +55 -0
- package/cli-template/src/tools/tools.ts +88 -0
- package/cli-template/tests/jest-simple-reporter.js +10 -0
- package/cli-template/tests/mcp/sse/mcp-sse-client-handling.md +111 -0
- package/cli-template/tests/mcp/sse/test-sse-npm-package.js +96 -0
- package/cli-template/tests/mcp/test-cases.js +143 -0
- package/cli-template/tests/mcp/test-http.js +63 -0
- package/cli-template/tests/mcp/test-sse.js +67 -0
- package/cli-template/tests/mcp/test-stdio.js +78 -0
- package/cli-template/tests/utils.ts +154 -0
- package/cli-template/tsconfig.json +48 -0
- package/cli-template/update.cjs +631 -0
- package/dist/core/_types_/active-directory-config.d.ts +24 -0
- package/dist/core/_types_/active-directory-config.d.ts.map +1 -0
- package/dist/core/_types_/active-directory-config.js +2 -0
- package/dist/core/_types_/active-directory-config.js.map +1 -0
- package/dist/core/bootstrap/init-config.d.ts.map +1 -1
- package/dist/core/bootstrap/init-config.js +14 -3
- package/dist/core/bootstrap/init-config.js.map +1 -1
- package/dist/core/bootstrap/startup-info.js +1 -1
- package/dist/core/bootstrap/startup-info.js.map +1 -1
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/init-mcp-server.js +1 -1
- package/dist/core/init-mcp-server.js.map +1 -1
- package/dist/core/token/gen-token-app/gen-token-server.d.ts.map +1 -1
- package/dist/core/token/gen-token-app/gen-token-server.js +85 -9
- package/dist/core/token/gen-token-app/gen-token-server.js.map +1 -1
- package/dist/core/token/gen-token-app/html.d.ts +8 -1
- package/dist/core/token/gen-token-app/html.d.ts.map +1 -1
- package/dist/core/token/gen-token-app/html.js +98 -2
- package/dist/core/token/gen-token-app/html.js.map +1 -1
- package/dist/core/token/gen-token-app/ntlm-auth-options.d.ts +4 -0
- package/dist/core/token/gen-token-app/ntlm-auth-options.d.ts.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-auth-options.js +85 -0
- package/dist/core/token/gen-token-app/ntlm-auth-options.js.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-domain-config.d.ts +16 -0
- package/dist/core/token/gen-token-app/ntlm-domain-config.d.ts.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-domain-config.js +71 -0
- package/dist/core/token/gen-token-app/ntlm-domain-config.js.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-integration.d.ts +6 -0
- package/dist/core/token/gen-token-app/ntlm-integration.d.ts.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-integration.js +73 -0
- package/dist/core/token/gen-token-app/ntlm-integration.js.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-session-storage.d.ts +16 -0
- package/dist/core/token/gen-token-app/ntlm-session-storage.d.ts.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-session-storage.js +74 -0
- package/dist/core/token/gen-token-app/ntlm-session-storage.js.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-templates.d.ts +21 -0
- package/dist/core/token/gen-token-app/ntlm-templates.d.ts.map +1 -0
- package/dist/core/token/gen-token-app/ntlm-templates.js +211 -0
- package/dist/core/token/gen-token-app/ntlm-templates.js.map +1 -0
- package/dist/core/token/{token.d.ts → token-auth.d.ts} +1 -1
- package/dist/core/token/token-auth.d.ts.map +1 -0
- package/dist/core/token/{token.js → token-auth.js} +4 -6
- package/dist/core/token/token-auth.js.map +1 -0
- package/dist/core/token/token-core.d.ts +5 -1
- package/dist/core/token/token-core.d.ts.map +1 -1
- package/dist/core/token/token-core.js +13 -3
- package/dist/core/token/token-core.js.map +1 -1
- package/dist/core/web/about-page/render.js +1 -1
- package/dist/core/web/about-page/render.js.map +1 -1
- package/dist/core/web/server-http.js +1 -1
- package/dist/core/web/server-http.js.map +1 -1
- package/package.json +10 -3
- package/dist/core/token/token.d.ts.map +0 -1
- 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);
|