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
@@ -1,2540 +0,0 @@
1
- # FA-MCP-SDK API Documentation
2
-
3
- ## Overview
4
-
5
- The FA-MCP-SDK is a comprehensive TypeScript framework for building Model Context
6
- Protocol (MCP) servers. This documentation covers how to use the SDK
7
- to create your own MCP server project.
8
-
9
- ## Getting Started
10
-
11
- ### Installation
12
-
13
- ```bash
14
- npm install fa-mcp-sdk
15
- ```
16
-
17
- ### Project Structure
18
-
19
- When creating a new MCP server, your project structure should follow this pattern:
20
-
21
- ```
22
- my-mcp-server/
23
- ├── config/ # Environment configurations
24
- │ ├── default.yaml # Base configuration
25
- │ ├── development.yaml # Development settings
26
- │ ├── production.yaml # Production settings
27
- │ └── test.yaml # Test environment
28
- ├── src/ # Source code
29
- │ ├── _types_/ # TypeScript type definitions
30
- │ ├── api/ # REST API routes (HTTP transport)
31
- │ │ ├── router.ts # Express router
32
- │ │ └── swagger.ts # API documentation
33
- │ ├── prompts/ # Agent prompts
34
- │ │ ├── agent-brief.ts # Agent brief
35
- │ │ ├── agent-prompt.ts # Main agent prompt
36
- │ │ └── custom-prompts.ts # Custom prompts
37
- │ ├── tools/ # MCP tool implementations
38
- │ │ ├── handle-tool-call.ts # Tool execution handler
39
- │ │ └── tools.ts # Tool definitions
40
- │ ├── custom-resources.ts # Custom MCP resources
41
- │ └── start.ts # Application entry point
42
- ├── tests/ # Test suites
43
- │ ├── mcp/ # MCP protocol tests
44
- │ └── utils.ts # Test utilities
45
- ├── .env # Environment variables
46
- ├── package.json # NPM package configuration
47
- └── tsconfig.json # TypeScript configuration
48
- ```
49
-
50
- ## Core API Reference
51
-
52
- ### Main Initialization Function
53
-
54
- #### `initMcpServer(data: McpServerData): Promise<void>`
55
-
56
- The primary function for starting your MCP server.
57
-
58
- **Example Usage in `src/start.ts`:**
59
-
60
- ```typescript
61
- import { initMcpServer, McpServerData, CustomAuthValidator } from 'fa-mcp-sdk';
62
- import { tools } from './tools/tools.js';
63
- import { handleToolCall } from './tools/handle-tool-call.js';
64
- import { AGENT_BRIEF } from './prompts/agent-brief.js';
65
- import { AGENT_PROMPT } from './prompts/agent-prompt.js';
66
-
67
- // Optional: Custom Authentication validator (black box function)
68
- const customAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> => {
69
- // Your custom authentication logic here - full request object available
70
- // Can access headers, IP, user-agent, etc.
71
- const authHeader = req.headers.authorization;
72
- const userID = req.headers['x-user-id'];
73
- const clientIP = req.headers['x-real-ip'] || req.connection?.remoteAddress;
74
-
75
- // Implement any authentication logic (database, LDAP, API, custom rules, etc.)
76
- const isAuthenticated = await authenticateRequest(req);
77
-
78
- if (isAuthenticated) {
79
- return {
80
- success: true,
81
- authType: 'basic',
82
- username: userID || 'unknown',
83
- };
84
- } else {
85
- return {
86
- success: false,
87
- error: 'Custom authentication failed',
88
- };
89
- }
90
- };
91
-
92
- const serverData: McpServerData = {
93
- tools,
94
- toolHandler: handleToolCall,
95
- agentBrief: AGENT_BRIEF,
96
- agentPrompt: AGENT_PROMPT,
97
-
98
- // Optional: Provide custom authentication function
99
- customAuthValidator: customAuthValidator,
100
-
101
- // ... other configuration
102
- };
103
-
104
- await initMcpServer(serverData);
105
- ```
106
-
107
- ### Core Types and Interfaces
108
-
109
- #### `McpServerData`
110
-
111
- Main configuration interface for your MCP server.
112
-
113
- ```typescript
114
- interface McpServerData {
115
- // MCP Core Components
116
- tools: Tool[]; // Your tool definitions
117
- toolHandler: (params: { name: string; arguments?: any; headers?: Record<string, string> }) => Promise<any>; // Tool execution function
118
-
119
- // Agent Configuration
120
- agentBrief: string; // Brief description of your agent
121
- agentPrompt: string; // System prompt for your agent
122
- customPrompts?: IPromptData[]; // Additional custom prompts
123
-
124
- // Resources
125
- requiredHttpHeaders?: IRequiredHttpHeader[] | null; // HTTP headers for authentication
126
- customResources?: IResourceData[] | null; // Custom resource definitions
127
-
128
- // Authentication
129
- customAuthValidator?: CustomAuthValidator; // Custom authentication validator function
130
-
131
- // HTTP Server Components (for HTTP transport)
132
- httpComponents?: {
133
- apiRouter?: Router | null; // Express router for additional endpoints
134
- endpointsOn404?: IEndpointsOn404; // Custom 404 handling
135
- swagger?: ISwaggerData | null; // OpenAPI/Swagger configuration
136
- };
137
-
138
- // UI Assets
139
- assets?: {
140
- favicon?: string; // SVG content for favicon
141
- maintainerHtml?: string; // Support contact HTML snippet
142
- };
143
-
144
- // Consul Integration
145
- getConsulUIAddress?: (serviceId: string) => string; // Function to generate Consul UI URLs
146
- }
147
- ```
148
-
149
- #### `IPromptData`
150
-
151
- Configuration for custom prompts in `src/prompts/custom-prompts.ts`.
152
-
153
- ```typescript
154
- interface IPromptData {
155
- name: string; // Unique prompt identifier
156
- description: string; // Human-readable description
157
- arguments: []; // Expected arguments (currently empty array)
158
- content: IPromptContent; // Static string or dynamic function
159
- requireAuth?: boolean; // Whether authentication is required
160
- }
161
-
162
- type IPromptContent = string | TPromptContentFunction;
163
- type TPromptContentFunction = (request: IGetPromptRequest) => string | Promise<string>;
164
- ```
165
-
166
- Example `src/prompts/custom-prompts.ts`:
167
- ```typescript
168
- import { IPromptData } from 'fa-mcp-sdk';
169
-
170
- export const customPrompts: IPromptData[] = [
171
- {
172
- name: 'custom_prompt',
173
- description: 'A custom prompt for specific tasks',
174
- arguments: [],
175
- content: (request) => {
176
- const { sample } = request.params.arguments || {};
177
- return `Custom prompt content with parameter: ${sample}`;
178
- },
179
- },
180
- ];
181
- ```
182
-
183
- #### `IResourceData`
184
-
185
- Configuration for custom resources in `src/custom-resources.ts`.
186
-
187
- ```typescript
188
- interface IResourceData {
189
- uri: string; // Unique resource URI (e.g., "custom-resource://data1")
190
- name: string; // Resource name
191
- title?: string; // Optional display title
192
- description: string; // Human-readable description
193
- mimeType: string; // MIME type (e.g., "text/plain", "application/json")
194
- content: IResourceContent; // Static content or dynamic function
195
- requireAuth?: boolean; // Whether authentication is required
196
- }
197
- ```
198
-
199
- Example `src/custom-resources.ts`:
200
- ```typescript
201
- import { IResourceData } from 'fa-mcp-sdk';
202
-
203
- export const customResources: IResourceData[] = [
204
- {
205
- uri: 'custom-resource://resource1',
206
- name: 'resource1',
207
- description: 'Example resource with dynamic content',
208
- mimeType: 'text/plain',
209
- content: (uri) => {
210
- return `Dynamic content for ${uri} at ${new Date().toISOString()}`;
211
- },
212
- },
213
- ];
214
- ```
215
-
216
- ### Tool Development
217
-
218
- #### Tool Definition in `src/tools/tools.ts`
219
-
220
- ```typescript
221
- import { Tool } from '@modelcontextprotocol/sdk/types.js';
222
- import { IToolInputSchema } from 'fa-mcp-sdk';
223
-
224
- export const tools: Tool[] = [
225
- {
226
- name: 'my_custom_tool',
227
- description: 'Description of what this tool does',
228
- inputSchema: {
229
- type: 'object',
230
- properties: {
231
- query: {
232
- type: 'string',
233
- description: 'Input query or text',
234
- },
235
- options: {
236
- type: 'object',
237
- description: 'Optional configuration',
238
- },
239
- },
240
- required: ['query'],
241
- },
242
- },
243
- ];
244
- ```
245
-
246
- #### Tool Handler in `src/tools/handle-tool-call.ts`
247
-
248
- ```typescript
249
- import { formatToolResult, ToolExecutionError, logger } from 'fa-mcp-sdk';
250
-
251
- export const handleToolCall = async (params: { name: string, arguments?: any, headers?: Record<string, string> }): Promise<any> => {
252
- const { name, arguments: args, headers } = params;
253
-
254
- logger.info(`Tool called: ${name}`);
255
-
256
- // Access normalized HTTP headers (all header names are lowercase)
257
- if (headers) {
258
- const authHeader = headers.authorization;
259
- const userAgent = headers['user-agent'];
260
- const customHeader = headers['x-custom-header'];
261
- logger.info(`Headers available: authorization=${!!authHeader}, user-agent=${userAgent}`);
262
- }
263
-
264
- try {
265
- switch (name) {
266
- case 'my_custom_tool':
267
- return await handleMyCustomTool(args);
268
-
269
- default:
270
- throw new ToolExecutionError(name, `Unknown tool: ${name}`);
271
- }
272
- } catch (error) {
273
- logger.error(`Tool execution failed for ${name}:`, error);
274
- throw error;
275
- }
276
- };
277
-
278
- async function handleMyCustomTool(args: any): Promise<string> {
279
- const { query, options } = args || {};
280
-
281
- if (!query) {
282
- throw new ToolExecutionError('my_custom_tool', 'Query parameter is required');
283
- }
284
-
285
- // Your tool logic here
286
- const result = {
287
- message: `Processed: ${query}`,
288
- timestamp: new Date().toISOString(),
289
- options: options || {},
290
- };
291
-
292
- return formatToolResult(result);
293
- }
294
- ```
295
-
296
- #### HTTP Headers in Tool Handler
297
-
298
- The FA-MCP-SDK automatically passes normalized HTTP headers to your `toolHandler` function, enabling context-aware tool execution based on client information.
299
-
300
- **Key Features:**
301
- - All headers are automatically normalized to lowercase
302
- - Available in both HTTP and SSE transports (SSE provides empty headers object)
303
- - Headers are sanitized and only string values are passed
304
- - Array header values are joined with `', '` separator
305
-
306
- **Example Usage:**
307
-
308
- ```typescript
309
- export const handleToolCall = async (params: {
310
- name: string,
311
- arguments?: any,
312
- headers?: Record<string, string>
313
- }): Promise<any> => {
314
- const { name, arguments: args, headers } = params;
315
-
316
- // Access client information via headers
317
- if (headers) {
318
- const authHeader = headers.authorization; // Lowercase normalized
319
- const userAgent = headers['user-agent']; // Browser/client info
320
- const clientIP = headers['x-real-ip'] || headers['x-forwarded-for']; // Proxy headers
321
- const customData = headers['x-custom-header']; // Custom headers
322
-
323
- logger.info(`Tool ${name} called by ${userAgent} from IP ${clientIP}`);
324
-
325
- // Conditional logic based on client
326
- if (userAgent?.includes('mobile')) {
327
- return await handleMobileRequest(args);
328
- }
329
-
330
- // Custom authorization beyond standard auth
331
- if (customData === 'admin-mode' && authHeader) {
332
- return await handleAdminRequest(args);
333
- }
334
- }
335
-
336
- // Regular tool logic
337
- switch (name) {
338
- case 'get_user_data':
339
- // Use headers for audit logging
340
- return await getUserData(args, {
341
- clientIP: headers?.['x-real-ip'],
342
- userAgent: headers?.['user-agent']
343
- });
344
- }
345
- };
346
- ```
347
-
348
- **Header Normalization Details:**
349
-
350
- ```typescript
351
- // Original headers from client:
352
- {
353
- 'Authorization': 'Bearer token123',
354
- 'X-Custom-Header': 'value',
355
- 'USER-AGENT': 'MyClient/1.0'
356
- }
357
-
358
- // Normalized headers passed to toolHandler:
359
- {
360
- 'authorization': 'Bearer token123',
361
- 'x-custom-header': 'value',
362
- 'user-agent': 'MyClient/1.0'
363
- }
364
- ```
365
-
366
- **Transport Differences:**
367
-
368
- - **HTTP Transport**: Full headers available from Express request object
369
- - **SSE Transport**: Headers preserved from initial SSE connection establishment (GET /sse request)
370
-
371
- **Common Use Cases:**
372
- - Client identification and analytics
373
- - Custom authorization checks beyond standard authentication
374
- - Request routing based on client capabilities
375
- - Audit logging with client context
376
- - Rate limiting per client type
377
-
378
- ### Configuration Management
379
-
380
- #### Using `appConfig`
381
-
382
- Access configuration in your code:
383
-
384
- ```typescript
385
- import { appConfig } from 'fa-mcp-sdk';
386
-
387
- // Access configuration values
388
- const serverPort = appConfig.webServer.port;
389
- const dbEnabled = appConfig.isMainDBUsed;
390
- const transport = appConfig.mcp.transportType; // 'stdio' | 'http'
391
- ```
392
-
393
- #### Configuration Files
394
-
395
- **`config/default.yaml`** - Base configuration:
396
- ```yaml
397
- accessPoints:
398
- myService:
399
- title: 'My remote service'
400
- host: <host>
401
- port: 9999
402
- token: '***'
403
- noConsul: true # Use if the service developers do not provide registration in consul
404
- consulServiceName: <consulServiceName>
405
-
406
- # --------------------------------------------------
407
- # CACHING Reduces API calls by caching responses
408
- # --------------------------------------------------
409
- cache:
410
- # Default Cache TTL in seconds
411
- ttlSeconds: 300
412
- # Default maximum number of cached items
413
- maxItems: 1000
414
-
415
- consul:
416
- check:
417
- interval: '10s'
418
- timeout: '5s'
419
- deregistercriticalserviceafter: '3m'
420
- agent:
421
- # Credentials for getting information about services in the DEV DC
422
- dev:
423
- dc: '{{consul.agent.dev.dc}}'
424
- host: '{{consul.agent.dev.host}}'
425
- port: 443
426
- secure: true
427
- # Token for getting information about DEV services
428
- token: '***'
429
- # Credentials for getting information about services in the PROD DC
430
- prd:
431
- dc: '{{consul.agent.prd.dc}}'
432
- host: '{{consul.agent.prd.host}}'
433
- port: 443
434
- secure: true
435
- # Token for obtaining information about PROD services
436
- token: '***'
437
- # Credentials for registering the service with Consul
438
- reg:
439
- # The host of the consul agent where the service will be registered. If not specified, the server on which the service is running is used
440
- host: null
441
- port: 8500
442
- secure: false
443
- # Token for registering the service in the consul agent
444
- token: '***'
445
- service:
446
- enable: {{consul.service.enable}} # true - Allows registration of the service with the consul
447
- name: <name> # <name> will be replaced by <package.json>.name at initialization
448
- instance: '{{SERVICE_INSTANCE}}' # This value will be specified as a suffix in the id of the service
449
- version: <version> # <version> will be replaced by <package.json>.version at initialization
450
- description: <description> # <description> will be replaced by <package.json>.description at initialization
451
- tags: [] # If null or empty array - Will be pulled up from package.keywords at initialization
452
- meta:
453
- # "Home" page link template
454
- who: 'http://{address}:{port}/'
455
- envCode: # Used to generate the service ID
456
- prod: {{consul.envCode.prod}} # Production environment code
457
- dev: {{consul.envCode.dev}} # Development environment code
458
-
459
- db:
460
- postgres:
461
- dbs:
462
- main:
463
- label: 'My Database'
464
- host: '' # To exclude the use of the database, you need to set host = ''
465
- port: 5432
466
- database: <database>
467
- user: <user>
468
- password: <password>
469
- usedExtensions: []
470
-
471
- logger:
472
- level: info
473
- useFileLogger: {{logger.useFileLogger}} # To use or not to use logging to a file
474
- # Absolute path to the folder where logs will be written. Default <proj_root>/../logs
475
- dir: '{{logger.dir}}'
476
-
477
- mcp:
478
- transportType: http # stdio | http
479
- # Response format configuration.
480
- # - structuredContent - default - the response in result.structuredContent returns JSON
481
- # - text - in the response, serialized JSON is returned in result.content[0].text
482
- toolAnswerAs: text # text | structuredContent
483
- rateLimit:
484
- maxRequests: 100
485
- windowMs: 60000 # 1 minute
486
-
487
- swagger:
488
- servers: # An array of servers that will be added to swagger docs
489
- # - url: http://localhost:9020
490
- # description: "Development server (localhost)"
491
- # - url: http://0.0.0.0:9020
492
- # description: "Development server (all interfaces)"
493
- # - url: http://<prod_server_host_or_ip>:{{port}}
494
- # description: "PROD server"
495
- - url: https://{{mcp.domain}}
496
- description: "PROD server"
497
-
498
- uiColor:
499
- # Font color of the header and a number of interface elements on the HOME page
500
- primary: '#0f65dc'
501
-
502
- webServer:
503
- host: '0.0.0.0'
504
- port: {{port}}
505
- # array of hosts that CORS skips
506
- originHosts: ['localhost', '0.0.0.0']
507
- # Authentication is configured here only when accessing the MCP server
508
- # Authentication in services that enable tools, resources, and prompts
509
- # is implemented more deeply. To do this, you need to use the information passed in HTTP headers
510
- # You can also use a custom authorization function
511
- auth:
512
- enabled: false # Enables/disables authorization
513
- # ========================================================================
514
- # PERMANENT SERVER TOKENS
515
- # Static tokens for server-to-server communication
516
- # CPU cost: O(1) - fastest authentication method
517
- #
518
- # To enable this authentication, you need to set auth.enabled = true
519
- # and set one token of at least 20 characters in length
520
- # ========================================================================
521
- permanentServerTokens: [ ] # Add your server tokens here: ['token1', 'token2']
522
-
523
- # ========================================================================
524
- # JWT TOKEN WITH SYMMETRIC ENCRYPTION
525
- # Custom JWT tokens with AES-256 encryption
526
- # CPU cost: Medium - decryption + JSON parsing
527
- #
528
- # To enable this authentication, you need to set auth.enabled = true and set
529
- # encryptKey to at least 20 characters
530
- # ========================================================================
531
- jwtToken:
532
- # Symmetric encryption key to generate a token for this MCP (minimum 8 chars)
533
- encryptKey: '***'
534
- # If webServer.auth.enabled and the parameter true, the service name and the service specified in the token will be checked
535
- checkMCPName: true
536
-
537
- # ========================================================================
538
- # Basic Authentication - Base64 encoded username:password
539
- # CPU cost: Medium - Base64 decoding + string comparison
540
- # To enable this authentication, you need to set auth.enabled = true
541
- # and set username and password to valid values
542
- # ========================================================================
543
- basic:
544
- username: ''
545
- password: '***'
546
- ```
547
-
548
- **`config/local.yaml`** - local overrides. Usually contains secrets.
549
-
550
- ### Cache Management
551
-
552
- #### `getCache(options?): CacheManager`
553
-
554
- Get or create a global cache instance for your MCP server.
555
-
556
- ```typescript
557
- import { getCache, CacheManager } from 'fa-mcp-sdk';
558
-
559
- // Create default cache instance
560
- const cache = getCache();
561
-
562
- // Create cache with custom options
563
- const customCache = getCache({
564
- ttlSeconds: 600, // Default TTL: 10 minutes
565
- maxItems: 5000, // Max cached items
566
- checkPeriod: 300, // Cleanup interval in seconds
567
- verbose: true // Enable debug logging
568
- });
569
- ```
570
-
571
- #### Cache Methods
572
-
573
- The `CacheManager` provides the following methods:
574
-
575
- | Method | Description | Example |
576
- |--------|-------------|---------|
577
- | `get<T>(key)` | Get value from cache | `const user = cache.get<User>('user:123');` |
578
- | `set<T>(key, value, ttl?)` | Set value in cache | `cache.set('user:123', userData, 300);` |
579
- | `has(key)` | Check if key exists | `if (cache.has('user:123')) { ... }` |
580
- | `del(key)` | Delete key from cache | `cache.del('user:123');` |
581
- | `take<T>(key)` | Get and delete (single use) | `const otp = cache.take<string>('otp:123');` |
582
- | `mget<T>(keys[])` | Get multiple values | `const users = cache.mget(['user:1', 'user:2']);` |
583
- | `mset(items[])` | Set multiple values | `cache.mset([{key: 'a', val: 1}, {key: 'b', val: 2}]);` |
584
- | `getOrSet<T>(key, factory, ttl?)` | Get or compute value | `const data = await cache.getOrSet('key', () => fetchData());` |
585
- | `keys()` | List all keys | `const allKeys = cache.keys();` |
586
- | `flush()` | Clear all entries | `cache.flush();` |
587
- | `ttl(key, seconds)` | Update key TTL | `cache.ttl('user:123', 600);` |
588
- | `getTtl(key)` | Get remaining TTL | `const remaining = cache.getTtl('user:123');` |
589
- | `getStats()` | Get cache statistics | `const stats = cache.getStats();` |
590
- | `close()` | Close cache resources | `cache.close();` |
591
-
592
- #### Usage Examples
593
-
594
- ```typescript
595
- import { getCache } from 'fa-mcp-sdk';
596
-
597
- const cache = getCache();
598
-
599
- // Basic caching
600
- cache.set('user:123', { name: 'John', email: 'john@example.com' });
601
- const user = cache.get<User>('user:123');
602
-
603
- // Cache with TTL (time to live)
604
- cache.set('session:abc', sessionData, 1800); // 30 minutes
605
-
606
- // Single-use values (OTP, tokens)
607
- cache.set('otp:user123', '123456', 300);
608
- const otp = cache.take('otp:user123'); // Gets and deletes
609
-
610
- // Get-or-set pattern
611
- const expensiveData = await cache.getOrSet(
612
- 'computation:key',
613
- async () => {
614
- // This function runs only on cache miss
615
- return await performExpensiveOperation();
616
- },
617
- 3600 // Cache for 1 hour
618
- );
619
-
620
- // Batch operations
621
- const userData = cache.mget(['user:1', 'user:2', 'user:3']);
622
- cache.mset([
623
- { key: 'user:1', val: user1Data },
624
- { key: 'user:2', val: user2Data, ttl: 600 }
625
- ]);
626
-
627
- // Cache monitoring
628
- const stats = cache.getStats();
629
- console.log(`Hit rate: ${(stats.hitRate * 100).toFixed(1)}%`);
630
- console.log(`Keys: ${stats.keys}, Memory: ${stats.vsize} bytes`);
631
- ```
632
-
633
- ### Database Integration
634
-
635
- To disable the use of the database, you need to set appConfig.db.postgres.dbs.main.host to an empty value.
636
- In this case, when the configuration is formed, appConfig.isMainDBUsed is set to false.
637
-
638
-
639
- If you enable database support (`isMainDBUsed: true` in config):
640
-
641
- ```typescript
642
- import {
643
- queryMAIN,
644
- execMAIN,
645
- oneRowMAIN,
646
- getMainDBConnectionStatus
647
- } from 'fa-mcp-sdk';
648
-
649
- // Check database connection. If there is no connection, the application stops
650
- await checkMainDB();
651
-
652
- // queryMAIN - the main function of executing SQL queries to the main database
653
-
654
- // Function Signature:
655
- const queryMAIN = async <R extends QueryResultRow = any> (
656
- arg: string | IQueryPgArgsCOptional,
657
- sqlValues?: any[],
658
- throwError = false,
659
- ): Promise<QueryResult<R> | undefined> {...}
660
-
661
- // Types used:
662
- export interface IQueryPgArgs {
663
- connectionId: string,
664
- poolConfig?: PoolConfig & IDbOptionsPg,
665
- client?: IPoolPg,
666
- sqlText: string,
667
- sqlValues?: any[],
668
- throwError?: boolean,
669
- prefix?: string,
670
- registerTypesFunctions?: IRegisterTypeFn[],
671
- }
672
- export interface IQueryPgArgsCOptional extends Omit<IQueryPgArgs, 'connectionId'> {
673
- connectionId?: string
674
- }
675
-
676
- // Examples of use
677
- const users1 = await queryMAIN('SELECT * FROM users WHERE active = $1', [true]);
678
- // Alternative use case
679
- const users2 = await queryMAIN({ sqlText: 'SELECT * FROM users WHERE active = $1', sqlValues: [true] });
680
-
681
-
682
- // execMAIN - execute SQL commands without returning result set
683
- // Function Signature:
684
- const execMAIN = async (
685
- arg: string | IQueryPgArgsCOptional,
686
- ): Promise<number | undefined> {...}
687
-
688
- // Examples:
689
- await execMAIN('INSERT INTO logs (message, created_at) VALUES ($1, $2)',
690
- ['Server started', new Date()]);
691
- await execMAIN({ sqlText: 'UPDATE users SET active = $1 WHERE id = $2', sqlValues: [false, userId] });
692
-
693
- // queryRsMAIN - execute SQL and return rows array directly
694
- // Function Signature:
695
- const queryRsMAIN = async <R extends QueryResultRow = any> (
696
- arg: string | IQueryPgArgsCOptional,
697
- sqlValues?: any[],
698
- throwError = false,
699
- ): Promise<R[] | undefined> {...}
700
-
701
- // Example:
702
- const users = await queryRsMAIN<User>('SELECT * FROM users WHERE active = $1', [true]);
703
-
704
- // oneRowMAIN - execute SQL and return single row
705
- // Function Signature:
706
- const oneRowMAIN = async <R extends QueryResultRow = any> (
707
- arg: string | IQueryPgArgsCOptional,
708
- sqlValues?: any[],
709
- throwError = false,
710
- ): Promise<R | undefined> {...}
711
-
712
- // Example:
713
- const user = await oneRowMAIN<User>('SELECT * FROM users WHERE id = $1', [userId]);
714
-
715
- // getMainDBConnectionStatus - check database connection status
716
- // Function Signature:
717
- const getMainDBConnectionStatus = async (): Promise<string> {...}
718
-
719
- // Possible return values: 'connected' | 'disconnected' | 'error' | 'db_not_used'
720
- const status = await getMainDBConnectionStatus();
721
-
722
- // checkMainDB - verify database connectivity (stops application if failed)
723
- // Function Signature:
724
- const checkMainDB = async (): Promise<void> {...}
725
-
726
- // Example:
727
- await checkMainDB(); // Throws or exits process if DB connection fails
728
-
729
- // getInsertSqlMAIN - generate INSERT SQL statement
730
- // Function Signature:
731
- const getInsertSqlMAIN = async <U extends TDBRecord = TDBRecord> (arg: {
732
- commonSchemaAndTable: string,
733
- recordset: TRecordSet<U>,
734
- excludeFromInsert?: string[],
735
- addOutputInserted?: boolean,
736
- isErrorOnConflict?: boolean,
737
- keepSerialFields?: boolean,
738
- }): Promise<string> {...}
739
-
740
- // Example:
741
- const insertSql = await getInsertSqlMAIN({
742
- commonSchemaAndTable: 'public.users',
743
- recordset: [{ name: 'John', email: 'john@example.com' }],
744
- addOutputInserted: true
745
- });
746
-
747
- // getMergeSqlMAIN - generate UPSERT (INSERT...ON CONFLICT) SQL statement
748
- // Function Signature:
749
- const getMergeSqlMAIN = async <U extends TDBRecord = TDBRecord> (arg: {
750
- commonSchemaAndTable: string,
751
- recordset: TRecordSet<U>,
752
- conflictFields?: string[],
753
- omitFields?: string[],
754
- updateFields?: string[],
755
- fieldsExcludedFromUpdatePart?: string[],
756
- noUpdateIfNull?: boolean,
757
- mergeCorrection?: (_sql: string) => string,
758
- returning?: string,
759
- }): Promise<string> {...}
760
-
761
- // Example:
762
- const mergeSql = await getMergeSqlMAIN({
763
- commonSchemaAndTable: 'public.users',
764
- recordset: [{ id: 1, name: 'John Updated', email: 'john@example.com' }],
765
- conflictFields: ['email'],
766
- returning: '*'
767
- });
768
-
769
- // mergeByBatch - execute merge operations in batches
770
- // Function Signature:
771
- const mergeByBatch = async <U extends TDBRecord = TDBRecord> (arg: {
772
- recordset: TRecordSet<U>,
773
- getMergeSqlFn: Function
774
- batchSize?: number
775
- }): Promise<any[]> {...}
776
-
777
- // Example:
778
- const results = await mergeByBatch({
779
- recordset: largeDataSet,
780
- getMergeSqlFn: (batch) => getMergeSqlMAIN({
781
- commonSchemaAndTable: 'public.users',
782
- recordset: batch
783
- }),
784
- batchSize: 500
785
- });
786
- ```
787
-
788
- ### Error Handling
789
-
790
- #### Custom Error Classes
791
-
792
- ```typescript
793
- import { BaseMcpError, ToolExecutionError, ValidationError } from 'fa-mcp-sdk';
794
-
795
- // Create custom error types
796
- class MyCustomError extends BaseMcpError {
797
- constructor(message: string) {
798
- super(message, 'CUSTOM_ERROR');
799
- }
800
- }
801
-
802
- // Use built-in error types
803
- if (!validInput) {
804
- throw new ValidationError('Input validation failed');
805
- }
806
-
807
- if (toolFailed) {
808
- throw new ToolExecutionError('my_tool', 'Tool execution failed');
809
- }
810
- ```
811
-
812
- #### Error Utilities
813
-
814
- ```typescript
815
- import {
816
- createJsonRpcErrorResponse,
817
- toError,
818
- toStr,
819
- addErrorMessage
820
- } from 'fa-mcp-sdk';
821
-
822
- // createJsonRpcErrorResponse - create JSON-RPC 2.0 error response
823
- // Function Signature:
824
- function createJsonRpcErrorResponse (
825
- error: Error | BaseMcpError,
826
- requestId?: string | number | null,
827
- ): any {...}
828
-
829
- // Example:
830
- try {
831
- // some operation
832
- } catch (error) {
833
- const jsonRpcError = createJsonRpcErrorResponse(error, 'request-123');
834
- res.json(jsonRpcError);
835
- }
836
-
837
- // toError - safely convert any value to Error object
838
- // Function Signature:
839
- const toError = (err: any): Error {...}
840
-
841
- // Examples:
842
- const err1 = toError(new Error('Original error')); // Returns original Error
843
- const err2 = toError('String error message'); // Returns new Error('String error message')
844
- const err3 = toError({ message: 'Object error' }); // Returns new Error('[object Object]')
845
-
846
- // toStr - safely convert error to string message
847
- // Function Signature:
848
- const toStr = (err: any): string {...}
849
-
850
- // Examples:
851
- const msg1 = toStr(new Error('Test error')); // Returns 'Test error'
852
- const msg2 = toStr('String message'); // Returns 'String message'
853
- const msg3 = toStr(null); // Returns 'Unknown error'
854
-
855
- // addErrorMessage - add context to existing error message
856
- // Function Signature:
857
- const addErrorMessage = (err: any, msg: string): void {...}
858
-
859
- // Example:
860
- const originalError = new Error('Connection failed');
861
- addErrorMessage(originalError, 'Database operation failed');
862
- // originalError.message is now: 'Database operation failed. Connection failed'
863
- ```
864
-
865
- ### Authentication and Security
866
-
867
- #### Token-based Authentication
868
-
869
- ```typescript
870
- import {
871
- ICheckTokenResult,
872
- checkJwtToken,
873
- generateToken
874
- } from 'fa-mcp-sdk';
875
-
876
- // Types used:
877
- export interface ICheckTokenResult {
878
- payload?: ITokenPayload, // Token payload with user data
879
- errorReason?: string, // Error message if validation failed
880
- isTokenDecrypted?: boolean, // Whether token was successfully decrypted
881
- }
882
-
883
- export interface ITokenPayload {
884
- user: string, // Username
885
- expire: number, // Expiration timestamp
886
- [key: string]: any, // Additional payload data
887
- }
888
-
889
- // checkJwtToken - validate token and return detailed result
890
- // Function Signature:
891
- const checkJwtToken = (arg: {
892
- token: string,
893
- expectedUser?: string,
894
- expectedService?: string,
895
- }): ICheckTokenResult {...}
896
-
897
- // Example:
898
- const tokenResult = checkJwtToken({
899
- token: 'user_provided_token',
900
- expectedUser: 'john_doe',
901
- expectedService: 'my-mcp-server'
902
- });
903
-
904
- if (!tokenResult.errorReason) {
905
- console.log('Valid token for user:', tokenResult.payload?.user);
906
- } else {
907
- console.log('Auth failed:', tokenResult.errorReason);
908
- }
909
-
910
- // generateToken - create JWT token
911
- // Function Signature:
912
- const generateToken = (user: string, liveTimeSec: number, payload?: any): string {...}
913
-
914
- // Example:
915
- const token = generateToken('john_doe', 3600, { role: 'admin' }); // 1 hour token
916
-
917
- // Deprecated: authByToken was replaced by createAuthMW universal middleware
918
- // Use createAuthMW instead for all authentication scenarios:
919
-
920
- // Example - Modern approach:
921
- app.post('/api/secure', createAuthMW(), (req, res) => {
922
- // User is authenticated, authInfo available on req
923
- const authInfo = (req as any).authInfo;
924
- res.json({
925
- message: 'Access granted',
926
- authType: authInfo?.authType,
927
- username: authInfo?.username
928
- });
929
- });
930
-
931
- ```
932
-
933
- #### Token Generation
934
-
935
- ```typescript
936
- import { generateTokenApp } from 'fa-mcp-sdk';
937
-
938
- // generateTokenApp - start token generation web application
939
- // Function Signature:
940
- async function generateTokenApp (...args: any[]): Promise<void> {...}
941
-
942
- // Starts a web server for generating authentication tokens
943
- // Uses NTLM authentication if configured
944
- // Web interface available at configured host:port
945
-
946
- // Example:
947
- await generateTokenApp(); // Uses default configuration from appConfig
948
-
949
- // Token generation app provides:
950
- // - Web interface for token creation
951
- // - NTLM domain authentication support
952
- // - JWT token generation with configurable expiration
953
- // - Integration with Active Directory (if configured)
954
-
955
- // Configuration in config/default.yaml:
956
- // webServer:
957
- // auth:
958
- // token:
959
- // encryptKey: '***' # Symmetric key for token encryption
960
- //
961
- // Optional NTLM configuration:
962
- // ntlm:
963
- // domain: 'DOMAIN'
964
- // domainController: 'dc.domain.com'
965
- ```
966
-
967
- #### Test Authentication Headers
968
-
969
- ```typescript
970
- import { getAuthHeadersForTests } from 'fa-mcp-sdk';
971
-
972
- // getAuthHeadersForTests - automatically generate authentication headers for testing
973
- // Function Signature:
974
- function getAuthHeadersForTests(): object {...}
975
-
976
- // Determines authentication headers based on appConfig.webServer.auth configuration.
977
- // Returns Authorization header using the first valid auth method found.
978
- //
979
- // Priority order (CPU-optimized, fastest first):
980
- // 1. permanentServerTokens - if at least one token is defined
981
- // 2. basic auth - if username AND password are both set
982
- // 3. JWT token - if jwtToken.encryptKey is set, generates token on the fly
983
- //
984
- // Returns empty object if auth is not enabled or no valid method configured.
985
-
986
- // Examples:
987
- const headers = getAuthHeadersForTests();
988
-
989
- // Use in fetch requests
990
- const response = await fetch('http://localhost:3000/mcp', {
991
- method: 'POST',
992
- headers: {
993
- 'Content-Type': 'application/json',
994
- ...headers // Automatically adds Authorization header if auth is enabled
995
- },
996
- body: JSON.stringify(requestBody)
997
- });
998
-
999
- // Use with test clients
1000
- import { McpHttpClient } from 'fa-mcp-sdk';
1001
-
1002
- const client = new McpHttpClient('http://localhost:3000');
1003
- const authHeaders = getAuthHeadersForTests();
1004
- const result = await client.callTool('my_tool', { query: 'test' }, authHeaders);
1005
-
1006
- // Return value examples based on configuration:
1007
-
1008
- // If permanentServerTokens configured:
1009
- // { Authorization: 'Bearer server-token-1' }
1010
-
1011
- // If basic auth configured:
1012
- // { Authorization: 'Basic YWRtaW46cGFzc3dvcmQ=' } // base64 of 'admin:password'
1013
-
1014
- // If JWT encryptKey configured:
1015
- // { Authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...' }
1016
-
1017
- // If auth.enabled = false or no valid method:
1018
- // {}
1019
-
1020
- // Typical test setup:
1021
- import { getAuthHeadersForTests, appConfig } from 'fa-mcp-sdk';
1022
-
1023
- describe('MCP Server Tests', () => {
1024
- const baseUrl = `http://localhost:${appConfig.webServer.port}`;
1025
- const authHeaders = getAuthHeadersForTests();
1026
-
1027
- it('should call tool with authentication', async () => {
1028
- const response = await fetch(`${baseUrl}/mcp`, {
1029
- method: 'POST',
1030
- headers: {
1031
- 'Content-Type': 'application/json',
1032
- ...authHeaders
1033
- },
1034
- body: JSON.stringify({
1035
- jsonrpc: '2.0',
1036
- method: 'tools/call',
1037
- params: { name: 'my_tool', arguments: { query: 'test' } },
1038
- id: 1
1039
- })
1040
- });
1041
-
1042
- expect(response.ok).toBe(true);
1043
- });
1044
- });
1045
- ```
1046
-
1047
- #### Token Generator Authorization Handler
1048
-
1049
- The Token Generator admin page (`/admin/`) can be protected with an additional
1050
- custom authorization layer beyond the standard authentication. This allows you
1051
- to implement fine-grained access control, such as restricting access to specific
1052
- AD groups or roles.
1053
-
1054
- ##### Types
1055
-
1056
- ```typescript
1057
- import { TokenGenAuthHandler, TokenGenAuthInput, AuthResult } from 'fa-mcp-sdk';
1058
-
1059
- // Input data passed to the authorization handler
1060
- interface TokenGenAuthInput {
1061
- user: string; // Username from authentication
1062
- domain?: string; // Domain (only for NTLM auth)
1063
- payload?: Record<string, any>; // JWT payload (only for jwtToken auth)
1064
- authType: 'jwtToken' | 'basic' | 'ntlm' | 'permanentServerTokens';
1065
- }
1066
-
1067
- // Authorization handler function type
1068
- type TokenGenAuthHandler = (input: TokenGenAuthInput) => Promise<AuthResult> | AuthResult;
1069
- ```
1070
-
1071
- ##### Configuration
1072
-
1073
- Add `tokenGenAuthHandler` to your `McpServerData` in `src/start.ts`:
1074
-
1075
- ```typescript
1076
- import { initMcpServer, McpServerData, TokenGenAuthHandler, initADGroupChecker } from 'fa-mcp-sdk';
1077
-
1078
- // Example 1: Restrict to specific AD groups (NTLM authentication)
1079
- const { isUserInGroup } = initADGroupChecker();
1080
-
1081
- const tokenGenAuthHandler: TokenGenAuthHandler = async (input) => {
1082
- // Only check for NTLM-authenticated users
1083
- if (input.authType === 'ntlm') {
1084
- const isAdmin = await isUserInGroup(input.user, 'TokenGeneratorAdmins');
1085
- if (!isAdmin) {
1086
- return {
1087
- success: false,
1088
- error: `User ${input.user} is not authorized to access Token Generator`,
1089
- };
1090
- }
1091
- }
1092
- return { success: true, username: input.user };
1093
- };
1094
-
1095
- // Example 2: Check JWT payload for specific claims
1096
- const tokenGenAuthHandler: TokenGenAuthHandler = async (input) => {
1097
- if (input.authType === 'jwtToken') {
1098
- const roles = input.payload?.roles || [];
1099
- if (!roles.includes('token-admin')) {
1100
- return {
1101
- success: false,
1102
- error: 'Missing required role: token-admin',
1103
- };
1104
- }
1105
- }
1106
- return { success: true, username: input.user };
1107
- };
1108
-
1109
- // Example 3: Simple whitelist check
1110
- const allowedUsers = ['admin', 'john.doe', 'jane.smith'];
1111
-
1112
- const tokenGenAuthHandler: TokenGenAuthHandler = (input) => {
1113
- if (!allowedUsers.includes(input.user.toLowerCase())) {
1114
- return {
1115
- success: false,
1116
- error: `User ${input.user} is not in the allowed users list`,
1117
- };
1118
- }
1119
- return { success: true, username: input.user };
1120
- };
1121
-
1122
- // Use in McpServerData
1123
- const serverData: McpServerData = {
1124
- tools,
1125
- toolHandler: handleToolCall,
1126
- agentBrief: AGENT_BRIEF,
1127
- agentPrompt: AGENT_PROMPT,
1128
-
1129
- // Add custom authorization for Token Generator
1130
- tokenGenAuthHandler,
1131
-
1132
- // ... other configuration
1133
- };
1134
-
1135
- await initMcpServer(serverData);
1136
- ```
1137
-
1138
- ##### Behavior
1139
-
1140
- - **If `tokenGenAuthHandler` is not provided**: All authenticated users can access Token Generator
1141
- - **If handler returns `{ success: true }`**: User is authorized
1142
- - **If handler returns `{ success: false, error: '...' }`**: User receives 403 Forbidden with error message
1143
- - **Handler errors**: Caught and returned as 403 with error message
1144
-
1145
- ##### Auth Type Input Details
1146
-
1147
- | Auth Type | `user` | `domain` | `payload` |
1148
- |-----------|--------|----------|-----------|
1149
- | `ntlm` | NTLM username | NTLM domain | - |
1150
- | `basic` | Basic auth username | - | - |
1151
- | `jwtToken` | JWT `user` claim | - | Full JWT payload |
1152
- | `permanentServerTokens` | "Unknown" | - | - |
1153
-
1154
- #### Multi-Authentication System
1155
-
1156
- The FA-MCP-SDK supports a comprehensive multi-authentication system that allows multiple authentication methods to work together with CPU-optimized performance ordering.
1157
-
1158
- ##### Types and Interfaces
1159
-
1160
- ```typescript
1161
- import {
1162
- AuthType,
1163
- AuthResult,
1164
- AuthDetectionResult,
1165
- CustomAuthValidator,
1166
- checkMultiAuth,
1167
- detectAuthConfiguration,
1168
- logAuthConfiguration,
1169
- createAuthMW, // Universal authentication middleware
1170
- getMultiAuthError, // Programmatic authentication checking
1171
- } from 'fa-mcp-sdk';
1172
-
1173
- // Authentication types in CPU priority order (low to high cost)
1174
- export type AuthType = 'permanentServerTokens' | 'jwtToken' | 'basic' | 'custom';
1175
-
1176
- // Custom Authentication validator function (black box - receives full request)
1177
- export type CustomAuthValidator = (req: any) => Promise<AuthResult> | AuthResult;
1178
-
1179
- // Authentication result interface
1180
- export interface AuthResult {
1181
- success: boolean;
1182
- error?: string;
1183
- authType?: AuthType;
1184
- username?: string;
1185
- isTokenDecrypted?: boolean; // only for JWT
1186
- payload?: any;
1187
- }
1188
-
1189
- // Authentication detection result
1190
- export interface AuthDetectionResult {
1191
- configured: AuthType[]; // Authentication types found in configuration
1192
- valid: AuthType[]; // Authentication types properly configured and ready
1193
- errors: Record<string, string[]>; // Configuration errors by auth type
1194
- }
1195
- ```
1196
-
1197
- ##### Core Multi-Authentication Functions
1198
-
1199
- ```typescript
1200
- // checkMultiAuth - validate using all configured authentication methods
1201
- // Function Signature:
1202
- async function checkMultiAuth(req: Request): Promise<AuthResult> {...}
1203
-
1204
- // Example:
1205
- const result = await checkMultiAuth(req);
1206
-
1207
- if (result.success) {
1208
- console.log(`Authenticated via ${result.authType} as ${result.username}`);
1209
- } else {
1210
- console.log('Authentication failed:', result.error);
1211
- }
1212
-
1213
- // detectAuthConfiguration - analyze auth configuration
1214
- // Function Signature:
1215
- function detectAuthConfiguration(): AuthDetectionResult {...}
1216
-
1217
- // Example:
1218
- const detection = detectAuthConfiguration();
1219
- console.log('Configured auth types:', detection.configured);
1220
- console.log('Valid auth types:', detection.valid);
1221
- console.log('Configuration errors:', detection.errors);
1222
-
1223
- // logAuthConfiguration - log auth system status (debugging)
1224
- // Function Signature:
1225
- function logAuthConfiguration(): void {...}
1226
-
1227
- // Example:
1228
- logAuthConfiguration();
1229
- // Output:
1230
- // Auth system configuration:
1231
- // - enabled: true
1232
- // - configured types: permanentServerTokens, basic
1233
- ```
1234
-
1235
- ##### Multi-Authentication Middleware
1236
-
1237
- ```typescript
1238
- import express from 'express';
1239
- import {
1240
- createAuthMW,
1241
- getMultiAuthError,
1242
- } from 'fa-mcp-sdk';
1243
-
1244
- // Universal authentication middleware with flexible options
1245
- const app = express();
1246
-
1247
- // Basic usage - handles all authentication scenarios automatically
1248
- const authMW = createAuthMW();
1249
- app.use('/api', authMW);
1250
-
1251
- app.get('/api/protected', (req, res) => {
1252
- const authInfo = (req as any).authInfo;
1253
- res.json({
1254
- message: 'Access granted',
1255
- authType: authInfo?.authType,
1256
- username: authInfo?.username,
1257
- });
1258
- });
1259
-
1260
- // Advanced usage with custom options
1261
- const customAuthMW = createAuthMW({
1262
- mcpPaths: ['/mcp', '/messages', '/sse', '/custom'], // Custom MCP paths
1263
- logConfig: true, // Force logging
1264
- });
1265
- app.use('/custom-endpoints', customAuthMW);
1266
-
1267
- // createAuthMW - Universal authentication middleware
1268
- // Function Signature:
1269
- function createAuthMW(options?: {
1270
- mcpPaths?: string[]; // Paths to check for public MCP requests (default: ['/mcp', '/messages', '/sse'])
1271
- logConfig?: boolean; // Log auth configuration on first request (default: from LOG_AUTH_CONFIG env)
1272
- }): (req: Request, res: Response, next: NextFunction) => Promise<void>
1273
-
1274
- // Features:
1275
- // ✅ Combines all authentication methods (standard + custom validator)
1276
- // ✅ Supports public MCP resources/prompts (requireAuth: false)
1277
- // ✅ Configurable MCP paths
1278
- // ✅ CPU-optimized authentication order
1279
- // ✅ Automatic auth method detection
1280
- // ✅ Request context enrichment (req.authInfo)
1281
-
1282
- // getMultiAuthError - Programmatic authentication checking
1283
- // Function Signature:
1284
- async function getMultiAuthError(req: Request): Promise<{ code: number, message: string } | undefined>
1285
-
1286
- // Returns error object if authentication failed, undefined if successful
1287
- // Uses checkMultiAuth internally - supports all authentication methods
1288
-
1289
- // Example - Custom middleware with different auth levels
1290
- app.use('/api/custom', async (req, res, next) => {
1291
- if (req.path.startsWith('/api/custom/public')) {
1292
- return next(); // Public endpoints
1293
- }
1294
-
1295
- if (req.path.startsWith('/api/custom/admin')) {
1296
- // Admin endpoints - require server tokens only
1297
- const token = (req.headers.authorization || '').replace(/^Bearer */, '');
1298
- if (appConfig.webServer.auth.permanentServerTokens.includes(token)) {
1299
- return next();
1300
- }
1301
- return res.status(403).json({ error: 'Admin access required' });
1302
- }
1303
-
1304
- // Regular endpoints - use full multi-auth
1305
- try {
1306
- const authError = await getMultiAuthError(req);
1307
- if (authError) {
1308
- res.status(authError.code).send(authError.message);
1309
- return;
1310
- }
1311
- next();
1312
- } catch (error) {
1313
- res.status(500).send('Authentication error');
1314
- }
1315
- });
1316
-
1317
- ```
1318
-
1319
- ##### Custom Authentication
1320
-
1321
- You can provide custom authentication validation functions through the `McpServerData` interface. The custom validator receives the full Express request object, allowing for flexible authentication logic:
1322
-
1323
- ```typescript
1324
- import { McpServerData, CustomAuthValidator } from 'fa-mcp-sdk';
1325
-
1326
- // Database-backed authentication with request context
1327
- const databaseAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> => {
1328
- try {
1329
- // Extract authentication data from various sources
1330
- const authHeader = req.headers.authorization;
1331
- const username = req.headers['x-username'];
1332
- const apiKey = req.headers['x-api-key'];
1333
-
1334
- if (authHeader?.startsWith('Basic ')) {
1335
- const [user, pass] = Buffer.from(authHeader.slice(6), 'base64').toString().split(':');
1336
- const dbUser = await getUserFromDatabase(user);
1337
-
1338
- if (dbUser && await comparePassword(pass, dbUser.hashedPassword)) {
1339
- return {
1340
- success: true,
1341
- authType: 'basic',
1342
- username: dbUser.username,
1343
- payload: { userId: dbUser.id, roles: dbUser.roles }
1344
- };
1345
- }
1346
- }
1347
-
1348
- if (apiKey && username) {
1349
- const isValid = await validateUserApiKey(username, apiKey);
1350
- if (isValid) {
1351
- return {
1352
- success: true,
1353
- authType: 'basic',
1354
- username: username,
1355
- payload: { apiKey: apiKey.substring(0, 8) + '...' }
1356
- };
1357
- }
1358
- }
1359
-
1360
- return { success: false, error: 'Invalid credentials' };
1361
- } catch (error) {
1362
- console.error('Database authentication error:', error);
1363
- return { success: false, error: 'Database authentication error' };
1364
- }
1365
- };
1366
-
1367
- // IP-based authentication with time restrictions
1368
- const ipBasedAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> => {
1369
- try {
1370
- const clientIP = req.headers['x-real-ip'] || req.connection?.remoteAddress;
1371
- const userAgent = req.headers['user-agent'];
1372
-
1373
- // Check IP whitelist
1374
- if (!isIPAllowed(clientIP)) {
1375
- return { success: false, error: `IP address ${clientIP} not allowed` };
1376
- }
1377
-
1378
- // Block bots and crawlers
1379
- if (userAgent?.includes('bot') || userAgent?.includes('crawler')) {
1380
- return { success: false, error: 'Bots and crawlers are not allowed' };
1381
- }
1382
-
1383
- // Time-based restrictions (business hours only)
1384
- const hour = new Date().getHours();
1385
- if (hour < 9 || hour > 17) {
1386
- return { success: false, error: 'Access only allowed during business hours (9-17)' };
1387
- }
1388
-
1389
- return {
1390
- success: true,
1391
- authType: 'basic',
1392
- username: `ip-${clientIP}`,
1393
- payload: { clientIP, userAgent, accessTime: new Date().toISOString() }
1394
- };
1395
- } catch (error) {
1396
- console.error('IP authentication error:', error);
1397
- return { success: false, error: 'IP authentication error' };
1398
- }
1399
- };
1400
-
1401
- // External service authentication
1402
- const externalServiceAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> => {
1403
- try {
1404
- const token = req.headers.authorization?.replace('Bearer ', '');
1405
- const clientId = req.headers['x-client-id'];
1406
-
1407
- if (!token || !clientId) {
1408
- return { success: false, error: 'Missing token or client ID' };
1409
- }
1410
-
1411
- const response = await fetch('https://auth.example.com/validate', {
1412
- method: 'POST',
1413
- headers: { 'Content-Type': 'application/json' },
1414
- body: JSON.stringify({ token, clientId, ip: req.ip })
1415
- });
1416
-
1417
- if (!response.ok) {
1418
- return { success: false, error: 'External service validation failed' };
1419
- }
1420
-
1421
- const result = await response.json();
1422
- if (result.valid === true) {
1423
- return {
1424
- success: true,
1425
- authType: 'basic',
1426
- username: result.username || clientId,
1427
- payload: {
1428
- clientId,
1429
- externalUserId: result.userId,
1430
- scopes: result.scopes
1431
- }
1432
- };
1433
- } else {
1434
- return { success: false, error: result.error || 'Invalid token' };
1435
- }
1436
- } catch (error) {
1437
- console.error('External service authentication error:', error);
1438
- return { success: false, error: 'External service authentication error' };
1439
- }
1440
- };
1441
-
1442
- // Multi-factor authentication with context
1443
- const mfaAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> => {
1444
- try {
1445
- const authHeader = req.headers.authorization;
1446
- const mfaToken = req.headers['x-mfa-token'];
1447
- const userSession = req.headers['x-session-id'];
1448
-
1449
- if (!authHeader?.startsWith('Basic ') || !mfaToken) {
1450
- return { success: false, error: 'Missing basic auth or MFA token' };
1451
- }
1452
-
1453
- const [username, password] = Buffer.from(authHeader.slice(6), 'base64').toString().split(':');
1454
-
1455
- // Validate base credentials
1456
- const user = await getUserFromDatabase(username);
1457
- if (!user || !(await comparePassword(password, user.hashedPassword))) {
1458
- return { success: false, error: 'Invalid username or password' };
1459
- }
1460
-
1461
- // Validate MFA token and session
1462
- const mfaValid = await validateMFAToken(username, mfaToken, userSession);
1463
- if (mfaValid) {
1464
- return {
1465
- success: true,
1466
- authType: 'basic',
1467
- username: username,
1468
- payload: {
1469
- userId: user.id,
1470
- sessionId: userSession,
1471
- mfaMethod: 'totp' // or whatever MFA method was used
1472
- }
1473
- };
1474
- } else {
1475
- return { success: false, error: 'Invalid MFA token or session' };
1476
- }
1477
- } catch (error) {
1478
- console.error('MFA authentication error:', error);
1479
- return { success: false, error: 'MFA authentication error' };
1480
- }
1481
- };
1482
-
1483
- // Use custom validator in MCP server
1484
- const serverData: McpServerData = {
1485
- tools,
1486
- toolHandler,
1487
- agentBrief: 'My MCP Server',
1488
- agentPrompt: 'Server with custom authentication',
1489
-
1490
- // Provide custom authentication validator (black box function)
1491
- customAuthValidator: databaseAuthValidator, // or ipBasedAuthValidator, externalServiceAuthValidator, mfaAuthValidator
1492
-
1493
- // ... other configuration
1494
- };
1495
-
1496
- await initMcpServer(serverData);
1497
- ```
1498
-
1499
- ##### Usage Examples
1500
-
1501
- ```typescript
1502
- // Test authentication programmatically
1503
- app.post('/test-token', async (req, res) => {
1504
- const { token } = req.body;
1505
- if (!token) {
1506
- return res.status(400).json({ error: 'Token required' });
1507
- }
1508
-
1509
- try {
1510
- const result = await checkMultiAuth(req);
1511
- res.json({
1512
- valid: result.success,
1513
- authType: result.authType,
1514
- error: result.error,
1515
- username: result.username,
1516
- hasPayload: !!result.payload
1517
- });
1518
- } catch (error) {
1519
- res.status(500).json({ error: 'Authentication test failed' });
1520
- }
1521
- });
1522
-
1523
- // Different authentication requirements for different endpoints
1524
- app.use('/rest', createAuthMW()); // Standard auth with all methods
1525
- app.use('/graphql', createAuthMW({ logConfig: false })); // Silent auth
1526
- app.use('/websocket', createAuthMW({ mcpPaths: [] })); // No public MCP paths
1527
- ```
1528
-
1529
- **Authentication Logic Flow:**
1530
-
1531
- The enhanced authentication system follows this logic:
1532
-
1533
- 1. **If configured auth methods exist** (permanentServerTokens, jwtToken, basic + auth.enabled = true):
1534
- - Standard MCP authentication runs first
1535
- - If successful AND custom validator exists → run custom validator additionally
1536
- - Both must pass for authentication to succeed
1537
-
1538
- 2. **If no standard auth OR standard auth fails:**
1539
- - Custom validator runs as fallback (if configured)
1540
- - Can authenticate using custom logic alone
1541
-
1542
- 3. **Custom validator is completely independent:**
1543
- - Receives full Express request object
1544
- - Can implement any authentication/authorization logic
1545
- - Works as black box as requested
1546
-
1547
- **Client Usage Examples:**
1548
-
1549
- ```bash
1550
- # Using permanent server token
1551
- curl -H "Authorization: Bearer server-token-1" http://localhost:3000/mcp
1552
-
1553
- # Using JWT token
1554
- curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhb..." http://localhost:3000/mcp
1555
-
1556
- # Using Basic Authentication
1557
- curl -H "Authorization: Basic $(echo -n 'admin:password' | base64)" http://localhost:3000/mcp
1558
-
1559
- # Using custom headers for custom validator
1560
- curl -H "X-User-ID: john.doe" \
1561
- -H "X-API-Key: custom-api-key-12345" \
1562
- -H "X-Client-IP: 192.168.1.10" \
1563
- http://localhost:3000/mcp
1564
-
1565
- # Using custom authentication with context
1566
- curl -H "Authorization: Bearer token123" \
1567
- -H "X-MFA-Token: 123456" \
1568
- -H "X-Session-ID: sess_abc123" \
1569
- http://localhost:3000/mcp
1570
- ```
1571
-
1572
- The multi-authentication system automatically tries authentication methods in CPU-optimized order (fastest first) and returns on the first successful match, providing both performance and flexibility.
1573
-
1574
- ### Check if a user belongs to an AD group
1575
-
1576
- #### Configuration (`config/local.yaml`)
1577
-
1578
- ```yaml
1579
- ad:
1580
- domains:
1581
- MYDOMAIN:
1582
- default: true
1583
- controllers: ['ldap://dc1.corp.com']
1584
- username: 'svc_account@corp.com'
1585
- password: '***'
1586
- # baseDn: 'DC=corp,DC=com' # Optional, auto-derived from controller URL
1587
- ```
1588
-
1589
- #### Usage
1590
-
1591
- ```typescript
1592
- import { initADGroupChecker } from 'fa-mcp-sdk';
1593
-
1594
- const { isUserInGroup, groupChecker } = initADGroupChecker();
1595
-
1596
- const isAdmin = await isUserInGroup('john.doe', 'Admins');
1597
- const isDeveloper = await isUserInGroup('john.doe', 'Developers');
1598
-
1599
- groupChecker.clearCache(); // Clear cache if needed
1600
- ```
1601
-
1602
- ### Advanced Authorization with AD Group Membership
1603
-
1604
- This section demonstrates how to implement additional authorization based on Active Directory (AD)
1605
- group membership. These examples assume JWT token authentication (`jwtToken`) is configured,
1606
- and the user information is extracted from the JWT payload.
1607
-
1608
- #### Configuration for AD Group Authorization
1609
-
1610
- First, extend your configuration to include the required AD group:
1611
-
1612
- **`src/_types_/custom-config.ts`:**
1613
- ```typescript
1614
- import { AppConfig } from 'fa-mcp-sdk';
1615
-
1616
- export interface IGroupAccessConfig {
1617
- groupAccess: {
1618
- /** AD group required for access */
1619
- requiredGroup: string;
1620
- /** Bypass group check for debugging (default: false) */
1621
- bypassGroupCheck?: boolean;
1622
- /** Cache TTL in seconds (default: 300) */
1623
- cacheTtlSeconds?: number;
1624
- };
1625
- }
1626
-
1627
- export interface CustomAppConfig extends AppConfig, IGroupAccessConfig {}
1628
- ```
1629
-
1630
- **`config/default.yaml`:**
1631
- ```yaml
1632
- groupAccess:
1633
- requiredGroup: "DOMAIN\\MCP-Users"
1634
- bypassGroupCheck: false
1635
- cacheTtlSeconds: 300
1636
- ```
1637
-
1638
- #### Example 1: HTTP Server Level Access Restriction
1639
-
1640
- This example uses `customAuthValidator` to check AD group membership at the HTTP server level.
1641
- If the user is not in the required group, a 403 Forbidden error is returned before any
1642
- MCP request processing.
1643
-
1644
- **`src/start.ts`:**
1645
- ```typescript
1646
- import {
1647
- appConfig,
1648
- initMcpServer,
1649
- McpServerData,
1650
- CustomAuthValidator,
1651
- AuthResult,
1652
- initADGroupChecker,
1653
- checkJwtToken,
1654
- } from 'fa-mcp-sdk';
1655
- import { tools } from './tools/tools.js';
1656
- import { handleToolCall } from './tools/handle-tool-call.js';
1657
- import { AGENT_BRIEF } from './prompts/agent-brief.js';
1658
- import { AGENT_PROMPT } from './prompts/agent-prompt.js';
1659
- import { CustomAppConfig } from './_types_/custom-config.js';
1660
-
1661
- // Get typed config
1662
- const config = appConfig as CustomAppConfig;
1663
-
1664
- // Initialize AD group checker
1665
- const { isUserInGroup } = initADGroupChecker();
1666
-
1667
- /**
1668
- * Custom authentication validator with AD group membership check
1669
- * Returns 403 Forbidden if user is not in the required AD group
1670
- */
1671
- const customAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> => {
1672
- const authHeader = req.headers.authorization;
1673
-
1674
- if (!authHeader?.startsWith('Bearer ')) {
1675
- return { success: false, error: 'Missing or invalid Authorization header' };
1676
- }
1677
-
1678
- const token = authHeader.slice(7);
1679
-
1680
- // Validate JWT token
1681
- const tokenResult = checkJwtToken({ token });
1682
- if (tokenResult.errorReason) {
1683
- return { success: false, error: tokenResult.errorReason };
1684
- }
1685
-
1686
- const payload = tokenResult.payload;
1687
- if (!payload?.user) {
1688
- return { success: false, error: 'Invalid token: missing user' };
1689
- }
1690
-
1691
- const username = payload.user;
1692
-
1693
- // Bypass group check if configured (for debugging)
1694
- if (config.groupAccess.bypassGroupCheck) {
1695
- return {
1696
- success: true,
1697
- authType: 'jwtToken',
1698
- username,
1699
- payload,
1700
- isTokenDecrypted: tokenResult.isTokenDecrypted,
1701
- };
1702
- }
1703
-
1704
- // Check AD group membership
1705
- const requiredGroup = config.groupAccess.requiredGroup;
1706
- try {
1707
- const isInGroup = await isUserInGroup(username, requiredGroup);
1708
-
1709
- if (!isInGroup) {
1710
- return {
1711
- success: false,
1712
- error: `Forbidden: User '${username}' is not a member of group '${requiredGroup}'`,
1713
- };
1714
- }
1715
-
1716
- return {
1717
- success: true,
1718
- authType: 'jwtToken',
1719
- username,
1720
- payload,
1721
- isTokenDecrypted: tokenResult.isTokenDecrypted,
1722
- };
1723
- } catch (error) {
1724
- const errorMessage = error instanceof Error ? error.message : String(error);
1725
- return {
1726
- success: false,
1727
- error: `AD group check failed: ${errorMessage}`,
1728
- };
1729
- }
1730
- };
1731
-
1732
- const startProject = async (): Promise<void> => {
1733
- const serverData: McpServerData = {
1734
- tools,
1735
- toolHandler: handleToolCall,
1736
- agentBrief: AGENT_BRIEF,
1737
- agentPrompt: AGENT_PROMPT,
1738
-
1739
- // Enable custom authentication with AD group check
1740
- customAuthValidator,
1741
-
1742
- // ... other configuration
1743
- };
1744
-
1745
- await initMcpServer(serverData);
1746
- };
1747
-
1748
- startProject().catch(console.error);
1749
- ```
1750
-
1751
- **Result**: If the user is not in the required AD group, they receive HTTP 403 Forbidden
1752
- response before any MCP processing occurs.
1753
-
1754
- #### Example 2: Access Restriction to ALL MCP Tools
1755
-
1756
- This example restricts access to all MCP tools by checking AD group membership in the
1757
- `toolHandler` function. If the user is not in the required group, the tool call returns
1758
- an MCP error with "Forbidden" message.
1759
-
1760
- **`src/tools/handle-tool-call.ts`:**
1761
- ```typescript
1762
- import {
1763
- formatToolResult,
1764
- ToolExecutionError,
1765
- logger,
1766
- appConfig,
1767
- initADGroupChecker,
1768
- } from 'fa-mcp-sdk';
1769
- import { CustomAppConfig } from '../_types_/custom-config.js';
1770
-
1771
- // Get typed config
1772
- const config = appConfig as CustomAppConfig;
1773
-
1774
- // Initialize AD group checker
1775
- const { isUserInGroup } = initADGroupChecker();
1776
-
1777
- /**
1778
- * Check if user has access to MCP tools based on AD group membership
1779
- */
1780
- async function checkToolAccess(payload: { user: string; [key: string]: any } | undefined): Promise<void> {
1781
- // Skip check if bypass is enabled
1782
- if (config.groupAccess.bypassGroupCheck) {
1783
- return;
1784
- }
1785
-
1786
- if (!payload?.user) {
1787
- throw new ToolExecutionError('authorization', 'Forbidden: User information not available');
1788
- }
1789
-
1790
- const username = payload.user;
1791
- const requiredGroup = config.groupAccess.requiredGroup;
1792
-
1793
- try {
1794
- const isInGroup = await isUserInGroup(username, requiredGroup);
1795
-
1796
- if (!isInGroup) {
1797
- throw new ToolExecutionError(
1798
- 'authorization',
1799
- `Forbidden: User '${username}' is not authorized to use MCP tools. ` +
1800
- `Required group: '${requiredGroup}'`
1801
- );
1802
- }
1803
- } catch (error) {
1804
- if (error instanceof ToolExecutionError) {
1805
- throw error;
1806
- }
1807
- const errorMessage = error instanceof Error ? error.message : String(error);
1808
- throw new ToolExecutionError('authorization', `Forbidden: AD group check failed - ${errorMessage}`);
1809
- }
1810
- }
1811
-
1812
- export const handleToolCall = async (params: {
1813
- name: string;
1814
- arguments?: any;
1815
- headers?: Record<string, string>;
1816
- payload?: { user: string; [key: string]: any };
1817
- }): Promise<any> => {
1818
- const { name, arguments: args, headers, payload } = params;
1819
-
1820
- logger.info(`Tool called: ${name} by user: ${payload?.user || 'unknown'}`);
1821
-
1822
- // Check AD group membership for ALL tools
1823
- await checkToolAccess(payload);
1824
-
1825
- try {
1826
- switch (name) {
1827
- case 'my_tool':
1828
- return await handleMyTool(args);
1829
- case 'another_tool':
1830
- return await handleAnotherTool(args);
1831
- default:
1832
- throw new ToolExecutionError(name, `Unknown tool: ${name}`);
1833
- }
1834
- } catch (error) {
1835
- logger.error(`Tool execution failed for ${name}:`, error);
1836
- throw error;
1837
- }
1838
- };
1839
-
1840
- async function handleMyTool(args: any): Promise<any> {
1841
- // Tool implementation
1842
- return formatToolResult({ message: 'Tool executed successfully', args });
1843
- }
1844
-
1845
- async function handleAnotherTool(args: any): Promise<any> {
1846
- // Tool implementation
1847
- return formatToolResult({ message: 'Another tool executed', args });
1848
- }
1849
- ```
1850
-
1851
- **Result**: If the user is not in the required AD group, any tool call returns an MCP error:
1852
- ```json
1853
- {
1854
- "jsonrpc": "2.0",
1855
- "error": {
1856
- "code": -32603,
1857
- "message": "Forbidden: User 'john.doe' is not authorized to use MCP tools. Required group: 'DOMAIN\\MCP-Users'"
1858
- },
1859
- "id": 1
1860
- }
1861
- ```
1862
-
1863
- #### Example 3: Access Restriction to a SPECIFIC MCP Tool
1864
-
1865
- This example restricts access to specific MCP tools based on AD group membership.
1866
- Different tools can require different AD groups.
1867
-
1868
- **`src/_types_/custom-config.ts`:**
1869
- ```typescript
1870
- import { AppConfig } from 'fa-mcp-sdk';
1871
-
1872
- export interface IToolGroupAccessConfig {
1873
- toolGroupAccess: {
1874
- /** Default group required for tools without specific configuration */
1875
- defaultGroup?: string;
1876
- /** Specific group requirements per tool */
1877
- tools: Record<string, {
1878
- /** AD group required for this tool */
1879
- requiredGroup: string;
1880
- /** Allow access without group check (default: false) */
1881
- public?: boolean;
1882
- }>;
1883
- /** Bypass all group checks (for debugging) */
1884
- bypassGroupCheck?: boolean;
1885
- };
1886
- }
1887
-
1888
- export interface CustomAppConfig extends AppConfig, IToolGroupAccessConfig {}
1889
- ```
1890
-
1891
- **`config/default.yaml`:**
1892
- ```yaml
1893
- toolGroupAccess:
1894
- defaultGroup: "DOMAIN\\MCP-Users"
1895
- bypassGroupCheck: false
1896
- tools:
1897
- get_public_data:
1898
- public: true # No group check required
1899
- get_user_data:
1900
- requiredGroup: "DOMAIN\\MCP-Users"
1901
- modify_data:
1902
- requiredGroup: "DOMAIN\\MCP-DataModifiers"
1903
- admin_operation:
1904
- requiredGroup: "DOMAIN\\MCP-Admins"
1905
- ```
1906
-
1907
- **`src/tools/handle-tool-call.ts`:**
1908
- ```typescript
1909
- import {
1910
- formatToolResult,
1911
- ToolExecutionError,
1912
- logger,
1913
- appConfig,
1914
- initADGroupChecker,
1915
- } from 'fa-mcp-sdk';
1916
- import { CustomAppConfig } from '../_types_/custom-config.js';
1917
-
1918
- // Get typed config
1919
- const config = appConfig as CustomAppConfig;
1920
-
1921
- // Initialize AD group checker
1922
- const { isUserInGroup } = initADGroupChecker();
1923
-
1924
- /**
1925
- * Check if user has access to a specific tool based on AD group membership
1926
- */
1927
- async function checkToolAccess(
1928
- toolName: string,
1929
- payload: { user: string; [key: string]: any } | undefined
1930
- ): Promise<void> {
1931
- const toolAccess = config.toolGroupAccess;
1932
-
1933
- // Skip check if bypass is enabled
1934
- if (toolAccess.bypassGroupCheck) {
1935
- return;
1936
- }
1937
-
1938
- const toolConfig = toolAccess.tools[toolName];
1939
-
1940
- // If tool is marked as public, allow access
1941
- if (toolConfig?.public) {
1942
- return;
1943
- }
1944
-
1945
- // Check user availability
1946
- if (!payload?.user) {
1947
- throw new ToolExecutionError(
1948
- toolName,
1949
- `Forbidden: User information not available for tool '${toolName}'`
1950
- );
1951
- }
1952
-
1953
- const username = payload.user;
1954
-
1955
- // Determine required group: tool-specific or default
1956
- const requiredGroup = toolConfig?.requiredGroup || toolAccess.defaultGroup;
1957
-
1958
- if (!requiredGroup) {
1959
- // No group configured - allow access
1960
- return;
1961
- }
1962
-
1963
- try {
1964
- const isInGroup = await isUserInGroup(username, requiredGroup);
1965
-
1966
- if (!isInGroup) {
1967
- throw new ToolExecutionError(
1968
- toolName,
1969
- `Forbidden: User '${username}' is not authorized to use tool '${toolName}'. ` +
1970
- `Required group: '${requiredGroup}'`
1971
- );
1972
- }
1973
-
1974
- logger.info(`User '${username}' authorized for tool '${toolName}' via group '${requiredGroup}'`);
1975
- } catch (error) {
1976
- if (error instanceof ToolExecutionError) {
1977
- throw error;
1978
- }
1979
- const errorMessage = error instanceof Error ? error.message : String(error);
1980
- throw new ToolExecutionError(
1981
- toolName,
1982
- `Forbidden: AD group check failed for tool '${toolName}' - ${errorMessage}`
1983
- );
1984
- }
1985
- }
1986
-
1987
- export const handleToolCall = async (params: {
1988
- name: string;
1989
- arguments?: any;
1990
- headers?: Record<string, string>;
1991
- payload?: { user: string; [key: string]: any };
1992
- }): Promise<any> => {
1993
- const { name, arguments: args, headers, payload } = params;
1994
-
1995
- logger.info(`Tool called: ${name} by user: ${payload?.user || 'unknown'}`);
1996
-
1997
- // Check AD group membership for the specific tool
1998
- await checkToolAccess(name, payload);
1999
-
2000
- try {
2001
- switch (name) {
2002
- case 'get_public_data':
2003
- // Public tool - no group check was performed
2004
- return await handleGetPublicData(args);
2005
-
2006
- case 'get_user_data':
2007
- // Requires MCP-Users group
2008
- return await handleGetUserData(args);
2009
-
2010
- case 'modify_data':
2011
- // Requires MCP-DataModifiers group
2012
- return await handleModifyData(args);
2013
-
2014
- case 'admin_operation':
2015
- // Requires MCP-Admins group
2016
- return await handleAdminOperation(args);
2017
-
2018
- default:
2019
- // Unknown tools use defaultGroup if configured
2020
- throw new ToolExecutionError(name, `Unknown tool: ${name}`);
2021
- }
2022
- } catch (error) {
2023
- logger.error(`Tool execution failed for ${name}:`, error);
2024
- throw error;
2025
- }
2026
- };
2027
-
2028
- async function handleGetPublicData(args: any): Promise<any> {
2029
- return formatToolResult({ message: 'Public data retrieved', data: { public: true } });
2030
- }
2031
-
2032
- async function handleGetUserData(args: any): Promise<any> {
2033
- return formatToolResult({ message: 'User data retrieved', data: args });
2034
- }
2035
-
2036
- async function handleModifyData(args: any): Promise<any> {
2037
- return formatToolResult({ message: 'Data modified', modified: args });
2038
- }
2039
-
2040
- async function handleAdminOperation(args: any): Promise<any> {
2041
- return formatToolResult({ message: 'Admin operation completed', operation: args });
2042
- }
2043
- ```
2044
-
2045
- **Result**: Each tool enforces its own AD group requirements:
2046
- - `get_public_data` - accessible to everyone (public)
2047
- - `get_user_data` - requires `DOMAIN\MCP-Users` group
2048
- - `modify_data` - requires `DOMAIN\MCP-DataModifiers` group
2049
- - `admin_operation` - requires `DOMAIN\MCP-Admins` group
2050
-
2051
- If a user tries to call a tool without being in the required group:
2052
- ```json
2053
- {
2054
- "jsonrpc": "2.0",
2055
- "error": {
2056
- "code": -32603,
2057
- "message": "Forbidden: User 'john.doe' is not authorized to use tool 'admin_operation'. Required group: 'DOMAIN\\MCP-Admins'"
2058
- },
2059
- "id": 1
2060
- }
2061
- ```
2062
-
2063
- #### Summary: Authorization Levels
2064
-
2065
- | Level | Location | Error Type | Use Case |
2066
- |-------|----------|------------|----------|
2067
- | HTTP Server | `customAuthValidator` | HTTP 403 Forbidden | Block unauthorized users completely |
2068
- | All Tools | `toolHandler` (global check) | MCP Tool Error | Allow HTTP access, restrict all tool usage |
2069
- | Specific Tool | `toolHandler` (per-tool check) | MCP Tool Error | Fine-grained tool-level permissions |
2070
-
2071
-
2072
- ### Utility Functions
2073
-
2074
- #### General Utilities
2075
-
2076
- ```typescript
2077
- import {
2078
- trim,
2079
- isMainModule,
2080
- isNonEmptyObject,
2081
- isObject,
2082
- ppj,
2083
- encodeSvgForDataUri,
2084
- getAsset
2085
- } from 'fa-mcp-sdk';
2086
-
2087
- // trim - safely trim string with null/undefined handling
2088
- // Function Signature:
2089
- const trim = (s: any): string {...}
2090
-
2091
- // Examples:
2092
- const cleanText1 = trim(' hello '); // Returns 'hello'
2093
- const cleanText2 = trim(null); // Returns ''
2094
- const cleanText3 = trim(undefined); // Returns ''
2095
- const cleanText4 = trim(123); // Returns '123'
2096
-
2097
- // isMainModule - check if current module is the main entry point
2098
- // Function Signature:
2099
- const isMainModule = (url: string): boolean {...}
2100
-
2101
- // Example:
2102
- if (isMainModule(import.meta.url)) {
2103
- console.log('Running as main module');
2104
- startServer();
2105
- }
2106
-
2107
- // isObject - check if value is an object (not null, not array)
2108
- // Function Signature:
2109
- const isObject = (o: any): boolean {...}
2110
-
2111
- // Examples:
2112
- isObject({}); // Returns true
2113
- isObject({ key: 'value' }); // Returns true
2114
- isObject([]); // Returns false
2115
- isObject(null); // Returns false
2116
- isObject('string'); // Returns false
2117
-
2118
- // isNonEmptyObject - check if value is non-empty object with defined values
2119
- // Function Signature:
2120
- const isNonEmptyObject = (o: any): boolean {...}
2121
-
2122
- // Examples:
2123
- isNonEmptyObject({ key: 'value' }); // Returns true
2124
- isNonEmptyObject({}); // Returns false
2125
- isNonEmptyObject({ key: undefined }); // Returns false
2126
- isNonEmptyObject([]); // Returns false
2127
-
2128
- // ppj - pretty-print JSON with 2-space indentation
2129
- // Function Signature:
2130
- const ppj = (v: any): string {...}
2131
-
2132
- // Example:
2133
- const formatted = ppj({ user: 'john', age: 30 });
2134
- // Returns:
2135
- // {
2136
- // "user": "john",
2137
- // "age": 30
2138
- // }
2139
-
2140
- // encodeSvgForDataUri - encode SVG content for use in data URI
2141
- // Function Signature:
2142
- const encodeSvgForDataUri = (svg: string): string {...}
2143
-
2144
- // Example:
2145
- const svgContent = '<svg xmlns="http://www.w3.org/2000/svg"><circle r="10"/></svg>';
2146
- const encoded = encodeSvgForDataUri(svgContent);
2147
- const dataUri = `data:image/svg+xml,${encoded}`;
2148
-
2149
- // getAsset - get asset file content from src/asset folder
2150
- // Function Signature:
2151
- const getAsset = (relPathFromAssetRoot: string): string | undefined {...}
2152
-
2153
- // Example:
2154
- const logoContent = getAsset('logo.svg'); // Reads from src/asset/logo.svg
2155
- const iconContent = getAsset('icons/star.svg'); // Reads from src/asset/icons/star.svg
2156
- ```
2157
-
2158
- #### Network Utilities
2159
-
2160
- ```typescript
2161
- import { isPortAvailable, checkPortAvailability } from 'fa-mcp-sdk';
2162
-
2163
- // isPortAvailable - check if port is available for binding
2164
- // Function Signature:
2165
- function isPortAvailable (port: number, host: string = '0.0.0.0'): Promise<boolean> {...}
2166
-
2167
- // Examples:
2168
- const available1 = await isPortAvailable(3000); // Check on all interfaces
2169
- const available2 = await isPortAvailable(3000, 'localhost'); // Check on localhost
2170
- const available3 = await isPortAvailable(8080, '192.168.1.10'); // Check on specific IP
2171
-
2172
- if (available1) {
2173
- console.log('Port 3000 is available');
2174
- } else {
2175
- console.log('Port 3000 is occupied');
2176
- }
2177
-
2178
- // checkPortAvailability - check port with error handling
2179
- // Function Signature:
2180
- async function checkPortAvailability (
2181
- port: number,
2182
- host: string = '0.0.0.0',
2183
- exitOnError: boolean = true
2184
- ): Promise<void> {...}
2185
-
2186
- // Examples:
2187
- try {
2188
- // Throws error if port is busy
2189
- await checkPortAvailability(3000, 'localhost', true);
2190
- console.log('Port is available, can start server');
2191
- } catch (error) {
2192
- console.log('Port is busy:', error.message);
2193
- }
2194
-
2195
- // Don't exit process on busy port
2196
- try {
2197
- await checkPortAvailability(3000, 'localhost', false);
2198
- console.log('Port is available');
2199
- } catch (error) {
2200
- console.log('Port is occupied, will use different port');
2201
- // Continue execution instead of exiting
2202
- }
2203
- ```
2204
-
2205
- #### Tool Result Formatting
2206
-
2207
- ```typescript
2208
- import { formatToolResult, getJsonFromResult } from 'fa-mcp-sdk';
2209
-
2210
- // formatToolResult - format tool execution results based on configuration
2211
- // Function Signature:
2212
- function formatToolResult (json: any): any {...}
2213
-
2214
- // Behavior depends on appConfig.mcp.toolAnswerAs setting:
2215
- // - 'structuredContent': Returns { structuredContent: json }
2216
- // - 'text': Returns { content: [{ type: 'text', text: JSON.stringify(json, null, 2) }] }
2217
-
2218
- // Examples:
2219
- const result = {
2220
- message: 'Operation completed',
2221
- data: { count: 42, items: ['a', 'b'] },
2222
- timestamp: new Date().toISOString(),
2223
- };
2224
-
2225
- const formattedResult = formatToolResult(result);
2226
-
2227
- // If toolAnswerAs = 'structuredContent':
2228
- // {
2229
- // structuredContent: {
2230
- // message: 'Operation completed',
2231
- // data: { count: 42, items: ['a', 'b'] },
2232
- // timestamp: '2025-01-01T12:00:00.000Z'
2233
- // }
2234
- // }
2235
-
2236
- // If toolAnswerAs = 'text':
2237
- // {
2238
- // content: [{
2239
- // type: 'text',
2240
- // text: '{\n "message": "Operation completed",\n "data": {\n "count": 42,\n "items": ["a", "b"]\n },\n "timestamp": "2025-01-01T12:00:00.000Z"\n}'
2241
- // }]
2242
- // }
2243
-
2244
- // getJsonFromResult - extract original JSON from formatted result
2245
- // Function Signature:
2246
- const getJsonFromResult = <T = any> (result: any): T {...}
2247
-
2248
- // Examples:
2249
- const originalData1 = getJsonFromResult<MyDataType>(formattedResult);
2250
-
2251
- // Works with both response formats:
2252
- const structuredResponse = { structuredContent: { user: 'john', age: 30 } };
2253
- const textResponse = {
2254
- content: [{ type: 'text', text: '{"user":"john","age":30}' }]
2255
- };
2256
-
2257
- const data1 = getJsonFromResult(structuredResponse); // { user: 'john', age: 30 }
2258
- const data2 = getJsonFromResult(textResponse); // { user: 'john', age: 30 }
2259
- ```
2260
-
2261
- ### Logging
2262
-
2263
- ```typescript
2264
- import { logger, fileLogger } from 'fa-mcp-sdk';
2265
-
2266
- // Console logging
2267
- logger.info('Server started successfully');
2268
- logger.warn('Warning message');
2269
- logger.error('Error occurred', error);
2270
-
2271
- // File logging (if configured)
2272
- fileLogger.info('This goes to file');
2273
-
2274
- // Ensure file logs are written before shutdown
2275
- await fileLogger.asyncFinish();
2276
- ```
2277
-
2278
- ### Event System
2279
-
2280
- ```typescript
2281
- import { eventEmitter } from 'fa-mcp-sdk';
2282
-
2283
- // Listen for events
2284
- eventEmitter.on('server:started', (data) => {
2285
- console.log('Server started with config:', data);
2286
- });
2287
-
2288
- // Emit custom events
2289
- eventEmitter.emit('custom:event', { data: 'example' });
2290
- ```
2291
-
2292
- ### Testing Your MCP Server
2293
-
2294
- #### Test Structure
2295
-
2296
- Create tests in your `tests/` directory:
2297
-
2298
- **`tests/utils.ts`** - Test utilities:
2299
- ```typescript
2300
- import { ITestResult, logResultToFile, formatResultAsMarkdown } from 'fa-mcp-sdk';
2301
-
2302
- export interface ITestResult {
2303
- fullId: string;
2304
- toolName: string;
2305
- description: string;
2306
- parameters: unknown | null;
2307
- timestamp: string;
2308
- duration: number;
2309
- status: 'pending' | 'passed' | 'failed' | 'skipped' | 'expected_failure';
2310
- response: unknown | null;
2311
- error: string | null;
2312
- }
2313
-
2314
- // Log test results
2315
- await logResultToFile(testResult);
2316
-
2317
- // Format as markdown
2318
- const markdown = formatResultAsMarkdown(testResult);
2319
- ```
2320
-
2321
- #### Test Clients
2322
-
2323
- Use the provided test clients to test your MCP server:
2324
-
2325
- **STDIO Transport Testing:**
2326
-
2327
- ```typescript
2328
- // noinspection JSAnnotator
2329
-
2330
- import { McpStdioClient } from 'fa-mcp-sdk';
2331
- import { spawn } from 'child_process';
2332
-
2333
- const proc = spawn('node', ['dist/start.js', 'stdio'], {
2334
- stdio: ['pipe', 'pipe', 'pipe'],
2335
- env: { ...process.env, NODE_ENV: 'test' },
2336
- });
2337
-
2338
- const client = new McpStdioClient(proc);
2339
-
2340
- // Test tools
2341
- const result = await client.callTool('my_custom_tool', { query: 'test' });
2342
- console.log(result);
2343
-
2344
- // Test prompts
2345
- const prompt = await client.getPrompt('agent_brief');
2346
- console.log(prompt);
2347
- ```
2348
-
2349
- **HTTP Transport Testing:**
2350
- ```typescript
2351
- import { McpHttpClient } from 'fa-mcp-sdk';
2352
-
2353
- const client = new McpHttpClient('http://localhost:3000');
2354
-
2355
- // Test with authentication headers
2356
- const result = await client.callTool('my_custom_tool', { query: 'test' }, {
2357
- 'Authorization': 'Bearer your-jwt-token'
2358
- });
2359
- ```
2360
-
2361
- **SSE Transport Testing:**
2362
- ```typescript
2363
- import { McpSseClient } from 'fa-mcp-sdk';
2364
-
2365
- const client = new McpSseClient('http://localhost:3000');
2366
- const result = await client.callTool('my_custom_tool', { query: 'test' });
2367
- ```
2368
-
2369
- #### Test Categories and Recommendations
2370
-
2371
- 1. **Prompt Tests**:
2372
- - Test that prompts are listed correctly
2373
- - Test prompt content retrieval
2374
- - Test dynamic prompt generation
2375
-
2376
- 2. **Resource Tests**:
2377
- - Test resource listing
2378
- - Test resource content reading
2379
- - Test dynamic resource generation
2380
-
2381
- 3. **Tool Tests**:
2382
- - Test tool listing
2383
- - Test tool execution with valid parameters
2384
- - Test error handling for invalid parameters
2385
- - Test tool response formatting
2386
-
2387
- 4. **Transport Tests**:
2388
- - Test all transport types your server supports
2389
- - Test authentication (if enabled)
2390
- - Test error responses
2391
-
2392
- Example test implementation:
2393
- ```typescript
2394
- // tests/mcp/test-tools.js
2395
- async function testMyCustomTool(client) {
2396
- const name = 'Test my_custom_tool execution';
2397
- try {
2398
- const result = await client.callTool('my_custom_tool', { query: 'test input' });
2399
- const success = result?.response?.includes('Processed');
2400
- return success ?
2401
- { name, passed: true, details: result } :
2402
- { name, passed: false, details: result };
2403
- } catch (error) {
2404
- return { name, passed: false, details: { error: error.message } };
2405
- }
2406
- }
2407
- ```
2408
-
2409
- ### Consul Integration
2410
-
2411
- If using Consul for service discovery:
2412
-
2413
- ```typescript
2414
- import {
2415
- getConsulAPI,
2416
- accessPointUpdater,
2417
- deregisterServiceFromConsul
2418
- } from 'fa-mcp-sdk';
2419
-
2420
- // getConsulAPI - get configured Consul client instance
2421
- // Function Signature:
2422
- const getConsulAPI = async (): Promise<any> {...}
2423
-
2424
- // Returns Consul API client configured from appConfig.consul settings
2425
- // Example:
2426
- const consulApi = await getConsulAPI();
2427
- const services = await consulApi.catalog.service.list();
2428
- console.log('Available services:', services);
2429
-
2430
- // deregisterServiceFromConsul - remove service registration from Consul
2431
- // Function Signature:
2432
- const deregisterServiceFromConsul = async (): Promise<void> {...}
2433
-
2434
- // Note: This function reads serviceId from command line arguments (process.argv)
2435
- // Usage in command line context:
2436
- // node script.js <serviceId> [agentHost] [agentPort]
2437
-
2438
- // Example programmatic usage:
2439
- await deregisterServiceFromConsul();
2440
-
2441
- // accessPointUpdater - manage access point lifecycle
2442
- // Object with start/stop methods:
2443
- const accessPointUpdater = {
2444
- start(): void; // Start automatic access point updates
2445
- stop(): void; // Stop automatic access point updates
2446
- }
2447
-
2448
- // Examples:
2449
- accessPointUpdater.start(); // Automatically starts if appConfig.accessPoints configured
2450
- accessPointUpdater.stop(); // Stop updates (called automatically on shutdown)
2451
-
2452
- // Access point configuration in config/default.yaml:
2453
- // accessPoints:
2454
- // myService:
2455
- // title: 'My remote service'
2456
- // host: <host>
2457
- // port: 9999
2458
- // token: '***'
2459
- // noConsul: true
2460
- // consulServiceName: <consulServiceName>
2461
- ```
2462
-
2463
- ### Graceful Shutdown
2464
-
2465
- ```typescript
2466
- import { gracefulShutdown } from 'fa-mcp-sdk';
2467
-
2468
- // gracefulShutdown - perform graceful application shutdown
2469
- // Function Signature:
2470
- async function gracefulShutdown (signal: string, exitCode: number = 0): Promise<void> {...}
2471
-
2472
- // Automatically handles:
2473
- // - Stopping Consul service registration
2474
- // - Closing database connections
2475
- // - Flushing file logs
2476
- // - Stopping access point updater
2477
- // - Process exit with specified code
2478
-
2479
- // Examples:
2480
- // Manual shutdown
2481
- process.on('SIGUSR2', () => {
2482
- gracefulShutdown('SIGUSR2', 0);
2483
- });
2484
-
2485
- // Emergency shutdown
2486
- process.on('uncaughtException', (error) => {
2487
- console.error('Uncaught exception:', error);
2488
- gracefulShutdown('UNCAUGHT_EXCEPTION', 1);
2489
- });
2490
-
2491
- // Note: SDK automatically registers SIGINT and SIGTERM handlers
2492
- // in initMcpServer(), so manual registration is only needed for custom signals
2493
- ```
2494
-
2495
- ### Transport Types
2496
-
2497
- #### STDIO Transport
2498
- - Use for CLI tools and local development
2499
- - Configure with `mcp.transportType: "stdio"`
2500
- - Lightweight, no HTTP overhead
2501
-
2502
- #### HTTP Transport
2503
- - Use for web-based integrations
2504
- - Configure with `mcp.transportType: "http"`
2505
- - Supports REST API, authentication, Swagger docs
2506
- - Requires `webServer` configuration
2507
-
2508
- #### Server-Sent Events (SSE)
2509
- - Real-time streaming over HTTP
2510
- - Good for long-running operations
2511
- - Maintains persistent connections
2512
-
2513
- ### Best Practices
2514
-
2515
- #### Project Organization
2516
- 1. **Keep tools focused** - One responsibility per tool
2517
- 2. **Use TypeScript** - Leverage type safety throughout
2518
- 3. **Organize by feature** - Group related functionality
2519
- 4. **Configure environments** - Use separate configs for dev/prod
2520
-
2521
- #### Tool Development
2522
- 1. **Validate inputs** - Always check required parameters
2523
- 2. **Use formatToolResult()** - Consistent response formatting
2524
- 3. **Handle errors gracefully** - Use appropriate error classes
2525
- 4. **Log operations** - Use the provided logger
2526
-
2527
- #### Testing
2528
- 1. **Test all transports** - Ensure compatibility
2529
- 2. **Include error cases** - Test failure scenarios
2530
- 3. **Use provided clients** - Leverage built-in test utilities
2531
- 4. **Document test cases** - Clear, descriptive test names
2532
-
2533
- #### Security
2534
- 1. **Environment variables** - Never hardcode secrets
2535
- 2. **Authentication** - Enable for production HTTP servers
2536
- 3. **Input validation** - Validate all user inputs
2537
- 4. **Error messages** - Don't leak sensitive information
2538
-
2539
- This documentation provides everything needed to build, test, and deploy your own
2540
- MCP server using the FA-MCP-SDK framework.