mcp-zenskar 1.1.9 → 1.1.10

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/README.md CHANGED
@@ -239,7 +239,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
239
239
  | `listRawMetrics` | List Usage Events with filtering; backend/API may also call these raw metrics |
240
240
  | `createRawMetric` | Create a Usage Event schema; backend/API calls this a raw metric |
241
241
  | `getRawMetricById` | Get a Usage Event by ID; backend/API may also call it a raw metric |
242
- | `getRawMetricLogs` | Get logs for a Usage Event (raw metric) |
242
+ | `getRawMetricLogs` | Get recent event rows for a Usage Event using the same preview-query path as the frontend |
243
243
  | `getRawMetricBySlug` | Get a Usage Event by API slug; backend/API may also call it a raw metric |
244
244
  | `ingestRawMetricEvent` | Ingest a usage event |
245
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-zenskar",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Model Context Protocol (MCP) server for Zenskar API - customer management, invoicing, and billing operations",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -1060,7 +1060,7 @@
1060
1060
  },
1061
1061
  {
1062
1062
  "name": "getRawMetricLogs",
1063
- "description": "Get Usage Event logs for a specific resource by ID. Backend/API terminology may also call this a raw metric.",
1063
+ "description": "Get Usage Event event rows for a specific resource by ID. This follows the same visual-preview query path used by the frontend meter page, so it returns recent event rows rather than only backend sample logs. Backend/API terminology may also call this a raw metric.",
1064
1064
  "args": [
1065
1065
  {
1066
1066
  "name": "rawMetricId",
@@ -1068,6 +1068,43 @@
1068
1068
  "type": "string",
1069
1069
  "required": true,
1070
1070
  "position": "path"
1071
+ },
1072
+ {
1073
+ "name": "filters",
1074
+ "description": "Optional visual-query filter array, matching the frontend usage-event viewer format. Default is an empty filter list.",
1075
+ "type": "array",
1076
+ "required": false,
1077
+ "position": "body"
1078
+ },
1079
+ {
1080
+ "name": "limit",
1081
+ "description": "Maximum number of event rows to return. Defaults to 20, matching frontend behavior.",
1082
+ "type": "integer",
1083
+ "required": false,
1084
+ "position": "body",
1085
+ "default": 20
1086
+ },
1087
+ {
1088
+ "name": "offset",
1089
+ "description": "Pagination offset for event rows. Defaults to 0.",
1090
+ "type": "integer",
1091
+ "required": false,
1092
+ "position": "body",
1093
+ "default": 0
1094
+ },
1095
+ {
1096
+ "name": "aggregate_operation",
1097
+ "description": "Optional aggregate operation object used by the frontend for event counts (for example COUNT on timestamp). Leave null for normal row listing.",
1098
+ "type": "object",
1099
+ "required": false,
1100
+ "position": "body"
1101
+ },
1102
+ {
1103
+ "name": "order_by",
1104
+ "description": "Optional ordering array. Defaults to [{column:'timestamp', type:'DESC'}] to match frontend event viewing.",
1105
+ "type": "array",
1106
+ "required": false,
1107
+ "position": "body"
1071
1108
  }
1072
1109
  ],
1073
1110
  "requestTemplate": {
@@ -1075,7 +1112,7 @@
1075
1112
  "method": "GET"
1076
1113
  },
1077
1114
  "responseTemplate": {
1078
- "prependBody": "## Usage Event Logs\n\nHere are the logs for the specified usage event:\n\n"
1115
+ "prependBody": "## Usage Event Logs\n\nHere are recent event rows for the specified usage event:\n\n"
1079
1116
  }
1080
1117
  },
