iit-mcp-autotask 0.4.2 → 0.4.3

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
@@ -32,7 +32,7 @@ miss never fails the call (typed `absent`); only a missing root record fails the
32
32
  | C — dedicated workflow (23) | `autotask_ticket_query` · `autotask_ticket_full_context` · `autotask_ticket_sla_health_check` · `autotask_ticket_find_by_ticket_number` · `autotask_company_full_profile` · `autotask_timeEntry_query` · `autotask_timeEntry_create` · `autotask_quote_get` · `autotask_invoice_get` · `autotask_invoice_markup` · `autotask_configurationItem_profile` · `autotask_configurationItems_find_by_serial` · `autotask_contact_full_profile` · `autotask_contact_find_by_email` · `autotask_project_get_context` · `autotask_serviceCall_query` · `autotask_serviceCall_full_profile` · `autotask_contract_full_profile` · `autotask_contract_query` · `autotask_resource_workload` · `autotask_timeOffRequest_query` · `autotask_company_unbilled_work` · `autotask_aggregate` (names verbatim from the SDK manifest where they exist — case inconsistency included) | Bounded filtered lists, one-call composites, natural-key lookups, the single dedicated write |
33
33
 
34
34
  Machine-readable catalogue: [`capabilities.json`](capabilities.json) · server metadata: [`server.json`](server.json) ·
35
- per-workflow mapping: [`docs/capability-matrix.md`](docs/capability-matrix.md) · full design: [`docs/architecture.md`](docs/architecture.md).
35
+ per-workflow mapping: [`docs/capability-matrix.md`](docs/capability-matrix.md) · full design: [`docs/architecture.md`](docs/architecture.md) · contract P&L manual recipe (the `autotask_contract_profitability` gap until it ships): [`docs/contract-economics.md`](docs/contract-economics.md).
36
36
 
37
37
  ### The 5-META generic layer
38
38
 
package/capabilities.json CHANGED
@@ -539,12 +539,12 @@
539
539
  },
