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.
- package/cli-template/.claude/agents/fa-mcp-sdk.md +158 -0
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +216 -0
- package/cli-template/FA-MCP-SDK-DOC/01-getting-started.md +209 -0
- package/cli-template/FA-MCP-SDK-DOC/02-tools-and-api.md +321 -0
- package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +415 -0
- package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +544 -0
- package/cli-template/FA-MCP-SDK-DOC/05-ad-authorization.md +476 -0
- package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +394 -0
- package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +171 -0
- package/dist/core/_types_/types.d.ts +0 -5
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/web/home-api.js +1 -1
- package/dist/core/web/home-api.js.map +1 -1
- package/dist/core/web/openapi.d.ts +64 -0
- package/dist/core/web/openapi.d.ts.map +1 -0
- package/dist/core/web/openapi.js +235 -0
- package/dist/core/web/openapi.js.map +1 -0
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +11 -9
- package/dist/core/web/server-http.js.map +1 -1
- package/dist/core/web/static/home/index.html +4 -2
- package/dist/core/web/static/home/script.js +2 -2
- package/package.json +9 -12
- package/src/template/api/router.ts +66 -4
- package/src/template/start.ts +0 -5
- package/cli-template/FA-MCP-SDK.md +0 -2540
- 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.
|