iit-mcp-autotask 0.13.0 → 0.14.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/README.md CHANGED
@@ -8,10 +8,10 @@ context into one call, a generic read layer for the long tail, one result envelo
8
8
  packaged playbooks, strong security boundaries, and predictable per-tenant in-process state (pooled
9
9
  client + SDK-owned TTL caches).
10
10
 
11
- > **Package status (2026-10-22):** the runtime dependency `node-autotask` is published to public npm
12
- > at `0.24.0`; the committed `pnpm-lock.yaml` pins that
11
+ > **Package status (2026-10-24):** the runtime dependency `node-autotask` is published to public npm
12
+ > at `0.25.0`; the committed `pnpm-lock.yaml` pins that
13
13
  > release with its integrity hash, so installs resolve deterministically. The dependency is always npm
14
- > (node-autotask pinned exactly at `0.24.0`) — never a `file:`, `git:`, `link:` or workspace reference
14
+ > (node-autotask pinned exactly at `0.25.0`) — never a `file:`, `git:`, `link:` or workspace reference
15
15
  > to a local checkout.
16
16
 
17
17
  > **0.6.0 is a breaking release** (unreleased): tool names, result shapes and configuration all
@@ -44,7 +44,13 @@ its contract, `autotask_read` runs it.
44
44
  - `autotask_read` runs any read by exact key, or up to 10 at once (`items`). Unbilled work
45
45
  (`companies.unbilledWork`), contract profitability (`contracts.profitability`), aggregates
46
46
  (`operations.aggregate`) and SLA health (`tickets.slaHealthCheck`) run here; `autotask_guide`
47
- holds their recipes. `autotask_read` and `autotask_fetch_many` answer ONE envelope (the next minor,
47
+ holds their recipes. The two time-entry window keys (`time_entries.dateWorkedWindow`,
48
+ `time_entries.postedWithin`) also take flat named inputs instead of `args`, for clients that cannot
49
+ send a mixed-type array (Copilot Studio): `{operation, from, to, posted?, limit?, ticketID?,
50
+ resourceID?}`, `from`/`to` as inclusive `YYYY-MM-DD` dateWorked days (never UTC-converted),
51
+ `ticketID`/`resourceID` as numbers, no `tz` (it does not shift dateWorked). The SDK's
52
+ `foldFlattenedOpts` maps them onto its named invoke form, so the result and `truncated` flag are
53
+ the positional call's. `autotask_read` and `autotask_fetch_many` answer ONE envelope (the next minor,
48
54
  breaking): `data = { results: Slot[], ok, failed, budget?, budgetWarning? }`, an array in input
49
55
  order even for a single read (`data.results[0].result`). A slot is `{ ok: true, operation, result,
50
56
  windowNote? }` or `{ ok: false, operation, error }`; fetch_many slots also echo `resource` and `id`
