mcp-zenskar 1.0.12 → 1.0.13
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/package.json +1 -1
- package/src/mcp-config.json +56 -49
- package/src/server.js +33 -0
- package/test-mcp.mjs +296 -0
package/package.json
CHANGED
package/src/mcp-config.json
CHANGED
|
@@ -24,7 +24,14 @@
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"name": "search",
|
|
27
|
-
"description": "
|
|
27
|
+
"description": "Advanced key-value search query. Format: 'field=value' or 'field1=value1,field2=value2'. Supports any customer field including nested JSON paths. Examples: 'customer_name=Acme', 'email=john@example.com', 'customer_name=Acme,external_id=EXT-100', 'custom_data.users=abc@dev.in'. String fields use case-insensitive contains matching. Do NOT pass plain text — always use field=value format.",
|
|
28
|
+
"type": "string",
|
|
29
|
+
"required": false,
|
|
30
|
+
"position": "query"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "search_name_external_id",
|
|
34
|
+
"description": "Quick relevance-ranked search across customer_name and external_id. Accepts plain text (e.g., 'Acme'). Results are ranked: exact match first, then starts-with, then contains. Simpler alternative to the 'search' parameter when you only need to find customers by name or external ID.",
|
|
28
35
|
"type": "string",
|
|
29
36
|
"required": false,
|
|
30
37
|
"position": "query"
|
|
@@ -38,14 +45,14 @@
|
|
|
38
45
|
"position": "query"
|
|
39
46
|
},
|
|
40
47
|
{
|
|
41
|
-
"name": "
|
|
48
|
+
"name": "sort_key",
|
|
42
49
|
"description": "Key to sort the results by (e.g., 'id').",
|
|
43
50
|
"type": "string",
|
|
44
51
|
"required": false,
|
|
45
52
|
"position": "query"
|
|
46
53
|
},
|
|
47
54
|
{
|
|
48
|
-
"name": "
|
|
55
|
+
"name": "sort_type",
|
|
49
56
|
"description": "Sort order type (e.g., 'asc' for ascending).",
|
|
50
57
|
"type": "string",
|
|
51
58
|
"required": false,
|
|
@@ -108,7 +115,7 @@
|
|
|
108
115
|
"position": "query"
|
|
109
116
|
},
|
|
110
117
|
{
|
|
111
|
-
"name": "
|
|
118
|
+
"name": "phone_number__ilike",
|
|
112
119
|
"description": "Case-insensitive partial match for phone number.",
|
|
113
120
|
"type": "string",
|
|
114
121
|
"required": false,
|
|
@@ -218,14 +225,14 @@
|
|
|
218
225
|
"position": "query"
|
|
219
226
|
},
|
|
220
227
|
{
|
|
221
|
-
"name": "
|
|
228
|
+
"name": "sort_key",
|
|
222
229
|
"description": "Key to sort the results by (e.g., 'id').",
|
|
223
230
|
"type": "string",
|
|
224
231
|
"required": false,
|
|
225
232
|
"position": "query"
|
|
226
233
|
},
|
|
227
234
|
{
|
|
228
|
-
"name": "
|
|
235
|
+
"name": "sort_type",
|
|
229
236
|
"description": "Sort order type (e.g., 'asc').",
|
|
230
237
|
"type": "string",
|
|
231
238
|
"required": false,
|
|
@@ -265,14 +272,14 @@
|
|
|
265
272
|
"description": "Retrieve a paginated list of invoices, with options for filtering by customer, status, amounts, due date, creation date, and related customer/contract details.",
|
|
266
273
|
"args": [
|
|
267
274
|
{
|
|
268
|
-
"name": "
|
|
275
|
+
"name": "customer_id",
|
|
269
276
|
"description": "Filter invoices by a specific customer ID.",
|
|
270
277
|
"type": "string",
|
|
271
278
|
"required": false,
|
|
272
279
|
"position": "query"
|
|
273
280
|
},
|
|
274
281
|
{
|
|
275
|
-
"name": "
|
|
282
|
+
"name": "customer_external_id",
|
|
276
283
|
"description": "Filter invoices by a customer's external ID.",
|
|
277
284
|
"type": "string",
|
|
278
285
|
"required": false,
|
|
@@ -317,70 +324,70 @@
|
|
|
317
324
|
"position": "query"
|
|
318
325
|
},
|
|
319
326
|
{
|
|
320
|
-
"name": "
|
|
327
|
+
"name": "contract_id",
|
|
321
328
|
"description": "Filter by contract ID.",
|
|
322
329
|
"type": "string",
|
|
323
330
|
"required": false,
|
|
324
331
|
"position": "query"
|
|
325
332
|
},
|
|
326
333
|
{
|
|
327
|
-
"name": "
|
|
334
|
+
"name": "external_id",
|
|
328
335
|
"description": "Filter by invoice external ID.",
|
|
329
336
|
"type": "string",
|
|
330
337
|
"required": false,
|
|
331
338
|
"position": "query"
|
|
332
339
|
},
|
|
333
340
|
{
|
|
334
|
-
"name": "
|
|
341
|
+
"name": "invoice_total__gte",
|
|
335
342
|
"description": "Filter by invoice total greater than or equal to.",
|
|
336
343
|
"type": "number",
|
|
337
344
|
"required": false,
|
|
338
345
|
"position": "query"
|
|
339
346
|
},
|
|
340
347
|
{
|
|
341
|
-
"name": "
|
|
348
|
+
"name": "invoice_total__lte",
|
|
342
349
|
"description": "Filter by invoice total less than or equal to.",
|
|
343
350
|
"type": "number",
|
|
344
351
|
"required": false,
|
|
345
352
|
"position": "query"
|
|
346
353
|
},
|
|
347
354
|
{
|
|
348
|
-
"name": "
|
|
355
|
+
"name": "amount_due__gte",
|
|
349
356
|
"description": "Filter by amount due greater than or equal to.",
|
|
350
357
|
"type": "number",
|
|
351
358
|
"required": false,
|
|
352
359
|
"position": "query"
|
|
353
360
|
},
|
|
354
361
|
{
|
|
355
|
-
"name": "
|
|
362
|
+
"name": "amount_due__lte",
|
|
356
363
|
"description": "Filter by amount due less than or equal to.",
|
|
357
364
|
"type": "number",
|
|
358
365
|
"required": false,
|
|
359
366
|
"position": "query"
|
|
360
367
|
},
|
|
361
368
|
{
|
|
362
|
-
"name": "
|
|
369
|
+
"name": "due_date__gte",
|
|
363
370
|
"description": "Filter by due date greater than or equal to (ISO 8601).",
|
|
364
371
|
"type": "string",
|
|
365
372
|
"required": false,
|
|
366
373
|
"position": "query"
|
|
367
374
|
},
|
|
368
375
|
{
|
|
369
|
-
"name": "
|
|
376
|
+
"name": "due_date__lte",
|
|
370
377
|
"description": "Filter by due date less than or equal to (ISO 8601).",
|
|
371
378
|
"type": "string",
|
|
372
379
|
"required": false,
|
|
373
380
|
"position": "query"
|
|
374
381
|
},
|
|
375
382
|
{
|
|
376
|
-
"name": "
|
|
383
|
+
"name": "created_at__gte",
|
|
377
384
|
"description": "Filter by creation date greater than or equal to (ISO 8601).",
|
|
378
385
|
"type": "string",
|
|
379
386
|
"required": false,
|
|
380
387
|
"position": "query"
|
|
381
388
|
},
|
|
382
389
|
{
|
|
383
|
-
"name": "
|
|
390
|
+
"name": "created_at__lte",
|
|
384
391
|
"description": "Filter by creation date less than or equal to (ISO 8601).",
|
|
385
392
|
"type": "string",
|
|
386
393
|
"required": false,
|
|
@@ -421,7 +428,7 @@
|
|
|
421
428
|
"position": "path"
|
|
422
429
|
},
|
|
423
430
|
{
|
|
424
|
-
"name": "
|
|
431
|
+
"name": "customer_id",
|
|
425
432
|
"description": "The ID of the customer associated with the invoice (optional).",
|
|
426
433
|
"type": "string",
|
|
427
434
|
"required": false,
|
|
@@ -448,7 +455,7 @@
|
|
|
448
455
|
"position": "path"
|
|
449
456
|
},
|
|
450
457
|
{
|
|
451
|
-
"name": "
|
|
458
|
+
"name": "contract_id",
|
|
452
459
|
"description": "The ID of the contract associated with the invoice.",
|
|
453
460
|
"type": "string",
|
|
454
461
|
"required": true,
|
|
@@ -502,7 +509,7 @@
|
|
|
502
509
|
"position": "query"
|
|
503
510
|
},
|
|
504
511
|
{
|
|
505
|
-
"name": "
|
|
512
|
+
"name": "customer_id",
|
|
506
513
|
"description": "Optional customer ID associated with the invoice.",
|
|
507
514
|
"type": "string",
|
|
508
515
|
"required": false,
|
|
@@ -695,7 +702,7 @@
|
|
|
695
702
|
"position": "query"
|
|
696
703
|
},
|
|
697
704
|
{
|
|
698
|
-
"name": "
|
|
705
|
+
"name": "sort_key",
|
|
699
706
|
"description": "Key to sort the results by (e.g., 'id').",
|
|
700
707
|
"type": "string",
|
|
701
708
|
"required": false,
|
|
@@ -703,7 +710,7 @@
|
|
|
703
710
|
"position": "query"
|
|
704
711
|
},
|
|
705
712
|
{
|
|
706
|
-
"name": "
|
|
713
|
+
"name": "sort_type",
|
|
707
714
|
"description": "Sort order type (e.g., 'asc').",
|
|
708
715
|
"type": "string",
|
|
709
716
|
"required": false,
|
|
@@ -732,7 +739,7 @@
|
|
|
732
739
|
"position": "query"
|
|
733
740
|
},
|
|
734
741
|
{
|
|
735
|
-
"name": "
|
|
742
|
+
"name": "created_at__gte",
|
|
736
743
|
"description": "Filter by creation date greater than or equal to (ISO 8601).",
|
|
737
744
|
"type": "string",
|
|
738
745
|
"required": false,
|
|
@@ -764,35 +771,35 @@
|
|
|
764
771
|
"description": "Get billable metric estimates based on the specified criteria.",
|
|
765
772
|
"args": [
|
|
766
773
|
{
|
|
767
|
-
"name": "
|
|
774
|
+
"name": "aggregate_id",
|
|
768
775
|
"description": "The ID of the billable metric.",
|
|
769
776
|
"type": "string",
|
|
770
777
|
"required": true,
|
|
771
778
|
"position": "query"
|
|
772
779
|
},
|
|
773
780
|
{
|
|
774
|
-
"name": "
|
|
781
|
+
"name": "customer_id",
|
|
775
782
|
"description": "The ID of the customer.",
|
|
776
783
|
"type": "string",
|
|
777
784
|
"required": true,
|
|
778
785
|
"position": "query"
|
|
779
786
|
},
|
|
780
787
|
{
|
|
781
|
-
"name": "
|
|
788
|
+
"name": "start_date",
|
|
782
789
|
"description": "Start date of the estimate (YYYY-MM-DD).",
|
|
783
790
|
"type": "string",
|
|
784
791
|
"required": true,
|
|
785
792
|
"position": "query"
|
|
786
793
|
},
|
|
787
794
|
{
|
|
788
|
-
"name": "
|
|
795
|
+
"name": "end_date",
|
|
789
796
|
"description": "End date of the estimate (YYYY-MM-DD).",
|
|
790
797
|
"type": "string",
|
|
791
798
|
"required": true,
|
|
792
799
|
"position": "query"
|
|
793
800
|
},
|
|
794
801
|
{
|
|
795
|
-
"name": "
|
|
802
|
+
"name": "get_single",
|
|
796
803
|
"description": "Whether to get a single estimate (defaults to true).",
|
|
797
804
|
"type": "boolean",
|
|
798
805
|
"required": false,
|
|
@@ -840,14 +847,14 @@
|
|
|
840
847
|
"position": "path"
|
|
841
848
|
},
|
|
842
849
|
{
|
|
843
|
-
"name": "
|
|
850
|
+
"name": "start_date",
|
|
844
851
|
"description": "Start date for filtering logs (YYYY-MM-DD).",
|
|
845
852
|
"type": "string",
|
|
846
853
|
"required": false,
|
|
847
854
|
"position": "query"
|
|
848
855
|
},
|
|
849
856
|
{
|
|
850
|
-
"name": "
|
|
857
|
+
"name": "end_date",
|
|
851
858
|
"description": "End date for filtering logs (YYYY-MM-DD).",
|
|
852
859
|
"type": "string",
|
|
853
860
|
"required": false,
|
|
@@ -861,14 +868,14 @@
|
|
|
861
868
|
"position": "query"
|
|
862
869
|
},
|
|
863
870
|
{
|
|
864
|
-
"name": "
|
|
871
|
+
"name": "customer_id",
|
|
865
872
|
"description": "Filter by customer ID.",
|
|
866
873
|
"type": "string",
|
|
867
874
|
"required": false,
|
|
868
875
|
"position": "query"
|
|
869
876
|
},
|
|
870
877
|
{
|
|
871
|
-
"name": "
|
|
878
|
+
"name": "query_limit",
|
|
872
879
|
"description": "Limit the number of log entries (defaults to 100).",
|
|
873
880
|
"type": "integer",
|
|
874
881
|
"required": false,
|
|
@@ -911,7 +918,7 @@
|
|
|
911
918
|
"position": "query"
|
|
912
919
|
},
|
|
913
920
|
{
|
|
914
|
-
"name": "
|
|
921
|
+
"name": "sort_key",
|
|
915
922
|
"description": "Key to sort the results by (e.g., 'created_at').",
|
|
916
923
|
"type": "string",
|
|
917
924
|
"required": false,
|
|
@@ -919,7 +926,7 @@
|
|
|
919
926
|
"position": "query"
|
|
920
927
|
},
|
|
921
928
|
{
|
|
922
|
-
"name": "
|
|
929
|
+
"name": "sort_type",
|
|
923
930
|
"description": "Sort order type (e.g., 'asc').",
|
|
924
931
|
"type": "string",
|
|
925
932
|
"required": false,
|
|
@@ -1212,14 +1219,14 @@
|
|
|
1212
1219
|
"position": "query"
|
|
1213
1220
|
},
|
|
1214
1221
|
{
|
|
1215
|
-
"name": "
|
|
1222
|
+
"name": "sort_key",
|
|
1216
1223
|
"description": "Key to sort the results by (e.g., 'id').",
|
|
1217
1224
|
"type": "string",
|
|
1218
1225
|
"required": false,
|
|
1219
1226
|
"position": "query"
|
|
1220
1227
|
},
|
|
1221
1228
|
{
|
|
1222
|
-
"name": "
|
|
1229
|
+
"name": "sort_type",
|
|
1223
1230
|
"description": "Sort order type (e.g., 'asc').",
|
|
1224
1231
|
"type": "string",
|
|
1225
1232
|
"required": false,
|
|
@@ -1241,28 +1248,28 @@
|
|
|
1241
1248
|
"position": "query"
|
|
1242
1249
|
},
|
|
1243
1250
|
{
|
|
1244
|
-
"name": "
|
|
1251
|
+
"name": "customer_id",
|
|
1245
1252
|
"description": "Filter by customer ID.",
|
|
1246
1253
|
"type": "string",
|
|
1247
1254
|
"required": false,
|
|
1248
1255
|
"position": "query"
|
|
1249
1256
|
},
|
|
1250
1257
|
{
|
|
1251
|
-
"name": "
|
|
1258
|
+
"name": "connector_id",
|
|
1252
1259
|
"description": "Filter by connector ID.",
|
|
1253
1260
|
"type": "string",
|
|
1254
1261
|
"required": false,
|
|
1255
1262
|
"position": "query"
|
|
1256
1263
|
},
|
|
1257
1264
|
{
|
|
1258
|
-
"name": "
|
|
1265
|
+
"name": "external_id__ilike",
|
|
1259
1266
|
"description": "Case-insensitive partial match for external ID.",
|
|
1260
1267
|
"type": "string",
|
|
1261
1268
|
"required": false,
|
|
1262
1269
|
"position": "query"
|
|
1263
1270
|
},
|
|
1264
1271
|
{
|
|
1265
|
-
"name": "
|
|
1272
|
+
"name": "payment_method",
|
|
1266
1273
|
"description": "Filter by payment method (e.g., 'card', 'cash', 'bank_transfer').",
|
|
1267
1274
|
"type": "string",
|
|
1268
1275
|
"required": false,
|
|
@@ -1326,14 +1333,14 @@
|
|
|
1326
1333
|
"position": "query"
|
|
1327
1334
|
},
|
|
1328
1335
|
{
|
|
1329
|
-
"name": "
|
|
1336
|
+
"name": "currency_code__ilike",
|
|
1330
1337
|
"description": "Case-insensitive partial match for currency code.",
|
|
1331
1338
|
"type": "string",
|
|
1332
1339
|
"required": false,
|
|
1333
1340
|
"position": "query"
|
|
1334
1341
|
},
|
|
1335
1342
|
{
|
|
1336
|
-
"name": "
|
|
1343
|
+
"name": "receipt_number__ilike",
|
|
1337
1344
|
"description": "Case-insensitive partial match for receipt number.",
|
|
1338
1345
|
"type": "string",
|
|
1339
1346
|
"required": false,
|
|
@@ -1368,7 +1375,7 @@
|
|
|
1368
1375
|
"position": "query"
|
|
1369
1376
|
},
|
|
1370
1377
|
{
|
|
1371
|
-
"name": "
|
|
1378
|
+
"name": "parent_id",
|
|
1372
1379
|
"description": "Filter by parent payment ID.",
|
|
1373
1380
|
"type": "string",
|
|
1374
1381
|
"required": false,
|
|
@@ -1389,14 +1396,14 @@
|
|
|
1389
1396
|
"position": "query"
|
|
1390
1397
|
},
|
|
1391
1398
|
{
|
|
1392
|
-
"name": "
|
|
1399
|
+
"name": "created_at__gte",
|
|
1393
1400
|
"description": "Filter by creation date greater than or equal to (ISO 8601).",
|
|
1394
1401
|
"type": "string",
|
|
1395
1402
|
"required": false,
|
|
1396
1403
|
"position": "query"
|
|
1397
1404
|
},
|
|
1398
1405
|
{
|
|
1399
|
-
"name": "
|
|
1406
|
+
"name": "created_at__lte",
|
|
1400
1407
|
"description": "Filter by creation date less than or equal to (ISO 8601).",
|
|
1401
1408
|
"type": "string",
|
|
1402
1409
|
"required": false,
|
|
@@ -1868,7 +1875,7 @@
|
|
|
1868
1875
|
"enum": [
|
|
1869
1876
|
"Quantity",
|
|
1870
1877
|
"Feature",
|
|
1871
|
-
"
|
|
1878
|
+
"Credits"
|
|
1872
1879
|
]
|
|
1873
1880
|
},
|
|
1874
1881
|
{
|
|
@@ -2433,9 +2440,9 @@
|
|
|
2433
2440
|
},
|
|
2434
2441
|
{
|
|
2435
2442
|
"name": "status",
|
|
2436
|
-
"description": "Contract status (
|
|
2443
|
+
"description": "Contract status (required, typically 'draft' for new contracts).",
|
|
2437
2444
|
"type": "string",
|
|
2438
|
-
"required":
|
|
2445
|
+
"required": true,
|
|
2439
2446
|
"position": "body",
|
|
2440
2447
|
"default": "draft",
|
|
2441
2448
|
"enum": ["draft", "active", "paused", "expired", "disputed"]
|
package/src/server.js
CHANGED
|
@@ -409,6 +409,35 @@ async function executeAPICall(tool, args) {
|
|
|
409
409
|
nestAddress('ship_to_', 'ship_to_address');
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
// Same address nesting for createBusinessEntity
|
|
413
|
+
if (tool.name === 'createBusinessEntity') {
|
|
414
|
+
const nestAddress = (prefix, targetKey) => {
|
|
415
|
+
const fieldMap = {
|
|
416
|
+
[`${prefix}line1`]: 'line1',
|
|
417
|
+
[`${prefix}line2`]: 'line2',
|
|
418
|
+
[`${prefix}line3`]: 'line3',
|
|
419
|
+
[`${prefix}city`]: 'city',
|
|
420
|
+
[`${prefix}state`]: 'state',
|
|
421
|
+
[`${prefix}zipCode`]: 'zipCode',
|
|
422
|
+
[`${prefix}country`]: 'country'
|
|
423
|
+
};
|
|
424
|
+
const obj = {};
|
|
425
|
+
let has = false;
|
|
426
|
+
Object.entries(fieldMap).forEach(([flatKey, nestedKey]) => {
|
|
427
|
+
if (bodyParams[flatKey] !== undefined) {
|
|
428
|
+
obj[nestedKey] = bodyParams[flatKey];
|
|
429
|
+
delete bodyParams[flatKey];
|
|
430
|
+
has = true;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
if (has) {
|
|
434
|
+
bodyParams[targetKey] = obj;
|
|
435
|
+
logger.debug(`[${tool.name}] Transformed flat ${prefix}* fields into ${targetKey} object`);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
nestAddress('address_', 'address');
|
|
439
|
+
}
|
|
440
|
+
|
|
412
441
|
// Smart defaults for helper tools
|
|
413
442
|
if (tool.name === 'extractContractFromRaw') {
|
|
414
443
|
// Auto-populate organization_id from user context or env var if not provided
|
|
@@ -447,6 +476,10 @@ async function executeAPICall(tool, args) {
|
|
|
447
476
|
|
|
448
477
|
if (Object.keys(bodyParams).length > 0) {
|
|
449
478
|
body = JSON.stringify(bodyParams);
|
|
479
|
+
} else if (method === 'PATCH' || method === 'POST' || method === 'PUT') {
|
|
480
|
+
// Always send at least an empty JSON body for non-GET methods
|
|
481
|
+
// Some endpoints (e.g., approveInvoice) require a body even when no body params are provided
|
|
482
|
+
body = '{}';
|
|
450
483
|
}
|
|
451
484
|
}
|
|
452
485
|
|
package/test-mcp.mjs
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server Integration Test
|
|
4
|
+
* Tests the Zenskar MCP server via the actual MCP protocol (stdio transport).
|
|
5
|
+
*/
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
|
|
9
|
+
const ORG_ID = '888ae523-8878-4ed7-85cc-6c0a54320568';
|
|
10
|
+
const API_KEY = 'sandbox_yA9yieTXSqnGxROOoyLMWIP2CTuCm5j_311CVHu16-U';
|
|
11
|
+
|
|
12
|
+
const userContext = {
|
|
13
|
+
organization: ORG_ID,
|
|
14
|
+
authorization: API_KEY,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let client;
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
const failures = [];
|
|
21
|
+
|
|
22
|
+
async function callTool(name, args = {}) {
|
|
23
|
+
const result = await client.callTool({
|
|
24
|
+
name,
|
|
25
|
+
arguments: { ...args, __userContext: userContext },
|
|
26
|
+
});
|
|
27
|
+
// MCP returns content array; extract text
|
|
28
|
+
const text = result.content?.map(c => c.text).join('') || '';
|
|
29
|
+
const json = (() => { try { return JSON.parse(text); } catch { return null; } })();
|
|
30
|
+
return { text, json, raw: result };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function test(label, fn) {
|
|
34
|
+
try {
|
|
35
|
+
await fn();
|
|
36
|
+
passed++;
|
|
37
|
+
console.log(` ✅ ${label}`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
failed++;
|
|
40
|
+
failures.push({ label, error: err.message || String(err) });
|
|
41
|
+
console.log(` ❌ ${label}: ${err.message || err}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assert(cond, msg) {
|
|
46
|
+
if (!cond) throw new Error(msg);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── MAIN ────────────────────────────────────────────────────
|
|
50
|
+
async function main() {
|
|
51
|
+
console.log('Starting MCP server via stdio...\n');
|
|
52
|
+
|
|
53
|
+
const transport = new StdioClientTransport({
|
|
54
|
+
command: 'node',
|
|
55
|
+
args: ['src/server.js'],
|
|
56
|
+
env: { ...process.env, ZENSKAR_ORGANIZATION: ORG_ID, ZENSKAR_API_KEY: API_KEY },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
client = new Client({ name: 'test-client', version: '1.0.0' });
|
|
60
|
+
await client.connect(transport);
|
|
61
|
+
|
|
62
|
+
// List available tools
|
|
63
|
+
const { tools } = await client.listTools();
|
|
64
|
+
console.log(`MCP server connected. ${tools.length} tools registered.\n`);
|
|
65
|
+
|
|
66
|
+
// ─── CUSTOMERS ──────────────────────────────────────────
|
|
67
|
+
console.log('── Customers ──');
|
|
68
|
+
|
|
69
|
+
let customerId;
|
|
70
|
+
await test('listCustomers (basic)', async () => {
|
|
71
|
+
const { json } = await callTool('listCustomers', { limit: 2 });
|
|
72
|
+
const resp = json?.api_response || json;
|
|
73
|
+
assert(resp?.results?.length > 0, 'No results returned');
|
|
74
|
+
assert(resp.total_count > 0, 'total_count missing');
|
|
75
|
+
customerId = resp.results[0].id;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await test('listCustomers (search field=value)', async () => {
|
|
79
|
+
const { json } = await callTool('listCustomers', { search: 'customer_name=Star' });
|
|
80
|
+
const resp = json?.api_response || json;
|
|
81
|
+
assert(resp?.total_count <= 10, `Expected filtered results, got ${resp?.total_count}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await test('listCustomers (search_name_external_id)', async () => {
|
|
85
|
+
const { json } = await callTool('listCustomers', { search_name_external_id: 'Star', limit: 5 });
|
|
86
|
+
const resp = json?.api_response || json;
|
|
87
|
+
assert(resp?.total_count <= 10, `Expected filtered results, got ${resp?.total_count}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await test('getCustomerById', async () => {
|
|
91
|
+
if (!customerId) throw new Error('No customer ID from previous test');
|
|
92
|
+
const { json } = await callTool('getCustomerById', { customerId });
|
|
93
|
+
const resp = json?.api_response || json;
|
|
94
|
+
assert(resp?.id === customerId, 'ID mismatch');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── CONTACTS ──────────────────────────────────────────
|
|
98
|
+
console.log('\n── Contacts ──');
|
|
99
|
+
|
|
100
|
+
let contactId;
|
|
101
|
+
await test('listContacts', async () => {
|
|
102
|
+
const { json } = await callTool('listContacts', { limit: 2 });
|
|
103
|
+
const resp = json?.api_response || json;
|
|
104
|
+
assert(resp?.results?.length > 0, 'No contacts');
|
|
105
|
+
contactId = resp.results[0].id;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await test('getContactById', async () => {
|
|
109
|
+
if (!contactId) throw new Error('No contact ID');
|
|
110
|
+
const { json } = await callTool('getContactById', { contactId });
|
|
111
|
+
const resp = json?.api_response || json;
|
|
112
|
+
assert(resp?.id === contactId, 'ID mismatch');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ─── INVOICES ──────────────────────────────────────────
|
|
116
|
+
console.log('\n── Invoices ──');
|
|
117
|
+
|
|
118
|
+
let invoiceId;
|
|
119
|
+
await test('listInvoices (basic)', async () => {
|
|
120
|
+
const { json } = await callTool('listInvoices', { limit: 2 });
|
|
121
|
+
const resp = json?.api_response || json;
|
|
122
|
+
assert(resp?.results?.length > 0, 'No invoices');
|
|
123
|
+
invoiceId = resp.results[0].id;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await test('listInvoices (customer_id filter)', async () => {
|
|
127
|
+
if (!customerId) throw new Error('No customer ID');
|
|
128
|
+
const { json } = await callTool('listInvoices', { customer_id: customerId, limit: 1 });
|
|
129
|
+
const resp = json?.api_response || json;
|
|
130
|
+
// Should filter (may return 0 if customer has no invoices, but should not error)
|
|
131
|
+
assert(resp?.total_count !== undefined, 'Missing total_count');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await test('listInvoices (invoice_total__gte filter)', async () => {
|
|
135
|
+
const { json } = await callTool('listInvoices', { invoice_total__gte: 100, limit: 1 });
|
|
136
|
+
const resp = json?.api_response || json;
|
|
137
|
+
assert(resp?.total_count !== undefined, 'Missing total_count');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await test('getInvoiceById', async () => {
|
|
141
|
+
if (!invoiceId) throw new Error('No invoice ID');
|
|
142
|
+
const { json } = await callTool('getInvoiceById', { invoiceId });
|
|
143
|
+
const resp = json?.api_response || json;
|
|
144
|
+
assert(resp?.id === invoiceId, 'ID mismatch');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await test('getInvoiceLineItems', async () => {
|
|
148
|
+
if (!invoiceId) throw new Error('No invoice ID');
|
|
149
|
+
const { json } = await callTool('getInvoiceLineItems', { invoiceId });
|
|
150
|
+
const resp = json?.api_response || json;
|
|
151
|
+
assert(resp !== null, 'No response');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await test('getInvoiceSummary', async () => {
|
|
155
|
+
if (!invoiceId) throw new Error('No invoice ID');
|
|
156
|
+
const { json } = await callTool('getInvoiceSummary', { invoiceId });
|
|
157
|
+
const resp = json?.api_response || json;
|
|
158
|
+
assert(resp !== null, 'No response');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await test('getAllInvoiceTags', async () => {
|
|
162
|
+
const { json } = await callTool('getAllInvoiceTags');
|
|
163
|
+
const resp = json?.api_response || json;
|
|
164
|
+
assert(resp?.tags, 'No tags returned');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ─── PAYMENTS ──────────────────────────────────────────
|
|
168
|
+
console.log('\n── Payments ──');
|
|
169
|
+
|
|
170
|
+
let paymentId;
|
|
171
|
+
await test('listAllPayments (basic)', async () => {
|
|
172
|
+
const { json } = await callTool('listAllPayments', { limit: 2 });
|
|
173
|
+
const resp = json?.api_response || json;
|
|
174
|
+
assert(resp?.results?.length > 0, 'No payments');
|
|
175
|
+
paymentId = resp.results[0].id;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await test('listAllPayments (sort_key + payment_method filter)', async () => {
|
|
179
|
+
const { json } = await callTool('listAllPayments', { sort_key: 'created_at', sort_type: 'DESC', payment_method: 'bank_transfer', limit: 1 });
|
|
180
|
+
const resp = json?.api_response || json;
|
|
181
|
+
assert(resp?.total_count !== undefined, 'Missing total_count');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await test('getPaymentById', async () => {
|
|
185
|
+
if (!paymentId) throw new Error('No payment ID');
|
|
186
|
+
const { json } = await callTool('getPaymentById', { paymentId });
|
|
187
|
+
const resp = json?.api_response || json;
|
|
188
|
+
assert(resp?.id === paymentId, 'ID mismatch');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ─── CONTRACTS ──────────────────────────────────────────
|
|
192
|
+
console.log('\n── Contracts ──');
|
|
193
|
+
|
|
194
|
+
await test('listContracts (basic)', async () => {
|
|
195
|
+
const { json } = await callTool('listContracts', { limit: 2 });
|
|
196
|
+
const resp = json?.api_response || json;
|
|
197
|
+
assert(resp?.results?.length > 0, 'No contracts');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await test('listContracts (status filter)', async () => {
|
|
201
|
+
const { json } = await callTool('listContracts', { status: 'active', limit: 1 });
|
|
202
|
+
const resp = json?.api_response || json;
|
|
203
|
+
assert(resp?.total_count !== undefined, 'Missing total_count');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─── AGGREGATES (Billable Metrics) ──────────────────────
|
|
207
|
+
console.log('\n── Billable Metrics ──');
|
|
208
|
+
|
|
209
|
+
let aggregateId;
|
|
210
|
+
await test('listAggregates', async () => {
|
|
211
|
+
const { json } = await callTool('listAggregates', { limit: 2 });
|
|
212
|
+
const resp = json?.api_response || json;
|
|
213
|
+
assert(resp?.results?.length > 0, 'No aggregates');
|
|
214
|
+
aggregateId = resp.results[0].id;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await test('getAggregateById', async () => {
|
|
218
|
+
if (!aggregateId) throw new Error('No aggregate ID');
|
|
219
|
+
const { json } = await callTool('getAggregateById', { aggregateId });
|
|
220
|
+
const resp = json?.api_response || json;
|
|
221
|
+
assert(resp?.id === aggregateId, 'ID mismatch');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await test('getAggregateSchemas', async () => {
|
|
225
|
+
const { json } = await callTool('getAggregateSchemas');
|
|
226
|
+
const resp = json?.api_response || json;
|
|
227
|
+
assert(Array.isArray(resp), 'Expected array of schemas');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await test('getAggregateEstimates (snake_case params)', async () => {
|
|
231
|
+
if (!aggregateId || !customerId) throw new Error('Missing IDs');
|
|
232
|
+
// This may return 404 domain error (no data) but should NOT 422
|
|
233
|
+
try {
|
|
234
|
+
const { json } = await callTool('getAggregateEstimates', {
|
|
235
|
+
aggregate_id: aggregateId,
|
|
236
|
+
customer_id: customerId,
|
|
237
|
+
start_date: '2025-01-01',
|
|
238
|
+
end_date: '2025-12-31',
|
|
239
|
+
});
|
|
240
|
+
// If we get here, great
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// 404 domain error is OK (AGGREGATE_ESTIMATE_NOT_FOUND), 422 is NOT
|
|
243
|
+
if (e.message?.includes('422')) throw new Error('Got 422 — snake_case params not working');
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ─── RAW METRICS ──────────────────────────────────────
|
|
248
|
+
console.log('\n── Raw Metrics ──');
|
|
249
|
+
|
|
250
|
+
let rawMetricSlug;
|
|
251
|
+
await test('listRawMetrics', async () => {
|
|
252
|
+
const { json } = await callTool('listRawMetrics', { limit: 2 });
|
|
253
|
+
const resp = json?.api_response || json;
|
|
254
|
+
assert(resp?.results?.length > 0, 'No raw metrics');
|
|
255
|
+
rawMetricSlug = resp.results[0].api_slug;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await test('getRawMetricBySlug', async () => {
|
|
259
|
+
if (!rawMetricSlug) throw new Error('No slug');
|
|
260
|
+
const { json } = await callTool('getRawMetricBySlug', { rawMetricSlug });
|
|
261
|
+
const resp = json?.api_response || json;
|
|
262
|
+
assert(resp?.api_slug === rawMetricSlug, 'Slug mismatch');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─── OTHER TOOLS ──────────────────────────────────────
|
|
266
|
+
console.log('\n── Other Tools ──');
|
|
267
|
+
|
|
268
|
+
await test('getCurrentDateTime', async () => {
|
|
269
|
+
const { json } = await callTool('getCurrentDateTime');
|
|
270
|
+
const resp = json?.api_response || json;
|
|
271
|
+
assert(resp?.currentDate || resp?.currentDateTime, 'No date returned');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await test('getCustomerPortalConfiguration', async () => {
|
|
275
|
+
const { json } = await callTool('getCustomerPortalConfiguration');
|
|
276
|
+
const resp = json?.api_response || json;
|
|
277
|
+
assert(resp !== null, 'No response');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ─── SUMMARY ──────────────────────────────────────────
|
|
281
|
+
console.log(`\n${'═'.repeat(50)}`);
|
|
282
|
+
console.log(`RESULTS: ${passed} passed, ${failed} failed out of ${passed + failed} tests`);
|
|
283
|
+
if (failures.length > 0) {
|
|
284
|
+
console.log('\nFailed tests:');
|
|
285
|
+
failures.forEach(f => console.log(` ❌ ${f.label}: ${f.error}`));
|
|
286
|
+
}
|
|
287
|
+
console.log(`${'═'.repeat(50)}\n`);
|
|
288
|
+
|
|
289
|
+
await client.close();
|
|
290
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
main().catch(err => {
|
|
294
|
+
console.error('Fatal error:', err);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|