@armor/zuora-mcp 1.1.0 → 1.3.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/tools.js CHANGED
@@ -79,6 +79,12 @@ export const schemas = {
79
79
  .string()
80
80
  .min(1)
81
81
  .describe("Account ID or account number"),
82
+ maxResults: z
83
+ .number()
84
+ .int()
85
+ .min(1)
86
+ .optional()
87
+ .describe("Maximum number of subscriptions to return. When specified, results are truncated server-side. Omit to return all subscriptions for the account."),
82
88
  }),
83
89
  // Payment Tools
84
90
  getPayment: z.object({
@@ -116,15 +122,27 @@ export const schemas = {
116
122
  "Syntax: SELECT field1, field2 FROM ObjectName WHERE condition. " +
117
123
  "Key objects: Account, Invoice, Payment, Subscription, RatePlan, " +
118
124
  "RatePlanCharge, Product, ProductRatePlan, Contact. " +
119
- "Limitations: No JOINs. Max 2000 records per call. " +
125
+ "Limitations: No JOINs supported. Max 2000 records per call. " +
120
126
  "Use continue_zoql_query with queryLocator for pagination. " +
121
127
  "Example: SELECT Id, AccountNumber, Balance FROM Account WHERE Status = 'Active'"),
128
+ maxResults: z
129
+ .number()
130
+ .int()
131
+ .min(1)
132
+ .optional()
133
+ .describe("Maximum records to return. When specified, results are truncated server-side to exactly this count. ZOQL has no LIMIT clause, so this parameter handles it at the application layer. Omit to return all records from the first page (up to 2000)."),
122
134
  }),
123
135
  continueZoqlQuery: z.object({
124
136
  queryLocator: z
125
137
  .string()
126
138
  .min(1)
127
139
  .describe("Query locator from a previous ZOQL query result for pagination"),
140
+ maxResults: z
141
+ .number()
142
+ .int()
143
+ .min(1)
144
+ .optional()
145
+ .describe("Maximum records to return from this continuation page. When specified, results are truncated server-side. Omit to return all records from the page (up to 2000)."),
128
146
  }),
129
147
  // Product Catalog Tools
