@procurementexpress.com/mcp 1.0.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.
Files changed (35) hide show
  1. package/.claude/skills/bump-version/SKILL.md +77 -0
  2. package/.claude/skills/commit/SKILL.md +73 -0
  3. package/.claude/skills/npm-publish/SKILL.md +65 -0
  4. package/.claude/skills/pex-approval-flows/SKILL.md +122 -0
  5. package/.claude/skills/pex-approval-flows/references/conditions.md +90 -0
  6. package/.claude/skills/pex-auth/SKILL.md +80 -0
  7. package/.claude/skills/pex-budgets/SKILL.md +73 -0
  8. package/.claude/skills/pex-companies/SKILL.md +113 -0
  9. package/.claude/skills/pex-departments/SKILL.md +61 -0
  10. package/.claude/skills/pex-invoices/SKILL.md +125 -0
  11. package/.claude/skills/pex-invoices/references/line-items.md +55 -0
  12. package/.claude/skills/pex-payments/SKILL.md +79 -0
  13. package/.claude/skills/pex-purchase-orders/SKILL.md +167 -0
  14. package/.claude/skills/pex-purchase-orders/references/line-items.md +53 -0
  15. package/.claude/skills/pex-purchase-orders/references/workflows.md +74 -0
  16. package/.claude/skills/pex-settings/SKILL.md +128 -0
  17. package/.claude/skills/pex-suppliers/SKILL.md +113 -0
  18. package/README.md +118 -25
  19. package/dist/api-client.d.ts +1 -0
  20. package/dist/api-client.js +3 -0
  21. package/dist/tools/approval-flows.js +130 -25
  22. package/dist/tools/budgets.js +30 -20
  23. package/dist/tools/comments.js +4 -4
  24. package/dist/tools/companies.js +57 -7
  25. package/dist/tools/departments.js +6 -6
  26. package/dist/tools/invoices.js +100 -31
  27. package/dist/tools/payments.js +45 -13
  28. package/dist/tools/products.js +10 -5
  29. package/dist/tools/purchase-orders.js +178 -35
  30. package/dist/tools/supplementary.js +57 -14
  31. package/dist/tools/suppliers.js +38 -19
  32. package/dist/tools/tax-rates.js +15 -9
  33. package/dist/tools/users.js +8 -5
  34. package/dist/tools/webhooks.js +59 -9
  35. package/package.json +5 -4
@@ -1,26 +1,30 @@
1
1
  import { z } from "zod";
2
2
  import { jsonResponse, withErrorHandling } from "../tool-helpers.js";
3
3
  const approvalConditionSchema = z.object({
4
- property: z.string().describe("Condition property (budget, department, supplier, requester, gross_amount, net_amount, custom_field_xxx)"),
5
- operator: z.string().describe("Operator: equals(0), not_equals(1), greater_than(2), less_than(3), contains(4), not_contains(5)"),
6
- value: z.string().describe("Condition value (single ID or comma-separated IDs)"),
7
- custom_field_id: z.number().int().optional().describe("Custom field ID (for custom_field properties)"),
4
+ id: z.number().int().optional().describe("Condition ID (for updates)"),
5
+ property: z.string().describe("Condition property: budget, department, supplier, requester, gross_amount, net_amount, or custom_field_<id>"),
6
+ operator: z.enum(["equals", "not_equals", "greater_than", "less_than", "is_any_of", "is_none_of", "exists", "not_exists", "contains", "not_contains"]).describe("Condition operator"),
7
+ value: z.string().describe("Condition value (single ID or comma-separated IDs for contains/not_contains)"),
8
+ custom_field_id: z.number().int().optional().describe("Custom field ID (when property is custom_field_<id>)"),
9
+ _destroy: z.boolean().optional().describe("Set true to remove this condition on update"),
8
10
  });
