fa-mcp-sdk 0.2.146 → 0.2.182

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +1 -1
  2. package/bin/fa-mcp.js +66 -54
  3. package/cli-template/.env.example +2 -2
  4. package/cli-template/{fa-mcp-sdk-spec.md → FA-MCP-SDK.md} +699 -42
  5. package/cli-template/README.md +2 -2
  6. package/cli-template/package.json +1 -1
  7. package/cli-template/r/TEST HTTP.xml +9 -0
  8. package/cli-template/{run/TEST SSE.run.xml → r/TEST SSE.xml } +2 -2
  9. package/cli-template/{run/TEST STDIO.run.xml → r/TEST STDIO.xml } +2 -2
  10. package/cli-template/r/generate-token.xml +14 -0
  11. package/cli-template/{run/kill-server.run.xml → r/kill-server.xml} +2 -2
  12. package/cli-template/{run/kill-token-gen-server.xml → r/remove-nul.xml} +4 -5
  13. package/{cli-template/config → config}/_local.yaml +28 -14
  14. package/{cli-template/config → config}/custom-environment-variables.yaml +3 -0
  15. package/{cli-template/config → config}/default.yaml +50 -10
  16. package/{cli-template/config → config}/development.yaml +4 -4
  17. package/config/local.yaml +89 -0
  18. package/{cli-template/config → config}/production.yaml +4 -4
  19. package/dist/core/_types_/active-directory-config.d.ts +3 -0
  20. package/dist/core/_types_/active-directory-config.d.ts.map +1 -1
  21. package/dist/core/_types_/config.d.ts +5 -1
  22. package/dist/core/_types_/config.d.ts.map +1 -1
  23. package/dist/core/_types_/types.d.ts +40 -1
  24. package/dist/core/_types_/types.d.ts.map +1 -1
  25. package/dist/core/ad/group-checker.d.ts +13 -0
  26. package/dist/core/ad/group-checker.d.ts.map +1 -0
  27. package/dist/core/ad/group-checker.js +86 -0
  28. package/dist/core/ad/group-checker.js.map +1 -0
  29. package/dist/core/auth/admin-auth.d.ts +16 -0
  30. package/dist/core/auth/admin-auth.d.ts.map +1 -0
  31. package/dist/core/auth/admin-auth.js +159 -0
  32. package/dist/core/auth/admin-auth.js.map +1 -0
  33. package/dist/core/auth/basic.d.ts +6 -0
  34. package/dist/core/auth/basic.d.ts.map +1 -0
  35. package/dist/core/auth/basic.js +26 -0
  36. package/dist/core/auth/basic.js.map +1 -0
  37. package/dist/core/auth/{jwt-validation.d.ts → jwt.d.ts} +4 -3
  38. package/dist/core/auth/jwt.d.ts.map +1 -0
  39. package/dist/core/auth/{jwt-validation.js → jwt.js} +9 -19
  40. package/dist/core/auth/jwt.js.map +1 -0
  41. package/dist/core/auth/middleware.d.ts.map +1 -1
  42. package/dist/core/auth/middleware.js +3 -3
  43. package/dist/core/auth/middleware.js.map +1 -1
  44. package/dist/core/auth/multi-auth.d.ts +14 -6
  45. package/dist/core/auth/multi-auth.d.ts.map +1 -1
  46. package/dist/core/auth/multi-auth.js +151 -141
  47. package/dist/core/auth/multi-auth.js.map +1 -1
  48. package/dist/core/auth/permanent.d.ts +6 -0
  49. package/dist/core/auth/permanent.d.ts.map +1 -0
  50. package/dist/core/auth/permanent.js +15 -0
  51. package/dist/core/auth/permanent.js.map +1 -0
  52. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts +1 -1
  53. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts.map +1 -1
  54. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js +8 -10
  55. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js.map +1 -1
  56. package/dist/core/auth/token-generator/ntlm/ntlm-integration.d.ts.map +1 -1
  57. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js +9 -2
  58. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js.map +1 -1
  59. package/dist/core/auth/token-generator/server.d.ts.map +1 -1
  60. package/dist/core/auth/token-generator/server.js +59 -25
  61. package/dist/core/auth/token-generator/server.js.map +1 -1
  62. package/dist/core/auth/types.d.ts +4 -3
  63. package/dist/core/auth/types.d.ts.map +1 -1
  64. package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
  65. package/dist/core/bootstrap/startup-info.js +19 -0
  66. package/dist/core/bootstrap/startup-info.js.map +1 -1
  67. package/dist/core/consul/access-points-updater.js +1 -1
  68. package/dist/core/consul/access-points-updater.js.map +1 -1
  69. package/dist/core/consul/get-consul-api.d.ts +1 -1
  70. package/dist/core/consul/get-consul-api.d.ts.map +1 -1
  71. package/dist/core/consul/get-consul-api.js +1 -1
  72. package/dist/core/consul/get-consul-api.js.map +1 -1
  73. package/dist/core/consul/register.d.ts +1 -1
  74. package/dist/core/consul/register.d.ts.map +1 -1
  75. package/dist/core/index.d.ts +4 -2
  76. package/dist/core/index.d.ts.map +1 -1
  77. package/dist/core/index.js +3 -1
  78. package/dist/core/index.js.map +1 -1
  79. package/dist/core/init-mcp-server.d.ts.map +1 -1
  80. package/dist/core/init-mcp-server.js +1 -1
  81. package/dist/core/init-mcp-server.js.map +1 -1
  82. package/dist/core/utils/testing/McpSseClient.js.map +1 -1
  83. package/dist/core/web/admin-router.d.ts +10 -0
  84. package/dist/core/web/admin-router.d.ts.map +1 -0
  85. package/dist/core/web/admin-router.js +309 -0
  86. package/dist/core/web/admin-router.js.map +1 -0
  87. package/dist/core/web/favicon-svg.d.ts +1 -1
  88. package/dist/core/web/favicon-svg.d.ts.map +1 -1
  89. package/dist/core/web/favicon-svg.js +21 -3
  90. package/dist/core/web/favicon-svg.js.map +1 -1
  91. package/dist/core/web/home-api.d.ts +7 -0
  92. package/dist/core/web/home-api.d.ts.map +1 -0
  93. package/dist/core/web/home-api.js +106 -0
  94. package/dist/core/web/home-api.js.map +1 -0
  95. package/dist/core/web/server-http.d.ts +1 -0
  96. package/dist/core/web/server-http.d.ts.map +1 -1
  97. package/dist/core/web/server-http.js +60 -25
  98. package/dist/core/web/server-http.js.map +1 -1
  99. package/dist/core/web/static/home/index.html +218 -0
  100. package/dist/core/web/static/home/script.js +643 -0
  101. package/dist/core/web/{about-page/css.js → static/styles.css} +435 -105
  102. package/dist/core/web/static/token-gen/index.html +105 -0
  103. package/dist/core/web/static/token-gen/jwt-icon.svg +3 -0
  104. package/dist/core/web/static/token-gen/logout.svg +4 -0
  105. package/dist/core/web/static/token-gen/script.js +553 -0
  106. package/dist/core/web/static/token-gen/user.svg +4 -0
  107. package/dist/core/web/svg-icons.d.ts +7 -0
  108. package/dist/core/web/svg-icons.d.ts.map +1 -0
  109. package/dist/core/web/svg-icons.js +78 -0
  110. package/dist/core/web/svg-icons.js.map +1 -0
  111. package/package.json +7 -3
  112. package/scripts/copy-static.js +31 -0
  113. package/src/template/_types_/custom-config.ts +83 -0
  114. package/src/template/asset/logo.svg +4 -0
  115. package/src/template/start.ts +3 -3
  116. package/src/template/tools/handle-tool-call.ts +2 -1
  117. package/src/tests/mcp/test-http.js +10 -2
  118. package/src/tests/mcp/test-sse.js +10 -2
  119. package/src/tests/mcp/test-stdio.js +1 -2
  120. package/cli-template/run/TEST HTTP.run.xml +0 -5
  121. package/cli-template/run/TEST search.run.xml +0 -11
  122. package/cli-template/run/remove-nul.js.run.xml +0 -5
  123. package/dist/core/auth/jwt-validation.d.ts.map +0 -1
  124. package/dist/core/auth/jwt-validation.js.map +0 -1
  125. package/dist/core/auth/token-generator/html.d.ts +0 -9
  126. package/dist/core/auth/token-generator/html.d.ts.map +0 -1
  127. package/dist/core/auth/token-generator/html.js +0 -862
  128. package/dist/core/auth/token-generator/html.js.map +0 -1
  129. package/dist/core/web/about-page/css.d.ts +0 -2
  130. package/dist/core/web/about-page/css.d.ts.map +0 -1
  131. package/dist/core/web/about-page/css.js.map +0 -1
  132. package/dist/core/web/about-page/render.d.ts +0 -2
  133. package/dist/core/web/about-page/render.d.ts.map +0 -1
  134. package/dist/core/web/about-page/render.js +0 -773
  135. package/dist/core/web/about-page/render.js.map +0 -1
  136. package/src/template/_examples/multi-auth-examples.ts +0 -541
  137. /package/cli-template/{run/== START ==.run.xml → r/== START ==.xml} +0 -0
  138. /package/cli-template/{run/cb.run.xml → r/cb.xml} +0 -0
  139. /package/cli-template/{run/ci.run.xml → r/ci.xml} +0 -0
  140. /package/cli-template/{run/lint.run.xml → r/lint.xml} +0 -0
  141. /package/cli-template/{run/lint_fix.run.xml → r/lint_fix.xml} +0 -0
  142. /package/cli-template/{run/reinstall.run.xml → r/reinstall.xml} +0 -0
  143. /package/{cli-template/config → config}/test.yaml +0 -0
  144. /package/{src/template/asset/favicon.svg → dist/core/web/static/logo.svg} +0 -0
  145. /package/{cli-template/scripts → scripts}/kill-port.js +0 -0
  146. /package/{cli-template/scripts → scripts}/npm/patch_node_modules.js +0 -0
  147. /package/{cli-template/scripts → scripts}/npm/run.js +0 -0
  148. /package/{cli-template/scripts → scripts}/npm/yarn-ci.ps1 +0 -0
  149. /package/{cli-template/scripts → scripts}/npm/yarn-ci.sh +0 -0
  150. /package/{cli-template/scripts → scripts}/npm/yarn-reinstall.ps1 +0 -0
  151. /package/{cli-template/scripts → scripts}/npm/yarn-reinstall.sh +0 -0
  152. /package/{cli-template/scripts → scripts}/pre-commit +0 -0
  153. /package/{cli-template/scripts → scripts}/remove-nul.js +0 -0