130
148
  listProducts: z.object({
@@ -198,6 +216,12 @@ export const schemas = {
198
216
  .enum(["=", "!=", "LIKE", ">", "<", ">=", "<="])
199
217
  .default("=")
200
218
  .describe("Comparison operator (default: =). Use LIKE for partial name matches with % wildcard."),
219
+ maxResults: z
220
+ .number()
221
+ .int()
222
+ .min(1)
223
+ .optional()
224
+ .describe("Maximum number of accounts to return. When specified, results are truncated server-side to exactly this count. Supports any count including values beyond the 2000-per-page ZOQL limit via auto-pagination. Omit to return all matching records."),
201
225
  }),
202
226
  listUsage: z.object({
203
227
  accountKey: z
@@ -218,6 +242,73 @@ export const schemas = {
218
242
  .default(20)
219
243
  .describe("Records per page (default: 20, max: 40)"),
220
244
  }),
245
+ // User Management Tools
246
+ listUsers: z.object({
247
+ startIndex: z
248
+ .number()
249
+ .int()
250
+ .min(1)
251
+ .default(1)
252
+ .describe("1-based start index for SCIM pagination (default: 1)"),
253
+ count: z
254
+ .number()
255
+ .int()
256
+ .min(1)
257
+ .max(100)
258
+ .default(100)
259
+ .describe("Number of users to return per page (default: 100, max: 100)"),
260
+ filter: z
261
+ .string()
262
+ .max(500)
263
+ .optional()
264
+ .describe("SCIM filter string (optional). " +
265
+ "Examples: status eq \"Active\", userName eq \"user@example.com\""),
266
+ }),
267
+ getUser: z.object({
268
+ userId: z
269
+ .string()
270
+ .uuid("User ID must be a valid UUID")
271
+ .describe("Zuora user ID (UUID format)"),
272
+ }),
273
+ // Bill Run Read Tools
274
+ getBillRun: z.object({
275
+ billRunId: z
276
+ .string()
277
+ .min(1)
278
+ .describe("Bill run ID (e.g., 2c92c0f8...)"),
279
+ }),
280
+ listBillRuns: z.object({
281
+ page: z
282
+ .number()
283
+ .int()
284
+ .min(1)
285
+ .default(1)
286
+ .describe("Page number (default: 1)"),
287
+ pageSize: z
288
+ .number()
289
+ .int()
290
+ .min(1)
291
+ .max(40)
292
+ .default(20)
293
+ .describe("Records per page (default: 20, max: 40)"),
294
+ }),
295
+ // Contact Read Tools
296
+ getContact: z.object({
297
+ contactId: z
298
+ .string()
299
+ .min(1)
300
+ .describe("Contact ID (e.g., 2c92c0f8...)"),
301
+ }),
302
+ // Describe API
303
+ describeObject: z.object({
304
+ objectType: z
305
+ .string()
306
+ .min(1)
307
+ .max(100)
308
+ .regex(/^[A-Z][a-zA-Z]*$/, "Object type must be PascalCase (e.g., Account, Invoice, Subscription)")
309
+ .describe("Zuora object type name in PascalCase (e.g., Account, Invoice, Subscription, " +
310
+ "Payment, RatePlan, RatePlanCharge, Product, ProductRatePlan, BillRun, Contact)"),
311
+ }),
221
312
  // Phase 3: Write Operations
222
313
  createPayment: z.object({
223
314
  accountId: z
@@ -597,6 +688,126 @@ export const schemas = {
597
688
  .describe("UUID to prevent duplicate refunds on retries. " +
598
689
  "Generate a new UUID v4 for each distinct refund intent."),
599
690
  }),
691
+ // Bill Run Write Tools
692
+ createBillRun: z.object({
693
+ targetDate: z
694
+ .string()
695
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
696
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
697
+ .describe("Target date for the bill run in YYYY-MM-DD format. Invoices are generated for charges through this date."),
698
+ invoiceDate: z
699
+ .string()
700
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
701
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
702
+ .optional()
703
+ .describe("Invoice date in YYYY-MM-DD format (optional, defaults to targetDate)"),
704
+ autoPost: z
705
+ .boolean()
706
+ .default(false)
707
+ .describe("Automatically post invoices after generation (default: false)"),
708
+ autoEmail: z
709
+ .boolean()
710
+ .default(false)
711
+ .describe("Automatically email invoices after posting (default: false). Requires autoPost to be true."),
712
+ name: z
713
+ .string()
714
+ .max(255)
715
+ .optional()
716
+ .describe("Optional name/label for the bill run"),
717
+ idempotencyKey: z
718
+ .string()
719
+ .uuid("Idempotency key must be a valid UUID")
720
+ .describe("UUID to prevent duplicate bill runs on retries. " +
721
+ "Generate a new UUID v4 for each distinct bill run intent."),
722
+ }).superRefine((data, ctx) => {
723
+ if (data.autoEmail && !data.autoPost) {
724
+ ctx.addIssue({
725
+ code: z.ZodIssueCode.custom,
726
+ path: ["autoEmail"],
727
+ message: "autoEmail requires autoPost to be true — invoices must be posted before they can be emailed",
728
+ });
729
+ }
730
+ }),
731
+ // Contact Write Tools
732
+ createContact: z.object({
733
+ accountId: z
734
+ .string()
735
+ .min(1)
736
+ .describe("Zuora account UUID to create the contact for"),
737
+ firstName: z
738
+ .string()
739
+ .min(1)
740
+ .max(100)
741
+ .describe("Contact first name"),
742
+ lastName: z
743
+ .string()
744
+ .min(1)
745
+ .max(100)
746
+ .describe("Contact last name"),
747
+ workEmail: z
748
+ .string()
749
+ .email("Must be a valid email address")
750
+ .optional()
751
+ .describe("Work email address"),
752
+ personalEmail: z
753
+ .string()
754
+ .email("Must be a valid email address")
755
+ .optional()
756
+ .describe("Personal email address"),
757
+ workPhone: z.string().max(40).optional().describe("Work phone number"),
758
+ homePhone: z.string().max(40).optional().describe("Home phone number"),
759
+ mobilePhone: z.string().max(40).optional().describe("Mobile phone number"),
760
+ fax: z.string().max(40).optional().describe("Fax number"),
761
+ address1: z.string().max(255).optional().describe("Street address line 1"),
762
+ address2: z.string().max(255).optional().describe("Street address line 2"),
763
+ city: z.string().max(100).optional().describe("City"),
764
+ state: z.string().max(100).optional().describe("State or province"),
765
+ country: z.string().max(100).optional().describe("Country (ISO 3166 name or code)"),
766
+ postalCode: z.string().max(20).optional().describe("Postal/ZIP code"),
767
+ county: z.string().max(100).optional().describe("County"),
768
+ taxRegion: z.string().max(100).optional().describe("Tax region"),
769
+ description: z.string().max(2000).optional().describe("Contact description/notes"),
770
+ nickname: z.string().max(100).optional().describe("Contact nickname"),
771
+ idempotencyKey: z
772
+ .string()
773
+ .uuid("Idempotency key must be a valid UUID")
774
+ .describe("UUID to prevent duplicate contact creation on retries. " +
775
+ "Generate a new UUID v4 for each distinct contact intent."),
776
+ }),
777
+ updateContact: z.object({
778
+ contactId: z
779
+ .string()
780
+ .min(1)
781
+ .describe("Contact ID to update"),
782
+ firstName: z.string().min(1).max(100).optional().describe("Updated first name"),
783
+ lastName: z.string().min(1).max(100).optional().describe("Updated last name"),
784
+ workEmail: z.string().email("Must be a valid email address").optional().describe("Updated work email"),
785
+ personalEmail: z.string().email("Must be a valid email address").optional().describe("Updated personal email"),
786
+ workPhone: z.string().max(40).optional().describe("Updated work phone"),
787
+ homePhone: z.string().max(40).optional().describe("Updated home phone"),
788
+ mobilePhone: z.string().max(40).optional().describe("Updated mobile phone"),
789
+ fax: z.string().max(40).optional().describe("Updated fax number"),
790
+ address1: z.string().max(255).optional().describe("Updated street address line 1"),
791
+ address2: z.string().max(255).optional().describe("Updated street address line 2"),
792
+ city: z.string().max(100).optional().describe("Updated city"),
793
+ state: z.string().max(100).optional().describe("Updated state or province"),
794
+ country: z.string().max(100).optional().describe("Updated country"),
795
+ postalCode: z.string().max(20).optional().describe("Updated postal/ZIP code"),
796
+ county: z.string().max(100).optional().describe("Updated county"),
797
+ taxRegion: z.string().max(100).optional().describe("Updated tax region"),
798
+ description: z.string().max(2000).optional().describe("Updated description/notes"),
799
+ nickname: z.string().max(100).optional().describe("Updated nickname"),
800
+ }).superRefine((data, ctx) => {
801
+ const { contactId: _, ...updateFields } = data;
802
+ const hasUpdate = Object.values(updateFields).some((v) => v !== undefined);
803
+ if (!hasUpdate) {
804
+ ctx.addIssue({
805
+ code: z.ZodIssueCode.custom,
806
+ path: [],
807
+ message: "At least one field to update must be provided",
808
+ });
809
+ }
810
+ }),
600
811
  // ==================== Composite Tool Schemas ====================
601
812
  findAccountsByProduct: z.object({
602
813
  productName: z
@@ -648,6 +859,27 @@ export const schemas = {
648
859
  .string()
649
860
  .min(1)
650
861
  .describe("Account number or ID (e.g., A00012345)"),
862
+ maxInvoices: z
863
+ .number()
864
+ .int()
865
+ .min(1)
866
+ .optional()
867
+ .default(20)
868
+ .describe("Maximum number of recent invoices to fetch (default: 20)."),
869
+ maxPayments: z
870
+ .number()
871
+ .int()
872
+ .min(1)
873
+ .optional()
874
+ .default(10)
875
+ .describe("Maximum number of recent payments to fetch (default: 10)."),
876
+ maxSubscriptions: z
877
+ .number()
878
+ .int()
879
+ .min(1)
880
+ .optional()
881
+ .default(10)
882
+ .describe("Maximum number of active subscriptions to include (default: 10)."),
651
883
  }),
652
884
  getRevenueByProduct: z.object({
653
885
  limit: z
@@ -824,13 +1056,23 @@ export class ToolHandlers {
824
1056
  }
825
1057
  async listSubscriptions(input) {
826
1058
  try {
827
- const { accountKey } = schemas.listSubscriptions.parse(input);
1059
+ const { accountKey, maxResults } = schemas.listSubscriptions.parse(input);
828
1060
  const result = await this.client.listSubscriptionsByAccount(accountKey);
829
- const count = result.subscriptions?.length ?? 0;
1061
+ const allSubs = result.subscriptions ?? [];
1062
+ const subscriptions = maxResults !== undefined
1063
+ ? allSubs.slice(0, maxResults)
1064
+ : allSubs;
830
1065
  return {
831
1066
  success: true,
832
- message: `Found ${count} subscription(s) for account ${accountKey}`,
833
- data: result,
1067
+ message: maxResults !== undefined && allSubs.length > maxResults
1068
+ ? `Returning ${subscriptions.length} of ${allSubs.length} subscription(s) for account ${accountKey}`
1069
+ : `Found ${subscriptions.length} subscription(s) for account ${accountKey}`,
1070
+ data: {
1071
+ subscriptions,
1072
+ ...(maxResults !== undefined && allSubs.length > maxResults
1073
+ ? { totalAvailable: allSubs.length }
1074
+ : {}),
1075
+ },
834
1076
  };
835
1077
  }
836
1078
  catch (error) {
@@ -884,7 +1126,22 @@ export class ToolHandlers {
884
1126
  // --- ZOQL Query Handlers ---
885
1127
  async executeZoqlQuery(input) {
886
1128
  try {
887
- const { zoqlQuery } = schemas.executeZoqlQuery.parse(input);
1129
+ const { zoqlQuery, maxResults } = schemas.executeZoqlQuery.parse(input);
1130
+ if (maxResults !== undefined) {
1131
+ // Auto-paginate and truncate to exact count requested
1132
+ const allRecords = await this.client.queryAll(zoqlQuery, maxResults);
1133
+ const records = allRecords.slice(0, maxResults);
1134
+ return {
1135
+ success: true,
1136
+ message: `Returning ${records.length} record(s)`,
1137
+ data: {
1138
+ size: records.length,
1139
+ records,
1140
+ done: true,
1141
+ },
1142
+ };
1143
+ }
1144
+ // No maxResults — return first page as before
888
1145
  const result = await this.client.executeQuery(zoqlQuery);
889
1146
  const moreAvailable = result.queryLocator
890
1147
  ? " (more available via continue_zoql_query)"
@@ -904,8 +1161,25 @@ export class ToolHandlers {
904
1161
  }
905
1162
  async continueZoqlQuery(input) {
906
1163
  try {
907
- const { queryLocator } = schemas.continueZoqlQuery.parse(input);
1164
+ const { queryLocator, maxResults } = schemas.continueZoqlQuery.parse(input);
908
1165
  const result = await this.client.queryMore(queryLocator);
1166
+ if (maxResults !== undefined) {
1167
+ const allRecords = result.records ?? [];
1168
+ const records = allRecords.slice(0, maxResults);
1169
+ const moreAvailable = (allRecords.length > maxResults || result.queryLocator)
1170
+ ? " (more available)"
1171
+ : "";
1172
+ return {
1173
+ success: true,
1174
+ message: `Continuation returned ${records.length} record(s)${moreAvailable}`,
1175
+ data: {
1176
+ size: records.length,
1177
+ records,
1178
+ queryLocator: result.queryLocator,
1179
+ done: result.done,
1180
+ },
1181
+ };
1182
+ }
909
1183
  const moreAvailable = result.queryLocator
910
1184
  ? " (more available)"
911
1185
  : "";
@@ -998,7 +1272,21 @@ export class ToolHandlers {
998
1272
  }
999
1273
  async searchAccounts(input) {
1000
1274
  try {
1001
- const { field, value, operator } = schemas.searchAccounts.parse(input);
1275
+ const { field, value, operator, maxResults } = schemas.searchAccounts.parse(input);
1276
+ if (maxResults !== undefined) {
1277
+ // Use queryAll for auto-pagination, then truncate to exact count
1278
+ const allRecords = await this.client.searchAccountsWithLimit(field, value, operator, maxResults);
1279
+ return {
1280
+ success: true,
1281
+ message: `Returning ${allRecords.length} account(s) matching search criteria`,
1282
+ data: {
1283
+ size: allRecords.length,
1284
+ records: allRecords,
1285
+ done: true,
1286
+ },
1287
+ };
1288
+ }
1289
+ // No maxResults — return full result as before
1002
1290
  const result = await this.client.searchAccounts(field, value, operator);
1003
1291
  const count = result.records?.length ?? 0;
1004
1292
  return {
@@ -1032,6 +1320,115 @@ export class ToolHandlers {
1032
1320
  };
1033
1321
  }
1034
1322
  }
1323
+ // --- User Management Handlers ---
1324
+ async listUsers(input) {
1325
+ try {
1326
+ const { startIndex, count, filter } = schemas.listUsers.parse(input);
1327
+ const result = await this.client.listUsers(startIndex, count, filter);
1328
+ const userCount = result.Resources?.length ?? 0;
1329
+ const filterNote = filter ? ` (filter: ${filter})` : "";
1330
+ return {
1331
+ success: true,
1332
+ message: `Found ${userCount} user(s) of ${result.totalResults} total${filterNote}`,
1333
+ data: result,
1334
+ };
1335
+ }
1336
+ catch (error) {
1337
+ return {
1338
+ success: false,
1339
+ message: `Failed to list users: ${error instanceof Error ? error.message : String(error)}`,
1340
+ };
1341
+ }
1342
+ }
1343
+ async getUser(input) {
1344
+ try {
1345
+ const { userId } = schemas.getUser.parse(input);
1346
+ const user = await this.client.getUser(userId);
1347
+ return {
1348
+ success: true,
1349
+ message: `Retrieved user ${user.userName ?? userId}`,
1350
+ data: user,
1351
+ };
1352
+ }
1353
+ catch (error) {
1354
+ return {
1355
+ success: false,
1356
+ message: `Failed to get user: ${error instanceof Error ? error.message : String(error)}`,
1357
+ };
1358
+ }
1359
+ }
1360
+ // --- Bill Run Handlers ---
1361
+ async getBillRun(input) {
1362
+ try {
1363
+ const { billRunId } = schemas.getBillRun.parse(input);
1364
+ const billRun = await this.client.getBillRun(billRunId);
1365
+ return {
1366
+ success: true,
1367
+ message: `Retrieved bill run ${billRun.billRunNumber ?? billRunId} (status: ${billRun.status})`,
1368
+ data: billRun,
1369
+ };
1370
+ }
1371
+ catch (error) {
1372
+ return {
1373
+ success: false,
1374
+ message: `Failed to get bill run: ${error instanceof Error ? error.message : String(error)}`,
1375
+ };
1376
+ }
1377
+ }
1378
+ async listBillRuns(input) {
1379
+ try {
1380
+ const { page, pageSize } = schemas.listBillRuns.parse(input);
1381
+ const result = await this.client.listBillRuns(page, pageSize);
1382
+ const count = result.billRuns?.length ?? 0;
1383
+ return {
1384
+ success: true,
1385
+ message: `Found ${count} bill run(s)`,
1386
+ data: result,
1387
+ };
1388
+ }
1389
+ catch (error) {
1390
+ return {
1391
+ success: false,
1392
+ message: `Failed to list bill runs: ${error instanceof Error ? error.message : String(error)}`,
1393
+ };
1394
+ }
1395
+ }
1396
+ // --- Contact Handlers ---
1397
+ async getContact(input) {
1398
+ try {
1399
+ const { contactId } = schemas.getContact.parse(input);
1400
+ const contact = await this.client.getContact(contactId);
1401
+ return {
1402
+ success: true,
1403
+ message: `Retrieved contact ${contact.firstName} ${contact.lastName} (${contactId})`,
1404
+ data: contact,
1405
+ };
1406
+ }
1407
+ catch (error) {
1408
+ return {
1409
+ success: false,
1410
+ message: `Failed to get contact: ${error instanceof Error ? error.message : String(error)}`,
1411
+ };
1412
+ }
1413
+ }
1414
+ // --- Describe API Handlers ---
1415
+ async describeObject(input) {
1416
+ try {
1417
+ const { objectType } = schemas.describeObject.parse(input);
1418
+ const result = await this.client.describeObject(objectType);
1419
+ return {
1420
+ success: true,
1421
+ message: `Described ${result.objectName}: ${result.fieldCount} field(s)`,
1422
+ data: result,
1423
+ };
1424
+ }
1425
+ catch (error) {
1426
+ return {
1427
+ success: false,
1428
+ message: `Failed to describe object: ${error instanceof Error ? error.message : String(error)}`,
1429
+ };
1430
+ }
1431
+ }
1035
1432
  // --- Phase 3: Write Operation Handlers ---
1036
1433
  async createPayment(input) {
1037
1434
  try {
@@ -1059,7 +1456,7 @@ export class ToolHandlers {
1059
1456
  });
1060
1457
  return {
1061
1458
  success: true,
1062
- message: `Applied payment ${result.paymentNumber} successfully`,
1459
+ message: `Applied payment ${paymentId} (applied: ${result.appliedAmount}) successfully`,
1063
1460
  data: result,
1064
1461
  };
1065
1462
  }
@@ -1207,6 +1604,59 @@ export class ToolHandlers {
1207
1604
  };
1208
1605
  }
1209
1606
  }
1607
+ // --- Bill Run Write Handlers ---
1608
+ async createBillRun(input) {
1609
+ try {
1610
+ const { idempotencyKey, ...billRunData } = schemas.createBillRun.parse(input);
1611
+ const result = await this.client.createBillRun(billRunData, idempotencyKey);
1612
+ return {
1613
+ success: true,
1614
+ message: `Created bill run ${result.billRunNumber} (status: ${result.status})`,
1615
+ data: result,
1616
+ };
1617
+ }
1618
+ catch (error) {
1619
+ return {
1620
+ success: false,
1621
+ message: `Failed to create bill run: ${error instanceof Error ? error.message : String(error)}`,
1622
+ };
1623
+ }
1624
+ }
1625
+ // --- Contact Write Handlers ---
1626
+ async createContact(input) {
1627
+ try {
1628
+ const { idempotencyKey, ...contactData } = schemas.createContact.parse(input);
1629
+ const result = await this.client.createContact(contactData, idempotencyKey);
1630
+ return {
1631
+ success: true,
1632
+ message: `Created contact (id: ${result.id}) for account ${contactData.accountId}`,
1633
+ data: result,
1634
+ };
1635
+ }
1636
+ catch (error) {
1637
+ return {
1638
+ success: false,
1639
+ message: `Failed to create contact: ${error instanceof Error ? error.message : String(error)}`,
1640
+ };
1641
+ }
1642
+ }
1643
+ async updateContact(input) {
1644
+ try {
1645
+ const { contactId, ...updateData } = schemas.updateContact.parse(input);
1646
+ const result = await this.client.updateContact(contactId, updateData);
1647
+ return {
1648
+ success: true,
1649
+ message: `Updated contact ${contactId} successfully`,
1650
+ data: result,
1651
+ };
1652
+ }
1653
+ catch (error) {
1654
+ return {
1655
+ success: false,
1656
+ message: `Failed to update contact: ${error instanceof Error ? error.message : String(error)}`,
1657
+ };
1658
+ }
1659
+ }
1210
1660
  // --- Composite Tool Handlers ---
1211
1661
  async findAccountsByProduct(input) {
1212
1662
  try {
@@ -1319,7 +1769,7 @@ export class ToolHandlers {
1319
1769
  // Query overdue invoices: Posted, has balance, past due date
1320
1770
  const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, AccountId, InvoiceDate, DueDate, Amount, Balance ` +
1321
1771
  `FROM Invoice ` +
1322
- `WHERE Status = 'Posted' AND Balance > ${minBalance} AND DueDate < '${today}'`, limit);
1772
+ `WHERE Status = 'Posted' AND Balance > ${Number(minBalance)} AND DueDate < '${today}'`, limit);
1323
1773
  if (invoices.length === 0) {
1324
1774
  return {
1325
1775
  success: true,
@@ -1416,7 +1866,7 @@ export class ToolHandlers {
1416
1866
  accountName: getString(acct ?? {}, "Name") ?? "",
1417
1867
  status: getString(sub, "Status") ?? "",
1418
1868
  termEndDate: termEnd,
1419
- autoRenew: sub.AutoRenew === true,
1869
+ autoRenew: getString(sub, "AutoRenew") === "true",
1420
1870
  contractedMrr: getNumber(sub, "ContractedMrr") ?? 0,
1421
1871
  ratePlanNames: rpNamesBySub.get(subId) ?? [],
1422
1872
  daysUntilExpiry: daysBetween(today, termEnd),
@@ -1445,17 +1895,17 @@ export class ToolHandlers {
1445
1895
  }
1446
1896
  async getAccountBillingOverview(input) {
1447
1897
  try {
1448
- const { accountKey } = schemas.getAccountBillingOverview.parse(input);
1898
+ const { accountKey, maxInvoices, maxPayments, maxSubscriptions } = schemas.getAccountBillingOverview.parse(input);
1449
1899
  // Step 1: Get account via REST API
1450
1900
  const account = await this.client.getAccount(accountKey);
1451
1901
  // Step 2: Get recent invoices via ZOQL
1452
1902
  const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, InvoiceDate, DueDate, Amount, Balance, Status ` +
1453
1903
  `FROM Invoice WHERE AccountId = '${escapeZoql(account.id)}' ` +
1454
- `ORDER BY InvoiceDate DESC`, 20);
1904
+ `ORDER BY InvoiceDate DESC`, maxInvoices);
1455
1905
  // Step 3: Get recent payments via ZOQL
1456
1906
  const payments = await queryAll(this.client, `SELECT Id, PaymentNumber, Amount, EffectiveDate, Status ` +
1457
1907
  `FROM Payment WHERE AccountId = '${escapeZoql(account.id)}' ` +
1458
- `ORDER BY EffectiveDate DESC`, 10);
1908
+ `ORDER BY EffectiveDate DESC`, maxPayments);
1459
1909
  // Step 4: Get subscriptions via REST API
1460
1910
  const subResult = await this.client.listSubscriptionsByAccount(accountKey);
1461
1911
  const subscriptions = subResult.subscriptions ?? [];
@@ -1493,7 +1943,7 @@ export class ToolHandlers {
1493
1943
  totalOutstanding,
1494
1944
  overdueCount,
1495
1945
  overdueAmount,
1496
- recentInvoices: invoices.slice(0, 10).map((inv) => ({
1946
+ recentInvoices: invoices.slice(0, maxInvoices).map((inv) => ({
1497
1947
  invoiceNumber: getString(inv, "InvoiceNumber") ?? "",
1498
1948
  invoiceDate: getString(inv, "InvoiceDate") ?? "",
1499
1949
  dueDate: getString(inv, "DueDate") ?? "",
@@ -1503,7 +1953,7 @@ export class ToolHandlers {
1503
1953
  })),
1504
1954
  },
1505
1955
  paymentSummary: {
1506
- recentPayments: payments.slice(0, 5).map((p) => ({
1956
+ recentPayments: payments.slice(0, maxPayments).map((p) => ({
1507
1957
  paymentNumber: getString(p, "PaymentNumber") ?? "",
1508
1958
  amount: getNumber(p, "Amount") ?? 0,
1509
1959
  effectiveDate: getString(p, "EffectiveDate") ?? "",
@@ -1513,7 +1963,7 @@ export class ToolHandlers {
1513
1963
  subscriptionSummary: {
1514
1964
  activeCount: activeSubscriptions.length,
1515
1965
  totalMrr,
1516
- subscriptions: activeSubscriptions.slice(0, 10).map((s) => ({
1966
+ subscriptions: activeSubscriptions.slice(0, maxSubscriptions).map((s) => ({
1517
1967
  subscriptionNumber: s.subscriptionNumber,
1518
1968
  status: s.status,
1519
1969
  termEndDate: s.termEndDate,
@@ -1539,7 +1989,7 @@ export class ToolHandlers {
1539
1989
  try {
1540
1990
  const { limit } = schemas.getRevenueByProduct.parse(input);
1541
1991
  // Step 1: Get all active subscriptions with MRR
1542
- const subscriptions = await queryAll(this.client, `SELECT Id, AccountId, SubscriptionNumber, ContractedMrr, Status ` +
1992
+ const subscriptions = await queryAll(this.client, `SELECT Id, AccountId, SubscriptionNumber, ContractedMrr, TotalContractedValue, Status ` +
1543
1993
  `FROM Subscription WHERE Status = 'Active'`, 5000);
1544
1994
  if (subscriptions.length === 0) {
1545
1995
  return {
@@ -1588,6 +2038,7 @@ export class ToolHandlers {
1588
2038
  };
1589
2039
  existing.subscriptionCount++;
1590
2040
  existing.totalMrr += mrr;
2041
+ existing.totalTcv += getNumber(sub, "TotalContractedValue") ?? 0;
1591
2042
  existing.subscriptions.push({
1592
2043
  subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
1593
2044
  accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
@@ -1693,9 +2144,9 @@ export class ToolHandlers {
1693
2144
  try {
1694
2145
  const { daysBack, limit } = schemas.getRecentlyCancelledSubscriptions.parse(input);
1695
2146
  const sinceDate = subtractDays(new Date(), daysBack);
1696
- const subscriptions = await queryAll(this.client, `SELECT Id, SubscriptionNumber, AccountId, Status, TermEndDate, UpdatedDate, ContractedMrr ` +
2147
+ const subscriptions = await queryAll(this.client, `SELECT Id, SubscriptionNumber, AccountId, Status, TermEndDate, CancelledDate, ContractedMrr ` +
1697
2148
  `FROM Subscription ` +
1698
- `WHERE Status = 'Cancelled' AND UpdatedDate >= '${sinceDate}'`, limit * 2);
2149
+ `WHERE Status = 'Cancelled' AND CancelledDate >= '${sinceDate}'`, limit * 2);
1699
2150
  if (subscriptions.length === 0) {
1700
2151
  return {
1701
2152
  success: true,
@@ -1730,7 +2181,7 @@ export class ToolHandlers {
1730
2181
  accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
1731
2182
  accountName: getString(acct ?? {}, "Name") ?? "",
1732
2183
  status: getString(sub, "Status") ?? "",
1733
- cancelledDate: getString(sub, "UpdatedDate") ?? "",
2184
+ cancelledDate: getString(sub, "CancelledDate") ?? "",
1734
2185
  termEndDate: getString(sub, "TermEndDate") ?? "",
1735
2186
  contractedMrr: getNumber(sub, "ContractedMrr") ?? 0,
1736
2187
  ratePlanNames: rpNamesBySub.get(subId) ?? [],
@@ -1845,7 +2296,7 @@ export class ToolHandlers {
1845
2296
  // Signal 2: Expiring subscriptions (next 30 days, not auto-renewing)
1846
2297
  const expiringSubscriptions = await queryAll(this.client, `SELECT Id, AccountId, ContractedMrr, TermEndDate ` +
1847
2298
  `FROM Subscription ` +
1848
- `WHERE Status = 'Active' AND TermEndDate >= '${today}' AND TermEndDate <= '${thirtyDaysAhead}' AND AutoRenew = false`, 2000);
2299
+ `WHERE Status = 'Active' AND TermEndDate >= '${today}' AND TermEndDate <= '${thirtyDaysAhead}' AND AutoRenew = 'false'`, 2000);
1849
2300
  // Signal 3: Recent payment failures
1850
2301
  const failedPayments = await queryAll(this.client, `SELECT Id, AccountId ` +
1851
2302
  `FROM Payment ` +
@@ -1997,7 +2448,7 @@ export class ToolHandlers {
1997
2448
  }
1998
2449
  const rpcIds = collectIds(ratePlanCharges, "Id");
1999
2450
  // Step 3: Find InvoiceItems by RatePlanChargeId
2000
- const invoiceItems = await queryWithBatchedIds(this.client, "Id, InvoiceId, ChargeAmount, ChargeName, ServiceStartDate, ServiceEndDate, SubscriptionId", "InvoiceItem", "RatePlanChargeId", rpcIds, undefined, limit);
2451
+ const invoiceItems = await queryWithBatchedIds(this.client, "Id, InvoiceId, ChargeAmount, ChargeName, ServiceStartDate, ServiceEndDate, SubscriptionId, RatePlanChargeId", "InvoiceItem", "RatePlanChargeId", rpcIds, undefined, limit);
2001
2452
  if (invoiceItems.length === 0) {
2002
2453
  return {
2003
2454
  success: true,
@@ -2100,7 +2551,8 @@ export const toolRegistrations = [
2100
2551
  {
2101
2552
  name: "list_subscriptions",
2102
2553
  description: "List all subscriptions for a Zuora account. " +
2103
- "Returns subscription names, statuses, and term dates.",
2554
+ "Returns subscription names, statuses, and term dates. " +
2555
+ "Use the maxResults parameter when the user requests a specific number of subscriptions.",
2104
2556
  inputSchema: schemas.listSubscriptions,
2105
2557
  invoke: (handlers, args) => handlers.listSubscriptions(args),
2106
2558
  },
@@ -2126,8 +2578,9 @@ export const toolRegistrations = [
2126
2578
  "Syntax: SELECT field1, field2 FROM ObjectName WHERE condition. " +
2127
2579
  "Key objects: Account, Invoice, Payment, Subscription, RatePlan, " +
2128
2580
  "RatePlanCharge, Product, ProductRatePlan, Contact. " +
2129
- "Limitations: No JOINs supported. Max 2000 records per call. " +
2130
- "Use continue_zoql_query with queryLocator for pagination. " +
2581
+ "Limitations: No JOINs supported. ZOQL has no LIMIT clause — use the maxResults " +
2582
+ "parameter to control how many records are returned (e.g., maxResults=10). " +
2583
+ "Use continue_zoql_query with queryLocator for manual pagination when maxResults is not set. " +
2131
2584
  "Example: SELECT Id, AccountNumber, Name, Balance FROM Account WHERE Status = 'Active'",
2132
2585
  inputSchema: schemas.executeZoqlQuery,
2133
2586
  invoke: (handlers, args) => handlers.executeZoqlQuery(args),
@@ -2135,7 +2588,8 @@ export const toolRegistrations = [
2135
2588
  {
2136
2589
  name: "continue_zoql_query",
2137
2590
  description: "Continue a previous ZOQL query that returned a queryLocator " +
2138
- "(indicates more records available). Returns the next batch of up to 2000 records.",
2591
+ "(indicates more records available). Returns the next batch of up to 2000 records. " +
2592
+ "Use the maxResults parameter to return fewer records from the continuation page.",
2139
2593
  inputSchema: schemas.continueZoqlQuery,
2140
2594
  invoke: (handlers, args) => handlers.continueZoqlQuery(args),
2141
2595
  },
@@ -2179,6 +2633,8 @@ export const toolRegistrations = [
2179
2633
  "AccountNumber, Status, Currency, or Balance. Use LIKE operator with % " +
2180
2634
  "wildcard for partial name matching (e.g., field='Name', operator='LIKE', " +
2181
2635
  "value='Acme%'). Returns Id, AccountNumber, Name, Status, Balance, Currency. " +
2636
+ "Use the maxResults parameter when the user requests a specific number of records " +
2637
+ "(e.g., 'get 10 active accounts' → maxResults=10). " +
2182
2638
  "For additional fields, use execute_zoql_query directly.",
2183
2639
  inputSchema: schemas.searchAccounts,
2184
2640
  invoke: (handlers, args) => handlers.searchAccounts(args),
@@ -2191,6 +2647,59 @@ export const toolRegistrations = [
2191
2647
  inputSchema: schemas.listUsage,
2192
2648
  invoke: (handlers, args) => handlers.listUsage(args),
2193
2649
  },
2650
+ // User Management
2651
+ {
2652
+ name: "list_users",
2653
+ description: "List Zuora platform users with optional SCIM filter (e.g., status eq 'Active'). " +
2654
+ "Uses REST API, not ZOQL — the User object is not ZOQL-queryable. " +
2655
+ "Supports pagination via startIndex/count. " +
2656
+ "Returns user names, emails, statuses, roles, and last login times.",
2657
+ inputSchema: schemas.listUsers,
2658
+ invoke: (handlers, args) => handlers.listUsers(args),
2659
+ },
2660
+ {
2661
+ name: "get_user",
2662
+ description: "Get details of a specific Zuora platform user by their user ID (UUID). " +
2663
+ "Returns user name, email, status, role, profile, and last login time.",
2664
+ inputSchema: schemas.getUser,
2665
+ invoke: (handlers, args) => handlers.getUser(args),
2666
+ },
2667
+ // Bill Run Tools
2668
+ {
2669
+ name: "get_bill_run",
2670
+ description: "Get details and status of a Zuora bill run by bill run ID. " +
2671
+ "Returns bill run number, status (Pending/Processing/Completed/Error/Canceled/Posted), " +
2672
+ "target date, invoice date, and auto-post/email settings.",
2673
+ inputSchema: schemas.getBillRun,
2674
+ invoke: (handlers, args) => handlers.getBillRun(args),
2675
+ },
2676
+ {
2677
+ name: "list_bill_runs",
2678
+ description: "List bill runs in Zuora with pagination. " +
2679
+ "Returns bill run numbers, statuses, target dates, and settings.",
2680
+ inputSchema: schemas.listBillRuns,
2681
+ invoke: (handlers, args) => handlers.listBillRuns(args),
2682
+ },
2683
+ // Contact Tools
2684
+ {
2685
+ name: "get_contact",
2686
+ description: "Get full details of a Zuora contact by contact ID. " +
2687
+ "Returns name, emails, phone numbers, address, tax region, and timestamps. " +
2688
+ "Use ZOQL to find contact IDs: SELECT Id, FirstName, LastName FROM Contact WHERE AccountId = '...'",
2689
+ inputSchema: schemas.getContact,
2690
+ invoke: (handlers, args) => handlers.getContact(args),
2691
+ },
2692
+ // Describe API
2693
+ {
2694
+ name: "describe_object",
2695
+ description: "Get field metadata for any Zuora object type. Returns all fields with names, types, " +
2696
+ "and properties (selectable, createable, updateable, filterable, required). " +
2697
+ "Essential for building correct ZOQL queries — use this to discover available fields " +
2698
+ "before writing SELECT statements. Object type must be PascalCase (e.g., Account, " +
2699
+ "Invoice, Subscription, Payment, RatePlan, RatePlanCharge, BillRun, Contact).",
2700
+ inputSchema: schemas.describeObject,
2701
+ invoke: (handlers, args) => handlers.describeObject(args),
2702
+ },
2194
2703
  // Phase 3: Write Operations (require confirmation before calling)
2195
2704
  // Payment Creation (HIGH risk)
2196
2705
  {
@@ -2312,6 +2821,38 @@ export const toolRegistrations = [
2312
2821
  inputSchema: schemas.createRefund,
2313
2822
  invoke: (handlers, args) => handlers.createRefund(args),
2314
2823
  },
2824
+ // Bill Run Creation (MEDIUM risk)
2825
+ {
2826
+ name: "create_bill_run",
2827
+ description: "FINANCIAL WRITE OPERATION (MEDIUM RISK): Create a bill run to generate invoices for a billing cycle. " +
2828
+ "Bill runs process all eligible charges through the target date and generate draft invoices. " +
2829
+ "Confirm the target date with the user before calling. " +
2830
+ "Set autoPost=true to automatically post generated invoices. " +
2831
+ "Set autoEmail=true (requires autoPost) to email invoices to customers. " +
2832
+ "Requires a UUID idempotency key to prevent duplicate bill runs. " +
2833
+ "The bill run starts in Pending status and progresses through Processing to Completed.",
2834
+ inputSchema: schemas.createBillRun,
2835
+ invoke: (handlers, args) => handlers.createBillRun(args),
2836
+ },
2837
+ // Contact Creation (LOW risk)
2838
+ {
2839
+ name: "create_contact",
2840
+ description: "WRITE OPERATION (LOW RISK): Create a new contact for a Zuora account. " +
2841
+ "Requires account ID, first name, and last name. " +
2842
+ "Contacts can be used as bill-to or sold-to addresses via update_account. " +
2843
+ "Requires a UUID idempotency key to prevent duplicate contacts.",
2844
+ inputSchema: schemas.createContact,
2845
+ invoke: (handlers, args) => handlers.createContact(args),
2846
+ },
2847
+ // Contact Update (LOW risk)
2848
+ {
2849
+ name: "update_contact",
2850
+ description: "WRITE OPERATION (LOW RISK): Update an existing Zuora contact's information. " +
2851
+ "Only include fields that need to change. " +
2852
+ "ALWAYS use get_contact to review current details before updating.",
2853
+ inputSchema: schemas.updateContact,
2854
+ invoke: (handlers, args) => handlers.updateContact(args),
2855
+ },
2315
2856
  // ==================== Composite Tools (Read-Only) ====================
2316
2857
  {
2317
2858
  name: "find_accounts_by_product",