fa-mcp-sdk 0.2.182 → 0.2.192

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 (31) hide show
  1. package/cli-template/.claude/agents/fa-mcp-sdk.md +158 -0
  2. package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +216 -0
  3. package/cli-template/FA-MCP-SDK-DOC/01-getting-started.md +209 -0
  4. package/cli-template/FA-MCP-SDK-DOC/02-tools-and-api.md +321 -0
  5. package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +415 -0
  6. package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +544 -0
  7. package/cli-template/FA-MCP-SDK-DOC/05-ad-authorization.md +476 -0
  8. package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +394 -0
  9. package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +171 -0
  10. package/dist/core/_types_/types.d.ts +0 -5
  11. package/dist/core/_types_/types.d.ts.map +1 -1
  12. package/dist/core/index.d.ts +2 -1
  13. package/dist/core/index.d.ts.map +1 -1
  14. package/dist/core/index.js +2 -0
  15. package/dist/core/index.js.map +1 -1
  16. package/dist/core/web/home-api.js +1 -1
  17. package/dist/core/web/home-api.js.map +1 -1
  18. package/dist/core/web/openapi.d.ts +64 -0
  19. package/dist/core/web/openapi.d.ts.map +1 -0
  20. package/dist/core/web/openapi.js +235 -0
  21. package/dist/core/web/openapi.js.map +1 -0
  22. package/dist/core/web/server-http.d.ts.map +1 -1
  23. package/dist/core/web/server-http.js +11 -9
  24. package/dist/core/web/server-http.js.map +1 -1
  25. package/dist/core/web/static/home/index.html +4 -2
  26. package/dist/core/web/static/home/script.js +2 -2
  27. package/package.json +9 -12
  28. package/src/template/api/router.ts +66 -4
  29. package/src/template/start.ts +0 -5
  30. package/cli-template/FA-MCP-SDK.md +0 -2540
  31. package/src/template/api/swagger.ts +0 -167
