things-mcp 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b5dce71de1e5a5690e3acbf04a39844482fd77a225255e6c0f6ed6174c14746
4
- data.tar.gz: 2ae46521ad48efabb5ea24351f9bae9fc36df2ea1017e0f5a3ff4ef1b7ca6362
3
+ metadata.gz: 66584b39e6d5286abeb4a75f08f029bab5c7c1b7b7f42129ee12986086fa7927
4
+ data.tar.gz: bb7603686e15d0dc366f746e263dbb99cebb00ce35377e8ae49fb7f468658dc0
5
5
  SHA512:
6
- metadata.gz: cb4149f6a999b9928d25d889a8b0d48f998d76c07573588a6b1dfff94a86ccfa2a4ee75156093c9c22483d7822852cbb2559de0418b58217e229cfb8d2f21cf5
7
- data.tar.gz: d919bf9e96b98c7b8e54d69067f8a7aec30d4e53a0f62398fb30c54eadaedce0c18a95c1e12b2878444600356496f4c45a9332a143b8542feb876f81f05be23d
6
+ metadata.gz: 5ff846805290fb8e7e628b8809855c2538852be5f0ea69766d176fa6065081ae93af010b2958c68bdc4ce5014f580375939701bf7964a1d915ea3d72d0d5f30e
7
+ data.tar.gz: 8450f45b5c44b9cf8a37762e48cb7c4fe2b64a6e409ebcd2603036726c49535ad862b9ded6796dc13788694562c10e1406ec7547c7364c1bfa1d91b61312b8e9
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gemspec
6
6
 
7
7
  # Development dependencies
8
8
  group :development do
9
+ gem "debug"
9
10
  gem "rake"
10
11
  gem "rubocop-shopify", require: false
11
12
  end
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Things 3 MCP Server (Ruby)
2
2
 
3
- A Model Context Protocol (MCP) server for Things 3, implemented in Ruby.
3
+ A (clunky) Model Context Protocol (MCP) server for Things 3, implemented in Ruby.
4
4
 
5
5
  ## Features
6
6
 
@@ -16,19 +16,22 @@ A Model Context Protocol (MCP) server for Things 3, implemented in Ruby.
16
16
  2. **Ruby 3.2+** installed (required by the MCP gem dependency)
17
17
  3. **No authentication required** - the server reads from Things' local database
18
18
 
19
- > **Note for macOS users**: The system Ruby (typically 2.6.x) is too old. Install a modern Ruby version:
20
- > ```bash
21
- > # Using Homebrew (recommended)
22
- > brew install ruby
23
- >
24
- > # Or using rbenv
25
- > brew install rbenv
26
- > rbenv install 3.4.1
27
- > rbenv global 3.4.1
28
- > ```
19
+ **Note for macOS users**: The system Ruby is possibly too old. Install a modern Ruby.
29
20
 
30
21
  ## Installation
31
22
 
23
+ ### Option 1: Using the Released Gem (Recommended)
24
+
25
+ 1. Install the gem:
26
+
27
+ ```bash
28
+ gem install things-mcp
29
+ ```
30
+
31
+ 2. The MCP server will be available as `things_mcp_server` in your PATH.
32
+
33
+ ### Option 2: From Source
34
+
32
35
  1. Clone this repository:
33
36
 
34
37
  ```bash
@@ -56,12 +59,6 @@ bin/test_connection
56
59
  THINGS_AUTH_TOKEN=your_token_here bin/test_connection
57
60
  ```
58
61
 
59
- 3. **Via Rake:**
60
-
61
- ```bash
62
- bundle exec rake test
63
- ```
64
-
65
62
  The test script will:
66
63
 
67
64
  - ✓ Check if Things app is running
@@ -71,7 +68,15 @@ The test script will:
71
68
  - ✓ Verify the todo in the database
72
69
  - ✓ Test update operations by completing the test todo (if auth token provided)
73
70
 
