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.
- package/README.md +1 -1
- package/bin/fa-mcp.js +66 -54
- package/cli-template/.env.example +2 -2
- package/cli-template/{fa-mcp-sdk-spec.md → FA-MCP-SDK.md} +699 -42
- package/cli-template/README.md +2 -2
- package/cli-template/package.json +1 -1
- package/cli-template/r/TEST HTTP.xml +9 -0
- package/cli-template/{run/TEST SSE.run.xml → r/TEST SSE.xml } +2 -2
- package/cli-template/{run/TEST STDIO.run.xml → r/TEST STDIO.xml } +2 -2
- package/cli-template/r/generate-token.xml +14 -0
- package/cli-template/{run/kill-server.run.xml → r/kill-server.xml} +2 -2
- package/cli-template/{run/kill-token-gen-server.xml → r/remove-nul.xml} +4 -5
- package/{cli-template/config → config}/_local.yaml +28 -14
- package/{cli-template/config → config}/custom-environment-variables.yaml +3 -0
- package/{cli-template/config → config}/default.yaml +50 -10
- package/{cli-template/config → config}/development.yaml +4 -4
- package/config/local.yaml +89 -0
- package/{cli-template/config → config}/production.yaml +4 -4
- package/dist/core/_types_/active-directory-config.d.ts +3 -0
- package/dist/core/_types_/active-directory-config.d.ts.map +1 -1
- package/dist/core/_types_/config.d.ts +5 -1
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/_types_/types.d.ts +40 -1
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/ad/group-checker.d.ts +13 -0
- package/dist/core/ad/group-checker.d.ts.map +1 -0
- package/dist/core/ad/group-checker.js +86 -0
- package/dist/core/ad/group-checker.js.map +1 -0
- package/dist/core/auth/admin-auth.d.ts +16 -0
- package/dist/core/auth/admin-auth.d.ts.map +1 -0
- package/dist/core/auth/admin-auth.js +159 -0
- package/dist/core/auth/admin-auth.js.map +1 -0
- package/dist/core/auth/basic.d.ts +6 -0
- package/dist/core/auth/basic.d.ts.map +1 -0
- package/dist/core/auth/basic.js +26 -0
- package/dist/core/auth/basic.js.map +1 -0
- package/dist/core/auth/{jwt-validation.d.ts → jwt.d.ts} +4 -3
- package/dist/core/auth/jwt.d.ts.map +1 -0
- package/dist/core/auth/{jwt-validation.js → jwt.js} +9 -19
- package/dist/core/auth/jwt.js.map +1 -0
- package/dist/core/auth/middleware.d.ts.map +1 -1
- package/dist/core/auth/middleware.js +3 -3
- package/dist/core/auth/middleware.js.map +1 -1
- package/dist/core/auth/multi-auth.d.ts +14 -6
- package/dist/core/auth/multi-auth.d.ts.map +1 -1
- package/dist/core/auth/multi-auth.js +151 -141
- package/dist/core/auth/multi-auth.js.map +1 -1
- package/dist/core/auth/permanent.d.ts +6 -0
- package/dist/core/auth/permanent.d.ts.map +1 -0
- package/dist/core/auth/permanent.js +15 -0
- package/dist/core/auth/permanent.js.map +1 -0
- package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js +8 -10
- package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-integration.d.ts.map +1 -1
- package/dist/core/auth/token-generator/ntlm/ntlm-integration.js +9 -2
- package/dist/core/auth/token-generator/ntlm/ntlm-integration.js.map +1 -1
- package/dist/core/auth/token-generator/server.d.ts.map +1 -1
- package/dist/core/auth/token-generator/server.js +59 -25
- package/dist/core/auth/token-generator/server.js.map +1 -1
- package/dist/core/auth/types.d.ts +4 -3
- package/dist/core/auth/types.d.ts.map +1 -1
- package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
- package/dist/core/bootstrap/startup-info.js +19 -0
- package/dist/core/bootstrap/startup-info.js.map +1 -1
- package/dist/core/consul/access-points-updater.js +1 -1
- package/dist/core/consul/access-points-updater.js.map +1 -1
- package/dist/core/consul/get-consul-api.d.ts +1 -1
- package/dist/core/consul/get-consul-api.d.ts.map +1 -1
- package/dist/core/consul/get-consul-api.js +1 -1
- package/dist/core/consul/get-consul-api.js.map +1 -1
- package/dist/core/consul/register.d.ts +1 -1
- package/dist/core/consul/register.d.ts.map +1 -1
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/init-mcp-server.d.ts.map +1 -1
- package/dist/core/init-mcp-server.js +1 -1
- package/dist/core/init-mcp-server.js.map +1 -1
- package/dist/core/utils/testing/McpSseClient.js.map +1 -1
- package/dist/core/web/admin-router.d.ts +10 -0
- package/dist/core/web/admin-router.d.ts.map +1 -0
- package/dist/core/web/admin-router.js +309 -0
- package/dist/core/web/admin-router.js.map +1 -0
- package/dist/core/web/favicon-svg.d.ts +1 -1
- package/dist/core/web/favicon-svg.d.ts.map +1 -1
- package/dist/core/web/favicon-svg.js +21 -3
- package/dist/core/web/favicon-svg.js.map +1 -1
- package/dist/core/web/home-api.d.ts +7 -0
- package/dist/core/web/home-api.d.ts.map +1 -0
- package/dist/core/web/home-api.js +106 -0
- package/dist/core/web/home-api.js.map +1 -0
- package/dist/core/web/server-http.d.ts +1 -0
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +60 -25
- package/dist/core/web/server-http.js.map +1 -1
- package/dist/core/web/static/home/index.html +218 -0
- package/dist/core/web/static/home/script.js +643 -0
- package/dist/core/web/{about-page/css.js → static/styles.css} +435 -105
- package/dist/core/web/static/token-gen/index.html +105 -0
- package/dist/core/web/static/token-gen/jwt-icon.svg +3 -0
- package/dist/core/web/static/token-gen/logout.svg +4 -0
- package/dist/core/web/static/token-gen/script.js +553 -0
- package/dist/core/web/static/token-gen/user.svg +4 -0
- package/dist/core/web/svg-icons.d.ts +7 -0
- package/dist/core/web/svg-icons.d.ts.map +1 -0
- package/dist/core/web/svg-icons.js +78 -0
- package/dist/core/web/svg-icons.js.map +1 -0
- package/package.json +7 -3
- package/scripts/copy-static.js +31 -0
- package/src/template/_types_/custom-config.ts +83 -0
- package/src/template/asset/logo.svg +4 -0
- package/src/template/start.ts +3 -3
- package/src/template/tools/handle-tool-call.ts +2 -1
- package/src/tests/mcp/test-http.js +10 -2
- package/src/tests/mcp/test-sse.js +10 -2
- package/src/tests/mcp/test-stdio.js +1 -2
- package/cli-template/run/TEST HTTP.run.xml +0 -5
- package/cli-template/run/TEST search.run.xml +0 -11
- package/cli-template/run/remove-nul.js.run.xml +0 -5
- package/dist/core/auth/jwt-validation.d.ts.map +0 -1
- package/dist/core/auth/jwt-validation.js.map +0 -1
- package/dist/core/auth/token-generator/html.d.ts +0 -9
- package/dist/core/auth/token-generator/html.d.ts.map +0 -1
- package/dist/core/auth/token-generator/html.js +0 -862
- package/dist/core/auth/token-generator/html.js.map +0 -1
- package/dist/core/web/about-page/css.d.ts +0 -2
- package/dist/core/web/about-page/css.d.ts.map +0 -1
- package/dist/core/web/about-page/css.js.map +0 -1
- package/dist/core/web/about-page/render.d.ts +0 -2
- package/dist/core/web/about-page/render.d.ts.map +0 -1
- package/dist/core/web/about-page/render.js +0 -773
- package/dist/core/web/about-page/render.js.map +0 -1
- package/src/template/_examples/multi-auth-examples.ts +0 -541
- /package/cli-template/{run/== START ==.run.xml → r/== START ==.xml} +0 -0
- /package/cli-template/{run/cb.run.xml → r/cb.xml} +0 -0
- /package/cli-template/{run/ci.run.xml → r/ci.xml} +0 -0
- /package/cli-template/{run/lint.run.xml → r/lint.xml} +0 -0
- /package/cli-template/{run/lint_fix.run.xml → r/lint_fix.xml} +0 -0
- /package/cli-template/{run/reinstall.run.xml → r/reinstall.xml} +0 -0
- /package/{cli-template/config → config}/test.yaml +0 -0
- /package/{src/template/asset/favicon.svg → dist/core/web/static/logo.svg} +0 -0
- /package/{cli-template/scripts → scripts}/kill-port.js +0 -0
- /package/{cli-template/scripts → scripts}/npm/patch_node_modules.js +0 -0
- /package/{cli-template/scripts → scripts}/npm/run.js +0 -0
- /package/{cli-template/scripts → scripts}/npm/yarn-ci.ps1 +0 -0
- /package/{cli-template/scripts → scripts}/npm/yarn-ci.sh +0 -0
- /package/{cli-template/scripts → scripts}/npm/yarn-reinstall.ps1 +0 -0
- /package/{cli-template/scripts → scripts}/npm/yarn-reinstall.sh +0 -0
- /package/{cli-template/scripts → scripts}/pre-commit +0 -0
- /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
|
-
# "
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
889
|
+
// checkJwtToken - validate token and return detailed result
|
|
892
890
|
// Function Signature:
|
|
893
|
-
const
|
|
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 =
|
|
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' | '
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
|
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
|