1081
1118
  {
package/src/server.js CHANGED
@@ -211,6 +211,20 @@ function normalizeAccountCategory(category) {
211
211
  return normalized;
212
212
  }
213
213
 
214
+ function toDisplayAmount(value) {
215
+ const num = Number(value);
216
+ if (!Number.isFinite(num)) return null;
217
+ return num / 100;
218
+ }
219
+
220
+ function normalizeReportAccountCategory(reportType, category) {
221
+ const normalized = normalizeAccountCategory(category);
222
+ if (reportType === 'getBalanceSheet' && normalized === 'Liabilities') {
223
+ return 'Liabilities & Equity';
224
+ }
225
+ return normalized;
226
+ }
227
+
214
228
  function getCategoryOrder(reportType, category) {
215
229
  const balanceSheetOrder = {
216
230
  Assets: 1,
@@ -247,7 +261,7 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
247
261
  const sectionsMap = new Map();
248
262
 
249
263
  rows.forEach(row => {
250
- const category = normalizeAccountCategory(row.account_category);
264
+ const category = normalizeReportAccountCategory(reportType, row.account_category);
251
265
  const periodKey = buildPeriodKey(row);
252
266
  const periodEntry = periodMap.get(periodKey) || {
253
267
  key: periodKey,
@@ -290,8 +304,11 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
290
304
  interval_start: row.interval_start || null,
291
305
  interval_end: row.interval_end || null,
292
306
  balance: row.balance ?? 0,
307
+ display_balance: toDisplayAmount(row.balance ?? 0),
293
308
  debits: row.debits ?? 0,
294
- credits: row.credits ?? 0
309
+ display_debits: toDisplayAmount(row.debits ?? 0),
310
+ credits: row.credits ?? 0,
311
+ display_credits: toDisplayAmount(row.credits ?? 0)
295
312
  };
296
313
  account.total_balance += Number(row.balance || 0);
297
314
  account.total_debits += Number(row.debits || 0);
@@ -302,11 +319,17 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
302
319
  interval_end: row.interval_end || null,
303
320
  balance: 0,
304
321
  debits: 0,
305
- credits: 0
322
+ credits: 0,
323
+ display_balance: 0,
324
+ display_debits: 0,
325
+ display_credits: 0
306
326
  };
307
327
  sectionPeriod.balance += Number(row.balance || 0);
308
328
  sectionPeriod.debits += Number(row.debits || 0);
309
329
  sectionPeriod.credits += Number(row.credits || 0);
330
+ sectionPeriod.display_balance = toDisplayAmount(sectionPeriod.balance);
331
+ sectionPeriod.display_debits = toDisplayAmount(sectionPeriod.debits);
332
+ sectionPeriod.display_credits = toDisplayAmount(sectionPeriod.credits);
310
333
  section.totals_by_period[periodKey] = sectionPeriod;
311
334
  section.total_balance += Number(row.balance || 0);
312
335
  section.total_debits += Number(row.debits || 0);
@@ -328,8 +351,11 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
328
351
  })),
329
352
  totals_by_period: summarizePeriods(section.totals_by_period),
330
353
  total_balance: section.total_balance,
354
+ display_total_balance: toDisplayAmount(section.total_balance),
331
355
  total_debits: section.total_debits,
332
- total_credits: section.total_credits
356
+ display_total_debits: toDisplayAmount(section.total_debits),
357
+ total_credits: section.total_credits,
358
+ display_total_credits: toDisplayAmount(section.total_credits)
333
359
  }));
334
360
 
335
361
  return {
@@ -339,6 +365,127 @@ function buildAccountingStatementView(reportType, rows, accountLookup) {
339
365
  };
340
366
  }
341
367
 
