@t4dhg/mcp-factorial 3.2.0 → 5.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/dist/index.js CHANGED
@@ -21,6 +21,7 @@ loadEnv();
21
21
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
22
22
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
23
23
  import * as z from 'zod';
24
+ import { cache } from './cache.js';
24
25
  import {
25
26
  // Employees - Read
26
27
  listEmployees, getEmployee, searchEmployees,
@@ -65,6 +66,7 @@ createJobPosting, updateJobPosting, deleteJobPosting, createCandidate, updateCan
65
66
  // Payroll (Read-only)
66
67
  listPayrollSupplements, getPayrollSupplement, listTaxIdentifiers, getTaxIdentifier, listFamilySituations, getFamilySituation, } from './api.js';
67
68
  import { formatPaginationInfo } from './pagination.js';
69
+ import { wrapHighRiskToolHandler, textResponse } from './tool-utils.js';
68
70
  const server = new McpServer({
69
71
  name: 'factorial-hr',
70
72
  version: '3.0.0',
@@ -272,36 +274,17 @@ server.registerTool('update_employee', {
272
274
  });
273
275
  server.registerTool('terminate_employee', {
274
276
  title: 'Terminate Employee',
275
- description: 'Terminate an employee (soft delete). This is a high-risk operation that sets the termination date.',
277
+ description: 'Terminate an employee (soft delete). This is a HIGH-RISK operation that requires confirmation.',
276
278
  inputSchema: {
277
279
  id: z.number().describe('The employee ID to terminate'),
278
280
  terminated_on: z.string().describe('Termination date (YYYY-MM-DD)'),
279
281
  reason: z.string().max(500).optional().describe('Termination reason'),
282
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
280
283
  },
281
- }, async ({ id, terminated_on, reason }) => {
282
- try {
283
- const employee = await terminateEmployee(id, terminated_on, reason);
284
- return {
285
- content: [
286
- {
287
- type: 'text',
288
- text: `Employee terminated successfully. Termination date: ${terminated_on}\n\n${JSON.stringify(employee, null, 2)}`,
289
- },
290
- ],
291
- };
292
- }
293
- catch (error) {
294
- return {
295
- content: [
296
- {
297
- type: 'text',
298
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
299
- },
300
- ],
301
- isError: true,
302
- };
303
- }
304
- });
284
+ }, wrapHighRiskToolHandler('terminate_employee', async ({ id, terminated_on, reason }) => {
285
+ const employee = await terminateEmployee(id, terminated_on, reason);
286
+ return textResponse(`Employee terminated successfully. Termination date: ${terminated_on}\n\n${JSON.stringify(employee, null, 2)}`);
287
+ }));
305
288
  // ============================================================================
306
289
  // Team Tools
307
290
  // ============================================================================
@@ -441,34 +424,15 @@ server.registerTool('update_team', {
441
424
  });
442
425
  server.registerTool('delete_team', {
443
426
  title: 'Delete Team',
444
- description: 'Delete a team. This is a high-risk operation.',
427
+ description: 'Delete a team. This is a HIGH-RISK operation that requires confirmation.',
445
428
  inputSchema: {
446
429
  id: z.number().describe('The team ID to delete'),
430
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
447
431
  },
448
- }, async ({ id }) => {
449
- try {
450
- await deleteTeam(id);
451
- return {
452
- content: [
453
- {
454
- type: 'text',
455
- text: `Team ${id} deleted successfully.`,
456
- },
457
- ],
458
- };
459
- }
460
- catch (error) {
461
- return {
462
- content: [
463
- {
464
- type: 'text',
465
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
466
- },
467
- ],
468
- isError: true,
469
- };
470
- }
471
- });
432
+ }, wrapHighRiskToolHandler('delete_team', async ({ id }) => {
433
+ await deleteTeam(id);
434
+ return textResponse(`Team ${id} deleted successfully.`);
435
+ }));
472
436
  // ============================================================================
473
437
  // Location Tools
474
438
  // ============================================================================
@@ -616,51 +580,41 @@ server.registerTool('update_location', {
616
580
  });
