fa-mcp-sdk 0.4.27 → 0.4.30

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 (34) hide show
  1. package/README.md +5 -1
  2. package/bin/fa-mcp.js +1 -1
  3. package/cli-template/.claude/skills/gen-jwt/SKILL.md +113 -0
  4. package/cli-template/CLAUDE.md +14 -0
  5. package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +1 -1
  6. package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +123 -0
  7. package/cli-template/package.json +1 -1
  8. package/config/_local.yaml +12 -0
  9. package/config/custom-environment-variables.yaml +1 -0
  10. package/config/default.yaml +12 -0
  11. package/config/local.yaml +7 -18
  12. package/dist/core/_types_/config.d.ts +3 -1
  13. package/dist/core/_types_/config.d.ts.map +1 -1
  14. package/dist/core/auth/admin-auth.d.ts +12 -1
  15. package/dist/core/auth/admin-auth.d.ts.map +1 -1
  16. package/dist/core/auth/admin-auth.js +124 -64
  17. package/dist/core/auth/admin-auth.js.map +1 -1
  18. package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
  19. package/dist/core/bootstrap/startup-info.js +1 -0
  20. package/dist/core/bootstrap/startup-info.js.map +1 -1
  21. package/dist/core/web/admin-router.d.ts.map +1 -1
  22. package/dist/core/web/admin-router.js +34 -25
  23. package/dist/core/web/admin-router.js.map +1 -1
  24. package/dist/core/web/server-http.d.ts.map +1 -1
  25. package/dist/core/web/server-http.js +71 -0
  26. package/dist/core/web/server-http.js.map +1 -1
  27. package/dist/core/web/static/agent-tester/index.html +8 -3
  28. package/dist/core/web/static/agent-tester/script.js +59 -15
  29. package/dist/core/web/static/agent-tester/styles.css +27 -1
  30. package/dist/core/web/static/styles.css +30 -0
  31. package/dist/core/web/static/token-gen/index.html +24 -2
  32. package/dist/core/web/static/token-gen/script.js +171 -34
  33. package/package.json +1 -1
  34. package/scripts/generate-jwt.js +191 -0
@@ -5,7 +5,8 @@ let keyValuePairCount = 0;
5
5
  // ===========================
6
6
 
7
7
  const AUTH_TOKEN_KEY = 'adminAuthToken';
8
- let requiresBearerToken = false;
8
+ let requiresFrontendAuth = false;
9
+ let authMethods = []; // ['token', 'basic']
9
10
 
10
11
  // Get stored auth token from sessionStorage