368
+ function normalizeChartAccountCategory(account) {
369
+ if (!account) return '';
370
+ const category = account.account_category || '';
371
+ if (category === 'Liabilities') return 'Liabilities & Equity';
372
+ return category;
373
+ }
374
+
375
+ function buildChartOfAccountsView(accounts) {
376
+ const baseAccounts = [...accounts]
377
+ .map(account => ({
378
+ ...account,
379
+ account_category: normalizeChartAccountCategory(account)
380
+ }))
381
+ .sort((a, b) => {
382
+ const categoryDiff = normalizeChartAccountCategory(a).localeCompare(normalizeChartAccountCategory(b));
383
+ return categoryDiff !== 0 ? categoryDiff : String(a.name || '').localeCompare(String(b.name || ''));
384
+ });
385
+
386
+ const grouped = new Map();
387
+ baseAccounts.forEach(account => {
388
+ const category = normalizeChartAccountCategory(account) || 'Uncategorized';
389
+ if (!grouped.has(category)) grouped.set(category, []);
390
+ grouped.get(category).push(account);
391
+ });
392
+
393
+ const syntheticGroups = ['Assets', 'Liabilities & Equity', 'Equity', 'Income', 'Expenses'];
394
+ syntheticGroups.forEach(category => {
395
+ if (!grouped.has(category)) grouped.set(category, []);
396
+ });
397
+
398
+ const sections = Array.from(grouped.entries())
399
+ .sort((a, b) => a[0].localeCompare(b[0]))
400
+ .map(([category, entries]) => ({
401
+ id: category,
402
+ name: category,
403
+ description: category,
404
+ account_category: category,
405
+ is_parent: true,
406
+ parent_path: null,
407
+ children: entries.map(entry => ({
408
+ ...entry,
409
+ parent_path: entry.parent_path || category
410
+ }))
411
+ }));
412
+
413
+ const liabEqSection = sections.find(section => section.name === 'Liabilities & Equity');
414
+ if (liabEqSection) {
415
+ const hasEquity = liabEqSection.children.some(child => child.name === 'Equity');
416
+ if (!hasEquity) {
417
+ liabEqSection.children.push({
418
+ id: 'Equity',
419
+ name: 'Equity',
420
+ description: 'Equity',
421
+ account_category: 'Liabilities & Equity',
422
+ balance_normality: 'credit',
423
+ is_parent: true,
424
+ parent_path: 'Liabilities & Equity',
425
+ custom_data: { default_account: false },
426
+ children: []
427
+ });
428
+ liabEqSection.children.push({
429
+ id: 'Equity:Retained Earnings',
430
+ name: 'Retained Earnings',
431
+ description: 'Retained Earnings',
432
+ account_category: 'Liabilities & Equity',
433
+ balance_normality: 'credit',
434
+ is_parent: false,
435
+ parent_path: 'Equity',
436
+ custom_data: { default_account: false }
437
+ });
438
+ }
439
+ }
440
+
441
+ return sections;
442
+ }
443
+
444
+ function enrichJobsResult(result) {
445
+ if (!result || !Array.isArray(result.results)) return result;
446
+ const counts = result.results.reduce((acc, job) => {
447
+ const status = job.status || 'unknown';
448
+ acc[status] = (acc[status] || 0) + 1;
449
+ return acc;
450
+ }, {});
451
+
452
+ return {
453
+ ...result,
454
+ summary: {
455
+ total_count: result.total_count ?? result.results.length,
456
+ returned_count: result.results.length,
457
+ page_status_counts: counts,
458
+ has_more: !!result.next,
459
+ note: result.next
460
+ ? `Showing ${result.results.length} jobs from the current page. Use the cursor to continue through the remaining jobs.`
461
+ : `Showing ${result.results.length} jobs from the current page.`
462
+ }
463
+ };
464
+ }
465
+
466
+ function enrichListAccountsResult(result) {
467
+ if (!result || !Array.isArray(result.results)) return result;
468
+ const sortedResults = [...result.results].sort((a, b) => {
469
+ const categoryA = normalizeAccountCategory(a.account_category);
470
+ const categoryB = normalizeAccountCategory(b.account_category);
471
+ const categoryDiff = categoryA.localeCompare(categoryB);
472
+ return categoryDiff !== 0 ? categoryDiff : String(a.name || '').localeCompare(String(b.name || ''));
473
+ });
474
+
475
+ const grouped = sortedResults.reduce((acc, account) => {
476
+ const category = normalizeAccountCategory(account.account_category);
477
+ acc[category] = acc[category] || [];
478
+ acc[category].push(account);
479
+ return acc;
480
+ }, {});
481
+
482
+ return {
483
+ ...result,
484
+ results: sortedResults,
485
+ grouped_view: grouped
486
+ };
487
+ }
488
+
342
489
  async function fetchJson(url, headers) {
343
490
  const response = await fetch(url, { method: 'GET', headers });
344
491
  const text = await response.text();
@@ -353,6 +500,58 @@ async function fetchJson(url, headers) {
353
500
  }
354
501
  }
355
502
 