74
- 2. Run the MCP server:
71
+ ## Running the MCP Server
72
+
73
+ ### If Using the Gem
74
+
75
+ ```bash
76
+ things_mcp_server
77
+ ```
78
+
79
+ ### If Using from Source
75
80
 
76
81
  ```bash
77
82
  bundle exec ruby bin/things_mcp_server
@@ -90,6 +95,20 @@ Add to your Claude Desktop configuration file:
90
95
  - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
91
96
  - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
92
97
 
98
+ #### Using the Released Gem (Recommended)
99
+
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "things": {
104
+ "command": "things_mcp_server"
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ #### Using from Source
111
+
93
112
  ```json
94
113
  {
95
114
  "mcpServers": {
@@ -116,6 +135,23 @@ For `update-todo` and `update-project` operations, you need to provide a Things
116
135
 
117
136
  2. **Add the token to your configuration:**
118
137
 
138
+ #### Using the Released Gem
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "things": {
144
+ "command": "things_mcp_server",
145
+ "env": {
146
+ "THINGS_AUTH_TOKEN": "your_authorization_token_here"
147
+ }
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ #### Using from Source
154
+
119
155
  ```json
120
156
  {
121
157
  "mcpServers": {
@@ -131,6 +167,8 @@ For `update-todo` and `update-project` operations, you need to provide a Things
131
167
  }
132
168
  ```
133
169
 
170
+ **Note:** Replace `YOUR_USERNAME` with your actual macOS username. If you have trouble finding the exact database path, you can run `find ~/Library/Group\ Containers -name "main.sqlite" 2>/dev/null | grep Things` to locate it.
171
+
134
172
  ⚠️ **Security Note:** Keep your authorization token private. It allows full access to modify your Things data.
135
173
 
136
174
  ### Other MCP Clients
@@ -193,15 +231,18 @@ Once configured, you can use your MCP-compatible AI client to:
193
231
  - `add-project` - Create a new project in Things
194
232
  - `update-todo` - Update an existing todo ⚠️ _Requires auth token_
195
233
  - `update-project` - Update an existing project ⚠️ _Requires auth token_
234
+
235
+ > **Note:** When adding tags to existing todos/projects, the tags must already exist in Things. The URL scheme will not create new tags automatically.
236
+
196
237
  - `show-item` - Show a specific item or list in Things
197
238
  - `search-items` - Search for items and open in Things
198
239
 
199
240
  ## Development
200
241
 
201
- Run tests:
242
+ Run connection tests:
202
243
 
203
244
  ```bash
204
- bundle exec rake test
245
+ bin/test_connection
205
246
  ```
206
247
 
207
248
  Run linter:
@@ -210,12 +251,6 @@ Run linter:
210
251
  bundle exec rake rubocop
211
252
  ```
212
253
 
213
- Run both tests and linter:
214
-
215
- ```bash
216
- bundle exec rake
217
- ```
218
-
219
254
  ## License
220
255
 
221
256
  MIT
data/bin/test_connection CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require_relative "../lib/things_mcp"
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "things_mcp"
6
6
 
7
7
  puts "Testing Things MCP Ruby Server"
8
8
  puts "=" * 40
@@ -71,7 +71,8 @@ begin
71
71
  inbox_todos = ThingsMcp::Database.get_inbox
72
72
  test_todo = inbox_todos.find { |t| t[:title] == test_todo_title }
73
73
  break if test_todo
74
- sleep(1) if i < 2 # Don't sleep after the last attempt
74
+
75
+ sleep(1) if i < 2 # Don't sleep after the last attempt
75
76
  end
76
77
 
77
78
  if test_todo
@@ -81,6 +82,7 @@ begin
81
82
  "completed" => true,
82
83
  })
83
84
 
85
+ # rubocop:disable Metrics/BlockNesting
84
86
  if complete_result[:success]
85
87
  puts "✅ Update operations working - test todo completed and cleaned up"
86
88
  else
@@ -88,6 +90,7 @@ begin
88
90
  puts " You may need to manually complete: '#{test_todo_title}'"
89
91
  @test_failed = true
90
92
  end
93
+ # rubocop:enable Metrics/BlockNesting
91
94
  else
92
95
  puts "❌ Could not find test todo in database after 3 attempts"
93
96
  puts " Database sync delay longer than expected - please manually complete: '#{test_todo_title}'"
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require_relative "../lib/things_mcp/server"
4
+ lib = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require "things_mcp/server"
6
7
 
7
8
  # Run the MCP server
8
9
  ThingsMcp::Server.new.run
@@ -11,31 +11,7 @@ module ThingsMcp
11
11
  # This class provides read-only access to the Things 3 database, enabling retrieval of todos, projects, areas, and
12
12
  # tags. It handles dynamic database path resolution and provides formatted data structures.
13
13
  class Database
14
-
15
14
  class << self
16
- def find_database_path
17
- group_containers_dir = "#{Dir.home}/Library/Group Containers"
18
-
19
- # Find Things-specific directories to avoid permission issues
20
- things_dirs = Dir.glob("#{group_containers_dir}/*").select do |dir|
21
- File.basename(dir).include?("culturedcode.ThingsMac")
22
- end
23
-
24
- things_dirs.each do |things_dir|
25
- # Try new format first (Things 3.15.16+)
26
- new_pattern = "#{things_dir}/ThingsData-*/Things Database.thingsdatabase/main.sqlite"
27
- matches = Dir.glob(new_pattern)
28
- return matches.first unless matches.empty?
29
-
30
- # Fall back to old format
31
- old_pattern = "#{things_dir}/Things Database.thingsdatabase/main.sqlite"
32
- return old_pattern if File.exist?(old_pattern)
33
- end
34
-
35
- # Check environment variable
36
- ENV["THINGSDB"] if ENV["THINGSDB"] && File.exist?(ENV["THINGSDB"])
37
- end
38
-
39
15
  def database_path
40
16
  @database_path ||= find_database_path
41
17
  end
@@ -47,15 +23,6 @@ module ThingsMcp
47
23
  def with_database(&block)
48
24
  db_path = database_path
49
25
  unless db_path
50
- $stderr.puts "DEBUG: Database path search failed"
51
- $stderr.puts "DEBUG: Home directory: #{Dir.home}"
52
- $stderr.puts "DEBUG: Group Containers exists: #{File.exist?("#{Dir.home}/Library/Group Containers")}"
53
- if File.exist?("#{Dir.home}/Library/Group Containers")
54
- containers = Dir.glob("#{Dir.home}/Library/Group Containers/*")
55
- $stderr.puts "DEBUG: Found containers: #{containers.size}"
56
- things_containers = containers.select { |c| File.basename(c).include?("culturedcode") }
57
- $stderr.puts "DEBUG: Things containers: #{things_containers}"
58
- end
59
26
  raise "Things database not found. Please ensure Things 3 is installed and has been launched at least once."
60
27
  end
61
28
 
@@ -335,9 +302,48 @@ module ThingsMcp
335
302
 
336
303
  private
337
304
 
305
+ def find_database_path
306
+ # Use actual user's home directory, not the process owner's
307
+ actual_home = ENV["HOME"] || Dir.home
308
+ # If running as root, try to find the real user's home directory
309
+ if actual_home == "/var/root" && ENV["SUDO_USER"]
310
+ actual_home = "/Users/#{ENV["SUDO_USER"]}"
311
+ elsif actual_home == "/var/root"
312
+ # Fallback: look for the most recent user directory
313
+ user_dirs = Dir.glob("/Users/*").select { |d| File.directory?(d) && !File.basename(d).start_with?(".") }
314
+ actual_home = user_dirs.max_by { |d| File.mtime(d) } if user_dirs.any?
315
+ end
316
+
317
+ group_containers_dir = "#{actual_home}/Library/Group Containers"
318
+
319
+ # Find Things-specific directories to avoid permission issues
320
+ things_dirs = Dir.glob("#{group_containers_dir}/*").select do |dir|
321
+ File.basename(dir).include?("culturedcode.ThingsMac")
322
+ end
323
+
324
+ things_dirs.each do |things_dir|
325
+ # First try to find the current database (not in Backups folder)
326
+ current_pattern = "#{things_dir}/*/Things Database.thingsdatabase/main.sqlite"
327
+ current_matches = Dir.glob(current_pattern).reject { |path| path.include?("/Backups/") }
328
+
329
+ unless current_matches.empty?
330
+ return current_matches.first
331
+ end
332
+
333
+ # Fallback: look for any main.sqlite but exclude backups
334
+ fallback_pattern = "#{things_dir}/*/*/main.sqlite"
335
+ fallback_matches = Dir.glob(fallback_pattern).reject { |path| path.include?("/Backups/") }
336
+
337
+ unless fallback_matches.empty?
338
+ return fallback_matches.first
339
+ end
340
+ end
341
+
342
+ nil
343
+ end
344
+
338
345
  def todo_columns
339
- "uuid, title, notes, status, project, area, start, startDate, " \
340
- "deadline, creationDate, userModificationDate"
346
+ "uuid, title, notes, status, project, area, start, startDate, deadline, creationDate, userModificationDate"
341
347
  end
342
348
 
343
349
  def build_todo_query(project_uuid: nil)
@@ -147,6 +147,5 @@ module ThingsMcp
147
147
 
148
148
  lines.join("\n")
149
149
  end
150
-
151
150
  end
152
151
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require_relative "database"
5
- require_relative "url_scheme"
6
- require_relative "formatters"
4
+ require "things_mcp/database"
5
+ require "things_mcp/url_scheme"
6
+ require "things_mcp/formatters"
7
7
 
8
8
  module ThingsMcp
9
9
  # Tool call handlers for MCP server
@@ -206,8 +206,8 @@ module ThingsMcp
206
206
 
207
207
  def handle_update_todo(args)
208
208
  unless ENV["THINGS_AUTH_TOKEN"]
209
- return "❌ Update operations require authentication. Please set THINGS_AUTH_TOKEN environment variable. " +
210
- "See README for setup instructions."
209
+ return "❌ Update operations require authentication. Please set THINGS_AUTH_TOKEN environment variable. " \
210
+ "See README for setup instructions."
211
211
  end
212
212
 
213
213
  result = UrlScheme.update_todo(args)
@@ -221,8 +221,8 @@ module ThingsMcp
221
221
 
222
222
  def handle_update_project(args)
223
223
  unless ENV["THINGS_AUTH_TOKEN"]
224
- return "❌ Update operations require authentication. Please set THINGS_AUTH_TOKEN environment variable. " +
225
- "See README for setup instructions."
224
+ return "❌ Update operations require authentication. Please set THINGS_AUTH_TOKEN environment variable. " \
225
+ "See README for setup instructions."
226
226
  end
227
227
 
228
228
  result = UrlScheme.update_project(args)
@@ -3,10 +3,10 @@
3
3
  require "mcp"
4
4
  require "mcp/transports/stdio"
5
5
  require "logger"
6
- require_relative "tools"
7
- require_relative "handlers"
8
- require_relative "database"
9
- require_relative "url_scheme"
6
+ require "things_mcp/tools"
7
+ require "things_mcp/handlers"
8
+ require "things_mcp/database"
9
+ require "things_mcp/url_scheme"
10
10
 
11
11
  module ThingsMcp
12
12
  # MCP server implementation for Things 3 integration
@@ -27,7 +27,7 @@ module ThingsMcp
27
27
  @server = MCP::Server.new(
28
28
  name: "things",
29
29
  version: "0.1.0",
30
- tools: @tool_classes
30
+ tools: @tool_classes,
31
31
  )
32
32
  rescue => e
33
33
  $stderr.puts "ERROR during initialization: #{e.class}: #{e.message}"
@@ -62,12 +62,12 @@ module ThingsMcp
62
62
  def check_ruby_version
63
63
  required_version = Gem::Version.new("3.2.0")
64
64
  current_version = Gem::Version.new(RUBY_VERSION)
65
-
65
+
66
66
  return if current_version >= required_version
67
67
 
68
68
  $stderr.puts "❌ Ruby #{RUBY_VERSION} is too old! This server requires Ruby 3.2+"
69
69
  $stderr.flush
70
- exit 1
70
+ exit(1)
71
71
  end
72
72
 
73
73
  def create_tool_classes
@@ -107,12 +107,15 @@ module ThingsMcp
107
107
  },
108
108
  ])
109
109
  rescue => e
110
- MCP::Tool::Response.new([
111
- {
112
- type: "text",
113
- text: "Error: #{e.message}",
114
- },
115
- ])
110
+ # Log the full error to stderr for debugging
111
+ $stderr.puts "ERROR in tool #{name}: #{e.class}: #{e.message}"
112
+ $stderr.puts "Arguments: #{string_arguments.inspect}"
113
+ $stderr.puts "Backtrace:"
114
+ e.backtrace.each { |line| $stderr.puts " #{line}" }
115
+ $stderr.flush
116
+
117
+ # Also raise the error so MCP can handle it properly
118
+ raise e
116
119
  end
117
120
  end
118
121
  end
@@ -112,9 +112,8 @@ module ThingsMcp
112
112
  # Build query string and convert + to %20 for spaces
113
113
  query_string = URI.encode_www_form(params)
114
114
  query_string = query_string.gsub("+", "%20") unless query_string.empty?
115
-
116
- full_url = query_string.empty? ? base_url : "#{base_url}?#{query_string}"
117
115
 
116
+ full_url = query_string.empty? ? base_url : "#{base_url}?#{query_string}"
118
117
 
119
118
  # Ensure Things is running
120
119
  unless things_running?
@@ -151,6 +150,5 @@ module ThingsMcp
151
150
 
152
151
  auth_required_operations.any? { |op| url.start_with?(op) }
153
152
  end
154
-
155
153
  end
156
154
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThingsMcp
4
+ VERSION = "0.1.2"
5
+ end
data/lib/things_mcp.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mcp"
4
- require_relative "things_mcp/server"
5
- require_relative "things_mcp/database"
6
- require_relative "things_mcp/handlers"
7
- require_relative "things_mcp/tools"
8
- require_relative "things_mcp/url_scheme"
9
- require_relative "things_mcp/formatters"
4
+ require "things_mcp/server"
5
+ require "things_mcp/database"
6
+ require "things_mcp/handlers"
7
+ require "things_mcp/tools"
8
+ require "things_mcp/url_scheme"
9
+ require "things_mcp/formatters"
10
10
 
11
11
  # ThingsMcp provides a Model Context Protocol (MCP) server for Things 3
12
12
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: things-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari
@@ -10,52 +10,51 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: mcp
13
+ name: logger
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.1'
18
+ version: '1.4'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.1'
25
+ version: '1.4'
26
26
  - !ruby/object:Gem::Dependency
27
- name: sqlite3
27
+ name: mcp
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '2.0'
32
+ version: '0.1'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.0'
39
+ version: '0.1'
40
40
  - !ruby/object:Gem::Dependency
41
- name: logger
41
+ name: sqlite3
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.4'
46
+ version: '2.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.4'
53
+ version: '2.0'
54
54
  description: A Model Context Protocol (MCP) server for Things 3, implemented in Ruby
55
55
  email:
56
56
  - hakanensari@gmail.com
57
57
  executables:
58
- - test_connection
59
58
  - things_mcp_server
60
59
  extensions: []
61
60
  extra_rdoc_files: []
@@ -73,6 +72,7 @@ files:
73
72
  - lib/things_mcp/server.rb
74
73
  - lib/things_mcp/tools.rb
75
74
  - lib/things_mcp/url_scheme.rb
75
+ - lib/things_mcp/version.rb
76
76
  homepage: https://github.com/hakanensari/things-mcp-ruby
77
77
  licenses:
78
78
  - MIT