things-app-mcp 1.0.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 +216 -0
- package/dist/applescript.d.ts +89 -0
- package/dist/applescript.d.ts.map +1 -0
- package/dist/applescript.js +343 -0
- package/dist/applescript.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +897 -0
- package/dist/index.js.map +1 -0
- package/dist/things-url.d.ts +183 -0
- package/dist/things-url.d.ts.map +1 -0
- package/dist/things-url.js +223 -0
- package/dist/things-url.js.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Things MCP Server
|
|
4
|
+
*
|
|
5
|
+
* An MCP server that provides tools and resources for interacting with
|
|
6
|
+
* Things 3 (macOS) via the Things URL Scheme and AppleScript/JXA.
|
|
7
|
+
*
|
|
8
|
+
* Tools (write operations via URL scheme):
|
|
9
|
+
* - add-todo: Create a new to-do
|
|
10
|
+
* - add-project: Create a new project
|
|
11
|
+
* - update-todo: Update an existing to-do (requires auth-token)
|
|
12
|
+
* - update-project: Update an existing project (requires auth-token)
|
|
13
|
+
* - show: Navigate to a list, project, area, tag, or to-do
|
|
14
|
+
* - search: Open the search screen in Things
|
|
15
|
+
* - add-json: Create complex projects/to-dos via JSON
|
|
16
|
+
*
|
|
17
|
+
* Tools (read operations via AppleScript/JXA):
|
|
18
|
+
* - get-todos: Get to-dos from a list, project, area, or by tag
|
|
19
|
+
* - get-projects: Get all projects
|
|
20
|
+
* - get-areas: Get all areas
|
|
21
|
+
* - get-tags: Get all tags
|
|
22
|
+
* - get-todo-by-id: Get a specific to-do by ID
|
|
23
|
+
* - get-project-by-id: Get a specific project by ID
|
|
24
|
+
* - search-todos: Search to-dos by title/notes content
|
|
25
|
+
* - get-recent-todos: Get recently modified to-dos
|
|
26
|
+
*/
|
|
27
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
28
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
import { buildAddTodoURL, buildAddProjectURL, buildUpdateTodoURL, buildUpdateProjectURL, buildShowURL, buildSearchURL, buildJsonURL, } from "./things-url.js";
|
|
31
|
+
import { openThingsURL, getTodosFromList, getTodosFromProject, getTodosFromArea, getTodoById, getProjects, getProjectById, getAreas, getTags, getTodosByTag, getRecentTodos, searchTodosByTitle, } from "./applescript.js";
|
|
32
|
+
// --------------------------------------------------------------------------
|
|
33
|
+
// Server Setup
|
|
34
|
+
// --------------------------------------------------------------------------
|
|
35
|
+
const server = new McpServer({
|
|
36
|
+
name: "things-app-mcp",
|
|
37
|
+
version: "1.0.0",
|
|
38
|
+
}, {
|
|
39
|
+
capabilities: {
|
|
40
|
+
tools: {},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
// --------------------------------------------------------------------------
|
|
44
|
+
// URL Scheme Tools (Write Operations)
|
|
45
|
+
// --------------------------------------------------------------------------
|
|
46
|
+
server.registerTool("add-todo", {
|
|
47
|
+
title: "Add To-Do",
|
|
48
|
+
description: "Create a new to-do in Things. Supports setting title, notes, when/deadline dates, tags, checklist items, and assigning to projects/areas. Uses the Things URL scheme.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
title: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Title of the to-do"),
|
|
54
|
+
titles: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Multiple to-do titles separated by newlines (takes priority over title)"),
|
|
58
|
+
notes: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Notes for the to-do (max 10,000 chars)"),
|
|
62
|
+
when: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("When to schedule: today, tomorrow, evening, anytime, someday, YYYY-MM-DD, or YYYY-MM-DD@HH:MM for a reminder"),
|
|
66
|
+
deadline: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Deadline date: YYYY-MM-DD or natural language like 'next friday'"),
|
|
70
|
+
tags: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Comma-separated tag names (must already exist in Things)"),
|
|
74
|
+
checklistItems: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Checklist items separated by newlines (max 100)"),
|
|
78
|
+
listId: z
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("ID of a project or area to add to (takes precedence over list)"),
|
|
82
|
+
list: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Title of a project or area to add to"),
|
|
86
|
+
headingId: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe("ID of a heading within a project"),
|
|
90
|
+
heading: z
|
|
91
|
+
.string()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe("Title of a heading within a project"),
|
|
94
|
+
completed: z
|
|
95
|
+
.boolean()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe("Set to true to mark as completed"),
|
|
98
|
+
canceled: z
|
|
99
|
+
.boolean()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe("Set to true to mark as canceled (takes priority over completed)"),
|
|
102
|
+
showQuickEntry: z
|
|
103
|
+
.boolean()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe("Show the quick entry dialog instead of adding directly"),
|
|
106
|
+
reveal: z
|
|
107
|
+
.boolean()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe("Navigate to and show the newly created to-do"),
|
|
110
|
+
creationDate: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("Creation date in ISO8601 format"),
|
|
114
|
+
completionDate: z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Completion date in ISO8601 format"),
|
|
118
|
+
},
|
|
119
|
+
annotations: {
|
|
120
|
+
title: "Add To-Do",
|
|
121
|
+
openWorldHint: true,
|
|
122
|
+
},
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
const url = buildAddTodoURL({
|
|
125
|
+
title: args.title,
|
|
126
|
+
titles: args.titles,
|
|
127
|
+
notes: args.notes,
|
|
128
|
+
when: args.when,
|
|
129
|
+
deadline: args.deadline,
|
|
130
|
+
tags: args.tags,
|
|
131
|
+
"checklist-items": args.checklistItems,
|
|
132
|
+
"list-id": args.listId,
|
|
133
|
+
list: args.list,
|
|
134
|
+
"heading-id": args.headingId,
|
|
135
|
+
heading: args.heading,
|
|
136
|
+
completed: args.completed,
|
|
137
|
+
canceled: args.canceled,
|
|
138
|
+
"show-quick-entry": args.showQuickEntry,
|
|
139
|
+
reveal: args.reveal,
|
|
140
|
+
"creation-date": args.creationDate,
|
|
141
|
+
"completion-date": args.completionDate,
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
const result = await openThingsURL(url);
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: `${result}\n\nURL: ${url}`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: `Error creating to-do: ${error instanceof Error ? error.message : String(error)}\n\nAttempted URL: ${url}`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
isError: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
server.registerTool("add-project", {
|
|
167
|
+
title: "Add Project",
|
|
168
|
+
description: "Create a new project in Things. Supports setting title, notes, when/deadline dates, tags, area assignment, and initial to-dos.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
title: z
|
|
171
|
+
.string()
|
|
172
|
+
.optional()
|
|
173
|
+
.describe("Title of the project"),
|
|
174
|
+
notes: z
|
|
175
|
+
.string()
|
|
176
|
+
.optional()
|
|
177
|
+
.describe("Notes for the project (max 10,000 chars)"),
|
|
178
|
+
when: z
|
|
179
|
+
.string()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("When to schedule: today, tomorrow, evening, anytime, someday, YYYY-MM-DD, or YYYY-MM-DD@HH:MM"),
|
|
182
|
+
deadline: z
|
|
183
|
+
.string()
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Deadline date: YYYY-MM-DD or natural language"),
|
|
186
|
+
tags: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Comma-separated tag names"),
|
|
190
|
+
areaId: z
|
|
191
|
+
.string()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("ID of an area to add to (takes precedence over area)"),
|
|
194
|
+
area: z
|
|
195
|
+
.string()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Title of an area to add to"),
|
|
198
|
+
todos: z
|
|
199
|
+
.string()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("To-do titles separated by newlines to create inside the project"),
|
|
202
|
+
completed: z.boolean().optional().describe("Set to true to mark as completed"),
|
|
203
|
+
canceled: z.boolean().optional().describe("Set to true to mark as canceled"),
|
|
204
|
+
reveal: z
|
|
205
|
+
.boolean()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Navigate into the newly created project"),
|
|
208
|
+
creationDate: z.string().optional().describe("Creation date in ISO8601 format"),
|
|
209
|
+
completionDate: z.string().optional().describe("Completion date in ISO8601 format"),
|
|
210
|
+
},
|
|
211
|
+
annotations: {
|
|
212
|
+
title: "Add Project",
|
|
213
|
+
openWorldHint: true,
|
|
214
|
+
},
|
|
215
|
+
}, async (args) => {
|
|
216
|
+
const url = buildAddProjectURL({
|
|
217
|
+
title: args.title,
|
|
218
|
+
notes: args.notes,
|
|
219
|
+
when: args.when,
|
|
220
|
+
deadline: args.deadline,
|
|
221
|
+
tags: args.tags,
|
|
222
|
+
"area-id": args.areaId,
|
|
223
|
+
area: args.area,
|
|
224
|
+
"to-dos": args.todos,
|
|
225
|
+
completed: args.completed,
|
|
226
|
+
canceled: args.canceled,
|
|
227
|
+
reveal: args.reveal,
|
|
228
|
+
"creation-date": args.creationDate,
|
|
229
|
+
"completion-date": args.completionDate,
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
const result = await openThingsURL(url);
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: `${result}\n\nURL: ${url}` }],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: `Error creating project: ${error instanceof Error ? error.message : String(error)}\n\nAttempted URL: ${url}`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
isError: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
server.registerTool("update-todo", {
|
|
250
|
+
title: "Update To-Do",
|
|
251
|
+
description: "Update an existing to-do in Things. Requires the to-do ID and your Things auth-token. Supports changing title, notes, dates, tags, checklist, list assignment, and status.",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
authToken: z
|
|
254
|
+
.string()
|
|
255
|
+
.describe("Things URL scheme authorization token (find in Things Settings > General > Things URLs)"),
|
|
256
|
+
id: z.string().describe("ID of the to-do to update"),
|
|
257
|
+
title: z.string().optional().describe("New title"),
|
|
258
|
+
notes: z
|
|
259
|
+
.string()
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("Replace notes (pass empty string to clear)"),
|
|
262
|
+
prependNotes: z.string().optional().describe("Text to prepend to existing notes"),
|
|
263
|
+
appendNotes: z.string().optional().describe("Text to append to existing notes"),
|
|
264
|
+
when: z
|
|
265
|
+
.string()
|
|
266
|
+
.optional()
|
|
267
|
+
.describe("When to schedule: today, tomorrow, evening, someday, YYYY-MM-DD, or YYYY-MM-DD@HH:MM"),
|
|
268
|
+
deadline: z
|
|
269
|
+
.string()
|
|
270
|
+
.optional()
|
|
271
|
+
.describe("Deadline date (pass empty string to clear)"),
|
|
272
|
+
tags: z
|
|
273
|
+
.string()
|
|
274
|
+
.optional()
|
|
275
|
+
.describe("Comma-separated tags to replace all current tags"),
|
|
276
|
+
addTags: z
|
|
277
|
+
.string()
|
|
278
|
+
.optional()
|
|
279
|
+
.describe("Comma-separated tags to add to existing tags"),
|
|
280
|
+
checklistItems: z
|
|
281
|
+
.string()
|
|
282
|
+
.optional()
|
|
283
|
+
.describe("Newline-separated checklist items to replace all existing"),
|
|
284
|
+
prependChecklistItems: z
|
|
285
|
+
.string()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe("Newline-separated checklist items to prepend"),
|
|
288
|
+
appendChecklistItems: z
|
|
289
|
+
.string()
|
|
290
|
+
.optional()
|
|
291
|
+
.describe("Newline-separated checklist items to append"),
|
|
292
|
+
listId: z
|
|
293
|
+
.string()
|
|
294
|
+
.optional()
|
|
295
|
+
.describe("ID of project or area to move to"),
|
|
296
|
+
list: z
|
|
297
|
+
.string()
|
|
298
|
+
.optional()
|
|
299
|
+
.describe("Title of project or area to move to"),
|
|
300
|
+
headingId: z.string().optional().describe("ID of heading within project"),
|
|
301
|
+
heading: z.string().optional().describe("Title of heading within project"),
|
|
302
|
+
completed: z.boolean().optional().describe("Set completion status"),
|
|
303
|
+
canceled: z.boolean().optional().describe("Set canceled status"),
|
|
304
|
+
reveal: z.boolean().optional().describe("Navigate to the updated to-do"),
|
|
305
|
+
duplicate: z
|
|
306
|
+
.boolean()
|
|
307
|
+
.optional()
|
|
308
|
+
.describe("Duplicate the to-do before updating"),
|
|
309
|
+
creationDate: z.string().optional().describe("Creation date in ISO8601 format"),
|
|
310
|
+
completionDate: z.string().optional().describe("Completion date in ISO8601 format"),
|
|
311
|
+
},
|
|
312
|
+
annotations: {
|
|
313
|
+
title: "Update To-Do",
|
|
314
|
+
openWorldHint: true,
|
|
315
|
+
},
|
|
316
|
+
}, async (args) => {
|
|
317
|
+
const url = buildUpdateTodoURL({
|
|
318
|
+
"auth-token": args.authToken,
|
|
319
|
+
id: args.id,
|
|
320
|
+
title: args.title,
|
|
321
|
+
notes: args.notes,
|
|
322
|
+
"prepend-notes": args.prependNotes,
|
|
323
|
+
"append-notes": args.appendNotes,
|
|
324
|
+
when: args.when,
|
|
325
|
+
deadline: args.deadline,
|
|
326
|
+
tags: args.tags,
|
|
327
|
+
"add-tags": args.addTags,
|
|
328
|
+
"checklist-items": args.checklistItems,
|
|
329
|
+
"prepend-checklist-items": args.prependChecklistItems,
|
|
330
|
+
"append-checklist-items": args.appendChecklistItems,
|
|
331
|
+
"list-id": args.listId,
|
|
332
|
+
list: args.list,
|
|
333
|
+
"heading-id": args.headingId,
|
|
334
|
+
heading: args.heading,
|
|
335
|
+
completed: args.completed,
|
|
336
|
+
canceled: args.canceled,
|
|
337
|
+
reveal: args.reveal,
|
|
338
|
+
duplicate: args.duplicate,
|
|
339
|
+
"creation-date": args.creationDate,
|
|
340
|
+
"completion-date": args.completionDate,
|
|
341
|
+
});
|
|
342
|
+
try {
|
|
343
|
+
const result = await openThingsURL(url);
|
|
344
|
+
return {
|
|
345
|
+
content: [{ type: "text", text: `${result}\n\nURL: ${url}` }],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{
|
|
352
|
+
type: "text",
|
|
353
|
+
text: `Error updating to-do: ${error instanceof Error ? error.message : String(error)}\n\nAttempted URL: ${url}`,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
isError: true,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
server.registerTool("update-project", {
|
|
361
|
+
title: "Update Project",
|
|
362
|
+
description: "Update an existing project in Things. Requires the project ID and your Things auth-token.",
|
|
363
|
+
inputSchema: {
|
|
364
|
+
authToken: z
|
|
365
|
+
.string()
|
|
366
|
+
.describe("Things URL scheme authorization token"),
|
|
367
|
+
id: z.string().describe("ID of the project to update"),
|
|
368
|
+
title: z.string().optional().describe("New title"),
|
|
369
|
+
notes: z.string().optional().describe("Replace notes"),
|
|
370
|
+
prependNotes: z.string().optional().describe("Text to prepend to existing notes"),
|
|
371
|
+
appendNotes: z.string().optional().describe("Text to append to existing notes"),
|
|
372
|
+
when: z.string().optional().describe("When to schedule"),
|
|
373
|
+
deadline: z.string().optional().describe("Deadline date"),
|
|
374
|
+
tags: z.string().optional().describe("Replace all tags"),
|
|
375
|
+
addTags: z.string().optional().describe("Add tags"),
|
|
376
|
+
areaId: z.string().optional().describe("ID of area to move to"),
|
|
377
|
+
area: z.string().optional().describe("Title of area to move to"),
|
|
378
|
+
completed: z.boolean().optional().describe("Set completion status"),
|
|
379
|
+
canceled: z.boolean().optional().describe("Set canceled status"),
|
|
380
|
+
reveal: z.boolean().optional().describe("Navigate to the project"),
|
|
381
|
+
duplicate: z.boolean().optional().describe("Duplicate before updating"),
|
|
382
|
+
creationDate: z.string().optional().describe("Creation date in ISO8601"),
|
|
383
|
+
completionDate: z.string().optional().describe("Completion date in ISO8601"),
|
|
384
|
+
},
|
|
385
|
+
annotations: {
|
|
386
|
+
title: "Update Project",
|
|
387
|
+
openWorldHint: true,
|
|
388
|
+
},
|
|
389
|
+
}, async (args) => {
|
|
390
|
+
const url = buildUpdateProjectURL({
|
|
391
|
+
"auth-token": args.authToken,
|
|
392
|
+
id: args.id,
|
|
393
|
+
title: args.title,
|
|
394
|
+
notes: args.notes,
|
|
395
|
+
"prepend-notes": args.prependNotes,
|
|
396
|
+
"append-notes": args.appendNotes,
|
|
397
|
+
when: args.when,
|
|
398
|
+
deadline: args.deadline,
|
|
399
|
+
tags: args.tags,
|
|
400
|
+
"add-tags": args.addTags,
|
|
401
|
+
"area-id": args.areaId,
|
|
402
|
+
area: args.area,
|
|
403
|
+
completed: args.completed,
|
|
404
|
+
canceled: args.canceled,
|
|
405
|
+
reveal: args.reveal,
|
|
406
|
+
duplicate: args.duplicate,
|
|
407
|
+
"creation-date": args.creationDate,
|
|
408
|
+
"completion-date": args.completionDate,
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
const result = await openThingsURL(url);
|
|
412
|
+
return {
|
|
413
|
+
content: [{ type: "text", text: `${result}\n\nURL: ${url}` }],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return {
|
|
418
|
+
content: [
|
|
419
|
+
{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: `Error updating project: ${error instanceof Error ? error.message : String(error)}\n\nAttempted URL: ${url}`,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
isError: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
server.registerTool("show", {
|
|
429
|
+
title: "Show in Things",
|
|
430
|
+
description: "Navigate to and show a list, project, area, tag, or to-do in Things. Built-in list IDs: inbox, today, anytime, upcoming, someday, logbook, tomorrow, deadlines, repeating, all-projects, logged-projects.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
id: z
|
|
433
|
+
.string()
|
|
434
|
+
.optional()
|
|
435
|
+
.describe("ID of item to show, or a built-in list ID (inbox, today, anytime, upcoming, someday, logbook, tomorrow, deadlines, repeating, all-projects, logged-projects)"),
|
|
436
|
+
query: z
|
|
437
|
+
.string()
|
|
438
|
+
.optional()
|
|
439
|
+
.describe("Name of an area, project, tag, or built-in list to show (ignored if id is set)"),
|
|
440
|
+
filter: z
|
|
441
|
+
.string()
|
|
442
|
+
.optional()
|
|
443
|
+
.describe("Comma-separated tag names to filter the list by"),
|
|
444
|
+
},
|
|
445
|
+
annotations: {
|
|
446
|
+
title: "Show in Things",
|
|
447
|
+
readOnlyHint: true,
|
|
448
|
+
openWorldHint: true,
|
|
449
|
+
},
|
|
450
|
+
}, async (args) => {
|
|
451
|
+
const url = buildShowURL({
|
|
452
|
+
id: args.id,
|
|
453
|
+
query: args.query,
|
|
454
|
+
filter: args.filter,
|
|
455
|
+
});
|
|
456
|
+
try {
|
|
457
|
+
const result = await openThingsURL(url);
|
|
458
|
+
return {
|
|
459
|
+
content: [{ type: "text", text: `${result}\n\nURL: ${url}` }],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
content: [
|
|
465
|
+
{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: `Error showing in Things: ${error instanceof Error ? error.message : String(error)}\n\nAttempted URL: ${url}`,
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
isError: true,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
server.registerTool("search", {
|
|
475
|
+
title: "Search in Things",
|
|
476
|
+
description: "Open the search screen in Things with an optional search query.",
|
|
477
|
+
inputSchema: {
|
|
478
|
+
query: z.string().optional().describe("Search query text"),
|
|
479
|
+
},
|
|
480
|
+
annotations: {
|
|
481
|
+
title: "Search in Things",
|
|
482
|
+
readOnlyHint: true,
|
|
483
|
+
openWorldHint: true,
|
|
484
|
+
},
|
|
485
|
+
}, async (args) => {
|
|
486
|
+
const url = buildSearchURL({ query: args.query });
|
|
487
|
+
try {
|
|
488
|
+
const result = await openThingsURL(url);
|
|
489
|
+
return {
|
|
490
|
+
content: [{ type: "text", text: `${result}\n\nURL: ${url}` }],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
return {
|
|
495
|
+
content: [
|
|
496
|
+
{
|
|
497
|
+
type: "text",
|
|
498
|
+
text: `Error searching in Things: ${error instanceof Error ? error.message : String(error)}\n\nAttempted URL: ${url}`,
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
isError: true,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
server.registerTool("add-json", {
|
|
506
|
+
title: "Add via JSON",
|
|
507
|
+
description: 'Create complex projects and to-dos using the Things JSON command. Supports nested projects with headings, checklist items, and to-dos. The data should be an array of objects with "type" (to-do, project, heading, checklist-item) and "attributes" fields. For updates, include "operation": "update" and "id" fields, and provide auth-token.',
|
|
508
|
+
inputSchema: {
|
|
509
|
+
data: z
|
|
510
|
+
.string()
|
|
511
|
+
.describe("JSON string containing an array of Things objects. Each object has 'type' (to-do/project/heading/checklist-item), optional 'operation' (create/update), optional 'id' (for updates), and 'attributes' (title, notes, when, deadline, tags, items, etc.)"),
|
|
512
|
+
authToken: z
|
|
513
|
+
.string()
|
|
514
|
+
.optional()
|
|
515
|
+
.describe("Things auth-token (required when data contains update operations)"),
|
|
516
|
+
reveal: z
|
|
517
|
+
.boolean()
|
|
518
|
+
.optional()
|
|
519
|
+
.describe("Navigate to the first created item"),
|
|
520
|
+
},
|
|
521
|
+
annotations: {
|
|
522
|
+
title: "Add via JSON",
|
|
523
|
+
openWorldHint: true,
|
|
524
|
+
},
|
|
525
|
+
}, async (args) => {
|
|
526
|
+
let parsedData;
|
|
527
|
+
try {
|
|
528
|
+
parsedData = JSON.parse(args.data);
|
|
529
|
+
if (!Array.isArray(parsedData)) {
|
|
530
|
+
return {
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "text",
|
|
534
|
+
text: "Error: data must be a JSON array of Things objects",
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
isError: true,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return {
|
|
543
|
+
content: [
|
|
544
|
+
{
|
|
545
|
+
type: "text",
|
|
546
|
+
text: "Error: Invalid JSON string. The data must be a valid JSON array.",
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
isError: true,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const url = buildJsonURL({
|
|
553
|
+
data: parsedData,
|
|
554
|
+
"auth-token": args.authToken,
|
|
555
|
+
reveal: args.reveal,
|
|
556
|
+
});
|
|
557
|
+
try {
|
|
558
|
+
const result = await openThingsURL(url);
|
|
559
|
+
return {
|
|
560
|
+
content: [
|
|
561
|
+
{
|
|
562
|
+
type: "text",
|
|
563
|
+
text: `${result}\n\nURL: ${url}`,
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
return {
|
|
570
|
+
content: [
|
|
571
|
+
{
|
|
572
|
+
type: "text",
|
|
573
|
+
text: `Error executing JSON command: ${error instanceof Error ? error.message : String(error)}\n\nAttempted URL: ${url}`,
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
isError: true,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
// --------------------------------------------------------------------------
|
|
581
|
+
// AppleScript/JXA Tools (Read Operations)
|
|
582
|
+
// --------------------------------------------------------------------------
|
|
583
|
+
server.registerTool("get-todos", {
|
|
584
|
+
title: "Get To-Dos",
|
|
585
|
+
description: "Get to-dos from Things by list, project, area, or tag. Specify exactly one source. Uses AppleScript (macOS only).",
|
|
586
|
+
inputSchema: {
|
|
587
|
+
list: z
|
|
588
|
+
.string()
|
|
589
|
+
.optional()
|
|
590
|
+
.describe("Built-in list name: Inbox, Today, Anytime, Upcoming, Someday, Logbook"),
|
|
591
|
+
project: z
|
|
592
|
+
.string()
|
|
593
|
+
.optional()
|
|
594
|
+
.describe("Project name to get to-dos from"),
|
|
595
|
+
area: z
|
|
596
|
+
.string()
|
|
597
|
+
.optional()
|
|
598
|
+
.describe("Area name to get to-dos from"),
|
|
599
|
+
tag: z
|
|
600
|
+
.string()
|
|
601
|
+
.optional()
|
|
602
|
+
.describe("Tag name to filter to-dos by"),
|
|
603
|
+
},
|
|
604
|
+
annotations: {
|
|
605
|
+
title: "Get To-Dos",
|
|
606
|
+
readOnlyHint: true,
|
|
607
|
+
},
|
|
608
|
+
}, async (args) => {
|
|
609
|
+
try {
|
|
610
|
+
let todos;
|
|
611
|
+
if (args.list) {
|
|
612
|
+
todos = await getTodosFromList(args.list);
|
|
613
|
+
}
|
|
614
|
+
else if (args.project) {
|
|
615
|
+
todos = await getTodosFromProject(args.project);
|
|
616
|
+
}
|
|
617
|
+
else if (args.area) {
|
|
618
|
+
todos = await getTodosFromArea(args.area);
|
|
619
|
+
}
|
|
620
|
+
else if (args.tag) {
|
|
621
|
+
todos = await getTodosByTag(args.tag);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
// Default to Today
|
|
625
|
+
todos = await getTodosFromList("Today");
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
content: [
|
|
629
|
+
{
|
|
630
|
+
type: "text",
|
|
631
|
+
text: JSON.stringify(todos, null, 2),
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
return {
|
|
638
|
+
content: [
|
|
639
|
+
{
|
|
640
|
+
type: "text",
|
|
641
|
+
text: `Error getting to-dos: ${error instanceof Error ? error.message : String(error)}`,
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
isError: true,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
server.registerTool("get-todo-by-id", {
|
|
649
|
+
title: "Get To-Do by ID",
|
|
650
|
+
description: "Get a specific to-do by its ID. Uses AppleScript (macOS only).",
|
|
651
|
+
inputSchema: {
|
|
652
|
+
id: z.string().describe("The ID of the to-do to retrieve"),
|
|
653
|
+
},
|
|
654
|
+
annotations: {
|
|
655
|
+
title: "Get To-Do by ID",
|
|
656
|
+
readOnlyHint: true,
|
|
657
|
+
},
|
|
658
|
+
}, async (args) => {
|
|
659
|
+
try {
|
|
660
|
+
const todo = await getTodoById(args.id);
|
|
661
|
+
return {
|
|
662
|
+
content: [
|
|
663
|
+
{
|
|
664
|
+
type: "text",
|
|
665
|
+
text: JSON.stringify(todo, null, 2),
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
return {
|
|
672
|
+
content: [
|
|
673
|
+
{
|
|
674
|
+
type: "text",
|
|
675
|
+
text: `Error getting to-do: ${error instanceof Error ? error.message : String(error)}`,
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
isError: true,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
server.registerTool("get-projects", {
|
|
683
|
+
title: "Get Projects",
|
|
684
|
+
description: "Get all projects from Things. Uses AppleScript (macOS only).",
|
|
685
|
+
inputSchema: {},
|
|
686
|
+
annotations: {
|
|
687
|
+
title: "Get Projects",
|
|
688
|
+
readOnlyHint: true,
|
|
689
|
+
},
|
|
690
|
+
}, async () => {
|
|
691
|
+
try {
|
|
692
|
+
const projects = await getProjects();
|
|
693
|
+
return {
|
|
694
|
+
content: [
|
|
695
|
+
{
|
|
696
|
+
type: "text",
|
|
697
|
+
text: JSON.stringify(projects, null, 2),
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
return {
|
|
704
|
+
content: [
|
|
705
|
+
{
|
|
706
|
+
type: "text",
|
|
707
|
+
text: `Error getting projects: ${error instanceof Error ? error.message : String(error)}`,
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
isError: true,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
server.registerTool("get-project-by-id", {
|
|
715
|
+
title: "Get Project by ID",
|
|
716
|
+
description: "Get a specific project by its ID. Uses AppleScript (macOS only).",
|
|
717
|
+
inputSchema: {
|
|
718
|
+
id: z.string().describe("The ID of the project to retrieve"),
|
|
719
|
+
},
|
|
720
|
+
annotations: {
|
|
721
|
+
title: "Get Project by ID",
|
|
722
|
+
readOnlyHint: true,
|
|
723
|
+
},
|
|
724
|
+
}, async (args) => {
|
|
725
|
+
try {
|
|
726
|
+
const project = await getProjectById(args.id);
|
|
727
|
+
return {
|
|
728
|
+
content: [
|
|
729
|
+
{
|
|
730
|
+
type: "text",
|
|
731
|
+
text: JSON.stringify(project, null, 2),
|
|
732
|
+
},
|
|
733
|
+
],
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
return {
|
|
738
|
+
content: [
|
|
739
|
+
{
|
|
740
|
+
type: "text",
|
|
741
|
+
text: `Error getting project: ${error instanceof Error ? error.message : String(error)}`,
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
isError: true,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
server.registerTool("get-areas", {
|
|
749
|
+
title: "Get Areas",
|
|
750
|
+
description: "Get all areas from Things. Uses AppleScript (macOS only).",
|
|
751
|
+
inputSchema: {},
|
|
752
|
+
annotations: {
|
|
753
|
+
title: "Get Areas",
|
|
754
|
+
readOnlyHint: true,
|
|
755
|
+
},
|
|
756
|
+
}, async () => {
|
|
757
|
+
try {
|
|
758
|
+
const areas = await getAreas();
|
|
759
|
+
return {
|
|
760
|
+
content: [
|
|
761
|
+
{
|
|
762
|
+
type: "text",
|
|
763
|
+
text: JSON.stringify(areas, null, 2),
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
return {
|
|
770
|
+
content: [
|
|
771
|
+
{
|
|
772
|
+
type: "text",
|
|
773
|
+
text: `Error getting areas: ${error instanceof Error ? error.message : String(error)}`,
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
isError: true,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
server.registerTool("get-tags", {
|
|
781
|
+
title: "Get Tags",
|
|
782
|
+
description: "Get all tags from Things. Uses AppleScript (macOS only).",
|
|
783
|
+
inputSchema: {},
|
|
784
|
+
annotations: {
|
|
785
|
+
title: "Get Tags",
|
|
786
|
+
readOnlyHint: true,
|
|
787
|
+
},
|
|
788
|
+
}, async () => {
|
|
789
|
+
try {
|
|
790
|
+
const tags = await getTags();
|
|
791
|
+
return {
|
|
792
|
+
content: [
|
|
793
|
+
{
|
|
794
|
+
type: "text",
|
|
795
|
+
text: JSON.stringify(tags, null, 2),
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
return {
|
|
802
|
+
content: [
|
|
803
|
+
{
|
|
804
|
+
type: "text",
|
|
805
|
+
text: `Error getting tags: ${error instanceof Error ? error.message : String(error)}`,
|
|
806
|
+
},
|
|
807
|
+
],
|
|
808
|
+
isError: true,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
server.registerTool("search-todos", {
|
|
813
|
+
title: "Search To-Dos",
|
|
814
|
+
description: "Search for to-dos by title or notes content. Uses AppleScript (macOS only).",
|
|
815
|
+
inputSchema: {
|
|
816
|
+
query: z
|
|
817
|
+
.string()
|
|
818
|
+
.describe("Search query to match against to-do titles and notes"),
|
|
819
|
+
},
|
|
820
|
+
annotations: {
|
|
821
|
+
title: "Search To-Dos",
|
|
822
|
+
readOnlyHint: true,
|
|
823
|
+
},
|
|
824
|
+
}, async (args) => {
|
|
825
|
+
try {
|
|
826
|
+
const todos = await searchTodosByTitle(args.query);
|
|
827
|
+
return {
|
|
828
|
+
content: [
|
|
829
|
+
{
|
|
830
|
+
type: "text",
|
|
831
|
+
text: JSON.stringify(todos, null, 2),
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
return {
|
|
838
|
+
content: [
|
|
839
|
+
{
|
|
840
|
+
type: "text",
|
|
841
|
+
text: `Error searching to-dos: ${error instanceof Error ? error.message : String(error)}`,
|
|
842
|
+
},
|
|
843
|
+
],
|
|
844
|
+
isError: true,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
server.registerTool("get-recent-todos", {
|
|
849
|
+
title: "Get Recent To-Dos",
|
|
850
|
+
description: "Get recently modified to-dos. Uses AppleScript (macOS only).",
|
|
851
|
+
inputSchema: {
|
|
852
|
+
days: z
|
|
853
|
+
.number()
|
|
854
|
+
.optional()
|
|
855
|
+
.describe("Number of days to look back (default: 7)"),
|
|
856
|
+
},
|
|
857
|
+
annotations: {
|
|
858
|
+
title: "Get Recent To-Dos",
|
|
859
|
+
readOnlyHint: true,
|
|
860
|
+
},
|
|
861
|
+
}, async (args) => {
|
|
862
|
+
try {
|
|
863
|
+
const todos = await getRecentTodos(args.days ?? 7);
|
|
864
|
+
return {
|
|
865
|
+
content: [
|
|
866
|
+
{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: JSON.stringify(todos, null, 2),
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
return {
|
|
875
|
+
content: [
|
|
876
|
+
{
|
|
877
|
+
type: "text",
|
|
878
|
+
text: `Error getting recent to-dos: ${error instanceof Error ? error.message : String(error)}`,
|
|
879
|
+
},
|
|
880
|
+
],
|
|
881
|
+
isError: true,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
// --------------------------------------------------------------------------
|
|
886
|
+
// Start Server
|
|
887
|
+
// --------------------------------------------------------------------------
|
|
888
|
+
async function main() {
|
|
889
|
+
const transport = new StdioServerTransport();
|
|
890
|
+
await server.connect(transport);
|
|
891
|
+
console.error("Things MCP server running on stdio");
|
|
892
|
+
}
|
|
893
|
+
main().catch((error) => {
|
|
894
|
+
console.error("Fatal error in main():", error);
|
|
895
|
+
process.exit(1);
|
|
896
|
+
});
|
|
897
|
+
//# sourceMappingURL=index.js.map
|