503
+ async function executeFrontendStyleRawMetricLogs(args, headers, baseUrl) {
504
+ const rawMetricId = args.rawMetricId;
505
+ const rawMetricUrl = `${baseUrl}/rawmetric/${encodeURIComponent(rawMetricId)}`;
506
+ const rawMetric = await fetchJson(rawMetricUrl, headers);
507
+
508
+ if (!rawMetric?.api_slug) {
509
+ throw new Error(`Unable to resolve api_slug for raw metric ${rawMetricId}`);
510
+ }
511
+
512
+ const previewHeaders = {
513
+ ...headers,
514
+ apiversion: '20240301'
515
+ };
516
+
517
+ const payload = {
518
+ limit: args.limit ?? 20,
519
+ offset: args.offset ?? 0,
520
+ order_by: args.order_by ?? [{ column: 'timestamp', type: 'DESC' }],
521
+ aggregate_operation: args.aggregate_operation ?? null,
522
+ customer_mapping: null,
523
+ end_date_mapping: null,
524
+ start_date_mapping: null,
525
+ table_name: `raw_metric_${rawMetric.api_slug}`,
526
+ visual_query: {
527
+ groups: [
528
+ {
529
+ filters: Array.isArray(args.filters) ? args.filters : [],
530
+ logic: 'AND'
531
+ }
532
+ ],
533
+ logic: 'AND'
534
+ }
535
+ };
536
+
537
+ const response = await fetch(`${baseUrl}/aggregate/visualquery/preview`, {
538
+ method: 'POST',
539
+ headers: previewHeaders,
540
+ body: JSON.stringify(payload)
541
+ });
542
+
543
+ const responseText = await response.text();
544
+ if (!response.ok) {
545
+ throw new Error(`API request failed: ${response.status} ${response.statusText}\nResponse: ${responseText}`);
546
+ }
547
+
548
+ try {
549
+ return JSON.parse(responseText);
550
+ } catch (_error) {
551
+ return responseText;
552
+ }
553
+ }
554
+
356
555
  async function enrichAccountingReportResult(toolName, result, headers, baseUrl) {
357
556
  if (!result || !Array.isArray(result.results)) {
358
557
  return result;
@@ -374,16 +573,92 @@ async function enrichAccountingReportResult(toolName, result, headers, baseUrl)
374
573
  ...row,
375
574
  account_name: account.name || row.account_id || null,
376
575
  account_description: account.description || null,
377
- account_category: normalizeAccountCategory(account.account_category),
576
+ account_category: normalizeReportAccountCategory(toolName, account.account_category),
378
577
  account_parent_path: account.parent_path || null,
379
- balance_normality: account.balance_normality || null
578
+ balance_normality: account.balance_normality || null,
579
+ display_balance: toDisplayAmount(row.balance ?? 0),
580
+ display_debits: toDisplayAmount(row.debits ?? 0),
581
+ display_credits: toDisplayAmount(row.credits ?? 0)
380
582
  };
381
583
  });
382
584
 