@@ -79,7 +79,6 @@ const customAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult
79
79
  return {
80
80
  success: true,
81
81
  authType: 'basic',
82
- tokenType: 'custom',
83
82
  username: userID || 'unknown',
84
83
  };
85
84
  } else {
@@ -451,7 +450,7 @@ consul:
451
450
  description: <description> # <description> will be replaced by <package.json>.description at initialization
452
451
  tags: [] # If null or empty array - Will be pulled up from package.keywords at initialization
453
452
  meta:
454
- # "About" page link template
453
+ # "Home" page link template
455
454
  who: 'http://{address}:{port}/'
456
455
  envCode: # Used to generate the service ID
457
456
  prod: {{consul.envCode.prod}} # Production environment code
@@ -497,7 +496,7 @@ swagger:
497
496
  description: "PROD server"
498
497
 
499
498
  uiColor:
500
- # Font color of the header and a number of interface elements on the ABOUT page
499
+ # Font color of the header and a number of interface elements on the HOME page
501
500
  primary: '#0f65dc'
502
501
 
503
502
  webServer:
@@ -870,13 +869,12 @@ addErrorMessage(originalError, 'Database operation failed');
870
869
  ```typescript
871
870
  import {
872
871
  ICheckTokenResult,
873
- checkToken,
872
+ checkJwtToken,
874
873
  generateToken
875
874
  } from 'fa-mcp-sdk';
876
875
 
877
876
  // Types used:
878
877
  export interface ICheckTokenResult {
879
- inTokenType?: TTokenType // 'permanent' | 'JWT'
880
878
  payload?: ITokenPayload, // Token payload with user data
881
879
  errorReason?: string, // Error message if validation failed
882
880
  isTokenDecrypted?: boolean, // Whether token was successfully decrypted
@@ -888,16 +886,16 @@ export interface ITokenPayload {
888
886
  [key: string]: any, // Additional payload data
889
887
  }
890
888
 
891
- // checkToken - validate token and return detailed result
889
+ // checkJwtToken - validate token and return detailed result
892
890
  // Function Signature:
893
- const checkToken = (arg: {
891
+ const checkJwtToken = (arg: {
894
892
  token: string,
895
893
  expectedUser?: string,
896
894
  expectedService?: string,
897
895
  }): ICheckTokenResult {...}
898
896
 
899
897
  // Example:
900
- const tokenResult = checkToken({
898
+ const tokenResult = checkJwtToken({
901
899
  token: 'user_provided_token',
902
900
  expectedUser: 'john_doe',
903
901
  expectedService: 'my-mcp-server'
@@ -966,6 +964,193 @@ await generateTokenApp(); // Uses default configuration from appConfig
966
964
  // domainController: 'dc.domain.com'
967
965
  ```
968
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
+
969
1154
  #### Multi-Authentication System
970
1155
 
971
1156
  The FA-MCP-SDK supports a comprehensive multi-authentication system that allows multiple authentication methods to work together with CPU-optimized performance ordering.
@@ -979,7 +1164,6 @@ import {
979
1164
  AuthDetectionResult,
980
1165
  CustomAuthValidator,
981
1166
  checkMultiAuth,
982
- checkCombinedAuth,
983
1167
  detectAuthConfiguration,
984
1168
  logAuthConfiguration,
985
1169
  createAuthMW, // Universal authentication middleware
@@ -987,19 +1171,19 @@ import {
987
1171
  } from 'fa-mcp-sdk';
988
1172
 
989
1173
  // Authentication types in CPU priority order (low to high cost)
990
- export type AuthType = 'permanentServerTokens' | 'basic' | 'jwtToken';
1174
+ export type AuthType = 'permanentServerTokens' | 'jwtToken' | 'basic' | 'custom';
991
1175
 
992
1176
  // Custom Authentication validator function (black box - receives full request)
993
1177
  export type CustomAuthValidator = (req: any) => Promise<AuthResult> | AuthResult;
994
1178
 
995
1179
  // Authentication result interface
996
1180
  export interface AuthResult {
997
- success: boolean;
998
- error?: string;
999
- authType?: AuthType;
1000
- tokenType?: string;
1001
- username?: string;
1002
- payload?: any;
1181
+ success: boolean;
1182
+ error?: string;
1183
+ authType?: AuthType;
1184
+ username?: string;
1185
+ isTokenDecrypted?: boolean; // only for JWT
1186
+ payload?: any;
1003
1187
  }
1004
1188
 
1005
1189
  // Authentication detection result
@@ -1026,24 +1210,6 @@ if (result.success) {
1026
1210
  console.log('Authentication failed:', result.error);
1027
1211
  }
1028
1212
 
1029
- // checkCombinedAuth - validate using configured auth + custom validator
1030
- // Function Signature:
1031
- async function checkCombinedAuth( req: any ): Promise<AuthResult> {...}
1032
-
1033
- // This is the enhanced function that:
1034
- // 1. Runs standard MCP auth methods (if configured)
1035
- // 2. Additionally runs custom validator (if configured)
1036
- // 3. Can use custom validator as fallback if standard auth fails
1037
-
1038
- // Example:
1039
- const authResult = await checkCombinedAuth(req);
1040
-
1041
- if (authResult.success) {
1042
- console.log(`Authentication successful via ${authResult.authType}`);
1043
- } else {
1044
- console.log('Combined authentication failed:', authResult.error);
1045
- }
1046
-
1047
1213
  // detectAuthConfiguration - analyze auth configuration
1048
1214
  // Function Signature:
1049
1215
  function detectAuthConfiguration(): AuthDetectionResult {...}
@@ -1088,7 +1254,6 @@ app.get('/api/protected', (req, res) => {
1088
1254
  message: 'Access granted',
1089
1255
  authType: authInfo?.authType,
1090
1256
  username: authInfo?.username,
1091
- tokenType: authInfo?.tokenType
1092
1257
  });
1093
1258
  });
