things-api 0.10.0 → 0.11.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 +38 -12
- package/dist/cli/clock.d.ts +8 -0
- package/dist/cli/clock.js +24 -0
- package/dist/cli/clock.js.map +1 -0
- package/dist/cli/commands/area.js +8 -9
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +45 -0
- package/dist/cli/commands/install-skill.js +207 -0
- package/dist/cli/commands/install-skill.js.map +1 -0
- package/dist/cli/commands/project.js +7 -6
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +15 -2
- package/dist/cli/commands/reads.js +45 -17
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.js +7 -2
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +6 -6
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +14 -8
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +29 -32
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/glyphs.d.ts +16 -0
- package/dist/cli/glyphs.js +23 -0
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +56 -6
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +33 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +45 -0
- package/dist/cli/move-hint.js +196 -0
- package/dist/cli/move-hint.js.map +1 -0
- package/dist/cli/period.d.ts +2 -8
- package/dist/cli/period.js +44 -30
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.js +11 -1
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.js +16 -7
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/skill-check.d.ts +21 -0
- package/dist/cli/skill-check.js +82 -0
- package/dist/cli/skill-check.js.map +1 -0
- package/dist/cli/skill.d.ts +62 -0
- package/dist/cli/skill.js +142 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tag-filters.d.ts +6 -2
- package/dist/cli/tag-filters.js +8 -6
- package/dist/cli/tag-filters.js.map +1 -1
- package/dist/cli/verb-hint.js +27 -2
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/cli/version.d.ts +4 -0
- package/dist/cli/version.js +18 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/client.d.ts +35 -12
- package/dist/client.js +42 -18
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +11 -1
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +550 -506
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/clock.d.ts +61 -0
- package/dist/model/clock.js +71 -0
- package/dist/model/clock.js.map +1 -0
- package/dist/model/dates.d.ts +26 -3
- package/dist/model/dates.js +84 -3
- package/dist/model/dates.js.map +1 -1
- package/dist/model/entities.d.ts +17 -3
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +2 -2
- package/dist/model/mappers.js +25 -7
- package/dist/model/mappers.js.map +1 -1
- package/dist/read/area-view.d.ts +1 -1
- package/dist/read/area-view.js +8 -6
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.d.ts +1 -1
- package/dist/read/detail.js +11 -4
- package/dist/read/detail.js.map +1 -1
- package/dist/read/log-boundary.d.ts +1 -1
- package/dist/read/log-boundary.js +13 -3
- package/dist/read/log-boundary.js.map +1 -1
- package/dist/read/project-view.d.ts +1 -1
- package/dist/read/project-view.js +8 -6
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +3 -1
- package/dist/read/queries.js +27 -5
- package/dist/read/queries.js.map +1 -1
- package/dist/read/snapshot.d.ts +1 -1
- package/dist/read/snapshot.js +9 -2
- package/dist/read/snapshot.js.map +1 -1
- package/dist/read/truncation.d.ts +1 -1
- package/dist/read/truncation.js +2 -2
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +12 -11
- package/dist/read/views.js +48 -43
- package/dist/read/views.js.map +1 -1
- package/dist/write/accessibility-probe.d.ts +1 -1
- package/dist/write/accessibility-probe.js +8 -0
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.d.ts +1 -1
- package/dist/write/automation-probe.js +8 -0
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.js +3 -2
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +1 -1
- package/dist/write/commands.js +54 -15
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +23 -0
- package/dist/write/guards.js.map +1 -1
- package/dist/write/pipeline.d.ts +54 -2
- package/dist/write/pipeline.js +112 -20
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.js +8 -1
- package/dist/write/planner.js.map +1 -1
- package/dist/write/pre-state.d.ts +37 -0
- package/dist/write/pre-state.js +42 -0
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/recurrence-rule-blob.d.ts +65 -0
- package/dist/write/recurrence-rule-blob.js +150 -0
- package/dist/write/recurrence-rule-blob.js.map +1 -0
- package/dist/write/vectors/registry.d.ts +5 -7
- package/dist/write/vectors/registry.js +49 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +44 -0
- package/dist/write/vectors/simulator.js +1136 -0
- package/dist/write/vectors/simulator.js.map +1 -0
- package/dist/write/vectors/types.d.ts +35 -0
- package/dist/write/vectors/ui-certification.d.ts +9 -3
- package/dist/write/vectors/ui-certification.js +3 -6
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +13 -0
- package/dist/write/vectors/ui-recipes.js +52 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +15 -0
- package/dist/write/vectors/ui.js +54 -6
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +110 -1
- package/dist/write/verify/delta.js +177 -15
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +7 -1
- package/dist/write/verify/poller.js +20 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +18 -3
- package/skills/things-cli/SKILL.md +53 -0
- package/skills/things-cli/references/contracts.md +41 -0
- package/skills/things-cli/references/gui.md +11 -0
- package/skills/things-cli/references/model.md +51 -0
package/dist/mcp/server.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
-
import { ALL_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, blockedCode, BOUNCE_MAX_ITEMS, capabilitiesTable, DATE_FORMAT, DEFAULT_LIST_LIMIT, diagnose, FILTER_CONTRACT, hasTagPresence, LIMIT_DESC, MCP_WHEN_LABELS, noUuidMatch, omitEmpty, OMIT_EMPTY_NOTE, OPERATION_KINDS, openThings, PKG_VERSION, PROJECT_LIMIT_DESC, PROJECT_PREVIEW_LIMIT, REF_FORMAT, ReferenceResolutionError, REMINDER_FORMAT, schemaWarnings, splitWhenSugar, tagFilterFields, tagFlagConflict, validateViewArgs, verifyFailedCode, WHEN_VALUES, } from "../index.js";
|
|
16
|
+
import { ALL_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, blockedCode, BOUNCE_MAX_ITEMS, capabilitiesTable, DATE_FORMAT, DEFAULT_LIST_LIMIT, diagnose, FILTER_CONTRACT, hasTagPresence, isValidTimeZone, LIMIT_DESC, MCP_WHEN_LABELS, noUuidMatch, omitEmpty, OMIT_EMPTY_NOTE, OPERATION_KINDS, openThings, PKG_VERSION, PROJECT_LIMIT_DESC, PROJECT_PREVIEW_LIMIT, REF_FORMAT, ReferenceResolutionError, REMINDER_FORMAT, schemaWarnings, splitWhenSugar, tagFilterFields, tagFlagConflict, validateViewArgs, verifyFailedCode, WHEN_VALUES, } from "../index.js";
|
|
17
17
|
function jsonResult(data) {
|
|
18
18
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
19
19
|
}
|
|
@@ -72,15 +72,40 @@ function resolveCap(value, all, defaultLimit) {
|
|
|
72
72
|
return null;
|
|
73
73
|
return value ?? defaultLimit;
|
|
74
74
|
}
|
|
75
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Resolve MCP limit/all (flat read tools) into a row cap (null = every row).
|
|
77
|
+
* all:true wins: it lifts the cap and takes precedence over a limit passed
|
|
78
|
+
* alongside it — an explicit "everything" request resolves the contradiction
|
|
79
|
+
* rather than erroring — so the pair is never a usage error on the flat tools.
|
|
80
|
+
*/
|
|
76
81
|
function resolveLimit(args) {
|
|
77
|
-
|
|
82
|
+
if (args.all === true)
|
|
83
|
+
return null;
|
|
84
|
+
return args.limit ?? DEFAULT_LIST_LIMIT;
|
|
78
85
|
}
|
|
86
|
+
/** Precedence notes appended to `limit`/`all` wherever a flat tool accepts both. */
|
|
87
|
+
const LIMIT_IGNORED_NOTE = "ignored when all is set";
|
|
88
|
+
const ALL_WINS_NOTE = "wins over limit if both are set";
|
|
79
89
|
/** Shared limit/all input schema fragment for the flat read tools. */
|
|
80
90
|
const limitShape = {
|
|
81
|
-
limit: z.number().int().min(1).optional().describe(LIMIT_DESC),
|
|
82
|
-
all: z.boolean().optional().describe(ALL_DESC),
|
|
91
|
+
limit: z.number().int().min(1).optional().describe(`${LIMIT_DESC}; ${LIMIT_IGNORED_NOTE}`),
|
|
92
|
+
all: z.boolean().optional().describe(`${ALL_DESC}; ${ALL_WINS_NOTE}`),
|
|
83
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* The per-call time-zone knob for date-sensitive tools: an IANA zone that
|
|
96
|
+
* evaluates every date boundary (today/evening/upcoming/logbook/overdue/…) for
|
|
97
|
+
* the consumer's calendar, overriding the server's THINGS_TZ for THIS call.
|
|
98
|
+
*/
|
|
99
|
+
const TZ_DESC = "IANA time zone (e.g. Asia/Tokyo) to evaluate date boundaries in for this call — " +
|
|
100
|
+
"overrides the server default. Reminder times stay wall-clock and are never shifted.";
|
|
101
|
+
const tzShape = { tz: z.string().optional().describe(TZ_DESC) };
|
|
102
|
+
/** A usage result when `tz` is present but not a recognized IANA zone; null when it is valid/absent. */
|
|
103
|
+
function badTz(tz) {
|
|
104
|
+
if (tz !== undefined && !isValidTimeZone(tz)) {
|
|
105
|
+
return usage(`tz is not a valid IANA time zone: "${tz}" — expected e.g. "America/New_York" or "Asia/Tokyo"`);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
84
109
|
function errorResult(error) {
|
|
85
110
|
return { content: [{ type: "text", text: JSON.stringify(error) }], isError: true };
|
|
86
111
|
}
|
|
@@ -176,6 +201,20 @@ const MCP_UNTAGGED_CONFLICT = "untagged does not combine with tag/exact_tag";
|
|
|
176
201
|
const dryRunShape = {
|
|
177
202
|
dry_run: z.boolean().optional().describe("Preview the planned change without applying anything"),
|
|
178
203
|
};
|
|
204
|
+
/**
|
|
205
|
+
* The per-call opt-in for the tools that reach a change only by driving the
|
|
206
|
+
* local Things app's accessibility interface. Shared by `repeat`, the
|
|
207
|
+
* `convert_to_project` heading action, and the `areas` reorder scope.
|
|
208
|
+
*/
|
|
209
|
+
const driveGuiShape = {
|
|
210
|
+
dangerously_drive_gui: z
|
|
211
|
+
.boolean()
|
|
212
|
+
.optional()
|
|
213
|
+
.describe("Required: this drives the local Things app through its accessibility interface to " +
|
|
214
|
+
"make a change the app offers nowhere else. It briefly interacts with the app's UI " +
|
|
215
|
+
"on the machine running this server, and must be turned on first with `things config " +
|
|
216
|
+
"set ui-enabled true`. Intended for a dedicated always-on Mac."),
|
|
217
|
+
};
|
|
179
218
|
/** How a tag value may be expressed on any tag-accepting tool. */
|
|
180
219
|
const TAG_REF_FORMAT = "each a tag name or a parent/child path; must exist unless create_tags is set";
|
|
181
220
|
/** create_tags param, shared by every tag-accepting write tool. */
|
|
@@ -211,10 +250,18 @@ function buildInstructions(getClient) {
|
|
|
211
250
|
"rather than being guessed at. Create missing tags/areas/projects first (add_tag, " +
|
|
212
251
|
"add_area, add_project).",
|
|
213
252
|
`- Scheduling vocabulary: when = ${WHEN_VALUES}; deadlines are ${DATE_FORMAT}; reminders ` +
|
|
214
|
-
`are ${REMINDER_FORMAT}
|
|
253
|
+
`are ${REMINDER_FORMAT}. Resolve relative calendar phrases against the Calendar context ` +
|
|
254
|
+
`below (or a date-sensitive read result's meta.clock.today), then pass the explicit date.`,
|
|
215
255
|
"- Every write tool accepts dry_run: true to preview the change without applying it. " +
|
|
216
|
-
"
|
|
217
|
-
"
|
|
256
|
+
"A preview creates no state, so later calls cannot reference an item that only appeared in " +
|
|
257
|
+
"a dry-run result. Operations with cascading or permanent effects require the explicit " +
|
|
258
|
+
"confirmation parameter named in their description; refused calls return an error saying " +
|
|
259
|
+
"what to pass.",
|
|
260
|
+
"- Read-result semantics: an item's tags are its direct tags; its effective tags also include " +
|
|
261
|
+
"tags inherited from its containing project and area. todaySection appears only for an item " +
|
|
262
|
+
"in Today, naming its section there (today or evening); an unscheduled start=active item is " +
|
|
263
|
+
"in Anytime and omits the field. Completing an item makes it findable in Logbook.",
|
|
264
|
+
"- For capped reads, pass limit to cap rows or all: true for everything; if both are set, all wins.",
|
|
218
265
|
`- Read results are compact: ${OMIT_EMPTY_NOTE}`,
|
|
219
266
|
];
|
|
220
267
|
try {
|
|
@@ -222,9 +269,12 @@ function buildInstructions(getClient) {
|
|
|
222
269
|
const areas = c.read.areas();
|
|
223
270
|
const tags = c.read.tags();
|
|
224
271
|
const projects = c.read.projects();
|
|
272
|
+
const clock = c.clockMeta();
|
|
225
273
|
const shown = projects.slice(0, INSTRUCTIONS_MAX_PROJECTS);
|
|
226
274
|
const overflow = projects.length - shown.length;
|
|
227
|
-
lines.push("", "Current inventory (read at server start — refresh with list_collections):",
|
|
275
|
+
lines.push("", "Current inventory (read at server start — refresh with list_collections):", ...(clock !== undefined
|
|
276
|
+
? [`- Calendar context at server start: ${JSON.stringify(clock)}`]
|
|
277
|
+
: []), `- Areas (${areas.length}): ${areas.map((a) => a.title).join(", ") || "none"}`, `- Tags (${tags.length}): ${tags.map(tagLabel).join(", ") || "none"}`, `- Open projects (${projects.length}): ${shown.map((p) => p.title).join("; ") || "none"}` +
|
|
228
278
|
(overflow > 0 ? `; …and ${overflow} more (list_collections for all)` : ""));
|
|
229
279
|
}
|
|
230
280
|
catch {
|
|
@@ -285,6 +335,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
285
335
|
...(args.acknowledge_tag_subtree === true && { acknowledgeTagSubtree: true }),
|
|
286
336
|
...(args.dangerously_drive_gui === true && { dangerouslyDriveGui: true }),
|
|
287
337
|
...(args.create_tags === true && { createTags: true }),
|
|
338
|
+
...(args.tz !== undefined && { zone: args.tz }),
|
|
288
339
|
});
|
|
289
340
|
/** Run a handler, mapping environment/usage throws to tool errors. */
|
|
290
341
|
const guard = async (fn) => {
|
|
@@ -314,22 +365,32 @@ export function createThingsMcpServer(options = {}) {
|
|
|
314
365
|
* longer matches the validated schema and its data may be incomplete. No
|
|
315
366
|
* block is added when the schema checks out or the read itself errored.
|
|
316
367
|
*/
|
|
317
|
-
const readGuard = async (fn) => {
|
|
368
|
+
const readGuard = async (fn, tz) => {
|
|
318
369
|
const result = await guard(fn);
|
|
319
370
|
if (result.isError === true)
|
|
320
371
|
return result;
|
|
321
372
|
let warnings = [];
|
|
373
|
+
let clock;
|
|
322
374
|
try {
|
|
323
|
-
|
|
375
|
+
const c = getClient();
|
|
376
|
+
warnings = schemaWarnings(c.schemaStatus());
|
|
377
|
+
// The clock honesty field for this call's effective zone (the per-call
|
|
378
|
+
// tz over the server default) — present only when a consumer zone /
|
|
379
|
+
// pinned now is in effect.
|
|
380
|
+
clock = c.clockMeta(tz);
|
|
324
381
|
}
|
|
325
382
|
catch {
|
|
326
383
|
warnings = [];
|
|
327
384
|
}
|
|
328
|
-
|
|
385
|
+
const meta = {
|
|
386
|
+
...(warnings.length > 0 && { warnings }),
|
|
387
|
+
...(clock !== undefined && { clock }),
|
|
388
|
+
};
|
|
389
|
+
if (Object.keys(meta).length === 0)
|
|
329
390
|
return result;
|
|
330
391
|
return {
|
|
331
392
|
...result,
|
|
332
|
-
content: [...result.content, { type: "text", text: JSON.stringify({ meta
|
|
393
|
+
content: [...result.content, { type: "text", text: JSON.stringify({ meta }) }],
|
|
333
394
|
};
|
|
334
395
|
};
|
|
335
396
|
/** Resolve a uuid to to-do/project for the type-generic item tools. */
|
|
@@ -360,6 +421,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
360
421
|
inputSchema: {
|
|
361
422
|
view: z.enum(["today", "inbox", "anytime", "upcoming", "someday", "logbook", "trash"]),
|
|
362
423
|
...tagFilterShape,
|
|
424
|
+
...tzShape,
|
|
363
425
|
evening: z.boolean().optional().describe("today only: show only the This Evening section"),
|
|
364
426
|
show_active_project_items: z
|
|
365
427
|
.union([z.boolean(), z.number().int().min(1)])
|
|
@@ -382,7 +444,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
382
444
|
.int()
|
|
383
445
|
.min(1)
|
|
384
446
|
.optional()
|
|
385
|
-
.describe(`flat views only (not anytime/someday): ${LIMIT_DESC}`),
|
|
447
|
+
.describe(`flat views only (not anytime/someday): ${LIMIT_DESC}; ${LIMIT_IGNORED_NOTE}`),
|
|
386
448
|
area_limit: z
|
|
387
449
|
.number()
|
|
388
450
|
.int()
|
|
@@ -398,10 +460,14 @@ export function createThingsMcpServer(options = {}) {
|
|
|
398
460
|
all: z
|
|
399
461
|
.boolean()
|
|
400
462
|
.optional()
|
|
401
|
-
.describe("show everything (flat views: no row limit; anytime/someday: no per-block caps)"
|
|
463
|
+
.describe("show everything (flat views: no row limit; anytime/someday: no per-block caps); " +
|
|
464
|
+
ALL_WINS_NOTE),
|
|
402
465
|
},
|
|
403
466
|
annotations: READ_ONLY,
|
|
404
467
|
}, async (args) => readGuard(() => {
|
|
468
|
+
const badZone = badTz(args.tz);
|
|
469
|
+
if (badZone !== null)
|
|
470
|
+
return badZone;
|
|
405
471
|
// Tag-conflict AND overdue-applicability both derive from the shared
|
|
406
472
|
// contract: read_view honors overdue only on today/inbox/anytime/someday
|
|
407
473
|
// (the current-work views), matching FILTER_CONTRACT.
|
|
@@ -434,8 +500,6 @@ export function createThingsMcpServer(options = {}) {
|
|
|
434
500
|
"to cap that section's project lists");
|
|
435
501
|
}
|
|
436
502
|
const limit = resolveLimit(args);
|
|
437
|
-
if (limit === "conflict")
|
|
438
|
-
return usage("pass at most one of limit / all");
|
|
439
503
|
const areaLimit = resolveCap(args.area_limit, args.all, AREA_PREVIEW_LIMIT);
|
|
440
504
|
const projectLimit = resolveCap(args.project_limit, args.all, PROJECT_PREVIEW_LIMIT);
|
|
441
505
|
if (areaLimit === "conflict" || projectLimit === "conflict") {
|
|
@@ -443,26 +507,34 @@ export function createThingsMcpServer(options = {}) {
|
|
|
443
507
|
}
|
|
444
508
|
const c = getClient();
|
|
445
509
|
const filter = validated.filter;
|
|
510
|
+
const zone = args.tz !== undefined ? { zone: args.tz } : {};
|
|
446
511
|
switch (args.view) {
|
|
447
512
|
case "today": {
|
|
448
513
|
const { view, truncation } = c.read.today({
|
|
449
514
|
...filter,
|
|
515
|
+
...zone,
|
|
450
516
|
...(args.evening === true && { eveningOnly: true }),
|
|
451
517
|
limit,
|
|
452
518
|
});
|
|
453
519
|
return truncatedResult(view, truncation);
|
|
454
520
|
}
|
|
455
521
|
case "inbox": {
|
|
456
|
-
const { items, truncation } = c.read.inbox({ ...filter, limit });
|
|
522
|
+
const { items, truncation } = c.read.inbox({ ...filter, ...zone, limit });
|
|
457
523
|
return truncatedResult(items, truncation);
|
|
458
524
|
}
|
|
459
525
|
case "anytime": {
|
|
460
|
-
const { view, grouped } = c.read.anytime({
|
|
526
|
+
const { view, grouped } = c.read.anytime({
|
|
527
|
+
...filter,
|
|
528
|
+
...zone,
|
|
529
|
+
areaLimit,
|
|
530
|
+
projectLimit,
|
|
531
|
+
});
|
|
461
532
|
return groupedResult(view, grouped);
|
|
462
533
|
}
|
|
463
534
|
case "upcoming": {
|
|
464
535
|
const { items, truncation } = c.read.upcoming({
|
|
465
536
|
...filter,
|
|
537
|
+
...zone,
|
|
466
538
|
...(args.horizon !== undefined && { horizon: args.horizon }),
|
|
467
539
|
limit,
|
|
468
540
|
});
|
|
@@ -475,6 +547,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
475
547
|
}
|
|
476
548
|
const { view, grouped } = c.read.someday({
|
|
477
549
|
...filter,
|
|
550
|
+
...zone,
|
|
478
551
|
...((active === true || typeof active === "number") && {
|
|
479
552
|
activeProjectItems: true,
|
|
480
553
|
}),
|
|
@@ -485,15 +558,15 @@ export function createThingsMcpServer(options = {}) {
|
|
|
485
558
|
return groupedResult(view, grouped);
|
|
486
559
|
}
|
|
487
560
|
case "logbook": {
|
|
488
|
-
const { items, truncation } = c.read.logbook({ ...filter, limit });
|
|
561
|
+
const { items, truncation } = c.read.logbook({ ...filter, ...zone, limit });
|
|
489
562
|
return truncatedResult(items, truncation);
|
|
490
563
|
}
|
|
491
564
|
case "trash": {
|
|
492
|
-
const { items, truncation } = c.read.trash({ limit });
|
|
565
|
+
const { items, truncation } = c.read.trash({ ...zone, limit });
|
|
493
566
|
return truncatedResult(items, truncation);
|
|
494
567
|
}
|
|
495
568
|
}
|
|
496
|
-
}));
|
|
569
|
+
}, args.tz));
|
|
497
570
|
server.registerTool("search", {
|
|
498
571
|
description: "Find items by title/notes substring. Returns open, untrashed items by default; " +
|
|
499
572
|
"include more with logged/trashed/all. Scope with project/area/tag — scope " +
|
|
@@ -502,6 +575,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
502
575
|
inputSchema: {
|
|
503
576
|
query: z.string(),
|
|
504
577
|
...tagFilterShape,
|
|
578
|
+
...tzShape,
|
|
505
579
|
project: z
|
|
506
580
|
.string()
|
|
507
581
|
.optional()
|
|
@@ -516,11 +590,14 @@ export function createThingsMcpServer(options = {}) {
|
|
|
516
590
|
all: z
|
|
517
591
|
.boolean()
|
|
518
592
|
.optional()
|
|
519
|
-
.describe(
|
|
520
|
-
limit: z.number().int().min(1).optional().describe(LIMIT_DESC),
|
|
593
|
+
.describe(`Everything, unbounded: open + logged + trashed, no row limit; ${ALL_WINS_NOTE}`),
|
|
594
|
+
limit: z.number().int().min(1).optional().describe(`${LIMIT_DESC}; ${LIMIT_IGNORED_NOTE}`),
|
|
521
595
|
},
|
|
522
596
|
annotations: READ_ONLY,
|
|
523
597
|
}, async (args) => readGuard(() => {
|
|
598
|
+
const badZone = badTz(args.tz);
|
|
599
|
+
if (badZone !== null)
|
|
600
|
+
return badZone;
|
|
524
601
|
// Tag-conflict AND the overdue/status-widening incompatibility both
|
|
525
602
|
// derive from the shared contract (search: statusWidening = true).
|
|
526
603
|
const validated = validateViewArgs("search", {
|
|
@@ -537,11 +614,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
537
614
|
if (!validated.ok)
|
|
538
615
|
return usage(validated.message);
|
|
539
616
|
const limit = resolveLimit(args);
|
|
540
|
-
if (limit === "conflict")
|
|
541
|
-
return usage("pass at most one of limit / all");
|
|
542
617
|
const { items, truncation } = getClient().read.search(args.query, {
|
|
543
618
|
limit,
|
|
544
619
|
...validated.filter,
|
|
620
|
+
...(args.tz !== undefined && { zone: args.tz }),
|
|
545
621
|
...(args.project !== undefined && { project: args.project }),
|
|
546
622
|
...(args.area !== undefined && { area: args.area }),
|
|
547
623
|
...(args.type !== undefined && { type: args.type }),
|
|
@@ -550,7 +626,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
550
626
|
...(args.all === true && { all: true }),
|
|
551
627
|
});
|
|
552
628
|
return truncatedResult(items, truncation);
|
|
553
|
-
}));
|
|
629
|
+
}, args.tz));
|
|
554
630
|
server.registerTool("changes_since", {
|
|
555
631
|
description: "List items created or modified since a moment — including trashed, logged, and " +
|
|
556
632
|
"repeating items (inspect each item's fields to tell them apart). Edits to tags, " +
|
|
@@ -559,19 +635,25 @@ export function createThingsMcpServer(options = {}) {
|
|
|
559
635
|
inputSchema: {
|
|
560
636
|
since: z.string().describe("ISO date-time, e.g. 2026-07-06T08:00:00"),
|
|
561
637
|
...limitShape,
|
|
638
|
+
...tzShape,
|
|
562
639
|
},
|
|
563
640
|
annotations: READ_ONLY,
|
|
564
641
|
}, async (args) => readGuard(() => {
|
|
642
|
+
const badZone = badTz(args.tz);
|
|
643
|
+
if (badZone !== null)
|
|
644
|
+
return badZone;
|
|
565
645
|
const limit = resolveLimit(args);
|
|
566
|
-
if (limit === "conflict")
|
|
567
|
-
return usage("pass at most one of limit / all");
|
|
568
646
|
const since = new Date(args.since);
|
|
569
647
|
if (Number.isNaN(since.getTime())) {
|
|
570
648
|
return usage(`since is not a parseable date: ${args.since}`);
|
|
571
649
|
}
|
|
572
|
-
const { items, truncation } = getClient().read.changes({
|
|
650
|
+
const { items, truncation } = getClient().read.changes({
|
|
651
|
+
since,
|
|
652
|
+
limit,
|
|
653
|
+
...(args.tz !== undefined && { zone: args.tz }),
|
|
654
|
+
});
|
|
573
655
|
return truncatedResult(items, truncation);
|
|
574
|
-
}));
|
|
656
|
+
}, args.tz));
|
|
575
657
|
server.registerTool("get_item", {
|
|
576
658
|
description: "Full detail for one item by uuid: notes, schedule, reminder, deadline, tags " +
|
|
577
659
|
"(direct and inherited), checklist with per-item state, repeat schedule, and its " +
|
|
@@ -593,6 +675,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
593
675
|
inputSchema: {
|
|
594
676
|
uuid: z.string().describe("Project uuid or unique name"),
|
|
595
677
|
...tagOnlyShape,
|
|
678
|
+
...tzShape,
|
|
596
679
|
overdue: z
|
|
597
680
|
.boolean()
|
|
598
681
|
.optional()
|
|
@@ -600,13 +683,17 @@ export function createThingsMcpServer(options = {}) {
|
|
|
600
683
|
},
|
|
601
684
|
annotations: READ_ONLY,
|
|
602
685
|
}, async (args) => readGuard(() => {
|
|
686
|
+
const badZone = badTz(args.tz);
|
|
687
|
+
if (badZone !== null)
|
|
688
|
+
return badZone;
|
|
603
689
|
if (tagFlagConflict(tagPresence(args)))
|
|
604
690
|
return usage(MCP_UNTAGGED_CONFLICT);
|
|
605
691
|
return readResult(getClient().read.projectView(args.uuid, {
|
|
606
692
|
overdue: args.overdue === true,
|
|
607
693
|
...tagFilterFields(tagPresence(args)),
|
|
694
|
+
...(args.tz !== undefined && { zone: args.tz }),
|
|
608
695
|
}));
|
|
609
|
-
}));
|
|
696
|
+
}, args.tz));
|
|
610
697
|
server.registerTool("get_area", {
|
|
611
698
|
description: "One area's contents: metadata plus its direct to-dos (active first), its " +
|
|
612
699
|
"projects in canonical order, later (scheduled/repeating/someday), and logged items. " +
|
|
@@ -617,6 +704,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
617
704
|
inputSchema: {
|
|
618
705
|
ref: z.string().describe("Area uuid or unique name"),
|
|
619
706
|
...tagOnlyShape,
|
|
707
|
+
...tzShape,
|
|
620
708
|
area_limit: z
|
|
621
709
|
.number()
|
|
622
710
|
.int()
|
|
@@ -637,6 +725,9 @@ export function createThingsMcpServer(options = {}) {
|
|
|
637
725
|
},
|
|
638
726
|
annotations: READ_ONLY,
|
|
639
727
|
}, async (args) => readGuard(() => {
|
|
728
|
+
const badZone = badTz(args.tz);
|
|
729
|
+
if (badZone !== null)
|
|
730
|
+
return badZone;
|
|
640
731
|
if (tagFlagConflict(tagPresence(args)))
|
|
641
732
|
return usage(MCP_UNTAGGED_CONFLICT);
|
|
642
733
|
const areaLimit = resolveCap(args.area_limit, args.all, AREA_PREVIEW_LIMIT);
|
|
@@ -647,11 +738,12 @@ export function createThingsMcpServer(options = {}) {
|
|
|
647
738
|
const { view, grouped } = getClient().read.areaView(args.ref, {
|
|
648
739
|
overdue: args.overdue === true,
|
|
649
740
|
...tagFilterFields(tagPresence(args)),
|
|
741
|
+
...(args.tz !== undefined && { zone: args.tz }),
|
|
650
742
|
areaLimit,
|
|
651
743
|
projectLimit,
|
|
652
744
|
});
|
|
653
745
|
return groupedResult(view, grouped);
|
|
654
|
-
}));
|
|
746
|
+
}, args.tz));
|
|
655
747
|
server.registerTool("list_collections", {
|
|
656
748
|
description: "List every project, area, or tag (tags include their parent-tag nesting). Use to " +
|
|
657
749
|
"refresh the inventory summarized in the server instructions. The tag filters scope " +
|
|
@@ -660,6 +752,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
660
752
|
inputSchema: {
|
|
661
753
|
kind: z.enum(["projects", "areas", "tags"]),
|
|
662
754
|
...tagOnlyShape,
|
|
755
|
+
...tzShape,
|
|
663
756
|
overdue: z
|
|
664
757
|
.boolean()
|
|
665
758
|
.optional()
|
|
@@ -667,6 +760,9 @@ export function createThingsMcpServer(options = {}) {
|
|
|
667
760
|
},
|
|
668
761
|
annotations: READ_ONLY,
|
|
669
762
|
}, async (args) => readGuard(() => {
|
|
763
|
+
const badZone = badTz(args.tz);
|
|
764
|
+
if (badZone !== null)
|
|
765
|
+
return badZone;
|
|
670
766
|
const c = getClient();
|
|
671
767
|
// areas/tags are not dated entities and have no per-row tag list to
|
|
672
768
|
// filter — overdue and the tag filters are vacuous there, rejected
|
|
@@ -692,17 +788,18 @@ export function createThingsMcpServer(options = {}) {
|
|
|
692
788
|
? c.read.projects({
|
|
693
789
|
overdue: args.overdue === true,
|
|
694
790
|
...tagFilterFields(tagPresence(args)),
|
|
791
|
+
...(args.tz !== undefined && { zone: args.tz }),
|
|
695
792
|
})
|
|
696
793
|
: args.kind === "areas"
|
|
697
794
|
? c.read.areas()
|
|
698
795
|
: c.read.tags());
|
|
699
|
-
}));
|
|
796
|
+
}, args.tz));
|
|
700
797
|
// ---------------------------------------------------------------- to-dos
|
|
701
798
|
const whenSchema = z.string().optional().describe(WHEN_VALUES);
|
|
702
799
|
server.registerTool("add_todo", {
|
|
703
800
|
description: "Create a to-do and return its uuid. Optionally schedule it, set a reminder or " +
|
|
704
801
|
"deadline, tag it, give it a checklist, and place it in a project or area " +
|
|
705
|
-
"(optionally under
|
|
802
|
+
"(optionally under a heading within that project). A reminder " +
|
|
706
803
|
"requires when = today, evening, or a date. Adding into a completed or canceled " +
|
|
707
804
|
"project reopens that project — pass acknowledge_project_reopen to confirm.",
|
|
708
805
|
inputSchema: {
|
|
@@ -721,10 +818,14 @@ export function createThingsMcpServer(options = {}) {
|
|
|
721
818
|
.optional()
|
|
722
819
|
.describe("Confirm adding into a completed/canceled project (this reopens it)"),
|
|
723
820
|
...createTagsShape,
|
|
821
|
+
...tzShape,
|
|
724
822
|
...dryRunShape,
|
|
725
823
|
},
|
|
726
824
|
annotations: NON_DESTRUCTIVE,
|
|
727
825
|
}, async (args) => guard(async () => {
|
|
826
|
+
const badZone = badTz(args.tz);
|
|
827
|
+
if (badZone !== null)
|
|
828
|
+
return badZone;
|
|
728
829
|
const sugar = splitWhenSugar(args.when, args.reminder !== undefined, MCP_WHEN_LABELS);
|
|
729
830
|
if (sugar.kind === "error")
|
|
730
831
|
return usage(sugar.message);
|
|
@@ -743,76 +844,189 @@ export function createThingsMcpServer(options = {}) {
|
|
|
743
844
|
...(args.heading !== undefined && { heading: args.heading }),
|
|
744
845
|
}, writeOptions(args)));
|
|
745
846
|
}));
|
|
746
|
-
server.registerTool("
|
|
747
|
-
description: "
|
|
748
|
-
"append_notes/prepend_notes add a line to the
|
|
749
|
-
"
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
"
|
|
753
|
-
"
|
|
847
|
+
server.registerTool("update", {
|
|
848
|
+
description: "Edit an existing to-do, project, area, or tag — kind selects which. " +
|
|
849
|
+
"kind todo/project: title, notes (or append_notes/prepend_notes to add a line to the " +
|
|
850
|
+
"existing body, exclusive with notes), schedule (when), reminder/clear_reminder, and " +
|
|
851
|
+
"deadline/clear_deadline; changing the schedule keeps an existing reminder unless a new " +
|
|
852
|
+
"one is set, schedule and deadline changes are unavailable for repeating items, and " +
|
|
853
|
+
"clear_reminder needs the item scheduled for today or this evening (a reminder on a " +
|
|
854
|
+
"future date can only be changed, not cleared). kind area: title and/or tags (the full " +
|
|
855
|
+
"replacement set). kind tag: title, parent (nest under it) or unnest (to the top level; " +
|
|
856
|
+
"exclusive), and shortcut or clear_shortcut (exclusive). Tags must exist unless " +
|
|
857
|
+
"create_tags is set.",
|
|
754
858
|
inputSchema: {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
859
|
+
kind: z.enum(["todo", "project", "area", "tag"]),
|
|
860
|
+
uuid: z
|
|
861
|
+
.string()
|
|
862
|
+
.describe("The item to update — a to-do by uuid; a project, area, or tag also accepts a " +
|
|
863
|
+
"unique name"),
|
|
864
|
+
title: z.string().optional().describe("New title (any kind)"),
|
|
865
|
+
notes: z.string().optional().describe("todo/project: replaces the whole notes body"),
|
|
866
|
+
append_notes: z.string().optional().describe("todo/project: add a line after the notes"),
|
|
867
|
+
prepend_notes: z.string().optional().describe("todo/project: add a line before the notes"),
|
|
760
868
|
when: whenSchema,
|
|
761
|
-
reminder: z.string().optional().describe(REMINDER_FORMAT),
|
|
762
|
-
clear_reminder: z.boolean().optional(),
|
|
763
|
-
deadline: z.string().optional().describe(DATE_FORMAT),
|
|
764
|
-
clear_deadline: z.boolean().optional(),
|
|
869
|
+
reminder: z.string().optional().describe(`todo/project: ${REMINDER_FORMAT}`),
|
|
870
|
+
clear_reminder: z.boolean().optional().describe("todo/project: remove the reminder"),
|
|
871
|
+
deadline: z.string().optional().describe(`todo/project: ${DATE_FORMAT}`),
|
|
872
|
+
clear_deadline: z.boolean().optional().describe("todo/project: remove the deadline"),
|
|
873
|
+
tags: z
|
|
874
|
+
.array(z.string())
|
|
875
|
+
.optional()
|
|
876
|
+
.describe(`area: replace the tag set (full) — ${TAG_REF_FORMAT}`),
|
|
877
|
+
parent: z.string().optional().describe("tag: existing tag to nest under"),
|
|
878
|
+
unnest: z.boolean().optional().describe("tag: move the tag to the top level"),
|
|
879
|
+
shortcut: z.string().optional().describe("tag: keyboard shortcut character"),
|
|
880
|
+
clear_shortcut: z.boolean().optional().describe("tag: remove the keyboard shortcut"),
|
|
881
|
+
...createTagsShape,
|
|
882
|
+
...tzShape,
|
|
765
883
|
...dryRunShape,
|
|
766
884
|
},
|
|
767
885
|
annotations: NON_DESTRUCTIVE,
|
|
768
886
|
}, async (args) => guard(async () => {
|
|
769
|
-
const
|
|
770
|
-
if (
|
|
771
|
-
return
|
|
887
|
+
const badZone = badTz(args.tz);
|
|
888
|
+
if (badZone !== null)
|
|
889
|
+
return badZone;
|
|
890
|
+
const opts = writeOptions(args);
|
|
891
|
+
const c = getClient();
|
|
892
|
+
if (args.kind === "todo" || args.kind === "project") {
|
|
893
|
+
const notesModes = [args.notes, args.append_notes, args.prepend_notes].filter((v) => v !== undefined);
|
|
894
|
+
if (notesModes.length > 1) {
|
|
895
|
+
return usage("notes, append_notes, prepend_notes are exclusive");
|
|
896
|
+
}
|
|
897
|
+
if (args.reminder !== undefined && args.clear_reminder === true) {
|
|
898
|
+
return usage("pass at most one of reminder / clear_reminder");
|
|
899
|
+
}
|
|
900
|
+
if (args.deadline !== undefined && args.clear_deadline === true) {
|
|
901
|
+
return usage("pass at most one of deadline / clear_deadline");
|
|
902
|
+
}
|
|
903
|
+
if (args.kind === "todo") {
|
|
904
|
+
const sugar = splitWhenSugar(args.when, args.reminder !== undefined, MCP_WHEN_LABELS);
|
|
905
|
+
if (sugar.kind === "error")
|
|
906
|
+
return usage(sugar.message);
|
|
907
|
+
const when = sugar.kind === "split" ? sugar.when : args.when;
|
|
908
|
+
const reminder = sugar.kind === "split" ? sugar.reminder : args.reminder;
|
|
909
|
+
return mutationResult(await c.write.updateTodo(args.uuid, {
|
|
910
|
+
...(args.title !== undefined && { title: args.title }),
|
|
911
|
+
...(args.notes !== undefined && { notes: args.notes }),
|
|
912
|
+
...(args.append_notes !== undefined && { appendNotes: args.append_notes }),
|
|
913
|
+
...(args.prepend_notes !== undefined && { prependNotes: args.prepend_notes }),
|
|
914
|
+
...(when !== undefined && { when: when }),
|
|
915
|
+
...(reminder !== undefined && { reminder }),
|
|
916
|
+
...(args.clear_reminder === true && { reminder: null }),
|
|
917
|
+
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
918
|
+
...(args.clear_deadline === true && { deadline: null }),
|
|
919
|
+
}, opts));
|
|
920
|
+
}
|
|
921
|
+
return mutationResult(await c.write.updateProject(args.uuid, {
|
|
922
|
+
...(args.title !== undefined && { title: args.title }),
|
|
923
|
+
...(args.notes !== undefined && { notes: args.notes }),
|
|
924
|
+
...(args.append_notes !== undefined && { appendNotes: args.append_notes }),
|
|
925
|
+
...(args.prepend_notes !== undefined && { prependNotes: args.prepend_notes }),
|
|
926
|
+
...(args.when !== undefined && { when: args.when }),
|
|
927
|
+
...(args.reminder !== undefined && { reminder: args.reminder }),
|
|
928
|
+
...(args.clear_reminder === true && { reminder: null }),
|
|
929
|
+
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
930
|
+
...(args.clear_deadline === true && { deadline: null }),
|
|
931
|
+
}, opts));
|
|
932
|
+
}
|
|
933
|
+
if (args.kind === "area") {
|
|
934
|
+
if (args.title === undefined && args.tags === undefined) {
|
|
935
|
+
return usage("kind area requires title and/or tags");
|
|
936
|
+
}
|
|
937
|
+
return mutationResult(await c.write.updateArea(args.uuid, {
|
|
938
|
+
...(args.title !== undefined && { title: args.title }),
|
|
939
|
+
...(args.tags !== undefined && { tags: args.tags }),
|
|
940
|
+
}, opts));
|
|
941
|
+
}
|
|
942
|
+
// kind tag
|
|
943
|
+
if (args.title === undefined &&
|
|
944
|
+
args.parent === undefined &&
|
|
945
|
+
args.unnest === undefined &&
|
|
946
|
+
args.shortcut === undefined &&
|
|
947
|
+
args.clear_shortcut === undefined) {
|
|
948
|
+
return usage("kind tag requires title, parent, unnest, shortcut, and/or clear_shortcut");
|
|
772
949
|
}
|
|
773
|
-
if (args.
|
|
774
|
-
return usage("
|
|
950
|
+
if (args.parent !== undefined && args.unnest === true) {
|
|
951
|
+
return usage("parent and unnest are exclusive");
|
|
775
952
|
}
|
|
776
|
-
if (args.
|
|
777
|
-
return usage("
|
|
953
|
+
if (args.shortcut !== undefined && args.clear_shortcut === true) {
|
|
954
|
+
return usage("shortcut and clear_shortcut are exclusive");
|
|
778
955
|
}
|
|
779
|
-
|
|
780
|
-
if (sugar.kind === "error")
|
|
781
|
-
return usage(sugar.message);
|
|
782
|
-
const when = sugar.kind === "split" ? sugar.when : args.when;
|
|
783
|
-
const reminder = sugar.kind === "split" ? sugar.reminder : args.reminder;
|
|
784
|
-
return mutationResult(await getClient().write.updateTodo(args.uuid, {
|
|
956
|
+
return mutationResult(await c.write.updateTag(args.uuid, {
|
|
785
957
|
...(args.title !== undefined && { title: args.title }),
|
|
786
|
-
...(args.
|
|
787
|
-
...(args.
|
|
788
|
-
...(args.
|
|
789
|
-
...(
|
|
790
|
-
|
|
791
|
-
...(args.clear_reminder === true && { reminder: null }),
|
|
792
|
-
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
793
|
-
...(args.clear_deadline === true && { deadline: null }),
|
|
794
|
-
}, writeOptions(args)));
|
|
958
|
+
...(args.parent !== undefined && { parent: args.parent }),
|
|
959
|
+
...(args.unnest === true && { unnest: true }),
|
|
960
|
+
...(args.shortcut !== undefined && { shortcut: args.shortcut }),
|
|
961
|
+
...(args.clear_shortcut === true && { clearShortcut: true }),
|
|
962
|
+
}, opts));
|
|
795
963
|
}));
|
|
796
|
-
server.registerTool("
|
|
797
|
-
description: "Set a to-do's status: completed, canceled, or open
|
|
798
|
-
"completed/canceled
|
|
964
|
+
server.registerTool("set_status", {
|
|
965
|
+
description: "Set a to-do's or project's status (scope selects which): completed, canceled, or open " +
|
|
966
|
+
"(reopening a completed/canceled item). Not available for repeating to-dos. " +
|
|
967
|
+
"scope project, completing or canceling requires a children policy: 'require-resolved' " +
|
|
968
|
+
"errors if open to-dos remain; 'auto-complete'/'auto-cancel' resolves them together with " +
|
|
969
|
+
"the project (canceling never alters already-completed children). scope project, status " +
|
|
970
|
+
"open, restore_children also reopens the to-dos that were resolved with the project.",
|
|
799
971
|
inputSchema: {
|
|
800
|
-
|
|
972
|
+
scope: z.enum(["todo", "project"]),
|
|
973
|
+
uuid: z
|
|
974
|
+
.string()
|
|
975
|
+
.describe("The item — a to-do by uuid; a project also accepts a unique name"),
|
|
801
976
|
status: z.enum(["completed", "canceled", "open"]),
|
|
977
|
+
children: z
|
|
978
|
+
.enum(["require-resolved", "auto-complete", "auto-cancel"])
|
|
979
|
+
.optional()
|
|
980
|
+
.describe("scope project, completed/canceled: what to do with the project's open to-dos"),
|
|
981
|
+
restore_children: z
|
|
982
|
+
.boolean()
|
|
983
|
+
.optional()
|
|
984
|
+
.describe("scope project, open only: also reopen the to-dos resolved with the project"),
|
|
802
985
|
...dryRunShape,
|
|
803
986
|
},
|
|
804
987
|
annotations: NON_DESTRUCTIVE,
|
|
805
988
|
}, async (args) => guard(async () => {
|
|
806
989
|
const c = getClient();
|
|
807
990
|
const opts = writeOptions(args);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
991
|
+
if (args.scope === "todo") {
|
|
992
|
+
if (args.children !== undefined || args.restore_children !== undefined) {
|
|
993
|
+
return usage("children/restore_children apply only to scope project");
|
|
994
|
+
}
|
|
995
|
+
return mutationResult(args.status === "completed"
|
|
996
|
+
? await c.write.completeTodo(args.uuid, opts)
|
|
997
|
+
: args.status === "canceled"
|
|
998
|
+
? await c.write.cancelTodo(args.uuid, opts)
|
|
999
|
+
: await c.write.reopenTodo(args.uuid, opts));
|
|
1000
|
+
}
|
|
1001
|
+
// scope project
|
|
1002
|
+
if (args.status !== "open" && args.restore_children !== undefined) {
|
|
1003
|
+
return usage("restore_children applies only to status 'open'");
|
|
1004
|
+
}
|
|
1005
|
+
if (args.status === "completed") {
|
|
1006
|
+
if (args.children !== "require-resolved" && args.children !== "auto-complete") {
|
|
1007
|
+
return usage("status 'completed' requires children: 'require-resolved' or 'auto-complete'");
|
|
1008
|
+
}
|
|
1009
|
+
return mutationResult(await c.write.completeProject(args.uuid, { children: args.children }, opts));
|
|
1010
|
+
}
|
|
1011
|
+
if (args.status === "canceled") {
|
|
1012
|
+
if (args.children !== "require-resolved" && args.children !== "auto-cancel") {
|
|
1013
|
+
return usage("status 'canceled' requires children: 'require-resolved' or 'auto-cancel'");
|
|
1014
|
+
}
|
|
1015
|
+
return mutationResult(await c.write.cancelProject(args.uuid, { children: args.children }, opts));
|
|
1016
|
+
}
|
|
1017
|
+
if (args.children !== undefined) {
|
|
1018
|
+
return usage("children applies only to status 'completed' or 'canceled'");
|
|
1019
|
+
}
|
|
1020
|
+
const outcome = await c.write.reopenProject(args.uuid, {
|
|
1021
|
+
...opts,
|
|
1022
|
+
...(args.restore_children === true && { restoreChildren: true }),
|
|
1023
|
+
});
|
|
1024
|
+
return outcome.project.kind === "ok" || outcome.project.kind === "dry-run"
|
|
1025
|
+
? jsonResult(outcome)
|
|
1026
|
+
: mutationResult(outcome.project);
|
|
813
1027
|
}));
|
|
814
1028
|
server.registerTool("move_todo", {
|
|
815
|
-
description: "Move a to-do. Pass exactly one destination: a project
|
|
1029
|
+
description: "Move a to-do. Pass exactly one destination: a project or area (optionally an " +
|
|
816
1030
|
"existing heading within the project), to_inbox, or detach. Moving to the Inbox " +
|
|
817
1031
|
"removes any schedule; detach removes the project/area/heading assignment while " +
|
|
818
1032
|
"keeping the schedule. Moving into a completed or canceled project reopens that " +
|
|
@@ -965,17 +1179,44 @@ export function createThingsMcpServer(options = {}) {
|
|
|
965
1179
|
return mutationResult(await c.write.editChecklist(args.uuid, edit, writeOptions(args)));
|
|
966
1180
|
}));
|
|
967
1181
|
// ------------------------------------------------- to-dos AND projects
|
|
968
|
-
server.registerTool("
|
|
969
|
-
description: "
|
|
970
|
-
"
|
|
971
|
-
"
|
|
972
|
-
|
|
1182
|
+
server.registerTool("delete", {
|
|
1183
|
+
description: "Delete a to-do/project (kind item), an area, or a tag — kind selects which. " +
|
|
1184
|
+
"kind item: moves a to-do or project to the Trash (recoverable via restore_item until " +
|
|
1185
|
+
"the Trash is emptied; a deleted project takes its to-dos with it; not available for " +
|
|
1186
|
+
"repeating to-dos). kind area: PERMANENT — areas do not go to the Trash, so this cannot " +
|
|
1187
|
+
"be undone and requires dangerously_permanent; the area's to-dos move to the Trash and " +
|
|
1188
|
+
"its projects remain, no longer assigned to any area. kind tag: PERMANENT — requires " +
|
|
1189
|
+
"dangerously_permanent; the tag is removed from every item, and if it has nested child " +
|
|
1190
|
+
"tags they are ALL permanently deleted with it — pass acknowledge_tag_subtree to confirm.",
|
|
1191
|
+
inputSchema: {
|
|
1192
|
+
kind: z.enum(["item", "area", "tag"]),
|
|
1193
|
+
uuid: z
|
|
1194
|
+
.string()
|
|
1195
|
+
.describe("The target — an item by uuid; an area or tag also accepts a unique name"),
|
|
1196
|
+
dangerously_permanent: z
|
|
1197
|
+
.boolean()
|
|
1198
|
+
.optional()
|
|
1199
|
+
.describe("kind area/tag: confirm permanent, unrecoverable deletion"),
|
|
1200
|
+
acknowledge_tag_subtree: z
|
|
1201
|
+
.boolean()
|
|
1202
|
+
.optional()
|
|
1203
|
+
.describe("kind tag: confirm permanent deletion of ALL nested child tags too"),
|
|
1204
|
+
...dryRunShape,
|
|
1205
|
+
},
|
|
973
1206
|
annotations: DESTRUCTIVE,
|
|
974
1207
|
}, async (args) => guard(async () => {
|
|
975
1208
|
const c = getClient();
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
:
|
|
1209
|
+
const opts = writeOptions(args);
|
|
1210
|
+
switch (args.kind) {
|
|
1211
|
+
case "item":
|
|
1212
|
+
return mutationResult(itemType(args.uuid) === "to-do"
|
|
1213
|
+
? await c.write.deleteTodo(args.uuid, opts)
|
|
1214
|
+
: await c.write.deleteProject(args.uuid, opts));
|
|
1215
|
+
case "area":
|
|
1216
|
+
return mutationResult(await c.write.deleteArea(args.uuid, opts));
|
|
1217
|
+
case "tag":
|
|
1218
|
+
return mutationResult(await c.write.deleteTag(args.uuid, opts));
|
|
1219
|
+
}
|
|
979
1220
|
}));
|
|
980
1221
|
server.registerTool("restore_item", {
|
|
981
1222
|
description: "Restore a trashed to-do or project. A to-do returns to the Inbox without its " +
|
|
@@ -1022,17 +1263,80 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1022
1263
|
...(args.creation_date !== undefined && { creationDate: args.creation_date }),
|
|
1023
1264
|
...(args.notes !== undefined && { notes: args.notes }),
|
|
1024
1265
|
}, writeOptions(args)))));
|
|
1025
|
-
server.registerTool("
|
|
1026
|
-
description: "
|
|
1027
|
-
"
|
|
1028
|
-
"with `things setup shortcuts
|
|
1266
|
+
server.registerTool("heading", {
|
|
1267
|
+
description: "Manage a project heading — action selects which. create: a new heading in an existing " +
|
|
1268
|
+
"project (project + title; returns its uuid; uses the Things proxy shortcuts, set up " +
|
|
1269
|
+
"once with `things setup shortcuts`). rename: rename in place (uuid + title; works on " +
|
|
1270
|
+
"archived headings). archive: retire a heading so it leaves the active project view " +
|
|
1271
|
+
"(reversible with action unarchive); with open children pass children — complete or " +
|
|
1272
|
+
"cancel resolve them with the heading in one cascade, reparent moves them to the project " +
|
|
1273
|
+
"root keeping them open. unarchive: bring an archived heading back; restore_children also " +
|
|
1274
|
+
"reopens the children the archive resolved with it. convert_to_project: promote a to-do " +
|
|
1275
|
+
"or heading into a new project — this REPLACES the original and cannot be undone (a " +
|
|
1276
|
+
"converted heading's to-dos move under the new project), and requires " +
|
|
1277
|
+
"dangerously_drive_gui.",
|
|
1029
1278
|
inputSchema: {
|
|
1030
|
-
|
|
1031
|
-
|
|
1279
|
+
action: z.enum(["create", "rename", "archive", "unarchive", "convert_to_project"]),
|
|
1280
|
+
project: z.string().optional().describe(`create: existing project (${REF_FORMAT})`),
|
|
1281
|
+
uuid: z
|
|
1282
|
+
.string()
|
|
1283
|
+
.optional()
|
|
1284
|
+
.describe("rename/archive/unarchive/convert_to_project: the target's uuid"),
|
|
1285
|
+
title: z.string().optional().describe("create: the new heading; rename: the new title"),
|
|
1286
|
+
children: z
|
|
1287
|
+
.enum(["complete", "cancel", "reparent"])
|
|
1288
|
+
.optional()
|
|
1289
|
+
.describe("archive: required when the heading has open children"),
|
|
1290
|
+
restore_children: z
|
|
1291
|
+
.boolean()
|
|
1292
|
+
.optional()
|
|
1293
|
+
.describe("unarchive: also reopen the children archived with the heading"),
|
|
1294
|
+
...driveGuiShape,
|
|
1032
1295
|
...dryRunShape,
|
|
1033
1296
|
},
|
|
1034
|
-
annotations:
|
|
1035
|
-
}, async (args) => guard(async () =>
|
|
1297
|
+
annotations: DESTRUCTIVE,
|
|
1298
|
+
}, async (args) => guard(async () => {
|
|
1299
|
+
const c = getClient();
|
|
1300
|
+
const opts = writeOptions(args);
|
|
1301
|
+
switch (args.action) {
|
|
1302
|
+
case "create":
|
|
1303
|
+
if (args.project === undefined || args.title === undefined) {
|
|
1304
|
+
return usage('action "create" requires project and title');
|
|
1305
|
+
}
|
|
1306
|
+
return mutationResult(await c.write.createHeading(containerRef(args.project), args.title, opts));
|
|
1307
|
+
case "rename":
|
|
1308
|
+
if (args.uuid === undefined || args.title === undefined) {
|
|
1309
|
+
return usage('action "rename" requires uuid and title');
|
|
1310
|
+
}
|
|
1311
|
+
return mutationResult(await c.write.renameHeading(args.uuid, args.title, opts));
|
|
1312
|
+
case "archive": {
|
|
1313
|
+
if (args.uuid === undefined)
|
|
1314
|
+
return usage('action "archive" requires uuid');
|
|
1315
|
+
const r = await c.write.archiveHeading(args.uuid, args.children !== undefined ? { children: args.children } : {}, opts);
|
|
1316
|
+
return r.heading.kind === "ok" || r.heading.kind === "dry-run"
|
|
1317
|
+
? jsonResult(r)
|
|
1318
|
+
: mutationResult(r.heading);
|
|
1319
|
+
}
|
|
1320
|
+
case "unarchive": {
|
|
1321
|
+
if (args.uuid === undefined)
|
|
1322
|
+
return usage('action "unarchive" requires uuid');
|
|
1323
|
+
const r = await c.write.unarchiveHeading(args.uuid, args.restore_children === true ? { restoreChildren: true } : {}, opts);
|
|
1324
|
+
return r.heading.kind === "ok" || r.heading.kind === "dry-run"
|
|
1325
|
+
? jsonResult(r)
|
|
1326
|
+
: mutationResult(r.heading);
|
|
1327
|
+
}
|
|
1328
|
+
case "convert_to_project": {
|
|
1329
|
+
if (args.uuid === undefined) {
|
|
1330
|
+
return usage('action "convert_to_project" requires uuid');
|
|
1331
|
+
}
|
|
1332
|
+
const item = c.read.byUuid(args.uuid);
|
|
1333
|
+
if (item === null)
|
|
1334
|
+
throw new RangeError(`no item with uuid ${args.uuid}`);
|
|
1335
|
+
const op = item.type === "heading" ? "heading.convert-to-project" : "todo.convert-to-project";
|
|
1336
|
+
return mutationResult(await c.write.run(op, { uuid: args.uuid }, opts));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}));
|
|
1036
1340
|
server.registerTool("clear_reminder", {
|
|
1037
1341
|
description: "Clear a to-do's time-of-day reminder while keeping its scheduled date. Uses the " +
|
|
1038
1342
|
"Things proxy shortcuts when installed (in place, and the only path for a repeating " +
|
|
@@ -1041,59 +1345,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1041
1345
|
inputSchema: { uuid: z.string(), ...dryRunShape },
|
|
1042
1346
|
annotations: NON_DESTRUCTIVE,
|
|
1043
1347
|
}, async (args) => guard(async () => mutationResult(await getClient().write.clearReminder(args.uuid, writeOptions(args)))));
|
|
1044
|
-
server.registerTool("rename_heading", {
|
|
1045
|
-
description: "Rename a heading in place (works on archived headings too).",
|
|
1046
|
-
inputSchema: { uuid: z.string(), title: z.string(), ...dryRunShape },
|
|
1047
|
-
annotations: NON_DESTRUCTIVE,
|
|
1048
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.renameHeading(args.uuid, args.title, writeOptions(args)))));
|
|
1049
|
-
server.registerTool("archive_heading", {
|
|
1050
|
-
description: "Archive a heading — it leaves the active project view (reversible with " +
|
|
1051
|
-
"unarchive_heading). The preferred way to retire a heading: row deletion only " +
|
|
1052
|
-
"exists in the app's UI / Shortcuts behind a per-run consent dialog. With open " +
|
|
1053
|
-
"children the children policy is required: complete or cancel resolve them with " +
|
|
1054
|
-
"the heading in one cascade; reparent moves them to the project root first, " +
|
|
1055
|
-
"keeping them open (a compound sequence that undo reverses as one unit).",
|
|
1056
|
-
inputSchema: {
|
|
1057
|
-
uuid: z.string(),
|
|
1058
|
-
children: z
|
|
1059
|
-
.enum(["complete", "cancel", "reparent"])
|
|
1060
|
-
.optional()
|
|
1061
|
-
.describe("Required when the heading has open children"),
|
|
1062
|
-
...dryRunShape,
|
|
1063
|
-
},
|
|
1064
|
-
annotations: NON_DESTRUCTIVE,
|
|
1065
|
-
}, async (args) => guard(async () => {
|
|
1066
|
-
const r = await getClient().write.archiveHeading(args.uuid, args.children !== undefined ? { children: args.children } : {}, writeOptions(args));
|
|
1067
|
-
return r.heading.kind === "ok" || r.heading.kind === "dry-run"
|
|
1068
|
-
? jsonResult(r)
|
|
1069
|
-
: mutationResult(r.heading);
|
|
1070
|
-
}));
|
|
1071
|
-
server.registerTool("unarchive_heading", {
|
|
1072
|
-
description: "Un-archive a heading. restore_children also reopens the children the archive " +
|
|
1073
|
-
"cascade resolved with it (matching resolution timestamps; someday state " +
|
|
1074
|
-
"survives). Children resolved at other times are never touched.",
|
|
1075
|
-
inputSchema: {
|
|
1076
|
-
uuid: z.string(),
|
|
1077
|
-
restore_children: z.boolean().optional(),
|
|
1078
|
-
...dryRunShape,
|
|
1079
|
-
},
|
|
1080
|
-
annotations: NON_DESTRUCTIVE,
|
|
1081
|
-
}, async (args) => guard(async () => {
|
|
1082
|
-
const r = await getClient().write.unarchiveHeading(args.uuid, args.restore_children === true ? { restoreChildren: true } : {}, writeOptions(args));
|
|
1083
|
-
return r.heading.kind === "ok" || r.heading.kind === "dry-run"
|
|
1084
|
-
? jsonResult(r)
|
|
1085
|
-
: mutationResult(r.heading);
|
|
1086
|
-
}));
|
|
1087
1348
|
// -------------------------------------------- GUI-driven (Accessibility)
|
|
1088
|
-
const driveGuiShape = {
|
|
1089
|
-
dangerously_drive_gui: z
|
|
1090
|
-
.boolean()
|
|
1091
|
-
.optional()
|
|
1092
|
-
.describe("Required: this drives the local Things app through its accessibility interface to " +
|
|
1093
|
-
"make a change the app offers nowhere else. It briefly interacts with the app's UI " +
|
|
1094
|
-
"on the machine running this server, and must be turned on first with `things config " +
|
|
1095
|
-
"set ui-enabled true`. Intended for a dedicated always-on Mac."),
|
|
1096
|
-
};
|
|
1097
1349
|
const WEEKDAY_ENUM = z.enum([
|
|
1098
1350
|
"sunday",
|
|
1099
1351
|
"monday",
|
|
@@ -1171,177 +1423,112 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1171
1423
|
fields.startDaysEarlier = a.start_days_earlier;
|
|
1172
1424
|
return fields;
|
|
1173
1425
|
};
|
|
1174
|
-
server.registerTool("
|
|
1175
|
-
description: "
|
|
1176
|
-
"
|
|
1177
|
-
"
|
|
1178
|
-
"monthly/yearly day, end bound,
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
uuid: args.uuid,
|
|
1188
|
-
frequency: args.frequency,
|
|
1189
|
-
interval: args.interval,
|
|
1190
|
-
...repeatExtras(args, args.frequency),
|
|
1191
|
-
}, writeOptions(args)))));
|
|
1192
|
-
server.registerTool("reschedule_repeat", {
|
|
1193
|
-
description: "Change a repeating to-do's rule in place, keeping the same item. Set the frequency and " +
|
|
1194
|
-
"interval, and optionally the weekday set, monthly/yearly day, end bound, reminders, or " +
|
|
1195
|
-
"deadline. This can be undone — it restores the previous rule.",
|
|
1196
|
-
inputSchema: {
|
|
1197
|
-
uuid: z.string().describe("The repeating to-do to reschedule"),
|
|
1198
|
-
...repeatRuleShape,
|
|
1199
|
-
...driveGuiShape,
|
|
1200
|
-
...dryRunShape,
|
|
1201
|
-
},
|
|
1202
|
-
annotations: NON_DESTRUCTIVE,
|
|
1203
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.run("todo.reschedule-repeat", {
|
|
1204
|
-
uuid: args.uuid,
|
|
1205
|
-
frequency: args.frequency,
|
|
1206
|
-
interval: args.interval,
|
|
1207
|
-
...repeatExtras(args, args.frequency),
|
|
1208
|
-
}, writeOptions(args)))));
|
|
1209
|
-
server.registerTool("set_repeat_state", {
|
|
1210
|
-
description: "Pause or resume a repeating to-do. 'pause' stops it spawning new occurrences but keeps " +
|
|
1211
|
-
"its rule; 'resume' starts it again. The two are inverses of each other.",
|
|
1212
|
-
inputSchema: {
|
|
1213
|
-
uuid: z.string().describe("The repeating to-do"),
|
|
1214
|
-
state: z.enum(["pause", "resume"]),
|
|
1215
|
-
...driveGuiShape,
|
|
1216
|
-
...dryRunShape,
|
|
1217
|
-
},
|
|
1218
|
-
annotations: NON_DESTRUCTIVE,
|
|
1219
|
-
}, async (args) => guard(async () => {
|
|
1220
|
-
const op = args.state === "pause" ? "todo.pause-repeat" : "todo.resume-repeat";
|
|
1221
|
-
return mutationResult(await getClient().write.run(op, { uuid: args.uuid }, writeOptions(args)));
|
|
1222
|
-
}));
|
|
1223
|
-
server.registerTool("reschedule_project_repeat", {
|
|
1224
|
-
description: "Change a repeating project's rule in place, keeping the same project. Set the frequency " +
|
|
1225
|
-
"and interval, and optionally the weekday set, monthly/yearly day, end bound, reminders, " +
|
|
1226
|
-
"or deadline. This can be undone — it restores the previous rule.",
|
|
1227
|
-
inputSchema: {
|
|
1228
|
-
uuid: z.string().describe(`The repeating project to reschedule (${REF_FORMAT})`),
|
|
1229
|
-
...repeatRuleShape,
|
|
1230
|
-
...driveGuiShape,
|
|
1231
|
-
...dryRunShape,
|
|
1232
|
-
},
|
|
1233
|
-
annotations: NON_DESTRUCTIVE,
|
|
1234
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.run("project.reschedule-repeat", {
|
|
1235
|
-
uuid: args.uuid,
|
|
1236
|
-
frequency: args.frequency,
|
|
1237
|
-
interval: args.interval,
|
|
1238
|
-
...repeatExtras(args, args.frequency),
|
|
1239
|
-
}, writeOptions(args)))));
|
|
1240
|
-
server.registerTool("set_project_repeat_state", {
|
|
1241
|
-
description: "Pause or resume a repeating project. 'pause' stops it spawning new occurrences but keeps " +
|
|
1242
|
-
"its rule; 'resume' starts it again. The two are inverses of each other.",
|
|
1243
|
-
inputSchema: {
|
|
1244
|
-
uuid: z.string().describe(`The repeating project (${REF_FORMAT})`),
|
|
1245
|
-
state: z.enum(["pause", "resume"]),
|
|
1246
|
-
...driveGuiShape,
|
|
1247
|
-
...dryRunShape,
|
|
1248
|
-
},
|
|
1249
|
-
annotations: NON_DESTRUCTIVE,
|
|
1250
|
-
}, async (args) => guard(async () => {
|
|
1251
|
-
const op = args.state === "pause" ? "project.pause-repeat" : "project.resume-repeat";
|
|
1252
|
-
return mutationResult(await getClient().write.run(op, { uuid: args.uuid }, writeOptions(args)));
|
|
1253
|
-
}));
|
|
1254
|
-
server.registerTool("reorder_area", {
|
|
1255
|
-
description: "Move an area to a new position in the area order. Give the area plus exactly one " +
|
|
1256
|
-
"destination: before/after another area, or position first/last. The move is made by " +
|
|
1257
|
-
"driving the Things window with the pointer — the app comes to the front and the " +
|
|
1258
|
-
"sidebar may scroll while the area is dragged; the area's projects and to-dos are " +
|
|
1259
|
-
"untouched. Area references are a uuid or a unique name.",
|
|
1426
|
+
server.registerTool("repeat", {
|
|
1427
|
+
description: "Manage recurrence on a to-do or project (scope) by driving the local Things app's " +
|
|
1428
|
+
"interface — every action needs dangerously_drive_gui. action start: turn a plain item " +
|
|
1429
|
+
"into a repeating one — this REPLACES it with a new series and cannot be undone (give " +
|
|
1430
|
+
"frequency + interval and optionally the weekday set, monthly/yearly day, end bound, " +
|
|
1431
|
+
"reminder, or per-occurrence deadline); returns a repeating block with instanceUuid (the " +
|
|
1432
|
+
"visible occurrence), templateUuid (the rule), and replacedUuid. action reschedule: " +
|
|
1433
|
+
"change a repeating item's rule in place, keeping the same item (undoable — it restores " +
|
|
1434
|
+
"the previous rule). action pause/resume: stop or restart its new occurrences, keeping " +
|
|
1435
|
+
"the rule. action create (scope project only): create a project and make it repeating in " +
|
|
1436
|
+
"one call — the project is created first and PERSISTS even if the make-repeating step " +
|
|
1437
|
+
"refuses; give an area to place it or omit it to create in Someday (only frequency and " +
|
|
1438
|
+
"interval are supported); returns the new project's uuid.",
|
|
1260
1439
|
inputSchema: {
|
|
1261
|
-
|
|
1262
|
-
|
|
1440
|
+
scope: z.enum(["todo", "project"]),
|
|
1441
|
+
action: z.enum(["start", "reschedule", "pause", "resume", "create"]),
|
|
1442
|
+
uuid: z
|
|
1263
1443
|
.string()
|
|
1264
1444
|
.optional()
|
|
1265
|
-
.describe("
|
|
1266
|
-
|
|
1445
|
+
.describe("start/reschedule/pause/resume: the item (a project also accepts a unique name)"),
|
|
1446
|
+
title: z.string().optional().describe("create (project): the new project's title"),
|
|
1447
|
+
notes: z.string().optional().describe("create (project): notes"),
|
|
1448
|
+
area: z.string().optional().describe(`create (project): destination area (${REF_FORMAT})`),
|
|
1449
|
+
project_deadline: z
|
|
1267
1450
|
.string()
|
|
1268
1451
|
.optional()
|
|
1269
|
-
.describe(
|
|
1270
|
-
|
|
1271
|
-
.
|
|
1452
|
+
.describe(`create (project): the project's due date — ${DATE_FORMAT}`),
|
|
1453
|
+
todos: z
|
|
1454
|
+
.array(z.string())
|
|
1272
1455
|
.optional()
|
|
1273
|
-
.describe("
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
},
|
|
1297
|
-
annotations: DESTRUCTIVE,
|
|
1298
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.makeRepeatingProject(args.uuid, {
|
|
1299
|
-
frequency: args.frequency,
|
|
1300
|
-
interval: args.interval,
|
|
1301
|
-
...repeatExtras(args, args.frequency),
|
|
1302
|
-
}, writeOptions(args)))));
|
|
1303
|
-
server.registerTool("create_repeating_project", {
|
|
1304
|
-
description: "Create a project and make it repeating in one call. TWO operations: the project is " +
|
|
1305
|
-
"created first and PERSISTS even if the make-repeating step refuses; then it is promoted " +
|
|
1306
|
-
"(which drives the GUI). Give an area to place it, or omit it to create in Someday. Only a " +
|
|
1307
|
-
"frequency and an interval are supported. Returns the new repeating project's uuid.",
|
|
1308
|
-
inputSchema: {
|
|
1309
|
-
title: z.string(),
|
|
1310
|
-
notes: z.string().optional(),
|
|
1311
|
-
area: z.string().optional().describe(`Destination area (${REF_FORMAT})`),
|
|
1312
|
-
deadline: z.string().optional().describe(DATE_FORMAT),
|
|
1313
|
-
todos: z.array(z.string()).optional().describe("Initial child to-do titles"),
|
|
1314
|
-
...baseRepeatShape,
|
|
1315
|
-
...driveGuiShape,
|
|
1316
|
-
...dryRunShape,
|
|
1317
|
-
},
|
|
1318
|
-
annotations: DESTRUCTIVE,
|
|
1319
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.createRepeatingProject({
|
|
1320
|
-
title: args.title,
|
|
1321
|
-
...(args.notes !== undefined && { notes: args.notes }),
|
|
1322
|
-
...(args.area !== undefined && { area: containerRef(args.area) }),
|
|
1323
|
-
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1324
|
-
...(args.todos !== undefined && { todos: args.todos }),
|
|
1325
|
-
frequency: args.frequency,
|
|
1326
|
-
interval: args.interval,
|
|
1327
|
-
}, writeOptions(args)))));
|
|
1328
|
-
server.registerTool("convert_to_project", {
|
|
1329
|
-
description: "Convert a to-do or a heading into a project. This REPLACES the original with a new " +
|
|
1330
|
-
"project (a converted to-do keeps its notes; a converted heading is promoted alongside " +
|
|
1331
|
-
"its project and its to-dos move under the new project). The original is gone and this " +
|
|
1332
|
-
"cannot be undone. Returns the new project's uuid.",
|
|
1333
|
-
inputSchema: {
|
|
1334
|
-
uuid: z.string().describe("The to-do or heading to convert"),
|
|
1456
|
+
.describe("create (project): initial child to-do titles"),
|
|
1457
|
+
frequency: z
|
|
1458
|
+
.enum(["daily", "weekly", "monthly", "yearly"])
|
|
1459
|
+
.optional()
|
|
1460
|
+
.describe("start/reschedule/create: how often it repeats"),
|
|
1461
|
+
interval: z
|
|
1462
|
+
.number()
|
|
1463
|
+
.int()
|
|
1464
|
+
.min(1)
|
|
1465
|
+
.max(99)
|
|
1466
|
+
.optional()
|
|
1467
|
+
.describe("start/reschedule/create: every N units (1–99)"),
|
|
1468
|
+
after_completion: repeatRuleShape.after_completion,
|
|
1469
|
+
weekdays: repeatRuleShape.weekdays,
|
|
1470
|
+
monthly_day: repeatRuleShape.monthly_day,
|
|
1471
|
+
monthly_weekday: repeatRuleShape.monthly_weekday,
|
|
1472
|
+
monthly_ordinal: repeatRuleShape.monthly_ordinal,
|
|
1473
|
+
yearly_month: repeatRuleShape.yearly_month,
|
|
1474
|
+
ends_after: repeatRuleShape.ends_after,
|
|
1475
|
+
ends_on: repeatRuleShape.ends_on,
|
|
1476
|
+
reminder: repeatRuleShape.reminder,
|
|
1477
|
+
deadline: repeatRuleShape.deadline,
|
|
1478
|
+
start_days_earlier: repeatRuleShape.start_days_earlier,
|
|
1335
1479
|
...driveGuiShape,
|
|
1336
1480
|
...dryRunShape,
|
|
1337
1481
|
},
|
|
1338
1482
|
annotations: DESTRUCTIVE,
|
|
1339
1483
|
}, async (args) => guard(async () => {
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1484
|
+
const c = getClient();
|
|
1485
|
+
const opts = writeOptions(args);
|
|
1486
|
+
const { frequency, interval } = args;
|
|
1487
|
+
if (args.action === "create") {
|
|
1488
|
+
if (args.scope !== "project")
|
|
1489
|
+
return usage('action "create" requires scope "project"');
|
|
1490
|
+
if (args.title === undefined)
|
|
1491
|
+
return usage('action "create" requires title');
|
|
1492
|
+
if (frequency === undefined || interval === undefined) {
|
|
1493
|
+
return usage('action "create" requires frequency and interval');
|
|
1494
|
+
}
|
|
1495
|
+
return mutationResult(await c.write.createRepeatingProject({
|
|
1496
|
+
title: args.title,
|
|
1497
|
+
...(args.notes !== undefined && { notes: args.notes }),
|
|
1498
|
+
...(args.area !== undefined && { area: containerRef(args.area) }),
|
|
1499
|
+
...(args.project_deadline !== undefined && { deadline: args.project_deadline }),
|
|
1500
|
+
...(args.todos !== undefined && { todos: args.todos }),
|
|
1501
|
+
frequency,
|
|
1502
|
+
interval,
|
|
1503
|
+
}, opts));
|
|
1504
|
+
}
|
|
1505
|
+
if (args.uuid === undefined)
|
|
1506
|
+
return usage(`action "${args.action}" requires uuid`);
|
|
1507
|
+
if (args.action === "pause" || args.action === "resume") {
|
|
1508
|
+
const op = args.scope === "todo"
|
|
1509
|
+
? args.action === "pause"
|
|
1510
|
+
? "todo.pause-repeat"
|
|
1511
|
+
: "todo.resume-repeat"
|
|
1512
|
+
: args.action === "pause"
|
|
1513
|
+
? "project.pause-repeat"
|
|
1514
|
+
: "project.resume-repeat";
|
|
1515
|
+
return mutationResult(await c.write.run(op, { uuid: args.uuid }, opts));
|
|
1516
|
+
}
|
|
1517
|
+
// start | reschedule
|
|
1518
|
+
if (frequency === undefined || interval === undefined) {
|
|
1519
|
+
return usage(`action "${args.action}" requires frequency and interval`);
|
|
1520
|
+
}
|
|
1521
|
+
const extras = repeatExtras(args, frequency);
|
|
1522
|
+
if (args.scope === "todo") {
|
|
1523
|
+
const op = args.action === "start" ? "todo.make-repeating" : "todo.reschedule-repeat";
|
|
1524
|
+
return mutationResult(await c.write.run(op, { uuid: args.uuid, frequency, interval, ...extras }, opts));
|
|
1525
|
+
}
|
|
1526
|
+
// scope project
|
|
1527
|
+
if (args.action === "reschedule") {
|
|
1528
|
+
return mutationResult(await c.write.run("project.reschedule-repeat", { uuid: args.uuid, frequency, interval, ...extras }, opts));
|
|
1529
|
+
}
|
|
1530
|
+
// scope project, action start → the dedicated make-repeating method
|
|
1531
|
+
return mutationResult(await c.write.makeRepeatingProject(args.uuid, { frequency, interval, ...extras }, opts));
|
|
1345
1532
|
}));
|
|
1346
1533
|
server.registerTool("duplicate_item", {
|
|
1347
1534
|
description: "Duplicate a to-do or project and return the copy's uuid; a duplicated project " +
|
|
@@ -1365,109 +1552,23 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1365
1552
|
when: whenSchema,
|
|
1366
1553
|
deadline: z.string().optional().describe(DATE_FORMAT),
|
|
1367
1554
|
todos: z.array(z.string()).optional().describe("Initial child to-do titles"),
|
|
1368
|
-
...
|
|
1369
|
-
},
|
|
1370
|
-
annotations: NON_DESTRUCTIVE,
|
|
1371
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.addProject({
|
|
1372
|
-
title: args.title,
|
|
1373
|
-
...(args.notes !== undefined && { notes: args.notes }),
|
|
1374
|
-
...(args.area !== undefined && { area: containerRef(args.area) }),
|
|
1375
|
-
...(args.when !== undefined && { when: args.when }),
|
|
1376
|
-
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1377
|
-
...(args.todos !== undefined && { todos: args.todos }),
|
|
1378
|
-
}, writeOptions(args)))));
|
|
1379
|
-
server.registerTool("update_project", {
|
|
1380
|
-
description: "Update a project's title, notes, schedule, reminder, or deadline. " +
|
|
1381
|
-
"append_notes/prepend_notes add a line to the existing notes (exclusive with notes). " +
|
|
1382
|
-
"Changing the schedule keeps an existing reminder unless the call sets a new one. " +
|
|
1383
|
-
"clear_reminder works while the project is scheduled for today or this evening; a " +
|
|
1384
|
-
"reminder on a future date can only be changed, not cleared.",
|
|
1385
|
-
inputSchema: {
|
|
1386
|
-
uuid: z.string().describe(`The project to update (${REF_FORMAT})`),
|
|
1387
|
-
title: z.string().optional(),
|
|
1388
|
-
notes: z.string().optional().describe("Replaces the whole notes body"),
|
|
1389
|
-
append_notes: z.string().optional(),
|
|
1390
|
-
prepend_notes: z.string().optional(),
|
|
1391
|
-
when: whenSchema,
|
|
1392
|
-
reminder: z.string().optional().describe(REMINDER_FORMAT),
|
|
1393
|
-
clear_reminder: z.boolean().optional(),
|
|
1394
|
-
deadline: z.string().optional().describe(DATE_FORMAT),
|
|
1395
|
-
clear_deadline: z.boolean().optional(),
|
|
1555
|
+
...tzShape,
|
|
1396
1556
|
...dryRunShape,
|
|
1397
1557
|
},
|
|
1398
1558
|
annotations: NON_DESTRUCTIVE,
|
|
1399
1559
|
}, async (args) => guard(async () => {
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1402
|
-
return
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
return usage("pass at most one of reminder / clear_reminder");
|
|
1406
|
-
}
|
|
1407
|
-
if (args.deadline !== undefined && args.clear_deadline === true) {
|
|
1408
|
-
return usage("pass at most one of deadline / clear_deadline");
|
|
1409
|
-
}
|
|
1410
|
-
return mutationResult(await getClient().write.updateProject(args.uuid, {
|
|
1411
|
-
...(args.title !== undefined && { title: args.title }),
|
|
1560
|
+
const badZone = badTz(args.tz);
|
|
1561
|
+
if (badZone !== null)
|
|
1562
|
+
return badZone;
|
|
1563
|
+
return mutationResult(await getClient().write.addProject({
|
|
1564
|
+
title: args.title,
|
|
1412
1565
|
...(args.notes !== undefined && { notes: args.notes }),
|
|
1413
|
-
...(args.
|
|
1414
|
-
...(args.prepend_notes !== undefined && { prependNotes: args.prepend_notes }),
|
|
1566
|
+
...(args.area !== undefined && { area: containerRef(args.area) }),
|
|
1415
1567
|
...(args.when !== undefined && { when: args.when }),
|
|
1416
|
-
...(args.reminder !== undefined && { reminder: args.reminder }),
|
|
1417
|
-
...(args.clear_reminder === true && { reminder: null }),
|
|
1418
1568
|
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1419
|
-
...(args.
|
|
1569
|
+
...(args.todos !== undefined && { todos: args.todos }),
|
|
1420
1570
|
}, writeOptions(args)));
|
|
1421
1571
|
}));
|
|
1422
|
-
server.registerTool("set_project_status", {
|
|
1423
|
-
description: "Complete, cancel, or reopen a project. Completing or canceling requires a " +
|
|
1424
|
-
"children policy: 'require-resolved' errors if open to-dos remain; " +
|
|
1425
|
-
"'auto-complete'/'auto-cancel' resolves them together with the project (canceling " +
|
|
1426
|
-
"never alters already-completed children). status 'open' reopens a completed or " +
|
|
1427
|
-
"canceled project; its children stay completed/canceled unless restore_children " +
|
|
1428
|
-
"also reopens the ones that were resolved together with the project.",
|
|
1429
|
-
inputSchema: {
|
|
1430
|
-
uuid: z.string().describe(`The project (${REF_FORMAT})`),
|
|
1431
|
-
status: z.enum(["completed", "canceled", "open"]),
|
|
1432
|
-
children: z
|
|
1433
|
-
.enum(["require-resolved", "auto-complete", "auto-cancel"])
|
|
1434
|
-
.optional()
|
|
1435
|
-
.describe("Required for completed/canceled: what to do with the project's open to-dos"),
|
|
1436
|
-
restore_children: z
|
|
1437
|
-
.boolean()
|
|
1438
|
-
.optional()
|
|
1439
|
-
.describe("open only: also reopen the to-dos that were resolved with the project"),
|
|
1440
|
-
...dryRunShape,
|
|
1441
|
-
},
|
|
1442
|
-
annotations: NON_DESTRUCTIVE,
|
|
1443
|
-
}, async (args) => guard(async () => {
|
|
1444
|
-
const c = getClient();
|
|
1445
|
-
if (args.status !== "open" && args.restore_children !== undefined) {
|
|
1446
|
-
return usage("restore_children applies only to status 'open'");
|
|
1447
|
-
}
|
|
1448
|
-
if (args.status === "completed") {
|
|
1449
|
-
if (args.children !== "require-resolved" && args.children !== "auto-complete") {
|
|
1450
|
-
return usage("status 'completed' requires children: 'require-resolved' or 'auto-complete'");
|
|
1451
|
-
}
|
|
1452
|
-
return mutationResult(await c.write.completeProject(args.uuid, { children: args.children }, writeOptions(args)));
|
|
1453
|
-
}
|
|
1454
|
-
if (args.status === "canceled") {
|
|
1455
|
-
if (args.children !== "require-resolved" && args.children !== "auto-cancel") {
|
|
1456
|
-
return usage("status 'canceled' requires children: 'require-resolved' or 'auto-cancel'");
|
|
1457
|
-
}
|
|
1458
|
-
return mutationResult(await c.write.cancelProject(args.uuid, { children: args.children }, writeOptions(args)));
|
|
1459
|
-
}
|
|
1460
|
-
if (args.children !== undefined) {
|
|
1461
|
-
return usage("children applies only to status 'completed' or 'canceled'");
|
|
1462
|
-
}
|
|
1463
|
-
const outcome = await c.write.reopenProject(args.uuid, {
|
|
1464
|
-
...writeOptions(args),
|
|
1465
|
-
...(args.restore_children === true && { restoreChildren: true }),
|
|
1466
|
-
});
|
|
1467
|
-
return outcome.project.kind === "ok" || outcome.project.kind === "dry-run"
|
|
1468
|
-
? jsonResult(outcome)
|
|
1469
|
-
: mutationResult(outcome.project);
|
|
1470
|
-
}));
|
|
1471
1572
|
server.registerTool("move_project", {
|
|
1472
1573
|
description: "Move a project into an area, or detach it from its current area. Pass exactly " +
|
|
1473
1574
|
"one of area / detach. The project's status and schedule are unaffected.",
|
|
@@ -1498,43 +1599,6 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1498
1599
|
},
|
|
1499
1600
|
annotations: NON_DESTRUCTIVE,
|
|
1500
1601
|
}, async (args) => guard(async () => mutationResult(await getClient().write.addArea({ title: args.title, ...(args.tags !== undefined && { tags: args.tags }) }, writeOptions(args)))));
|
|
1501
|
-
server.registerTool("update_area", {
|
|
1502
|
-
description: "Rename an area and/or replace its tags (the full set). Tags must exist unless " +
|
|
1503
|
-
"create_tags is set.",
|
|
1504
|
-
inputSchema: {
|
|
1505
|
-
target: z.string().describe(`Area to update (${REF_FORMAT})`),
|
|
1506
|
-
title: z.string().optional().describe("New name"),
|
|
1507
|
-
tags: z
|
|
1508
|
-
.array(z.string())
|
|
1509
|
-
.optional()
|
|
1510
|
-
.describe(`Tags (full replacement) — ${TAG_REF_FORMAT}`),
|
|
1511
|
-
...createTagsShape,
|
|
1512
|
-
...dryRunShape,
|
|
1513
|
-
},
|
|
1514
|
-
annotations: NON_DESTRUCTIVE,
|
|
1515
|
-
}, async (args) => guard(async () => {
|
|
1516
|
-
if (args.title === undefined && args.tags === undefined) {
|
|
1517
|
-
return usage("pass title and/or tags");
|
|
1518
|
-
}
|
|
1519
|
-
return mutationResult(await getClient().write.updateArea(args.target, {
|
|
1520
|
-
...(args.title !== undefined && { title: args.title }),
|
|
1521
|
-
...(args.tags !== undefined && { tags: args.tags }),
|
|
1522
|
-
}, writeOptions(args)));
|
|
1523
|
-
}));
|
|
1524
|
-
server.registerTool("delete_area", {
|
|
1525
|
-
description: "Delete an area PERMANENTLY — areas do not go to the Trash, so this cannot be " +
|
|
1526
|
-
"undone; requires dangerously_permanent. The area's to-dos move to the Trash; its " +
|
|
1527
|
-
"projects remain, no longer assigned to any area.",
|
|
1528
|
-
inputSchema: {
|
|
1529
|
-
target: z.string().describe(`Area to delete (${REF_FORMAT})`),
|
|
1530
|
-
dangerously_permanent: z
|
|
1531
|
-
.boolean()
|
|
1532
|
-
.optional()
|
|
1533
|
-
.describe("Confirm permanent, unrecoverable deletion"),
|
|
1534
|
-
...dryRunShape,
|
|
1535
|
-
},
|
|
1536
|
-
annotations: DESTRUCTIVE,
|
|
1537
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.deleteArea(args.target, writeOptions(args)))));
|
|
1538
1602
|
// ------------------------------------------------------------------ tags
|
|
1539
1603
|
server.registerTool("add_tag", {
|
|
1540
1604
|
description: "Create a tag, optionally nested under an existing parent tag.",
|
|
@@ -1545,61 +1609,6 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1545
1609
|
},
|
|
1546
1610
|
annotations: NON_DESTRUCTIVE,
|
|
1547
1611
|
}, async (args) => guard(async () => mutationResult(await getClient().write.addTag({ title: args.title, ...(args.parent !== undefined && { parent: args.parent }) }, writeOptions(args)))));
|
|
1548
|
-
server.registerTool("update_tag", {
|
|
1549
|
-
description: "Rename a tag (existing assignments follow the rename), nest it under another " +
|
|
1550
|
-
"existing tag, un-nest it to the top level, and set or clear its keyboard shortcut. " +
|
|
1551
|
-
"parent and unnest are exclusive; shortcut and clear_shortcut are exclusive.",
|
|
1552
|
-
inputSchema: {
|
|
1553
|
-
target: z.string().describe(`Tag to update (${REF_FORMAT})`),
|
|
1554
|
-
title: z.string().optional().describe("New name"),
|
|
1555
|
-
parent: z.string().optional().describe("Existing tag to nest under"),
|
|
1556
|
-
unnest: z.boolean().optional().describe("Move the tag to the top level"),
|
|
1557
|
-
shortcut: z.string().optional().describe("Keyboard shortcut character"),
|
|
1558
|
-
clear_shortcut: z.boolean().optional().describe("Remove the keyboard shortcut"),
|
|
1559
|
-
...dryRunShape,
|
|
1560
|
-
},
|
|
1561
|
-
annotations: NON_DESTRUCTIVE,
|
|
1562
|
-
}, async (args) => guard(async () => {
|
|
1563
|
-
if (args.title === undefined &&
|
|
1564
|
-
args.parent === undefined &&
|
|
1565
|
-
args.unnest === undefined &&
|
|
1566
|
-
args.shortcut === undefined &&
|
|
1567
|
-
args.clear_shortcut === undefined) {
|
|
1568
|
-
return usage("pass title, parent, unnest, shortcut, and/or clear_shortcut");
|
|
1569
|
-
}
|
|
1570
|
-
if (args.parent !== undefined && args.unnest === true) {
|
|
1571
|
-
return usage("parent and unnest are exclusive");
|
|
1572
|
-
}
|
|
1573
|
-
if (args.shortcut !== undefined && args.clear_shortcut === true) {
|
|
1574
|
-
return usage("shortcut and clear_shortcut are exclusive");
|
|
1575
|
-
}
|
|
1576
|
-
return mutationResult(await getClient().write.updateTag(args.target, {
|
|
1577
|
-
...(args.title !== undefined && { title: args.title }),
|
|
1578
|
-
...(args.parent !== undefined && { parent: args.parent }),
|
|
1579
|
-
...(args.unnest === true && { unnest: true }),
|
|
1580
|
-
...(args.shortcut !== undefined && { shortcut: args.shortcut }),
|
|
1581
|
-
...(args.clear_shortcut === true && { clearShortcut: true }),
|
|
1582
|
-
}, writeOptions(args)));
|
|
1583
|
-
}));
|
|
1584
|
-
server.registerTool("delete_tag", {
|
|
1585
|
-
description: "Delete a tag PERMANENTLY — tags do not go to the Trash, so this cannot be undone; " +
|
|
1586
|
-
"requires dangerously_permanent. The tag is removed from every item. If the tag " +
|
|
1587
|
-
"has nested child tags they are ALL permanently deleted with it — pass " +
|
|
1588
|
-
"acknowledge_tag_subtree to confirm.",
|
|
1589
|
-
inputSchema: {
|
|
1590
|
-
target: z.string().describe(`Tag to delete (${REF_FORMAT})`),
|
|
1591
|
-
dangerously_permanent: z
|
|
1592
|
-
.boolean()
|
|
1593
|
-
.optional()
|
|
1594
|
-
.describe("Confirm permanent, unrecoverable deletion"),
|
|
1595
|
-
acknowledge_tag_subtree: z
|
|
1596
|
-
.boolean()
|
|
1597
|
-
.optional()
|
|
1598
|
-
.describe("Confirm permanent deletion of ALL nested child tags too"),
|
|
1599
|
-
...dryRunShape,
|
|
1600
|
-
},
|
|
1601
|
-
annotations: DESTRUCTIVE,
|
|
1602
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.deleteTag(args.target, writeOptions(args)))));
|
|
1603
1612
|
// -------------------------------------------------- generic + discovery
|
|
1604
1613
|
server.registerTool("run_operation", {
|
|
1605
1614
|
description: "Run any cataloged operation by kind — the generic entry for operations without a " +
|
|
@@ -1684,7 +1693,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1684
1693
|
"Today/inbox/someday/project/headings/area ordering must first be enabled once " +
|
|
1685
1694
|
"via `things config set allow-experimental true`. This Evening and " +
|
|
1686
1695
|
`scope=projects handle at most ${BOUNCE_MAX_ITEMS} items per call. An area's ` +
|
|
1687
|
-
"to-dos and projects are ordered separately — one kind per call."
|
|
1696
|
+
"to-dos and projects are ordered separately — one kind per call. " +
|
|
1697
|
+
"scope=areas instead moves the sidebar areas themselves: give target plus exactly " +
|
|
1698
|
+
"one of before/after/position, and pass dangerously_drive_gui (it drives the local " +
|
|
1699
|
+
"Things app). Every other scope takes uuids.",
|
|
1688
1700
|
inputSchema: {
|
|
1689
1701
|
scope: z.enum([
|
|
1690
1702
|
"today",
|
|
@@ -1695,22 +1707,54 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1695
1707
|
"headings",
|
|
1696
1708
|
"area",
|
|
1697
1709
|
"projects",
|
|
1710
|
+
"areas",
|
|
1698
1711
|
]),
|
|
1699
1712
|
container: z
|
|
1700
1713
|
.string()
|
|
1701
1714
|
.optional()
|
|
1702
1715
|
.describe(`Project/area (${REF_FORMAT}) — required for those scopes`),
|
|
1703
|
-
uuids: z
|
|
1716
|
+
uuids: z
|
|
1717
|
+
.array(z.string())
|
|
1718
|
+
.optional()
|
|
1719
|
+
.describe("Desired order, top first (may be a subset) — required for every scope but areas"),
|
|
1704
1720
|
strategy: z.enum(["native", "bounce"]).optional(),
|
|
1721
|
+
target: z.string().optional().describe(`scope areas: the area to move (${REF_FORMAT})`),
|
|
1722
|
+
before: z
|
|
1723
|
+
.string()
|
|
1724
|
+
.optional()
|
|
1725
|
+
.describe(`scope areas: place it immediately above this area (${REF_FORMAT})`),
|
|
1726
|
+
after: z
|
|
1727
|
+
.string()
|
|
1728
|
+
.optional()
|
|
1729
|
+
.describe(`scope areas: place it immediately below this area (${REF_FORMAT})`),
|
|
1730
|
+
position: z
|
|
1731
|
+
.enum(["first", "last"])
|
|
1732
|
+
.optional()
|
|
1733
|
+
.describe("scope areas: move it to the top or bottom of the area list"),
|
|
1734
|
+
...driveGuiShape,
|
|
1705
1735
|
...dryRunShape,
|
|
1706
1736
|
},
|
|
1707
1737
|
annotations: NON_DESTRUCTIVE,
|
|
1708
|
-
}, async (args) => guard(async () =>
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1738
|
+
}, async (args) => guard(async () => {
|
|
1739
|
+
if (args.scope === "areas") {
|
|
1740
|
+
if (args.target === undefined)
|
|
1741
|
+
return usage('scope "areas" requires target');
|
|
1742
|
+
return mutationResult(await getClient().write.run("area.reorder", {
|
|
1743
|
+
target: args.target,
|
|
1744
|
+
...(args.before !== undefined && { before: args.before }),
|
|
1745
|
+
...(args.after !== undefined && { after: args.after }),
|
|
1746
|
+
...(args.position !== undefined && { position: args.position }),
|
|
1747
|
+
}, writeOptions(args)));
|
|
1748
|
+
}
|
|
1749
|
+
if (args.uuids === undefined)
|
|
1750
|
+
return usage(`scope "${args.scope}" requires uuids`);
|
|
1751
|
+
return mutationResult(await getClient().write.reorder({
|
|
1752
|
+
scope: args.scope,
|
|
1753
|
+
uuids: args.uuids,
|
|
1754
|
+
...(args.container !== undefined && { container: containerRef(args.container) }),
|
|
1755
|
+
...(args.strategy !== undefined && { strategy: args.strategy }),
|
|
1756
|
+
}, writeOptions(args)));
|
|
1757
|
+
}));
|
|
1714
1758
|
server.registerTool("undo", {
|
|
1715
1759
|
description: "Undo the last N changes, newest first (changes made directly in the Things app " +
|
|
1716
1760
|
"cannot be undone here). By default this undoes only changes made through THIS " +
|