omnifocus_mcp 1.0.0 → 1.0.2

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -1
  3. data/README.md +7 -16
  4. data/bin/omnifocus-mcp +10 -3
  5. data/lib/omnifocus_mcp/cli.rb +25 -0
  6. data/lib/omnifocus_mcp/infrastructure/script_runner.rb +1 -4
  7. data/lib/omnifocus_mcp/json_rpc_compat.rb +8 -1
  8. data/lib/omnifocus_mcp/mcp.rb +4 -1
  9. data/lib/omnifocus_mcp/tools/database_stats.rb +0 -154
  10. data/lib/omnifocus_mcp/tools/definitions/{add_omnifocus_task_tool.rb → add_omni_focus_task_tool.rb} +3 -3
  11. data/lib/omnifocus_mcp/tools/definitions/date_formatter.rb +2 -4
  12. data/lib/omnifocus_mcp/tools/generators/database_stats.rb +134 -3
  13. data/lib/omnifocus_mcp/tools/generators/edit_item.rb +1 -3
  14. data/lib/omnifocus_mcp/tools/generators/query_omnifocus.rb +22 -20
  15. data/lib/omnifocus_mcp/tools/operations/{add_omnifocus_task.rb → add_omni_focus_task.rb} +1 -1
  16. data/lib/omnifocus_mcp/tools/operations/batch_add_items/bulk_executor.rb +1 -1
  17. data/lib/omnifocus_mcp/tools/operations/batch_add_items/planner.rb +1 -0
  18. data/lib/omnifocus_mcp/tools/operations/batch_add_items.rb +2 -2
  19. data/lib/omnifocus_mcp/tools/operations/batch_remove_items.rb +4 -2
  20. data/lib/omnifocus_mcp/tools/presenters/query_results.rb +9 -7
  21. data/lib/omnifocus_mcp/version.rb +2 -1
  22. data/lib/omnifocus_mcp.rb +7 -7
  23. metadata +6 -11
  24. data/lib/omnifocus_mcp/infrastructure/.keep +0 -1
  25. data/lib/omnifocus_mcp/parsers/.keep +0 -1
  26. data/lib/omnifocus_mcp/tools/batch_report.rb +0 -9
  27. data/lib/omnifocus_mcp/tools/generators/.keep +0 -1
  28. data/lib/omnifocus_mcp/tools/query_omnifocus_formatter.rb +0 -9
  29. data/lib/omnifocus_mcp/utils/date_formatting.rb +0 -9
  30. /data/lib/omnifocus_mcp/tools/generators/{add_omnifocus_task.rb → add_omni_focus_task.rb} +0 -0
  31. /data/lib/omnifocus_mcp/tools/messages/{add_omnifocus_task.rb → add_omni_focus_task.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4eb86aa8048f5b98edf61273ab7936b859663199e2ad6c73bb11ec55605550d8
4
- data.tar.gz: 7dc7b4f11e986aa96d8b9d746d75a16e7222f13627577ca3e3c420ee4df21bb1
3
+ metadata.gz: b8c375f2413b7dd77c677c5abed0ccb49b4efebd2ed7e8fc36179f65cd763154
4
+ data.tar.gz: ca70f41c0e96bbb3a5048d1c673a950fe91f252c3b658eb09c99d0462452a35c
5
5
  SHA512:
6
- metadata.gz: d7886bb913d144e269a8d9605a7e09ac18a9cab49742a62b9bd4f8b835fd034e6f4984ed33dbc38df2709f6ea748b0902b69b6e823da5b7cd58548c645e195ef
7
- data.tar.gz: 033366a6ead47fccfb29bdb0e0df36a1817a124ecea767977bd7c397d26252884fc4e57a3f3e4d6e8b41dc397ad9e6bd2265bf532f3f55ee127d796a300ab00c
6
+ metadata.gz: e2887652751a36fec3f1fbadfbc9feca9c64a9063317f25a1c13a8f0817236c4f2fa0fdf022b4930d8cce7493a8a0b217c72ed100755b55a44a9733888f122a8
7
+ data.tar.gz: fd235093c277e4bd78d5886512d66489f2524c39c3af875877502bdd7fba3064de3b19287660e7582009eab69a9e7646ca98e0ab3c7b3cab451124f3a4940556
data/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [1.0.2]
4
+
5
+ - Remove `bundler/setup` from the executable so the gem runs correctly when
6
+ installed outside a Bundler-managed environment
7
+ - Refactor database stats generator to eliminate circular logic
8
+ - Remove unused `BatchReport` and `QueryOmnifocusFormatter` classes
9
+ - Reorganise spec suite into per-class files
10
+
11
+ ## [1.0.1] - 2026-06-27
12
+
13
+ - Fix UTF-8/US-ASCII encoding on stdio so MCP clients receive valid JSON-RPC messages
14
+ - Add `--version` / `-v` CLI flags and log server version on startup
15
+ - Omit empty date and numeric filters from query result formatting
4
16
 
5
17
  ## [1.0.0] - 2026-05-31
6
18
 
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # omnifocus-mcp (Ruby)
2
2
 
3
- `omnifocus-mcp` is a Ruby MCP server that lets LLM clients (Claude Desktop, MCP
4
- Inspector, etc.) work with OmniFocus on macOS over stdio. It exposes tools and
3
+ `omnifocus-mcp` is a Ruby MCP server that lets LLM clients (Claude Code / Desktop,
4
+ Cursor, Zed, etc.) work with OmniFocus on macOS over stdio. It exposes tools and
5
5
  resources for creating, editing, removing, querying, and reporting on OmniFocus
6
6
  tasks, projects, perspectives, and tags.
7
7
 
@@ -60,21 +60,9 @@ This tool was heavily inspired by [OmniFocus MCP Server](https://github.com/them
60
60
  gem install omnifocus-mcp
61
61
  ```
62
62
 
63
- ## Run
64
-
65
- ```sh
66
- omnifocus-mcp
67
- ```
68
-
69
- The server speaks MCP over stdio. Test it with the MCP inspector:
70
-
71
- ```sh
72
- npx @modelcontextprotocol/inspector omnifocus-mcp
73
- ```
74
-
75
63
  ## Configure an MCP Client
76
64
 
77
- After installing the executable, add this server to any MCP client that supports
65
+ After installing the executable, add this config to any MCP client that supports
78
66
  stdio servers:
79
67
 
80
68
  ```json
@@ -93,7 +81,7 @@ stdio servers:
93
81
  This server uses [fast-mcp](https://github.com/yjacquin/fast-mcp) 1.6, which
94
82
  does not currently expose MCP server instructions during client initialization.
95
83
  To give an MCP client better guidance, copy the instructions below into a skill,
96
- rule, your project's `AGENTS.md`, or another client-specific instruction file.
84
+ rule, your project's `AGENTS.md`, or your client-specific instruction file.
97
85
 
98
86
  ```text
99
87
  OmniFocus MCP server for macOS task management.
@@ -145,3 +133,6 @@ If a run is killed mid-flight, you can sweep leftover items with:
145
133
  ```sh
146
134
  bundle exec ruby spec/integration/cleanup.rb
147
135
  ```
136
+
137
+ You should backup your OmniFocus database before using this tool. Refer to the
138
+ warranty information in the LICENSE.
data/bin/omnifocus-mcp CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require "omnifocus_mcp"
4
+ # Keep in sync with OmnifocusMcp::VERSION_ARGS in lib/omnifocus_mcp/version.rb.
5
+ VERSION_ARGS = %w[--version -v version].freeze
6
+
7
+ if ARGV.intersect?(VERSION_ARGS)
8
+ require "omnifocus_mcp/version"
9
+ puts OmnifocusMcp::VERSION
10
+ exit 0
11
+ end
6
12
 
7
- OmnifocusMcp::Mcp.start
13
+ require "omnifocus_mcp"
14
+ OmnifocusMcp::Cli.run
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmnifocusMcp
4
+ # Command-line entry point for the `omnifocus-mcp` executable.
5
+ module Cli
6
+ module_function
7
+
8
+ def run(argv = ARGV)
9
+ if version_requested?(argv)
10
+ print_version
11
+ exit 0
12
+ end
13
+
14
+ Mcp.start
15
+ end
16
+
17
+ def version_requested?(argv)
18
+ argv.intersect?(OmnifocusMcp::VERSION_ARGS)
19
+ end
20
+
21
+ def print_version
22
+ puts VERSION
23
+ end
24
+ end
25
+ end
@@ -24,6 +24,7 @@ module OmnifocusMcp
24
24
 
25
25
  class << self
26
26
  def default = @default ||= new
27
+ def reset! = @default = new
27
28
 
28
29
  def runner = default.runner
29
30
 
@@ -31,10 +32,6 @@ module OmnifocusMcp
31
32
  default.runner = runner
32
33
  end
33
34
 
34
- def reset!
35
- @default = new
36
- end
37
-
38
35
  def execute_jxa(script) = default.execute_jxa(script)
39
36
  def execute_omnifocus_source(source, args: nil) = default.execute_omnifocus_source(source, args: args)
40
37
  def execute_omnifocus_script(script_path, args: nil) = default.execute_omnifocus_script(script_path, args: args)
@@ -10,12 +10,17 @@ module OmnifocusMcp
10
10
  module_function
11
11
 
12
12
  def apply!
13
+ $stdin.set_encoding(Encoding::UTF_8) if $stdin.respond_to?(:set_encoding)
13
14
  patch_server_send_error!
14
15
  patch_stdio_transport!
15
16
  end
16
17
 
17
18
  def normalize_id(id) = id.nil? ? UNKNOWN_REQUEST_ID : id
18
19
 
20
+ # MCP clients send UTF-8 JSON, but stdio may be US-ASCII-labelled (same as
21
+ # osascript stdout in ScriptRunner). Re-label before strip/parse.
22
+ def normalize_line(line) = line.to_s.dup.force_encoding(Encoding::UTF_8).strip
23
+
19
24
  def patch_server_send_error!
20
25
  return if server_send_error_patched?
21
26
 
@@ -29,6 +34,7 @@ module OmnifocusMcp
29
34
  end
30
35
  private_class_method :patch_server_send_error!
31
36
 
37
+ # rubocop:disable Metrics
32
38
  def patch_stdio_transport!
33
39
  return if stdio_transport_patched?
34
40
 
@@ -40,7 +46,7 @@ module OmnifocusMcp
40
46
  @running = true
41
47
 
42
48
  while @running && (line = $stdin.gets)
43
- stripped = line.strip
49
+ stripped = OmnifocusMcp::JsonRpcCompat.normalize_line(line)
44
50
  next if stripped.empty?
45
51
 
46
52
  begin
@@ -60,6 +66,7 @@ module OmnifocusMcp
60
66
  end
61
67
  end
62
68
  end
69
+ # rubocop:enable Metrics
63
70
  private_class_method :patch_stdio_transport!
64
71
 
65
72
  def server_send_error_patched?
@@ -41,7 +41,10 @@ module OmnifocusMcp
41
41
  end
42
42
  end
43
43
 
44
- def start = build_server.start
44
+ def start
45
+ OmnifocusMcp.logger.warn("Starting #{server_name} v#{server_version}")
46
+ build_server.start
47
+ end
45
48
 
46
49
  private
47
50
 
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../result"
4
- require_relative "../infrastructure/js_embed"
5
- require_relative "../infrastructure/script_runner"
6
-
7
3
  module OmnifocusMcp
8
4
  module Tools
9
5
  # Lightweight database overview helpers that don't require pulling the
@@ -12,11 +8,7 @@ module OmnifocusMcp
12
8
  # Provides:
13
9
  # * {.get_database_stats} — counts + last-modified timestamp
14
10
  # * {.get_changes_since} — incremental change feed since a timestamp
15
- # rubocop:disable Metrics/ModuleLength
16
11
  module DatabaseStats
17
- # Lightweight database statistics: counts + last-modified timestamp.
18
- #
19
- # @return [OmnifocusMcp::Result] +ok+ carries the stats Hash; +error+ carries a user-facing message.
20
12
  class << self
21
13
  def get_database_stats
22
14
  require_relative "operations/database_stats"
@@ -32,153 +24,7 @@ module OmnifocusMcp
32
24
 
33
25
  Operations::DatabaseStats.get_changes_since(since)
34
26
  end
35
-
36
- STATS_SCRIPT = <<~JS
37
- (() => {
38
- try {
39
- const allTasks = flattenedTasks;
40
- const activeTasks = allTasks.filter(task =>
41
- task.taskStatus !== Task.Status.Completed &&
42
- task.taskStatus !== Task.Status.Dropped
43
- );
44
-
45
- const allProjects = flattenedProjects;
46
- const activeProjects = allProjects.filter(project =>
47
- project.status === Project.Status.Active
48
- );
49
-
50
- const overdueCount = activeTasks.filter(task =>
51
- task.taskStatus === Task.Status.Overdue
52
- ).length;
53
-
54
- const nextActionCount = activeTasks.filter(task =>
55
- task.taskStatus === Task.Status.Next
56
- ).length;
57
-
58
- const flaggedCount = activeTasks.filter(task => task.flagged).length;
59
- const inboxCount = activeTasks.filter(task => task.inInbox).length;
60
-
61
- let lastModified = new Date(0);
62
- allTasks.forEach(task => {
63
- if (task.modificationDate && task.modificationDate > lastModified) {
64
- lastModified = task.modificationDate;
65
- }
66
- });
67
-
68
- return JSON.stringify({
69
- taskCount: allTasks.length,
70
- activeTaskCount: activeTasks.length,
71
- projectCount: allProjects.length,
72
- activeProjectCount: activeProjects.length,
73
- folderCount: flattenedFolders.length,
74
- tagCount: flattenedTags.filter(tag => tag.active).length,
75
- overdueCount: overdueCount,
76
- nextActionCount: nextActionCount,
77
- flaggedCount: flaggedCount,
78
- inboxCount: inboxCount,
79
- lastModified: lastModified.toISOString()
80
- });
81
-
82
- } catch (error) {
83
- return JSON.stringify({
84
- error: "Failed to get database stats: " + error.toString()
85
- });
86
- }
87
- })();
88
- JS
89
-
90
- # rubocop:disable Metrics/MethodLength
91
- def changes_script(since_iso)
92
- escaped_since = Infrastructure::JsEmbed.double_quoted_string(since_iso)
93
-
94
- <<~JS
95
- (() => {
96
- try {
97
- const sinceDate = new Date("#{escaped_since}");
98
-
99
- const allTasks = flattenedTasks;
100
-
101
- const newTasks = allTasks.filter(task =>
102
- task.creationDate && task.creationDate > sinceDate
103
- ).map(task => ({
104
- id: task.id.primaryKey,
105
- name: task.name,
106
- creationDate: task.creationDate.toISOString()
107
- }));
108
-
109
- const updatedTasks = allTasks.filter(task =>
110
- task.modificationDate &&
111
- task.modificationDate > sinceDate &&
112
- task.creationDate &&
113
- task.creationDate <= sinceDate
114
- ).map(task => ({
115
- id: task.id.primaryKey,
116
- name: task.name,
117
- modificationDate: task.modificationDate.toISOString()
118
- }));
119
-
120
- const completedTasks = allTasks.filter(task =>
121
- task.completionDate &&
122
- task.completionDate > sinceDate
123
- ).map(task => ({
124
- id: task.id.primaryKey,
125
- name: task.name,
126
- completionDate: task.completionDate.toISOString()
127
- }));
128
-
129
- const allProjects = flattenedProjects;
130
-
131
- const newProjects = allProjects.filter(project =>
132
- project.creationDate && project.creationDate > sinceDate
133
- ).map(project => ({
134
- id: project.id.primaryKey,
135
- name: project.name,
136
- creationDate: project.creationDate.toISOString()
137
- }));
138
-
139
- const updatedProjects = allProjects.filter(project =>
140
- project.modificationDate &&
141
- project.modificationDate > sinceDate &&
142
- project.creationDate &&
143
- project.creationDate <= sinceDate
144
- ).map(project => ({
145
- id: project.id.primaryKey,
146
- name: project.name,
147
- modificationDate: project.modificationDate.toISOString()
148
- }));
149
-
150
- return JSON.stringify({
151
- newTasks: newTasks,
152
- updatedTasks: updatedTasks,
153
- completedTasks: completedTasks,
154
- newProjects: newProjects,
155
- updatedProjects: updatedProjects
156
- });
157
-
158
- } catch (error) {
159
- return JSON.stringify({
160
- error: "Failed to get changes: " + error.toString()
161
- });
162
- }
163
- })();
164
- JS
165
- end
166
- # rubocop:enable Metrics/MethodLength
167
-
168
- private
169
-
170
- # Collapse a {Infrastructure::ScriptRunner} {Result} into a {Result} over the parsed Hash payload.
171
- def script_payload_result(execution)
172
- execution.and_then do |payload|
173
- if payload.is_a?(Hash) && payload["error"]
174
- Result.error(payload["error"])
175
- else
176
- Result.ok(payload)
177
- end
178
- end
179
- end
180
27
  end
181
28
  end
182
- # rubocop:enable Metrics/ModuleLength
183
29
  end
184
30
  end
@@ -4,14 +4,14 @@ require "fast_mcp"
4
4
 
5
5
  require_relative "mcp_envelope"
6
6
  require_relative "operation_factory"
7
- require_relative "../messages/add_omnifocus_task"
8
- require_relative "../operations/add_omnifocus_task"
7
+ require_relative "../messages/add_omni_focus_task"
8
+ require_relative "../operations/add_omni_focus_task"
9
9
  require_relative "../params"
10
10
 
11
11
  module OmnifocusMcp
12
12
  module Tools
13
13
  module Definitions
14
- # `FastMcp::Tool` for `add_omnifocus_task`.
14
+ # `FastMcp::Tool` for `add_omni_focus_task`.
15
15
  class AddOmniFocusTaskTool < FastMcp::Tool
16
16
  tool_name "add_omnifocus_task"
17
17
  description "Add a new task to OmniFocus"
@@ -24,13 +24,11 @@ module OmnifocusMcp
24
24
  private
25
25
 
26
26
  def us_short_date(date)
27
- # TODO: Fix the formatting. Make Rubocop happy
28
- format("%d/%d/%d", date.month, date.day, date.year)
27
+ format("%<month>d/%<day>d/%<year>d", month: date.month, day: date.day, year: date.year)
29
28
  end
30
29
 
31
30
  def us_compact_date(date)
32
- # TODO: Fix the formatting. Make Rubocop happy
33
- format("%d/%d", date.month, date.day)
31
+ format("%<month>d/%<day>d", month: date.month, day: date.day)
34
32
  end
35
33
 
36
34
  def format_parsed(iso)
@@ -1,14 +1,145 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../database_stats"
3
+ require_relative "../../infrastructure/js_embed"
4
4
 
5
5
  module OmnifocusMcp
6
6
  module Tools
7
7
  module Generators
8
8
  class DatabaseStats
9
9
  class << self
10
- def stats_script = Tools::DatabaseStats.singleton_class.const_get(:STATS_SCRIPT)
11
- def changes_script(...) = Tools::DatabaseStats.changes_script(...)
10
+ STATS_SCRIPT = <<~JS
11
+ (() => {
12
+ try {
13
+ const allTasks = flattenedTasks;
14
+ const activeTasks = allTasks.filter(task =>
15
+ task.taskStatus !== Task.Status.Completed &&
16
+ task.taskStatus !== Task.Status.Dropped
17
+ );
18
+
19
+ const allProjects = flattenedProjects;
20
+ const activeProjects = allProjects.filter(project =>
21
+ project.status === Project.Status.Active
22
+ );
23
+
24
+ const overdueCount = activeTasks.filter(task =>
25
+ task.taskStatus === Task.Status.Overdue
26
+ ).length;
27
+
28
+ const nextActionCount = activeTasks.filter(task =>
29
+ task.taskStatus === Task.Status.Next
30
+ ).length;
31
+
32
+ const flaggedCount = activeTasks.filter(task => task.flagged).length;
33
+ const inboxCount = activeTasks.filter(task => task.inInbox).length;
34
+
35
+ let lastModified = new Date(0);
36
+ allTasks.forEach(task => {
37
+ if (task.modificationDate && task.modificationDate > lastModified) {
38
+ lastModified = task.modificationDate;
39
+ }
40
+ });
41
+
42
+ return JSON.stringify({
43
+ taskCount: allTasks.length,
44
+ activeTaskCount: activeTasks.length,
45
+ projectCount: allProjects.length,
46
+ activeProjectCount: activeProjects.length,
47
+ folderCount: flattenedFolders.length,
48
+ tagCount: flattenedTags.filter(tag => tag.active).length,
49
+ overdueCount: overdueCount,
50
+ nextActionCount: nextActionCount,
51
+ flaggedCount: flaggedCount,
52
+ inboxCount: inboxCount,
53
+ lastModified: lastModified.toISOString()
54
+ });
55
+
56
+ } catch (error) {
57
+ return JSON.stringify({
58
+ error: "Failed to get database stats: " + error.toString()
59
+ });
60
+ }
61
+ })();
62
+ JS
63
+
64
+ def stats_script = STATS_SCRIPT
65
+
66
+ # rubocop:disable Metrics/MethodLength
67
+ def changes_script(since_iso)
68
+ escaped_since = Infrastructure::JsEmbed.double_quoted_string(since_iso)
69
+
70
+ <<~JS
71
+ (() => {
72
+ try {
73
+ const sinceDate = new Date("#{escaped_since}");
74
+
75
+ const allTasks = flattenedTasks;
76
+
77
+ const newTasks = allTasks.filter(task =>
78
+ task.creationDate && task.creationDate > sinceDate
79
+ ).map(task => ({
80
+ id: task.id.primaryKey,
81
+ name: task.name,
82
+ creationDate: task.creationDate.toISOString()
83
+ }));
84
+
85
+ const updatedTasks = allTasks.filter(task =>
86
+ task.modificationDate &&
87
+ task.modificationDate > sinceDate &&
88
+ task.creationDate &&
89
+ task.creationDate <= sinceDate
90
+ ).map(task => ({
91
+ id: task.id.primaryKey,
92
+ name: task.name,
93
+ modificationDate: task.modificationDate.toISOString()
94
+ }));
95
+
96
+ const completedTasks = allTasks.filter(task =>
97
+ task.completionDate &&
98
+ task.completionDate > sinceDate
99
+ ).map(task => ({
100
+ id: task.id.primaryKey,
101
+ name: task.name,
102
+ completionDate: task.completionDate.toISOString()
103
+ }));
104
+
105
+ const allProjects = flattenedProjects;
106
+
107
+ const newProjects = allProjects.filter(project =>
108
+ project.creationDate && project.creationDate > sinceDate
109
+ ).map(project => ({
110
+ id: project.id.primaryKey,
111
+ name: project.name,
112
+ creationDate: project.creationDate.toISOString()
113
+ }));
114
+
115
+ const updatedProjects = allProjects.filter(project =>
116
+ project.modificationDate &&
117
+ project.modificationDate > sinceDate &&
118
+ project.creationDate &&
119
+ project.creationDate <= sinceDate
120
+ ).map(project => ({
121
+ id: project.id.primaryKey,
122
+ name: project.name,
123
+ modificationDate: project.modificationDate.toISOString()
124
+ }));
125
+
126
+ return JSON.stringify({
127
+ newTasks: newTasks,
128
+ updatedTasks: updatedTasks,
129
+ completedTasks: completedTasks,
130
+ newProjects: newProjects,
131
+ updatedProjects: updatedProjects
132
+ });
133
+
134
+ } catch (error) {
135
+ return JSON.stringify({
136
+ error: "Failed to get changes: " + error.toString()
137
+ });
138
+ }
139
+ })();
140
+ JS
141
+ end
142
+ # rubocop:enable Metrics/MethodLength
12
143
  end
13
144
  end
14
145
  end
@@ -43,12 +43,10 @@ module OmnifocusMcp
43
43
  params = Params::McpBoundary.coerce(Params::EditItemParams, params)
44
44
  return missing_identifier_error if Utils::Blank.blank?(params.id, params.name)
45
45
 
46
- id = Infrastructure::AppleScript.escape(params.id.to_s)
47
46
  name = Infrastructure::AppleScript.escape(params.name.to_s)
48
47
  item_type = params.item_type.to_s
49
-
48
+ id = Infrastructure::AppleScript.escape(params.id.to_s)
50
49
  date_pre_scripts, date_assignments = collect_date_assignments(params)
51
-
52
50
  [
53
51
  date_pre_scripts.join("\n\n"),
54
52
  Infrastructure::AppleScript.tell_document(document_body(item_type, id, name, params, date_assignments))
@@ -84,25 +84,24 @@ module OmnifocusMcp
84
84
  # Build the full JXA query script for the given params.
85
85
  def generate_query_script(params)
86
86
  params = Params::McpBoundary.coerce(Params::QueryOmnifocusParams, params)
87
+
87
88
  entity = params.entity.to_s
88
- filters = params.filters || {}
89
- fields = params.fields
90
- limit = params.limit
91
- sort_by = params.sort_by
92
- sort_order = params.sort_order
93
- include_completed = params.include_completed == true
94
- summary = params.summary == true
95
89
 
90
+ filters = params.filters || {}
96
91
  filter_conditions = generate_filter_conditions(entity:, filters:)
97
- field_mapping = generate_field_mapping(entity, fields:)
98
- sort_property = resolve_sort_field(sort_by)
99
- sort_logic = sort_property ? generate_sort_logic(sort_property, sort_order:) : ""
92
+
93
+ field_mapping = generate_field_mapping(entity:, fields: params.fields)
94
+
95
+ sort_property = resolve_sort_field(params.sort_by)
96
+ sort_logic = sort_property ? generate_sort_logic(sort_property, sort_order: params.sort_order) : ""
97
+
98
+ limit = params.limit
100
99
  limit_logic = limit.is_a?(Integer) && limit.positive? ? "filtered = filtered.slice(0, #{limit});" : ""
101
100
 
102
101
  build_query_script(
103
102
  entity: entity,
104
- include_completed: include_completed,
105
- summary: summary,
103
+ include_completed: params.include_completed == true,
104
+ summary: params.summary == true,
106
105
  filter_conditions: filter_conditions,
107
106
  field_mapping: field_mapping,
108
107
  sort_logic: sort_logic,
@@ -159,7 +158,7 @@ module OmnifocusMcp
159
158
  # Build the per-item field-projection block. With no `fields` array
160
159
  # (nil or empty), returns the default field set for the entity.
161
160
  # Otherwise builds explicit mappings for each requested field.
162
- def generate_field_mapping(entity, fields: nil)
161
+ def generate_field_mapping(entity:, fields: nil)
163
162
  if fields.nil? || fields.empty?
164
163
  return default_task_mapping if entity == "tasks"
165
164
  return default_project_mapping if entity == "projects"
@@ -233,10 +232,7 @@ module OmnifocusMcp
233
232
 
234
233
  def apply_tag_status_filters(filters:, conditions:, entity:)
235
234
  if entity == "tasks" && filters[:tags] && !filters[:tags].empty?
236
- tag_condition = filters[:tags].map do |tag|
237
- %(item.tags.some(t => t.name === "#{escape_jxa(tag)}"))
238
- end.join(" || ")
239
- conditions << "if (!(#{tag_condition})) return false;"
235
+ conditions << tag_filter_condition(filters[:tags])
240
236
  end
241
237
 
242
238
  return unless filters[:status] && !filters[:status].empty?
@@ -250,6 +246,13 @@ module OmnifocusMcp
250
246
  conditions << "if (!(#{status_condition})) return false;"
251
247
  end
252
248
 
249
+ def tag_filter_condition(tags)
250
+ tag_condition = tags.map do |tag|
251
+ %(item.tags.some(t => t.name === "#{escape_jxa(tag)}"))
252
+ end.join(" || ")
253
+ "if (!(#{tag_condition})) return false;"
254
+ end
255
+
253
256
  def apply_task_date_filters(filters:, conditions:)
254
257
  conditions << "if (item.flagged !== #{filters[:flagged]}) return false;" unless filters[:flagged].nil?
255
258
 
@@ -260,12 +263,11 @@ module OmnifocusMcp
260
263
  push_same_day(conditions:, field: "dueDate", value: filters[:due_on])
261
264
  push_same_day(conditions:, field: "deferDate", value: filters[:defer_on])
262
265
  push_same_day(conditions:, field: "plannedDate", value: filters[:planned_on])
263
-
264
- push_within_past(conditions:, field: "added", value: filters[:added_within])
265
266
  push_same_day(conditions:, field: "added", value: filters[:added_on])
267
+ push_same_day(conditions:, field: "completionDate", value: filters[:completed_on])
266
268
 
269
+ push_within_past(conditions:, field: "added", value: filters[:added_within])
267
270
  push_within_past(conditions:, field: "completionDate", value: filters[:completed_within])
268
- push_same_day(conditions:, field: "completionDate", value: filters[:completed_on])
269
271
  end
270
272
 
271
273
  def apply_task_misc_filters(filters:, conditions:)
@@ -3,7 +3,7 @@
3
3
  require_relative "../../infrastructure/script_runner"
4
4
  require_relative "../../parsers/apple_script_envelope"
5
5
  require_relative "../../result"
6
- require_relative "../generators/add_omnifocus_task"
6
+ require_relative "../generators/add_omni_focus_task"
7
7
  require_relative "../params"
8
8
 
9
9
  module OmnifocusMcp
@@ -4,7 +4,7 @@ require_relative "../../../parsers/apple_script_envelope"
4
4
  require_relative "../../../infrastructure/script_runner"
5
5
  require_relative "../../../utils/blank"
6
6
  require_relative "../../../result"
7
- require_relative "../../generators/add_omnifocus_task"
7
+ require_relative "../../generators/add_omni_focus_task"
8
8
  require_relative "param_builder"
9
9
 
10
10
  module OmnifocusMcp
@@ -20,6 +20,7 @@ module OmnifocusMcp
20
20
  def prepare!
21
21
  mark_cycle_failures(cycle_messages: cycle_detector.detect)
22
22
  mark_unknown_parent_temp_id
23
+ self
23
24
  end
24
25
 
25
26
  # Stable order: by hierarchy_level (nil -> 0), then original index.
@@ -3,7 +3,7 @@
3
3
  require_relative "../../infrastructure/script_runner"
4
4
  require_relative "../../result"
5
5
  require_relative "../params"
6
- require_relative "add_omnifocus_task"
6
+ require_relative "add_omni_focus_task"
7
7
  require_relative "add_project"
8
8
  require_relative "batch_add_items/planner"
9
9
  require_relative "batch_add_items/batch_item"
@@ -33,10 +33,10 @@ module OmnifocusMcp
33
33
  def call(items)
34
34
  batch_items = Array(items).map { |item| coerce_item(item) }
35
35
  .then { |coerced| build_batch_items(coerced) }
36
- planner = Planner.new(batch_items).tap(&:prepare!)
37
36
 
38
37
  return OmnifocusMcp::Result.ok(batch_items.map(&:result)) if try_bulk_add!(batch_items:)
39
38
 
39
+ planner = Planner.new(batch_items).prepare!
40
40
  process_items(ordered: planner.processing_order, planner:)
41
41
  planner.finalize_unresolved!
42
42
 
@@ -32,8 +32,10 @@ module OmnifocusMcp
32
32
 
33
33
  def coerce_item(item)
34
34
  case item
35
- when Params::BatchRemoveItemParams then item
36
- when Hash then Params::BatchRemoveItemParams.from_hash(item)
35
+ when Params::BatchRemoveItemParams
36
+ item
37
+ when Hash
38
+ Params::BatchRemoveItemParams.from_hash(item)
37
39
  else raise ArgumentError, "expected BatchRemoveItemParams or Hash, got #{item.class}"
38
40
  end
39
41
  end
@@ -11,6 +11,7 @@ module OmnifocusMcp
11
11
  # Exposes `format_tasks`, `format_projects`, `format_folders`,
12
12
  # `format_query_results`, `format_filters`. Used by
13
13
  # {Definitions::QueryOmnifocusTool} to build the user-facing text reply.
14
+ # rubocop:disable Metrics
14
15
  module QueryResults
15
16
  class << self
16
17
  def format_query_results(items:, entity:, filters: nil)
@@ -49,15 +50,15 @@ module OmnifocusMcp
49
50
  parts << "has note: #{f["hasNote"]}" unless f["hasNote"].nil?
50
51
  parts << "inbox: #{f["inbox"]}" unless f["inbox"].nil?
51
52
 
52
- parts << format_on(label: "due", value: f["dueOn"]) unless f["dueOn"].nil?
53
- parts << format_on(label: "defer", value: f["deferOn"]) unless f["deferOn"].nil?
54
- parts << format_on(label: "planned", value: f["plannedOn"]) unless f["plannedOn"].nil?
53
+ parts << format_on(label: "due", value: f["dueOn"]) if f["dueOn"]
54
+ parts << format_on(label: "defer", value: f["deferOn"]) if f["deferOn"]
55
+ parts << format_on(label: "planned", value: f["plannedOn"]) if f["plannedOn"]
55
56
 
56
- parts << "added within #{f["addedWithin"]} days" unless f["addedWithin"].nil?
57
- parts << "added on day #{f["addedOn"]}" unless f["addedOn"].nil?
57
+ parts << "added within #{f["addedWithin"]} days" if f["addedWithin"]
58
+ parts << "added on day #{f["addedOn"]}" if f["addedOn"]
58
59
  parts << "repeating: #{f["isRepeating"]}" unless f["isRepeating"].nil?
59
- parts << "completed within #{f["completedWithin"]} days" unless f["completedWithin"].nil?
60
- parts << "completed on day #{f["completedOn"]}" unless f["completedOn"].nil?
60
+ parts << "completed within #{f["completedWithin"]} days" if f["completedWithin"]
61
+ parts << "completed on day #{f["completedOn"]}" if f["completedOn"]
61
62
 
62
63
  parts.join(", ")
63
64
  end
@@ -178,6 +179,7 @@ module OmnifocusMcp
178
179
  end
179
180
  end
180
181
  end
182
+ # rubocop:enable Metrics
181
183
  end
182
184
  end
183
185
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmnifocusMcp
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.2"
5
+ VERSION_ARGS = %w[--version -v version].freeze
5
6
  end
data/lib/omnifocus_mcp.rb CHANGED
@@ -22,7 +22,6 @@ require_relative "omnifocus_mcp/utils/apple_script_envelope"
22
22
  require_relative "omnifocus_mcp/utils/apple_script_helpers"
23
23
  require_relative "omnifocus_mcp/utils/blank"
24
24
  require_relative "omnifocus_mcp/utils/date_filter"
25
- require_relative "omnifocus_mcp/utils/date_formatting"
26
25
  require_relative "omnifocus_mcp/utils/iso_date"
27
26
  require_relative "omnifocus_mcp/utils/script_execution"
28
27
 
@@ -39,13 +38,13 @@ require_relative "omnifocus_mcp/tools/generators/query_omnifocus"
39
38
  require_relative "omnifocus_mcp/tools/generators/query_omnifocus_debug"
40
39
  require_relative "omnifocus_mcp/tools/generators/database_stats"
41
40
  require_relative "omnifocus_mcp/tools/generators/add_project"
42
- require_relative "omnifocus_mcp/tools/generators/add_omnifocus_task"
41
+ require_relative "omnifocus_mcp/tools/generators/add_omni_focus_task"
43
42
  require_relative "omnifocus_mcp/tools/generators/edit_item"
44
43
  require_relative "omnifocus_mcp/tools/generators/remove_item"
45
44
  require_relative "omnifocus_mcp/tools/operations/query_omnifocus"
46
45
  require_relative "omnifocus_mcp/tools/operations/query_omnifocus_debug"
47
46
  require_relative "omnifocus_mcp/tools/operations/database_stats"
48
- require_relative "omnifocus_mcp/tools/operations/add_omnifocus_task"
47
+ require_relative "omnifocus_mcp/tools/operations/add_omni_focus_task"
49
48
  require_relative "omnifocus_mcp/tools/operations/add_project"
50
49
  require_relative "omnifocus_mcp/tools/operations/batch_add_items"
51
50
  require_relative "omnifocus_mcp/tools/operations/batch_remove_items"
@@ -61,16 +60,16 @@ require_relative "omnifocus_mcp/tools/presenters/list_perspectives"
61
60
  require_relative "omnifocus_mcp/tools/presenters/list_tags"
62
61
  require_relative "omnifocus_mcp/tools/presenters/perspective_view"
63
62
  require_relative "omnifocus_mcp/tools/presenters/query_reply"
64
- require_relative "omnifocus_mcp/tools/batch_report"
63
+
65
64
  require_relative "omnifocus_mcp/tools/database_stats"
66
65
  require_relative "omnifocus_mcp/tools/presenters/query_results"
67
- require_relative "omnifocus_mcp/tools/query_omnifocus_formatter"
66
+
68
67
  require_relative "omnifocus_mcp/tools/query_statuses"
69
68
  require_relative "omnifocus_mcp/tools/definitions/date_formatter"
70
69
  require_relative "omnifocus_mcp/tools/definitions/key_normalizer"
71
70
  require_relative "omnifocus_mcp/tools/definitions/mcp_envelope"
72
71
  require_relative "omnifocus_mcp/tools/definitions/operation_factory"
73
- require_relative "omnifocus_mcp/tools/messages/add_omnifocus_task"
72
+ require_relative "omnifocus_mcp/tools/messages/add_omni_focus_task"
74
73
  require_relative "omnifocus_mcp/tools/messages/add_project"
75
74
  require_relative "omnifocus_mcp/tools/messages/batch_remove_items"
76
75
  require_relative "omnifocus_mcp/tools/messages/edit_item"
@@ -78,7 +77,7 @@ require_relative "omnifocus_mcp/tools/messages/list_tools"
78
77
  require_relative "omnifocus_mcp/tools/messages/remove_item"
79
78
 
80
79
  # Tool definitions wrap primitives for MCP exposure.
81
- require_relative "omnifocus_mcp/tools/definitions/add_omnifocus_task_tool"
80
+ require_relative "omnifocus_mcp/tools/definitions/add_omni_focus_task_tool"
82
81
  require_relative "omnifocus_mcp/tools/definitions/add_project_tool"
83
82
  require_relative "omnifocus_mcp/tools/definitions/batch_add_items_tool"
84
83
  require_relative "omnifocus_mcp/tools/definitions/batch_remove_items_tool"
@@ -100,3 +99,4 @@ require_relative "omnifocus_mcp/resources/today_resource"
100
99
 
101
100
  # MCP bootstrap depends on everything above.
102
101
  require_relative "omnifocus_mcp/mcp"
102
+ require_relative "omnifocus_mcp/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnifocus_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henry Maddocks
@@ -40,9 +40,9 @@ files:
40
40
  - Rakefile
41
41
  - bin/omnifocus-mcp
42
42
  - lib/omnifocus_mcp.rb
43
+ - lib/omnifocus_mcp/cli.rb
43
44
  - lib/omnifocus_mcp/config.rb
44
45
  - lib/omnifocus_mcp/infrastructure.rb
45
- - lib/omnifocus_mcp/infrastructure/.keep
46
46
  - lib/omnifocus_mcp/infrastructure/apple_script.rb
47
47
  - lib/omnifocus_mcp/infrastructure/apple_script_date_builder.rb
48
48
  - lib/omnifocus_mcp/infrastructure/js_embed.rb
@@ -51,7 +51,6 @@ files:
51
51
  - lib/omnifocus_mcp/logger.rb
52
52
  - lib/omnifocus_mcp/mcp.rb
53
53
  - lib/omnifocus_mcp/parsers.rb
54
- - lib/omnifocus_mcp/parsers/.keep
55
54
  - lib/omnifocus_mcp/parsers/apple_script_envelope.rb
56
55
  - lib/omnifocus_mcp/resources/base.rb
57
56
  - lib/omnifocus_mcp/resources/flagged_resource.rb
@@ -61,9 +60,8 @@ files:
61
60
  - lib/omnifocus_mcp/resources/stats_resource.rb
62
61
  - lib/omnifocus_mcp/resources/today_resource.rb
63
62
  - lib/omnifocus_mcp/result.rb
64
- - lib/omnifocus_mcp/tools/batch_report.rb
65
63
  - lib/omnifocus_mcp/tools/database_stats.rb
66
- - lib/omnifocus_mcp/tools/definitions/add_omnifocus_task_tool.rb
64
+ - lib/omnifocus_mcp/tools/definitions/add_omni_focus_task_tool.rb
67
65
  - lib/omnifocus_mcp/tools/definitions/add_project_tool.rb
68
66
  - lib/omnifocus_mcp/tools/definitions/batch_add_items_tool.rb
69
67
  - lib/omnifocus_mcp/tools/definitions/batch_remove_items_tool.rb
@@ -78,8 +76,7 @@ files:
78
76
  - lib/omnifocus_mcp/tools/definitions/query_omnifocus_tool.rb
79
77
  - lib/omnifocus_mcp/tools/definitions/remove_item_tool.rb
80
78
  - lib/omnifocus_mcp/tools/generators.rb
81
- - lib/omnifocus_mcp/tools/generators/.keep
82
- - lib/omnifocus_mcp/tools/generators/add_omnifocus_task.rb
79
+ - lib/omnifocus_mcp/tools/generators/add_omni_focus_task.rb
83
80
  - lib/omnifocus_mcp/tools/generators/add_project.rb
84
81
  - lib/omnifocus_mcp/tools/generators/database_stats.rb
85
82
  - lib/omnifocus_mcp/tools/generators/edit_item.rb
@@ -90,14 +87,14 @@ files:
90
87
  - lib/omnifocus_mcp/tools/generators/query_omnifocus_debug.rb
91
88
  - lib/omnifocus_mcp/tools/generators/remove_item.rb
92
89
  - lib/omnifocus_mcp/tools/messages.rb
93
- - lib/omnifocus_mcp/tools/messages/add_omnifocus_task.rb
90
+ - lib/omnifocus_mcp/tools/messages/add_omni_focus_task.rb
94
91
  - lib/omnifocus_mcp/tools/messages/add_project.rb
95
92
  - lib/omnifocus_mcp/tools/messages/batch_remove_items.rb
96
93
  - lib/omnifocus_mcp/tools/messages/edit_item.rb
97
94
  - lib/omnifocus_mcp/tools/messages/list_tools.rb
98
95
  - lib/omnifocus_mcp/tools/messages/remove_item.rb
99
96
  - lib/omnifocus_mcp/tools/operations.rb
100
- - lib/omnifocus_mcp/tools/operations/add_omnifocus_task.rb
97
+ - lib/omnifocus_mcp/tools/operations/add_omni_focus_task.rb
101
98
  - lib/omnifocus_mcp/tools/operations/add_project.rb
102
99
  - lib/omnifocus_mcp/tools/operations/batch_add_items.rb
103
100
  - lib/omnifocus_mcp/tools/operations/batch_add_items/batch_item.rb
@@ -123,14 +120,12 @@ files:
123
120
  - lib/omnifocus_mcp/tools/presenters/perspective_view.rb
124
121
  - lib/omnifocus_mcp/tools/presenters/query_reply.rb
125
122
  - lib/omnifocus_mcp/tools/presenters/query_results.rb
126
- - lib/omnifocus_mcp/tools/query_omnifocus_formatter.rb
127
123
  - lib/omnifocus_mcp/tools/query_statuses.rb
128
124
  - lib/omnifocus_mcp/utils/apple_script.rb
129
125
  - lib/omnifocus_mcp/utils/apple_script_envelope.rb
130
126
  - lib/omnifocus_mcp/utils/apple_script_helpers.rb
131
127
  - lib/omnifocus_mcp/utils/blank.rb
132
128
  - lib/omnifocus_mcp/utils/date_filter.rb
133
- - lib/omnifocus_mcp/utils/date_formatting.rb
134
129
  - lib/omnifocus_mcp/utils/iso_date.rb
135
130
  - lib/omnifocus_mcp/utils/omnifocus_scripts/getPerspectiveView.js
136
131
  - lib/omnifocus_mcp/utils/omnifocus_scripts/listPerspectives.js
@@ -1 +0,0 @@
1
-
@@ -1 +0,0 @@
1
-
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "presenters/batch_report"
4
-
5
- module OmnifocusMcp
6
- module Tools
7
- BatchReport = Presenters::BatchReport
8
- end
9
- end
@@ -1 +0,0 @@
1
-
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "presenters/query_results"
4
-
5
- module OmnifocusMcp
6
- module Tools
7
- QueryOmnifocusFormatter = Presenters::QueryResults
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../infrastructure/apple_script_date_builder"
4
-
5
- module OmnifocusMcp
6
- module Utils
7
- DateFormatting = Infrastructure::AppleScriptDateBuilder
8
- end
9
- end