@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,81 +1,188 @@
1
1
  import { z } from "zod";
2
2
  import { jsonResponse, textResponse, 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
  const lineItemSchema = z.object({
9
+ id: z.number().int().optional().describe("Line item ID (for updates)"),
4
10
  description: z.string().describe("Item description"),
5
11
  quantity: z.number().describe("Quantity"),
6
12
  unit_price: z.number().describe("Unit price"),
7
13
  budget_id: z.number().int().optional().describe("Budget ID"),
8
- vat: z.number().optional().describe("VAT amount"),
9
- item_number: z.string().optional().describe("Item number"),
10
- sequence_no: z.number().int().optional().describe("Sequence number"),
14
+ vat: z.number().optional().describe("VAT/tax percentage"),
11
15
  tax_rate_id: z.number().int().optional().describe("Tax rate ID"),
16
+ item_number: z.string().optional().describe("Item number"),
17
+ sequence_no: z.number().int().optional().describe("Sequence number for ordering"),
18
+ department_id: z.number().int().optional().describe("Department ID for the line item"),
19
+ product_id: z.number().int().optional().describe("Product ID"),
20
+ chart_of_account_id: z.number().int().optional().describe("Chart of account ID (GL code)"),
21
+ qbo_customer_id: z.number().int().optional().describe("QuickBooks customer ID"),
22
+ quickbooks_class_id: z.number().int().optional().describe("QuickBooks class ID"),
23
+ qbo_line_description: z.string().optional().describe("QuickBooks line description override"),
24
+ archived: z.boolean().optional().describe("Whether the line item is archived"),
25
+ _destroy: z.boolean().optional().describe("Set true to remove this line item on update"),
26
+ custom_field_values_attributes: z.array(customFieldValueSchema).optional().describe("Custom field values for this line item"),
12
27
  });
13
28
  export function registerPurchaseOrderTools(server, apiClient) {
14
29
  server.registerTool("list_purchase_orders", {
15
- description: "List purchase orders with pagination and search",
30
+ description: "List purchase orders with pagination, search, and filters. Returns paginated results with meta info (current_page, next_page, prev_page, total_pages, total_count).",
16
31
  inputSchema: {
17
- orders_page: z.number().int().positive().optional().describe("Page number (default: 1)"),
18
- search: z.string().optional().describe("Search term"),
19
- requests: z.boolean().optional().describe("Set to true to list pending approval requests"),
20
- sort: z.string().optional().describe("Sort field"),
21
- direction: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
32
+ page: z.number().int().positive().optional().describe("Page number (default: 1). Use 'orders_page' as alias."),
33
+ search: z.string().optional().describe("Search term — matches PO number, supplier name, notes, line item descriptions"),
34
+ status: z
35
+ .enum(["draft", "pending", "approved", "rejected", "cancelled", "paid"])
36
+ .optional()
37
+ .describe("Filter by PO status"),
38
+ delivery_status: z
39
+ .enum(["not_delivered", "partially_delivered", "complete_delivered"])
40
+ .optional()
41
+ .describe("Filter by delivery status"),
42
+ payment_status: z
43
+ .enum(["unpaid", "partially_paid", "paid", "invoice_received"])
44
+ .optional()
45
+ .describe("Filter by payment status"),
46
+ supplier_id: z.number().int().optional().describe("Filter by supplier ID"),
47
+ requester_id: z.number().int().optional().describe("Filter by creator/requester user ID"),
48
+ budget_id: z.number().int().optional().describe("Filter by budget ID"),
49
+ filter_dept_id: z.number().int().optional().describe("Filter by department ID"),
50
+ approver_id: z.number().int().optional().describe("Filter by approver user ID"),
51
+ archived: z.boolean().optional().describe("Filter archived POs (default: false)"),
52
+ date_filter: z
53
+ .enum(["current_month", "current_year", "last_month", "last_year"])
54
+ .optional()
55
+ .describe("Predefined date range filter"),
56
+ from: z.string().optional().describe("Custom date range start (format depends on company date_format setting)"),
57
+ to: z.string().optional().describe("Custom date range end"),
58
+ updated_after: z.string().optional().describe("ISO datetime — only return POs updated after this timestamp (includes line item and custom field changes)"),
59
+ sort: z.string().optional().describe("Sort column (e.g. 'submitted_on', 'total_gross_amount', 'suppliers.name')"),
60
+ direction: z.enum(["asc", "desc"]).optional().describe("Sort direction (default: desc)"),
61
+ requests: z.boolean().optional().describe("Set true to include pending approval requests"),
62
+ bell: z.boolean().optional().describe("Set true with requests=true to show only bell notification items"),
22
63
  },
23
64
  }, withErrorHandling(async (args) => {
24
65
  const params = new URLSearchParams();
25
- if (args.orders_page)
26
- params.set("orders_page", String(args.orders_page));
66
+ if (args.page)
67
+ params.set("orders_page", String(args.page));
27
68
  if (args.search)
28
69
  params.set("search", args.search);
29
- if (args.requests)
30
- params.set("requests", "true");
70
+ if (args.status)
71
+ params.set("status", args.status);
72
+ if (args.delivery_status)
73
+ params.set("delivery_status", args.delivery_status);
74
+ if (args.payment_status)
75
+ params.set("payment_status", args.payment_status);
76
+ if (args.supplier_id)
77
+ params.set("supplier_id", String(args.supplier_id));
78
+ if (args.requester_id)
79
+ params.set("requester_id", String(args.requester_id));
80
+ if (args.budget_id)
81
+ params.set("budget_id", String(args.budget_id));
82
+ if (args.filter_dept_id)
83
+ params.set("filter_dept_id", String(args.filter_dept_id));
84
+ if (args.approver_id)
85
+ params.set("approver_id", String(args.approver_id));
86
+ if (args.archived !== undefined)
87
+ params.set("archived", String(args.archived));
88
+ if (args.date_filter)
89
+ params.set("date_filter", args.date_filter);
90
+ if (args.from)
91
+ params.set("from", args.from);
92
+ if (args.to)
93
+ params.set("to", args.to);
94
+ if (args.updated_after)
95
+ params.set("updated_after", args.updated_after);
31
96
  if (args.sort)
32
97
  params.set("sort", args.sort);
33
98
  if (args.direction)
34
99
  params.set("direction", args.direction);
100
+ if (args.requests)
101
+ params.set("requests", "true");
102
+ if (args.bell)
103
+ params.set("bell", "true");
35
104
  const query = params.toString();
36
105
  const path = `${apiClient.buildPath("/purchase_orders")}${query ? `?${query}` : ""}`;
37
106
  const result = await apiClient.get(path);
38
107
  return jsonResponse(result);
39
108
  }));
40
109
  server.registerTool("get_purchase_order", {
41
- description: "Get purchase order details including line items, comments, approvals, and status flags",
110
+ description: "Get purchase order details including line items, comments, approvals, and status flags. The ID parameter accepts a numeric ID, approval-key, or slug.",
42
111
  inputSchema: {
43
- id: z.number().int().positive().describe("Purchase Order ID"),
112
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
44
113
  },
45
114
  }, withErrorHandling(async (args) => {
46
115
  const po = await apiClient.get(apiClient.buildPath(`/purchase_orders/${args.id}`));
47
116
  return jsonResponse(po);
48
117
  }));
49
118
  server.registerTool("create_purchase_order", {
50
- description: "Create a new purchase order. Use commit='Send' to submit or 'Draft' to save as draft",
119
+ description: "Create a new purchase order. Use commit='Send' to submit for approval or 'Draft' to save as draft. At least one line item is required.",
51
120
  inputSchema: {
52
121
  commit: z.enum(["Send", "Draft"]).describe("'Send' to submit for approval, 'Draft' to save as draft"),
53
122
  department_id: z.number().int().optional().describe("Department ID"),
54
123
  creator_id: z.number().int().describe("Creator user ID"),
55
- supplier_id: z.number().int().optional().describe("Supplier ID"),
56
- supplier_name: z.string().optional().describe("Supplier name (for new suppliers)"),
57
- currency_id: z.number().int().describe("Currency ID"),
58
- notes: z.string().optional().describe("PO notes"),
124
+ supplier_id: z.number().int().optional().describe("Existing supplier ID"),
125
+ supplier_name: z.string().optional().describe("Supplier name (used when supplier_id is not provided)"),
126
+ new_supplier_name: z.string().optional().describe("Create a new supplier with this name"),
127
+ currency_id: z.number().int().optional().describe("Currency ID (defaults to company/user currency)"),
128
+ iso_code: z.string().optional().describe("Currency ISO code (e.g. 'USD') — alternative to currency_id"),
129
+ notes: z.string().optional().describe("PO notes/description"),
130
+ submitted_on: z.string().optional().describe("Submission date (format depends on company date_format)"),
131
+ on_behalf_of: z.number().int().optional().describe("Create PO on behalf of this user ID (companyadmin only, user must be active employee)"),
59
132
  line_items: z.array(lineItemSchema).min(1).describe("Line items (at least one required)"),
60
- approver_list: z.array(z.number().int()).optional().describe("Approver IDs"),
133
+ approver_list: z.array(z.number().int()).optional().describe("Approver user IDs"),
134
+ custom_field_values_attributes: z.array(customFieldValueSchema).optional().describe("PO-level custom field values"),
61
135
  },
62
136
  }, withErrorHandling(async (args) => {
63
- const { commit, line_items, approver_list, ...poData } = args;
137
+ const { commit, line_items, approver_list, iso_code, on_behalf_of, custom_field_values_attributes, ...poData } = args;
64
138
  const body = {
65
139
  commit,
66
140
  purchase_order: {
67
141
  ...poData,
142
+ ...(iso_code ? { iso_code } : {}),
143
+ ...(on_behalf_of ? { on_behalf_of } : {}),
68
144
  purchase_order_items_attributes: line_items,
69
145
  ...(approver_list ? { approver_list } : {}),
146
+ ...(custom_field_values_attributes ? { custom_field_values_attributes } : {}),
70
147
  },
71
148
  };
72
149
  const po = await apiClient.post(apiClient.buildPath("/purchase_orders"), body);
73
150
  return jsonResponse(po);
74
151
  }));
152
+ server.registerTool("update_purchase_order", {
153
+ description: "Update an existing purchase order. Use commit='Send' to submit a draft for approval. The ID accepts numeric ID, approval-key, or slug.",
154
+ inputSchema: {
155
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
156
+ commit: z.enum(["Send", "Draft"]).optional().describe("'Send' to submit draft for approval"),
157
+ department_id: z.number().int().optional().describe("Department ID"),
158
+ supplier_id: z.number().int().optional().describe("Supplier ID"),
159
+ supplier_name: z.string().optional().describe("Supplier name"),
160
+ new_supplier_name: z.string().optional().describe("Create a new supplier with this name"),
161
+ currency_id: z.number().int().optional().describe("Currency ID"),
162
+ notes: z.string().optional().describe("PO notes"),
163
+ submitted_on: z.string().optional().describe("Submission date"),
164
+ line_items: z.array(lineItemSchema).optional().describe("Line items (include id to update existing, _destroy to remove)"),
165
+ approver_list: z.array(z.number().int()).optional().describe("Approver user IDs"),
166
+ custom_field_values_attributes: z.array(customFieldValueSchema).optional().describe("PO-level custom field values"),
167
+ },
168
+ }, withErrorHandling(async (args) => {
169
+ const { id, commit, line_items, approver_list, custom_field_values_attributes, ...poData } = args;
170
+ const body = {
171
+ ...(commit ? { commit } : {}),
172
+ purchase_order: {
173
+ ...poData,
174
+ ...(line_items ? { purchase_order_items_attributes: line_items } : {}),
175
+ ...(approver_list ? { approver_list } : {}),
176
+ ...(custom_field_values_attributes ? { custom_field_values_attributes } : {}),
177
+ },
178
+ };
179
+ const po = await apiClient.put(apiClient.buildPath(`/purchase_orders/${id}`), body);
180
+ return jsonResponse(po);
181
+ }));
75
182
  server.registerTool("approve_purchase_order", {
76
183
  description: "Approve a purchase order using the accept token from the approver request",
77
184
  inputSchema: {
78
- id: z.number().int().positive().describe("Purchase Order ID"),
185
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
79
186
  token: z.string().describe("Accept token from the approver request"),
80
187
  },
81
188
  }, withErrorHandling(async (args) => {
@@ -85,7 +192,7 @@ export function registerPurchaseOrderTools(server, apiClient) {
85
192
  server.registerTool("reject_purchase_order", {
86
193
  description: "Reject a purchase order using the reject token from the approver request",
87
194
  inputSchema: {
88
- id: z.number().int().positive().describe("Purchase Order ID"),
195
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
89
196
  token: z.string().describe("Reject token from the approver request"),
90
197
  },
91
198
  }, withErrorHandling(async (args) => {
@@ -93,50 +200,68 @@ export function registerPurchaseOrderTools(server, apiClient) {
93
200
  return jsonResponse(result);
94
201
  }));
95
202
  server.registerTool("override_and_approve_purchase_order", {
96
- description: "Override and approve a purchase order (finance users only, no token required)",
203
+ description: "Override and approve a purchase order (finance role required, no token needed)",
97
204
  inputSchema: {
98
- id: z.number().int().positive().describe("Purchase Order ID"),
205
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
99
206
  },
100
207
  }, withErrorHandling(async (args) => {
101
208
  const result = await apiClient.get(apiClient.buildPath(`/purchase_orders/${args.id}/override_and_approve`));
102
209
  return jsonResponse(result);
103
210
  }));
104
211
  server.registerTool("cancel_purchase_order", {
105
- description: "Cancel a purchase order",
212
+ description: "Cancel a purchase order (requires cancel permission)",
106
213
  inputSchema: {
107
- id: z.number().int().positive().describe("Purchase Order ID"),
214
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
108
215
  },
109
216
  }, withErrorHandling(async (args) => {
110
217
  const result = await apiClient.post(apiClient.buildPath(`/purchase_orders/${args.id}/cancel`));
111
218
  return jsonResponse(result);
112
219
  }));
113
220
  server.registerTool("archive_purchase_order", {
114
- description: "Archive a purchase order",
221
+ description: "Toggle archive status of a purchase order (finance role required). Calling again will dearchive.",
115
222
  inputSchema: {
116
- id: z.number().int().positive().describe("Purchase Order ID"),
223
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
117
224
  },
118
225
  }, withErrorHandling(async (args) => {
119
226
  const result = await apiClient.post(apiClient.buildPath(`/purchase_orders/${args.id}/archive`));
120
227
  return jsonResponse(result);
121
228
  }));
229
+ server.registerTool("delete_purchase_order", {
230
+ description: "Permanently delete a purchase order (requires destroy permission)",
231
+ inputSchema: {
232
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
233
+ },
234
+ }, withErrorHandling(async (args) => {
235
+ const result = await apiClient.delete(apiClient.buildPath(`/purchase_orders/${args.id}/delete`));
236
+ return jsonResponse(result);
237
+ }));
238
+ server.registerTool("generate_purchase_order_pdf", {
239
+ description: "Generate a PDF for a purchase order and return a download link",
240
+ inputSchema: {
241
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
242
+ },
243
+ }, withErrorHandling(async (args) => {
244
+ const result = await apiClient.get(apiClient.buildPath(`/purchase_orders/${args.id}/generate_pdf`));
245
+ return jsonResponse(result);
246
+ }));
122
247
  server.registerTool("get_pending_request_count", {
123
- description: "Get the count of pending approval requests",
248
+ description: "Get the count of pending approval requests for the current user",
124
249
  inputSchema: {},
125
250
  }, withErrorHandling(async () => {
126
251
  const result = await apiClient.get(apiClient.buildPath("/purchase_orders/pending_request_count"));
127
252
  return textResponse(`Pending requests: ${result.total_pending_request}`);
128
253
  }));
129
254
  server.registerTool("receive_purchase_order_items", {
130
- description: "Mark items as received for a purchase order",
255
+ description: "Mark line items as received (partial or full delivery) for a purchase order",
131
256
  inputSchema: {
132
- id: z.number().int().positive().describe("Purchase Order ID"),
257
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
133
258
  items: z
134
259
  .array(z.object({
135
- id: z.number().int().describe("PO item ID"),
260
+ id: z.number().int().describe("PO line item ID"),
136
261
  quantity: z.number().describe("Quantity received"),
137
262
  }))
138
263
  .describe("Items with received quantities"),
139
- delivered_on: z.string().describe("Delivery date"),
264
+ delivered_on: z.string().describe("Delivery date (format depends on company date_format)"),
140
265
  notes: z.string().optional().describe("Delivery notes"),
141
266
  },
142
267
  }, withErrorHandling(async (args) => {
@@ -149,4 +274,22 @@ export function registerPurchaseOrderTools(server, apiClient) {
149
274
  });
150
275
  return jsonResponse(result);
151
276
  }));
277
+ server.registerTool("cancel_receiving_items", {
278
+ description: "Cancel all received deliveries for a purchase order, reverting to not delivered status",
279
+ inputSchema: {
280
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
281
+ },
282
+ }, withErrorHandling(async (args) => {
283
+ const result = await apiClient.post(apiClient.buildPath(`/purchase_orders/${args.id}/cancel_receiving_items`));
284
+ return jsonResponse(result);
285
+ }));
286
+ server.registerTool("complete_purchase_order_delivery", {
287
+ description: "Mark a purchase order as fully delivered",
288
+ inputSchema: {
289
+ id: z.string().describe("Purchase Order ID, approval-key, or slug"),
290
+ },
291
+ }, withErrorHandling(async (args) => {
292
+ const result = await apiClient.post(apiClient.buildPath(`/purchase_orders/${args.id}/complete_delivery`));
293
+ return jsonResponse(result);
294
+ }));
152
295
  }
@@ -2,47 +2,92 @@ import { z } from "zod";
2
2
  import { jsonResponse, withErrorHandling } from "../tool-helpers.js";
3
3
  export function registerSupplementaryTools(server, apiClient) {
4
4
  server.registerTool("list_chart_of_accounts", {
5
- description: "List chart of accounts with optional search",
5
+ description: "List chart of accounts (GL codes) with pagination and optional search",
6
6
  inputSchema: {
7
- search: z.string().optional().describe("Search term"),
7
+ search: z.string().optional().describe("Search by account name or code"),
8
+ page: z.number().int().positive().optional().describe("Page number (default: 1)"),
9
+ per_page: z.number().int().positive().optional().describe("Results per page (default: company setting)"),
8
10
  },
9
11
  }, withErrorHandling(async (args) => {
10
12
  const params = new URLSearchParams();
11
13
  if (args.search)
12
14
  params.set("search", args.search);
15
+ if (args.page)
16
+ params.set("page", String(args.page));
17
+ if (args.per_page)
18
+ params.set("per_page", String(args.per_page));
13
19
  const query = params.toString();
14
20
  const path = `${apiClient.buildPath("/chart_of_accounts")}${query ? `?${query}` : ""}`;
15
21
  const result = await apiClient.get(path);
16
22
  return jsonResponse(result);
17
23
  }));
24
+ server.registerTool("get_chart_of_account", {
25
+ description: "Get a specific chart of account by ID",
26
+ inputSchema: {
27
+ id: z.number().int().positive().describe("Chart of Account ID"),
28
+ },
29
+ }, withErrorHandling(async (args) => {
30
+ const result = await apiClient.get(apiClient.buildPath(`/chart_of_accounts/${args.id}`));
31
+ return jsonResponse(result);
32
+ }));
18
33
  server.registerTool("list_qbo_customers", {
19
- description: "List QuickBooks customers with optional search",
34
+ description: "List QuickBooks customers with pagination and optional search",
20
35
  inputSchema: {
21
- search: z.string().optional().describe("Search term"),
36
+ search: z.string().optional().describe("Search by customer name"),
37
+ page: z.number().int().positive().optional().describe("Page number (default: 1)"),
38
+ per_page: z.number().int().positive().optional().describe("Results per page"),
22
39
  },
23
40
  }, withErrorHandling(async (args) => {
24
41
  const params = new URLSearchParams();
25
42
  if (args.search)
26
43
  params.set("search", args.search);
44
+ if (args.page)
45
+ params.set("page", String(args.page));
46
+ if (args.per_page)
47
+ params.set("per_page", String(args.per_page));
27
48
  const query = params.toString();
28
49
  const path = `${apiClient.buildPath("/qbo_customers")}${query ? `?${query}` : ""}`;
29
50
  const result = await apiClient.get(path);
30
51
  return jsonResponse(result);
31
52
  }));
53
+ server.registerTool("get_qbo_customer", {
54
+ description: "Get a specific QuickBooks customer by ID",
55
+ inputSchema: {
56
+ id: z.number().int().positive().describe("QBO Customer ID"),
57
+ },
58
+ }, withErrorHandling(async (args) => {
59
+ const result = await apiClient.get(apiClient.buildPath(`/qbo_customers/${args.id}`));
60
+ return jsonResponse(result);
61
+ }));
32
62
  server.registerTool("list_qbo_classes", {
33
- description: "List QuickBooks classes with optional search",
63
+ description: "List QuickBooks classes with pagination and optional search",
34
64
  inputSchema: {
35
- search: z.string().optional().describe("Search term"),
65
+ search: z.string().optional().describe("Search by class name"),
66
+ page: z.number().int().positive().optional().describe("Page number (default: 1)"),
67
+ per_page: z.number().int().positive().optional().describe("Results per page"),
36
68
  },
37
69
  }, withErrorHandling(async (args) => {
38
70
  const params = new URLSearchParams();
39
71
  if (args.search)
40
72
  params.set("search", args.search);
73
+ if (args.page)
74
+ params.set("page", String(args.page));
75
+ if (args.per_page)
76
+ params.set("per_page", String(args.per_page));
41
77
  const query = params.toString();
42
78
  const path = `${apiClient.buildPath("/qbo_classes")}${query ? `?${query}` : ""}`;
43
79
  const result = await apiClient.get(path);
44
80
  return jsonResponse(result);
45
81
  }));
82
+ server.registerTool("get_qbo_class", {
83
+ description: "Get a specific QuickBooks class by ID",
84
+ inputSchema: {
85
+ id: z.number().int().positive().describe("QuickBooks Class ID"),
86
+ },
87
+ }, withErrorHandling(async (args) => {
88
+ const result = await apiClient.get(apiClient.buildPath(`/qbo_classes/${args.id}`));
89
+ return jsonResponse(result);
90
+ }));
46
91
  server.registerTool("list_send_to_supplier_templates", {
47
92
  description: "List email templates for sending POs to suppliers",
48
93
  inputSchema: {},
@@ -51,16 +96,14 @@ export function registerSupplementaryTools(server, apiClient) {
51
96
  return jsonResponse(templates);
52
97
  }));
53
98
  server.registerTool("forward_purchase_order", {
54
- description: "Forward a purchase order to supplier(s) via email",
99
+ description: "Forward a purchase order to supplier(s) via email. The PO PDF is attached automatically.",
55
100
  inputSchema: {
56
101
  purchase_order_id: z.number().int().positive().describe("Purchase Order ID"),
57
- emails: z.array(z.string().email()).describe("Recipient email addresses"),
58
- cc: z.string().email().optional().describe("CC email address"),
59
- notes: z.string().optional().describe("Email body / template text"),
60
- email_subject: z.string().optional().describe("Email subject"),
61
- template_label: z.string().optional().describe("Template label"),
62
- save_template: z.boolean().optional().describe("Save as new template"),
63
- is_default: z.boolean().optional().describe("Mark as default template"),
102
+ emails: z.string().describe("Comma-separated recipient email addresses"),
103
+ cc: z.string().optional().describe("CC email address (defaults to PO creator's email)"),
104
+ note: z.string().optional().describe("Email body / note text"),
105
+ email_subject: z.string().optional().describe("Email subject line"),
106
+ uploads: z.array(z.number().int()).optional().describe("Upload IDs to attach (must belong to this PO)"),
64
107
  },
65
108
  }, withErrorHandling(async (args) => {
66
109
  const { purchase_order_id, ...data } = args;
@@ -2,27 +2,45 @@ import { z } from "zod";
2
2
  import { jsonResponse, withErrorHandling } from "../tool-helpers.js";
3
3
  export function registerSupplierTools(server, apiClient) {
4
4
  server.registerTool("list_suppliers", {
5
- description: "List suppliers with pagination and filters",
5
+ description: "List suppliers. When page param is provided, returns paginated response with meta (20 per page). Without page param, returns all suppliers as an array. Supports search by name and filtering by department/archived status.",
6
6
  inputSchema: {
7
- page: z.number().int().positive().optional().describe("Page number"),
8
- department_id: z.number().int().optional().describe("Filter by department ID"),
9
- archived: z.boolean().optional().describe("Filter by archived status"),
10
- top: z.number().int().positive().optional().describe("Limit to top N suppliers"),
7
+ page: z.number().int().positive().optional().describe("Page number — enables pagination (20 per page)"),
8
+ search: z.string().optional().describe("Search suppliers by name"),
9
+ department_id: z.number().int().optional().describe("Filter by department ID (includes suppliers without departments)"),
10
+ archived: z.boolean().optional().describe("Filter by archived status (default: false)"),
11
+ show_mappings: z.boolean().optional().describe("Include third-party ID mappings in response"),
11
12
  },
12
13
  }, withErrorHandling(async (args) => {
13
14
  const params = new URLSearchParams();
14
15
  if (args.page)
15
16
  params.set("page", String(args.page));
17
+ if (args.search)
18
+ params.set("search", args.search);
16
19
  if (args.department_id)
17
20
  params.set("department_id", String(args.department_id));
18
21
  if (args.archived !== undefined)
19
22
  params.set("archived", String(args.archived));
23
+ if (args.show_mappings)
24
+ params.set("show_mappings", "true");
25
+ const query = params.toString();
26
+ const path = `${apiClient.buildPath("/suppliers")}${query ? `?${query}` : ""}`;
27
+ const result = await apiClient.get(path);
28
+ return jsonResponse(result);
29
+ }));
30
+ server.registerTool("get_top_suppliers", {
31
+ description: "Get the user's most frequently used suppliers",
32
+ inputSchema: {
33
+ top: z.number().int().positive().optional().describe("Number of top suppliers to return (default: 5)"),
34
+ archived: z.boolean().optional().describe("Include archived suppliers"),
35
+ },
36
+ }, withErrorHandling(async (args) => {
37
+ const params = new URLSearchParams();
20
38
  if (args.top)
21
39
  params.set("top", String(args.top));
40
+ if (args.archived !== undefined)
41
+ params.set("archived", String(args.archived));
22
42
  const query = params.toString();
23
- const path = args.top
24
- ? `${apiClient.buildPath("/suppliers/top")}?${query}`
25
- : `${apiClient.buildPath("/suppliers")}${query ? `?${query}` : ""}`;
43
+ const path = `${apiClient.buildPath("/suppliers/top")}${query ? `?${query}` : ""}`;
26
44
  const result = await apiClient.get(path);
27
45
  return jsonResponse(result);
28
46
  }));
@@ -36,24 +54,22 @@ export function registerSupplierTools(server, apiClient) {
36
54
  return jsonResponse(supplier);
37
55
  }));
38
56
  server.registerTool("create_supplier", {
39
- description: "Create a new supplier (name must be unique)",
57
+ description: "Create a new supplier (name must be unique within the company). If company has supplier approval enabled, creates a pending approval request instead.",
40
58
  inputSchema: {
41
59
  name: z.string().describe("Supplier name (must be unique)"),
42
- email: z.string().email().optional().describe("Supplier email"),
60
+ email: z.string().optional().describe("Supplier email"),
43
61
  address: z.string().optional().describe("Address"),
44
62
  notes: z.string().optional().describe("Notes"),
45
- payment_details: z.string().optional().describe("Payment details"),
63
+ payment_details: z.string().optional().describe("Payment details/bank info"),
46
64
  phone_number: z.string().optional().describe("Phone number"),
47
65
  tax_number: z.string().optional().describe("Tax number"),
48
- contact_person: z.string().optional().describe("Contact person"),
49
- department_ids: z.array(z.number().int()).optional().describe("Department IDs"),
66
+ contact_person: z.string().optional().describe("Contact person name"),
67
+ uei: z.string().optional().describe("Unique Entity Identifier (UEI) for SAM.gov"),
68
+ cage_code: z.string().optional().describe("CAGE code for government contracting"),
69
+ department_ids: z.array(z.number().int()).optional().describe("Department IDs to restrict supplier to"),
50
70
  },
51
71
  }, withErrorHandling(async (args) => {
52
- const { department_ids, ...supplierData } = args;
53
- const body = { supplier: supplierData };
54
- if (department_ids)
55
- body.department_ids = department_ids;
56
- const supplier = await apiClient.post(apiClient.buildPath("/suppliers"), body);
72
+ const supplier = await apiClient.post(apiClient.buildPath("/suppliers"), { supplier: args });
57
73
  return jsonResponse(supplier);
58
74
  }));
59
75
  server.registerTool("update_supplier", {
@@ -61,7 +77,7 @@ export function registerSupplierTools(server, apiClient) {
61
77
  inputSchema: {
62
78
  id: z.number().int().positive().describe("Supplier ID"),
63
79
  name: z.string().optional().describe("Supplier name"),
64
- email: z.string().email().optional().describe("Email"),
80
+ email: z.string().optional().describe("Email"),
65
81
  address: z.string().optional().describe("Address"),
66
82
  notes: z.string().optional().describe("Notes"),
67
83
  payment_details: z.string().optional().describe("Payment details"),
@@ -69,6 +85,9 @@ export function registerSupplierTools(server, apiClient) {
69
85
  archived: z.boolean().optional().describe("Archive status"),
70
86
  tax_number: z.string().optional().describe("Tax number"),
71
87
  contact_person: z.string().optional().describe("Contact person"),
88
+ uei: z.string().optional().describe("Unique Entity Identifier (UEI)"),
89
+ cage_code: z.string().optional().describe("CAGE code"),
90
+ department_ids: z.array(z.number().int()).optional().describe("Department IDs"),
72
91
  },
73
92
  }, withErrorHandling(async (args) => {
74
93
  const { id, ...data } = args;
@@ -2,10 +2,17 @@ import { z } from "zod";
2
2
  import { jsonResponse, withErrorHandling } from "../tool-helpers.js";
3
3
  export function registerTaxRateTools(server, apiClient) {
4
4
  server.registerTool("list_tax_rates", {
5
- description: "List all tax rates (single and combined types)",
6
- inputSchema: {},
7
- }, withErrorHandling(async () => {
8
- const taxRates = await apiClient.get(apiClient.buildPath("/tax_rates"));
5
+ description: "List tax rates for the current company, filtered by archived status",
6
+ inputSchema: {
7
+ archived: z.boolean().optional().describe("Filter by archived status (default: false)"),
8
+ },
9
+ }, withErrorHandling(async (args) => {
10
+ const params = new URLSearchParams();
11
+ if (args.archived !== undefined)
12
+ params.set("archived", String(args.archived));
13
+ const query = params.toString();
14
+ const path = `${apiClient.buildPath("/tax_rates")}${query ? `?${query}` : ""}`;
15
+ const taxRates = await apiClient.get(path);
9
16
  return jsonResponse(taxRates);
10
17
  }));
11
18
  server.registerTool("get_tax_rate", {
@@ -18,11 +25,10 @@ export function registerTaxRateTools(server, apiClient) {
18
25
  return jsonResponse(taxRate);
19
26
  }));
20
27
  server.registerTool("create_tax_rate", {
21
- description: "Create a new tax rate",
28
+ description: "Create a new tax rate for the current company",
22
29
  inputSchema: {
23
- name: z.string().describe("Tax rate name"),
24
- value: z.number().describe("Tax rate value (percentage)"),
25
- company_id: z.number().int().describe("Company ID"),
30
+ name: z.string().describe("Tax rate name (e.g. 'VAT 20%')"),
31
+ value: z.number().describe("Tax rate percentage value (e.g. 20 for 20%)"),
26
32
  },
27
33
  }, withErrorHandling(async (args) => {
28
34
  const taxRate = await apiClient.post(apiClient.buildPath("/tax_rates"), { tax_rate: args });
@@ -33,7 +39,7 @@ export function registerTaxRateTools(server, apiClient) {
33
39
  inputSchema: {
34
40
  id: z.number().int().positive().describe("Tax Rate ID"),
35
41
  name: z.string().optional().describe("Tax rate name"),
36
- value: z.number().optional().describe("Tax rate value"),
42
+ value: z.number().optional().describe("Tax rate percentage value"),
37
43
  archived: z.boolean().optional().describe("Archive status"),
38
44
  },
39
45
  }, withErrorHandling(async (args) => {
@@ -9,26 +9,29 @@ export function registerUserTools(server, apiClient) {
9
9
  return jsonResponse(user);
10
10
  }));
11
11
  server.registerTool("update_current_user", {
12
- description: "Update the current user's profile (email, name, phone number, password)",
12
+ description: "Update the current user's profile. Include password_confirmation when changing password.",
13
13
  inputSchema: {
14
14
  email: z.string().email().optional().describe("New email address"),
15
- name: z.string().optional().describe("New name"),
16
- phone_number: z.string().optional().describe("New phone number"),
15
+ name: z.string().optional().describe("Full name"),
16
+ first_name: z.string().optional().describe("First name"),
17
+ last_name: z.string().optional().describe("Last name"),
18
+ phone_number: z.string().optional().describe("Phone number"),
17
19
  password: z.string().optional().describe("New password"),
20
+ password_confirmation: z.string().optional().describe("Password confirmation (required when changing password)"),
18
21
  },
19
22
  }, withErrorHandling(async (args) => {
20
23
  const user = await apiClient.put(apiClient.buildPath("/currentuser"), args);
21
24
  return jsonResponse(user);
22
25
  }));
23
26
  server.registerTool("list_currencies", {
24
- description: "List enabled currencies for the current company",
27
+ description: "List currencies enabled for the current company (company default currency listed first)",
25
28
  inputSchema: {},
26
29
  }, withErrorHandling(async () => {
27
30
  const currencies = await apiClient.get(apiClient.buildPath("/currencies"));
28
31
  return jsonResponse(currencies);
29
32
  }));
30
33
  server.registerTool("list_all_currencies", {
31
- description: "List all available currencies globally",
34
+ description: "List all available currencies globally, sorted by name (or by most popular if company ID is set)",
32
35
  inputSchema: {},
33
36
  }, withErrorHandling(async () => {
34
37
  const currencies = await apiClient.get(apiClient.buildPath("/all_currencies"));