11
12
  function getStoredToken () {
@@ -22,53 +23,114 @@ function clearStoredToken () {
22
23
  sessionStorage.removeItem(AUTH_TOKEN_KEY);
23
24
  }
24
25
 
25
- // Show token authentication modal
26
- function showTokenModal (errorMessage = null) {
26
+ // Show authentication modal
27
+ function showAuthModal (errorMessage = null) {
27
28
  const modal = document.getElementById('tokenModal');
28
- const errorDiv = document.getElementById('tokenAuthError');
29
+
30
+ // Clear errors on both forms
31
+ const tokenError = document.getElementById('tokenAuthError');
32
+ const basicError = document.getElementById('basicAuthError');
33
+ tokenError.style.display = 'none';
34
+ basicError.style.display = 'none';
29
35
 
30
36
  if (errorMessage) {
31
- errorDiv.innerHTML = `<strong>Error:</strong> ${errorMessage}`;
32
- errorDiv.style.display = 'block';
33
- } else {
34
- errorDiv.style.display = 'none';
37
+ // Show error in the active form
38
+ const activeError = document.getElementById('basicAuthForm').style.display !== 'none'
39
+ ? basicError : tokenError;
40
+ activeError.innerHTML = `<strong>Error:</strong> ${errorMessage}`;
41
+ activeError.style.display = 'block';
35
42
  }
36
43
 
37
44
  modal.style.display = 'flex';
38
45
  }
39
46
 
40
- // Hide token authentication modal
41
- function hideTokenModal () {
47
+ // Hide authentication modal
48
+ function hideAuthModal () {
42
49
  const modal = document.getElementById('tokenModal');
43
50
  modal.style.display = 'none';
44
51
  }
45
52
 
46
- // Authenticated fetch wrapper - adds Authorization header if token auth is required
53
+ // Setup auth tabs and forms based on available methods
54
+ function setupAuthForms (methods) {
55
+ const hasToken = methods.includes('token');
56
+ const hasBasic = methods.includes('basic');
57
+
58
+ const tabs = document.getElementById('adminAuthTabs');
59
+ const tokenForm = document.getElementById('tokenAuthForm');
60
+ const basicForm = document.getElementById('basicAuthForm');
61
+
62
+ if (hasToken && hasBasic) {
63
+ // Show tabs, default to token
64
+ tabs.style.display = 'flex';
65
+ tokenForm.style.display = 'block';
66
+ basicForm.style.display = 'none';
67
+ bindAuthTabs();
68
+ } else if (hasBasic) {
69
+ // Basic only
70
+ tabs.style.display = 'none';
71
+ tokenForm.style.display = 'none';
72
+ basicForm.style.display = 'block';
73
+ } else {
74
+ // Token only (default)
75
+ tabs.style.display = 'none';
76
+ tokenForm.style.display = 'block';
77
+ basicForm.style.display = 'none';
78
+ }
79
+ }
80
+
81
+ // Bind tab click handlers
82
+ function bindAuthTabs () {
83
+ const tabButtons = document.querySelectorAll('.admin-auth-tab');
84
+ const tokenForm = document.getElementById('tokenAuthForm');
85
+ const basicForm = document.getElementById('basicAuthForm');
86
+
87
+ tabButtons.forEach((btn) => {
88
+ btn.addEventListener('click', () => {
89
+ // Update active tab
90
+ tabButtons.forEach((b) => b.classList.remove('active'));
91
+ btn.classList.add('active');
92
+
93
+ // Hide errors
94
+ document.getElementById('tokenAuthError').style.display = 'none';
95
+ document.getElementById('basicAuthError').style.display = 'none';
96
+
97
+ // Toggle forms
98
+ const tab = btn.getAttribute('data-tab');
99
+ if (tab === 'basic') {
100
+ tokenForm.style.display = 'none';
101
+ basicForm.style.display = 'block';
102
+ } else {
103
+ tokenForm.style.display = 'block';
104
+ basicForm.style.display = 'none';
105
+ }
106
+ });
107
+ });
108
+ }
109
+
110
+ // Authenticated fetch wrapper - adds Authorization header
47
111
  async function authFetch (url, options = {}) {
48
112
  const token = getStoredToken();
49
113
 
50
- if (requiresBearerToken && token) {
114
+ if (requiresFrontendAuth && token) {
51
115
  options.headers = {
52
116
  ...options.headers,
53
- 'Authorization': `Bearer ${token}`,
117
+ 'Authorization': token,
54
118
  };
55
119
  }
56
120
 
57
121
  const response = await fetch(url, options);
58
122
 
59
- // Handle 401 Unauthorized - show token modal if available
60
- if (response.status === 401 && requiresBearerToken) {
123
+ // Handle 401 Unauthorized
124
+ if (response.status === 401 && requiresFrontendAuth) {
61
125
  clearStoredToken();
62
126
  const errorData = await response.json().catch(() => ({}));
63
127
  const errorMessage = errorData.error || 'Authentication failed';
64
128
 
65
- // Try to show modal, but if it's not available, throw with descriptive error
66
129
  const modal = document.getElementById('tokenModal');
67
130
  if (modal) {
68
- showTokenModal(errorMessage + '. Please enter a valid token.');
131
+ showAuthModal(errorMessage + '. Please authenticate again.');
69
132
  }
70
133
 
71
- // Throw error with status code for form error handling
72
134
  const error = new Error(`401 Unauthorized: ${errorMessage}`);
73
135
  error.status = 401;
74
136
  throw error;
@@ -84,13 +146,15 @@ async function initializeAuth () {
84
146
  const response = await fetch('/admin/api/auth-config');
85
147
  const config = await response.json();
86
148
 
87
- if (config.success && config.requiresBearerToken) {
88
- requiresBearerToken = true;
149
+ if (config.success && config.requiresFrontendAuth) {
150
+ requiresFrontendAuth = true;
151
+ authMethods = config.methods || [];
152
+ setupAuthForms(authMethods);
89
153
 
90
154
  // Check if we have a stored token
91
155
  const storedToken = getStoredToken();
92
156
  if (!storedToken) {
93
- showTokenModal();
157
+ showAuthModal();
94
158
  return false;
95
159
  }
96
160
 
@@ -101,13 +165,36 @@ async function initializeAuth () {
101
165
 
102
166
  if (!verifyData.success || !verifyData.isAuthenticated) {
103
167
  clearStoredToken();
104
- showTokenModal('Token is invalid or expired.');
168
+ showAuthModal('Token is invalid or expired.');
105
169
  return false;
106
170
  }
107
171
  } catch {
108
172
  // authFetch already handles 401 and shows modal
109
173
  return false;
110
174
  }
175
+ } else if (config.success && config.requiresBearerToken) {
176
+ // Backward compat: old-style bearer-only
177
+ requiresFrontendAuth = true;
178
+ authMethods = ['token'];
179
+ setupAuthForms(authMethods);
180
+
181
+ const storedToken = getStoredToken();
182
+ if (!storedToken) {
183
+ showAuthModal();
184
+ return false;
185
+ }
186
+
187
+ try {
188
+ const verifyResponse = await authFetch('/admin/api/auth-status');
189
+ const verifyData = await verifyResponse.json();
190
+ if (!verifyData.success || !verifyData.isAuthenticated) {
191
+ clearStoredToken();
192
+ showAuthModal('Token is invalid or expired.');
193
+ return false;
194
+ }
195
+ } catch {
196
+ return false;
197
+ }
111
198
  }
112
199
 
113
200
  return true;
@@ -129,32 +216,81 @@ function setupTokenAuthForm () {
129
216
  const token = tokenInput.value.trim();
130
217
 
131
218
  if (!token) {
132
- showTokenModal('Please enter a token.');
219
+ showAuthModal('Please enter a token.');
133
220
  return;
134
221
  }
135
222
 
136
- // Store token and try to authenticate
137
- storeToken(token);
223
+ // Store as Bearer token
224
+ storeToken(`Bearer ${token}`);
138
225
 
139
226
  try {
140
227
  const response = await authFetch('/admin/api/auth-status');
141
228
  const data = await response.json();
142
229
 
143
230
  if (data.success && data.isAuthenticated) {
144
- hideTokenModal();
231
+ hideAuthModal();
145
232
  tokenInput.value = '';
146
- // Reload auth status and initialize form
147
233
  loadAuthStatus();
148
234
  initializeForm();
149
235
  } else {
150
236
  clearStoredToken();
151
- showTokenModal(data.error || 'Invalid token.');
237
+ showAuthModal(data.error || 'Invalid token.');
152
238
  }
153
239
  } catch (error) {
154
- // Error already handled in authFetch
155
240
  if (error.message !== 'Unauthorized') {
156
241
  clearStoredToken();
157
- showTokenModal('Authentication failed: ' + error.message);
242
+ showAuthModal('Authentication failed: ' + error.message);
243
+ }
244
+ }
245
+ });
246
+ }
247
+
248
+ // Handle basic authentication form submission
249
+ function setupBasicAuthForm () {
250
+ const form = document.getElementById('basicAuthForm');
251
+ if (!form) {return;}
252
+
253
+ form.addEventListener('submit', async (e) => {
254
+ e.preventDefault();
255
+
256
+ const usernameInput = document.getElementById('authUsername');
257
+ const passwordInput = document.getElementById('authPassword');
258
+ const username = usernameInput.value.trim();
259
+ const password = passwordInput.value;
260
+
261
+ if (!username || !password) {
262
+ const errorDiv = document.getElementById('basicAuthError');
263
+ errorDiv.innerHTML = '<strong>Error:</strong> Please enter username and password.';
264
+ errorDiv.style.display = 'block';
265
+ return;
266
+ }
267
+
268
+ // Store as Basic auth header
269
+ const encoded = btoa(`${username}:${password}`);
270
+ storeToken(`Basic ${encoded}`);
271
+
272
+ try {
273
+ const response = await authFetch('/admin/api/auth-status');
274
+ const data = await response.json();
275
+
276
+ if (data.success && data.isAuthenticated) {
277
+ hideAuthModal();
278
+ usernameInput.value = '';
279
+ passwordInput.value = '';
280
+ loadAuthStatus();
281
+ initializeForm();
282
+ } else {
283
+ clearStoredToken();
284
+ const errorDiv = document.getElementById('basicAuthError');
285
+ errorDiv.innerHTML = `<strong>Error:</strong> ${data.error || 'Invalid credentials.'}`;
286
+ errorDiv.style.display = 'block';
287
+ }
288
+ } catch (error) {
289
+ if (error.status !== 401) {
290
+ clearStoredToken();
291
+ const errorDiv = document.getElementById('basicAuthError');
292
+ errorDiv.innerHTML = `<strong>Error:</strong> Authentication failed: ${error.message}`;
293
+ errorDiv.style.display = 'block';
158
294
  }
159
295
  }
160
296
  });
@@ -514,10 +650,10 @@ async function initializeForm () {
514
650
  // eslint-disable-next-line unused-imports/no-unused-vars
515
651
  async function logout () {
516
652
  try {
517
- // For token-based auth, just clear the stored token
518
- if (requiresBearerToken) {
653
+ // For frontend auth, clear the stored credentials
654
+ if (requiresFrontendAuth) {
519
655
  clearStoredToken();
520
- showTokenModal();
656
+ showAuthModal();
521
657
  // Clear auth status display
522
658
  const container = document.getElementById('authStatusContainer');
523
659
  if (container) {
@@ -547,8 +683,9 @@ async function logout () {
547
683
 
548
684
  // Initialization on page load
549
685
  document.addEventListener('DOMContentLoaded', async () => {
550
- // Setup token auth form handler
686
+ // Setup auth form handlers
551
687
  setupTokenAuthForm();
688
+ setupBasicAuthForm();
552
689
 
553
690
  // Initialize authentication (check if token is needed and valid)
554
691
  const authOk = await initializeAuth();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fa-mcp-sdk",
3
3
  "productName": "FA MCP SDK",
4
- "version": "0.4.27",
4
+ "version": "0.4.30",
5
5
  "description": "Core infrastructure and templates for building Model Context Protocol (MCP) servers with TypeScript",
6
6
  "type": "module",
7
7
  "main": "dist/core/index.js",
@@ -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__');