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 ADDED
@@ -0,0 +1,216 @@
1
+ # Things App MCP
2
+
3
+ An MCP (Model Context Protocol) server for [Things 3](https://culturedcode.com/things/) on macOS. Enables AI assistants like Claude to create, read, update, and manage your tasks directly in Things.
4
+
5
+ ## Features
6
+
7
+ ### Write Operations (Things URL Scheme)
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+ | `add-todo` | Create a new to-do with title, notes, dates, tags, checklist, project/area assignment |
12
+ | `add-project` | Create a new project with to-dos, notes, dates, tags, area assignment |
13
+ | `update-todo` | Update an existing to-do (requires auth-token) |
14
+ | `update-project` | Update an existing project (requires auth-token) |
15
+ | `show` | Navigate to a list, project, area, tag, or specific to-do |
16
+ | `search` | Open the Things search screen |
17
+ | `add-json` | Create complex structures via the Things JSON command |
18
+
19
+ ### Read Operations (AppleScript/JXA)
20
+
21
+ | Tool | Description |
22
+ |------|-------------|
23
+ | `get-todos` | Get to-dos from a list (Inbox, Today, etc.), project, area, or by tag |
24
+ | `get-todo-by-id` | Get a specific to-do by its ID |
25
+ | `get-projects` | Get all projects |
26
+ | `get-project-by-id` | Get a specific project by its ID |
27
+ | `get-areas` | Get all areas |
28
+ | `get-tags` | Get all tags |
29
+ | `search-todos` | Search to-dos by title/notes content |
30
+ | `get-recent-todos` | Get recently modified to-dos |
31
+
32
+ ## Requirements
33
+
34
+ - **macOS** (required for AppleScript/JXA and `open` command)
35
+ - **Things 3** installed
36
+ - **Node.js** >= 18
37
+ - **Things URL Scheme** enabled (Things > Settings > General > Enable Things URLs)
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ # Clone and build
43
+ git clone <repository-url>
44
+ cd things-app-mcp
45
+ npm install
46
+ npm run build
47
+ ```
48
+
49
+ Or install globally:
50
+
51
+ ```bash
52
+ npm install -g things-app-mcp
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ ### Claude Desktop
58
+
59
+ Add to your Claude Desktop configuration file:
60
+
61
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "things": {
67
+ "command": "node",
68
+ "args": ["/path/to/things-app-mcp/dist/index.js"]
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Cursor
75
+
76
+ Add to your Cursor MCP settings (`.cursor/mcp.json`):
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "things": {
82
+ "command": "node",
83
+ "args": ["/path/to/things-app-mcp/dist/index.js"]
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Auth Token (for update operations)
90
+
91
+ To use `update-todo` and `update-project`, you need your Things auth-token:
92
+
93
+ 1. Open Things on Mac
94
+ 2. Go to **Things > Settings > General > Enable Things URLs > Manage**
95
+ 3. Copy your authorization token
96
+ 4. Pass it as the `authToken` parameter when calling update tools
97
+
98
+ ## Usage Examples
99
+
100
+ ### Adding a To-Do
101
+
102
+ ```
103
+ "Add a to-do called 'Buy groceries' scheduled for today with tags 'Errand'"
104
+ ```
105
+
106
+ The AI will call `add-todo` with:
107
+ ```json
108
+ {
109
+ "title": "Buy groceries",
110
+ "when": "today",
111
+ "tags": "Errand"
112
+ }
113
+ ```
114
+
115
+ ### Creating a Project with To-Dos
116
+
117
+ ```
118
+ "Create a project called 'Launch Website' in the Work area with to-dos: Design mockups, Build frontend, Deploy"
119
+ ```
120
+
121
+ The AI will call `add-project` with:
122
+ ```json
123
+ {
124
+ "title": "Launch Website",
125
+ "area": "Work",
126
+ "todos": "Design mockups\nBuild frontend\nDeploy"
127
+ }
128
+ ```
129
+
130
+ ### Complex Project via JSON
131
+
132
+ ```
133
+ "Create a vacation planning project with headings for Travel, Accommodation, and Activities"
134
+ ```
135
+
136
+ The AI will call `add-json` with structured JSON data containing nested headings and to-dos.
137
+
138
+ ### Reading To-Dos
139
+
140
+ ```
141
+ "What's on my Today list?"
142
+ ```
143
+
144
+ The AI will call `get-todos` with `{ "list": "Today" }` and return the structured data.
145
+
146
+ ### Updating a To-Do
147
+
148
+ ```
149
+ "Mark the 'Buy groceries' todo as complete"
150
+ ```
151
+
152
+ The AI will first search/get the to-do to find its ID, then call `update-todo` with the auth-token.
153
+
154
+ ## Things URL Scheme Reference
155
+
156
+ This MCP server implements the full [Things URL Scheme v2](https://culturedcode.com/things/support/articles/2803573/):
157
+
158
+ ### Date Formats
159
+
160
+ | Format | Example | Description |
161
+ |--------|---------|-------------|
162
+ | Named | `today`, `tomorrow`, `evening`, `anytime`, `someday` | Built-in schedule options |
163
+ | Date | `2026-03-15` | Specific date |
164
+ | Date + Time | `2026-03-15@14:00` | Date with reminder |
165
+ | Natural language | `next friday`, `in 3 days` | English natural language (parsed by Things) |
166
+
167
+ ### Built-in List IDs (for `show` tool)
168
+
169
+ `inbox`, `today`, `anytime`, `upcoming`, `someday`, `logbook`, `tomorrow`, `deadlines`, `repeating`, `all-projects`, `logged-projects`
170
+
171
+ ### JSON Command Object Types
172
+
173
+ | Type | Description |
174
+ |------|-------------|
175
+ | `to-do` | A task with title, notes, when, deadline, tags, checklist-items |
176
+ | `project` | A project with title, notes, items (to-dos and headings) |
177
+ | `heading` | A section heading within a project |
178
+ | `checklist-item` | A checklist item within a to-do |
179
+
180
+ ## Architecture
181
+
182
+ ```
183
+ things-app-mcp/
184
+ src/
185
+ index.ts # MCP server entry point with all tool registrations
186
+ things-url.ts # Things URL scheme builder (URL construction)
187
+ applescript.ts # AppleScript/JXA executor (read operations)
188
+ dist/ # Compiled JavaScript output
189
+ package.json
190
+ tsconfig.json
191
+ ```
192
+
193
+ ### How It Works
194
+
195
+ - **Write operations** construct `things:///` URLs and open them via macOS `open` command. Things processes the URL and creates/updates items accordingly.
196
+ - **Read operations** use JXA (JavaScript for Automation) scripts executed via `osascript` to query the Things database directly and return structured JSON data.
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ # Install dependencies
202
+ npm install
203
+
204
+ # Build
205
+ npm run build
206
+
207
+ # Watch mode
208
+ npm run dev
209
+
210
+ # Run directly
211
+ npm start
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1,89 @@
1
+ /**
2
+ * AppleScript executor for reading data from Things 3.
3
+ * Uses `osascript` to run JXA (JavaScript for Automation) or AppleScript.
4
+ */
5
+ /**
6
+ * Opens a Things URL scheme link via `open` command on macOS.
7
+ */
8
+ export declare function openThingsURL(url: string): Promise<string>;
9
+ export interface ThingsTodo {
10
+ id: string;
11
+ name: string;
12
+ status: "open" | "completed" | "canceled";
13
+ notes: string;
14
+ tags: string;
15
+ dueDate: string | null;
16
+ activationDate: string | null;
17
+ creationDate: string;
18
+ modificationDate: string;
19
+ completionDate: string | null;
20
+ projectName: string | null;
21
+ areaName: string | null;
22
+ }
23
+ export interface ThingsProject {
24
+ id: string;
25
+ name: string;
26
+ status: "open" | "completed" | "canceled";
27
+ notes: string;
28
+ tags: string;
29
+ dueDate: string | null;
30
+ activationDate: string | null;
31
+ creationDate: string;
32
+ modificationDate: string;
33
+ completionDate: string | null;
34
+ areaName: string | null;
35
+ todoCount: number;
36
+ }
37
+ export interface ThingsArea {
38
+ id: string;
39
+ name: string;
40
+ tags: string;
41
+ }
42
+ export interface ThingsTag {
43
+ name: string;
44
+ }
45
+ /**
46
+ * Get to-dos from a specific list (inbox, today, anytime, upcoming, someday, logbook).
47
+ */
48
+ export declare function getTodosFromList(listName: string): Promise<ThingsTodo[]>;
49
+ /**
50
+ * Get to-dos from a specific project by name.
51
+ */
52
+ export declare function getTodosFromProject(projectName: string): Promise<ThingsTodo[]>;
53
+ /**
54
+ * Get to-dos from a specific area by name.
55
+ */
56
+ export declare function getTodosFromArea(areaName: string): Promise<ThingsTodo[]>;
57
+ /**
58
+ * Get a specific to-do by ID.
59
+ */
60
+ export declare function getTodoById(id: string): Promise<ThingsTodo>;
61
+ /**
62
+ * Get all projects.
63
+ */
64
+ export declare function getProjects(): Promise<ThingsProject[]>;
65
+ /**
66
+ * Get a specific project by ID.
67
+ */
68
+ export declare function getProjectById(id: string): Promise<ThingsProject>;
69
+ /**
70
+ * Get all areas.
71
+ */
72
+ export declare function getAreas(): Promise<ThingsArea[]>;
73
+ /**
74
+ * Get all tags.
75
+ */
76
+ export declare function getTags(): Promise<ThingsTag[]>;
77
+ /**
78
+ * Get to-dos tagged with a specific tag name.
79
+ */
80
+ export declare function getTodosByTag(tagName: string): Promise<ThingsTodo[]>;
81
+ /**
82
+ * Get recently modified to-dos (within the last N days).
83
+ */
84
+ export declare function getRecentTodos(days?: number): Promise<ThingsTodo[]>;
85
+ /**
86
+ * Search for to-dos by title substring.
87
+ */
88
+ export declare function searchTodosByTitle(query: string): Promise<ThingsTodo[]>;
89
+ //# sourceMappingURL=applescript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applescript.d.ts","sourceRoot":"","sources":["../src/applescript.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA6CH;;GAEG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUhE;AAMD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAMD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,EAAE,CAAC,CAuCvB;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,EAAE,CAAC,CAsBvB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,EAAE,CAAC,CAsBvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAsBjE;AAED;;GAEG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAqB5D;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAsBvE;AAED;;GAEG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAYtD;AAED;;GAEG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAUpD;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAyB1E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,GAAE,MAAU,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CA0B5E;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,UAAU,EAAE,CAAC,CA2BvB"}
@@ -0,0 +1,343 @@
1
+ /**
2
+ * AppleScript executor for reading data from Things 3.
3
+ * Uses `osascript` to run JXA (JavaScript for Automation) or AppleScript.
4
+ */
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ const execFileAsync = promisify(execFile);
8
+ /**
9
+ * Executes an AppleScript and returns the output.
10
+ */
11
+ async function runAppleScript(script) {
12
+ try {
13
+ const { stdout } = await execFileAsync("osascript", ["-e", script], {
14
+ timeout: 15000,
15
+ maxBuffer: 1024 * 1024 * 5, // 5MB
16
+ });
17
+ return stdout.trim();
18
+ }
19
+ catch (error) {
20
+ const err = error;
21
+ throw new Error(`AppleScript execution failed: ${err.stderr || err.message}`);
22
+ }
23
+ }
24
+ /**
25
+ * Executes JXA (JavaScript for Automation) and returns parsed JSON.
26
+ */
27
+ async function runJXA(script) {
28
+ try {
29
+ const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", script], {
30
+ timeout: 15000,
31
+ maxBuffer: 1024 * 1024 * 5,
32
+ });
33
+ return JSON.parse(stdout.trim());
34
+ }
35
+ catch (error) {
36
+ const err = error;
37
+ throw new Error(`JXA execution failed: ${err.stderr || err.message}`);
38
+ }
39
+ }
40
+ /**
41
+ * Opens a Things URL scheme link via `open` command on macOS.
42
+ */
43
+ export async function openThingsURL(url) {
44
+ try {
45
+ await execFileAsync("open", [url], { timeout: 10000 });
46
+ return `Successfully opened Things URL: ${url}`;
47
+ }
48
+ catch (error) {
49
+ const err = error;
50
+ throw new Error(`Failed to open Things URL: ${err.stderr || err.message}`);
51
+ }
52
+ }
53
+ // --------------------------------------------------------------------------
54
+ // Query Functions
55
+ // --------------------------------------------------------------------------
56
+ /**
57
+ * Get to-dos from a specific list (inbox, today, anytime, upcoming, someday, logbook).
58
+ */
59
+ export async function getTodosFromList(listName) {
60
+ const validLists = [
61
+ "Inbox",
62
+ "Today",
63
+ "Anytime",
64
+ "Upcoming",
65
+ "Someday",
66
+ "Logbook",
67
+ "Trash",
68
+ ];
69
+ const normalizedList = listName.charAt(0).toUpperCase() + listName.slice(1).toLowerCase();
70
+ if (!validLists.includes(normalizedList)) {
71
+ throw new Error(`Invalid list name: ${listName}. Valid lists are: ${validLists.join(", ")}`);
72
+ }
73
+ const script = `
74
+ const things = Application("Things3");
75
+ const todos = things.lists.byName("${normalizedList}").toDos();
76
+ const result = todos.map(todo => ({
77
+ id: todo.id(),
78
+ name: todo.name(),
79
+ status: todo.status(),
80
+ notes: todo.notes() || "",
81
+ tags: todo.tagNames() || "",
82
+ dueDate: todo.dueDate() ? todo.dueDate().toISOString() : null,
83
+ activationDate: todo.activationDate() ? todo.activationDate().toISOString() : null,
84
+ creationDate: todo.creationDate().toISOString(),
85
+ modificationDate: todo.modificationDate().toISOString(),
86
+ completionDate: todo.completionDate() ? todo.completionDate().toISOString() : null,
87
+ projectName: (() => { try { return todo.project().name(); } catch(e) { return null; } })(),
88
+ areaName: (() => { try { return todo.area().name(); } catch(e) { return null; } })()
89
+ }));
90
+ JSON.stringify(result);
91
+ `;
92
+ return runJXA(script);
93
+ }
94
+ /**
95
+ * Get to-dos from a specific project by name.
96
+ */
97
+ export async function getTodosFromProject(projectName) {
98
+ const escapedName = projectName.replace(/"/g, '\\"');
99
+ const script = `
100
+ const things = Application("Things3");
101
+ const todos = things.projects.byName("${escapedName}").toDos();
102
+ const result = todos.map(todo => ({
103
+ id: todo.id(),
104
+ name: todo.name(),
105
+ status: todo.status(),
106
+ notes: todo.notes() || "",
107
+ tags: todo.tagNames() || "",
108
+ dueDate: todo.dueDate() ? todo.dueDate().toISOString() : null,
109
+ activationDate: todo.activationDate() ? todo.activationDate().toISOString() : null,
110
+ creationDate: todo.creationDate().toISOString(),
111
+ modificationDate: todo.modificationDate().toISOString(),
112
+ completionDate: todo.completionDate() ? todo.completionDate().toISOString() : null,
113
+ projectName: "${escapedName}",
114
+ areaName: (() => { try { return todo.area().name(); } catch(e) { return null; } })()
115
+ }));
116
+ JSON.stringify(result);
117
+ `;
118
+ return runJXA(script);
119
+ }
120
+ /**
121
+ * Get to-dos from a specific area by name.
122
+ */
123
+ export async function getTodosFromArea(areaName) {
124
+ const escapedName = areaName.replace(/"/g, '\\"');
125
+ const script = `
126
+ const things = Application("Things3");
127
+ const todos = things.areas.byName("${escapedName}").toDos();
128
+ const result = todos.map(todo => ({
129
+ id: todo.id(),
130
+ name: todo.name(),
131
+ status: todo.status(),
132
+ notes: todo.notes() || "",
133
+ tags: todo.tagNames() || "",
134
+ dueDate: todo.dueDate() ? todo.dueDate().toISOString() : null,
135
+ activationDate: todo.activationDate() ? todo.activationDate().toISOString() : null,
136
+ creationDate: todo.creationDate().toISOString(),
137
+ modificationDate: todo.modificationDate().toISOString(),
138
+ completionDate: todo.completionDate() ? todo.completionDate().toISOString() : null,
139
+ projectName: (() => { try { return todo.project().name(); } catch(e) { return null; } })(),
140
+ areaName: "${escapedName}"
141
+ }));
142
+ JSON.stringify(result);
143
+ `;
144
+ return runJXA(script);
145
+ }
146
+ /**
147
+ * Get a specific to-do by ID.
148
+ */
149
+ export async function getTodoById(id) {
150
+ const escapedId = id.replace(/"/g, '\\"');
151
+ const script = `
152
+ const things = Application("Things3");
153
+ const todo = things.toDos.byId("${escapedId}");
154
+ const result = {
155
+ id: todo.id(),
156
+ name: todo.name(),
157
+ status: todo.status(),
158
+ notes: todo.notes() || "",
159
+ tags: todo.tagNames() || "",
160
+ dueDate: todo.dueDate() ? todo.dueDate().toISOString() : null,
161
+ activationDate: todo.activationDate() ? todo.activationDate().toISOString() : null,
162
+ creationDate: todo.creationDate().toISOString(),
163
+ modificationDate: todo.modificationDate().toISOString(),
164
+ completionDate: todo.completionDate() ? todo.completionDate().toISOString() : null,
165
+ projectName: (() => { try { return todo.project().name(); } catch(e) { return null; } })(),
166
+ areaName: (() => { try { return todo.area().name(); } catch(e) { return null; } })()
167
+ };
168
+ JSON.stringify(result);
169
+ `;
170
+ return runJXA(script);
171
+ }
172
+ /**
173
+ * Get all projects.
174
+ */
175
+ export async function getProjects() {
176
+ const script = `
177
+ const things = Application("Things3");
178
+ const projects = things.projects();
179
+ const result = projects.map(proj => ({
180
+ id: proj.id(),
181
+ name: proj.name(),
182
+ status: proj.status(),
183
+ notes: proj.notes() || "",
184
+ tags: proj.tagNames() || "",
185
+ dueDate: proj.dueDate() ? proj.dueDate().toISOString() : null,
186
+ activationDate: proj.activationDate() ? proj.activationDate().toISOString() : null,
187
+ creationDate: proj.creationDate().toISOString(),
188
+ modificationDate: proj.modificationDate().toISOString(),
189
+ completionDate: proj.completionDate() ? proj.completionDate().toISOString() : null,
190
+ areaName: (() => { try { return proj.area().name(); } catch(e) { return null; } })(),
191
+ todoCount: proj.toDos().length
192
+ }));
193
+ JSON.stringify(result);
194
+ `;
195
+ return runJXA(script);
196
+ }
197
+ /**
198
+ * Get a specific project by ID.
199
+ */
200
+ export async function getProjectById(id) {
201
+ const escapedId = id.replace(/"/g, '\\"');
202
+ const script = `
203
+ const things = Application("Things3");
204
+ const proj = things.projects.byId("${escapedId}");
205
+ const result = {
206
+ id: proj.id(),
207
+ name: proj.name(),
208
+ status: proj.status(),
209
+ notes: proj.notes() || "",
210
+ tags: proj.tagNames() || "",
211
+ dueDate: proj.dueDate() ? proj.dueDate().toISOString() : null,
212
+ activationDate: proj.activationDate() ? proj.activationDate().toISOString() : null,
213
+ creationDate: proj.creationDate().toISOString(),
214
+ modificationDate: proj.modificationDate().toISOString(),
215
+ completionDate: proj.completionDate() ? proj.completionDate().toISOString() : null,
216
+ areaName: (() => { try { return proj.area().name(); } catch(e) { return null; } })(),
217
+ todoCount: proj.toDos().length
218
+ };
219
+ JSON.stringify(result);
220
+ `;
221
+ return runJXA(script);
222
+ }
223
+ /**
224
+ * Get all areas.
225
+ */
226
+ export async function getAreas() {
227
+ const script = `
228
+ const things = Application("Things3");
229
+ const areas = things.areas();
230
+ const result = areas.map(area => ({
231
+ id: area.id(),
232
+ name: area.name(),
233
+ tags: area.tagNames() || ""
234
+ }));
235
+ JSON.stringify(result);
236
+ `;
237
+ return runJXA(script);
238
+ }
239
+ /**
240
+ * Get all tags.
241
+ */
242
+ export async function getTags() {
243
+ const script = `
244
+ const things = Application("Things3");
245
+ const tags = things.tags();
246
+ const result = tags.map(tag => ({
247
+ name: tag.name()
248
+ }));
249
+ JSON.stringify(result);
250
+ `;
251
+ return runJXA(script);
252
+ }
253
+ /**
254
+ * Get to-dos tagged with a specific tag name.
255
+ */
256
+ export async function getTodosByTag(tagName) {
257
+ const escapedName = tagName.replace(/"/g, '\\"');
258
+ const script = `
259
+ const things = Application("Things3");
260
+ const allTodos = things.toDos();
261
+ const result = allTodos.filter(todo => {
262
+ const tags = todo.tagNames() || "";
263
+ return tags.split(", ").some(t => t === "${escapedName}");
264
+ }).map(todo => ({
265
+ id: todo.id(),
266
+ name: todo.name(),
267
+ status: todo.status(),
268
+ notes: todo.notes() || "",
269
+ tags: todo.tagNames() || "",
270
+ dueDate: todo.dueDate() ? todo.dueDate().toISOString() : null,
271
+ activationDate: todo.activationDate() ? todo.activationDate().toISOString() : null,
272
+ creationDate: todo.creationDate().toISOString(),
273
+ modificationDate: todo.modificationDate().toISOString(),
274
+ completionDate: todo.completionDate() ? todo.completionDate().toISOString() : null,
275
+ projectName: (() => { try { return todo.project().name(); } catch(e) { return null; } })(),
276
+ areaName: (() => { try { return todo.area().name(); } catch(e) { return null; } })()
277
+ }));
278
+ JSON.stringify(result);
279
+ `;
280
+ return runJXA(script);
281
+ }
282
+ /**
283
+ * Get recently modified to-dos (within the last N days).
284
+ */
285
+ export async function getRecentTodos(days = 7) {
286
+ const script = `
287
+ const things = Application("Things3");
288
+ const cutoff = new Date();
289
+ cutoff.setDate(cutoff.getDate() - ${days});
290
+ const allTodos = things.toDos();
291
+ const result = allTodos.filter(todo => {
292
+ const modDate = todo.modificationDate();
293
+ return modDate && modDate >= cutoff;
294
+ }).map(todo => ({
295
+ id: todo.id(),
296
+ name: todo.name(),
297
+ status: todo.status(),
298
+ notes: todo.notes() || "",
299
+ tags: todo.tagNames() || "",
300
+ dueDate: todo.dueDate() ? todo.dueDate().toISOString() : null,
301
+ activationDate: todo.activationDate() ? todo.activationDate().toISOString() : null,
302
+ creationDate: todo.creationDate().toISOString(),
303
+ modificationDate: todo.modificationDate().toISOString(),
304
+ completionDate: todo.completionDate() ? todo.completionDate().toISOString() : null,
305
+ projectName: (() => { try { return todo.project().name(); } catch(e) { return null; } })(),
306
+ areaName: (() => { try { return todo.area().name(); } catch(e) { return null; } })()
307
+ }));
308
+ JSON.stringify(result);
309
+ `;
310
+ return runJXA(script);
311
+ }
312
+ /**
313
+ * Search for to-dos by title substring.
314
+ */
315
+ export async function searchTodosByTitle(query) {
316
+ const escapedQuery = query.replace(/"/g, '\\"').replace(/\\/g, "\\\\");
317
+ const script = `
318
+ const things = Application("Things3");
319
+ const allTodos = things.toDos();
320
+ const queryLower = "${escapedQuery}".toLowerCase();
321
+ const result = allTodos.filter(todo => {
322
+ const name = todo.name() || "";
323
+ const notes = todo.notes() || "";
324
+ return name.toLowerCase().includes(queryLower) || notes.toLowerCase().includes(queryLower);
325
+ }).map(todo => ({
326
+ id: todo.id(),
327
+ name: todo.name(),
328
+ status: todo.status(),
329
+ notes: todo.notes() || "",
330
+ tags: todo.tagNames() || "",
331
+ dueDate: todo.dueDate() ? todo.dueDate().toISOString() : null,
332
+ activationDate: todo.activationDate() ? todo.activationDate().toISOString() : null,
333
+ creationDate: todo.creationDate().toISOString(),
334
+ modificationDate: todo.modificationDate().toISOString(),
335
+ completionDate: todo.completionDate() ? todo.completionDate().toISOString() : null,
336
+ projectName: (() => { try { return todo.project().name(); } catch(e) { return null; } })(),
337
+ areaName: (() => { try { return todo.area().name(); } catch(e) { return null; } })()
338
+ }));
339
+ JSON.stringify(result);
340
+ `;
341
+ return runJXA(script);
342
+ }
343
+ //# sourceMappingURL=applescript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applescript.js","sourceRoot":"","sources":["../src/applescript.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
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
+ export {};
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}