things-api 0.18.0 → 0.19.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 +16 -9
- package/deputy/AppIcon.icns +0 -0
- package/deputy/VERSION +1 -1
- package/deputy/helpers-Info.plist +4 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +3 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
- package/deputy/reader/Info.plist +2 -0
- package/deputy/reader/entitlements.plist +30 -0
- package/deputy/reader/main.swift +129 -40
- package/deputy/src/server.swift +25 -2
- package/deputy/src/tcc.swift +58 -0
- package/dist/audit/schema.d.ts +13 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +160 -0
- package/dist/capability.js +416 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli/commands/doctor.js +127 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +178 -51
- package/dist/cli/commands/helpers.js.map +1 -1
- package/dist/cli/commands/op-result.js +6 -0
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/reads.js +47 -1
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +9 -0
- package/dist/cli/commands/setup.js +63 -83
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/writes.d.ts +1 -1
- package/dist/cli/commands/writes.js +241 -107
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/dry-run.d.ts +1 -1
- package/dist/cli/dry-run.js +1 -1
- package/dist/cli/glyphs.d.ts +23 -4
- package/dist/cli/glyphs.js +46 -19
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +49 -27
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/period.d.ts +3 -1
- package/dist/cli/period.js +1 -1
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.js +26 -1
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +31 -4
- package/dist/cli/render.js +127 -15
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +22 -2
- package/dist/client.js +36 -13
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +6 -2
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +219 -5
- package/dist/deputy/install.js +803 -31
- package/dist/deputy/install.js.map +1 -1
- package/dist/deputy/notices.js +2 -2
- package/dist/deputy/notices.js.map +1 -1
- package/dist/deputy/protocol.d.ts +59 -5
- package/dist/deputy/protocol.js +48 -11
- package/dist/deputy/protocol.js.map +1 -1
- package/dist/deputy/routing.d.ts +21 -0
- package/dist/deputy/routing.js +111 -27
- package/dist/deputy/routing.js.map +1 -1
- package/dist/diagnose.d.ts +14 -1
- package/dist/diagnose.js +46 -6
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +78 -0
- package/dist/direct-setup.js +484 -0
- package/dist/direct-setup.js.map +1 -0
- package/dist/host-access.d.ts +63 -0
- package/dist/host-access.js +165 -0
- package/dist/host-access.js.map +1 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +27 -1
- package/dist/mcp/server.js +285 -100
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.d.ts +9 -0
- package/dist/op-result.js +5 -0
- package/dist/op-result.js.map +1 -1
- package/dist/read/filter-contract.d.ts +1 -1
- package/dist/read/filter-contract.js +4 -0
- package/dist/read/filter-contract.js.map +1 -1
- package/dist/read/shape.js +4 -0
- package/dist/read/shape.js.map +1 -1
- package/dist/read/views.d.ts +29 -0
- package/dist/read/views.js +60 -0
- package/dist/read/views.js.map +1 -1
- package/dist/session-grant.d.ts +48 -0
- package/dist/session-grant.js +170 -0
- package/dist/session-grant.js.map +1 -0
- package/dist/surface-copy.d.ts +8 -0
- package/dist/surface-copy.js +8 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/wizard.d.ts +58 -0
- package/dist/wizard.js +167 -0
- package/dist/wizard.js.map +1 -0
- package/dist/write/availability.d.ts +1 -1
- package/dist/write/availability.js +3 -3
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.d.ts +20 -0
- package/dist/write/batch.js +94 -15
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +9 -0
- package/dist/write/capabilities.js +2 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/clear-reminder.js +1 -1
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.d.ts +12 -1
- package/dist/write/commands.js +112 -224
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +3 -1
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.js +19 -2
- package/dist/write/guards.js.map +1 -1
- package/dist/write/lock.d.ts +16 -13
- package/dist/write/lock.js +55 -0
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +9 -13
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/operations.d.ts +36 -21
- package/dist/write/operations.js +7 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +14 -0
- package/dist/write/opid.js +22 -0
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +124 -0
- package/dist/write/param-schema.js +747 -0
- package/dist/write/param-schema.js.map +1 -0
- package/dist/write/pipeline.d.ts +76 -2
- package/dist/write/pipeline.js +190 -2
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +8 -0
- package/dist/write/pre-state.js +29 -2
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +539 -439
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +25 -8
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +28 -1
- package/dist/write/repeat-rule.js +54 -4
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +11 -0
- package/dist/write/resolution-timestamps.js +83 -38
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +4 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/template-mutation.d.ts +137 -0
- package/dist/write/template-mutation.js +451 -0
- package/dist/write/template-mutation.js.map +1 -0
- package/dist/write/undo.js +10 -20
- package/dist/write/undo.js.map +1 -1
- package/dist/write/update-fields.d.ts +151 -0
- package/dist/write/update-fields.js +427 -0
- package/dist/write/update-fields.js.map +1 -0
- package/dist/write/vectors/applescript.js +3 -0
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +1 -1
- package/dist/write/vectors/registry.js +10 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +2 -2
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +7 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +57 -0
- package/dist/write/vectors/ui-recipes.d.ts +19 -0
- package/dist/write/vectors/ui-recipes.js +30 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +13 -0
- package/dist/write/vectors/ui.js +62 -13
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +21 -0
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +2 -1
- package/schema/envelope.schema.json +5 -3
- package/scripts/build-helpers.sh +5 -0
- package/skills/things-cli/SKILL.md +5 -3
- package/skills/things-cli/references/contracts.md +1 -0
- package/skills/things-cli/references/data-model.md +3 -0
- package/skills/things-cli/references/errors.md +1 -1
- package/skills/things-cli/references/gui.md +1 -0
- package/skills/things-cli/references/repeating.md +6 -0
- package/skills/things-cli/references/tag-cleanup.md +64 -0
package/dist/mcp/server.js
CHANGED
|
@@ -16,7 +16,64 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
-
import { ALL_DESC, AREA_FILTER_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, blockedCode, capabilitiesTable, DATE_FORMAT, DEFAULT_LIST_LIMIT, diagnose, FILTER_CONTRACT, FULL_DESC, hasTagPresence, isLooseRef, isValidTimeZone, LIMIT_DESC, looseShadowNotice, MCP_WHEN_LABELS, mutationWireData, noUuidMatch, omitEmpty, OMIT_EMPTY_NOTE, OP_ID_RE, opResult, OPERATION_KINDS, openThings, PKG_VERSION, PROJECT_LIMIT_DESC, PROJECT_PREVIEW_LIMIT, REF_FORMAT, REF_RULE_NOTE, ReferenceResolutionError, REMINDER_FORMAT, RESOLUTION_DATE_FORMAT, schemaWarnings, shapeReadPayload, withTodayBucketTotals, withAreaBucketTotals, withSectionTotals, withUpcomingBlockTotals, splitWhenSugar, tagFilterFields, tagFlagConflict, validateViewArgs, verifyFailedCode, WHEN_VALUES, } from "../index.js";
|
|
19
|
+
import { ALL_DESC, AREA_FILTER_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, blockedCode, buildUpdatePatch, capabilitiesTable, DATE_FORMAT, DEFAULT_LIST_LIMIT, diagnose, FILTER_CONTRACT, FULL_DESC, hasTagPresence, isLooseRef, isValidTimeZone, LIMIT_DESC, looseShadowNotice, MCP_UPDATE_LABELS, MCP_WHEN_LABELS, mutationWireData, NOTES_FORMAT, noUuidMatch, omitEmpty, OMIT_EMPTY_NOTE, OP_ID_RE, opResult, OPERATION_KINDS, openThings, readAllowed, readCapability, uiAllowed, uiCapability, writeAllowed, writeCapability, PKG_VERSION, PROJECT_LIMIT_DESC, PROJECT_PREVIEW_LIMIT, REF_FORMAT, REF_RULE_NOTE, ReferenceResolutionError, REMINDER_FORMAT, RESOLUTION_DATE_FORMAT, schemaWarnings, shapeReadPayload, withTodayBucketTotals, withAreaBucketTotals, withSectionTotals, withUpcomingBlockTotals, splitWhenSugar, tagFilterFields, tagFlagConflict, validateViewArgs, verifyFailedCode, WHEN_VALUES, } from "../index.js";
|
|
20
|
+
/** Tools that mutate through the app. Everything unlisted is a plain read. */
|
|
21
|
+
const WRITE_TOOLS = new Set([
|
|
22
|
+
"add_todo",
|
|
23
|
+
"update",
|
|
24
|
+
"set_status",
|
|
25
|
+
"move_todo",
|
|
26
|
+
"set_tags",
|
|
27
|
+
"edit_checklist",
|
|
28
|
+
"delete",
|
|
29
|
+
"restore_item",
|
|
30
|
+
"heading",
|
|
31
|
+
"clear_reminder",
|
|
32
|
+
"duplicate_item",
|
|
33
|
+
"clone_item",
|
|
34
|
+
"add_project",
|
|
35
|
+
"move_project",
|
|
36
|
+
"add_area",
|
|
37
|
+
"add_tag",
|
|
38
|
+
"log_now",
|
|
39
|
+
"run_operation",
|
|
40
|
+
"batch",
|
|
41
|
+
"reorder",
|
|
42
|
+
"undo",
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* Tools with NO headless spelling at all — every path through them drives the
|
|
46
|
+
* Things window. A tool that merely *can* reach a GUI op (`heading`'s promote,
|
|
47
|
+
* `reorder`'s areas) is NOT listed: those have working headless branches, and
|
|
48
|
+
* the pipeline's own Article IV gate refuses the GUI ones per call.
|
|
49
|
+
*/
|
|
50
|
+
const UI_TOOLS = new Set(["repeat", "convert_to_project"]);
|
|
51
|
+
/** The always-available diagnostics. */
|
|
52
|
+
const DIAGNOSTIC_TOOLS = new Set(["doctor", "capabilities", "op_result"]);
|
|
53
|
+
function toolClass(name) {
|
|
54
|
+
if (DIAGNOSTIC_TOOLS.has(name))
|
|
55
|
+
return "none";
|
|
56
|
+
if (UI_TOOLS.has(name))
|
|
57
|
+
return "ui";
|
|
58
|
+
if (WRITE_TOOLS.has(name))
|
|
59
|
+
return "write";
|
|
60
|
+
return "read";
|
|
61
|
+
}
|
|
62
|
+
/** The one remediation every baked refusal ends with. */
|
|
63
|
+
const RESTART_REMEDIATION = "this server surveyed its permissions once at startup and does not re-check them, because a " +
|
|
64
|
+
"grant made now would attach to whoever launched it: stop this server, run the setup command " +
|
|
65
|
+
"above at the machine, then start it again.";
|
|
66
|
+
/** The vector classes this server can actually serve, for the instructions. */
|
|
67
|
+
function availableClasses(capability) {
|
|
68
|
+
const available = [];
|
|
69
|
+
if (readAllowed(capability.read))
|
|
70
|
+
available.push("read");
|
|
71
|
+
if (writeAllowed(capability.write))
|
|
72
|
+
available.push("write");
|
|
73
|
+
if (uiAllowed(capability.ui))
|
|
74
|
+
available.push("gui-driving");
|
|
75
|
+
return available;
|
|
76
|
+
}
|
|
20
77
|
function jsonResult(data) {
|
|
21
78
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
22
79
|
}
|
|
@@ -402,7 +459,7 @@ const tagLabel = (t) => t.parent === null ? t.title : `${t.parent} > ${t.title}`
|
|
|
402
459
|
* names without a discovery round-trip. Degrades to conventions-only when
|
|
403
460
|
* the database is not readable.
|
|
404
461
|
*/
|
|
405
|
-
function buildInstructions(getClient) {
|
|
462
|
+
function buildInstructions(getClient, capability) {
|
|
406
463
|
const lines = [
|
|
407
464
|
"This server reads and modifies the user's Things 3 data: to-dos, projects, areas, and tags.",
|
|
408
465
|
"",
|
|
@@ -418,6 +475,8 @@ function buildInstructions(getClient) {
|
|
|
418
475
|
`- Scheduling vocabulary: when = ${WHEN_VALUES}; deadlines are ${DATE_FORMAT}; reminders ` +
|
|
419
476
|
`are ${REMINDER_FORMAT}. Resolve relative calendar phrases against the Calendar context ` +
|
|
420
477
|
`below (or a date-sensitive read result's meta.clock.today), then pass the explicit date.`,
|
|
478
|
+
`- Notes vocabulary: the notes field is ${NOTES_FORMAT}. Newlines are kept as written, so ` +
|
|
479
|
+
`pass a multi-line body directly rather than flattening it into one line.`,
|
|
421
480
|
"- Every write tool accepts dry_run: true to preview the change without applying it. " +
|
|
422
481
|
"A preview creates no state, so later calls cannot reference an item that only appeared in " +
|
|
423
482
|
"a dry-run result. Operations with cascading or permanent effects require the explicit " +
|
|
@@ -430,6 +489,27 @@ function buildInstructions(getClient) {
|
|
|
430
489
|
"- For capped reads, pass limit to cap rows or all: true for everything; if both are set, all wins.",
|
|
431
490
|
`- Read results are compact: ${OMIT_EMPTY_NOTE}`,
|
|
432
491
|
];
|
|
492
|
+
// What this server may do, decided once at startup and true for its whole
|
|
493
|
+
// life (permissions doctrine, Article II). Stated up front so an agent plans
|
|
494
|
+
// around the real surface instead of discovering it one refusal at a time.
|
|
495
|
+
const available = availableClasses(capability);
|
|
496
|
+
const missing = [];
|
|
497
|
+
if (!readAllowed(capability.read))
|
|
498
|
+
missing.push(`reads — ${capability.read.detail}`);
|
|
499
|
+
if (!writeAllowed(capability.write))
|
|
500
|
+
missing.push(`changes — ${capability.write.detail}`);
|
|
501
|
+
if (!uiAllowed(capability.ui))
|
|
502
|
+
missing.push(`GUI-driven operations — ${capability.ui.detail}`);
|
|
503
|
+
lines.push("", "Permissions (checked once at server start, fixed for this server's lifetime):", `- Available here: ${available.length > 0 ? available.join(", ") : "nothing — every tool below refuses"}.`, ...(missing.length > 0
|
|
504
|
+
? [
|
|
505
|
+
`- Unavailable here: ${missing.join("; ")}.`,
|
|
506
|
+
"- Tools needing an unavailable capability refuse immediately rather than trying and " +
|
|
507
|
+
"raising a macOS permission dialog nobody is present to answer. Remediation for each " +
|
|
508
|
+
"is in its refusal. These grants attach to whichever process launched this server, so " +
|
|
509
|
+
"they cannot change while it runs: the fix is always stop the server, run the named " +
|
|
510
|
+
"setup command at the machine, start it again.",
|
|
511
|
+
]
|
|
512
|
+
: ["- Nothing is missing; no tool will refuse for want of a permission."]));
|
|
433
513
|
try {
|
|
434
514
|
const c = getClient();
|
|
435
515
|
// Under a container scope the inventory below is already limited to in-scope
|
|
@@ -499,7 +579,83 @@ export function createThingsMcpServer(options = {}) {
|
|
|
499
579
|
});
|
|
500
580
|
return client;
|
|
501
581
|
};
|
|
502
|
-
|
|
582
|
+
// THE STARTUP BAKE (permissions doctrine, Article II). One prompt-free survey
|
|
583
|
+
// before the transport is connected; the verdict then shapes the instructions
|
|
584
|
+
// and gates the tools, and is never re-taken.
|
|
585
|
+
const capability = options.capability ?? {
|
|
586
|
+
read: readCapability(options.dbPath !== undefined ? { dbPath: options.dbPath } : {}),
|
|
587
|
+
write: writeCapability(),
|
|
588
|
+
ui: uiCapability(),
|
|
589
|
+
};
|
|
590
|
+
const warn = options.onStartupWarning ?? ((line) => process.stderr.write(`${line}\n`));
|
|
591
|
+
const unavailable = [
|
|
592
|
+
...(readAllowed(capability.read) ? [] : ["reads"]),
|
|
593
|
+
...(writeAllowed(capability.write) ? [] : ["changes"]),
|
|
594
|
+
...(uiAllowed(capability.ui) ? [] : ["GUI-driven operations"]),
|
|
595
|
+
];
|
|
596
|
+
if (unavailable.length > 0) {
|
|
597
|
+
warn(`things-api MCP: ${unavailable.join(", ")} unavailable on this machine — ` +
|
|
598
|
+
`${capability.read.detail}; ${capability.write.detail}; ${capability.ui.detail}. ` +
|
|
599
|
+
"Tools needing them refuse; restart this server after running setup.");
|
|
600
|
+
}
|
|
601
|
+
const server = new McpServer({ name: "things-api", version: PKG_VERSION }, { instructions: buildInstructions(getClient, capability) });
|
|
602
|
+
/**
|
|
603
|
+
* The baked refusal for one tool class, or null when the class is served.
|
|
604
|
+
* Article II in one function: the answer is the STARTUP verdict, quoted, with
|
|
605
|
+
* the setup command and the restart rule — never a fresh probe, and never a
|
|
606
|
+
* dispatch that could put a dialog on screen.
|
|
607
|
+
*/
|
|
608
|
+
const bakedRefusal = (name) => {
|
|
609
|
+
switch (toolClass(name)) {
|
|
610
|
+
case "none":
|
|
611
|
+
return null;
|
|
612
|
+
case "read":
|
|
613
|
+
return readAllowed(capability.read)
|
|
614
|
+
? null
|
|
615
|
+
: errorResult({
|
|
616
|
+
code: "environment",
|
|
617
|
+
message: `this tool reads the Things database, and ${capability.read.detail}`,
|
|
618
|
+
remediation: [...capability.read.remediation, RESTART_REMEDIATION].join("; "),
|
|
619
|
+
});
|
|
620
|
+
case "write":
|
|
621
|
+
// A write read-verifies, so it needs both classes; name the one that
|
|
622
|
+
// is actually missing rather than a generic "cannot write".
|
|
623
|
+
if (!readAllowed(capability.read)) {
|
|
624
|
+
return errorResult({
|
|
625
|
+
code: "environment",
|
|
626
|
+
message: `this tool changes Things data and verifies the result by reading it back, and ${capability.read.detail}`,
|
|
627
|
+
remediation: [...capability.read.remediation, RESTART_REMEDIATION].join("; "),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
return writeAllowed(capability.write)
|
|
631
|
+
? null
|
|
632
|
+
: errorResult({
|
|
633
|
+
code: "environment",
|
|
634
|
+
message: `this tool changes Things data by driving the app, and ${capability.write.detail}`,
|
|
635
|
+
remediation: [...capability.write.remediation, RESTART_REMEDIATION].join("; "),
|
|
636
|
+
});
|
|
637
|
+
case "ui":
|
|
638
|
+
return uiAllowed(capability.ui)
|
|
639
|
+
? null
|
|
640
|
+
: errorResult({
|
|
641
|
+
code: "environment",
|
|
642
|
+
message: `this tool is delivered by driving the Things window, and ${capability.ui.detail}`,
|
|
643
|
+
remediation: [...capability.ui.remediation, RESTART_REMEDIATION].join("; "),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
/**
|
|
648
|
+
* `server.registerTool` with the baked gate in front of every callback. Every
|
|
649
|
+
* registration below goes through this, so a tool cannot be added without a
|
|
650
|
+
* class — {@link toolClass} defaults an unlisted name to `read`, the weakest
|
|
651
|
+
* useful assumption, rather than to ungated.
|
|
652
|
+
*/
|
|
653
|
+
const registerTool = ((name, config, cb) => server.registerTool(name, config, ((...args) => {
|
|
654
|
+
const refusal = bakedRefusal(name);
|
|
655
|
+
if (refusal !== null)
|
|
656
|
+
return refusal;
|
|
657
|
+
return cb(...args);
|
|
658
|
+
})));
|
|
503
659
|
// The audit author for every write on this connection, derived once from the
|
|
504
660
|
// client's handshake identity (clientInfo.name). Read per call: clientInfo is
|
|
505
661
|
// populated when the initialize handshake completes, before any tool can run.
|
|
@@ -608,14 +764,18 @@ export function createThingsMcpServer(options = {}) {
|
|
|
608
764
|
return item.type;
|
|
609
765
|
};
|
|
610
766
|
// ------------------------------------------------------------------ reads
|
|
611
|
-
|
|
767
|
+
registerTool("read_view", {
|
|
612
768
|
description: "Read a Things list as the app presents it: today (two children buckets — " +
|
|
613
769
|
"children.today and children.evening, evening expires daily; the whole-view " +
|
|
614
770
|
"count due/overdue vs. other rides the result's second block), inbox, anytime, " +
|
|
615
771
|
"upcoming, someday, logbook, trash, or deadlines (a flat items list of everything " +
|
|
616
772
|
"carrying a deadline — to-dos and projects — in deadline order, most-overdue first; " +
|
|
617
773
|
"repeating items appear at their next occurrence's projected deadline; scope with " +
|
|
618
|
-
"today/overdue/project/area/tag)
|
|
774
|
+
"today/overdue/project/area/tag), or repeaters (every repeating series in the " +
|
|
775
|
+
"library — to-dos and projects — each carrying its decoded rule under " +
|
|
776
|
+
"repeating.rule; series appear in no other view, which show the occurrences a " +
|
|
777
|
+
"series spawns instead; paused and ended series are included; ordered by next " +
|
|
778
|
+
"occurrence, the ones with none last). For upcoming, " +
|
|
619
779
|
"horizon > 1 also includes future occurrences of repeating items (up to 10 each). " +
|
|
620
780
|
"anytime/someday return sections in canonical order (area + items; null area = the " +
|
|
621
781
|
"top-level block); children of someday/future-scheduled projects are excluded " +
|
|
@@ -639,6 +799,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
639
799
|
"logbook",
|
|
640
800
|
"trash",
|
|
641
801
|
"deadlines",
|
|
802
|
+
"repeaters",
|
|
642
803
|
]),
|
|
643
804
|
...tagFilterShape,
|
|
644
805
|
...tzShape,
|
|
@@ -854,10 +1015,14 @@ export function createThingsMcpServer(options = {}) {
|
|
|
854
1015
|
});
|
|
855
1016
|
return truncatedResult(shapeReadPayload("deadlines", items, full, c.refPromoter()), truncation);
|
|
856
1017
|
}
|
|
1018
|
+
case "repeaters": {
|
|
1019
|
+
const { items, truncation } = c.read.repeaters({ ...filter, ...zone, limit });
|
|
1020
|
+
return truncatedResult(shapeReadPayload("repeaters", items, full, c.refPromoter()), truncation);
|
|
1021
|
+
}
|
|
857
1022
|
}
|
|
858
1023
|
}, args.tz, () => (filterMeta !== undefined ? { filter: filterMeta } : undefined), () => looseAreaWarnings(getClient(), args.area));
|
|
859
1024
|
});
|
|
860
|
-
|
|
1025
|
+
registerTool("search", {
|
|
861
1026
|
description: "Find items by title/notes substring. Returns open, untrashed items by default; " +
|
|
862
1027
|
"include more with logged/trashed/all. Scope with project/area/tag — scope " +
|
|
863
1028
|
"references must name existing items. " +
|
|
@@ -920,7 +1085,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
920
1085
|
});
|
|
921
1086
|
return truncatedResult(shapeReadPayload("search", items, args.full === true, getClient().refPromoter()), truncation);
|
|
922
1087
|
}, args.tz, undefined, () => looseAreaWarnings(getClient(), args.area)));
|
|
923
|
-
|
|
1088
|
+
registerTool("changes_since", {
|
|
924
1089
|
description: "List items created or modified since a moment — including trashed, logged, and " +
|
|
925
1090
|
"repeating items (inspect each item's fields to tell them apart). Edits to tags, " +
|
|
926
1091
|
"areas, and checklist items do not mark the containing item as modified. " +
|
|
@@ -950,7 +1115,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
950
1115
|
});
|
|
951
1116
|
return truncatedResult(shapeReadPayload("changes", items, args.full === true, getClient().refPromoter()), truncation);
|
|
952
1117
|
}, args.tz));
|
|
953
|
-
|
|
1118
|
+
registerTool("get_item", {
|
|
954
1119
|
description: "Full detail for one item by uuid: notes, schedule, reminder, deadline, tags " +
|
|
955
1120
|
"(direct and inherited), checklist with per-item state, repeat schedule, and its " +
|
|
956
1121
|
"project/area/heading. " +
|
|
@@ -965,7 +1130,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
965
1130
|
? errorResult({ code: "not-found", message: noUuidMatch("item", args.uuid) })
|
|
966
1131
|
: readResult(shapeReadPayload("detail", item, false, getClient().refPromoter()));
|
|
967
1132
|
}));
|
|
968
|
-
|
|
1133
|
+
registerTool("get_project", {
|
|
969
1134
|
description: "One project's full contents: metadata plus its to-dos grouped under their headings. " +
|
|
970
1135
|
"Open headings (and archived ones not yet swept to the logbook) stay live, carrying " +
|
|
971
1136
|
"archived once archived; a swept archived heading moves into the logged region as a " +
|
|
@@ -997,7 +1162,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
997
1162
|
...(args.tz !== undefined && { zone: args.tz }),
|
|
998
1163
|
}), args.full === true, getClient().refPromoter()));
|
|
999
1164
|
}, args.tz));
|
|
1000
|
-
|
|
1165
|
+
registerTool("get_area", {
|
|
1001
1166
|
description: "One area's contents: metadata plus its direct to-dos (active first), its " +
|
|
1002
1167
|
"projects in canonical order, and later (scheduled/repeating/someday). The area " +
|
|
1003
1168
|
"logbook is not returned here — read it with read_view logbook + area; trashed " +
|
|
@@ -1061,7 +1226,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1061
1226
|
return groupedResult(withAreaBucketTotals(shapeReadPayload("area-view", view, args.full === true, getClient().refPromoter()), totals), truncation);
|
|
1062
1227
|
}, args.tz, undefined, () => (areaNotice !== undefined ? [areaNotice] : undefined));
|
|
1063
1228
|
});
|
|
1064
|
-
|
|
1229
|
+
registerTool("list_collections", {
|
|
1065
1230
|
description: "List every project, area, or tag (tags include their parent-tag nesting). Use to " +
|
|
1066
1231
|
"refresh the inventory summarized in the server instructions. The tag filters scope " +
|
|
1067
1232
|
"the projects list by each project's own tags (areas/tags reject them). " +
|
|
@@ -1116,7 +1281,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1116
1281
|
}, args.tz));
|
|
1117
1282
|
// ---------------------------------------------------------------- to-dos
|
|
1118
1283
|
const whenSchema = z.string().optional().describe(WHEN_VALUES);
|
|
1119
|
-
|
|
1284
|
+
registerTool("add_todo", {
|
|
1120
1285
|
description: "Create a to-do and return its uuid. Optionally schedule it, set a reminder or " +
|
|
1121
1286
|
"deadline, tag it, give it a checklist, and place it in a project or area " +
|
|
1122
1287
|
"(optionally under a heading within that project). A reminder " +
|
|
@@ -1124,7 +1289,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1124
1289
|
"project reopens that project — pass acknowledge_project_reopen to confirm.",
|
|
1125
1290
|
inputSchema: {
|
|
1126
1291
|
title: z.string(),
|
|
1127
|
-
notes: z.string().optional(),
|
|
1292
|
+
notes: z.string().optional().describe(`Notes body — ${NOTES_FORMAT}`),
|
|
1128
1293
|
when: whenSchema,
|
|
1129
1294
|
reminder: z.string().optional().describe(REMINDER_FORMAT),
|
|
1130
1295
|
deadline: z.string().optional().describe(DATE_FORMAT),
|
|
@@ -1177,7 +1342,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1177
1342
|
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
1178
1343
|
}, writeOptions(args)));
|
|
1179
1344
|
}));
|
|
1180
|
-
|
|
1345
|
+
registerTool("update", {
|
|
1181
1346
|
description: "Edit an existing to-do, project, area, or tag — kind selects which. " +
|
|
1182
1347
|
"kind todo/project: title, notes (or append_notes/prepend_notes to add a line to the " +
|
|
1183
1348
|
"existing body, exclusive with notes), schedule (when), reminder/clear_reminder, and " +
|
|
@@ -1198,7 +1363,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1198
1363
|
.describe("The item to update — a to-do by uuid; a project, area, or tag also accepts a " +
|
|
1199
1364
|
"unique name"),
|
|
1200
1365
|
title: z.string().optional().describe("New title (any kind)"),
|
|
1201
|
-
notes: z
|
|
1366
|
+
notes: z
|
|
1367
|
+
.string()
|
|
1368
|
+
.optional()
|
|
1369
|
+
.describe(`todo/project: replaces the whole notes body — ${NOTES_FORMAT}`),
|
|
1202
1370
|
append_notes: z.string().optional().describe("todo/project: add a line after the notes"),
|
|
1203
1371
|
prepend_notes: z.string().optional().describe("todo/project: add a line before the notes"),
|
|
1204
1372
|
when: whenSchema,
|
|
@@ -1220,6 +1388,17 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1220
1388
|
.array(z.string())
|
|
1221
1389
|
.optional()
|
|
1222
1390
|
.describe(`area: replace the tag set (full) — ${TAG_REF_FORMAT}`),
|
|
1391
|
+
exception: z
|
|
1392
|
+
.boolean()
|
|
1393
|
+
.optional()
|
|
1394
|
+
.describe("todo, repeating only: change just the NEXT occurrence and leave the series alone " +
|
|
1395
|
+
"(the occurrence is created if it has not appeared yet). Refused when the series " +
|
|
1396
|
+
"already lands on the requested day, and when the series names no next date at " +
|
|
1397
|
+
"all (a paused one — resume it first). The result's occurrence field names both the occurrence " +
|
|
1398
|
+
"and the series. Each call takes another occurrence out of the series, so pass " +
|
|
1399
|
+
"op_id when retrying: a resubmission with the same key replays the first result " +
|
|
1400
|
+
"instead of creating a second occurrence. Undo restores the occurrence's own " +
|
|
1401
|
+
"change but cannot remove the occurrence or rewind the series"),
|
|
1223
1402
|
parent: z.string().optional().describe("tag: existing tag to nest under"),
|
|
1224
1403
|
unnest: z.boolean().optional().describe("tag: move the tag to the top level"),
|
|
1225
1404
|
shortcut: z.string().optional().describe("tag: keyboard shortcut character"),
|
|
@@ -1238,49 +1417,23 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1238
1417
|
const opts = writeOptions(args);
|
|
1239
1418
|
const c = getClient();
|
|
1240
1419
|
if (args.kind === "todo" || args.kind === "project") {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
if (
|
|
1246
|
-
return usage(
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
return usage(sugar.message);
|
|
1255
|
-
const when = sugar.kind === "split" ? sugar.when : args.when;
|
|
1256
|
-
const reminder = sugar.kind === "split" ? sugar.reminder : args.reminder;
|
|
1257
|
-
return mutationResult(await c.write.updateTodo(args.uuid, {
|
|
1258
|
-
...(args.title !== undefined && { title: args.title }),
|
|
1259
|
-
...(args.notes !== undefined && { notes: args.notes }),
|
|
1260
|
-
...(args.append_notes !== undefined && { appendNotes: args.append_notes }),
|
|
1261
|
-
...(args.prepend_notes !== undefined && { prependNotes: args.prepend_notes }),
|
|
1262
|
-
...(when !== undefined && { when: when }),
|
|
1263
|
-
...(reminder !== undefined && { reminder }),
|
|
1264
|
-
...(args.clear_reminder === true && { reminder: null }),
|
|
1265
|
-
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1266
|
-
...(args.clear_deadline === true && { deadline: null }),
|
|
1267
|
-
...(args.created_at !== undefined && { createdAt: args.created_at }),
|
|
1268
|
-
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
1269
|
-
}, opts));
|
|
1420
|
+
// The patch — and every exclusive pair in the vocabulary, the `@time`
|
|
1421
|
+
// sugar included — comes from the ONE builder the CLI shares, so the two
|
|
1422
|
+
// surfaces cannot drift on which flags they accept (#491 doctrine).
|
|
1423
|
+
const built = buildUpdatePatch(args, MCP_UPDATE_LABELS);
|
|
1424
|
+
if (built.kind === "error")
|
|
1425
|
+
return usage(built.message);
|
|
1426
|
+
if (args.exception === true) {
|
|
1427
|
+
if (args.kind !== "todo")
|
|
1428
|
+
return usage("exception applies to kind todo");
|
|
1429
|
+
if (args.created_at !== undefined || args.completed_at !== undefined) {
|
|
1430
|
+
return usage("exception cannot be combined with created_at/completed_at");
|
|
1431
|
+
}
|
|
1432
|
+
return mutationResult(await c.write.updateTodoOccurrence(args.uuid, built.patch, opts));
|
|
1270
1433
|
}
|
|
1271
|
-
return mutationResult(
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
...(args.append_notes !== undefined && { appendNotes: args.append_notes }),
|
|
1275
|
-
...(args.prepend_notes !== undefined && { prependNotes: args.prepend_notes }),
|
|
1276
|
-
...(args.when !== undefined && { when: args.when }),
|
|
1277
|
-
...(args.reminder !== undefined && { reminder: args.reminder }),
|
|
1278
|
-
...(args.clear_reminder === true && { reminder: null }),
|
|
1279
|
-
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1280
|
-
...(args.clear_deadline === true && { deadline: null }),
|
|
1281
|
-
...(args.created_at !== undefined && { createdAt: args.created_at }),
|
|
1282
|
-
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
1283
|
-
}, opts));
|
|
1434
|
+
return mutationResult(args.kind === "todo"
|
|
1435
|
+
? await c.write.updateTodo(args.uuid, built.patch, opts)
|
|
1436
|
+
: await c.write.updateProject(args.uuid, built.patch, opts));
|
|
1284
1437
|
}
|
|
1285
1438
|
if (args.kind === "area") {
|
|
1286
1439
|
if (args.title === undefined && args.tags === undefined) {
|
|
@@ -1313,9 +1466,15 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1313
1466
|
...(args.clear_shortcut === true && { clearShortcut: true }),
|
|
1314
1467
|
}, opts));
|
|
1315
1468
|
}));
|
|
1316
|
-
|
|
1469
|
+
registerTool("set_status", {
|
|
1317
1470
|
description: "Set a to-do's or project's status (scope selects which): completed, canceled, or open " +
|
|
1318
|
-
"(reopening a completed/canceled item).
|
|
1471
|
+
"(reopening a completed/canceled item). scope todo, a REPEATING to-do: completed/canceled " +
|
|
1472
|
+
"resolves the series' CURRENT occurrence — the unfinished one if there is one, otherwise " +
|
|
1473
|
+
"the next one, created for the purpose — and leaves the series running; the result's " +
|
|
1474
|
+
"occurrence field names both uuids and says which of the two was created. Each call is a " +
|
|
1475
|
+
"new resolution that takes the FOLLOWING occurrence, so pass op_id when retrying: a " +
|
|
1476
|
+
"resubmission with the same key replays the first result instead of creating a second " +
|
|
1477
|
+
"occurrence. Reopening a repeating to-do is not available. " +
|
|
1319
1478
|
"scope project, completing or canceling requires a children policy: 'require-resolved' " +
|
|
1320
1479
|
"errors if open to-dos remain; 'auto-complete'/'auto-cancel' resolves them together with " +
|
|
1321
1480
|
"the project (canceling never alters already-completed children). scope project, status " +
|
|
@@ -1401,7 +1560,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1401
1560
|
? jsonResult(outcome)
|
|
1402
1561
|
: mutationResult(outcome.project);
|
|
1403
1562
|
}));
|
|
1404
|
-
|
|
1563
|
+
registerTool("move_todo", {
|
|
1405
1564
|
description: "Move one or more to-dos as an ordered block (spec §4). MOVE changes WHAT a to-do " +
|
|
1406
1565
|
"belongs to (membership somewhere); to REARRANGE to-dos that already share a container " +
|
|
1407
1566
|
"without changing membership, call this with a position (first/last/before/after) and " +
|
|
@@ -1474,7 +1633,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1474
1633
|
};
|
|
1475
1634
|
return moveResult(await getClient().write.moveTodos(request, writeOptions(args)));
|
|
1476
1635
|
}));
|
|
1477
|
-
|
|
1636
|
+
registerTool("set_tags", {
|
|
1478
1637
|
description: "Replace or extend a to-do's or project's tags. mode 'replace' (default) sets exactly " +
|
|
1479
1638
|
"the given list — an empty list removes all tags; mode 'add' merges with the current " +
|
|
1480
1639
|
"tags. Tags must exist unless create_tags is set (or create them first with add_tag).",
|
|
@@ -1501,7 +1660,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1501
1660
|
? await c.write.setProjectTags(args.uuid, args.tags, opts)
|
|
1502
1661
|
: await c.write.setTags(args.uuid, args.tags, opts));
|
|
1503
1662
|
}));
|
|
1504
|
-
|
|
1663
|
+
registerTool("edit_checklist", {
|
|
1505
1664
|
description: "Edit a to-do's checklist. The single-item actions add / remove / check / uncheck " +
|
|
1506
1665
|
"/ rename / move change one item — targeted by title or 1-based index — and leave " +
|
|
1507
1666
|
"every other item and its checked state untouched (duplicate titles resolve " +
|
|
@@ -1591,11 +1750,16 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1591
1750
|
return mutationResult(await c.write.editChecklist(args.uuid, edit, writeOptions(args)));
|
|
1592
1751
|
}));
|
|
1593
1752
|
// ------------------------------------------------- to-dos AND projects
|
|
1594
|
-
|
|
1753
|
+
registerTool("delete", {
|
|
1595
1754
|
description: "Delete a to-do/project (kind item), an area, or a tag — kind selects which. " +
|
|
1596
1755
|
"kind item: moves a to-do or project to the Trash (recoverable via restore_item until " +
|
|
1597
|
-
"the Trash is emptied; a deleted project takes its to-dos with it
|
|
1598
|
-
"
|
|
1756
|
+
"the Trash is emptied; a deleted project takes its to-dos with it). Deleting a repeating " +
|
|
1757
|
+
"series' TEMPLATE is allowed and ends the series: it stops generating new occurrences and " +
|
|
1758
|
+
"its existing occurrences are left in place (the result names how many, and the current " +
|
|
1759
|
+
"one). That one cannot be undone — neither undo nor restore_item brings a template back, " +
|
|
1760
|
+
"so no undo token is returned; the series returns only via Put Back in the Things app's " +
|
|
1761
|
+
"Trash. Deleting a single occurrence leaves the series running. kind area: PERMANENT — " +
|
|
1762
|
+
"areas do not go to the Trash, so this cannot " +
|
|
1599
1763
|
"be undone and requires dangerously_permanent; deleting an area moves its to-dos and " +
|
|
1600
1764
|
"projects to the Trash, so a non-empty area is refused unless you pass " +
|
|
1601
1765
|
"allow_non_empty_area (empty it first to keep its contents). kind tag: PERMANENT — requires " +
|
|
@@ -1637,7 +1801,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1637
1801
|
return mutationResult(await c.write.deleteTag(args.uuid, opts));
|
|
1638
1802
|
}
|
|
1639
1803
|
}));
|
|
1640
|
-
|
|
1804
|
+
registerTool("restore_item", {
|
|
1641
1805
|
description: "Restore a trashed to-do or project. A to-do returns to the Inbox without its " +
|
|
1642
1806
|
"previous schedule or project/area. A project is restored in place: its schedule, " +
|
|
1643
1807
|
"area, and children come back exactly as they were.",
|
|
@@ -1655,11 +1819,11 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1655
1819
|
// write tools: add_todo / add_project (Logbook import), update (rewrite an
|
|
1656
1820
|
// already-resolved item's timestamps), and set_status (backdate on
|
|
1657
1821
|
// complete/cancel) — matching the CLI's --created-at/--completed-at flags.
|
|
1658
|
-
|
|
1822
|
+
registerTool("heading", {
|
|
1659
1823
|
description: "Manage a project's headings — action selects which; project is always required, and a " +
|
|
1660
1824
|
"heading is selected by its exact title or its uuid (never an ordinal). add_heading: a " +
|
|
1661
1825
|
"new heading in the project (project + title; returns its uuid; uses the Things proxy " +
|
|
1662
|
-
"shortcuts, set up once with `things setup
|
|
1826
|
+
"shortcuts, set up once with `things setup`); a placement flag positions it, " +
|
|
1663
1827
|
"else it appends. rename_heading: rename in place (project + heading + title; works on " +
|
|
1664
1828
|
"archived headings). archive_heading: retire a heading so it leaves the active project " +
|
|
1665
1829
|
"view (reversible with unarchive_heading); with open children pass children — complete " +
|
|
@@ -1820,7 +1984,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1820
1984
|
}
|
|
1821
1985
|
}
|
|
1822
1986
|
}));
|
|
1823
|
-
|
|
1987
|
+
registerTool("convert_to_project", {
|
|
1824
1988
|
description: "Promote a to-do into a project. This REPLACES the to-do with a new project (its notes " +
|
|
1825
1989
|
"are kept); the to-do's identity is gone and it cannot be undone. Requires " +
|
|
1826
1990
|
"dangerously_drive_gui. The new project's uuid is on the result. (To promote a HEADING, " +
|
|
@@ -1834,7 +1998,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1834
1998
|
},
|
|
1835
1999
|
annotations: DESTRUCTIVE,
|
|
1836
2000
|
}, async (args) => guard(async () => mutationResult(await getClient().write.run("todo.convert-to-project", { uuid: args.uuid }, writeOptions(args)))));
|
|
1837
|
-
|
|
2001
|
+
registerTool("clear_reminder", {
|
|
1838
2002
|
description: "Clear a to-do's time-of-day reminder while keeping its scheduled date. Uses the " +
|
|
1839
2003
|
"Things proxy shortcuts when installed (in place, and the only path for a repeating " +
|
|
1840
2004
|
"to-do); otherwise a non-repeating dated to-do falls back to a URL re-schedule that " +
|
|
@@ -1921,7 +2085,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1921
2085
|
fields.startDaysEarlier = a.start_days_earlier;
|
|
1922
2086
|
return fields;
|
|
1923
2087
|
};
|
|
1924
|
-
|
|
2088
|
+
registerTool("repeat", {
|
|
1925
2089
|
description: "Manage recurrence on a to-do or project (scope) by driving the local Things app's " +
|
|
1926
2090
|
"interface — every action needs dangerously_drive_gui. action start: turn a plain item " +
|
|
1927
2091
|
"into a repeating one (promote-via-clone: a disposable copy is promoted and the ORIGINAL " +
|
|
@@ -1943,15 +2107,16 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1943
2107
|
.optional()
|
|
1944
2108
|
.describe("start/reschedule/pause/resume: the item (a project also accepts a unique name)"),
|
|
1945
2109
|
title: z.string().optional().describe("add: the new item's title"),
|
|
1946
|
-
notes: z.string().optional().describe(
|
|
2110
|
+
notes: z.string().optional().describe(`add: notes — ${NOTES_FORMAT}`),
|
|
1947
2111
|
area: z.string().optional().describe(`add (project): destination area (${REF_FORMAT})`),
|
|
1948
2112
|
project_deadline: z
|
|
1949
2113
|
.string()
|
|
1950
2114
|
.optional()
|
|
1951
|
-
.describe(`add: due date (${DATE_FORMAT}).
|
|
1952
|
-
`
|
|
1953
|
-
`
|
|
1954
|
-
`other, both must agree).
|
|
2115
|
+
.describe(`add: due date (${DATE_FORMAT}). It deadlines EVERY occurrence — each starts ` +
|
|
2116
|
+
`(deadline − when) days before its own deadline, so it needs a concrete when on or ` +
|
|
2117
|
+
`before it. For a to-do it is an alternative spelling of start_days_earlier (give ` +
|
|
2118
|
+
`one or the other, both must agree). Not available with after_completion on a ` +
|
|
2119
|
+
`project, whose occurrences are created without a deadline.`),
|
|
1955
2120
|
todos: z.array(z.string()).optional().describe("add (project): initial child to-do titles"),
|
|
1956
2121
|
frequency: z
|
|
1957
2122
|
.enum(["daily", "weekly", "monthly", "yearly"])
|
|
@@ -1992,10 +2157,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1992
2157
|
}
|
|
1993
2158
|
// add-repeating carries only the calendar-anchor rule fields here; the rule
|
|
1994
2159
|
// reminder is set with a follow-up reschedule. A concrete `project_deadline`
|
|
1995
|
-
// maps to the RULE
|
|
1996
|
-
// deadline-free)
|
|
1997
|
-
//
|
|
1998
|
-
//
|
|
2160
|
+
// maps to the RULE on BOTH scopes (DBLSPAWN1 — each occurrence deadlined,
|
|
2161
|
+
// the created item deadline-free). For a to-do, `start_days_earlier` is the
|
|
2162
|
+
// alternative spelling of that deadline geometry (the two must AGREE when
|
|
2163
|
+
// both are given — enforced in the library).
|
|
1999
2164
|
const { reminder: _reminder, deadline: _deadline, startDaysEarlier, ...ruleExtras } = repeatExtras(args, frequency);
|
|
2000
2165
|
if (args.scope === "todo") {
|
|
2001
2166
|
return mutationResult(await c.write.addRepeatingTodo({
|
|
@@ -2049,7 +2214,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2049
2214
|
// scope project, action start → the dedicated make-repeating method
|
|
2050
2215
|
return mutationResult(await c.write.makeRepeatingProject(args.uuid, { frequency, interval, ...extras }, opts));
|
|
2051
2216
|
}));
|
|
2052
|
-
|
|
2217
|
+
registerTool("duplicate_item", {
|
|
2053
2218
|
description: "Duplicate a to-do or project and return the copy's uuid; a duplicated project " +
|
|
2054
2219
|
"includes its children. Not available for repeating items.",
|
|
2055
2220
|
inputSchema: { uuid: z.string(), ...dryRunShape, ...preserveModifiedShape, ...opIdShape },
|
|
@@ -2060,7 +2225,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2060
2225
|
? await c.write.duplicateTodo(args.uuid, writeOptions(args))
|
|
2061
2226
|
: await c.write.duplicateProject(args.uuid, writeOptions(args)));
|
|
2062
2227
|
}));
|
|
2063
|
-
|
|
2228
|
+
registerTool("clone_item", {
|
|
2064
2229
|
description: "Clone a to-do or project — a faithful content copy through official write surfaces and " +
|
|
2065
2230
|
"return the clone's uuid. Copies title, notes, tags, when, reminder, deadline, checklist " +
|
|
2066
2231
|
"items and their checked state, container, and completed/canceled state with the exact " +
|
|
@@ -2097,12 +2262,12 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2097
2262
|
: await c.write.cloneProject(args.uuid, clone, writeOptions(args)));
|
|
2098
2263
|
}));
|
|
2099
2264
|
// -------------------------------------------------------------- projects
|
|
2100
|
-
|
|
2265
|
+
registerTool("add_project", {
|
|
2101
2266
|
description: "Create a project and return its uuid. Optionally place it in an area, schedule " +
|
|
2102
2267
|
"it, set a deadline, and seed it with initial to-dos.",
|
|
2103
2268
|
inputSchema: {
|
|
2104
2269
|
title: z.string(),
|
|
2105
|
-
notes: z.string().optional(),
|
|
2270
|
+
notes: z.string().optional().describe(`Notes body — ${NOTES_FORMAT}`),
|
|
2106
2271
|
area: z.string().optional().describe(`Destination area (${REF_FORMAT})`),
|
|
2107
2272
|
when: whenSchema,
|
|
2108
2273
|
deadline: z.string().optional().describe(DATE_FORMAT),
|
|
@@ -2137,7 +2302,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2137
2302
|
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
2138
2303
|
}, writeOptions(args)));
|
|
2139
2304
|
}));
|
|
2140
|
-
|
|
2305
|
+
registerTool("move_project", {
|
|
2141
2306
|
description: "Move one or more projects as an ordered block (spec §4/§5). Pass at most one " +
|
|
2142
2307
|
"destination: to_area, or no_area (leave the area — a project's complete detach). To " +
|
|
2143
2308
|
"REORDER projects among their siblings without changing area, pass a position " +
|
|
@@ -2172,7 +2337,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2172
2337
|
return moveResult(await getClient().write.moveProjects(request, writeOptions(args)));
|
|
2173
2338
|
}));
|
|
2174
2339
|
// ----------------------------------------------------------------- areas
|
|
2175
|
-
|
|
2340
|
+
registerTool("add_area", {
|
|
2176
2341
|
description: "Create an area, optionally tagged. Tags must exist unless create_tags is set.",
|
|
2177
2342
|
inputSchema: {
|
|
2178
2343
|
title: z.string(),
|
|
@@ -2185,7 +2350,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2185
2350
|
annotations: NON_DESTRUCTIVE,
|
|
2186
2351
|
}, async (args) => guard(async () => mutationResult(await getClient().write.addArea({ title: args.title, ...(args.tags !== undefined && { tags: args.tags }) }, writeOptions(args)))));
|
|
2187
2352
|
// ------------------------------------------------------------------ tags
|
|
2188
|
-
|
|
2353
|
+
registerTool("add_tag", {
|
|
2189
2354
|
description: "Create a tag, optionally nested under an existing parent tag.",
|
|
2190
2355
|
inputSchema: {
|
|
2191
2356
|
title: z.string(),
|
|
@@ -2196,7 +2361,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2196
2361
|
},
|
|
2197
2362
|
annotations: NON_DESTRUCTIVE,
|
|
2198
2363
|
}, async (args) => guard(async () => mutationResult(await getClient().write.addTag({ title: args.title, ...(args.parent !== undefined && { parent: args.parent }) }, writeOptions(args)))));
|
|
2199
|
-
|
|
2364
|
+
registerTool("log_now", {
|
|
2200
2365
|
description: "Move completed items to the Logbook now. The result discloses how many items were moved " +
|
|
2201
2366
|
"(observed.logged); when none are waiting it logs nothing — a clean no-op, not an error. " +
|
|
2202
2367
|
"This cannot be undone.",
|
|
@@ -2204,10 +2369,13 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2204
2369
|
annotations: DESTRUCTIVE,
|
|
2205
2370
|
}, async (args) => guard(async () => mutationResult(await getClient().write.logNow(writeOptions(args)))));
|
|
2206
2371
|
// -------------------------------------------------- generic + discovery
|
|
2207
|
-
|
|
2372
|
+
registerTool("run_operation", {
|
|
2208
2373
|
description: "Run any cataloged operation by kind — the generic entry for operations without a " +
|
|
2209
2374
|
"dedicated tool (e.g. trash.empty). Call capabilities first for the catalog of " +
|
|
2210
|
-
"operation kinds and their parameter shapes
|
|
2375
|
+
"operation kinds and their parameter shapes (each entry carries a params list naming " +
|
|
2376
|
+
"every field, whether it is optional, and the shape it accepts). A malformed params " +
|
|
2377
|
+
"bag — an unknown field, a wrong type, a bare string where a container reference " +
|
|
2378
|
+
"object belongs — is refused before anything runs.",
|
|
2211
2379
|
inputSchema: {
|
|
2212
2380
|
op: z.enum(OPERATION_KINDS),
|
|
2213
2381
|
params: z
|
|
@@ -2233,11 +2401,13 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2233
2401
|
},
|
|
2234
2402
|
annotations: DESTRUCTIVE,
|
|
2235
2403
|
}, async (args) => guard(async () => mutationResult(await getClient().write.run(args.op, args.params, writeOptions(args)))));
|
|
2236
|
-
|
|
2404
|
+
registerTool("batch", {
|
|
2237
2405
|
description: "Run several operations in order, each independently — there are no transactions, " +
|
|
2238
2406
|
"and a failure does not roll back earlier operations. A statically-invalid operation " +
|
|
2239
|
-
"(
|
|
2240
|
-
"refuses the WHOLE batch before anything runs, naming
|
|
2407
|
+
"(unknown op, an unknown or wrongly-typed param, a $ref to an undeclared/forward " +
|
|
2408
|
+
"temp_id, a duplicate temp_id) refuses the WHOLE batch before anything runs, naming " +
|
|
2409
|
+
"every bad operation and the field it names. A container param takes an object " +
|
|
2410
|
+
'({"project": {"uuid": "…"}}), never a bare string. Otherwise ' +
|
|
2241
2411
|
"per-operation results return in order. By DEFAULT a runtime failure STOPS the batch " +
|
|
2242
2412
|
"(later operations reported not-run, with resume guidance in the summary); " +
|
|
2243
2413
|
"continue_on_error runs past failures. " +
|
|
@@ -2248,8 +2418,11 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2248
2418
|
"reference a tag by title) and unique per batch. IDEMPOTENCY: op_id makes resubmission " +
|
|
2249
2419
|
"safe — an operation matching an earlier success is reported already-applied, not " +
|
|
2250
2420
|
"re-created (put an op_id on EVERY operation so a stopped batch can be resubmitted " +
|
|
2251
|
-
"verbatim to resume).
|
|
2252
|
-
"
|
|
2421
|
+
"verbatim to resume). TIMELINE: preserve_modified applies to EVERY operation (an " +
|
|
2422
|
+
"operation may override it with its own options.preserve_modified), so a bulk re-tag " +
|
|
2423
|
+
"stays off the modification-date timeline under one undo token. The result adds " +
|
|
2424
|
+
"temp_id_mapping (handle → uuid) and undo_token, which reverses the whole batch as one " +
|
|
2425
|
+
"unit via the undo tool.",
|
|
2253
2426
|
inputSchema: {
|
|
2254
2427
|
ops: z
|
|
2255
2428
|
.array(z.object({
|
|
@@ -2271,6 +2444,11 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2271
2444
|
acknowledge_tag_subtree: z.boolean().optional(),
|
|
2272
2445
|
allow_non_empty_area: z.boolean().optional(),
|
|
2273
2446
|
dangerously_drive_gui: z.boolean().optional(),
|
|
2447
|
+
preserve_modified: z
|
|
2448
|
+
.boolean()
|
|
2449
|
+
.optional()
|
|
2450
|
+
.describe("Keep THIS operation off the modification-date timeline; overrides the " +
|
|
2451
|
+
"run-level preserve_modified (false opts one operation back on)"),
|
|
2274
2452
|
})
|
|
2275
2453
|
.optional(),
|
|
2276
2454
|
}))
|
|
@@ -2297,6 +2475,12 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2297
2475
|
...(o?.acknowledge_tag_subtree === true && { acknowledgeTagSubtree: true }),
|
|
2298
2476
|
...(o?.allow_non_empty_area === true && { allowNonEmptyArea: true }),
|
|
2299
2477
|
...(o?.dangerously_drive_gui === true && { dangerouslyDriveGui: true }),
|
|
2478
|
+
// Per-op override of the run-level preserve_modified; passed through
|
|
2479
|
+
// when EXPLICIT (including false, which opts one op back onto the
|
|
2480
|
+
// timeline) so the batch engine's `??` precedence sees it.
|
|
2481
|
+
...(o?.preserve_modified !== undefined && {
|
|
2482
|
+
preserveModified: o.preserve_modified,
|
|
2483
|
+
}),
|
|
2300
2484
|
...(ceiling !== undefined && { maxDisruption: ceiling }),
|
|
2301
2485
|
};
|
|
2302
2486
|
return {
|
|
@@ -2310,6 +2494,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2310
2494
|
const batchResult = await getClient().write.batch(ops, {
|
|
2311
2495
|
...(args.dry_run === true && { dryRun: true }),
|
|
2312
2496
|
...(args.continue_on_error === true && { continueOnError: true }),
|
|
2497
|
+
...(args.preserve_modified === true && { preserveModified: true }),
|
|
2313
2498
|
actor: mcpActor(),
|
|
2314
2499
|
});
|
|
2315
2500
|
// First block: the per-op results, each FLATTENED to the wire shape (a
|
|
@@ -2340,7 +2525,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2340
2525
|
],
|
|
2341
2526
|
};
|
|
2342
2527
|
}));
|
|
2343
|
-
|
|
2528
|
+
registerTool("reorder", {
|
|
2344
2529
|
description: "The ONE reorder tool — rearrange a single-KIND set IN PLACE: to-dos, projects, headings, " +
|
|
2345
2530
|
"OR sidebar areas. This REARRANGES, never changes what an item belongs to (to change " +
|
|
2346
2531
|
"membership use move_todo / move_project). All refs must be one kind; only to-dos and " +
|
|
@@ -2390,7 +2575,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2390
2575
|
};
|
|
2391
2576
|
return moveResult(await getClient().write.reorderAny(request, writeOptions(args)));
|
|
2392
2577
|
}));
|
|
2393
|
-
|
|
2578
|
+
registerTool("undo", {
|
|
2394
2579
|
description: "Undo the last N changes, newest first (changes made directly in the Things app " +
|
|
2395
2580
|
"cannot be undone here). By default this undoes only changes made through THIS " +
|
|
2396
2581
|
"connection — this client's own writes; it will not touch the user's own edits, or " +
|
|
@@ -2453,7 +2638,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2453
2638
|
});
|
|
2454
2639
|
return jsonResult(items);
|
|
2455
2640
|
}));
|
|
2456
|
-
|
|
2641
|
+
registerTool("capabilities", {
|
|
2457
2642
|
description: "Support reference for every operation kind usable with run_operation and batch: " +
|
|
2458
2643
|
"whether it is available, its caveats, and the confirmation parameters it needs.",
|
|
2459
2644
|
inputSchema: {
|
|
@@ -2464,7 +2649,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2464
2649
|
},
|
|
2465
2650
|
annotations: READ_ONLY,
|
|
2466
2651
|
}, async (args) => guard(() => jsonResult(capabilitiesTable(args.op))));
|
|
2467
|
-
|
|
2652
|
+
registerTool("doctor", {
|
|
2468
2653
|
description: "Check the environment: whether the Things app and its database are reachable, " +
|
|
2469
2654
|
"whether changes can be made, any one-time setup still needed (macOS permissions, " +
|
|
2470
2655
|
"the app's 'Enable Things URLs' setting), whether the environment changed since " +
|
|
@@ -2488,7 +2673,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2488
2673
|
? jsonResult(report)
|
|
2489
2674
|
: errorResult(error ?? { code: "unexpected", message: "no report" });
|
|
2490
2675
|
}));
|
|
2491
|
-
|
|
2676
|
+
registerTool("op_result", {
|
|
2492
2677
|
description: "Look up what happened to a write you dispatched with op_id, from the local change history " +
|
|
2493
2678
|
"alone (opens no database, changes nothing). Use it to recover the outcome when the write " +
|
|
2494
2679
|
"call was interrupted before it returned: report FOUND (the final result + target + " +
|