617
581
  server.registerTool('delete_location', {
618
582
  title: 'Delete Location',
619
- description: 'Delete a location. This is a high-risk operation.',
583
+ description: 'Delete a location. This is a HIGH-RISK operation that requires confirmation.',
620
584
  inputSchema: {
621
585
  id: z.number().describe('The location ID to delete'),
586
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
622
587
  },
623
- }, async ({ id }) => {
624
- try {
625
- await deleteLocation(id);
626
- return {
627
- content: [
628
- {
629
- type: 'text',
630
- text: `Location ${id} deleted successfully.`,
631
- },
632
- ],
633
- };
634
- }
635
- catch (error) {
636
- return {
637
- content: [
638
- {
639
- type: 'text',
640
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
641
- },
642
- ],
643
- isError: true,
644
- };
645
- }
646
- });
588
+ }, wrapHighRiskToolHandler('delete_location', async ({ id }) => {
589
+ await deleteLocation(id);
590
+ return textResponse(`Location ${id} deleted successfully.`);
591
+ }));
647
592
  // ============================================================================
648
593
  // Contract Tools
649
594
  // ============================================================================
650
595
  server.registerTool('get_employee_contracts', {
651
596
  title: 'Get Employee Contracts',
652
- description: 'Get contract versions for an employee, including job title and effective date.',
597
+ description: 'Get contract versions for an employee. Returns contract summary (id, employee_id, job_title, effective_on).',
653
598
  inputSchema: {
654
599
  employee_id: z.number().describe('The employee ID'),
600
+ page: z.number().optional().default(1).describe('Page number'),
601
+ limit: z.number().optional().default(20).describe('Items per page (max: 100, default: 20)'),
655
602
  },
656
- }, async ({ employee_id }) => {
603
+ }, async ({ employee_id, page, limit }) => {
657
604
  try {
658
- const result = await listContracts(employee_id);
605
+ const result = await listContracts(employee_id, { page, limit });
606
+ // Create summary format
607
+ const summary = result.data.map(c => ({
608
+ id: c.id,
609
+ employee_id: c.employee_id,
610
+ job_title: c.job_title,
611
+ effective_on: c.effective_on,
612
+ }));
659
613
  return {
660
614
  content: [
661
615
  {
662
616
  type: 'text',
663
- text: `Found ${result.data.length} contracts:\n\n${JSON.stringify(result.data, null, 2)}`,
617
+ text: `Found ${result.data.length} contracts for employee ${employee_id} (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
664
618
  },
665
619
  ],
666
620
  };
@@ -942,34 +896,15 @@ server.registerTool('update_leave', {
942
896
  });
943
897
  server.registerTool('cancel_leave', {
944
898
  title: 'Cancel Leave Request',
945
- description: 'Cancel a leave request.',
899
+ description: 'Cancel a leave request. This operation requires confirmation.',
946
900
  inputSchema: {
947
901
  id: z.number().describe('The leave ID to cancel'),
902
+ confirm: z.boolean().optional().describe('Set to true to confirm this operation'),
948
903
  },
949
- }, async ({ id }) => {
950
- try {
951
- await cancelLeave(id);
952
- return {
953
- content: [
954
- {
955
- type: 'text',
956
- text: `Leave request ${id} cancelled successfully.`,
957
- },
958
- ],
959
- };
960
- }
961
- catch (error) {
962
- return {
963
- content: [
964
- {
965
- type: 'text',
966
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
967
- },
968
- ],
969
- isError: true,
970
- };
971
- }
972
- });
904
+ }, wrapHighRiskToolHandler('cancel_leave', async ({ id }) => {
905
+ await cancelLeave(id);
906
+ return textResponse(`Leave request ${id} cancelled successfully.`);
907
+ }));
973
908
  server.registerTool('approve_leave', {
974
909
  title: 'Approve Leave Request',
975
910
  description: 'Approve a pending leave request.',
@@ -1003,35 +938,16 @@ server.registerTool('approve_leave', {
1003
938
  });
1004
939
  server.registerTool('reject_leave', {
1005
940
  title: 'Reject Leave Request',
1006
- description: 'Reject a pending leave request.',
941
+ description: 'Reject a pending leave request. This operation requires confirmation.',
1007
942
  inputSchema: {
1008
943
  id: z.number().describe('The leave ID to reject'),
1009
944
  reason: z.string().max(500).optional().describe('Rejection reason'),
945
+ confirm: z.boolean().optional().describe('Set to true to confirm this operation'),
1010
946
  },
1011
- }, async ({ id, reason }) => {
1012
- try {
1013
- const leave = await rejectLeave(id, reason ? { reason } : undefined);
1014
- return {
1015
- content: [
1016
- {
1017
- type: 'text',
1018
- text: `Leave request rejected:\n\n${JSON.stringify(leave, null, 2)}`,
1019
- },
1020
- ],
1021
- };
1022
- }
1023
- catch (error) {
1024
- return {
1025
- content: [
1026
- {
1027
- type: 'text',
1028
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
1029
- },
1030
- ],
1031
- isError: true,
1032
- };
1033
- }
1034
- });
947
+ }, wrapHighRiskToolHandler('reject_leave', async ({ id, reason }) => {
948
+ const leave = await rejectLeave(id, reason ? { reason } : undefined);
949
+ return textResponse(`Leave request rejected:\n\n${JSON.stringify(leave, null, 2)}`);
950
+ }));
1035
951
  // ============================================================================
1036
952
  // Attendance / Shift Tools
1037
953
  // ============================================================================
@@ -1185,34 +1101,15 @@ server.registerTool('update_shift', {
1185
1101
  });
1186
1102
  server.registerTool('delete_shift', {
1187
1103
  title: 'Delete Shift',
1188
- description: 'Delete a shift record.',
1104
+ description: 'Delete a shift record. This operation requires confirmation.',
1189
1105
  inputSchema: {
1190
1106
  id: z.number().describe('The shift ID to delete'),
1107
+ confirm: z.boolean().optional().describe('Set to true to confirm this operation'),
1191
1108
  },
1192
- }, async ({ id }) => {
1193
- try {
1194
- await deleteShift(id);
1195
- return {
1196
- content: [
1197
- {
1198
- type: 'text',
1199
- text: `Shift ${id} deleted successfully.`,
1200
- },
1201
- ],
1202
- };
1203
- }
1204
- catch (error) {
1205
- return {
1206
- content: [
1207
- {
1208
- type: 'text',
1209
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
1210
- },
1211
- ],
1212
- isError: true,
1213
- };
1214
- }
1215
- });
1109
+ }, wrapHighRiskToolHandler('delete_shift', async ({ id }) => {
1110
+ await deleteShift(id);
1111
+ return textResponse(`Shift ${id} deleted successfully.`);
1112
+ }));
1216
1113
  // ============================================================================
1217
1114
  // Document Tools (Read-Only)
1218
1115
  // ============================================================================
@@ -1348,20 +1245,30 @@ server.registerTool('get_document', {
1348
1245
  });
1349
1246
  server.registerTool('get_employee_documents', {
1350
1247
  title: 'Get Employee Documents',
1351
- description: 'Get all documents for a specific employee. Returns document metadata including file URLs.',
1248
+ description: 'Get all documents for a specific employee. Returns document summary (id, name, folder_id, employee_id, author_id, mime_type, size_bytes). Use get_document for full details.',
1352
1249
  inputSchema: {
1353
1250
  employee_id: z.number().describe('The employee ID'),
1354
1251
  page: z.number().optional().default(1).describe('Page number'),
1355
- limit: z.number().optional().default(100).describe('Items per page (max: 100)'),
1252
+ limit: z.number().optional().default(20).describe('Items per page (max: 100, default: 20)'),
1356
1253
  },
1357
1254
  }, async ({ employee_id, page, limit }) => {
1358
1255
  try {
1359
1256
  const result = await listDocuments({ employee_ids: [employee_id], page, limit });
1257
+ // Create summary format aligned with list_documents tool
1258
+ const summary = result.data.map(d => ({
1259
+ id: d.id,
1260
+ name: d.name,
1261
+ folder_id: d.folder_id,
1262
+ employee_id: d.employee_id,
1263
+ author_id: d.author_id,
1264
+ mime_type: d.mime_type,
1265
+ size_bytes: d.size_bytes,
1266
+ }));
1360
1267
  return {
1361
1268
  content: [
1362
1269
  {
1363
1270
  type: 'text',
1364
- text: `Found ${result.data.length} documents for employee ${employee_id} (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(result.data, null, 2)}`,
1271
+ text: `Found ${result.data.length} documents for employee ${employee_id} (${formatPaginationInfo(result.meta)}):\n\n${JSON.stringify(summary, null, 2)}`,
1365
1272
  },
1366
1273
  ],
1367
1274
  };
@@ -1589,29 +1496,15 @@ server.registerTool('update_project', {
1589
1496
  });
1590
1497
  server.registerTool('delete_project', {
1591
1498
  title: 'Delete Project',
1592
- description: 'Delete a project. This is a high-risk operation.',
1499
+ description: 'Delete a project. This is a HIGH-RISK operation that requires confirmation.',
1593
1500
  inputSchema: {
1594
1501
  id: z.number().describe('The project ID to delete'),
1502
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
1595
1503
  },
1596
- }, async ({ id }) => {
1597
- try {
1598
- await deleteProject(id);
1599
- return {
1600
- content: [{ type: 'text', text: `Project ${id} deleted successfully.` }],
1601
- };
1602
- }
1603
- catch (error) {
1604
- return {
1605
- content: [
1606
- {
1607
- type: 'text',
1608
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
1609
- },
1610
- ],
1611
- isError: true,
1612
- };
1613
- }
1614
- });
1504
+ }, wrapHighRiskToolHandler('delete_project', async ({ id }) => {
1505
+ await deleteProject(id);
1506
+ return textResponse(`Project ${id} deleted successfully.`);
1507
+ }));
1615
1508
  server.registerTool('list_project_tasks', {
1616
1509
  title: 'List Project Tasks',
1617
1510
  description: 'Get tasks for a project.',
@@ -2048,29 +1941,15 @@ server.registerTool('update_training', {
2048
1941
  });
2049
1942
  server.registerTool('delete_training', {
2050
1943
  title: 'Delete Training',
2051
- description: 'Delete a training program.',
1944
+ description: 'Delete a training program. This is a HIGH-RISK operation that requires confirmation.',
2052
1945
  inputSchema: {
2053
1946
  id: z.number().describe('The training ID to delete'),
1947
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2054
1948
  },
2055
- }, async ({ id }) => {
2056
- try {
2057
- await deleteTraining(id);
2058
- return {
2059
- content: [{ type: 'text', text: `Training ${id} deleted successfully.` }],
2060
- };
2061
- }
2062
- catch (error) {
2063
- return {
2064
- content: [
2065
- {
2066
- type: 'text',
2067
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2068
- },
2069
- ],
2070
- isError: true,
2071
- };
2072
- }
2073
- });
1949
+ }, wrapHighRiskToolHandler('delete_training', async ({ id }) => {
1950
+ await deleteTraining(id);
1951
+ return textResponse(`Training ${id} deleted successfully.`);
1952
+ }));
2074
1953
  server.registerTool('list_training_sessions', {
2075
1954
  title: 'List Training Sessions',
2076
1955
  description: 'Get sessions for a training program.',
@@ -2575,29 +2454,15 @@ server.registerTool('update_job_posting', {
2575
2454
  });
2576
2455
  server.registerTool('delete_job_posting', {
2577
2456
  title: 'Delete Job Posting',
2578
- description: 'Delete a job posting.',
2457
+ description: 'Delete a job posting. This is a HIGH-RISK operation that requires confirmation.',
2579
2458
  inputSchema: {
2580
2459
  id: z.number().describe('Job posting ID to delete'),
2460
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2581
2461
  },
2582
- }, async ({ id }) => {
2583
- try {
2584
- await deleteJobPosting(id);
2585
- return {
2586
- content: [{ type: 'text', text: `Job posting ${id} deleted successfully.` }],
2587
- };
2588
- }
2589
- catch (error) {
2590
- return {
2591
- content: [
2592
- {
2593
- type: 'text',
2594
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2595
- },
2596
- ],
2597
- isError: true,
2598
- };
2599
- }
2600
- });
2462
+ }, wrapHighRiskToolHandler('delete_job_posting', async ({ id }) => {
2463
+ await deleteJobPosting(id);
2464
+ return textResponse(`Job posting ${id} deleted successfully.`);
2465
+ }));
2601
2466
  server.registerTool('list_candidates', {
2602
2467
  title: 'List Candidates',
2603
2468
  description: 'Get all candidates.',
@@ -2721,29 +2586,15 @@ server.registerTool('update_candidate', {
2721
2586
  });
2722
2587
  server.registerTool('delete_candidate', {
2723
2588
  title: 'Delete Candidate',
2724
- description: 'Delete a candidate.',
2589
+ description: 'Delete a candidate. This is a HIGH-RISK operation that requires confirmation.',
2725
2590
  inputSchema: {
2726
2591
  id: z.number().describe('Candidate ID to delete'),
2592
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2727
2593
  },
2728
- }, async ({ id }) => {
2729
- try {
2730
- await deleteCandidate(id);
2731
- return {
2732
- content: [{ type: 'text', text: `Candidate ${id} deleted successfully.` }],
2733
- };
2734
- }
2735
- catch (error) {
2736
- return {
2737
- content: [
2738
- {
2739
- type: 'text',
2740
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2741
- },
2742
- ],
2743
- isError: true,
2744
- };
2745
- }
2746
- });
2594
+ }, wrapHighRiskToolHandler('delete_candidate', async ({ id }) => {
2595
+ await deleteCandidate(id);
2596
+ return textResponse(`Candidate ${id} deleted successfully.`);
2597
+ }));
2747
2598
  server.registerTool('list_applications', {
2748
2599
  title: 'List Applications',
2749
2600
  description: 'Get job applications.',
@@ -2862,29 +2713,15 @@ server.registerTool('update_application', {
2862
2713
  });
2863
2714
  server.registerTool('delete_application', {
2864
2715
  title: 'Delete Application',
2865
- description: 'Delete a job application.',
2716
+ description: 'Delete a job application. This is a HIGH-RISK operation that requires confirmation.',
2866
2717
  inputSchema: {
2867
2718
  id: z.number().describe('Application ID to delete'),
2719
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2868
2720
  },
2869
- }, async ({ id }) => {
2870
- try {
2871
- await deleteApplication(id);
2872
- return {
2873
- content: [{ type: 'text', text: `Application ${id} deleted successfully.` }],
2874
- };
2875
- }
2876
- catch (error) {
2877
- return {
2878
- content: [
2879
- {
2880
- type: 'text',
2881
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2882
- },
2883
- ],
2884
- isError: true,
2885
- };
2886
- }
2887
- });
2721
+ }, wrapHighRiskToolHandler('delete_application', async ({ id }) => {
2722
+ await deleteApplication(id);
2723
+ return textResponse(`Application ${id} deleted successfully.`);
2724
+ }));
2888
2725
  server.registerTool('advance_application', {
2889
2726
  title: 'Advance Application',
2890
2727
  description: 'Move an application to the next hiring stage.',
@@ -3532,6 +3369,18 @@ Please provide:
3532
3369
  };
3533
3370
  });
3534
3371
  // ============================================================================
3372
+ // Graceful Shutdown
3373
+ // ============================================================================
3374
+ /**
3375
+ * Clean up resources on process termination
3376
+ */
3377
+ function shutdown() {
3378
+ cache.destroy();
3379
+ process.exit(0);
3380
+ }
3381
+ process.on('SIGINT', shutdown);
3382
+ process.on('SIGTERM', shutdown);
3383
+ // ============================================================================
3535
3384
  // Start Server
3536
3385
  // ============================================================================
3537
3386
  async function main() {