@t4dhg/mcp-factorial 4.0.0 → 6.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,34 +580,15 @@ 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
  // ============================================================================
@@ -951,34 +896,15 @@ server.registerTool('update_leave', {
951
896
  });
952
897
  server.registerTool('cancel_leave', {
953
898
  title: 'Cancel Leave Request',
954
- description: 'Cancel a leave request.',
899
+ description: 'Cancel a leave request. This operation requires confirmation.',
955
900
  inputSchema: {
956
901
  id: z.number().describe('The leave ID to cancel'),
902
+ confirm: z.boolean().optional().describe('Set to true to confirm this operation'),
957
903
  },
958
- }, async ({ id }) => {
959
- try {
960
- await cancelLeave(id);
961
- return {
962
- content: [
963
- {
964
- type: 'text',
965
- text: `Leave request ${id} cancelled successfully.`,
966
- },
967
- ],
968
- };
969
- }
970
- catch (error) {
971
- return {
972
- content: [
973
- {
974
- type: 'text',
975
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
976
- },
977
- ],
978
- isError: true,
979
- };
980
- }
981
- });
904
+ }, wrapHighRiskToolHandler('cancel_leave', async ({ id }) => {
905
+ await cancelLeave(id);
906
+ return textResponse(`Leave request ${id} cancelled successfully.`);
907
+ }));
982
908
  server.registerTool('approve_leave', {
983
909
  title: 'Approve Leave Request',
984
910
  description: 'Approve a pending leave request.',
@@ -1012,35 +938,16 @@ server.registerTool('approve_leave', {
1012
938
  });
1013
939
  server.registerTool('reject_leave', {
1014
940
  title: 'Reject Leave Request',
1015
- description: 'Reject a pending leave request.',
941
+ description: 'Reject a pending leave request. This operation requires confirmation.',
1016
942
  inputSchema: {
1017
943
  id: z.number().describe('The leave ID to reject'),
1018
944
  reason: z.string().max(500).optional().describe('Rejection reason'),
945
+ confirm: z.boolean().optional().describe('Set to true to confirm this operation'),
1019
946
  },
1020
- }, async ({ id, reason }) => {
1021
- try {
1022
- const leave = await rejectLeave(id, reason ? { reason } : undefined);
1023
- return {
1024
- content: [
1025
- {
1026
- type: 'text',
1027
- text: `Leave request rejected:\n\n${JSON.stringify(leave, null, 2)}`,
1028
- },
1029
- ],
1030
- };
1031
- }
1032
- catch (error) {
1033
- return {
1034
- content: [
1035
- {
1036
- type: 'text',
1037
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
1038
- },
1039
- ],
1040
- isError: true,
1041
- };
1042
- }
1043
- });
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
+ }));
1044
951
  // ============================================================================
1045
952
  // Attendance / Shift Tools
1046
953
  // ============================================================================
@@ -1194,34 +1101,15 @@ server.registerTool('update_shift', {
1194
1101
  });
1195
1102
  server.registerTool('delete_shift', {
1196
1103
  title: 'Delete Shift',
1197
- description: 'Delete a shift record.',
1104
+ description: 'Delete a shift record. This operation requires confirmation.',
1198
1105
  inputSchema: {
1199
1106
  id: z.number().describe('The shift ID to delete'),
1107
+ confirm: z.boolean().optional().describe('Set to true to confirm this operation'),
1200
1108
  },
1201
- }, async ({ id }) => {
1202
- try {
1203
- await deleteShift(id);
1204
- return {
1205
- content: [
1206
- {
1207
- type: 'text',
1208
- text: `Shift ${id} deleted successfully.`,
1209
- },
1210
- ],
1211
- };
1212
- }
1213
- catch (error) {
1214
- return {
1215
- content: [
1216
- {
1217
- type: 'text',
1218
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
1219
- },
1220
- ],
1221
- isError: true,
1222
- };
1223
- }
1224
- });
1109
+ }, wrapHighRiskToolHandler('delete_shift', async ({ id }) => {
1110
+ await deleteShift(id);
1111
+ return textResponse(`Shift ${id} deleted successfully.`);
1112
+ }));
1225
1113
  // ============================================================================
1226
1114
  // Document Tools (Read-Only)
1227
1115
  // ============================================================================