9
11
  const approvalStepSchema = z.object({
10
- step_no: z.number().int().describe("Step number"),
11
- all_should_approve: z.boolean().describe("Whether all approvers in step must approve"),
12
- approver_user_ids: z.array(z.number().int()).describe("Approver user IDs"),
13
- conditions: z.array(approvalConditionSchema).optional().describe("Step-level conditions"),
12
+ id: z.number().int().optional().describe("Step ID (for updates)"),
13
+ step_no: z.number().int().describe("Step number (execution order)"),
14
+ all_should_approve: z.boolean().describe("True = all approvers in step must approve; false = any one approver suffices"),
15
+ approver_user_ids: z.array(z.number().int()).describe("User IDs of approvers for this step"),
16
+ conditions: z.array(approvalConditionSchema).optional().describe("Step-level conditions (all must match for step to activate)"),
17
+ _destroy: z.boolean().optional().describe("Set true to remove this step on update"),
14
18
  });
15
19
  export function registerApprovalFlowTools(server, apiClient) {
16
20
  server.registerTool("list_approval_flows", {
17
- description: "List approval flows with search and pagination",
21
+ description: "List active approval flows with search and pagination. Returns paginated results with meta. Requires approval flows feature to be enabled.",
18
22
  inputSchema: {
19
- search: z.string().optional().describe("Search term"),
20
- page: z.number().int().positive().optional().describe("Page number"),
21
- per_page: z.number().int().positive().optional().describe("Results per page"),
22
- sort: z.string().optional().describe("Sort field"),
23
- direction: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
23
+ search: z.string().optional().describe("Search by flow name (case-insensitive)"),
24
+ page: z.number().int().positive().optional().describe("Page number (default: 1)"),
25
+ per_page: z.number().int().positive().optional().describe("Results per page (allowed: 10, 20, 50, 100)"),
26
+ sort: z.string().optional().describe("Sort column (e.g. 'id', 'name', 'created_at')"),
27
+ direction: z.enum(["asc", "desc"]).optional().describe("Sort direction (default: desc)"),
24
28
  },
25
29
  }, withErrorHandling(async (args) => {
26
30
  const params = new URLSearchParams();
@@ -49,13 +53,13 @@ export function registerApprovalFlowTools(server, apiClient) {
49
53
  return jsonResponse(flow);
50
54
  }));
51
55
  server.registerTool("create_approval_flow", {
52
- description: "Create an approval flow with steps, approvers, and conditions. document_type: 0=purchase_order, 1=invoice",
56
+ description: "Create an approval flow with steps, approvers, and conditions. document_type: 0=purchase_order, 1=invoice. Flow-level conditions determine which documents match this flow. Step-level conditions determine which steps activate.",
53
57
  inputSchema: {
54
58
  name: z.string().describe("Flow name"),
55
59
  document_type: z.number().int().min(0).max(1).describe("0=purchase_order, 1=invoice"),
56
- self_approval_allowed: z.boolean().optional().describe("Allow self-approval"),
57
- steps: z.array(approvalStepSchema).describe("Approval steps with approvers"),
58
- conditions: z.array(approvalConditionSchema).optional().describe("Flow-level conditions"),
60
+ self_approval_allowed: z.boolean().optional().describe("Allow PO creator to self-approve if they are an approver"),
61
+ steps: z.array(approvalStepSchema).describe("Approval steps with approvers (executed in step_no order)"),
62
+ conditions: z.array(approvalConditionSchema).optional().describe("Flow-level conditions (all must match for flow to apply)"),
59
63
  },
60
64
  }, withErrorHandling(async (args) => {
61
65
  const body = {
@@ -64,6 +68,7 @@ export function registerApprovalFlowTools(server, apiClient) {
64
68
  document_type: args.document_type,
65
69
  self_approval_allowed: args.self_approval_allowed,
66
70
  approval_steps_attributes: args.steps.map((step) => ({
71
+ ...(step.id ? { id: step.id } : {}),
67
72
  step_no: step.step_no,
68
73
  all_should_approve: step.all_should_approve,
69
74
  approval_step_approvers_attributes: step.approver_user_ids.map((uid) => ({
@@ -81,30 +86,84 @@ export function registerApprovalFlowTools(server, apiClient) {
81
86
  const flow = await apiClient.post(apiClient.buildPath("/approval_flows"), body);
82
87
  return jsonResponse(flow);
83
88
  }));
89
+ server.registerTool("update_approval_flow", {
90
+ description: "Update an existing approval flow. Include step/condition IDs to update existing items, omit ID for new items, use _destroy to remove.",
91
+ inputSchema: {
92
+ id: z.number().int().positive().describe("Approval Flow ID"),
93
+ name: z.string().optional().describe("Flow name"),
94
+ document_type: z.number().int().min(0).max(1).optional().describe("0=purchase_order, 1=invoice"),
95
+ self_approval_allowed: z.boolean().optional().describe("Allow self-approval"),
96
+ steps: z.array(approvalStepSchema).optional().describe("Approval steps"),
97
+ conditions: z.array(approvalConditionSchema).optional().describe("Flow-level conditions"),
98
+ },
99
+ }, withErrorHandling(async (args) => {
100
+ const { id, steps, conditions, ...flowData } = args;
101
+ const body = {
102
+ approval_flow: {
103
+ ...flowData,
104
+ ...(steps
105
+ ? {
106
+ approval_steps_attributes: steps.map((step) => ({
107
+ ...(step.id ? { id: step.id } : {}),
108
+ step_no: step.step_no,
109
+ all_should_approve: step.all_should_approve,
110
+ ...(step._destroy ? { _destroy: true } : {}),
111
+ approval_step_approvers_attributes: step.approver_user_ids.map((uid) => ({
112
+ user_id: uid,
113
+ })),
114
+ ...(step.conditions
115
+ ? { approval_conditions_attributes: step.conditions }
116
+ : {}),
117
+ })),
118
+ }
119
+ : {}),
120
+ ...(conditions
121
+ ? { approval_conditions_attributes: conditions }
122
+ : {}),
123
+ },
124
+ };
125
+ const flow = await apiClient.put(apiClient.buildPath(`/approval_flows/${id}`), body);
126
+ return jsonResponse(flow);
127
+ }));
84
128
  server.registerTool("delete_approval_flow", {
85
- description: "Delete an approval flow",
129
+ description: "Delete an approval flow permanently",
86
130
  inputSchema: { id: z.number().int().positive().describe("Approval Flow ID") },
87
131
  }, withErrorHandling(async (args) => {
88
132
  const result = await apiClient.delete(apiClient.buildPath(`/approval_flows/${args.id}`));
89
133
  return jsonResponse(result);
90
134
  }));
91
135
  server.registerTool("archive_approval_flow", {
92
- description: "Archive an approval flow",
136
+ description: "Archive an approval flow (soft delete — can be restored)",
93
137
  inputSchema: { id: z.number().int().positive().describe("Approval Flow ID") },
94
138
  }, withErrorHandling(async (args) => {
95
139
  const result = await apiClient.put(apiClient.buildPath(`/approval_flows/${args.id}/archive`));
96
140
  return jsonResponse(result);
97
141
  }));
142
+ server.registerTool("publish_approval_flow", {
143
+ description: "Publish an approval flow to make it active",
144
+ inputSchema: { id: z.number().int().positive().describe("Approval Flow ID") },
145
+ }, withErrorHandling(async (args) => {
146
+ const result = await apiClient.patch(apiClient.buildPath(`/approval_flows/${args.id}/publish`));
147
+ return jsonResponse(result);
148
+ }));
149
+ server.registerTool("unpublish_approval_flow", {
150
+ description: "Unpublish an approval flow to deactivate it",
151
+ inputSchema: { id: z.number().int().positive().describe("Approval Flow ID") },
152
+ }, withErrorHandling(async (args) => {
153
+ const result = await apiClient.patch(apiClient.buildPath(`/approval_flows/${args.id}/unpublish`));
154
+ return jsonResponse(result);
155
+ }));
98
156
  server.registerTool("list_approval_flow_runs", {
99
- description: "List runs for an approval flow with status and date filters",
157
+ description: "List approval flow runs (entities that went through this flow) with status and date filters",
100
158
  inputSchema: {
101
159
  id: z.number().int().positive().describe("Approval Flow ID"),
102
- status: z.enum(["in_progress", "completed", "rejected"]).optional().describe("Filter by status"),
160
+ status: z.enum(["in_progress", "completed", "rejected"]).optional().describe("Filter by run status"),
103
161
  keyword: z.string().optional().describe("Search keyword"),
104
- date_range: z.enum(["24h", "7d", "30d", "60d", "custom"]).optional().describe("Date range filter"),
105
- date_from: z.string().optional().describe("Start date (MM/DD/YYYY) for custom range"),
106
- date_to: z.string().optional().describe("End date (MM/DD/YYYY) for custom range"),
162
+ date_range: z.enum(["24h", "7d", "30d", "60d", "custom"]).optional().describe("Predefined date range filter"),
163
+ date_from: z.string().optional().describe("Start date for custom range"),
164
+ date_to: z.string().optional().describe("End date for custom range"),
107
165
  page: z.number().int().positive().optional().describe("Page number"),
166
+ per_page: z.number().int().positive().optional().describe("Results per page"),
108
167
  },
109
168
  }, withErrorHandling(async (args) => {
110
169
  const { id, ...filters } = args;
@@ -118,4 +177,50 @@ export function registerApprovalFlowTools(server, apiClient) {
118
177
  const result = await apiClient.get(path);
119
178
  return jsonResponse(result);
120
179
  }));
180
+ server.registerTool("get_approval_flow_entity", {
181
+ description: "Get details about a specific entity (purchase order or invoice) that went through an approval flow",
182
+ inputSchema: {
183
+ id: z.number().int().positive().describe("Approval Flow ID"),
184
+ entity_id: z.number().int().positive().describe("Entity ID (purchase order or invoice ID)"),
185
+ },
186
+ }, withErrorHandling(async (args) => {
187
+ const path = `${apiClient.buildPath(`/approval_flows/${args.id}/show_entity`)}?entity_id=${args.entity_id}`;
188
+ const result = await apiClient.get(path);
189
+ return jsonResponse(result);
190
+ }));
191
+ server.registerTool("list_approval_flow_versions", {
192
+ description: "List all version history of an approval flow",
193
+ inputSchema: {
194
+ id: z.number().int().positive().describe("Approval Flow ID"),
195
+ },
196
+ }, withErrorHandling(async (args) => {
197
+ const result = await apiClient.get(apiClient.buildPath(`/approval_flows/${args.id}/versions`));
198
+ return jsonResponse(result);
199
+ }));
200
+ server.registerTool("get_approval_flow_version_details", {
201
+ description: "Get full details of a specific version of an approval flow",
202
+ inputSchema: {
203
+ id: z.number().int().positive().describe("Approval Flow ID"),
204
+ version_id: z.number().int().positive().describe("Version ID"),
205
+ },
206
+ }, withErrorHandling(async (args) => {
207
+ const path = `${apiClient.buildPath(`/approval_flows/${args.id}/version_details`)}?version_id=${args.version_id}`;
208
+ const result = await apiClient.get(path);
209
+ return jsonResponse(result);
210
+ }));
211
+ server.registerTool("rerun_approval_flows", {
212
+ description: "Rerun approval flows for specific purchase orders and/or invoices. Useful when approval flow rules have changed and need to be reapplied.",
213
+ inputSchema: {
214
+ order_ids: z.array(z.number().int()).optional().describe("Purchase order IDs to rerun approval flows for"),
215
+ invoice_ids: z.array(z.number().int()).optional().describe("Invoice IDs to rerun approval flows for"),
216
+ },
217
+ }, withErrorHandling(async (args) => {
218
+ const body = {};
219
+ if (args.order_ids)
220
+ body.order_ids = args.order_ids;
221
+ if (args.invoice_ids)
222
+ body.invoice_ids = args.invoice_ids;
223
+ const result = await apiClient.post(apiClient.buildPath("/approval_flows/rerun_approval_flows"), body);
224
+ return jsonResponse(result);
225
+ }));
121
226
  }
@@ -1,31 +1,33 @@
1
1
  import { z } from "zod";
2
2
  import { jsonResponse, withErrorHandling } from "../tool-helpers.js";
3
+ const customFieldValueSchema = z.object({
4
+ id: z.number().int().optional().describe("Custom field value ID (for updates)"),
5
+ value: z.string().describe("Custom field value"),
6
+ custom_field_id: z.number().int().describe("Custom field ID"),
7
+ });
3
8
  export function registerBudgetTools(server, apiClient) {
4
9
  server.registerTool("list_budgets", {
5
- description: "List budgets with pagination. Filter by department, archived status, or active only",
10
+ description: "List budgets for the current company. Filter by department and/or archived status. Returns all matching budgets (no pagination).",
6
11
  inputSchema: {
7
- page: z.number().int().positive().optional().describe("Page number (default: 1)"),
8
- archived: z.boolean().optional().describe("Filter by archived status"),
9
- only_active: z.boolean().optional().describe("Show only active budgets"),
10
12
  department_id: z.number().int().optional().describe("Filter by department ID"),
13
+ archived: z.boolean().optional().describe("Filter by archived status (default: false)"),
14
+ show_mappings: z.boolean().optional().describe("Include third-party ID mappings in response"),
11
15
  },
12
16
  }, withErrorHandling(async (args) => {
13
17
  const params = new URLSearchParams();
14
- if (args.page)
15
- params.set("page", String(args.page));
16
- if (args.archived !== undefined)
17
- params.set("archived", String(args.archived));
18
- if (args.only_active)
19
- params.set("only_active", "true");
20
18
  if (args.department_id)
21
19
  params.set("department_id", String(args.department_id));
20
+ if (args.archived !== undefined)
21
+ params.set("archived", String(args.archived));
22
+ if (args.show_mappings)
23
+ params.set("show_mappings", "true");
22
24
  const query = params.toString();
23
25
  const path = `${apiClient.buildPath("/budgets")}${query ? `?${query}` : ""}`;
24
26
  const result = await apiClient.get(path);
25
27
  return jsonResponse(result);
26
28
  }));
27
29
  server.registerTool("get_budget", {
28
- description: "Get a specific budget by ID, including remaining amount",
30
+ description: "Get a specific budget by ID, including remaining amount and associated departments/approvers",
29
31
  inputSchema: {
30
32
  id: z.number().int().positive().describe("Budget ID"),
31
33
  },
@@ -34,19 +36,22 @@ export function registerBudgetTools(server, apiClient) {
34
36
  return jsonResponse(budget);
35
37
  }));
36
38
  server.registerTool("create_budget", {
37
- description: "Create a new budget",
39
+ description: "Create a new budget. Dates must match the company's date_format setting.",
38
40
  inputSchema: {
39
41
  name: z.string().describe("Budget name"),
40
42
  amount: z.number().describe("Budget amount"),
41
- currency_id: z.number().int().describe("Currency ID"),
42
- creator_id: z.number().int().describe("Creator user ID"),
43
+ currency_id: z.number().int().optional().describe("Currency ID (defaults to company currency)"),
44
+ creator_id: z.number().int().optional().describe("Creator user ID"),
43
45
  cost_code: z.string().optional().describe("Cost code"),
44
46
  cost_type: z.string().optional().describe("Cost type"),
45
- start_date: z.string().optional().describe("Start date (must match company date format)"),
46
- end_date: z.string().optional().describe("End date"),
47
- allow_anyone_to_approve_a_po: z.boolean().optional().describe("Allow anyone to approve"),
47
+ start_date: z.string().optional().describe("Start date (must match company date_format setting)"),
48
+ end_date: z.string().optional().describe("End date (must match company date_format setting)"),
49
+ allow_anyone_to_approve_a_po: z.boolean().optional().describe("Allow anyone to approve POs against this budget"),
50
+ chart_of_account_id: z.number().int().optional().describe("Chart of account ID (GL code)"),
51
+ qbo_class: z.string().optional().describe("QuickBooks class"),
48
52
  approver_ids: z.array(z.number().int()).optional().describe("Approver user IDs"),
49
- department_ids: z.array(z.number().int()).optional().describe("Department IDs"),
53
+ department_ids: z.array(z.number().int()).optional().describe("Department IDs to associate"),
54
+ custom_field_values_attributes: z.array(customFieldValueSchema).optional().describe("Budget-level custom field values"),
50
55
  },
51
56
  }, withErrorHandling(async (args) => {
52
57
  const budget = await apiClient.post(apiClient.buildPath("/budgets"), { budget: args });
@@ -58,12 +63,17 @@ export function registerBudgetTools(server, apiClient) {
58
63
  id: z.number().int().positive().describe("Budget ID"),
59
64
  name: z.string().optional().describe("Budget name"),
60
65
  amount: z.number().optional().describe("Budget amount"),
66
+ currency_id: z.number().int().optional().describe("Currency ID"),
61
67
  cost_code: z.string().optional().describe("Cost code"),
62
68
  cost_type: z.string().optional().describe("Cost type"),
63
- start_date: z.string().optional().describe("Start date"),
64
- end_date: z.string().optional().describe("End date"),
69
+ start_date: z.string().optional().describe("Start date (must match company date_format setting)"),
70
+ end_date: z.string().optional().describe("End date (must match company date_format setting)"),
71
+ allow_anyone_to_approve_a_po: z.boolean().optional().describe("Allow anyone to approve"),
72
+ chart_of_account_id: z.number().int().optional().describe("Chart of account ID"),
73
+ qbo_class: z.string().optional().describe("QuickBooks class"),
65
74
  approver_ids: z.array(z.number().int()).optional().describe("Approver user IDs"),
66
75
  department_ids: z.array(z.number().int()).optional().describe("Department IDs"),
76
+ custom_field_values_attributes: z.array(customFieldValueSchema).optional().describe("Budget-level custom field values"),
67
77
  },
68
78
  }, withErrorHandling(async (args) => {
69
79
  const { id, ...data } = args;
@@ -2,23 +2,23 @@ import { z } from "zod";
2
2
  import { jsonResponse, withErrorHandling } from "../tool-helpers.js";
3
3
  export function registerCommentTools(server, apiClient) {
4
4
  server.registerTool("add_purchase_order_comment", {
5
- description: "Add a comment to a purchase order",
5
+ description: "Add a comment to a purchase order. The comment creator is set to the authenticated user.",
6
6
  inputSchema: {
7
7
  purchase_order_id: z.number().int().positive().describe("Purchase Order ID"),
8
8
  comment: z.string().describe("Comment text"),
9
9
  },
10
10
  }, withErrorHandling(async (args) => {
11
- const result = await apiClient.post(apiClient.buildPath(`/purchase_order/${args.purchase_order_id}/comments`), { comment: args.comment });
11
+ const result = await apiClient.post(apiClient.buildPath(`/purchase_orders/${args.purchase_order_id}/comments`), { comment: args.comment });
12
12
  return jsonResponse(result);
13
13
  }));
14
14
  server.registerTool("add_invoice_comment", {
15
- description: "Add a comment to an invoice",
15
+ description: "Add a comment to an invoice. The comment creator is set to the authenticated user.",
16
16
  inputSchema: {
17
17
  invoice_id: z.number().int().positive().describe("Invoice ID"),
18
18
  comment: z.string().describe("Comment text"),
19
19
  },
20
20
  }, withErrorHandling(async (args) => {
21
- const result = await apiClient.post(apiClient.buildPath(`/invoices/${args.invoice_id}/create_comment`), { comment: args.comment });
21
+ const result = await apiClient.post(apiClient.buildPath(`/invoices/${args.invoice_id}/create_comment`), { invoice_comments: { comment: args.comment } });
22
22
  return jsonResponse(result);
23
23
  }));
24
24
  }
@@ -9,7 +9,7 @@ export function registerCompanyTools(server, apiClient) {
9
9
  return jsonResponse(companies);
10
10
  }));
11
11
  server.registerTool("get_company", {
12
- description: "Get company details including settings, custom fields, and supported currencies",
12
+ description: "Get company details by ID including settings, custom fields, and supported currencies",
13
13
  inputSchema: {
14
14
  id: z.number().int().positive().describe("Company ID"),
15
15
  },
@@ -17,6 +17,13 @@ export function registerCompanyTools(server, apiClient) {
17
17
  const company = await apiClient.get(apiClient.buildPath(`/companies/${args.id}`));
18
18
  return jsonResponse(company);
19
19
  }));
20
+ server.registerTool("get_company_details", {
21
+ description: "Get details for the currently active company (set via set_active_company or PROCUREMENTEXPRESS_COMPANY_ID)",
22
+ inputSchema: {},
23
+ }, withErrorHandling(async () => {
24
+ const company = await apiClient.get(apiClient.buildPath("/companies/details"));
25
+ return jsonResponse(company);
26
+ }));
20
27
  server.registerTool("set_active_company", {
21
28
  description: "Set the active company ID for subsequent API calls. Required before most operations.",
22
29
  inputSchema: {
@@ -27,36 +34,43 @@ export function registerCompanyTools(server, apiClient) {
27
34
  return textResponse(`Active company set to ${args.company_id}`);
28
35
  }));
29
36
  server.registerTool("list_approvers", {
30
- description: "List approvers filtered by department (respects auto-approval settings)",
37
+ description: "List approvers for the current company. Optionally filter by department. Returns empty if company uses approval flows or has no unassigned budgets.",
31
38
  inputSchema: {
32
- department_id: z.number().int().positive().describe("Department ID to filter approvers"),
39
+ department_id: z.number().int().optional().describe("Filter approvers by department ID"),
33
40
  },
34
41
  }, withErrorHandling(async (args) => {
35
- const approvers = await apiClient.get(`${apiClient.buildPath("/companies/approvers")}?department_id=${args.department_id}`);
42
+ const params = new URLSearchParams();
43
+ if (args.department_id)
44
+ params.set("department_id", String(args.department_id));
45
+ const query = params.toString();
46
+ const path = `${apiClient.buildPath("/companies/approvers")}${query ? `?${query}` : ""}`;
47
+ const approvers = await apiClient.get(path);
36
48
  return jsonResponse(approvers);
37
49
  }));
38
50
  server.registerTool("list_all_approvers", {
39
- description: "List all approvers regardless of auto-approval routing",
51
+ description: "List all approvers for the current company regardless of auto-approval routing",
40
52
  inputSchema: {},
41
53
  }, withErrorHandling(async () => {
42
54
  const approvers = await apiClient.get(apiClient.buildPath("/companies/all_approvers"));
43
55
  return jsonResponse(approvers);
44
56
  }));
45
57
  server.registerTool("list_employees", {
46
- description: "List all employees of the current company with their roles",
58
+ description: "List all active employees of the current company with their roles (companyadmin role required)",
47
59
  inputSchema: {},
48
60
  }, withErrorHandling(async () => {
49
61
  const employees = await apiClient.get(apiClient.buildPath("/companies/employees"));
50
62
  return jsonResponse(employees);
51
63
  }));
52
64
  server.registerTool("invite_user", {
53
- description: "Invite a user to the company. Roles: companyadmin, approver, finance, teammember",
65
+ description: "Invite a user to the company. Roles: companyadmin, approver, finance, teammember. Requires available invite slots on the company plan.",
54
66
  inputSchema: {
55
67
  email: z.string().email().describe("Email address to invite"),
56
68
  name: z.string().describe("Name of the user"),
57
69
  roles: z
58
70
  .array(z.enum(["companyadmin", "approver", "finance", "teammember"]))
59
71
  .describe("Roles to assign"),
72
+ approval_limit: z.number().optional().describe("Approval limit amount (default: 0)"),
73
+ department_ids: z.array(z.number().int()).optional().describe("Department IDs to assign the user to"),
60
74
  },
61
75
  }, withErrorHandling(async (args) => {
62
76
  const result = await apiClient.post(apiClient.buildPath("/companies/send_user_invite"), {
@@ -64,4 +78,40 @@ export function registerCompanyTools(server, apiClient) {
64
78
  });
65
79
  return jsonResponse(result);
66
80
  }));
81
+ server.registerTool("get_invite_limit", {
82
+ description: "Get the remaining invite slots for the current company",
83
+ inputSchema: {},
84
+ }, withErrorHandling(async () => {
85
+ const result = await apiClient.get(apiClient.buildPath("/companies/invite_limit_left"));
86
+ return jsonResponse(result);
87
+ }));
88
+ server.registerTool("list_pending_invites", {
89
+ description: "List pending user invitations for the current company (companyadmin role required)",
90
+ inputSchema: {},
91
+ }, withErrorHandling(async () => {
92
+ const result = await apiClient.get(apiClient.buildPath("/companies/pending_invites"));
93
+ return jsonResponse(result);
94
+ }));
95
+ server.registerTool("cancel_invite", {
96
+ description: "Cancel a pending user invitation (companyadmin role required)",
97
+ inputSchema: {
98
+ token: z.string().describe("Invite token from the pending invite"),
99
+ },
100
+ }, withErrorHandling(async (args) => {
101
+ const result = await apiClient.post(apiClient.buildPath("/companies/cancel_invite"), {
102
+ token: args.token,
103
+ });
104
+ return jsonResponse(result);
105
+ }));
106
+ server.registerTool("resend_invite", {
107
+ description: "Resend a pending user invitation email (companyadmin role required)",
108
+ inputSchema: {
109
+ token: z.string().describe("Invite token from the pending invite"),
110
+ },
111
+ }, withErrorHandling(async (args) => {
112
+ const result = await apiClient.post(apiClient.buildPath("/companies/resend_invite"), {
113
+ token: args.token,
114
+ });
115
+ return jsonResponse(result);
116
+ }));
67
117
  }
@@ -2,10 +2,10 @@ import { z } from "zod";
2
2
  import { jsonResponse, withErrorHandling } from "../tool-helpers.js";
3
3
  export function registerDepartmentTools(server, apiClient) {
4
4
  server.registerTool("list_departments", {
5
- description: "List departments with optional filters for archived status and company-specific",
5
+ description: "List departments. By default returns only departments the current user has access to. Set company_specific=true to list all company departments regardless of user access.",
6
6
  inputSchema: {
7
- archived: z.boolean().optional().describe("Filter by archived status"),
8
- company_specific: z.boolean().optional().describe("Show only company-specific departments"),
7
+ archived: z.boolean().optional().describe("Filter by archived status (default: false)"),
8
+ company_specific: z.boolean().optional().describe("True to list all company departments, false/omit for user's departments only"),
9
9
  },
10
10
  }, withErrorHandling(async (args) => {
11
11
  const params = new URLSearchParams();
@@ -31,9 +31,9 @@ export function registerDepartmentTools(server, apiClient) {
31
31
  description: "Create a new department",
32
32
  inputSchema: {
33
33
  name: z.string().describe("Department name"),
34
- contact_person: z.string().optional().describe("Contact person"),
34
+ contact_person: z.string().optional().describe("Contact person name"),
35
35
  phone_number: z.string().optional().describe("Phone number"),
36
- email: z.string().email().optional().describe("Email"),
36
+ email: z.string().optional().describe("Email address"),
37
37
  address: z.string().optional().describe("Address"),
38
38
  tax_number: z.string().optional().describe("Tax number"),
39
39
  budget_ids: z.array(z.number().int()).optional().describe("Budget IDs to associate"),
@@ -53,7 +53,7 @@ export function registerDepartmentTools(server, apiClient) {
53
53
  archived: z.boolean().optional().describe("Archive status"),
54
54
  contact_person: z.string().optional().describe("Contact person"),
55
55
  phone_number: z.string().optional().describe("Phone number"),
56
- email: z.string().email().optional().describe("Email"),
56
+ email: z.string().optional().describe("Email"),
57
57
  address: z.string().optional().describe("Address"),
58
58
  tax_number: z.string().optional().describe("Tax number"),
59
59
  budget_ids: z.array(z.number().int()).optional().describe("Budget IDs"),