540
540
  {
541
541
  "id": "autotask.contract.full_profile",
542
- "description": "One contract + its services (+ units) + its billing rules in a single call: parts degrade to typed absent, never a failure; the contract itself missing → NOT_FOUND. Deeper machinery (bundles, adjustments, exclusions, charges): autotask_read; a contract list: autotask_contract_query.",
542
+ "description": "One contract + its services + billing rules + recent charges (50 newest) + service units in one call: parts degrade to typed absent, never a failure; the contract itself missing → NOT_FOUND. Deeper machinery (bundles, adjustments, exclusions): autotask_read; contract list: autotask_contract_query.",
543
543
  "mcpPrimitive": "tool",
544
544
  "toolName": "autotask_contract_full_profile",
545
545
  "type": "read",
546
546
  "layer": "C",
547
- "backingOperation": "contracts.get + contract_services.query + contract_billing_rules.query",
547
+ "backingOperation": "contracts.get + contract_services.query + contract_billing_rules.query + contract_charges.query + contract_service_units.query",
548
548
  "annotations": {
549
549
  "readOnly": true,
550
550
  "idempotent": true,
@@ -144,7 +144,7 @@ export declare function cacheOptionsForPreset(preset: 'on' | 'off', fieldCacheTt
144
144
  * fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
145
145
  * describes the SDK behind it.
146
146
  */
147
- export declare const BUILT_AGAINST_PLAN_HASH = "4c8d9f800e97c350acc681ade183f67474c044b674494eb78cd0f14a933dc1fc";
147
+ export declare const BUILT_AGAINST_PLAN_HASH = "6547153895b65c577ef59ced6c06361d0763d1299d6048531b8ffa94f77821d3";
148
148
  /**
149
149
  * Fail fast when the installed SDK's plan has moved out from under this surface.
150
150
  * Called at boot (main.ts) and safe to call repeatedly (pure comparison).
@@ -273,7 +273,7 @@ export const TOOL_DESCRIPTIONS = {
273
273
  autotask_project_get_context: `One project with its primary context: project + company + opportunity + lead resource. Project phase/task lists do NOT exist on this surface (no such vendor operations) — do not attempt them; charges/notes ride the generic layer (autotask_read). The project record alone: autotask_project_resolve.`,
274
274
  autotask_serviceCall_query: `Query ServiceCall records with a JSON filter — the on-site visit queue. Full profile (linked tickets, tasks, assignments): autotask_serviceCall_full_profile. Assignment writes ride autotask_write. Bounded: limit default 50, max 2500. per-call options: see the server instructions.`,
275
275
  autotask_serviceCall_full_profile: `One service call + its linked tickets + its tasks (incl. resource assignments). Parts degrade to typed absent (no tasks → NO_TASKS on the assignments part); the service call itself missing → NOT_FOUND. Prefer this before on-site-visit work (assignment writes ride autotask_write).`,
276
- autotask_contract_full_profile: `One contract + its services (+ units) + its billing rules in a single call: parts degrade to typed absent, never a failure; the contract itself missing → NOT_FOUND. Deeper machinery (bundles, adjustments, exclusions, charges): autotask_read; a contract list: autotask_contract_query.`,
276
+ autotask_contract_full_profile: `One contract + its services + billing rules + recent charges (50 newest) + service units in one call: parts degrade to typed absent, never a failure; the contract itself missing → NOT_FOUND. Deeper machinery (bundles, adjustments, exclusions): autotask_read; contract list: autotask_contract_query.`,
277
277
  autotask_contract_query: `Query Contract records with a JSON filter — e.g. active contracts for a company (the SLA & recurring-service view). One contract with its services/units/billing rules: autotask_contract_full_profile. Bounded: limit default 50, max 2500. per-call options: see the server instructions.`,
278
278
  autotask_resource_workload: `One resource's availability picture: open assigned tickets + service-call assignments + time-off balances + pending time-off requests. Parts degrade to typed absent; resource missing → NOT_FOUND. Approve/reject: autotask_write. The request list: autotask_timeOffRequest_query. Sensitive.`,
279
279
  autotask_timeOffRequest_query: `Query TimeOffRequest records with a JSON filter. Approve/reject ride autotask_write (time_off_requests.approve / .reject — dry-run-first; reject takes a reason). Availability: autotask_resource_workload. Bounded: limit default 50, max 2500. per-call options: see the server instructions.`,
@@ -339,7 +339,7 @@ export function cacheOptionsForPreset(preset, fieldCacheTtlMs) {
339
339
  * fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
340
340
  * describes the SDK behind it.
341
341
  */
342
- export const BUILT_AGAINST_PLAN_HASH = '4c8d9f800e97c350acc681ade183f67474c044b674494eb78cd0f14a933dc1fc';
342
+ export const BUILT_AGAINST_PLAN_HASH = '6547153895b65c577ef59ced6c06361d0763d1299d6048531b8ffa94f77821d3';
343
343
  /**
344
344
  * Fail fast when the installed SDK's plan has moved out from under this surface.
345
345
  * Called at boot (main.ts) and safe to call repeatedly (pure comparison).
@@ -13,7 +13,14 @@
13
13
  *
14
14
  * Parts run as bounded fan-outs over ONE pooled tenant client: the client's own concurrency
15
15
  * guard (the agent profile) bounds the in-flight vendor requests; the part list is small and
16
- * static per tool.
16
+ * static per tool. Wire budget per call: the contract profile is the widest — 1 root read + 4
17
+ * parts in one parallel fan-out (contract_services.query / contract_billing_rules.query /
18
+ * contract_charges.query / contract_service_units.query, each limit 50 → one page), EXCEPT the
19
+ * charges part, which rides the SDK's count-anchored `latest` 50 (the vendor has no sort
20
+ * parameter; newest-first = id-newest, tracking the vendor's createDate/datePurchased) — 1 count
21
+ * + at most 6 page POSTs while the matching set is <= 2500 rows (the SDK's full-set mode); above
22
+ * that the SDK spends its own id-threshold count-probe budget before a <= 6-page walk, and a
23
+ * blind surface takes the plain bounded walk. Common case: at most 11 vendor requests per call.
17
24
  */
18
25
  import { NotFoundError } from 'node-autotask';
19
26
  import { TICKET_FULL_CONTEXT_INPUT, COMPANY_FULL_PROFILE_INPUT, CONFIGURATION_ITEM_PROFILE_INPUT, PROJECT_GET_CONTEXT_INPUT, QUOTE_GET_INPUT, INVOICE_GET_INPUT, INVOICE_MARKUP_INPUT, CONTACT_FULL_PROFILE_INPUT, SERVICE_CALL_FULL_PROFILE_INPUT, CONTRACT_FULL_PROFILE_INPUT, RESOURCE_WORKLOAD_INPUT, UNBILLED_WORK_INPUT, AGGREGATE_INPUT, COMPOSITE_OUTPUT, resultOutput, aggregateOutput, unbilledWorkOutput, closedInput, } from './schemas.js';
@@ -25,6 +32,11 @@ import { assertSecretReadPermitted, assertWritePermitted } from '../policy.js';
25
32
  const TICKET_STATUS_CLOSED = 5;
26
33
  /** The cap on a fan-out `in` filter (the task-id join between two parts). */
27
34
  const IN_FILTER_CAP = 100;
35
+ /** The charges part's recent-N cap (issue #49): the N newest contract charges, newest first —
36
+ * served through the SDK's count-anchored `latest` (the vendor has no sort parameter). */
37
+ const CHARGES_RECENT_CAP = 50;
38
+ /** The units part's row cap (issue #49). */
39
+ const SERVICE_UNITS_CAP = 50;
28
40
  /** Run one part read: a miss/fault degrades to `{ value: null, absent: code }`, never a throw. */
29
41
  async function part(ctx, op, args, signal) {
30
42
  try {
@@ -370,12 +382,13 @@ function registerServiceCallFullProfile(server, ctx) {
370
382
  },
371
383
  });
372
384
  }
373
- // ---- autotask_contract_full_profile: the contract, its services, its billing rules ----
385
+ // ---- autotask_contract_full_profile: the contract, its services, its billing rules,
386
+ // ---- its recent charges (issue #49) and its service units (issue #49) ----
374
387
  function registerContractFullProfile(server, ctx) {
375
388
  const contractInput = closedInput(CONTRACT_FULL_PROFILE_INPUT);
376
389
  registerServerComposite(ctx, server, {
377
390
  tool: 'autotask_contract_full_profile',
378
- title: 'Get a contract with its services and billing rules',
391
+ title: 'Get a contract with its services, billing rules, recent charges and service units',
379
392
  description: ctx.description('autotask_contract_full_profile', 'contracts.get'),
380
393
  input: contractInput,
381
394
  rootOp: 'contracts.get',
@@ -383,16 +396,34 @@ function registerContractFullProfile(server, ctx) {
383
396
  const id = args.id;
384
397
  // The declared per-part limits (QUERY_LIMIT: 1-2500, default 50 — schema-validated at the
385
398
  // protocol layer); each part reads its own declared argument, not one shared phantom key.
399
+ // The two issue #49 parts carry NO input knobs (the closed bag is unchanged): the charges
400
+ // part is the FIXED recent-N and the units part the fixed row cap.
386
401
  const serviceLimit = args.service_limit ?? 50;
387
402
  const billingRuleLimit = args.billing_rule_limit ?? 50;
388
403
  const root = await c.ops.invokeRead(c.client, 'contracts.get', [id], undefined, signal);
389
404
  if (root === undefined)
390
405
  return { root: null, rootOp: 'contracts.get', parts: {} };
391
- const [services, billingRules] = await Promise.all([
406
+ const [services, billingRules, charges, units] = await Promise.all([
392
407
  part(c, 'contract_services.query', [{ filter: [{ field: 'contractID', op: 'eq', value: id }], limit: serviceLimit }], signal),
393
408
  part(c, 'contract_billing_rules.query', [{ filter: [{ field: 'contractID', op: 'eq', value: id }], limit: billingRuleLimit }], signal),
409
+ // The recent charges (issue #49): the vendor has NO sort parameter — the SDK's
410
+ // count-anchored `latest` (the N highest-id rows, id-descending) is the only ordering
411
+ // channel; the id order tracks the vendor's createDate/datePurchased, so newest-first
412
+ // is id-newest-first. limit === latest is the legal coexistence (limit >= latest) and
413
+ // keeps the walk window explicit (1 count + at most 6 page POSTs).
414
+ part(c, 'contract_charges.query', [{ filter: [{ field: 'contractID', op: 'eq', value: id }], limit: CHARGES_RECENT_CAP, latest: CHARGES_RECENT_CAP }], signal),
415
+ part(c, 'contract_service_units.query', [{ filter: [{ field: 'contractID', op: 'eq', value: id }], limit: SERVICE_UNITS_CAP }], signal),
394
416
  ]);
395
- return { root, rootOp: 'contracts.get', parts: { services: ['contract_services.query', services], billing_rules: ['contract_billing_rules.query', billingRules] } };
417
+ return {
418
+ root,
419
+ rootOp: 'contracts.get',
420
+ parts: {
421
+ services: ['contract_services.query', services],
422
+ billing_rules: ['contract_billing_rules.query', billingRules],
423
+ charges: ['contract_charges.query', charges],
424
+ units: ['contract_service_units.query', units],
425
+ },
426
+ };
396
427
  },
397
428
  });
398
429
  }
@@ -385,7 +385,11 @@ export declare function queryEnvelope(fields?: readonly string[]): z.ZodType;
385
385
  /** The five bounded query tools' output: the typed envelope under `result` (mirrors `resultOutput`). */
386
386
  export declare function queryEnvelopeOutput(fields?: readonly string[]): z.ZodType;
387
387
  /** A composite answer (SDK or server-built): the root record + one entry per part; a part miss
388
- * is `null + absent`, never a tool failure — only a missing ROOT fails the composite. */
388
+ * is `null + absent`, never a tool failure — only a missing ROOT fails the composite.
389
+ * The part names are tool-specific (this shared envelope stays a record): the contract
390
+ * profile serves `services` / `billing_rules` / `charges` (the recent-50 via the SDK's
391
+ * count-anchored `latest`, newest first — issue #49) / `units` (issue #49), every one riding
392
+ * the same value/absent envelope. */
389
393
  export declare const COMPOSITE_OUTPUT: z.ZodObject<{
390
394
  root: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
391
395
  parts: z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -408,6 +408,8 @@ export const CONTRACT_FULL_PROFILE_INPUT = z.object({
408
408
  id: ID.describe('The numeric contract id.'),
409
409
  service_limit: QUERY_LIMIT,
410
410
  billing_rule_limit: QUERY_LIMIT,
411
+ // issue #49: the two new parts (charges — the FIXED recent-50 via the SDK's `latest`; units —
412
+ // the fixed 50-row cap) carry NO input keys: the closed bag above is unchanged.
411
413
  });
412
414
  export const RESOURCE_WORKLOAD_INPUT = z.object({
413
415
  id: ID.describe('The numeric resource id.'),
@@ -588,7 +590,11 @@ export function queryEnvelopeOutput(fields = []) {
588
590
  return z.object({ result: queryEnvelope(fields) });
589
591
  }
590
592
  /** A composite answer (SDK or server-built): the root record + one entry per part; a part miss
591
- * is `null + absent`, never a tool failure — only a missing ROOT fails the composite. */
593
+ * is `null + absent`, never a tool failure — only a missing ROOT fails the composite.
594
+ * The part names are tool-specific (this shared envelope stays a record): the contract
595
+ * profile serves `services` / `billing_rules` / `charges` (the recent-50 via the SDK's
596
+ * count-anchored `latest`, newest first — issue #49) / `units` (issue #49), every one riding
597
+ * the same value/absent envelope. */
592
598
  export const COMPOSITE_OUTPUT = z.object({
593
599
  root: dataField('The root record (a missing root fails the call with the typed vendor error).'),
594
600
  parts: z
@@ -24,7 +24,7 @@ import type { Config } from '../config.js';
24
24
  import type { Logger } from '../logger.js';
25
25
  /** The wire identity of this server (the initialize answer + the `/metadata` payload). */
26
26
  export declare const SERVER_NAME = "iit-mcp-autotask";
27
- export declare const SERVER_VERSION = "0.4.2";
27
+ export declare const SERVER_VERSION = "0.4.3";
28
28
  export declare const SERVER_TITLE = "Autotask MCP Server";
29
29
  /**
30
30
  * The MCP protocol era this server advertises (the `/metadata` stamp, both levels).
@@ -62,7 +62,7 @@ export declare const SERVER_INSTRUCTIONS: string;
62
62
  /** The full identity stamp (the `/metadata` build block — the era pair, FR-2). */
63
63
  export declare const SERVER_IDENTITY: {
64
64
  readonly name: "iit-mcp-autotask";
65
- readonly version: "0.4.2";
65
+ readonly version: "0.4.3";
66
66
  readonly mcpProtocolBaseline: "2026-07-28";
67
67
  readonly legacyProtocolFallback: "2025-11-25";
68
68
  };
@@ -27,7 +27,7 @@ import { UNTRUSTED_NOTE } from './catalog-data.js';
27
27
  import { registerTools } from './register.js';
28
28
  /** The wire identity of this server (the initialize answer + the `/metadata` payload). */
29
29
  export const SERVER_NAME = 'iit-mcp-autotask';
30
- export const SERVER_VERSION = '0.4.2';
30
+ export const SERVER_VERSION = '0.4.3';
31
31
  export const SERVER_TITLE = 'Autotask MCP Server';
32
32
  /**
33
33
  * The MCP protocol era this server advertises (the `/metadata` stamp, both levels).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iit-mcp-autotask",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "MCP server for the Autotask (Datto) PSA domain, built on the node-autotask SDK",
5
5
  "license": "MIT",
6
6
  "author": "Max Soukhomlinov",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@modelcontextprotocol/server": "^2.2.0",
50
- "node-autotask": "0.10.0",
50
+ "node-autotask": "0.11.0",
51
51
  "zod": "^4.4.3"
52
52
  },
53
53
  "devDependencies": {
package/server.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iit-mcp-autotask",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "MCP server for the Autotask (Datto) PSA API, powered by the node-autotask SDK",
5
5
  "mcpProtocolBaseline": "2026-07-28",
6
6
  "legacyProtocolFallback": "2025-11-25",
@@ -29,7 +29,7 @@
29
29
  "capabilitiesManifest": "capabilities.json",
30
30
  "sdk": {
31
31
  "package": "node-autotask",
32
- "versionRange": "0.10.0"
32
+ "versionRange": "0.11.0"
33
33
  },
34
34
  "vendorExtensions": false
35
35
  }