@@ -1334,6 +1222,18 @@ server.registerTool('get_document', {
1334
1222
  }, async ({ id }) => {
1335
1223
  try {
1336
1224
  const document = await getDocument(id);
1225
+ // Defensive check for undefined/null document
1226
+ if (!document) {
1227
+ return {
1228
+ content: [
1229
+ {
1230
+ type: 'text',
1231
+ text: `Error: Document with ID ${id} exists but returned no data. This may be a Factorial API issue.`,
1232
+ },
1233
+ ],
1234
+ isError: true,
1235
+ };
1236
+ }
1337
1237
  return {
1338
1238
  content: [
1339
1239
  {
@@ -1369,12 +1269,12 @@ server.registerTool('get_employee_documents', {
1369
1269
  // Create summary format aligned with list_documents tool
1370
1270
  const summary = result.data.map(d => ({
1371
1271
  id: d.id,
1372
- name: d.name,
1272
+ name: d.name ?? '[No name]',
1373
1273
  folder_id: d.folder_id,
1374
1274
  employee_id: d.employee_id,
1375
1275
  author_id: d.author_id,
1376
- mime_type: d.mime_type,
1377
- size_bytes: d.size_bytes,
1276
+ mime_type: d.mime_type ?? 'unknown',
1277
+ size_bytes: d.size_bytes ?? 0,
1378
1278
  }));
1379
1279
  return {
1380
1280
  content: [
@@ -1608,29 +1508,15 @@ server.registerTool('update_project', {
1608
1508
  });
1609
1509
  server.registerTool('delete_project', {
1610
1510
  title: 'Delete Project',
1611
- description: 'Delete a project. This is a high-risk operation.',
1511
+ description: 'Delete a project. This is a HIGH-RISK operation that requires confirmation.',
1612
1512
  inputSchema: {
1613
1513
  id: z.number().describe('The project ID to delete'),
1514
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
1614
1515
  },
1615
- }, async ({ id }) => {
1616
- try {
1617
- await deleteProject(id);
1618
- return {
1619
- content: [{ type: 'text', text: `Project ${id} deleted successfully.` }],
1620
- };
1621
- }
1622
- catch (error) {
1623
- return {
1624
- content: [
1625
- {
1626
- type: 'text',
1627
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
1628
- },
1629
- ],
1630
- isError: true,
1631
- };
1632
- }
1633
- });
1516
+ }, wrapHighRiskToolHandler('delete_project', async ({ id }) => {
1517
+ await deleteProject(id);
1518
+ return textResponse(`Project ${id} deleted successfully.`);
1519
+ }));
1634
1520
  server.registerTool('list_project_tasks', {
1635
1521
  title: 'List Project Tasks',
1636
1522
  description: 'Get tasks for a project.',
@@ -2067,29 +1953,15 @@ server.registerTool('update_training', {
2067
1953
  });
2068
1954
  server.registerTool('delete_training', {
2069
1955
  title: 'Delete Training',
2070
- description: 'Delete a training program.',
1956
+ description: 'Delete a training program. This is a HIGH-RISK operation that requires confirmation.',
2071
1957
  inputSchema: {
2072
1958
  id: z.number().describe('The training ID to delete'),
1959
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2073
1960
  },
2074
- }, async ({ id }) => {
2075
- try {
2076
- await deleteTraining(id);
2077
- return {
2078
- content: [{ type: 'text', text: `Training ${id} deleted successfully.` }],
2079
- };
2080
- }
2081
- catch (error) {
2082
- return {
2083
- content: [
2084
- {
2085
- type: 'text',
2086
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2087
- },
2088
- ],
2089
- isError: true,
2090
- };
2091
- }
2092
- });
1961
+ }, wrapHighRiskToolHandler('delete_training', async ({ id }) => {
1962
+ await deleteTraining(id);
1963
+ return textResponse(`Training ${id} deleted successfully.`);
1964
+ }));
2093
1965
  server.registerTool('list_training_sessions', {
2094
1966
  title: 'List Training Sessions',
2095
1967
  description: 'Get sessions for a training program.',
@@ -2594,29 +2466,15 @@ server.registerTool('update_job_posting', {
2594
2466
  });
2595
2467
  server.registerTool('delete_job_posting', {
2596
2468
  title: 'Delete Job Posting',
2597
- description: 'Delete a job posting.',
2469
+ description: 'Delete a job posting. This is a HIGH-RISK operation that requires confirmation.',
2598
2470
  inputSchema: {
2599
2471
  id: z.number().describe('Job posting ID to delete'),
2472
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2600
2473
  },
2601
- }, async ({ id }) => {
2602
- try {
2603
- await deleteJobPosting(id);
2604
- return {
2605
- content: [{ type: 'text', text: `Job posting ${id} deleted successfully.` }],
2606
- };
2607
- }
2608
- catch (error) {
2609
- return {
2610
- content: [
2611
- {
2612
- type: 'text',
2613
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2614
- },
2615
- ],
2616
- isError: true,
2617
- };
2618
- }
2619
- });
2474
+ }, wrapHighRiskToolHandler('delete_job_posting', async ({ id }) => {
2475
+ await deleteJobPosting(id);
2476
+ return textResponse(`Job posting ${id} deleted successfully.`);
2477
+ }));
2620
2478
  server.registerTool('list_candidates', {
2621
2479
  title: 'List Candidates',
2622
2480
  description: 'Get all candidates.',
@@ -2740,29 +2598,15 @@ server.registerTool('update_candidate', {
2740
2598
  });
2741
2599
  server.registerTool('delete_candidate', {
2742
2600
  title: 'Delete Candidate',
2743
- description: 'Delete a candidate.',
2601
+ description: 'Delete a candidate. This is a HIGH-RISK operation that requires confirmation.',
2744
2602
  inputSchema: {
2745
2603
  id: z.number().describe('Candidate ID to delete'),
2604
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2746
2605
  },
2747
- }, async ({ id }) => {
2748
- try {
2749
- await deleteCandidate(id);
2750
- return {
2751
- content: [{ type: 'text', text: `Candidate ${id} deleted successfully.` }],
2752
- };
2753
- }
2754
- catch (error) {
2755
- return {
2756
- content: [
2757
- {
2758
- type: 'text',
2759
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2760
- },
2761
- ],
2762
- isError: true,
2763
- };
2764
- }
2765
- });
2606
+ }, wrapHighRiskToolHandler('delete_candidate', async ({ id }) => {
2607
+ await deleteCandidate(id);
2608
+ return textResponse(`Candidate ${id} deleted successfully.`);
2609
+ }));
2766
2610
  server.registerTool('list_applications', {
2767
2611
  title: 'List Applications',
2768
2612
  description: 'Get job applications.',
@@ -2881,29 +2725,15 @@ server.registerTool('update_application', {
2881
2725
  });
2882
2726
  server.registerTool('delete_application', {
2883
2727
  title: 'Delete Application',
2884
- description: 'Delete a job application.',
2728
+ description: 'Delete a job application. This is a HIGH-RISK operation that requires confirmation.',
2885
2729
  inputSchema: {
2886
2730
  id: z.number().describe('Application ID to delete'),
2731
+ confirm: z.boolean().optional().describe('Set to true to confirm this high-risk operation'),
2887
2732
  },
2888
- }, async ({ id }) => {
2889
- try {
2890
- await deleteApplication(id);
2891
- return {
2892
- content: [{ type: 'text', text: `Application ${id} deleted successfully.` }],
2893
- };
2894
- }
2895
- catch (error) {
2896
- return {
2897
- content: [
2898
- {
2899
- type: 'text',
2900
- text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
2901
- },
2902
- ],
2903
- isError: true,
2904
- };
2905
- }
2906
- });
2733
+ }, wrapHighRiskToolHandler('delete_application', async ({ id }) => {
2734
+ await deleteApplication(id);
2735
+ return textResponse(`Application ${id} deleted successfully.`);
2736
+ }));
2907
2737
  server.registerTool('advance_application', {
2908
2738
  title: 'Advance Application',
2909
2739
  description: 'Move an application to the next hiring stage.',
@@ -3551,6 +3381,18 @@ Please provide:
3551
3381
  };
3552
3382
  });
3553
3383
  // ============================================================================
3384
+ // Graceful Shutdown
3385
+ // ============================================================================
3386
+ /**
3387
+ * Clean up resources on process termination
3388
+ */
3389
+ function shutdown() {
3390
+ cache.destroy();
3391
+ process.exit(0);
3392
+ }
3393
+ process.on('SIGINT', shutdown);
3394
+ process.on('SIGTERM', shutdown);
3395
+ // ============================================================================
3554
3396
  // Start Server
3555
3397
  // ============================================================================
3556
3398
  async function main() {