@t4dhg/mcp-factorial 1.1.0 → 2.0.0
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/LICENSE +21 -0
- package/README.md +123 -30
- package/dist/api.d.ts +104 -8
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +258 -100
- package/dist/api.js.map +1 -1
- package/dist/cache.d.ts +88 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +169 -0
- package/dist/cache.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +112 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +100 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +186 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-client.d.ts +43 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +146 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1012 -71
- package/dist/index.js.map +1 -1
- package/dist/pagination.d.ts +96 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +114 -0
- package/dist/pagination.js.map +1 -0
- package/dist/schemas.d.ts +502 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +241 -0
- package/dist/schemas.js.map +1 -0
- package/dist/types.d.ts +61 -50
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/package.json +42 -5
package/dist/index.js
CHANGED
|
@@ -4,68 +4,58 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Provides access to employee and organizational data from FactorialHR
|
|
6
6
|
* through the Model Context Protocol for use with Claude Code and other MCP clients.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - 22 tools for employees, teams, locations, contracts, time off, attendance, documents, and job catalog
|
|
10
|
+
* - Pagination support for all list operations
|
|
11
|
+
* - Caching for improved performance
|
|
12
|
+
* - Retry logic with exponential backoff
|
|
13
|
+
* - Runtime validation with Zod schemas
|
|
7
14
|
*/
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
// Load environment variables from .env file
|
|
12
|
-
// Priority: ENV_FILE_PATH > cwd/.env > home/.env
|
|
13
|
-
function loadEnv() {
|
|
14
|
-
// 1. Check if explicit path provided
|
|
15
|
-
if (process.env.ENV_FILE_PATH && existsSync(process.env.ENV_FILE_PATH)) {
|
|
16
|
-
config({ path: process.env.ENV_FILE_PATH });
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
// 2. Check current working directory
|
|
20
|
-
const cwdEnv = join(process.cwd(), '.env');
|
|
21
|
-
if (existsSync(cwdEnv)) {
|
|
22
|
-
config({ path: cwdEnv });
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
// 3. Check home directory
|
|
26
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
27
|
-
const homeEnv = join(homeDir, '.env');
|
|
28
|
-
if (existsSync(homeEnv)) {
|
|
29
|
-
config({ path: homeEnv });
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
// 4. Check common project locations
|
|
33
|
-
const commonPaths = [
|
|
34
|
-
join(homeDir, 'turborepo', '.env'),
|
|
35
|
-
join(homeDir, 'projects', '.env'),
|
|
36
|
-
];
|
|
37
|
-
for (const envPath of commonPaths) {
|
|
38
|
-
if (existsSync(envPath)) {
|
|
39
|
-
config({ path: envPath });
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// Fall back to default dotenv behavior
|
|
44
|
-
config();
|
|
45
|
-
}
|
|
15
|
+
import { loadEnv } from './config.js';
|
|
16
|
+
// Load environment variables before other imports
|
|
46
17
|
loadEnv();
|
|
47
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
18
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
48
19
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
49
20
|
import * as z from 'zod';
|
|
50
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
// Employees
|
|
23
|
+
listEmployees, getEmployee, searchEmployees,
|
|
24
|
+
// Teams
|
|
25
|
+
listTeams, getTeam,
|
|
26
|
+
// Locations
|
|
27
|
+
listLocations, getLocation,
|
|
28
|
+
// Contracts
|
|
29
|
+
listContracts,
|
|
30
|
+
// Time Off
|
|
31
|
+
listLeaves, getLeave, listLeaveTypes, getLeaveType, listAllowances,
|
|
32
|
+
// Shifts
|
|
33
|
+
listShifts, getShift,
|
|
34
|
+
// Documents
|
|
35
|
+
listFolders, getFolder, listDocuments, getDocument,
|
|
36
|
+
// Job Catalog
|
|
37
|
+
listJobRoles, getJobRole, listJobLevels, } from './api.js';
|
|
38
|
+
import { formatPaginationInfo } from './pagination.js';
|
|
51
39
|
const server = new McpServer({
|
|
52
40
|
name: 'factorial-hr',
|
|
53
|
-
version: '
|
|
41
|
+
version: '2.0.0',
|
|
54
42
|
});
|
|
55
43
|
// ============================================================================
|
|
56
44
|
// Employee Tools
|
|
57
45
|
// ============================================================================
|
|
58
46
|
server.registerTool('list_employees', {
|
|
59
47
|
title: 'List Employees',
|
|
60
|
-
description: 'Get
|
|
48
|
+
description: 'Get employees from FactorialHR. Can filter by team or location. Supports pagination.',
|
|
61
49
|
inputSchema: {
|
|
62
50
|
team_id: z.number().optional().describe('Filter by team ID'),
|
|
63
51
|
location_id: z.number().optional().describe('Filter by location ID'),
|
|
52
|
+
page: z.number().optional().default(1).describe('Page number (default: 1)'),
|
|
53
|
+
limit: z.number().optional().default(100).describe('Items per page (max: 100)'),
|
|
64
54
|
},
|
|
65
|
-
}, async ({ team_id, location_id }) => {
|
|
55
|
+
}, async ({ team_id, location_id, page, limit }) => {
|
|
66
56
|
try {
|
|
67
|
-
const
|
|
68
|
-
const summary =
|
|
57
|
+
const result = await listEmployees({ team_id, location_id, page, limit });
|
|
58
|
+
const summary = result.data.map(e => ({
|
|
69
59
|
id: e.id,
|
|
70
60
|
name: e.full_name,
|
|
71
61
|
email: e.email,
|
|
@@ -75,21 +65,24 @@ server.registerTool('list_employees', {
|
|
|
75
65
|
manager_id: e.manager_id,
|
|
76
66
|
hired_on: e.hired_on,
|
|
77
67
|
terminated_on: e.terminated_on,
|
|
78
|
-
birthday_on: e.birthday_on,
|
|
79
|
-
created_at: e.created_at,
|
|
80
68
|
}));
|
|
81
69
|
return {
|
|
82
70
|
content: [
|
|
83
71
|
{
|
|
84
72
|
type: 'text',
|
|
85
|
-
text: `Found ${
|
|
73
|
+
text: `Found ${result.data.length} employees (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
86
74
|
},
|
|
87
75
|
],
|
|
88
76
|
};
|
|
89
77
|
}
|
|
90
78
|
catch (error) {
|
|
91
79
|
return {
|
|
92
|
-
content: [
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
93
86
|
isError: true,
|
|
94
87
|
};
|
|
95
88
|
}
|
|
@@ -119,7 +112,12 @@ server.registerTool('get_employee', {
|
|
|
119
112
|
}
|
|
120
113
|
catch (error) {
|
|
121
114
|
return {
|
|
122
|
-
content: [
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
123
121
|
isError: true,
|
|
124
122
|
};
|
|
125
123
|
}
|
|
@@ -155,7 +153,12 @@ server.registerTool('search_employees', {
|
|
|
155
153
|
}
|
|
156
154
|
catch (error) {
|
|
157
155
|
return {
|
|
158
|
-
content: [
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: 'text',
|
|
159
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
159
162
|
isError: true,
|
|
160
163
|
};
|
|
161
164
|
}
|
|
@@ -165,12 +168,15 @@ server.registerTool('search_employees', {
|
|
|
165
168
|
// ============================================================================
|
|
166
169
|
server.registerTool('list_teams', {
|
|
167
170
|
title: 'List Teams',
|
|
168
|
-
description: 'Get all teams in the organization.',
|
|
169
|
-
inputSchema: {
|
|
170
|
-
|
|
171
|
+
description: 'Get all teams in the organization. Supports pagination.',
|
|
172
|
+
inputSchema: {
|
|
173
|
+
page: z.number().optional().default(1).describe('Page number'),
|
|
174
|
+
limit: z.number().optional().default(100).describe('Items per page'),
|
|
175
|
+
},
|
|
176
|
+
}, async ({ page, limit }) => {
|
|
171
177
|
try {
|
|
172
|
-
const
|
|
173
|
-
const summary =
|
|
178
|
+
const result = await listTeams({ page, limit });
|
|
179
|
+
const summary = result.data.map(t => ({
|
|
174
180
|
id: t.id,
|
|
175
181
|
name: t.name,
|
|
176
182
|
description: t.description,
|
|
@@ -180,14 +186,19 @@ server.registerTool('list_teams', {
|
|
|
180
186
|
content: [
|
|
181
187
|
{
|
|
182
188
|
type: 'text',
|
|
183
|
-
text: `Found ${
|
|
189
|
+
text: `Found ${result.data.length} teams (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
184
190
|
},
|
|
185
191
|
],
|
|
186
192
|
};
|
|
187
193
|
}
|
|
188
194
|
catch (error) {
|
|
189
195
|
return {
|
|
190
|
-
content: [
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
191
202
|
isError: true,
|
|
192
203
|
};
|
|
193
204
|
}
|
|
@@ -212,7 +223,12 @@ server.registerTool('get_team', {
|
|
|
212
223
|
}
|
|
213
224
|
catch (error) {
|
|
214
225
|
return {
|
|
215
|
-
content: [
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
216
232
|
isError: true,
|
|
217
233
|
};
|
|
218
234
|
}
|
|
@@ -222,12 +238,15 @@ server.registerTool('get_team', {
|
|
|
222
238
|
// ============================================================================
|
|
223
239
|
server.registerTool('list_locations', {
|
|
224
240
|
title: 'List Locations',
|
|
225
|
-
description: 'Get all company locations.',
|
|
226
|
-
inputSchema: {
|
|
227
|
-
|
|
241
|
+
description: 'Get all company locations. Supports pagination.',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
page: z.number().optional().default(1).describe('Page number'),
|
|
244
|
+
limit: z.number().optional().default(100).describe('Items per page'),
|
|
245
|
+
},
|
|
246
|
+
}, async ({ page, limit }) => {
|
|
228
247
|
try {
|
|
229
|
-
const
|
|
230
|
-
const summary =
|
|
248
|
+
const result = await listLocations({ page, limit });
|
|
249
|
+
const summary = result.data.map(l => ({
|
|
231
250
|
id: l.id,
|
|
232
251
|
name: l.name,
|
|
233
252
|
city: l.city,
|
|
@@ -237,14 +256,19 @@ server.registerTool('list_locations', {
|
|
|
237
256
|
content: [
|
|
238
257
|
{
|
|
239
258
|
type: 'text',
|
|
240
|
-
text: `Found ${
|
|
259
|
+
text: `Found ${result.data.length} locations (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
241
260
|
},
|
|
242
261
|
],
|
|
243
262
|
};
|
|
244
263
|
}
|
|
245
264
|
catch (error) {
|
|
246
265
|
return {
|
|
247
|
-
content: [
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: 'text',
|
|
269
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
248
272
|
isError: true,
|
|
249
273
|
};
|
|
250
274
|
}
|
|
@@ -269,7 +293,12 @@ server.registerTool('get_location', {
|
|
|
269
293
|
}
|
|
270
294
|
catch (error) {
|
|
271
295
|
return {
|
|
272
|
-
content: [
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: 'text',
|
|
299
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
273
302
|
isError: true,
|
|
274
303
|
};
|
|
275
304
|
}
|
|
@@ -285,31 +314,943 @@ server.registerTool('get_employee_contracts', {
|
|
|
285
314
|
},
|
|
286
315
|
}, async ({ employee_id }) => {
|
|
287
316
|
try {
|
|
288
|
-
const
|
|
317
|
+
const result = await listContracts(employee_id);
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{
|
|
321
|
+
type: 'text',
|
|
322
|
+
text: `Found ${result.data.length} contracts:\n\n${JSON.stringify(result.data, null, 2)}`,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
return {
|
|
329
|
+
content: [
|
|
330
|
+
{
|
|
331
|
+
type: 'text',
|
|
332
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
isError: true,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// Time Off / Leave Tools
|
|
341
|
+
// ============================================================================
|
|
342
|
+
server.registerTool('list_leaves', {
|
|
343
|
+
title: 'List Leaves',
|
|
344
|
+
description: 'Get time off/leave requests. Filter by employee, status, or date range. Supports pagination.',
|
|
345
|
+
inputSchema: {
|
|
346
|
+
employee_id: z.number().optional().describe('Filter by employee ID'),
|
|
347
|
+
status: z
|
|
348
|
+
.enum(['pending', 'approved', 'declined'])
|
|
349
|
+
.optional()
|
|
350
|
+
.describe('Filter by leave status'),
|
|
351
|
+
start_on_gte: z
|
|
352
|
+
.string()
|
|
353
|
+
.optional()
|
|
354
|
+
.describe('Filter leaves starting on or after this date (YYYY-MM-DD)'),
|
|
355
|
+
start_on_lte: z
|
|
356
|
+
.string()
|
|
357
|
+
.optional()
|
|
358
|
+
.describe('Filter leaves starting on or before this date (YYYY-MM-DD)'),
|
|
359
|
+
page: z.number().optional().default(1).describe('Page number'),
|
|
360
|
+
limit: z.number().optional().default(100).describe('Items per page (max: 100)'),
|
|
361
|
+
},
|
|
362
|
+
}, async ({ employee_id, status, start_on_gte, start_on_lte, page, limit }) => {
|
|
363
|
+
try {
|
|
364
|
+
const result = await listLeaves({
|
|
365
|
+
employee_id,
|
|
366
|
+
status,
|
|
367
|
+
start_on_gte,
|
|
368
|
+
start_on_lte,
|
|
369
|
+
page,
|
|
370
|
+
limit,
|
|
371
|
+
});
|
|
372
|
+
const summary = result.data.map(l => ({
|
|
373
|
+
id: l.id,
|
|
374
|
+
employee_id: l.employee_id,
|
|
375
|
+
leave_type_id: l.leave_type_id,
|
|
376
|
+
start_on: l.start_on,
|
|
377
|
+
finish_on: l.finish_on,
|
|
378
|
+
status: l.status,
|
|
379
|
+
days: l.duration_attributes?.days,
|
|
380
|
+
}));
|
|
381
|
+
return {
|
|
382
|
+
content: [
|
|
383
|
+
{
|
|
384
|
+
type: 'text',
|
|
385
|
+
text: `Found ${result.data.length} leaves (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: 'text',
|
|
395
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
isError: true,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
server.registerTool('get_leave', {
|
|
403
|
+
title: 'Get Leave Details',
|
|
404
|
+
description: 'Get detailed information about a specific leave request.',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
id: z.number().describe('The leave ID'),
|
|
407
|
+
},
|
|
408
|
+
}, async ({ id }) => {
|
|
409
|
+
try {
|
|
410
|
+
const leave = await getLeave(id);
|
|
411
|
+
return {
|
|
412
|
+
content: [
|
|
413
|
+
{
|
|
414
|
+
type: 'text',
|
|
415
|
+
text: `Leave details:\n\n${JSON.stringify(leave, null, 2)}`,
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: 'text',
|
|
425
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
isError: true,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
server.registerTool('list_leave_types', {
|
|
433
|
+
title: 'List Leave Types',
|
|
434
|
+
description: 'Get all leave types (vacation, sick leave, etc.) configured in the organization.',
|
|
435
|
+
inputSchema: {},
|
|
436
|
+
}, async () => {
|
|
437
|
+
try {
|
|
438
|
+
const types = await listLeaveTypes();
|
|
439
|
+
return {
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
type: 'text',
|
|
443
|
+
text: `Found ${types.length} leave types:\n\n${JSON.stringify(types, null, 2)}`,
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: 'text',
|
|
453
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
isError: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
server.registerTool('get_leave_type', {
|
|
461
|
+
title: 'Get Leave Type',
|
|
462
|
+
description: 'Get details about a specific leave type.',
|
|
463
|
+
inputSchema: {
|
|
464
|
+
id: z.number().describe('The leave type ID'),
|
|
465
|
+
},
|
|
466
|
+
}, async ({ id }) => {
|
|
467
|
+
try {
|
|
468
|
+
const leaveType = await getLeaveType(id);
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: 'text',
|
|
473
|
+
text: JSON.stringify(leaveType, null, 2),
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
return {
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
type: 'text',
|
|
483
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
isError: true,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
server.registerTool('list_allowances', {
|
|
491
|
+
title: 'List Time Off Allowances',
|
|
492
|
+
description: 'Get time off balances/allowances for employees. Shows available, consumed, and total days.',
|
|
493
|
+
inputSchema: {
|
|
494
|
+
employee_id: z.number().optional().describe('Filter by employee ID'),
|
|
495
|
+
page: z.number().optional().default(1).describe('Page number'),
|
|
496
|
+
limit: z.number().optional().default(100).describe('Items per page (max: 100)'),
|
|
497
|
+
},
|
|
498
|
+
}, async ({ employee_id, page, limit }) => {
|
|
499
|
+
try {
|
|
500
|
+
const result = await listAllowances({ employee_id, page, limit });
|
|
501
|
+
const summary = result.data.map(a => ({
|
|
502
|
+
id: a.id,
|
|
503
|
+
employee_id: a.employee_id,
|
|
504
|
+
leave_type_id: a.leave_type_id,
|
|
505
|
+
available_days: a.available_days,
|
|
506
|
+
consumed_days: a.consumed_days,
|
|
507
|
+
balance_days: a.balance_days,
|
|
508
|
+
valid_from: a.valid_from,
|
|
509
|
+
valid_to: a.valid_to,
|
|
510
|
+
}));
|
|
511
|
+
return {
|
|
512
|
+
content: [
|
|
513
|
+
{
|
|
514
|
+
type: 'text',
|
|
515
|
+
text: `Found ${result.data.length} allowances (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
return {
|
|
522
|
+
content: [
|
|
523
|
+
{
|
|
524
|
+
type: 'text',
|
|
525
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
isError: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// Attendance / Shift Tools
|
|
534
|
+
// ============================================================================
|
|
535
|
+
server.registerTool('list_shifts', {
|
|
536
|
+
title: 'List Shifts',
|
|
537
|
+
description: 'Get employee attendance shifts. Filter by employee or date range. Read-only access.',
|
|
538
|
+
inputSchema: {
|
|
539
|
+
employee_id: z.number().optional().describe('Filter by employee ID'),
|
|
540
|
+
clock_in_gte: z
|
|
541
|
+
.string()
|
|
542
|
+
.optional()
|
|
543
|
+
.describe('Filter shifts clocking in after this time (ISO 8601)'),
|
|
544
|
+
clock_in_lte: z
|
|
545
|
+
.string()
|
|
546
|
+
.optional()
|
|
547
|
+
.describe('Filter shifts clocking in before this time (ISO 8601)'),
|
|
548
|
+
page: z.number().optional().default(1).describe('Page number'),
|
|
549
|
+
limit: z.number().optional().default(100).describe('Items per page (max: 100)'),
|
|
550
|
+
},
|
|
551
|
+
}, async ({ employee_id, clock_in_gte, clock_in_lte, page, limit }) => {
|
|
552
|
+
try {
|
|
553
|
+
const result = await listShifts({ employee_id, clock_in_gte, clock_in_lte, page, limit });
|
|
554
|
+
const summary = result.data.map(s => ({
|
|
555
|
+
id: s.id,
|
|
556
|
+
employee_id: s.employee_id,
|
|
557
|
+
clock_in: s.clock_in,
|
|
558
|
+
clock_out: s.clock_out,
|
|
559
|
+
worked_hours: s.worked_hours,
|
|
560
|
+
break_minutes: s.break_minutes,
|
|
561
|
+
}));
|
|
562
|
+
return {
|
|
563
|
+
content: [
|
|
564
|
+
{
|
|
565
|
+
type: 'text',
|
|
566
|
+
text: `Found ${result.data.length} shifts (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
return {
|
|
573
|
+
content: [
|
|
574
|
+
{
|
|
575
|
+
type: 'text',
|
|
576
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
isError: true,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
server.registerTool('get_shift', {
|
|
584
|
+
title: 'Get Shift Details',
|
|
585
|
+
description: 'Get detailed information about a specific shift.',
|
|
586
|
+
inputSchema: {
|
|
587
|
+
id: z.number().describe('The shift ID'),
|
|
588
|
+
},
|
|
589
|
+
}, async ({ id }) => {
|
|
590
|
+
try {
|
|
591
|
+
const shift = await getShift(id);
|
|
289
592
|
return {
|
|
290
593
|
content: [
|
|
291
594
|
{
|
|
292
595
|
type: 'text',
|
|
293
|
-
text:
|
|
596
|
+
text: JSON.stringify(shift, null, 2),
|
|
294
597
|
},
|
|
295
598
|
],
|
|
296
599
|
};
|
|
297
600
|
}
|
|
298
601
|
catch (error) {
|
|
299
602
|
return {
|
|
300
|
-
content: [
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: 'text',
|
|
606
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
607
|
+
},
|
|
608
|
+
],
|
|
301
609
|
isError: true,
|
|
302
610
|
};
|
|
303
611
|
}
|
|
304
612
|
});
|
|
305
613
|
// ============================================================================
|
|
614
|
+
// Document Tools (Read-Only)
|
|
615
|
+
// ============================================================================
|
|
616
|
+
server.registerTool('list_folders', {
|
|
617
|
+
title: 'List Folders',
|
|
618
|
+
description: 'Get all document folders. Read-only access.',
|
|
619
|
+
inputSchema: {},
|
|
620
|
+
}, async () => {
|
|
621
|
+
try {
|
|
622
|
+
const folders = await listFolders();
|
|
623
|
+
return {
|
|
624
|
+
content: [
|
|
625
|
+
{
|
|
626
|
+
type: 'text',
|
|
627
|
+
text: `Found ${folders.length} folders:\n\n${JSON.stringify(folders, null, 2)}`,
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{
|
|
636
|
+
type: 'text',
|
|
637
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
isError: true,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
server.registerTool('get_folder', {
|
|
645
|
+
title: 'Get Folder',
|
|
646
|
+
description: 'Get details about a specific folder.',
|
|
647
|
+
inputSchema: {
|
|
648
|
+
id: z.number().describe('The folder ID'),
|
|
649
|
+
},
|
|
650
|
+
}, async ({ id }) => {
|
|
651
|
+
try {
|
|
652
|
+
const folder = await getFolder(id);
|
|
653
|
+
return {
|
|
654
|
+
content: [
|
|
655
|
+
{
|
|
656
|
+
type: 'text',
|
|
657
|
+
text: JSON.stringify(folder, null, 2),
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
return {
|
|
664
|
+
content: [
|
|
665
|
+
{
|
|
666
|
+
type: 'text',
|
|
667
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
isError: true,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
server.registerTool('list_documents', {
|
|
675
|
+
title: 'List Documents',
|
|
676
|
+
description: 'Get documents. Filter by folder. Read-only access.',
|
|
677
|
+
inputSchema: {
|
|
678
|
+
folder_id: z.number().optional().describe('Filter by folder ID'),
|
|
679
|
+
page: z.number().optional().default(1).describe('Page number'),
|
|
680
|
+
limit: z.number().optional().default(100).describe('Items per page (max: 100)'),
|
|
681
|
+
},
|
|
682
|
+
}, async ({ folder_id, page, limit }) => {
|
|
683
|
+
try {
|
|
684
|
+
const result = await listDocuments({ folder_id, page, limit });
|
|
685
|
+
const summary = result.data.map(d => ({
|
|
686
|
+
id: d.id,
|
|
687
|
+
name: d.name,
|
|
688
|
+
folder_id: d.folder_id,
|
|
689
|
+
mime_type: d.mime_type,
|
|
690
|
+
size_bytes: d.size_bytes,
|
|
691
|
+
}));
|
|
692
|
+
return {
|
|
693
|
+
content: [
|
|
694
|
+
{
|
|
695
|
+
type: 'text',
|
|
696
|
+
text: `Found ${result.data.length} documents (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
return {
|
|
703
|
+
content: [
|
|
704
|
+
{
|
|
705
|
+
type: 'text',
|
|
706
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
isError: true,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
server.registerTool('get_document', {
|
|
714
|
+
title: 'Get Document',
|
|
715
|
+
description: 'Get document metadata and URL. Read-only access.',
|
|
716
|
+
inputSchema: {
|
|
717
|
+
id: z.number().describe('The document ID'),
|
|
718
|
+
},
|
|
719
|
+
}, async ({ id }) => {
|
|
720
|
+
try {
|
|
721
|
+
const document = await getDocument(id);
|
|
722
|
+
return {
|
|
723
|
+
content: [
|
|
724
|
+
{
|
|
725
|
+
type: 'text',
|
|
726
|
+
text: JSON.stringify(document, null, 2),
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
return {
|
|
733
|
+
content: [
|
|
734
|
+
{
|
|
735
|
+
type: 'text',
|
|
736
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
isError: true,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
// ============================================================================
|
|
744
|
+
// Job Catalog Tools
|
|
745
|
+
// ============================================================================
|
|
746
|
+
server.registerTool('list_job_roles', {
|
|
747
|
+
title: 'List Job Roles',
|
|
748
|
+
description: 'Get all job roles defined in the job catalog.',
|
|
749
|
+
inputSchema: {},
|
|
750
|
+
}, async () => {
|
|
751
|
+
try {
|
|
752
|
+
const roles = await listJobRoles();
|
|
753
|
+
return {
|
|
754
|
+
content: [
|
|
755
|
+
{
|
|
756
|
+
type: 'text',
|
|
757
|
+
text: `Found ${roles.length} job roles:\n\n${JSON.stringify(roles, null, 2)}`,
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
return {
|
|
764
|
+
content: [
|
|
765
|
+
{
|
|
766
|
+
type: 'text',
|
|
767
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
isError: true,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
server.registerTool('get_job_role', {
|
|
775
|
+
title: 'Get Job Role',
|
|
776
|
+
description: 'Get details about a specific job role.',
|
|
777
|
+
inputSchema: {
|
|
778
|
+
id: z.number().describe('The job role ID'),
|
|
779
|
+
},
|
|
780
|
+
}, async ({ id }) => {
|
|
781
|
+
try {
|
|
782
|
+
const role = await getJobRole(id);
|
|
783
|
+
return {
|
|
784
|
+
content: [
|
|
785
|
+
{
|
|
786
|
+
type: 'text',
|
|
787
|
+
text: JSON.stringify(role, null, 2),
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
return {
|
|
794
|
+
content: [
|
|
795
|
+
{
|
|
796
|
+
type: 'text',
|
|
797
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
isError: true,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
server.registerTool('list_job_levels', {
|
|
805
|
+
title: 'List Job Levels',
|
|
806
|
+
description: 'Get all job levels defined in the job catalog.',
|
|
807
|
+
inputSchema: {},
|
|
808
|
+
}, async () => {
|
|
809
|
+
try {
|
|
810
|
+
const levels = await listJobLevels();
|
|
811
|
+
return {
|
|
812
|
+
content: [
|
|
813
|
+
{
|
|
814
|
+
type: 'text',
|
|
815
|
+
text: `Found ${levels.length} job levels:\n\n${JSON.stringify(levels, null, 2)}`,
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
return {
|
|
822
|
+
content: [
|
|
823
|
+
{
|
|
824
|
+
type: 'text',
|
|
825
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
826
|
+
},
|
|
827
|
+
],
|
|
828
|
+
isError: true,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
// ============================================================================
|
|
833
|
+
// MCP Resources
|
|
834
|
+
// ============================================================================
|
|
835
|
+
/**
|
|
836
|
+
* Build an org chart showing manager-report relationships
|
|
837
|
+
*/
|
|
838
|
+
async function buildOrgChart() {
|
|
839
|
+
const result = await listEmployees();
|
|
840
|
+
const employees = result.data;
|
|
841
|
+
// Build a map of manager_id -> reports
|
|
842
|
+
const managerMap = new Map();
|
|
843
|
+
for (const emp of employees) {
|
|
844
|
+
const managerId = emp.manager_id;
|
|
845
|
+
if (!managerMap.has(managerId)) {
|
|
846
|
+
managerMap.set(managerId, []);
|
|
847
|
+
}
|
|
848
|
+
managerMap.get(managerId).push(emp);
|
|
849
|
+
}
|
|
850
|
+
// Find top-level employees (no manager)
|
|
851
|
+
const topLevel = managerMap.get(null) || [];
|
|
852
|
+
// Recursive function to build tree
|
|
853
|
+
function buildTree(managerId, depth = 0) {
|
|
854
|
+
const reports = managerMap.get(managerId) || [];
|
|
855
|
+
const lines = [];
|
|
856
|
+
for (const emp of reports) {
|
|
857
|
+
const indent = ' '.repeat(depth);
|
|
858
|
+
lines.push(`${indent}- ${emp.full_name || 'Unknown'} (${emp.role || 'No role'}) [ID: ${emp.id}]`);
|
|
859
|
+
lines.push(...buildTree(emp.id, depth + 1));
|
|
860
|
+
}
|
|
861
|
+
return lines;
|
|
862
|
+
}
|
|
863
|
+
const lines = ['# Organization Chart\n'];
|
|
864
|
+
// Add top-level employees
|
|
865
|
+
for (const emp of topLevel) {
|
|
866
|
+
lines.push(`## ${emp.full_name || 'Unknown'} (${emp.role || 'No role'}) [ID: ${emp.id}]`);
|
|
867
|
+
lines.push(...buildTree(emp.id, 1));
|
|
868
|
+
lines.push('');
|
|
869
|
+
}
|
|
870
|
+
return lines.join('\n');
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Build employee directory organized by team
|
|
874
|
+
*/
|
|
875
|
+
async function buildEmployeeDirectory() {
|
|
876
|
+
const [empResult, teamsResult] = await Promise.all([listEmployees(), listTeams()]);
|
|
877
|
+
const employees = empResult.data;
|
|
878
|
+
const teams = teamsResult.data;
|
|
879
|
+
const lines = ['# Employee Directory\n'];
|
|
880
|
+
// Group employees by team
|
|
881
|
+
const teamEmployees = new Map();
|
|
882
|
+
for (const emp of employees) {
|
|
883
|
+
const teamIds = emp.team_ids || [];
|
|
884
|
+
if (teamIds.length === 0) {
|
|
885
|
+
const key = 'no-team';
|
|
886
|
+
if (!teamEmployees.has(key))
|
|
887
|
+
teamEmployees.set(key, []);
|
|
888
|
+
teamEmployees.get(key).push(emp);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
for (const teamId of teamIds) {
|
|
892
|
+
if (!teamEmployees.has(teamId))
|
|
893
|
+
teamEmployees.set(teamId, []);
|
|
894
|
+
teamEmployees.get(teamId).push(emp);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// Output by team
|
|
899
|
+
for (const team of teams) {
|
|
900
|
+
const emps = teamEmployees.get(team.id) || [];
|
|
901
|
+
lines.push(`## ${team.name}`);
|
|
902
|
+
if (team.description)
|
|
903
|
+
lines.push(`*${team.description}*\n`);
|
|
904
|
+
if (emps.length === 0) {
|
|
905
|
+
lines.push('No employees assigned.\n');
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
for (const emp of emps) {
|
|
909
|
+
lines.push(`- **${emp.full_name || 'Unknown'}** - ${emp.role || 'No role'}`);
|
|
910
|
+
if (emp.email)
|
|
911
|
+
lines.push(` - Email: ${emp.email}`);
|
|
912
|
+
}
|
|
913
|
+
lines.push('');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Employees without teams
|
|
917
|
+
const noTeam = teamEmployees.get('no-team') || [];
|
|
918
|
+
if (noTeam.length > 0) {
|
|
919
|
+
lines.push('## Unassigned\n');
|
|
920
|
+
for (const emp of noTeam) {
|
|
921
|
+
lines.push(`- **${emp.full_name || 'Unknown'}** - ${emp.role || 'No role'}`);
|
|
922
|
+
if (emp.email)
|
|
923
|
+
lines.push(` - Email: ${emp.email}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return lines.join('\n');
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Build location directory
|
|
930
|
+
*/
|
|
931
|
+
async function buildLocationDirectory() {
|
|
932
|
+
const [locResult, empResult] = await Promise.all([listLocations(), listEmployees()]);
|
|
933
|
+
const locations = locResult.data;
|
|
934
|
+
const employees = empResult.data;
|
|
935
|
+
// Group employees by location
|
|
936
|
+
const locationEmployees = new Map();
|
|
937
|
+
for (const emp of employees) {
|
|
938
|
+
if (emp.location_id) {
|
|
939
|
+
if (!locationEmployees.has(emp.location_id))
|
|
940
|
+
locationEmployees.set(emp.location_id, []);
|
|
941
|
+
locationEmployees.get(emp.location_id).push(emp);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const lines = ['# Location Directory\n'];
|
|
945
|
+
for (const loc of locations) {
|
|
946
|
+
const emps = locationEmployees.get(loc.id) || [];
|
|
947
|
+
lines.push(`## ${loc.name}`);
|
|
948
|
+
const address = [loc.city, loc.state, loc.country].filter(Boolean).join(', ');
|
|
949
|
+
if (address)
|
|
950
|
+
lines.push(`📍 ${address}\n`);
|
|
951
|
+
lines.push(`**Employees:** ${emps.length}`);
|
|
952
|
+
if (emps.length > 0 && emps.length <= 10) {
|
|
953
|
+
for (const emp of emps) {
|
|
954
|
+
lines.push(`- ${emp.full_name || 'Unknown'} (${emp.role || 'No role'})`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
else if (emps.length > 10) {
|
|
958
|
+
lines.push(`*... ${emps.length} employees (use list_employees with location_id=${loc.id} to see all)*`);
|
|
959
|
+
}
|
|
960
|
+
lines.push('');
|
|
961
|
+
}
|
|
962
|
+
return lines.join('\n');
|
|
963
|
+
}
|
|
964
|
+
// Register static resources
|
|
965
|
+
server.registerResource('org-chart', 'factorial://org-chart', {
|
|
966
|
+
description: 'Complete organizational hierarchy showing manager-report relationships. Useful for understanding team structure.',
|
|
967
|
+
mimeType: 'text/markdown',
|
|
968
|
+
}, async () => {
|
|
969
|
+
return {
|
|
970
|
+
contents: [
|
|
971
|
+
{
|
|
972
|
+
uri: 'factorial://org-chart',
|
|
973
|
+
mimeType: 'text/markdown',
|
|
974
|
+
text: await buildOrgChart(),
|
|
975
|
+
},
|
|
976
|
+
],
|
|
977
|
+
};
|
|
978
|
+
});
|
|
979
|
+
server.registerResource('employees-directory', 'factorial://employees/directory', {
|
|
980
|
+
description: 'Employee directory organized by team. Shows all employees with their roles and contact info.',
|
|
981
|
+
mimeType: 'text/markdown',
|
|
982
|
+
}, async () => {
|
|
983
|
+
return {
|
|
984
|
+
contents: [
|
|
985
|
+
{
|
|
986
|
+
uri: 'factorial://employees/directory',
|
|
987
|
+
mimeType: 'text/markdown',
|
|
988
|
+
text: await buildEmployeeDirectory(),
|
|
989
|
+
},
|
|
990
|
+
],
|
|
991
|
+
};
|
|
992
|
+
});
|
|
993
|
+
server.registerResource('locations-directory', 'factorial://locations/directory', {
|
|
994
|
+
description: 'Directory of all company locations with employee counts.',
|
|
995
|
+
mimeType: 'text/markdown',
|
|
996
|
+
}, async () => {
|
|
997
|
+
return {
|
|
998
|
+
contents: [
|
|
999
|
+
{
|
|
1000
|
+
uri: 'factorial://locations/directory',
|
|
1001
|
+
mimeType: 'text/markdown',
|
|
1002
|
+
text: await buildLocationDirectory(),
|
|
1003
|
+
},
|
|
1004
|
+
],
|
|
1005
|
+
};
|
|
1006
|
+
});
|
|
1007
|
+
server.registerResource('timeoff-policies', 'factorial://timeoff/policies', {
|
|
1008
|
+
description: 'All leave types and time off policies configured in the organization.',
|
|
1009
|
+
mimeType: 'application/json',
|
|
1010
|
+
}, async () => {
|
|
1011
|
+
const types = await listLeaveTypes();
|
|
1012
|
+
return {
|
|
1013
|
+
contents: [
|
|
1014
|
+
{
|
|
1015
|
+
uri: 'factorial://timeoff/policies',
|
|
1016
|
+
mimeType: 'application/json',
|
|
1017
|
+
text: JSON.stringify(types, null, 2),
|
|
1018
|
+
},
|
|
1019
|
+
],
|
|
1020
|
+
};
|
|
1021
|
+
});
|
|
1022
|
+
// Register resource template for teams
|
|
1023
|
+
const teamTemplate = new ResourceTemplate('factorial://teams/{team_id}', {
|
|
1024
|
+
list: async () => {
|
|
1025
|
+
const result = await listTeams();
|
|
1026
|
+
return {
|
|
1027
|
+
resources: result.data.map(t => ({
|
|
1028
|
+
uri: `factorial://teams/${t.id}`,
|
|
1029
|
+
name: t.name,
|
|
1030
|
+
description: t.description || undefined,
|
|
1031
|
+
mimeType: 'application/json',
|
|
1032
|
+
})),
|
|
1033
|
+
};
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
server.registerResource('team-details', teamTemplate, {
|
|
1037
|
+
description: 'Get detailed information about a specific team including all members.',
|
|
1038
|
+
mimeType: 'application/json',
|
|
1039
|
+
}, async (uri, variables) => {
|
|
1040
|
+
const teamId = parseInt(variables.team_id, 10);
|
|
1041
|
+
if (isNaN(teamId)) {
|
|
1042
|
+
throw new Error('Invalid team ID');
|
|
1043
|
+
}
|
|
1044
|
+
const [team, empResult] = await Promise.all([
|
|
1045
|
+
getTeam(teamId),
|
|
1046
|
+
listEmployees({ team_id: teamId }),
|
|
1047
|
+
]);
|
|
1048
|
+
const teamDetails = {
|
|
1049
|
+
...team,
|
|
1050
|
+
members: empResult.data.map(e => ({
|
|
1051
|
+
id: e.id,
|
|
1052
|
+
name: e.full_name,
|
|
1053
|
+
role: e.role,
|
|
1054
|
+
email: e.email,
|
|
1055
|
+
})),
|
|
1056
|
+
};
|
|
1057
|
+
return {
|
|
1058
|
+
contents: [
|
|
1059
|
+
{
|
|
1060
|
+
uri: uri.toString(),
|
|
1061
|
+
mimeType: 'application/json',
|
|
1062
|
+
text: JSON.stringify(teamDetails, null, 2),
|
|
1063
|
+
},
|
|
1064
|
+
],
|
|
1065
|
+
};
|
|
1066
|
+
});
|
|
1067
|
+
// ============================================================================
|
|
1068
|
+
// MCP Prompts
|
|
1069
|
+
// ============================================================================
|
|
1070
|
+
server.registerPrompt('onboard-employee', {
|
|
1071
|
+
description: 'Generate a personalized onboarding checklist for a new employee based on their team and role.',
|
|
1072
|
+
argsSchema: {
|
|
1073
|
+
employee_id: z.string().describe('The ID of the employee to onboard'),
|
|
1074
|
+
},
|
|
1075
|
+
}, async ({ employee_id }) => {
|
|
1076
|
+
const empId = parseInt(employee_id, 10);
|
|
1077
|
+
const employee = await getEmployee(empId);
|
|
1078
|
+
const teamsResult = await listTeams();
|
|
1079
|
+
const teams = teamsResult.data;
|
|
1080
|
+
const employeeTeams = teams.filter(t => employee.team_ids?.includes(t.id));
|
|
1081
|
+
const teamNames = employeeTeams.map(t => t.name).join(', ') || 'No team assigned';
|
|
1082
|
+
return {
|
|
1083
|
+
messages: [
|
|
1084
|
+
{
|
|
1085
|
+
role: 'user',
|
|
1086
|
+
content: {
|
|
1087
|
+
type: 'text',
|
|
1088
|
+
text: `Please create a comprehensive onboarding checklist for the following new employee:
|
|
1089
|
+
|
|
1090
|
+
**Employee Details:**
|
|
1091
|
+
- Name: ${employee.full_name}
|
|
1092
|
+
- Role: ${employee.role || 'Not specified'}
|
|
1093
|
+
- Team(s): ${teamNames}
|
|
1094
|
+
- Start Date: ${employee.hired_on || employee.start_date || 'Not specified'}
|
|
1095
|
+
- Email: ${employee.email}
|
|
1096
|
+
|
|
1097
|
+
Please include:
|
|
1098
|
+
1. First day essentials
|
|
1099
|
+
2. First week goals
|
|
1100
|
+
3. Team introductions
|
|
1101
|
+
4. Tools and access setup
|
|
1102
|
+
5. Key meetings to schedule
|
|
1103
|
+
6. 30/60/90 day milestones
|
|
1104
|
+
|
|
1105
|
+
Tailor the checklist to their specific role and team.`,
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
server.registerPrompt('analyze-org-structure', {
|
|
1112
|
+
description: 'Analyze the organizational structure for insights on reporting lines, team sizes, and distribution.',
|
|
1113
|
+
argsSchema: {
|
|
1114
|
+
focus_area: z
|
|
1115
|
+
.string()
|
|
1116
|
+
.optional()
|
|
1117
|
+
.describe('Area to focus on: reporting_lines, team_sizes, location_distribution'),
|
|
1118
|
+
},
|
|
1119
|
+
}, async ({ focus_area }) => {
|
|
1120
|
+
const [empResult, teamsResult, locResult] = await Promise.all([
|
|
1121
|
+
listEmployees(),
|
|
1122
|
+
listTeams(),
|
|
1123
|
+
listLocations(),
|
|
1124
|
+
]);
|
|
1125
|
+
const employees = empResult.data;
|
|
1126
|
+
const teams = teamsResult.data;
|
|
1127
|
+
const locations = locResult.data;
|
|
1128
|
+
// Compute basic stats
|
|
1129
|
+
const totalEmployees = employees.length;
|
|
1130
|
+
const managersCount = new Set(employees.map(e => e.manager_id).filter(Boolean)).size;
|
|
1131
|
+
const avgTeamSize = teams.length > 0
|
|
1132
|
+
? (teams.reduce((sum, t) => sum + (t.employee_ids?.length || 0), 0) / teams.length).toFixed(1)
|
|
1133
|
+
: 0;
|
|
1134
|
+
let focusPrompt = '';
|
|
1135
|
+
if (focus_area === 'reporting_lines') {
|
|
1136
|
+
focusPrompt =
|
|
1137
|
+
'Focus particularly on reporting line depth, span of control, and potential bottlenecks.';
|
|
1138
|
+
}
|
|
1139
|
+
else if (focus_area === 'team_sizes') {
|
|
1140
|
+
focusPrompt =
|
|
1141
|
+
'Focus particularly on team size distribution, under/over-staffed teams, and growth patterns.';
|
|
1142
|
+
}
|
|
1143
|
+
else if (focus_area === 'location_distribution') {
|
|
1144
|
+
focusPrompt =
|
|
1145
|
+
'Focus particularly on geographic distribution, remote vs on-site, and location-based team composition.';
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
messages: [
|
|
1149
|
+
{
|
|
1150
|
+
role: 'user',
|
|
1151
|
+
content: {
|
|
1152
|
+
type: 'text',
|
|
1153
|
+
text: `Please analyze the following organizational structure:
|
|
1154
|
+
|
|
1155
|
+
**Summary Statistics:**
|
|
1156
|
+
- Total Employees: ${totalEmployees}
|
|
1157
|
+
- Total Teams: ${teams.length}
|
|
1158
|
+
- Total Locations: ${locations.length}
|
|
1159
|
+
- Unique Managers: ${managersCount}
|
|
1160
|
+
- Average Team Size: ${avgTeamSize}
|
|
1161
|
+
|
|
1162
|
+
**Teams:**
|
|
1163
|
+
${teams.map(t => `- ${t.name}: ${t.employee_ids?.length || 0} members`).join('\n')}
|
|
1164
|
+
|
|
1165
|
+
**Locations:**
|
|
1166
|
+
${locations.map(l => `- ${l.name} (${[l.city, l.country].filter(Boolean).join(', ')})`).join('\n')}
|
|
1167
|
+
|
|
1168
|
+
${focusPrompt}
|
|
1169
|
+
|
|
1170
|
+
Please provide:
|
|
1171
|
+
1. Key observations about the org structure
|
|
1172
|
+
2. Potential areas of concern
|
|
1173
|
+
3. Recommendations for improvement
|
|
1174
|
+
4. Comparison to industry best practices`,
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
],
|
|
1178
|
+
};
|
|
1179
|
+
});
|
|
1180
|
+
server.registerPrompt('timeoff-report', {
|
|
1181
|
+
description: 'Generate a time off report for a team or date range.',
|
|
1182
|
+
argsSchema: {
|
|
1183
|
+
team_id: z.string().optional().describe('Team ID to filter by'),
|
|
1184
|
+
start_date: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
|
1185
|
+
end_date: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
|
1186
|
+
include_pending: z.string().optional().describe('Include pending requests (true/false)'),
|
|
1187
|
+
},
|
|
1188
|
+
}, async ({ team_id, start_date, end_date, include_pending }) => {
|
|
1189
|
+
const teamId = team_id ? parseInt(team_id, 10) : undefined;
|
|
1190
|
+
const includePending = include_pending === 'true';
|
|
1191
|
+
// Get leaves
|
|
1192
|
+
const leavesResult = await listLeaves({
|
|
1193
|
+
start_on_gte: start_date,
|
|
1194
|
+
start_on_lte: end_date,
|
|
1195
|
+
});
|
|
1196
|
+
// Get leave types for names
|
|
1197
|
+
const leaveTypes = await listLeaveTypes();
|
|
1198
|
+
const leaveTypeMap = new Map(leaveTypes.map(lt => [lt.id, lt.name]));
|
|
1199
|
+
// Filter by team if needed
|
|
1200
|
+
let leaves = leavesResult.data;
|
|
1201
|
+
if (teamId) {
|
|
1202
|
+
const teamEmps = (await listEmployees({ team_id: teamId })).data;
|
|
1203
|
+
const teamEmpIds = new Set(teamEmps.map(e => e.id));
|
|
1204
|
+
leaves = leaves.filter(l => teamEmpIds.has(l.employee_id));
|
|
1205
|
+
}
|
|
1206
|
+
// Filter by status
|
|
1207
|
+
if (!includePending) {
|
|
1208
|
+
leaves = leaves.filter(l => l.status === 'approved');
|
|
1209
|
+
}
|
|
1210
|
+
// Get employee names
|
|
1211
|
+
const empResult = await listEmployees();
|
|
1212
|
+
const empMap = new Map(empResult.data.map(e => [e.id, e.full_name]));
|
|
1213
|
+
const leaveSummary = leaves.map(l => ({
|
|
1214
|
+
employee: empMap.get(l.employee_id) || `Employee ${l.employee_id}`,
|
|
1215
|
+
type: leaveTypeMap.get(l.leave_type_id) || `Type ${l.leave_type_id}`,
|
|
1216
|
+
dates: `${l.start_on} to ${l.finish_on}`,
|
|
1217
|
+
days: l.duration_attributes?.days || 'N/A',
|
|
1218
|
+
status: l.status,
|
|
1219
|
+
}));
|
|
1220
|
+
return {
|
|
1221
|
+
messages: [
|
|
1222
|
+
{
|
|
1223
|
+
role: 'user',
|
|
1224
|
+
content: {
|
|
1225
|
+
type: 'text',
|
|
1226
|
+
text: `Please generate a time off report based on the following data:
|
|
1227
|
+
|
|
1228
|
+
**Report Parameters:**
|
|
1229
|
+
- Date Range: ${start_date || 'All'} to ${end_date || 'All'}
|
|
1230
|
+
- Team: ${teamId ? `Team ${teamId}` : 'All teams'}
|
|
1231
|
+
- Including Pending: ${includePending ? 'Yes' : 'No'}
|
|
1232
|
+
|
|
1233
|
+
**Time Off Requests (${leaves.length} total):**
|
|
1234
|
+
${JSON.stringify(leaveSummary, null, 2)}
|
|
1235
|
+
|
|
1236
|
+
Please provide:
|
|
1237
|
+
1. Summary of time off by type
|
|
1238
|
+
2. Peak absence periods
|
|
1239
|
+
3. Coverage concerns (if any patterns suggest coverage gaps)
|
|
1240
|
+
4. Recommendations for planning`,
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
],
|
|
1244
|
+
};
|
|
1245
|
+
});
|
|
1246
|
+
// ============================================================================
|
|
306
1247
|
// Start Server
|
|
307
1248
|
// ============================================================================
|
|
308
1249
|
async function main() {
|
|
309
1250
|
const transport = new StdioServerTransport();
|
|
310
1251
|
await server.connect(transport);
|
|
311
1252
|
}
|
|
312
|
-
main().catch(
|
|
1253
|
+
main().catch(error => {
|
|
313
1254
|
console.error('Fatal error:', error);
|
|
314
1255
|
process.exit(1);
|
|
315
1256
|
});
|