1094
1259
 
@@ -1119,7 +1284,7 @@ function createAuthMW(options?: {
1119
1284
  async function getMultiAuthError(req: Request): Promise<{ code: number, message: string } | undefined>
1120
1285
 
1121
1286
  // Returns error object if authentication failed, undefined if successful
1122
- // Uses checkCombinedAuth internally - supports all authentication methods
1287
+ // Uses checkMultiAuth internally - supports all authentication methods
1123
1288
 
1124
1289
  // Example - Custom middleware with different auth levels
1125
1290
  app.use('/api/custom', async (req, res, next) => {
@@ -1174,7 +1339,6 @@ const databaseAuthValidator: CustomAuthValidator = async (req): Promise<AuthResu
1174
1339
  return {
1175
1340
  success: true,
1176
1341
  authType: 'basic',
1177
- tokenType: 'basic',
1178
1342
  username: dbUser.username,
1179
1343
  payload: { userId: dbUser.id, roles: dbUser.roles }
1180
1344
  };
@@ -1187,7 +1351,6 @@ const databaseAuthValidator: CustomAuthValidator = async (req): Promise<AuthResu
1187
1351
  return {
1188
1352
  success: true,
1189
1353
  authType: 'basic',
1190
- tokenType: 'apiKey',
1191
1354
  username: username,
1192
1355
  payload: { apiKey: apiKey.substring(0, 8) + '...' }
1193
1356
  };
@@ -1226,7 +1389,6 @@ const ipBasedAuthValidator: CustomAuthValidator = async (req): Promise<AuthResul
1226
1389
  return {
1227
1390
  success: true,
1228
1391
  authType: 'basic',
1229
- tokenType: 'ipBased',
1230
1392
  username: `ip-${clientIP}`,
1231
1393
  payload: { clientIP, userAgent, accessTime: new Date().toISOString() }
1232
1394
  };
@@ -1261,7 +1423,6 @@ const externalServiceAuthValidator: CustomAuthValidator = async (req): Promise<A
1261
1423
  return {
1262
1424
  success: true,
1263
1425
  authType: 'basic',
1264
- tokenType: 'external',
1265
1426
  username: result.username || clientId,
1266
1427
  payload: {
1267
1428
  clientId,
@@ -1303,7 +1464,6 @@ const mfaAuthValidator: CustomAuthValidator = async (req): Promise<AuthResult> =
1303
1464
  return {
1304
1465
  success: true,
1305
1466
  authType: 'basic',
1306
- tokenType: 'mfa',
1307
1467
  username: username,
1308
1468
  payload: {
1309
1469
  userId: user.id,
@@ -1351,7 +1511,6 @@ app.post('/test-token', async (req, res) => {
1351
1511
  res.json({
1352
1512
  valid: result.success,
1353
1513
  authType: result.authType,
1354
- tokenType: result.tokenType,
1355
1514
  error: result.error,
1356
1515
  username: result.username,
1357
1516
  hasPayload: !!result.payload
@@ -1412,6 +1571,504 @@ curl -H "Authorization: Bearer token123" \
1412
1571
 
1413
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.
1414
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
+
1415
2072
  ### Utility Functions
1416
2073
 
1417
2074
  #### General Utilities