fa-mcp-sdk 0.4.29 → 0.4.32
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/README.md +5 -1
- package/cli-template/.claude/skills/gen-jwt/SKILL.md +113 -0
- package/cli-template/.gitattributes +1 -0
- package/cli-template/CLAUDE.md +14 -0
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +123 -0
- package/cli-template/package.json +1 -1
- package/config/_local.yaml +12 -0
- package/config/custom-environment-variables.yaml +1 -0
- package/config/default.yaml +12 -0
- package/config/local.yaml +7 -18
- package/dist/core/_types_/config.d.ts +3 -1
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/auth/admin-auth.d.ts +12 -1
- package/dist/core/auth/admin-auth.d.ts.map +1 -1
- package/dist/core/auth/admin-auth.js +124 -64
- package/dist/core/auth/admin-auth.js.map +1 -1
- package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
- package/dist/core/bootstrap/startup-info.js +1 -0
- package/dist/core/bootstrap/startup-info.js.map +1 -1
- package/dist/core/web/admin-router.d.ts.map +1 -1
- package/dist/core/web/admin-router.js +34 -25
- package/dist/core/web/admin-router.js.map +1 -1
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +71 -0
- package/dist/core/web/server-http.js.map +1 -1
- package/dist/core/web/static/styles.css +30 -0
- package/dist/core/web/static/token-gen/index.html +24 -2
- package/dist/core/web/static/token-gen/script.js +171 -34
- package/package.json +1 -1
- package/scripts/generate-jwt.js +191 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate JWT token for MCP server authentication.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <params>]
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* -u, --username Username (required). ENV: JWT_PAYLOAD_USERNAME
|
|
10
|
+
* -ttl Token lifetime: <N>s | <N>m | <N>d | <N>y (required). ENV: JWT_TTL
|
|
11
|
+
* -s, --service-name Service name (optional). ENV: JWT_PAYLOAD_SERVICE_NAME
|
|
12
|
+
* -p, --params Extra payload "key=value;key=value" (optional). ENV: JWT_PAYLOAD_PARAMS
|
|
13
|
+
*
|
|
14
|
+
* The encryptKey is read from config: webServer.auth.jwtToken.encryptKey
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import crypto from 'crypto';
|
|
18
|
+
import { readFileSync } from 'fs';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { dirname, resolve } from 'path';
|
|
21
|
+
import configModule from 'config';
|
|
22
|
+
|
|
23
|
+
// ── CLI argument parsing ────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function getArg (shortFlag, longFlag) {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (args[i] === shortFlag || args[i] === longFlag) {
|
|
29
|
+
return args[i + 1] || '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const username = getArg('-u', '--username') ?? process.env.JWT_PAYLOAD_USERNAME;
|
|
36
|
+
const ttlRaw = getArg('-ttl', '-ttl') ?? process.env.JWT_TTL;
|
|
37
|
+
const service = getArg('-s', '--service-name') ?? process.env.JWT_PAYLOAD_SERVICE_NAME;
|
|
38
|
+
const paramsRaw = getArg('-p', '--params') ?? process.env.JWT_PAYLOAD_PARAMS;
|
|
39
|
+
|
|
40
|
+
// ── Validation ──────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
if (!username || !username.trim()) {
|
|
43
|
+
console.error('Error: username is required (-u / --username or ENV JWT_PAYLOAD_USERNAME)');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!ttlRaw || !ttlRaw.trim()) {
|
|
48
|
+
console.error('Error: TTL is required (-ttl or ENV JWT_TTL). Format: <N>s | <N>m | <N>d | <N>y');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ttlMatch = /^(\d+)([smdy])$/.exec(ttlRaw.trim());
|
|
53
|
+
if (!ttlMatch) {
|
|
54
|
+
console.error(`Error: invalid TTL format "${ttlRaw}". Expected: <N>s | <N>m | <N>d | <N>y`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ttlValue = parseInt(ttlMatch[1], 10);
|
|
59
|
+
const ttlUnit = ttlMatch[2];
|
|
60
|
+
|
|
61
|
+
if (ttlValue <= 0) {
|
|
62
|
+
console.error('Error: TTL value must be greater than 0');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const TTL_MULTIPLIERS = { s: 1, m: 60, d: 86400, y: 31536000 };
|
|
67
|
+
const liveTimeSec = ttlValue * TTL_MULTIPLIERS[ttlUnit];
|
|
68
|
+
|
|
69
|
+
// ── Config ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
let encryptKey;
|
|
72
|
+
try {
|
|
73
|
+
encryptKey = configModule.get('webServer.auth.jwtToken.encryptKey');
|
|
74
|
+
} catch {
|
|
75
|
+
// config key not found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!encryptKey || String(encryptKey).trim() === '' || encryptKey === '***') {
|
|
79
|
+
console.error('Error: webServer.auth.jwtToken.encryptKey is not configured or has a placeholder value.');
|
|
80
|
+
console.error('Set it in config/local.yaml or via ENV WS_TOKEN_ENCRYPT_KEY');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Encryption (mirrors src/core/auth/jwt.ts) ───────────────────────
|
|
85
|
+
|
|
86
|
+
const ALGORITHM = 'aes-256-ctr';
|
|
87
|
+
const KEY = crypto
|
|
88
|
+
.createHash('sha256')
|
|
89
|
+
.update(String(encryptKey))
|
|
90
|
+
.digest('base64')
|
|
91
|
+
.substring(0, 32);
|
|
92
|
+
|
|
93
|
+
function encrypt (text) {
|
|
94
|
+
const buffer = Buffer.from(text);
|
|
95
|
+
const iv = crypto.randomBytes(16);
|
|
96
|
+
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
|
|
97
|
+
const encryptedBuf = Buffer.concat([iv, cipher.update(buffer), cipher.final()]);
|
|
98
|
+
return encryptedBuf.toString('hex');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Auto-detect service name if checkMCPName is enabled ─────────────
|
|
102
|
+
|
|
103
|
+
let effectiveService = service;
|
|
104
|
+
|
|
105
|
+
if ((!effectiveService || !effectiveService.trim())) {
|
|
106
|
+
let checkMCPName = false;
|
|
107
|
+
try {
|
|
108
|
+
checkMCPName = configModule.get('webServer.auth.jwtToken.checkMCPName');
|
|
109
|
+
} catch {
|
|
110
|
+
// config key not found
|
|
111
|
+
}
|
|
112
|
+
if (checkMCPName) {
|
|
113
|
+
// 1) Try SERVICE_NAME from .env
|
|
114
|
+
if (process.env.SERVICE_NAME && process.env.SERVICE_NAME.trim()) {
|
|
115
|
+
effectiveService = process.env.SERVICE_NAME.trim();
|
|
116
|
+
} else {
|
|
117
|
+
// 2) Fallback to package.json name
|
|
118
|
+
try {
|
|
119
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
120
|
+
const __dirname = dirname(__filename);
|
|
121
|
+
const pkgPath = resolve(__dirname, '..', 'package.json');
|
|
122
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
123
|
+
if (pkg.name) {
|
|
124
|
+
effectiveService = pkg.name;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// package.json not found or unreadable
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Build payload ───────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
const payload = {};
|
|
136
|
+
payload.user = username.trim().toLowerCase();
|
|
137
|
+
|
|
138
|
+
if (effectiveService && effectiveService.trim()) {
|
|
139
|
+
payload.service = effectiveService.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Parse extra params: "key1=value1;key2=value2"
|
|
143
|
+
if (paramsRaw && paramsRaw.trim()) {
|
|
144
|
+
const pairs = paramsRaw.trim().split(';');
|
|
145
|
+
for (const pair of pairs) {
|
|
146
|
+
const eqIdx = pair.indexOf('=');
|
|
147
|
+
if (eqIdx <= 0) {
|
|
148
|
+
console.error(`Error: invalid param format "${pair}". Expected "key=value"`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const key = pair.substring(0, eqIdx).trim();
|
|
152
|
+
const value = pair.substring(eqIdx + 1).trim();
|
|
153
|
+
if (!key) {
|
|
154
|
+
console.error(`Error: empty key in param "${pair}"`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
payload[key] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const expire = Date.now() + (liveTimeSec * 1000);
|
|
162
|
+
payload.expire = expire;
|
|
163
|
+
payload.iat = new Date().toISOString();
|
|
164
|
+
|
|
165
|
+
// ── Generate token ──────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const token = `${expire}.${encrypt(JSON.stringify(payload))}`;
|
|
168
|
+
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log('JWT Token generated successfully');
|
|
171
|
+
console.log('─'.repeat(50));
|
|
172
|
+
console.log(` User: ${payload.user}`);
|
|
173
|
+
if (payload.service) {
|
|
174
|
+
console.log(` Service: ${payload.service}`);
|
|
175
|
+
}
|
|
176
|
+
console.log(` TTL: ${ttlRaw} (${liveTimeSec} seconds)`);
|
|
177
|
+
console.log(` Expires: ${new Date(expire).toISOString()}`);
|
|
178
|
+
if (Object.keys(payload).filter((k) => !['user', 'service', 'expire', 'iat'].includes(k)).length) {
|
|
179
|
+
const extra = Object.entries(payload)
|
|
180
|
+
.filter(([k]) => !['user', 'service', 'expire', 'iat'].includes(k))
|
|
181
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
182
|
+
.join('; ');
|
|
183
|
+
console.log(` Params: ${extra}`);
|
|
184
|
+
}
|
|
185
|
+
console.log('─'.repeat(50));
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(token);
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('__PAYLOAD_JSON__');
|
|
190
|
+
console.log(JSON.stringify({ ...payload, ttl: ttlRaw, expire_iso: new Date(expire).toISOString() }));
|
|
191
|
+
console.log('__END_PAYLOAD_JSON__');
|