omnifocus-mcp-enhanced 1.6.8 → 1.6.10
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 +127 -9
- package/README.zh.md +127 -9
- package/dist/server.js +3 -1
- package/dist/tools/definitions/batchAddItems.js +3 -3
- package/dist/tools/definitions/getTaskById.js +34 -25
- package/dist/tools/definitions/getTaskById.test.js +32 -0
- package/dist/tools/definitions/plannedDateSchemas.test.js +7 -0
- package/dist/tools/definitions/readTaskAttachment.js +73 -0
- package/dist/tools/definitions/readTaskAttachment.test.js +32 -0
- package/dist/tools/dumpDatabase.js +3 -2
- package/dist/tools/primitives/addOmniFocusTask.js +16 -11
- package/dist/tools/primitives/addOmniFocusTask.test.js +18 -0
- package/dist/tools/primitives/addProject.js +12 -7
- package/dist/tools/primitives/addProject.test.js +18 -0
- package/dist/tools/primitives/batchAddItems.js +21 -1
- package/dist/tools/primitives/batchAddItems.test.js +17 -0
- package/dist/tools/primitives/editItem.js +26 -22
- package/dist/tools/primitives/editItem.test.js +32 -0
- package/dist/tools/primitives/getTaskById.js +30 -159
- package/dist/tools/primitives/readTaskAttachment.js +91 -0
- package/dist/tools/primitives/removeItem.js +8 -4
- package/dist/tools/primitives/taskAttachments.js +130 -0
- package/dist/tools/primitives/taskAttachments.test.js +39 -0
- package/dist/utils/appleScriptJson.js +27 -0
- package/dist/utils/appleScriptString.js +11 -0
- package/dist/utils/appleScriptString.test.js +7 -0
- package/dist/utils/omnifocusScripts/getTaskById.js +94 -0
- package/dist/utils/omnifocusScripts/omnifocusDump.js +30 -2
- package/dist/utils/omnifocusScripts/readTaskAttachment.js +100 -0
- package/docs/roadmap/2026-02-25-batch-move-tasks-plan.md +28 -0
- package/docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md +28 -0
- package/package.json +3 -3
- package/src/server.ts +9 -1
- package/src/tools/definitions/batchAddItems.ts +3 -3
- package/src/tools/definitions/getTaskById.test.ts +34 -0
- package/src/tools/definitions/getTaskById.ts +44 -34
- package/src/tools/definitions/plannedDateSchemas.test.ts +9 -0
- package/src/tools/definitions/readTaskAttachment.test.ts +36 -0
- package/src/tools/definitions/readTaskAttachment.ts +89 -0
- package/src/tools/dumpDatabase.ts +5 -3
- package/src/tools/primitives/addOmniFocusTask.test.ts +22 -0
- package/src/tools/primitives/addOmniFocusTask.ts +16 -11
- package/src/tools/primitives/addProject.test.ts +22 -0
- package/src/tools/primitives/addProject.ts +12 -7
- package/src/tools/primitives/batchAddItems.test.ts +22 -0
- package/src/tools/primitives/batchAddItems.ts +27 -2
- package/src/tools/primitives/editItem.test.ts +38 -0
- package/src/tools/primitives/editItem.ts +26 -22
- package/src/tools/primitives/getTaskById.ts +59 -164
- package/src/tools/primitives/readTaskAttachment.ts +140 -0
- package/src/tools/primitives/removeItem.ts +9 -5
- package/src/tools/primitives/taskAttachments.test.ts +52 -0
- package/src/tools/primitives/taskAttachments.ts +195 -0
- package/src/types.ts +13 -2
- package/src/utils/appleScriptJson.ts +27 -0
- package/src/utils/appleScriptString.test.ts +9 -0
- package/src/utils/appleScriptString.ts +11 -0
- package/src/utils/omnifocusScripts/getTaskById.js +94 -0
- package/src/utils/omnifocusScripts/omnifocusDump.js +30 -2
- package/src/utils/omnifocusScripts/readTaskAttachment.js +100 -0
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
|
|
|
17
17
|
|
|
18
18
|
## 🆕 Latest Release
|
|
19
19
|
|
|
20
|
+
- **v1.6.10** - Fixed Inbox task completion via `edit_item`, fixed AppleScript special-character handling for apostrophes/backslashes, fixed JSON result escaping for special characters, and clarified `batch_add_items` / `mcporter` usage with working examples.
|
|
21
|
+
- **v1.6.9** - Added task attachment support: `get_task_by_id` now lists attachment metadata, `dump_database` exports attachment/link metadata, and new `read_task_attachment` returns image attachments as MCP image content when possible.
|
|
20
22
|
- **v1.6.8** - Added stable task move support via `move_task` and `edit_item` (`newProjectId/newProjectName/newParentTaskId/newParentTaskName/moveToInbox`) with duplicate-name protection and cycle-prevention checks.
|
|
21
23
|
- **v1.6.6** - Added full Planned Date support (create/edit/read/filter/sort/export), including `plannedDate`/`newPlannedDate` and updated task displays.
|
|
22
24
|
|
|
@@ -38,6 +40,7 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
|
|
|
38
40
|
- **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
|
|
39
41
|
- **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
|
|
40
42
|
- **🤖 AI Integration** - Seamless Claude AI integration for intelligent workflows
|
|
43
|
+
- **🖼️ Attachment-Aware Reads** - Surface note attachments and linked files before deciding whether AI should inspect them
|
|
41
44
|
|
|
42
45
|
## 📦 Installation
|
|
43
46
|
|
|
@@ -215,6 +218,120 @@ Efficiently manage multiple tasks:
|
|
|
215
218
|
}
|
|
216
219
|
```
|
|
217
220
|
|
|
221
|
+
CLI tip for `mcporter`:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Prefer explicit JSON args for complex arrays / nested objects
|
|
225
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
226
|
+
"items": [
|
|
227
|
+
{
|
|
228
|
+
"type": "task",
|
|
229
|
+
"name": "Website Technical SEO",
|
|
230
|
+
"projectName": "SEO Project"
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
}'
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
If you pass a subtask with `parentTaskId` or `parentTaskName`, do not also pass `projectName`. Subtasks inherit the project from their parent task.
|
|
237
|
+
|
|
238
|
+
Working `mcporter` examples:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# 1) Batch-create top-level tasks in a project
|
|
242
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
243
|
+
"items": [
|
|
244
|
+
{
|
|
245
|
+
"type": "task",
|
|
246
|
+
"name": "Parent: Category A",
|
|
247
|
+
"projectName": "OmniFocus MCP Batch Test"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"type": "task",
|
|
251
|
+
"name": "Parent: Category B",
|
|
252
|
+
"projectName": "OmniFocus MCP Batch Test"
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
}'
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# 2) Create parent + child in one batch
|
|
260
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
261
|
+
"items": [
|
|
262
|
+
{
|
|
263
|
+
"type": "task",
|
|
264
|
+
"name": "Parent: Category A",
|
|
265
|
+
"projectName": "OmniFocus MCP Batch Test"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"type": "task",
|
|
269
|
+
"name": "Child: A1",
|
|
270
|
+
"parentTaskName": "Parent: Category A"
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
}'
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# 3) Safer two-step flow when adding many subtasks to existing parents
|
|
278
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
279
|
+
"items": [
|
|
280
|
+
{
|
|
281
|
+
"type": "task",
|
|
282
|
+
"name": "Child: A1",
|
|
283
|
+
"parentTaskName": "Parent: Category A"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"type": "task",
|
|
287
|
+
"name": "Child: A2",
|
|
288
|
+
"parentTaskName": "Parent: Category A"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"type": "task",
|
|
292
|
+
"name": "Child: B1",
|
|
293
|
+
"parentTaskName": "Parent: Category B"
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
}'
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This will fail, by design:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
303
|
+
"items": [
|
|
304
|
+
{
|
|
305
|
+
"type": "task",
|
|
306
|
+
"name": "Child: A1",
|
|
307
|
+
"projectName": "OmniFocus MCP Batch Test",
|
|
308
|
+
"parentTaskName": "Parent: Category A"
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
}'
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Because a subtask must inherit its project from the parent task.
|
|
315
|
+
|
|
316
|
+
### 6. 🖼️ Attachment Inspection
|
|
317
|
+
|
|
318
|
+
Discover images and linked files on a task first, then read only the attachment you need:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# List task details plus attachment metadata
|
|
322
|
+
get_task_by_id {
|
|
323
|
+
"taskId": "abc123"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Open an attachment returned by get_task_by_id
|
|
327
|
+
read_task_attachment {
|
|
328
|
+
"taskId": "abc123",
|
|
329
|
+
"attachmentId": "embedded-1"
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`get_task_by_id` now reports attachment IDs, names, MIME guesses, source (`embedded` vs `linked`), and sizes when available. `read_task_attachment` returns images as MCP image content when possible, so AI clients can inspect the image directly instead of parsing base64 from plain text.
|
|
334
|
+
|
|
218
335
|
## 🛠️ Complete Tool Reference
|
|
219
336
|
|
|
220
337
|
### 📊 Database & Task Management
|
|
@@ -226,21 +343,22 @@ Efficiently manage multiple tasks:
|
|
|
226
343
|
6. **move_task** - Move an existing task to project/parent task/inbox
|
|
227
344
|
7. **batch_add_items** - Bulk add (enhanced with subtask support)
|
|
228
345
|
8. **batch_remove_items** - Bulk remove
|
|
229
|
-
9. **get_task_by_id** - Query task information
|
|
346
|
+
9. **get_task_by_id** - Query task information, including attachment metadata
|
|
347
|
+
10. **read_task_attachment** - Read an attachment reported by `get_task_by_id`
|
|
230
348
|
|
|
231
349
|
### 🔍 Built-in Perspective Tools
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
350
|
+
11. **get_inbox_tasks** - Inbox perspective
|
|
351
|
+
12. **get_flagged_tasks** - Flagged perspective
|
|
352
|
+
13. **get_forecast_tasks** - Forecast perspective (due/deferred/planned task data included)
|
|
353
|
+
14. **get_tasks_by_tag** - Tag-based filtering
|
|
354
|
+
15. **filter_tasks** - Ultimate filtering with unlimited combinations
|
|
237
355
|
|
|
238
356
|
### 🌟 Custom Perspective Tools (NEW)
|
|
239
|
-
|
|
240
|
-
|
|
357
|
+
16. **list_custom_perspectives** - 🌟 **NEW**: List all custom perspectives with details
|
|
358
|
+
17. **get_custom_perspective_tasks** - 🌟 **NEW**: Access custom perspective with hierarchical display
|
|
241
359
|
|
|
242
360
|
### 📊 Analytics & Tracking
|
|
243
|
-
|
|
361
|
+
18. **get_today_completed_tasks** - View today's completed tasks
|
|
244
362
|
|
|
245
363
|
Batch move feature roadmap (future): [docs/roadmap/2026-02-25-batch-move-tasks-plan.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.md)
|
|
246
364
|
|
package/README.zh.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
## 🆕 最新版本
|
|
15
15
|
|
|
16
|
+
- **v1.6.10** - 修复 `edit_item` 无法完成 Inbox 任务的问题,修复 AppleScript 对单引号/反斜杠等特殊字符的处理,修复特殊字符导致的 JSON 返回解析失败,并补充 `batch_add_items` / `mcporter` 的可直接运行示例与说明。
|
|
17
|
+
- **v1.6.9** - 新增任务附件支持:`get_task_by_id` 会返回附件元信息,`dump_database` 导出附件/链接元信息,并新增 `read_task_attachment`,可在支持时直接把图片附件作为 MCP 图片内容返回。
|
|
16
18
|
- **v1.6.6** - 新增 Planned Date(计划日期)全链路支持:创建/编辑/读取/过滤/排序/导出,包含 `plannedDate` / `newPlannedDate`。
|
|
17
19
|
|
|
18
20
|
## ✨ 核心特性
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
- **📅 时间管理** - 截止日期、推迟日期、计划日期、估时和计划
|
|
34
36
|
- **🏷️ 高级标签** - 基于标签的精确/模糊匹配过滤
|
|
35
37
|
- **🤖 AI 集成** - 与 Claude AI 无缝集成,实现智能工作流
|
|
38
|
+
- **🖼️ 附件感知读取** - 先暴露备注附件和链接文件的元信息,再决定是否让 AI 继续查看附件内容
|
|
36
39
|
|
|
37
40
|
## 📦 安装
|
|
38
41
|
|
|
@@ -210,6 +213,120 @@ get_custom_perspective_tasks {
|
|
|
210
213
|
}
|
|
211
214
|
```
|
|
212
215
|
|
|
216
|
+
`mcporter` 调用提示:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# 复杂数组 / 嵌套对象,建议明确使用 --args JSON
|
|
220
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
221
|
+
"items": [
|
|
222
|
+
{
|
|
223
|
+
"type": "task",
|
|
224
|
+
"name": "网站技术 SEO",
|
|
225
|
+
"projectName": "SEO 项目"
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
}'
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
如果某条子任务传了 `parentTaskId` 或 `parentTaskName`,就不要再传 `projectName`。子任务会自动继承父任务所在项目。
|
|
232
|
+
|
|
233
|
+
可直接运行的 `mcporter` 示例:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# 1)批量创建项目下的顶层任务
|
|
237
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
238
|
+
"items": [
|
|
239
|
+
{
|
|
240
|
+
"type": "task",
|
|
241
|
+
"name": "父任务:分类A",
|
|
242
|
+
"projectName": "OmniFocus MCP 批量测试"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"type": "task",
|
|
246
|
+
"name": "父任务:分类B",
|
|
247
|
+
"projectName": "OmniFocus MCP 批量测试"
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# 2)单次批量里同时创建父任务和子任务
|
|
255
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
256
|
+
"items": [
|
|
257
|
+
{
|
|
258
|
+
"type": "task",
|
|
259
|
+
"name": "父任务:分类A",
|
|
260
|
+
"projectName": "OmniFocus MCP 批量测试"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"type": "task",
|
|
264
|
+
"name": "子任务:A1",
|
|
265
|
+
"parentTaskName": "父任务:分类A"
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}'
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# 3)更稳妥的两步法:父任务已存在时,再批量创建多个子任务
|
|
273
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
274
|
+
"items": [
|
|
275
|
+
{
|
|
276
|
+
"type": "task",
|
|
277
|
+
"name": "子任务:A1",
|
|
278
|
+
"parentTaskName": "父任务:分类A"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"type": "task",
|
|
282
|
+
"name": "子任务:A2",
|
|
283
|
+
"parentTaskName": "父任务:分类A"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"type": "task",
|
|
287
|
+
"name": "子任务:B1",
|
|
288
|
+
"parentTaskName": "父任务:分类B"
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}'
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
下面这种写法会失败,这属于预期行为:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
mcporter call omnifocus.batch_add_items --args '{
|
|
298
|
+
"items": [
|
|
299
|
+
{
|
|
300
|
+
"type": "task",
|
|
301
|
+
"name": "子任务:A1",
|
|
302
|
+
"projectName": "OmniFocus MCP 批量测试",
|
|
303
|
+
"parentTaskName": "父任务:分类A"
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
}'
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
因为子任务必须继承父任务所在项目,不能再单独传 `projectName`。
|
|
310
|
+
|
|
311
|
+
### 6. 🖼️ 附件查看
|
|
312
|
+
|
|
313
|
+
先读取任务和附件元信息,再按需打开具体附件:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# 读取任务详情和附件元信息
|
|
317
|
+
get_task_by_id {
|
|
318
|
+
"taskId": "abc123"
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# 打开 get_task_by_id 返回的某个附件
|
|
322
|
+
read_task_attachment {
|
|
323
|
+
"taskId": "abc123",
|
|
324
|
+
"attachmentId": "embedded-1"
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`get_task_by_id` 现在会返回附件 ID、名称、推断出的 MIME 类型、来源(`embedded` 或 `linked`)以及可用时的大小。`read_task_attachment` 会尽量把图片作为 MCP 图片内容直接返回,这样 AI 客户端可以直接查看图片,而不是只能读一段 base64 文本。
|
|
329
|
+
|
|
213
330
|
## 🛠️ 完整工具参考
|
|
214
331
|
|
|
215
332
|
### 📊 数据库与任务管理
|
|
@@ -221,21 +338,22 @@ get_custom_perspective_tasks {
|
|
|
221
338
|
6. **move_task** - 将已有任务转移到项目/父任务/Inbox
|
|
222
339
|
7. **batch_add_items** - 批量添加(增强子任务支持)
|
|
223
340
|
8. **batch_remove_items** - 批量删除
|
|
224
|
-
9. **get_task_by_id** -
|
|
341
|
+
9. **get_task_by_id** - 查询任务信息,并返回附件元信息
|
|
342
|
+
10. **read_task_attachment** - 读取 `get_task_by_id` 返回的任务附件
|
|
225
343
|
|
|
226
344
|
### 🔍 内置透视工具
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
345
|
+
11. **get_inbox_tasks** - 收件箱透视
|
|
346
|
+
12. **get_flagged_tasks** - 已标记透视
|
|
347
|
+
13. **get_forecast_tasks** - 预测透视(包含截止/推迟/计划日期任务数据)
|
|
348
|
+
14. **get_tasks_by_tag** - 基于标签的过滤
|
|
349
|
+
15. **filter_tasks** - 无限组合的终极过滤
|
|
232
350
|
|
|
233
351
|
### 🌟 自定义透视工具(新功能)
|
|
234
|
-
|
|
235
|
-
|
|
352
|
+
16. **list_custom_perspectives** - 🌟 **新功能**:列出所有自定义透视及详情
|
|
353
|
+
17. **get_custom_perspective_tasks** - 🌟 **新功能**:访问自定义透视,支持层级显示
|
|
236
354
|
|
|
237
355
|
### 📊 分析与跟踪
|
|
238
|
-
|
|
356
|
+
18. **get_today_completed_tasks** - 查看今日完成的任务
|
|
239
357
|
|
|
240
358
|
批量转移功能后续计划(Roadmap):[docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md)
|
|
241
359
|
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as moveTaskTool from './tools/definitions/moveTask.js';
|
|
|
11
11
|
import * as batchAddItemsTool from './tools/definitions/batchAddItems.js';
|
|
12
12
|
import * as batchRemoveItemsTool from './tools/definitions/batchRemoveItems.js';
|
|
13
13
|
import * as getTaskByIdTool from './tools/definitions/getTaskById.js';
|
|
14
|
+
import * as readTaskAttachmentTool from './tools/definitions/readTaskAttachment.js';
|
|
14
15
|
import * as getTodayCompletedTasksTool from './tools/definitions/getTodayCompletedTasks.js';
|
|
15
16
|
// Import perspective tools
|
|
16
17
|
import * as getInboxTasksTool from './tools/definitions/getInboxTasks.js';
|
|
@@ -25,7 +26,7 @@ import * as getCustomPerspectiveTasksTool from './tools/definitions/getCustomPer
|
|
|
25
26
|
// Create an MCP server
|
|
26
27
|
const server = new McpServer({
|
|
27
28
|
name: "OmniFocus MCP",
|
|
28
|
-
version: "1.6.
|
|
29
|
+
version: "1.6.9"
|
|
29
30
|
});
|
|
30
31
|
// Register tools
|
|
31
32
|
server.tool("dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler);
|
|
@@ -37,6 +38,7 @@ server.tool("move_task", "Move an existing task to a project, parent task, or in
|
|
|
37
38
|
server.tool("batch_add_items", "Add multiple tasks or projects to OmniFocus in a single operation", batchAddItemsTool.schema.shape, batchAddItemsTool.handler);
|
|
38
39
|
server.tool("batch_remove_items", "Remove multiple tasks or projects from OmniFocus in a single operation", batchRemoveItemsTool.schema.shape, batchRemoveItemsTool.handler);
|
|
39
40
|
server.tool("get_task_by_id", "Get information about a specific task by ID or name", getTaskByIdTool.schema.shape, getTaskByIdTool.handler);
|
|
41
|
+
server.tool("read_task_attachment", "Read a task attachment reported by get_task_by_id. Images are returned as MCP image content when possible.", readTaskAttachmentTool.schema.shape, readTaskAttachmentTool.handler);
|
|
40
42
|
server.tool("get_today_completed_tasks", "Get tasks completed today - view today's accomplishments", getTodayCompletedTasksTool.schema.shape, getTodayCompletedTasksTool.handler);
|
|
41
43
|
// Register perspective tools
|
|
42
44
|
server.tool("get_inbox_tasks", "Get tasks from OmniFocus inbox perspective", getInboxTasksTool.schema.shape, getInboxTasksTool.handler);
|
|
@@ -12,9 +12,9 @@ export const schema = z.object({
|
|
|
12
12
|
estimatedMinutes: z.number().optional().describe("Estimated time to complete the item, in minutes"),
|
|
13
13
|
tags: z.array(z.string()).optional().describe("Tags to assign to the item"),
|
|
14
14
|
// Task-specific properties
|
|
15
|
-
projectName: z.string().optional().describe("For tasks: The name
|
|
16
|
-
parentTaskId: z.string().optional().describe("For tasks: The ID
|
|
17
|
-
parentTaskName: z.string().optional().describe("For tasks: The name
|
|
15
|
+
projectName: z.string().optional().describe("For tasks: The project name for top-level tasks. Omit this when parentTaskId or parentTaskName is set."),
|
|
16
|
+
parentTaskId: z.string().optional().describe("For tasks: The parent task ID for subtasks. When this is set, do not also provide projectName."),
|
|
17
|
+
parentTaskName: z.string().optional().describe("For tasks: The parent task name for subtasks. Subtasks inherit project from their parent, so do not also provide projectName."),
|
|
18
18
|
// Project-specific properties
|
|
19
19
|
folderName: z.string().optional().describe("For projects: The name of the folder to add the project to"),
|
|
20
20
|
sequential: z.boolean().optional().describe("For projects: Whether tasks in the project should be sequential")
|
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { getTaskById } from '../primitives/getTaskById.js';
|
|
3
|
+
import { formatAttachmentSize } from '../primitives/taskAttachments.js';
|
|
3
4
|
export const schema = z.object({
|
|
4
5
|
taskId: z.string().optional().describe("The ID of the task to retrieve"),
|
|
5
6
|
taskName: z.string().optional().describe("The name of the task to retrieve (alternative to taskId)")
|
|
6
7
|
});
|
|
8
|
+
export function formatTaskInfo(task) {
|
|
9
|
+
let infoText = `📋 **Task Information**\n`;
|
|
10
|
+
infoText += `• **Name**: ${task.name}\n`;
|
|
11
|
+
infoText += `• **ID**: ${task.id}\n`;
|
|
12
|
+
if (task.note) {
|
|
13
|
+
infoText += `• **Note**: ${task.note}\n`;
|
|
14
|
+
}
|
|
15
|
+
if (task.parentId && task.parentName) {
|
|
16
|
+
infoText += `• **Parent Task**: ${task.parentName} (${task.parentId})\n`;
|
|
17
|
+
}
|
|
18
|
+
if (task.projectId && task.projectName) {
|
|
19
|
+
infoText += `• **Project**: ${task.projectName} (${task.projectId})\n`;
|
|
20
|
+
}
|
|
21
|
+
if (task.dueDate) {
|
|
22
|
+
infoText += `• **Due**: ${new Date(task.dueDate).toLocaleString()}\n`;
|
|
23
|
+
}
|
|
24
|
+
if (task.deferDate) {
|
|
25
|
+
infoText += `• **Defer**: ${new Date(task.deferDate).toLocaleString()}\n`;
|
|
26
|
+
}
|
|
27
|
+
if (task.plannedDate) {
|
|
28
|
+
infoText += `• **Planned**: ${new Date(task.plannedDate).toLocaleString()}\n`;
|
|
29
|
+
}
|
|
30
|
+
infoText += `• **Has Children**: ${task.hasChildren ? `Yes (${task.childrenCount} subtasks)` : 'No'}\n`;
|
|
31
|
+
infoText += `• **Attachments**: ${task.attachments.length}\n`;
|
|
32
|
+
if (task.attachments.length > 0) {
|
|
33
|
+
task.attachments.forEach(attachment => {
|
|
34
|
+
infoText += ` - ${attachment.id}: ${attachment.name} [${attachment.kind}, ${attachment.mimeType || 'unknown'}, ${attachment.source}, ${formatAttachmentSize(attachment.sizeBytes)}]\n`;
|
|
35
|
+
});
|
|
36
|
+
infoText += `• Use read_task_attachment with an attachment ID or name when you need to inspect the file.\n`;
|
|
37
|
+
}
|
|
38
|
+
return infoText;
|
|
39
|
+
}
|
|
7
40
|
export async function handler(args, extra) {
|
|
8
41
|
try {
|
|
9
42
|
// Validate that either taskId or taskName is provided
|
|
@@ -19,34 +52,10 @@ export async function handler(args, extra) {
|
|
|
19
52
|
// Call the getTaskById function
|
|
20
53
|
const result = await getTaskById(args);
|
|
21
54
|
if (result.success && result.task) {
|
|
22
|
-
const task = result.task;
|
|
23
|
-
// Format task information for display
|
|
24
|
-
let infoText = `📋 **Task Information**\n`;
|
|
25
|
-
infoText += `• **Name**: ${task.name}\n`;
|
|
26
|
-
infoText += `• **ID**: ${task.id}\n`;
|
|
27
|
-
if (task.note) {
|
|
28
|
-
infoText += `• **Note**: ${task.note}\n`;
|
|
29
|
-
}
|
|
30
|
-
if (task.parentId && task.parentName) {
|
|
31
|
-
infoText += `• **Parent Task**: ${task.parentName} (${task.parentId})\n`;
|
|
32
|
-
}
|
|
33
|
-
if (task.projectId && task.projectName) {
|
|
34
|
-
infoText += `• **Project**: ${task.projectName} (${task.projectId})\n`;
|
|
35
|
-
}
|
|
36
|
-
if (task.dueDate) {
|
|
37
|
-
infoText += `• **Due**: ${new Date(task.dueDate).toLocaleString()}\n`;
|
|
38
|
-
}
|
|
39
|
-
if (task.deferDate) {
|
|
40
|
-
infoText += `• **Defer**: ${new Date(task.deferDate).toLocaleString()}\n`;
|
|
41
|
-
}
|
|
42
|
-
if (task.plannedDate) {
|
|
43
|
-
infoText += `• **Planned**: ${new Date(task.plannedDate).toLocaleString()}\n`;
|
|
44
|
-
}
|
|
45
|
-
infoText += `• **Has Children**: ${task.hasChildren ? `Yes (${task.childrenCount} subtasks)` : 'No'}\n`;
|
|
46
55
|
return {
|
|
47
56
|
content: [{
|
|
48
57
|
type: "text",
|
|
49
|
-
text:
|
|
58
|
+
text: formatTaskInfo(result.task)
|
|
50
59
|
}]
|
|
51
60
|
};
|
|
52
61
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { formatTaskInfo } from './getTaskById.js';
|
|
4
|
+
test('formatTaskInfo includes attachment metadata for follow-up reads', () => {
|
|
5
|
+
const output = formatTaskInfo({
|
|
6
|
+
id: 'task-1',
|
|
7
|
+
name: 'Review screenshots',
|
|
8
|
+
note: 'Latest UI mocks are attached',
|
|
9
|
+
hasChildren: false,
|
|
10
|
+
childrenCount: 0,
|
|
11
|
+
tags: ['design'],
|
|
12
|
+
flagged: false,
|
|
13
|
+
completed: false,
|
|
14
|
+
linkedFileURLs: [],
|
|
15
|
+
attachments: [
|
|
16
|
+
{
|
|
17
|
+
id: 'embedded-1',
|
|
18
|
+
name: 'ui-mock.png',
|
|
19
|
+
kind: 'image',
|
|
20
|
+
mimeType: 'image/png',
|
|
21
|
+
sizeBytes: 4096,
|
|
22
|
+
source: 'embedded',
|
|
23
|
+
isImage: true
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
});
|
|
27
|
+
assert.match(output, /Attachments\*\*: 1/);
|
|
28
|
+
assert.match(output, /embedded-1/);
|
|
29
|
+
assert.match(output, /ui-mock\.png/);
|
|
30
|
+
assert.match(output, /image\/png/);
|
|
31
|
+
assert.match(output, /Use read_task_attachment/);
|
|
32
|
+
});
|
|
@@ -37,6 +37,13 @@ test('batch_add_items schema preserves plannedDate for task and project', () =>
|
|
|
37
37
|
assert.equal(parsed.items[0].plannedDate, '2026-02-14');
|
|
38
38
|
assert.equal(parsed.items[1].plannedDate, '2026-02-15');
|
|
39
39
|
});
|
|
40
|
+
test('batch_add_items schema documents subtask project inheritance', () => {
|
|
41
|
+
const itemSchema = batchAddItemsSchema.shape.items._def.type;
|
|
42
|
+
const projectNameDescription = itemSchema.shape.projectName.description;
|
|
43
|
+
const parentTaskNameDescription = itemSchema.shape.parentTaskName.description;
|
|
44
|
+
assert.match(projectNameDescription, /omit this when parentTaskId or parentTaskName is set/i);
|
|
45
|
+
assert.match(parentTaskNameDescription, /subtasks inherit project from their parent/i);
|
|
46
|
+
});
|
|
40
47
|
test('edit_item schema preserves newPlannedDate', () => {
|
|
41
48
|
const parsed = editItemSchema.parse({
|
|
42
49
|
itemType: 'task',
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { readTaskAttachment, validateReadTaskAttachmentParams } from '../primitives/readTaskAttachment.js';
|
|
3
|
+
import { formatAttachmentSize } from '../primitives/taskAttachments.js';
|
|
4
|
+
export const schema = z.object({
|
|
5
|
+
taskId: z.string().optional().describe('The ID of the task that owns the attachment'),
|
|
6
|
+
taskName: z.string().optional().describe('The name of the task that owns the attachment'),
|
|
7
|
+
attachmentId: z.string().optional().describe('The attachment ID reported by get_task_by_id'),
|
|
8
|
+
attachmentName: z.string().optional().describe('The attachment name reported by get_task_by_id')
|
|
9
|
+
});
|
|
10
|
+
export function buildAttachmentContentResponse(result) {
|
|
11
|
+
const attachment = result.attachment;
|
|
12
|
+
const summary = `📎 **Attachment**\n` +
|
|
13
|
+
`• **Name**: ${attachment.name}\n` +
|
|
14
|
+
`• **ID**: ${attachment.id}\n` +
|
|
15
|
+
`• **Source**: ${attachment.source}\n` +
|
|
16
|
+
`• **Type**: ${attachment.kind}\n` +
|
|
17
|
+
`• **MIME**: ${attachment.mimeType || 'unknown'}\n` +
|
|
18
|
+
`• **Size**: ${formatAttachmentSize(attachment.sizeBytes)}\n`;
|
|
19
|
+
const content = [
|
|
20
|
+
{
|
|
21
|
+
type: 'text',
|
|
22
|
+
text: summary
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
if (attachment.isImage && attachment.mimeType && result.content.base64) {
|
|
26
|
+
content.push({
|
|
27
|
+
type: 'image',
|
|
28
|
+
data: result.content.base64,
|
|
29
|
+
mimeType: attachment.mimeType
|
|
30
|
+
});
|
|
31
|
+
return { content };
|
|
32
|
+
}
|
|
33
|
+
if (result.content.text) {
|
|
34
|
+
content.push({
|
|
35
|
+
type: 'text',
|
|
36
|
+
text: result.content.text
|
|
37
|
+
});
|
|
38
|
+
return { content };
|
|
39
|
+
}
|
|
40
|
+
if (result.content.base64) {
|
|
41
|
+
content.push({
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: `Base64 content:\n${result.content.base64}`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { content };
|
|
47
|
+
}
|
|
48
|
+
export async function handler(args, extra) {
|
|
49
|
+
const validation = validateReadTaskAttachmentParams(args);
|
|
50
|
+
if (!validation.valid) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: `Error: ${validation.error}`
|
|
55
|
+
}],
|
|
56
|
+
isError: true
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const result = await readTaskAttachment(args);
|
|
60
|
+
if (!result.success || !result.attachment || !result.content) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: `Failed to read attachment: ${result.error}`
|
|
65
|
+
}],
|
|
66
|
+
isError: true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return buildAttachmentContentResponse({
|
|
70
|
+
attachment: result.attachment,
|
|
71
|
+
content: result.content
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { buildAttachmentContentResponse, schema } from './readTaskAttachment.js';
|
|
4
|
+
test('read_task_attachment schema preserves task and attachment selectors', () => {
|
|
5
|
+
const parsed = schema.parse({
|
|
6
|
+
taskId: 'task-1',
|
|
7
|
+
attachmentId: 'embedded-1'
|
|
8
|
+
});
|
|
9
|
+
assert.equal(parsed.taskId, 'task-1');
|
|
10
|
+
assert.equal(parsed.attachmentId, 'embedded-1');
|
|
11
|
+
});
|
|
12
|
+
test('buildAttachmentContentResponse returns image content blocks for image attachments', () => {
|
|
13
|
+
const response = buildAttachmentContentResponse({
|
|
14
|
+
attachment: {
|
|
15
|
+
id: 'embedded-1',
|
|
16
|
+
name: 'annotated.png',
|
|
17
|
+
kind: 'image',
|
|
18
|
+
mimeType: 'image/png',
|
|
19
|
+
sizeBytes: 12,
|
|
20
|
+
source: 'embedded',
|
|
21
|
+
isImage: true
|
|
22
|
+
},
|
|
23
|
+
content: {
|
|
24
|
+
base64: 'QUJDRA=='
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
assert.equal(response.content[0].type, 'text');
|
|
28
|
+
assert.match(response.content[0].text, /annotated\.png/);
|
|
29
|
+
assert.equal(response.content[1].type, 'image');
|
|
30
|
+
assert.equal(response.content[1].mimeType, 'image/png');
|
|
31
|
+
assert.equal(response.content[1].data, 'QUJDRA==');
|
|
32
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeOmniFocusScript } from '../utils/scriptExecution.js';
|
|
2
|
+
import { normalizeTaskAttachments } from './primitives/taskAttachments.js';
|
|
2
3
|
// Main function to dump the database
|
|
3
4
|
export async function dumpDatabase() {
|
|
4
5
|
try {
|
|
@@ -58,8 +59,8 @@ export async function dumpDatabase() {
|
|
|
58
59
|
isRepeating: false, // Not available in the new format
|
|
59
60
|
repetitionMethod: null, // Not available in the new format
|
|
60
61
|
repetitionRule: null, // Not available in the new format
|
|
61
|
-
attachments:
|
|
62
|
-
linkedFileURLs: [],
|
|
62
|
+
attachments: normalizeTaskAttachments(task.attachments, task.linkedFileURLs),
|
|
63
|
+
linkedFileURLs: task.linkedFileURLs || [],
|
|
63
64
|
notifications: [], // Default empty array
|
|
64
65
|
shouldUseFloatingTimeZone: false // Default value
|
|
65
66
|
};
|