@@ -0,0 +1,476 @@
1
+ # Advanced AD Group Authorization
2
+
3
+ This document demonstrates how to implement additional authorization based on Active Directory (AD)
4
+ group membership. These examples assume JWT token authentication (`jwtToken`) is configured,
5
+ and the user information is extracted from the JWT payload.
6
+
7
+ ## Configuration for AD Group Authorization
8
+
9
+ First, extend your configuration to include the required AD group:
10
+
11
+ **`src/_types_/custom-config.ts`:**
12
+ ```typescript
13
+ import { AppConfig } from 'fa-mcp-sdk';
14
+
15
+ export interface IGroupAccessConfig {
16
+ groupAccess: {
17
+ /** AD group required for access */
18
+ requiredGroup: string;
19
+ /** Bypass group check for debugging (default: false) */
20
+ bypassGroupCheck?: boolean;
21
+ /** Cache TTL in seconds (default: 300) */
22
+ cacheTtlSeconds?: number;
23
+ };
24
+ }
25
+
26
+ export interface CustomAppConfig extends AppConfig, IGroupAccessConfig {}
27
+ ```
28
+
29
+ **`config/default.yaml`:**
30
+ ```yaml
31
+ groupAccess:
32
+ requiredGroup: "DOMAIN\\MCP-Users"
33
+ bypassGroupCheck: false
34
+ cacheTtlSeconds: 300
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Example 1: HTTP Server Level Access Restriction
40
+
41
+ This example uses `customAuthValidator` to check AD group membership at the HTTP server level.
42
+ If the user is not in the required group, a 403 Forbidden error is returned before any
43
+ MCP request processing.
44
+
45
+ **`src/start.ts`:**
46
+ ```typescript
47
+ import {
48
+ appConfig,
49
+ initMcpServer,
50
+ McpServerData,
51
+ CustomAuthValidator,
52
+ AuthResult,
53
+ initADGroupChecker,
54
+ checkJwtToken,
55
+ } from 'fa-mcp-sdk';
56
+ import { tools } from './tools/tools.js';
57
+ import { handleToolCall } from './tools/handle-tool-call.js';
58
+ import { AGENT_BRIEF } from './prompts/agent-brief.js';
59
+ import { AGENT_PROMPT } from './prompts/agent-prompt.js';
60
+ import { CustomAppConfig } from './_types_/custom-config.js';
61
+
62
+ // Get typed config
63
+ const config = appConfig as CustomAppConfig;
64
+
65
+ // Initialize AD group checker
66
+ const { isUserInGroup } = initADGroupChecker();
67
+
68
+ /**
69
+ * Custom authentication validator with AD group membership check
70
+ * Returns 403 Forbidden if user is not in the required AD group
71
+ */
72
+ const customAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> => {
73
+ const authHeader = req.headers.authorization;
74
+
75
+ if (!authHeader?.startsWith('Bearer ')) {
76
+ return { success: false, error: 'Missing or invalid Authorization header' };
77
+ }
78
+
79
+ const token = authHeader.slice(7);
80
+
81
+ // Validate JWT token
82
+ const tokenResult = checkJwtToken({ token });
83
+ if (tokenResult.errorReason) {
84
+ return { success: false, error: tokenResult.errorReason };
85
+ }
86
+
87
+ const payload = tokenResult.payload;
88
+ if (!payload?.user) {
89
+ return { success: false, error: 'Invalid token: missing user' };
90
+ }
91
+
92
+ const username = payload.user;
93
+
94
+ // Bypass group check if configured (for debugging)
95
+ if (config.groupAccess.bypassGroupCheck) {
96
+ return {
97
+ success: true,
98
+ authType: 'jwtToken',
99
+ username,
100
+ payload,
101
+ isTokenDecrypted: tokenResult.isTokenDecrypted,
102
+ };
103
+ }
104
+
105
+ // Check AD group membership
106
+ const requiredGroup = config.groupAccess.requiredGroup;
107
+ try {
108
+ const isInGroup = await isUserInGroup(username, requiredGroup);
109
+
110
+ if (!isInGroup) {
111
+ return {
112
+ success: false,
113
+ error: `Forbidden: User '${username}' is not a member of group '${requiredGroup}'`,
114
+ };
115
+ }
116
+
117
+ return {
118
+ success: true,
119
+ authType: 'jwtToken',
120
+ username,
121
+ payload,
122
+ isTokenDecrypted: tokenResult.isTokenDecrypted,
123
+ };
124
+ } catch (error) {
125
+ const errorMessage = error instanceof Error ? error.message : String(error);
126
+ return {
127
+ success: false,
128
+ error: `AD group check failed: ${errorMessage}`,
129
+ };
130
+ }
131
+ };
132
+
133
+ const startProject = async (): Promise<void> => {
134
+ const serverData: McpServerData = {
135
+ tools,
136
+ toolHandler: handleToolCall,
137
+ agentBrief: AGENT_BRIEF,
138
+ agentPrompt: AGENT_PROMPT,
139
+
140
+ // Enable custom authentication with AD group check
141
+ customAuthValidator,
142
+
143
+ // ... other configuration
144
+ };
145
+
146
+ await initMcpServer(serverData);
147
+ };
148
+
149
+ startProject().catch(console.error);
150
+ ```
151
+
152
+ **Result**: If the user is not in the required AD group, they receive HTTP 403 Forbidden
153
+ response before any MCP processing occurs.
154
+
155
+ ---
156
+
157
+ ## Example 2: Access Restriction to ALL MCP Tools
158
+
159
+ This example restricts access to all MCP tools by checking AD group membership in the
160
+ `toolHandler` function. If the user is not in the required group, the tool call returns
161
+ an MCP error with "Forbidden" message.
162
+
163
+ **`src/tools/handle-tool-call.ts`:**
164
+ ```typescript
165
+ import {
166
+ formatToolResult,
167
+ ToolExecutionError,
168
+ logger,
169
+ appConfig,
170
+ initADGroupChecker,
171
+ } from 'fa-mcp-sdk';
172
+ import { CustomAppConfig } from '../_types_/custom-config.js';
173
+
174
+ // Get typed config
175
+ const config = appConfig as CustomAppConfig;
176
+
177
+ // Initialize AD group checker
178
+ const { isUserInGroup } = initADGroupChecker();
179
+
180
+ /**
181
+ * Check if user has access to MCP tools based on AD group membership
182
+ */
183
+ async function checkToolAccess(payload: { user: string; [key: string]: any } | undefined): Promise<void> {
184
+ // Skip check if bypass is enabled
185
+ if (config.groupAccess.bypassGroupCheck) {
186
+ return;
187
+ }
188
+
189
+ if (!payload?.user) {
190
+ throw new ToolExecutionError('authorization', 'Forbidden: User information not available');
191
+ }
192
+
193
+ const username = payload.user;
194
+ const requiredGroup = config.groupAccess.requiredGroup;
195
+
196
+ try {
197
+ const isInGroup = await isUserInGroup(username, requiredGroup);
198
+
199
+ if (!isInGroup) {
200
+ throw new ToolExecutionError(
201
+ 'authorization',
202
+ `Forbidden: User '${username}' is not authorized to use MCP tools. ` +
203
+ `Required group: '${requiredGroup}'`
204
+ );
205
+ }
206
+ } catch (error) {
207
+ if (error instanceof ToolExecutionError) {
208
+ throw error;
209
+ }
210
+ const errorMessage = error instanceof Error ? error.message : String(error);
211
+ throw new ToolExecutionError('authorization', `Forbidden: AD group check failed - ${errorMessage}`);
212
+ }
213
+ }
214
+
215
+ export const handleToolCall = async (params: {
216
+ name: string;
217
+ arguments?: any;
218
+ headers?: Record<string, string>;
219
+ payload?: { user: string; [key: string]: any };
220
+ }): Promise<any> => {
221
+ const { name, arguments: args, headers, payload } = params;
222
+
223
+ logger.info(`Tool called: ${name} by user: ${payload?.user || 'unknown'}`);
224
+
225
+ // Check AD group membership for ALL tools
226
+ await checkToolAccess(payload);
227
+
228
+ try {
229
+ switch (name) {
230
+ case 'my_tool':
231
+ return await handleMyTool(args);
232
+ case 'another_tool':
233
+ return await handleAnotherTool(args);
234
+ default:
235
+ throw new ToolExecutionError(name, `Unknown tool: ${name}`);
236
+ }
237
+ } catch (error) {
238
+ logger.error(`Tool execution failed for ${name}:`, error);
239
+ throw error;
240
+ }
241
+ };
242
+
243
+ async function handleMyTool(args: any): Promise<any> {
244
+ // Tool implementation
245
+ return formatToolResult({ message: 'Tool executed successfully', args });
246
+ }
247
+
248
+ async function handleAnotherTool(args: any): Promise<any> {
249
+ // Tool implementation
250
+ return formatToolResult({ message: 'Another tool executed', args });
251
+ }
252
+ ```
253
+
254
+ **Result**: If the user is not in the required AD group, any tool call returns an MCP error:
255
+ ```json
256
+ {
257
+ "jsonrpc": "2.0",
258
+ "error": {
259
+ "code": -32603,
260
+ "message": "Forbidden: User 'john.doe' is not authorized to use MCP tools. Required group: 'DOMAIN\\MCP-Users'"
261
+ },
262
+ "id": 1
263
+ }
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Example 3: Access Restriction to a SPECIFIC MCP Tool
269
+
270
+ This example restricts access to specific MCP tools based on AD group membership.
271
+ Different tools can require different AD groups.
272
+
273
+ **`src/_types_/custom-config.ts`:**
274
+ ```typescript
275
+ import { AppConfig } from 'fa-mcp-sdk';
276
+
277
+ export interface IToolGroupAccessConfig {
278
+ toolGroupAccess: {
279
+ /** Default group required for tools without specific configuration */
280
+ defaultGroup?: string;
281
+ /** Specific group requirements per tool */
282
+ tools: Record<string, {
283
+ /** AD group required for this tool */
284
+ requiredGroup: string;
285
+ /** Allow access without group check (default: false) */
286
+ public?: boolean;
287
+ }>;
288
+ /** Bypass all group checks (for debugging) */
289
+ bypassGroupCheck?: boolean;
290
+ };
291
+ }
292
+
293
+ export interface CustomAppConfig extends AppConfig, IToolGroupAccessConfig {}
294
+ ```
295
+
296
+ **`config/default.yaml`:**
297
+ ```yaml
298
+ toolGroupAccess:
299
+ defaultGroup: "DOMAIN\\MCP-Users"
300
+ bypassGroupCheck: false
301
+ tools:
302
+ get_public_data:
303
+ public: true # No group check required
304
+ get_user_data:
305
+ requiredGroup: "DOMAIN\\MCP-Users"
306
+ modify_data:
307
+ requiredGroup: "DOMAIN\\MCP-DataModifiers"
308
+ admin_operation:
309
+ requiredGroup: "DOMAIN\\MCP-Admins"
310
+ ```
311
+
312
+ **`src/tools/handle-tool-call.ts`:**
313
+ ```typescript
314
+ import {
315
+ formatToolResult,
316
+ ToolExecutionError,
317
+ logger,
318
+ appConfig,
319
+ initADGroupChecker,
320
+ } from 'fa-mcp-sdk';
321
+ import { CustomAppConfig } from '../_types_/custom-config.js';
322
+
323
+ // Get typed config
324
+ const config = appConfig as CustomAppConfig;
325
+
326
+ // Initialize AD group checker
327
+ const { isUserInGroup } = initADGroupChecker();
328
+
329
+ /**
330
+ * Check if user has access to a specific tool based on AD group membership
331
+ */
332
+ async function checkToolAccess(
333
+ toolName: string,
334
+ payload: { user: string; [key: string]: any } | undefined
335
+ ): Promise<void> {
336
+ const toolAccess = config.toolGroupAccess;
337
+
338
+ // Skip check if bypass is enabled
339
+ if (toolAccess.bypassGroupCheck) {
340
+ return;
341
+ }
342
+
343
+ const toolConfig = toolAccess.tools[toolName];
344
+
345
+ // If tool is marked as public, allow access
346
+ if (toolConfig?.public) {
347
+ return;
348
+ }
349
+
350
+ // Check user availability
351
+ if (!payload?.user) {
352
+ throw new ToolExecutionError(
353
+ toolName,
354
+ `Forbidden: User information not available for tool '${toolName}'`
355
+ );
356
+ }
357
+
358
+ const username = payload.user;
359
+
360
+ // Determine required group: tool-specific or default
361
+ const requiredGroup = toolConfig?.requiredGroup || toolAccess.defaultGroup;
362
+
363
+ if (!requiredGroup) {
364
+ // No group configured - allow access
365
+ return;
366
+ }
367
+
368
+ try {
369
+ const isInGroup = await isUserInGroup(username, requiredGroup);
370
+
371
+ if (!isInGroup) {
372
+ throw new ToolExecutionError(
373
+ toolName,
374
+ `Forbidden: User '${username}' is not authorized to use tool '${toolName}'. ` +
375
+ `Required group: '${requiredGroup}'`
376
+ );
377
+ }
378
+
379
+ logger.info(`User '${username}' authorized for tool '${toolName}' via group '${requiredGroup}'`);
380
+ } catch (error) {
381
+ if (error instanceof ToolExecutionError) {
382
+ throw error;
383
+ }
384
+ const errorMessage = error instanceof Error ? error.message : String(error);
385
+ throw new ToolExecutionError(
386
+ toolName,
387
+ `Forbidden: AD group check failed for tool '${toolName}' - ${errorMessage}`
388
+ );
389
+ }
390
+ }
391
+
392
+ export const handleToolCall = async (params: {
393
+ name: string;
394
+ arguments?: any;
395
+ headers?: Record<string, string>;
396
+ payload?: { user: string; [key: string]: any };
397
+ }): Promise<any> => {
398
+ const { name, arguments: args, headers, payload } = params;
399
+
400
+ logger.info(`Tool called: ${name} by user: ${payload?.user || 'unknown'}`);
401
+
402
+ // Check AD group membership for the specific tool
403
+ await checkToolAccess(name, payload);
404
+
405
+ try {
406
+ switch (name) {
407
+ case 'get_public_data':
408
+ // Public tool - no group check was performed
409
+ return await handleGetPublicData(args);
410
+
411
+ case 'get_user_data':
412
+ // Requires MCP-Users group
413
+ return await handleGetUserData(args);
414
+
415
+ case 'modify_data':
416
+ // Requires MCP-DataModifiers group
417
+ return await handleModifyData(args);
418
+
419
+ case 'admin_operation':
420
+ // Requires MCP-Admins group
421
+ return await handleAdminOperation(args);
422
+
423
+ default:
424
+ // Unknown tools use defaultGroup if configured
425
+ throw new ToolExecutionError(name, `Unknown tool: ${name}`);
426
+ }
427
+ } catch (error) {
428
+ logger.error(`Tool execution failed for ${name}:`, error);
429
+ throw error;
430
+ }
431
+ };
432
+
433
+ async function handleGetPublicData(args: any): Promise<any> {
434
+ return formatToolResult({ message: 'Public data retrieved', data: { public: true } });
435
+ }
436
+
437
+ async function handleGetUserData(args: any): Promise<any> {
438
+ return formatToolResult({ message: 'User data retrieved', data: args });
439
+ }
440
+
441
+ async function handleModifyData(args: any): Promise<any> {
442
+ return formatToolResult({ message: 'Data modified', modified: args });
443
+ }
444
+
445
+ async function handleAdminOperation(args: any): Promise<any> {
446
+ return formatToolResult({ message: 'Admin operation completed', operation: args });
447
+ }
448
+ ```
449
+
450
+ **Result**: Each tool enforces its own AD group requirements:
451
+ - `get_public_data` - accessible to everyone (public)
452
+ - `get_user_data` - requires `DOMAIN\MCP-Users` group
453
+ - `modify_data` - requires `DOMAIN\MCP-DataModifiers` group
454
+ - `admin_operation` - requires `DOMAIN\MCP-Admins` group
455
+
456
+ If a user tries to call a tool without being in the required group:
457
+ ```json
458
+ {
459
+ "jsonrpc": "2.0",
460
+ "error": {
461
+ "code": -32603,
462
+ "message": "Forbidden: User 'john.doe' is not authorized to use tool 'admin_operation'. Required group: 'DOMAIN\\MCP-Admins'"
463
+ },
464
+ "id": 1
465
+ }
466
+ ```
467
+
468
+ ---
469
+
470
+ ## Summary: Authorization Levels
471
+
472
+ | Level | Location | Error Type | Use Case |
473
+ |-------|----------|------------|----------|
474
+ | HTTP Server | `customAuthValidator` | HTTP 403 Forbidden | Block unauthorized users completely |
475
+ | All Tools | `toolHandler` (global check) | MCP Tool Error | Allow HTTP access, restrict all tool usage |
476
+ | Specific Tool | `toolHandler` (per-tool check) | MCP Tool Error | Fine-grained tool-level permissions |