iit-mcp-autotask 0.14.0 → 0.15.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 +9 -8
- package/dist/tools/context/types.d.ts +1 -2
- package/dist/tools/dispatch.d.ts +1 -1
- package/dist/tools/dispatch.js +18 -6
- package/dist/tools/meta.js +8 -32
- package/dist/tools/schemas.d.ts +14 -12
- package/dist/tools/schemas.js +32 -19
- package/dist/tools/server-version.d.ts +1 -1
- package/dist/tools/server-version.js +1 -1
- package/dist/tools/server.d.ts +1 -1
- package/package.json +1 -1
- package/server.json +1 -1
- package/skills/filter-semantics.md +2 -4
package/README.md
CHANGED
|
@@ -40,17 +40,18 @@ its contract, `autotask_read` runs it.
|
|
|
40
40
|
`include_api_users: true`; `by: "email"` narrows to the email field; a numeric identifier is looked up
|
|
41
41
|
as a Resource id); compact rows carry `apiUser` and the licence label when the tenant's labels tell it, never an email. Every match carries `ref`, its human reference. A sweep returns compact matches (`resource`, `ref`, `id`, the human fields) unless `expand: true` (the full record under `record`); `limit` caps the TOTAL matches across resources (resource order, then the SDK's order) and a contains sweep reports `truncated` and per-resource `counts {returned, found}`.
|
|
42
42
|
- `autotask_query` is the bounded filtered list over `resource`: `tickets`, `time_entries`,
|
|
43
|
-
`service_calls`, `contracts`, `time_off_requests` (limit default 50, max 2500).
|
|
43
|
+
`service_calls`, `contracts`, `time_off_requests` (limit default 50, max 2500). The served filter
|
|
44
|
+
schema declares each condition as `{field, op, value}` strings, so a client that builds one type
|
|
45
|
+
per array column writes `"29"`, `"true"`, `"2026-10-22"`; numbers, `in` arrays and and/or groups are
|
|
46
|
+
still accepted and passed through unchanged.
|
|
44
47
|
- `autotask_read` runs any read by exact key, or up to 10 at once (`items`). Unbilled work
|
|
45
48
|
(`companies.unbilledWork`), contract profitability (`contracts.profitability`), aggregates
|
|
46
49
|
(`operations.aggregate`) and SLA health (`tickets.slaHealthCheck`) run here; `autotask_guide`
|
|
47
|
-
holds their recipes.
|
|
48
|
-
|
|
49
|
-
send a mixed-type array
|
|
50
|
-
|
|
51
|
-
`
|
|
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,
|
|
50
|
+
holds their recipes. Its inputs are generic (`operation`, `args`, `items`): no operation-specific
|
|
51
|
+
fields, because form-filling clients (Copilot Studio) fill every optional input they are shown. A
|
|
52
|
+
client that cannot send a mixed-type array asks for a time entry window over a resource with
|
|
53
|
+
`autotask_query` `time_entries` (`resourceID` eq, `dateWorked` gte/lte, every value a string: the
|
|
54
|
+
served filter schema types `value` as a string). `autotask_read` and `autotask_fetch_many` answer ONE envelope (the next minor,
|
|
54
55
|
breaking): `data = { results: Slot[], ok, failed, budget?, budgetWarning? }`, an array in input
|
|
55
56
|
order even for a single read (`data.results[0].result`). A slot is `{ ok: true, operation, result,
|
|
56
57
|
windowNote? }` or `{ ok: false, operation, error }`; fetch_many slots also echo `resource` and `id`
|
|
@@ -33,8 +33,7 @@ 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
|
-
|
|
37
|
-
invokeRead(client: ToolClient, operation: string, args?: readonly unknown[] | Record<string, unknown>, opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
|
|
36
|
+
invokeRead(client: ToolClient, operation: string, args?: readonly unknown[], opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
|
|
38
37
|
invokeWrite(client: ToolClient, operation: string, args?: readonly unknown[], opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
|
|
39
38
|
invokeDelete(client: ToolClient, operation: string, args?: readonly unknown[], opts?: InvokeOptions, signal?: AbortSignal): Promise<unknown>;
|
|
40
39
|
resolveAny(client: ToolClient, identifier: number | string, opts?: {
|
package/dist/tools/dispatch.d.ts
CHANGED
|
@@ -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[]
|
|
81
|
+
export declare function readOneItem(ctx: ToolContext, operation: string, args: unknown[], signal: AbortSignal | undefined): Promise<ReadOutcome>;
|
package/dist/tools/dispatch.js
CHANGED
|
@@ -65,11 +65,19 @@ export class NeedsInputError extends Error {
|
|
|
65
65
|
export async function runTool(ctx, input, rawArgs, extra, work) {
|
|
66
66
|
return stamp(ctx.correlationId, await frame(ctx, input, rawArgs, extra, work));
|
|
67
67
|
}
|
|
68
|
+
/** `raw` without the keys `ignored` names (a copy; `raw` itself unchanged), or `raw` as is when there are none. */
|
|
69
|
+
function dropKeys(raw, ignored) {
|
|
70
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
71
|
+
return raw;
|
|
72
|
+
const keys = ignored(raw);
|
|
73
|
+
return keys.length === 0 ? raw : Object.fromEntries(Object.entries(raw).filter(([k]) => !keys.includes(k)));
|
|
74
|
+
}
|
|
68
75
|
async function frame(ctx, input, rawArgs, extra, work) {
|
|
69
|
-
const
|
|
76
|
+
const raw = dropKeys(rawArgs ?? {}, input.ignored);
|
|
77
|
+
const closed = closedArgsError(raw, input.validKeys, ctx.correlationId);
|
|
70
78
|
if (closed !== null)
|
|
71
79
|
return closed;
|
|
72
|
-
const parsed = input.schema.safeParse(
|
|
80
|
+
const parsed = input.schema.safeParse(raw);
|
|
73
81
|
if (!parsed.success) {
|
|
74
82
|
const issue = parsed.error.issues[0];
|
|
75
83
|
const field = issue === undefined ? undefined : issue.path.map(String).join('.');
|
|
@@ -191,15 +199,19 @@ export async function readOneItem(ctx, operation, args, signal) {
|
|
|
191
199
|
}
|
|
192
200
|
assertOperationPermitted(operation, rec, ctx.config, ctx.log);
|
|
193
201
|
// Issue #1: strip ONE exact outer <untrusted_content> wrapper from the string inputs.
|
|
194
|
-
const
|
|
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 : [];
|
|
202
|
+
const cleanArgs = stripUntrustedDeep(args);
|
|
197
203
|
// #18: the *.query / *.count ops take a params bag whose `filter` top-level shape is refused
|
|
198
204
|
// pre-request (zero wire on a refused shape).
|
|
199
205
|
if (operation.endsWith('.query') || operation.endsWith('.count')) {
|
|
200
206
|
// No params bag at all (args omitted / []): the same bounded default (#83); the SDK's own bag-less form is unbounded.
|
|
201
207
|
if (operation.endsWith('.query') && cleanArgs[0] === undefined)
|
|
202
208
|
cleanArgs[0] = { limit: QUERY_LIMIT.parse(undefined) };
|
|
209
|
+
const given = cleanArgs[0];
|
|
210
|
+
// #173: a digit-string limit (a client that writes every value as text, e.g. Copilot Studio's
|
|
211
|
+
// limit: "5") is that number; the SDK still range-checks it.
|
|
212
|
+
if (given !== null && typeof given === 'object' && !Array.isArray(given) && typeof given.limit === 'string' && /^\d{1,9}$/.test(given.limit)) {
|
|
213
|
+
cleanArgs[0] = { ...given, limit: Number(given.limit) };
|
|
214
|
+
}
|
|
203
215
|
const bag = cleanArgs[0];
|
|
204
216
|
if (bag !== null && typeof bag === 'object' && !Array.isArray(bag)) {
|
|
205
217
|
assertFilterShape(bag.filter, operation);
|
|
@@ -251,7 +263,7 @@ export async function readOneItem(ctx, operation, args, signal) {
|
|
|
251
263
|
cleanArgs[0] = { ...bag, labels: true };
|
|
252
264
|
}
|
|
253
265
|
}
|
|
254
|
-
const result = await ctx.ops.invokeRead(ctx.client, operation,
|
|
266
|
+
const result = await ctx.ops.invokeRead(ctx.client, operation, cleanArgs, undefined, signal);
|
|
255
267
|
if (result === undefined)
|
|
256
268
|
return { aborted: true, result: undefined };
|
|
257
269
|
const post = READ_POSTPROCESS[operation];
|
package/dist/tools/meta.js
CHANGED
|
@@ -16,16 +16,15 @@
|
|
|
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';
|
|
20
19
|
import { CATALOG, SDK_VERSION, TOOL_NAME_BY_OP, opsForTool, recordById, servedToolName, untrustedFieldsOf } from './catalog-data.js';
|
|
21
20
|
import { releaseClientOnError } from './context.js';
|
|
22
|
-
import { abortedAnswer, InputInvalidError,
|
|
21
|
+
import { abortedAnswer, InputInvalidError, readOneItem, requestSecrets } from './dispatch.js';
|
|
23
22
|
import { zoneClock } from './clock.js';
|
|
24
23
|
import { crossResourceUntrusted, honestReadError, markResult, ok, typedErrorPayload } from './results.js';
|
|
25
24
|
import { bm25Rank } from './rank.js';
|
|
26
25
|
import { playbooksFor } from './guide.js';
|
|
27
26
|
import { SERVER_NAME, SERVER_VERSION } from './server-version.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,
|
|
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, readIgnoredKeys, closedInput, } from './schemas.js';
|
|
29
28
|
import { refusalReason } from '../policy.js';
|
|
30
29
|
/** Search text per catalog row (key words + purpose), built once. */
|
|
31
30
|
const ROW_TEXT = CATALOG.catalog.map((r) => `${r.operation} ${r.operation.replace(/[._]/g, ' ')} ${r.purpose ?? ''}`);
|
|
@@ -251,11 +250,11 @@ async function guidanceFor(ctx, rec, reachable) {
|
|
|
251
250
|
example: `autotask_read {operation:"${rec.id}", args:[{filter:[{field:"id",op:"gt",value:0}],limit:10}]}`,
|
|
252
251
|
};
|
|
253
252
|
}
|
|
254
|
-
if (
|
|
255
|
-
// #173: the
|
|
253
|
+
if (rec.id === 'time_entries.dateWorkedWindow' || rec.id === 'time_entries.postedWithin') {
|
|
254
|
+
// #173: the positional [from, to, opts] mixes types; the same window as one uniform-string filter
|
|
256
255
|
return {
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
exampleArgs: ['2026-10-20', '2026-10-22', { resourceID: 123 }],
|
|
257
|
+
sameWindowAsQuery: 'autotask_query {resource:"time_entries", filter:[{field:"resourceID",op:"eq",value:"123"},{field:"dateWorked",op:"gte",value:"2026-10-20"},{field:"dateWorked",op:"lte",value:"2026-10-22"}], limit:500}: the same rows up to limit (the window caps at 500; dateWorked is a date, both days inclusive), without the posted counts, for a client that cannot send a mixed-type array.',
|
|
259
258
|
};
|
|
260
259
|
}
|
|
261
260
|
if (op === 'moveToTicket') {
|
|
@@ -315,41 +314,18 @@ async function describeRecord(ctx, operation, viaTool) {
|
|
|
315
314
|
...(names !== undefined ? { paramNames: names } : {}),
|
|
316
315
|
});
|
|
317
316
|
}
|
|
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
|
-
}
|
|
340
317
|
/** `autotask_read`: one read by exact key, or up to 10 (`items`) with per-item results. */
|
|
341
318
|
export function buildRead(ctx) {
|
|
342
319
|
return {
|
|
343
320
|
title: 'Run Autotask read operations by exact key',
|
|
344
|
-
input: closedInput(READ_INPUT),
|
|
321
|
+
input: closedInput(READ_INPUT, readIgnoredKeys),
|
|
345
322
|
run: async (raw, signal) => {
|
|
346
323
|
const args = raw;
|
|
347
324
|
if ((args.operation === undefined) === (args.items === undefined)) {
|
|
348
325
|
throw new InputInvalidError('read: pass exactly one of operation (with args) or items', 'operation');
|
|
349
326
|
}
|
|
350
|
-
const named = windowNamedArgs(ctx, args); // before the branch: a named input beside items is refused, never ignored
|
|
351
327
|
if (args.operation !== undefined) {
|
|
352
|
-
const out = await readOneItem(ctx, args.operation,
|
|
328
|
+
const out = await readOneItem(ctx, args.operation, args.args, signal);
|
|
353
329
|
if (out.aborted)
|
|
354
330
|
return abortedAnswer(ctx.correlationId);
|
|
355
331
|
// The server-side note (issue #56: the profitability window default applied) rides the
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -48,14 +48,18 @@ export interface ClosedInput {
|
|
|
48
48
|
input: StandardSchemaWithJSON<Record<string, unknown>, Record<string, unknown>>;
|
|
49
49
|
schema: z.ZodType;
|
|
50
50
|
validKeys: readonly string[];
|
|
51
|
+
/** The raw top-level keys to drop before the closed-key check (none for most tools). */
|
|
52
|
+
ignored: (raw: Record<string, unknown>) => readonly string[];
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* Wrap a zod object schema so its top level is CLOSED and the SDK never rejects it:
|
|
54
56
|
* - the ADVERTISED wire schema is the object's JSON Schema with `additionalProperties: false`;
|
|
55
57
|
* - `validate` returns the raw arguments unchanged — the call frame runs the closed-key check and
|
|
56
58
|
* the zod parse, so every refusal is an envelope (M1), never the SDK's plain-text error.
|
|
59
|
+
* `ignored(raw)` names raw keys dropped before the closed-key check (never advertised, never named
|
|
60
|
+
* as valid keys).
|
|
57
61
|
*/
|
|
58
|
-
export declare function closedInput(schema: z.ZodType): ClosedInput;
|
|
62
|
+
export declare function closedInput(schema: z.ZodType, ignored?: ClosedInput['ignored']): ClosedInput;
|
|
59
63
|
/**
|
|
60
64
|
* The envelope as served: every field typed only. The `status` / `next` values are listed once,
|
|
61
65
|
* in the server instructions (that is the byte budget: the zod form with enums and bounds costs
|
|
@@ -211,12 +215,16 @@ export declare const DESCRIBE_INPUT: z.ZodObject<{
|
|
|
211
215
|
}, z.core.$strip>;
|
|
212
216
|
export declare const READ_MAX_ITEMS = 10;
|
|
213
217
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
218
|
+
* 0.15.0: autotask_read carries no operation-specific inputs. The six flat window inputs of 0.14.x
|
|
219
|
+
* are gone from the served schema (a form-filling client fills every optional it is shown, e.g. a
|
|
220
|
+
* stray limit: 5 on a resources.query), but a client still holding the 0.14.x tool list may send
|
|
221
|
+
* them. On any other read they are dropped (they never meant anything there). Beside a window key
|
|
222
|
+
* (top level or in items) a set one is NOT dropped, so the closed-key check refuses it naming the
|
|
223
|
+
* key: dropping a resourceID or ticketID there would silently widen the window. null or '' is unset.
|
|
224
|
+
* ponytail: drop this list once no 0.14.x tool list can be cached (a later minor).
|
|
217
225
|
*/
|
|
218
|
-
export declare const
|
|
219
|
-
export declare
|
|
226
|
+
export declare const READ_RETIRED_KEYS: readonly ["from", "to", "posted", "limit", "ticketID", "resourceID"];
|
|
227
|
+
export declare function readIgnoredKeys(raw: Record<string, unknown>): readonly string[];
|
|
220
228
|
export declare const READ_INPUT: z.ZodObject<{
|
|
221
229
|
operation: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
222
230
|
args: z.ZodPreprocess<z.ZodDefault<z.ZodArray<z.ZodUnknown>>, unknown>;
|
|
@@ -224,12 +232,6 @@ export declare const READ_INPUT: z.ZodObject<{
|
|
|
224
232
|
operation: z.ZodString;
|
|
225
233
|
args: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
|
|
226
234
|
}, 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>;
|
|
233
235
|
}, z.core.$strip>;
|
|
234
236
|
export declare const RESOLVE_INPUT: z.ZodObject<{
|
|
235
237
|
identifier: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
package/dist/tools/schemas.js
CHANGED
|
@@ -65,8 +65,18 @@ const COMPANY_PROFILE_PART_LIMIT_LOCATIONS = z
|
|
|
65
65
|
* The JSON filter (pass-through for WELL-FORMED shapes: the condition grammar — 13 operators +
|
|
66
66
|
* and/or — is validated by the SDK pre-request; unsupported TOP-LEVEL shapes are refused by this
|
|
67
67
|
* server pre-request, see {@link assertFilterShape}).
|
|
68
|
+
*
|
|
69
|
+
* #173: the ADVERTISED schema types a condition's `value` as a string, so a client that builds one
|
|
70
|
+
* column type per array (Copilot Studio's Table) gets a uniform string column instead of inferring
|
|
71
|
+
* one from mixed values; the runtime stays pass-through (numbers, booleans, in/notIn arrays and
|
|
72
|
+
* and/or groups are accepted unchanged, nothing is converted). Only field/op/value are advertised:
|
|
73
|
+
* a form-filling client fills every property it is shown, and a filled `udf` or `items` would change
|
|
74
|
+
* the query.
|
|
68
75
|
*/
|
|
69
|
-
const FILTER = z
|
|
76
|
+
const FILTER = z
|
|
77
|
+
.unknown()
|
|
78
|
+
.meta({ type: 'array', items: { type: 'object', properties: { field: { type: 'string' }, op: { type: 'string' }, value: { type: 'string' } } } })
|
|
79
|
+
.describe('Conditions {field, op, value} (grammar: server instructions); write every value as a string: "29", "true", "2026-10-22" (numbers also work; in/notIn take an array, and/or groups take items). Field names are pre-validated against the field catalog: an unknown name is a CONFIG_ERROR with the field-catalog pointer, zero QUERY wire (a cold field cache makes one vendor metadata GET first); one udf:true condition per query is exempt.');
|
|
70
80
|
/**
|
|
71
81
|
* The pre-dispatch filter-shape gate (#18): accepts the two top-level shapes the pinned SDK
|
|
72
82
|
* understands — a conditions array, or a params bag whose `filter` field is a conditions array
|
|
@@ -100,8 +110,10 @@ export function assertEnrichApplicable(resource, keys, operation) {
|
|
|
100
110
|
* - the ADVERTISED wire schema is the object's JSON Schema with `additionalProperties: false`;
|
|
101
111
|
* - `validate` returns the raw arguments unchanged — the call frame runs the closed-key check and
|
|
102
112
|
* the zod parse, so every refusal is an envelope (M1), never the SDK's plain-text error.
|
|
113
|
+
* `ignored(raw)` names raw keys dropped before the closed-key check (never advertised, never named
|
|
114
|
+
* as valid keys).
|
|
103
115
|
*/
|
|
104
|
-
export function closedInput(schema) {
|
|
116
|
+
export function closedInput(schema, ignored = () => []) {
|
|
105
117
|
const advertised = z.toJSONSchema(schema, { target: 'draft-2020-12', io: 'input' });
|
|
106
118
|
const validKeys = Object.keys(advertised.properties ?? {});
|
|
107
119
|
const input = {
|
|
@@ -119,7 +131,7 @@ export function closedInput(schema) {
|
|
|
119
131
|
},
|
|
120
132
|
},
|
|
121
133
|
};
|
|
122
|
-
return { input, schema, validKeys };
|
|
134
|
+
return { input, schema, validKeys, ignored };
|
|
123
135
|
}
|
|
124
136
|
// ---------------------------------------------------------------------------
|
|
125
137
|
// The served output schema: the compact result envelope (one per tool)
|
|
@@ -203,30 +215,31 @@ export const DESCRIBE_INPUT = z.object({
|
|
|
203
215
|
});
|
|
204
216
|
const READ_ITEM = z.strictObject({ operation: OP_KEY.describe('Exact read key.'), args: ARGS });
|
|
205
217
|
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
218
|
/**
|
|
214
219
|
* An autotask_read input sent as null or '' is absent: form-filling clients (Copilot Studio) can
|
|
215
|
-
* send every unset optional that way
|
|
220
|
+
* send every unset optional that way. The advertised schema stays the plain type (no null union).
|
|
216
221
|
*/
|
|
217
222
|
const unset = (schema) => z.preprocess((v) => (v === null || v === '' ? undefined : v), schema);
|
|
223
|
+
/**
|
|
224
|
+
* 0.15.0: autotask_read carries no operation-specific inputs. The six flat window inputs of 0.14.x
|
|
225
|
+
* are gone from the served schema (a form-filling client fills every optional it is shown, e.g. a
|
|
226
|
+
* stray limit: 5 on a resources.query), but a client still holding the 0.14.x tool list may send
|
|
227
|
+
* them. On any other read they are dropped (they never meant anything there). Beside a window key
|
|
228
|
+
* (top level or in items) a set one is NOT dropped, so the closed-key check refuses it naming the
|
|
229
|
+
* key: dropping a resourceID or ticketID there would silently widen the window. null or '' is unset.
|
|
230
|
+
* ponytail: drop this list once no 0.14.x tool list can be cached (a later minor).
|
|
231
|
+
*/
|
|
232
|
+
export const READ_RETIRED_KEYS = ['from', 'to', 'posted', 'limit', 'ticketID', 'resourceID'];
|
|
233
|
+
const WINDOW_OPS = ['time_entries.dateWorkedWindow', 'time_entries.postedWithin'];
|
|
234
|
+
export function readIgnoredKeys(raw) {
|
|
235
|
+
const items = Array.isArray(raw.items) ? raw.items : [];
|
|
236
|
+
const window = WINDOW_OPS.includes(raw.operation) || items.some((i) => typeof i === 'object' && i !== null && WINDOW_OPS.includes(i.operation));
|
|
237
|
+
return READ_RETIRED_KEYS.filter((k) => k in raw && (!window || raw[k] === null || raw[k] === ''));
|
|
238
|
+
}
|
|
218
239
|
export const READ_INPUT = z.object({
|
|
219
240
|
operation: unset(OP_KEY.optional()).describe('Exact read key, e.g. tickets.get (autotask_catalog finds it).'),
|
|
220
241
|
args: unset(ARGS),
|
|
221
242
|
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()),
|
|
230
243
|
});
|
|
231
244
|
export const RESOLVE_INPUT = z.object({
|
|
232
245
|
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,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.
|
|
3
|
+
export const SERVER_VERSION = '0.15.0';
|
|
4
4
|
//# sourceMappingURL=server-version.js.map
|
package/dist/tools/server.d.ts
CHANGED
|
@@ -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.
|
|
63
|
+
readonly version: "0.15.0";
|
|
64
64
|
readonly mcpProtocolBaseline: "2026-07-28";
|
|
65
65
|
readonly legacyProtocolFallback: "2025-11-25";
|
|
66
66
|
};
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -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"]
|
|
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
|
|
31
|
+
6. Some clients (Copilot Studio) fail locally, before any request, when one array mixes value types (a number beside a string in one filter, or dateWorkedWindow args: strings plus an object). The autotask_query filter declares value as a string, so write every value as a string ("<resource id>", "2026-10-22", "true"); numbers still work for clients that send them. For a time entry window over a resource or ticket, query time entries instead of dateWorkedWindow: `autotask_query {resource:"time_entries", filter:[{field:"resourceID",op:"eq",value:"<resource id>"},{field:"dateWorked",op:"gte",value:"2026-10-20"},{field:"dateWorked",op:"lte",value:"2026-10-22"}], limit:500}`: the window's rows up to limit (dateWorkedWindow caps at 500; the query defaults to 50), both days inclusive because dateWorked is a date, without the posted counts (derive them from postedDate where the zone serves it). A bare window with no narrowing is `autotask_read {operation:"time_entries.dateWorkedWindow", args:["2026-10-20","2026-10-22"]}` (all strings). The server and SDK pass filter values through unchanged; string dates, ids and booleans are confirmed on resources and time_entries, other fields are not verified.
|
|
32
32
|
|
|
33
33
|
## Guardrails
|
|
34
34
|
|
|
@@ -68,6 +68,4 @@ 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 |
|
|
73
71
|
<!-- generated:end -->
|