chenhunghan commited on
Commit
0ef4703
·
unverified ·
1 Parent(s): f34adb9

feat: add getUsers/getOneUsers tool

Browse files

Signed-off-by: Hung-Han (Henry) Chen <chenhungh@gmail.com>

Files changed (2) hide show
  1. src/tools/getOneUser.ts +114 -0
  2. src/tools/getUsers.ts +157 -0
src/tools/getOneUser.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type InferSchema, type ToolMetadata } from "xmcp";
2
+ import { headers } from "xmcp/headers";
3
+ import { z } from "zod";
4
+ import { getScimBaseUrl } from "../utils/getSCIMBaseUrl";
5
+ import { getScimToken } from "../utils/getSCIMToken";
6
+ import { maskPII, PII_FIELDS } from "../utils/piiMasking";
7
+ import { readJsonBody } from "../utils/responseBody";
8
+
9
+ export const metadata: ToolMetadata = {
10
+ name: "get-one-user",
11
+ description: "Retrieve a single SCIM user resource by ID",
12
+ annotations: {
13
+ title: "Get One User Resource",
14
+ readOnlyHint: true,
15
+ destructiveHint: false,
16
+ idempotentHint: true,
17
+ openWorldHint: true,
18
+ },
19
+ };
20
+
21
+ export const schema = {
22
+ userId: z.string().describe("The ID of the user"),
23
+ attributes: z
24
+ .string()
25
+ .optional()
26
+ .describe(
27
+ "Comma-separated list of attribute names to return. Per RFC 7644 Section 3.9, only specified attributes will be returned (e.g., 'userName,emails')"
28
+ ),
29
+ excludedAttributes: z
30
+ .string()
31
+ .optional()
32
+ .describe(
33
+ "Comma-separated list of attribute names to exclude from the response. Per RFC 7644 Section 3.9"
34
+ ),
35
+ piiMasking: z
36
+ .boolean()
37
+ .optional()
38
+ .default(true)
39
+ .describe(
40
+ "Enable PII masking for sensitive fields (username, emails, phone numbers, addresses). When true, values are partially masked while maintaining readability. Default: true"
41
+ ),
42
+ };
43
+
44
+ export default async function getOneUser(params: InferSchema<typeof schema>) {
45
+ const {
46
+ userId,
47
+ attributes,
48
+ excludedAttributes,
49
+ piiMasking = true,
50
+ } = params;
51
+
52
+ const requestHeaders = headers();
53
+ const apiToken = getScimToken(requestHeaders);
54
+ const baseUrl = getScimBaseUrl(requestHeaders);
55
+
56
+ if (!apiToken) {
57
+ throw new Error(
58
+ "Missing required headers: x-scim-api-token or SCIM_API_TOKEN env"
59
+ );
60
+ }
61
+
62
+ if (!baseUrl) {
63
+ throw new Error(
64
+ "Missing required headers: x-scim-base-url or SCIM_API_BASE_URL env"
65
+ );
66
+ }
67
+
68
+ const url = new URL(`${baseUrl}/Users/${userId}`);
69
+
70
+ if (attributes) {
71
+ url.searchParams.append("attributes", attributes);
72
+ }
73
+
74
+ if (excludedAttributes) {
75
+ url.searchParams.append("excludedAttributes", excludedAttributes);
76
+ }
77
+
78
+ const response = await fetch(url, {
79
+ method: "GET",
80
+ headers: {
81
+ "Content-Type": "application/scim+json",
82
+ Authorization: `Bearer ${apiToken}`,
83
+ },
84
+ });
85
+
86
+ if (!response.ok) {
87
+ const errorBody = await readJsonBody(response);
88
+ throw new Error(
89
+ `Failed to get user: ${response.status} ${response.statusText}. ${JSON.stringify(errorBody)}`
90
+ );
91
+ }
92
+
93
+ let data = await response.json();
94
+
95
+ // Apply PII masking if enabled
96
+ if (piiMasking) {
97
+ data = maskPII(data, PII_FIELDS);
98
+ }
99
+
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: `Get one user successfully`,
105
+ },
106
+ {
107
+ type: "resource_link",
108
+ name: "User resource",
109
+ uri: `users://${userId}`,
110
+ },
111
+ ],
112
+ structuredContent: data ?? undefined,
113
+ };
114
+ }
src/tools/getUsers.ts ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type InferSchema, type ToolMetadata } from "xmcp";
2
+ import { headers } from "xmcp/headers";
3
+ import { z } from "zod";
4
+ import { getScimBaseUrl } from "../utils/getSCIMBaseUrl";
5
+ import { getScimToken } from "../utils/getSCIMToken";
6
+ import { maskPII, PII_FIELDS } from "../utils/piiMasking";
7
+ import { readJsonBody } from "../utils/responseBody";
8
+
9
+ export const metadata: ToolMetadata = {
10
+ name: "get-users",
11
+ description: "List/search SCIM user resources with optional filtering and pagination",
12
+ annotations: {
13
+ title: "Get Users",
14
+ readOnlyHint: true,
15
+ destructiveHint: false,
16
+ idempotentHint: true,
17
+ openWorldHint: true,
18
+ },
19
+ };
20
+
21
+ export const schema = {
22
+ filter: z
23
+ .string()
24
+ .optional()
25
+ .describe(
26
+ "SCIM filter expression per RFC 7644 Section 3.4.2.2. Examples: 'userName eq \"bjensen\"', 'emails.value co \"example.com\"'"
27
+ ),
28
+ startIndex: z
29
+ .number()
30
+ .optional()
31
+ .describe(
32
+ "The 1-based index of the first query result. A value less than 1 SHALL be interpreted as 1."
33
+ ),
34
+ count: z
35
+ .number()
36
+ .optional()
37
+ .describe(
38
+ "Non-negative integer. Specifies the desired maximum number of query results per page."
39
+ ),
40
+ sortBy: z
41
+ .string()
42
+ .optional()
43
+ .describe(
44
+ "Attribute path to sort results by (e.g., 'userName', 'meta.created')"
45
+ ),
46
+ sortOrder: z
47
+ .enum(["ascending", "descending"])
48
+ .optional()
49
+ .describe("Sort order for results. Default: ascending"),
50
+ attributes: z
51
+ .string()
52
+ .optional()
53
+ .describe(
54
+ "Comma-separated list of attribute names to return (e.g., 'userName,emails')"
55
+ ),
56
+ excludedAttributes: z
57
+ .string()
58
+ .optional()
59
+ .describe("Comma-separated list of attribute names to exclude"),
60
+ piiMasking: z
61
+ .boolean()
62
+ .optional()
63
+ .default(true)
64
+ .describe(
65
+ "Enable PII masking for sensitive fields (username, emails, phone numbers, addresses). When true, values are partially masked while maintaining readability. Default: true"
66
+ ),
67
+ };
68
+
69
+ export default async function getUsers(params: InferSchema<typeof schema>) {
70
+ const {
71
+ filter,
72
+ startIndex,
73
+ count,
74
+ sortBy,
75
+ sortOrder,
76
+ attributes,
77
+ excludedAttributes,
78
+ piiMasking = true,
79
+ } = params;
80
+
81
+ const requestHeaders = headers();
82
+ const apiToken = getScimToken(requestHeaders);
83
+ const baseUrl = getScimBaseUrl(requestHeaders);
84
+
85
+ if (!apiToken) {
86
+ throw new Error(
87
+ "Missing required headers: x-scim-api-token or SCIM_API_TOKEN env"
88
+ );
89
+ }
90
+
91
+ if (!baseUrl) {
92
+ throw new Error(
93
+ "Missing required headers: x-scim-base-url or SCIM_API_BASE_URL env"
94
+ );
95
+ }
96
+
97
+ const url = new URL(`${baseUrl}/Users`);
98
+
99
+ if (filter) {
100
+ url.searchParams.append("filter", filter);
101
+ }
102
+ if (startIndex !== undefined) {
103
+ url.searchParams.append("startIndex", startIndex.toString());
104
+ }
105
+ if (count !== undefined) {
106
+ url.searchParams.append("count", count.toString());
107
+ }
108
+ if (sortBy) {
109
+ url.searchParams.append("sortBy", sortBy);
110
+ }
111
+ if (sortOrder) {
112
+ url.searchParams.append("sortOrder", sortOrder);
113
+ }
114
+ if (attributes) {
115
+ url.searchParams.append("attributes", attributes);
116
+ }
117
+ if (excludedAttributes) {
118
+ url.searchParams.append("excludedAttributes", excludedAttributes);
119
+ }
120
+
121
+ const response = await fetch(url, {
122
+ method: "GET",
123
+ headers: {
124
+ "Content-Type": "application/scim+json",
125
+ Authorization: `Bearer ${apiToken}`,
126
+ },
127
+ });
128
+
129
+ if (!response.ok) {
130
+ const errorBody = await readJsonBody(response);
131
+ throw new Error(
132
+ `Failed to get users: ${response.status} ${response.statusText}. ${JSON.stringify(errorBody)}`
133
+ );
134
+ }
135
+
136
+ let data = await response.json();
137
+
138
+ // Apply PII masking if enabled
139
+ if (piiMasking) {
140
+ data = maskPII(data, PII_FIELDS);
141
+ }
142
+
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: `Get users successfully`,
148
+ },
149
+ {
150
+ type: "resource_link",
151
+ name: "User resources",
152
+ uri: "users://",
153
+ },
154
+ ],
155
+ structuredContent: data ?? undefined,
156
+ };
157
+ }