@@ -773,7 +779,7 @@ independent review with its resolutions, [`docs/reviews/0.6.0.md`](docs/reviews/
773
779
 
774
780
  ## SDK dependency
775
781
 
776
- `iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.24.0`** (an exact pin —
782
+ `iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.25.0`** (an exact pin —
777
783
  no range operator; subpaths `.` / `/capabilities` / `/operations` / `/resources` / `/types` /
778
784
  `/pool`) — the only **vendor** dependency; the other two runtime dependencies are
779
785
  `@modelcontextprotocol/server` (the MCP framework) and `zod` (schema validation). It never
@@ -783,7 +789,7 @@ user — the published tarball ships no lockfile) must resolve to the release th
783
789
  reviewed against, or the plan-hash boot check refuses to start.
784
790
 
785
791
  **Publishing `node-autotask` is the maintainer's action, done manually.** It is published on the
786
- public registry at `0.24.0` (published 2026-10-23); the
792
+ public registry at `0.25.0` (published 2026-10-23); the
787
793
  exact pin and the committed `pnpm-lock.yaml` (with `pnpm-workspace.yaml`) pin the real published
788
794
  tarball with its integrity hash, so installs resolve to exactly that release — the lockfile
789
795
  freezes the pnpm/git/Docker paths, the pin freezes the npm/npx paths. The docs are written against this
@@ -33,7 +33,8 @@ export interface QueryWalkResult {
33
33
  /** The SDK operations the handler layer drives, bound in ONE place (the mock seam). */
34
34
  export interface ToolOps {
35
35
  /** The MCP request's AbortSignal, when the transport provides one: an abandoned request stops consuming vendor budget. */
36
- invokeRead(client: ToolClient, operation: string, args?: readonly unknown[], opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
36
+ /** `args` is positional, or (#173, the window keys) the SDK's named form keyed by the record's inputSchema names. */
37
+ invokeRead(client: ToolClient, operation: string, args?: readonly unknown[] | Record<string, unknown>, opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
37
38
  invokeWrite(client: ToolClient, operation: string, args?: readonly unknown[], opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
38
39
  invokeDelete(client: ToolClient, operation: string, args?: readonly unknown[], opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
39
40
  resolveAny(client: ToolClient, identifier: number | string, opts?: {
@@ -78,4 +78,4 @@ export interface ReadOutcome {
78
78
  * governed invoke (the SDK's single read governor) and the untrusted projection of the result.
79
79
  * Errors are THROWN — the frame wraps them once, the `items` form maps them per slot.
80
80
  */
81
- export declare function readOneItem(ctx: ToolContext, operation: string, args: unknown[], signal: AbortSignal | undefined): Promise<ReadOutcome>;
81
+ export declare function readOneItem(ctx: ToolContext, operation: string, args: unknown[] | Record<string, unknown>, signal: AbortSignal | undefined): Promise<ReadOutcome>;
@@ -191,7 +191,9 @@ export async function readOneItem(ctx, operation, args, signal) {
191
191
  }
192
192
  assertOperationPermitted(operation, rec, ctx.config, ctx.log);
193
193
  // Issue #1: strip ONE exact outer <untrusted_content> wrapper from the string inputs.
194
- const cleanArgs = stripUntrustedDeep(args);
194
+ const stripped = stripUntrustedDeep(args);
195
+ // A named-form object (#173) only arrives for the window keys, which no positional branch below touches.
196
+ const cleanArgs = Array.isArray(stripped) ? stripped : [];
195
197
  // #18: the *.query / *.count ops take a params bag whose `filter` top-level shape is refused
196
198
  // pre-request (zero wire on a refused shape).
197
199
  if (operation.endsWith('.query') || operation.endsWith('.count')) {
@@ -249,7 +251,7 @@ export async function readOneItem(ctx, operation, args, signal) {
249
251
  cleanArgs[0] = { ...bag, labels: true };
250
252
  }
251
253
  }
252
- const result = await ctx.ops.invokeRead(ctx.client, operation, cleanArgs, undefined, signal);
254
+ const result = await ctx.ops.invokeRead(ctx.client, operation, Array.isArray(stripped) ? cleanArgs : stripped, undefined, signal);
253
255
  if (result === undefined)
254
256
  return { aborted: true, result: undefined };
255
257
  const post = READ_POSTPROCESS[operation];
@@ -16,15 +16,16 @@
16
16
  * fail-closed refusal, then the work, with the SDK error contract mapped on failure.
17
17
  */
18
18
  import { AutotaskConfigError } from 'node-autotask';
19
+ import { foldFlattenedOpts } from 'node-autotask/operations';
19
20
  import { CATALOG, SDK_VERSION, TOOL_NAME_BY_OP, opsForTool, recordById, servedToolName, untrustedFieldsOf } from './catalog-data.js';
20
21
  import { releaseClientOnError } from './context.js';
21
- import { abortedAnswer, InputInvalidError, readOneItem, requestSecrets } from './dispatch.js';
22
+ import { abortedAnswer, InputInvalidError, NeedsInputError, readOneItem, requestSecrets } from './dispatch.js';
22
23
  import { zoneClock } from './clock.js';
23
24
  import { crossResourceUntrusted, honestReadError, markResult, ok, typedErrorPayload } from './results.js';
24
25
  import { bm25Rank } from './rank.js';
25
26
  import { playbooksFor } from './guide.js';
26
27
  import { SERVER_NAME, SERVER_VERSION } from './server-version.js';
27
- import { CATALOG_INPUT, DESCRIBE_INPUT, COMPANY_FULL_PROFILE_INPUT, CONFIGURATION_ITEM_PROFILE_INPUT, CONTACT_FULL_PROFILE_INPUT, CONTRACT_FULL_PROFILE_INPUT, FETCH_MANY_INPUT, INVOICE_GET_INPUT, PROJECT_GET_CONTEXT_INPUT, QUERY_INPUT, QUOTE_GET_INPUT, RESOLVE_INPUT, RESOURCE_WORKLOAD_INPUT, SERVICE_CALL_FULL_PROFILE_INPUT, TICKET_FULL_CONTEXT_INPUT, QUERY_LIMIT, READ_INPUT, TENANT_STATUS_INPUT, TENANT_STATUS_PARTS, closedInput, } from './schemas.js';
28
+ import { CATALOG_INPUT, DESCRIBE_INPUT, COMPANY_FULL_PROFILE_INPUT, CONFIGURATION_ITEM_PROFILE_INPUT, CONTACT_FULL_PROFILE_INPUT, CONTRACT_FULL_PROFILE_INPUT, FETCH_MANY_INPUT, INVOICE_GET_INPUT, PROJECT_GET_CONTEXT_INPUT, QUERY_INPUT, QUOTE_GET_INPUT, RESOLVE_INPUT, RESOURCE_WORKLOAD_INPUT, SERVICE_CALL_FULL_PROFILE_INPUT, TICKET_FULL_CONTEXT_INPUT, QUERY_LIMIT, READ_INPUT, TENANT_STATUS_INPUT, TENANT_STATUS_PARTS, WINDOW_KEYS, WINDOW_OPS, closedInput, } from './schemas.js';
28
29
  import { refusalReason } from '../policy.js';
29
30
  /** Search text per catalog row (key words + purpose), built once. */
30
31
  const ROW_TEXT = CATALOG.catalog.map((r) => `${r.operation} ${r.operation.replace(/[._]/g, ' ')} ${r.purpose ?? ''}`);
@@ -250,6 +251,13 @@ async function guidanceFor(ctx, rec, reachable) {
250
251
  example: `autotask_read {operation:"${rec.id}", args:[{filter:[{field:"id",op:"gt",value:0}],limit:10}]}`,
251
252
  };
252
253
  }
254
+ if (WINDOW_OPS.includes(rec.id)) {
255
+ // #173: the flat named form, for clients that cannot send the mixed positional [from, to, opts]
256
+ return {
257
+ namedInputs: 'from, to (YYYY-MM-DD dateWorked days, inclusive; never UTC-converted), posted, limit, ticketID, resourceID (numbers) as top-level autotask_read fields instead of args; same result and truncated flag. tz is not offered: it does not shift dateWorked.',
258
+ example: `autotask_read {operation:"${rec.id}", from:"2026-10-20", to:"2026-10-22", resourceID:123}`,
259
+ };
260
+ }
253
261
  if (op === 'moveToTicket') {
254
262
  // the move ops (#107): zero wire, a pointer to the governed path
255
263
  return {
@@ -307,6 +315,28 @@ async function describeRecord(ctx, operation, viaTool) {
307
315
  ...(names !== undefined ? { paramNames: names } : {}),
308
316
  });
309
317
  }
318
+ /**
319
+ * #173: the window keys' flat named inputs, folded by the SDK (`foldFlattenedOpts`) into its named
320
+ * invoke form `{ from, to, opts }`; undefined when none is given (the positional call, unchanged).
321
+ * Refused before any request: a named input on any other key or in the items form, together with
322
+ * positional args, or without from and to.
323
+ */
324
+ function windowNamedArgs(ctx, args) {
325
+ const named = Object.fromEntries(WINDOW_KEYS.filter((k) => args[k] !== undefined).map((k) => [k, args[k]]));
326
+ const first = Object.keys(named)[0];
327
+ if (first === undefined)
328
+ return undefined;
329
+ const op = args.operation;
330
+ if (op === undefined || !WINDOW_OPS.includes(op)) {
331
+ throw new InputInvalidError(`read: ${first} is a named input of ${WINDOW_OPS.join(' and ')} only (with operation); other reads take args`, first);
332
+ }
333
+ if (args.args.length > 0)
334
+ throw new InputInvalidError(`read: ${op} takes named inputs or args, not both`, 'args');
335
+ const missing = ['from', 'to'].filter((k) => named[k] === undefined);
336
+ if (missing.length > 0)
337
+ throw new NeedsInputError(`read: ${op} needs both from and to (YYYY-MM-DD) with its named inputs`, missing[0], missing.map((k) => `/${k}`));
338
+ return foldFlattenedOpts(ctx.record(op), named);
339
+ }
310
340
  /** `autotask_read`: one read by exact key, or up to 10 (`items`) with per-item results. */
311
341
  export function buildRead(ctx) {
312
342
  return {
@@ -317,8 +347,9 @@ export function buildRead(ctx) {
317
347
  if ((args.operation === undefined) === (args.items === undefined)) {
318
348
  throw new InputInvalidError('read: pass exactly one of operation (with args) or items', 'operation');
319
349
  }
350
+ const named = windowNamedArgs(ctx, args); // before the branch: a named input beside items is refused, never ignored
320
351
  if (args.operation !== undefined) {
321
- const out = await readOneItem(ctx, args.operation, args.args, signal);
352
+ const out = await readOneItem(ctx, args.operation, named ?? args.args, signal);
322
353
  if (out.aborted)
323
354
  return abortedAnswer(ctx.correlationId);
324
355
  // The server-side note (issue #56: the profitability window default applied) rides the
@@ -210,13 +210,26 @@ export declare const DESCRIBE_INPUT: z.ZodObject<{
210
210
  resource: z.ZodOptional<z.ZodString>;
211
211
  }, z.core.$strip>;
212
212
  export declare const READ_MAX_ITEMS = 10;
213
+ /**
214
+ * #173: the two time-entry window keys also take flat, typed named inputs (the SDK's own names,
215
+ * as its MCP projection serves them), because a client that cannot send a mixed-type array cannot
216
+ * send their positional `[from, to, opts]`. `tz` is left out: it does not shift a dateWorked day.
217
+ */
218
+ export declare const WINDOW_OPS: readonly string[];
219
+ export declare const WINDOW_KEYS: readonly ["from", "to", "posted", "limit", "ticketID", "resourceID"];
213
220
  export declare const READ_INPUT: z.ZodObject<{
214
- operation: z.ZodOptional<z.ZodString>;
215
- args: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
216
- items: z.ZodOptional<z.ZodArray<z.ZodObject<{
221
+ operation: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
222
+ args: z.ZodPreprocess<z.ZodDefault<z.ZodArray<z.ZodUnknown>>, unknown>;
223
+ items: z.ZodPreprocess<z.ZodOptional<z.ZodArray<z.ZodObject<{
217
224
  operation: z.ZodString;
218
225
  args: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
219
- }, z.core.$strict>>>;
226
+ }, z.core.$strict>>>, unknown>;
227
+ from: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
228
+ to: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
229
+ posted: z.ZodPreprocess<z.ZodOptional<z.ZodBoolean>, unknown>;
230
+ limit: z.ZodPreprocess<z.ZodOptional<z.ZodNumber>, unknown>;
231
+ ticketID: z.ZodPreprocess<z.ZodOptional<z.ZodNumber>, unknown>;
232
+ resourceID: z.ZodPreprocess<z.ZodOptional<z.ZodNumber>, unknown>;
220
233
  }, z.core.$strip>;
221
234
  export declare const RESOLVE_INPUT: z.ZodObject<{
222
235
  identifier: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
@@ -203,10 +203,30 @@ export const DESCRIBE_INPUT = z.object({
203
203
  });
204
204
  const READ_ITEM = z.strictObject({ operation: OP_KEY.describe('Exact read key.'), args: ARGS });
205
205
  export const READ_MAX_ITEMS = 10;
206
+ /**
207
+ * #173: the two time-entry window keys also take flat, typed named inputs (the SDK's own names,
208
+ * as its MCP projection serves them), because a client that cannot send a mixed-type array cannot
209
+ * send their positional `[from, to, opts]`. `tz` is left out: it does not shift a dateWorked day.
210
+ */
211
+ export const WINDOW_OPS = ['time_entries.dateWorkedWindow', 'time_entries.postedWithin'];
212
+ export const WINDOW_KEYS = ['from', 'to', 'posted', 'limit', 'ticketID', 'resourceID'];
213
+ /**
214
+ * An autotask_read input sent as null or '' is absent: form-filling clients (Copilot Studio) can
215
+ * send every unset optional that way, e.g. args: null beside the named window inputs. The advertised schema stays the plain scalar (no null union).
216
+ */
217
+ const unset = (schema) => z.preprocess((v) => (v === null || v === '' ? undefined : v), schema);
206
218
  export const READ_INPUT = z.object({
207
- operation: OP_KEY.optional().describe('Exact read key, e.g. tickets.get (autotask_catalog finds it).'),
208
- args: ARGS,
209
- items: z.array(READ_ITEM).min(1).max(READ_MAX_ITEMS).optional().describe('Up to 10 reads in one call, each { operation, args }; use instead of operation.'),
219
+ operation: unset(OP_KEY.optional()).describe('Exact read key, e.g. tickets.get (autotask_catalog finds it).'),
220
+ args: unset(ARGS),
221
+ items: unset(z.array(READ_ITEM).min(1).max(READ_MAX_ITEMS).optional()).describe('Up to 10 reads in one call, each { operation, args }; use instead of operation.'),
222
+ // One description for the set: every served byte counts against the readonly-core budget. The SDK
223
+ // refuses a from/to that is not a real YYYY-MM-DD day, naming it, before any request.
224
+ from: unset(z.string().optional()).describe('time_entries.dateWorkedWindow/postedWithin only, instead of args: from, to are YYYY-MM-DD dateWorked days (inclusive); also posted, limit, ticketID, resourceID.'),
225
+ to: unset(z.string().optional()),
226
+ posted: unset(z.boolean().optional()),
227
+ limit: unset(z.number().int().min(1).optional()),
228
+ ticketID: unset(z.number().int().nonnegative().optional()),
229
+ resourceID: unset(z.number().int().nonnegative().optional()),
210
230
  });
211
231
  export const RESOLVE_INPUT = z.object({
212
232
  identifier: z.union([z.number().int().nonnegative(), z.string().min(1).max(200)]).describe('Numeric id, name, ticket number, serial or email. resources: numeric identifiers are looked up as Resource ids.'),
@@ -1,3 +1,3 @@
1
1
  /** The wire identity of this server: a leaf module (no imports) so any tool can read it without an import cycle. */
2
2
  export declare const SERVER_NAME = "iit-mcp-autotask";
3
- export declare const SERVER_VERSION = "0.13.0";
3
+ export declare const SERVER_VERSION = "0.14.0";
@@ -1,4 +1,4 @@
1
1
  /** The wire identity of this server: a leaf module (no imports) so any tool can read it without an import cycle. */
2
2
  export const SERVER_NAME = 'iit-mcp-autotask';
3
- export const SERVER_VERSION = '0.13.0';
3
+ export const SERVER_VERSION = '0.14.0';
4
4
  //# sourceMappingURL=server-version.js.map
@@ -60,7 +60,7 @@ export declare function serverInstructions(config: Pick<Config, 'AUTOTASK_PROFIL
60
60
  /** The full identity stamp (the `/metadata` build block — the era pair, FR-2). */
61
61
  export declare const SERVER_IDENTITY: {
62
62
  readonly name: "iit-mcp-autotask";
63
- readonly version: "0.13.0";
63
+ readonly version: "0.14.0";
64
64
  readonly mcpProtocolBaseline: "2026-07-28";
65
65
  readonly legacyProtocolFallback: "2025-11-25";
66
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iit-mcp-autotask",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/server": "^2.2.0",
56
- "node-autotask": "0.24.0",
56
+ "node-autotask": "0.25.0",
57
57
  "zod": "^4.6.7"
58
58
  },
59
59
  "devDependencies": {
package/server.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iit-mcp-autotask",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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",
@@ -63,7 +63,7 @@
63
63
  "capabilitiesManifest": "capabilities.json",
64
64
  "sdk": {
65
65
  "package": "node-autotask",
66
- "versionRange": "0.24.0"
66
+ "versionRange": "0.25.0"
67
67
  },
68
68
  "vendorExtensions": false,
69
69
  "credentialModelNote": "per-request; server-held under stdio or, on HTTP, only behind AUTOTASK_HTTP_AUTH=tokens; operator plans and the ledger are keyed by principal and tenant id, never by credential, and an apply uses the credential on the apply request"
@@ -5,7 +5,7 @@ version: 1
5
5
  profile: readonly
6
6
  description: Filter footguns for autotask_query: strict date lt, case-insensitive contains, FK 0 vs NULL, field-name checks, all-strings values.
7
7
  when_not_to_use: the filter grammar itself (the 13 operators + and/or grouping, server instructions); the per-operation notes (autotask_describe)
8
- ops: ["autotask_query:tickets.query", "autotask_query:time_entries.query", "autotask_query:service_calls.query", "autotask_query:contracts.query", "autotask_query:time_off_requests.query", "autotask_read:operations.fieldCatalog"]
8
+ ops: ["autotask_query:tickets.query", "autotask_query:time_entries.query", "autotask_query:service_calls.query", "autotask_query:contracts.query", "autotask_query:time_off_requests.query", "autotask_read:operations.fieldCatalog", "autotask_read:time_entries.dateWorkedWindow", "autotask_read:time_entries.postedWithin"]
9
9
  inputs: {"type": "object", "required": [], "properties": {}}
10
10
  outcomes: ["ok", "error"]
11
11
  hazards: ["gt/lt around a day boundary silently drops an edge day", "ticketID eq 0 finds nothing (time_entries store NULL)", "an unknown field name is a zero-query-wire CONFIG_ERROR (0.20.0 pre-validation; a cold field cache makes one vendor metadata GET first)"]
@@ -28,7 +28,7 @@ No inputs: this is a reference page about the filter behaviour of autotask_query
28
28
  3. tickets.search is a vendor contains on the TITLE only: no free-text search, no matched-field reporting, and vendor contains is looser than a literal substring (case/token matching can return MORE hits than a local title-substring check). The per-field contains of tickets.query is the precise tool.
29
29
  4. Unassigned FKs are not uniform. tickets store 0; time_entries store NULL. Unallocated time needs notExist (ticketID eq 0 finds nothing); assignedResourceID eq 0 and notExist differ.
30
30
  5. Field names are vendor casing, and an unknown field name now fails pre-dispatch as a typed CONFIG_ERROR with the field-catalog pointer, with zero query wire (0.20.0 pre-validation), on all five autotask_query operations (tickets.query, time_entries.query, service_calls.query, contracts.query, time_off_requests.query). One udf: true condition per query is exempt from the name check. List names for the queried resource with the scoped call `autotask_read {operation:"operations.fieldCatalog", args:[{resources:["<resource>"]}]}` (add `fields: [names]` to project; a scoped call also returns picklist labels by default on this server). Never the bare `operations.fieldCatalog`: it serves the whole five-resource catalog (~85 KB) and has no row for time_entries, service_calls, contracts or time_off_requests. Do not guess names. The answer is `data.results[0].result`.
31
- 6. Some clients (Copilot Studio) fail locally, before any request, when an array mixes value types: a number and a string in one filter, or dateWorkedWindow args (strings plus an object). For such a client, write every filter value as a string ("<resource id>", "2026-10-22", "true") and use autotask_query time_entries with a dateWorked gte/lte pair in place of dateWorkedWindow. The server and SDK pass values through unchanged; string dates and ids are confirmed to work, other types are not verified on every field.
31
+ 6. Some clients (Copilot Studio) fail locally, before any request, when an array mixes value types: a number and a string in one filter, or dateWorkedWindow args (strings plus an object). For such a client, write every filter value as a string ("<resource id>", "2026-10-22", "true"). For a time entry window use the named form instead of args, all flat scalars: `autotask_read {operation:"time_entries.dateWorkedWindow", from:"2026-10-20", to:"2026-10-22", resourceID:<resource id>}` (also posted, limit, ticketID; resourceID and ticketID are numbers; the same for time_entries.postedWithin). It returns the same rows, counts and truncated flag as the positional call; from and to are inclusive YYYY-MM-DD dateWorked days, never UTC-converted, and there is no tz input (tz does not shift dateWorked). The server and SDK pass filter values through unchanged; string dates and ids are confirmed to work, other types are not verified on every field.
32
32
 
33
33
  ## Guardrails
34
34
 
@@ -68,4 +68,6 @@ No inputs: this is a reference page about the filter behaviour of autotask_query
68
68
  | autotask_query | contracts.query | read | - |
69
69
  | autotask_query | time_off_requests.query | read | - |
70
70
  | autotask_read | operations.fieldCatalog | read | - |
71
+ | autotask_read | time_entries.dateWorkedWindow | read | from, to |
72
+ | autotask_read | time_entries.postedWithin | read | from, to |
71
73
  <!-- generated:end -->