fa-mcp-sdk 0.2.174 → 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/cli-template/{fa-mcp-sdk-spec.md → FA-MCP-SDK.md} +577 -1
- package/cli-template/package.json +2 -2
- package/config/_local.yaml +1 -1
- package/config/default.yaml +1 -1
- package/config/local.yaml +10 -2
- package/dist/core/_types_/types.d.ts +35 -0
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/web/admin-router.d.ts.map +1 -1
- package/dist/core/web/admin-router.js +112 -30
- package/dist/core/web/admin-router.js.map +1 -1
- package/dist/core/web/home-api.d.ts.map +1 -1
- package/dist/core/web/home-api.js +13 -0
- package/dist/core/web/home-api.js.map +1 -1
- package/dist/core/web/static/home/index.html +12 -0
- package/dist/core/web/static/home/script.js +7 -0
- package/dist/core/web/static/token-gen/index.html +64 -41
- package/dist/core/web/static/token-gen/script.js +195 -7
- package/package.json +1 -1
- package/src/template/_examples/multi-auth-examples.ts +0 -508
|
@@ -1044,6 +1044,113 @@ describe('MCP Server Tests', () => {
|
|
|
1044
1044
|
});
|
|
1045
1045
|
```
|
|
1046
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
|
+
|
|
1047
1154
|
#### Multi-Authentication System
|
|
1048
1155
|
|
|
1049
1156
|
The FA-MCP-SDK supports a comprehensive multi-authentication system that allows multiple authentication methods to work together with CPU-optimized performance ordering.
|
|
@@ -1064,7 +1171,7 @@ import {
|
|
|
1064
1171
|
} from 'fa-mcp-sdk';
|
|
1065
1172
|
|
|
1066
1173
|
// Authentication types in CPU priority order (low to high cost)
|
|
1067
|
-
export type AuthType = 'permanentServerTokens' | 'basic' | '
|
|
1174
|
+
export type AuthType = 'permanentServerTokens' | 'jwtToken' | 'basic' | 'custom';
|
|
1068
1175
|
|
|
1069
1176
|
// Custom Authentication validator function (black box - receives full request)
|
|
1070
1177
|
export type CustomAuthValidator = (req: any) => Promise<AuthResult> | AuthResult;
|
|
@@ -1492,6 +1599,475 @@ const isDeveloper = await isUserInGroup('john.doe', 'Developers');
|
|
|
1492
1599
|
groupChecker.clearCache(); // Clear cache if needed
|
|
1493
1600
|
```
|
|
1494
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
|
+
|
|
1495
2071
|
|
|
1496
2072
|
### Utility Functions
|
|
1497
2073
|
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"clean": "rimraf dist",
|
|
15
15
|
"cb": "npm run clean && npm run build",
|
|
16
|
-
"ci": "node --no-deprecation
|
|
17
|
-
"reinstall": "node --no-deprecation
|
|
16
|
+
"ci": "node --no-deprecation ./scripts/npm/run.js",
|
|
17
|
+
"reinstall": "node --no-deprecation ./scripts/npm/run.js reinstall",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
19
|
"lint": "eslint .",
|
|
20
20
|
"lint:fix": "eslint --fix .",
|
package/config/_local.yaml
CHANGED
|
@@ -121,7 +121,7 @@ webServer:
|
|
|
121
121
|
# Supports 4 authentication methods: permanentServerTokens, basic, jwtToken, ntlm
|
|
122
122
|
# ========================================================================
|
|
123
123
|
adminAuth:
|
|
124
|
-
enabled:
|
|
124
|
+
enabled: true # Enable/disable admin panel
|
|
125
125
|
# Authentication type for admin panel: 'permanentServerTokens' | 'basic' | 'jwtToken' | 'ntlm'
|
|
126
126
|
# For permanentServerTokens, basic, jwtToken - uses credentials from webServer.auth section
|
|
127
127
|
# For ntlm - uses AD configuration from ad.domains section (no additional credentials needed)
|
package/config/default.yaml
CHANGED
|
@@ -184,7 +184,7 @@ webServer:
|
|
|
184
184
|
# Supports 4 authentication methods: permanentServerTokens, basic, jwtToken, ntlm
|
|
185
185
|
# ========================================================================
|
|
186
186
|
adminAuth:
|
|
187
|
-
enabled:
|
|
187
|
+
enabled: true # Enable/disable admin panel
|
|
188
188
|
# Authentication type for admin panel: 'permanentServerTokens' | 'basic' | 'jwtToken' | 'ntlm'
|
|
189
189
|
# For permanentServerTokens, basic, jwtToken - uses credentials from webServer.auth section
|
|
190
190
|
# For ntlm - uses AD configuration from ad.domains section (no additional credentials needed)
|
package/config/local.yaml
CHANGED
|
@@ -66,7 +66,7 @@ webServer:
|
|
|
66
66
|
auth:
|
|
67
67
|
enabled: true
|
|
68
68
|
# An array of fixed tokens that pass to the MCP (use only for MCPs with green data or for development)
|
|
69
|
-
|
|
69
|
+
permanentServerTokens: ['test-perm-token']
|
|
70
70
|
jwtToken:
|
|
71
71
|
# Symmetric encryption key to generate a token for this MCP
|
|
72
72
|
encryptKey: '66666666-7777-8888-9999-000000000000'
|
|
@@ -76,6 +76,14 @@ webServer:
|
|
|
76
76
|
username: vpupkin
|
|
77
77
|
password: '1'
|
|
78
78
|
|
|
79
|
+
# ========================================================================
|
|
80
|
+
# ADMIN PANEL AUTHENTICATION
|
|
81
|
+
# Token generation page available at /admin endpoint
|
|
82
|
+
# Supports 4 authentication methods: permanentServerTokens, basic, jwtToken, ntlm
|
|
83
|
+
# ========================================================================
|
|
79
84
|
adminAuth:
|
|
80
85
|
enabled: true
|
|
81
|
-
type: 'ntlm'
|
|
86
|
+
# Authentication type for admin panel: 'permanentServerTokens' | 'basic' | 'jwtToken' | 'ntlm'
|
|
87
|
+
# For permanentServerTokens, basic, jwtToken - uses credentials from webServer.auth section
|
|
88
|
+
# For ntlm - uses AD configuration from ad.domains section (no additional credentials needed)
|
|
89
|
+
type: 'jwtToken'
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import { AuthResult } from '../auth/types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Input data for Token Generator authorization handler
|
|
6
|
+
* Contains user information based on the admin auth type
|
|
7
|
+
*/
|
|
8
|
+
export interface TokenGenAuthInput {
|
|
9
|
+
/** Username (from JWT payload, basic auth, or NTLM) */
|
|
10
|
+
user: string;
|
|
11
|
+
/** Domain name (only for NTLM authentication) */
|
|
12
|
+
domain?: string;
|
|
13
|
+
/** Full JWT payload (only for jwtToken authentication) */
|
|
14
|
+
payload?: Record<string, any>;
|
|
15
|
+
/** The authentication type used */
|
|
16
|
+
authType: 'jwtToken' | 'basic' | 'ntlm' | 'permanentServerTokens';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Custom authorization handler for Token Generator admin page
|
|
20
|
+
* Called after standard authentication to perform additional authorization checks
|
|
21
|
+
*
|
|
22
|
+
* @param input - User information from the authentication layer
|
|
23
|
+
* @returns AuthResult indicating whether user is authorized to access Token Generator
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Only allow users from specific AD groups
|
|
27
|
+
* const tokenGenAuthHandler: TokenGenAuthHandler = async (input) => {
|
|
28
|
+
* if (input.authType === 'ntlm') {
|
|
29
|
+
* const isAdmin = await isUserInGroup(input.user, 'TokenGeneratorAdmins');
|
|
30
|
+
* if (!isAdmin) {
|
|
31
|
+
* return { success: false, error: 'User is not in TokenGeneratorAdmins group' };
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* return { success: true, username: input.user };
|
|
35
|
+
* };
|
|
36
|
+
*/
|
|
37
|
+
export type TokenGenAuthHandler = (input: TokenGenAuthInput) => Promise<AuthResult> | AuthResult;
|
|
4
38
|
export interface IPromptData {
|
|
5
39
|
name: string;
|
|
6
40
|
description: string;
|
|
@@ -66,6 +100,7 @@ export interface McpServerData {
|
|
|
66
100
|
requiredHttpHeaders?: IRequiredHttpHeader[] | null;
|
|
67
101
|
customResources?: IResourceData[] | null;
|
|
68
102
|
customAuthValidator?: CustomAuthValidator;
|
|
103
|
+
tokenGenAuthHandler?: TokenGenAuthHandler;
|
|
69
104
|
httpComponents?: {
|
|
70
105
|
apiRouter?: Router | null;
|
|
71
106
|
endpointsOn404?: IEndpointsOn404;
|