585
+ let statementView = buildAccountingStatementView(toolName, enrichedRows, accountLookup);
586
+
587
+ if (toolName === 'getBalanceSheet') {
588
+ const params = new URLSearchParams();
589
+ if (headers.apiversion) params.set('apiversion', headers.apiversion);
590
+ const incomeStatementUrl = `${baseUrl}/accounting_new/income_statement/v2`;
591
+ const incomeStatement = await fetchJson(incomeStatementUrl, headers);
592
+ const incomeRows = Array.isArray(incomeStatement?.results) ? incomeStatement.results : [];
593
+ const retainedByPeriod = {};
594
+
595
+ incomeRows.forEach(row => {
596
+ const periodKey = buildPeriodKey(row);
597
+ const period = retainedByPeriod[periodKey] || {
598
+ interval_start: row.interval_start || null,
599
+ interval_end: row.interval_end || null,
600
+ balance: 0,
601
+ debits: 0,
602
+ credits: 0
603
+ };
604
+ period.balance += Number(row.balance || 0);
605
+ period.debits += Number(row.debits || 0);
606
+ period.credits += Number(row.credits || 0);
607
+ retainedByPeriod[periodKey] = period;
608
+ });
609
+
610
+ const retainedPeriods = summarizePeriods(
611
+ Object.fromEntries(
612
+ Object.entries(retainedByPeriod).map(([key, value]) => [
613
+ key,
614
+ {
615
+ ...value,
616
+ display_balance: toDisplayAmount(value.balance),
617
+ display_debits: toDisplayAmount(value.debits),
618
+ display_credits: toDisplayAmount(value.credits)
619
+ }
620
+ ])
621
+ )
622
+ );
623
+
624
+ const retainedAccount = {
625
+ account_id: 'Equity:Retained Earnings',
626
+ account_name: 'Retained Earnings (Derived)',
627
+ account_description: 'Derived from the companion income statement for MCP presentation.',
628
+ account_category: 'Liabilities & Equity',
629
+ parent_path: 'Equity',
630
+ balance_normality: 'credit',
631
+ periods: retainedPeriods,
632
+ total_balance: retainedPeriods.reduce((sum, period) => sum + Number(period.balance || 0), 0),
633
+ display_total_balance: toDisplayAmount(retainedPeriods.reduce((sum, period) => sum + Number(period.balance || 0), 0)),
634
+ total_debits: retainedPeriods.reduce((sum, period) => sum + Number(period.debits || 0), 0),
635
+ display_total_debits: toDisplayAmount(retainedPeriods.reduce((sum, period) => sum + Number(period.debits || 0), 0)),
636
+ total_credits: retainedPeriods.reduce((sum, period) => sum + Number(period.credits || 0), 0),
637
+ display_total_credits: toDisplayAmount(retainedPeriods.reduce((sum, period) => sum + Number(period.credits || 0), 0))
638
+ };
639
+
640
+ let liabEqSection = statementView.sections.find(section => section.category === 'Liabilities & Equity');
641
+ if (!liabEqSection) {
642
+ liabEqSection = {
643
+ category: 'Liabilities & Equity',
644
+ accounts: [],
645
+ totals_by_period: [],
646
+ total_balance: 0,
647
+ display_total_balance: 0,
648
+ total_debits: 0,
649
+ display_total_debits: 0,
650
+ total_credits: 0,
651
+ display_total_credits: 0
652
+ };
653
+ statementView.sections.push(liabEqSection);
654
+ }
655
+ liabEqSection.accounts.push(retainedAccount);
656
+ }
657
+
383
658
  return {
384
659
  ...result,
385
660
  results: enrichedRows,
386
- statement_view: buildAccountingStatementView(toolName, enrichedRows, accountLookup)
661
+ statement_view: statementView
387
662
  };
388
663
  } catch (error) {
389
664
  logger.warn(`[${toolName}] Failed to enrich accounting report output; returning raw report`, {
@@ -770,6 +1045,13 @@ async function executeAPICall(tool, args) {
770
1045
  });
771
1046
 
772
1047
  try {
1048
+ if (tool.name === 'getRawMetricLogs') {
1049
+ const baseUrl = process.env.ZENSKAR_API_BASE_URL || 'https://api.zenskar.com';
1050
+ const result = await executeFrontendStyleRawMetricLogs(cleanArgs, headers, baseUrl);
1051
+ logger.debug(`[${tool.name}] Successfully processed frontend-style usage-event logs`);
1052
+ return result;
1053
+ }
1054
+
773
1055
  const response = await fetch(fullUrl, {
774
1056
  method,
775
1057
  headers,
@@ -801,6 +1083,18 @@ async function executeAPICall(tool, args) {
801
1083
  const baseUrl = process.env.ZENSKAR_API_BASE_URL || 'https://api.zenskar.com';
802
1084
  result = await enrichAccountingReportResult(tool.name, result, headers, baseUrl);
803
1085
  }
1086
+ if (tool.name === 'listJobs') {
1087
+ result = enrichJobsResult(result);
1088
+ }
1089
+ if (tool.name === 'listAccounts') {
1090
+ result = enrichListAccountsResult(result);
1091
+ }
1092
+ if (tool.name === 'getChartOfAccounts' && Array.isArray(result)) {
1093
+ result = {
1094
+ raw_accounts: result,
1095
+ chart_view: buildChartOfAccountsView(result)
1096
+ };
1097
+ }
804
1098
 
805
1099
  // Apply response template if available
806
1100
  if (tool.responseTemplate?.prependBody) {