ticktick-mcp-server 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Dockerfile +15 -0
- data/Dockerfile.dev +8 -0
- data/README.md +104 -4
- data/exe/ticktick-auth +56 -0
- data/lib/ticktick/auth/callback_server.rb +91 -0
- data/lib/ticktick/auth/oauth_flow.rb +99 -0
- data/lib/ticktick/mcp/server/version.rb +1 -1
- metadata +35 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57893a42594197579a7d035f6e2b0f77ccb0de47a8b5eea857ec6d1d8b1f078d
|
|
4
|
+
data.tar.gz: 11c46a9168ad7edaf65894b347d6669909aa539a201f601b032ff6d40cb7ac3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e254267fa319f2d444826e38a56b5921b1dfaa663856d0430ea49e6efb228ad190dd616fe4019e20a7445e511bbf67a73c640925492dbcf385a009ba11a45ea
|
|
7
|
+
data.tar.gz: ad77646c2214318b9c8a509de044167203c92db95d33e18c84a3a9b5c5b52456ebd2a5d9a1e7d8672614c076e42dba5aeb953c162c73c5b23cd1268dbfb24104
|
data/.rubocop.yml
CHANGED
data/Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM ruby:3.4-alpine
|
|
3
|
+
|
|
4
|
+
RUN apk add --no-cache \
|
|
5
|
+
build-base \
|
|
6
|
+
ca-certificates \
|
|
7
|
+
tzdata
|
|
8
|
+
|
|
9
|
+
RUN addgroup -S mcp && adduser -S mcp -G mcp
|
|
10
|
+
|
|
11
|
+
RUN gem install ticktick-mcp-server --no-document
|
|
12
|
+
|
|
13
|
+
USER mcp
|
|
14
|
+
|
|
15
|
+
CMD ["ticktick-mcp-server"]
|
data/Dockerfile.dev
ADDED
data/README.md
CHANGED
|
@@ -4,12 +4,34 @@ A Ruby gem that exposes the [TickTick Open API](https://developer.ticktick.com/)
|
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
- Ruby >= 3.1.0
|
|
8
7
|
- A TickTick account with an Open API access token
|
|
8
|
+
- One of the following:
|
|
9
|
+
- Docker (recommended — no Ruby required)
|
|
10
|
+
- Ruby >= 3.1.0
|
|
9
11
|
|
|
10
12
|
## Installation
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
### Option 1: Docker (recommended)
|
|
15
|
+
|
|
16
|
+
No Ruby installation required. Pull the pre-built image from GitHub Container Registry:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
docker pull ghcr.io/j-o-lantern0422/ticktick-mcp-server:latest
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or build locally from the repository:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
docker build -t ticktick-mcp-server https://github.com/j-o-lantern0422/ticktick-mcp-server.git#main
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Option 2: RubyGems
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gem install ticktick-mcp-server
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Option 3: Clone the repository
|
|
13
35
|
|
|
14
36
|
```bash
|
|
15
37
|
git clone https://github.com/j-o-lantern0422/ticktick-mcp-server
|
|
@@ -26,11 +48,89 @@ Set the following environment variable with your TickTick Open API access token:
|
|
|
26
48
|
TICKTICK_ACCESS_TOKEN=your_access_token_here
|
|
27
49
|
```
|
|
28
50
|
|
|
29
|
-
|
|
51
|
+
### Getting an Access Token
|
|
52
|
+
|
|
53
|
+
Use the built-in `ticktick-auth` tool to obtain a token via OAuth2.
|
|
54
|
+
|
|
55
|
+
#### Step 1: Register your application
|
|
56
|
+
|
|
57
|
+
Go to the [TickTick Developer Center](https://developer.ticktick.com/) and create an OAuth2 application.
|
|
58
|
+
Set the redirect URI to:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
http://localhost:8585/callback
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Step 2: Run the authentication tool
|
|
65
|
+
|
|
66
|
+
**With Docker:**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
docker run --rm -p 8585:8585 ghcr.io/j-o-lantern0422/ticktick-mcp-server:latest \
|
|
70
|
+
ticktick-auth --client-id=YOUR_CLIENT_ID --client-secret=YOUR_CLIENT_SECRET
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**With the gem installed locally:**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
ticktick-auth --client-id=YOUR_CLIENT_ID --client-secret=YOUR_CLIENT_SECRET
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can also pass credentials via environment variables instead of flags:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export TICKTICK_CLIENT_ID=YOUR_CLIENT_ID
|
|
83
|
+
export TICKTICK_CLIENT_SECRET=YOUR_CLIENT_SECRET
|
|
84
|
+
ticktick-auth
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> **Note:** The default callback port is `8585`. Use `--port PORT` (or `TICKTICK_AUTH_PORT`) to change it.
|
|
88
|
+
> If you change the port, update the redirect URI in the developer settings accordingly.
|
|
89
|
+
|
|
90
|
+
#### Step 3: Authorize in the browser
|
|
91
|
+
|
|
92
|
+
The tool will print an authorization URL. Open it in your browser and approve the request.
|
|
93
|
+
|
|
94
|
+
#### Step 4: Copy the token
|
|
95
|
+
|
|
96
|
+
After authorization, the token is printed to stdout:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
TICKTICK_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Copy this value and use it as your `TICKTICK_ACCESS_TOKEN`.
|
|
30
103
|
|
|
31
104
|
## Usage
|
|
32
105
|
|
|
33
|
-
###
|
|
106
|
+
### Running with Docker
|
|
107
|
+
|
|
108
|
+
Add the following to your `claude_desktop_config.json`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"ticktick": {
|
|
114
|
+
"command": "docker",
|
|
115
|
+
"args": [
|
|
116
|
+
"run",
|
|
117
|
+
"--rm",
|
|
118
|
+
"-i",
|
|
119
|
+
"-e",
|
|
120
|
+
"TICKTICK_ACCESS_TOKEN=your_access_token_here",
|
|
121
|
+
"ghcr.io/j-o-lantern0422/ticktick-mcp-server:latest"
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> **Note:**
|
|
129
|
+
> - `-i` is required for STDIO communication between Claude Desktop and the MCP server.
|
|
130
|
+
> - Do **not** use `-t` (pseudo-tty). It causes CR/LF conversion that breaks the JSON-RPC protocol.
|
|
131
|
+
> - `--rm` automatically removes the container when the session ends.
|
|
132
|
+
|
|
133
|
+
### Claude Desktop Integration (gem installed locally)
|
|
34
134
|
|
|
35
135
|
Add the following to your `claude_desktop_config.json`:
|
|
36
136
|
|
data/exe/ticktick-auth
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$stdout.sync = true
|
|
5
|
+
|
|
6
|
+
require "optparse"
|
|
7
|
+
require "ticktick/auth/oauth_flow"
|
|
8
|
+
|
|
9
|
+
options = {
|
|
10
|
+
client_id: ENV["TICKTICK_CLIENT_ID"],
|
|
11
|
+
client_secret: ENV["TICKTICK_CLIENT_SECRET"],
|
|
12
|
+
port: (ENV["TICKTICK_AUTH_PORT"] || 8585).to_i
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
OptionParser.new do |opts|
|
|
16
|
+
opts.banner = "Usage: ticktick-auth [options]"
|
|
17
|
+
|
|
18
|
+
opts.on("--client-id ID", "TickTick OAuth2 client ID") { |v| options[:client_id] = v }
|
|
19
|
+
opts.on("--client-secret SECRET", "TickTick OAuth2 client secret") { |v| options[:client_secret] = v }
|
|
20
|
+
opts.on("--port PORT", Integer, "Callback server port (default: 8585)") { |v| options[:port] = v }
|
|
21
|
+
opts.on("-h", "--help", "Show this help") do
|
|
22
|
+
puts opts
|
|
23
|
+
exit 0
|
|
24
|
+
end
|
|
25
|
+
end.parse!
|
|
26
|
+
|
|
27
|
+
errors = []
|
|
28
|
+
errors << "client_id is required (--client-id or TICKTICK_CLIENT_ID)" unless options[:client_id]
|
|
29
|
+
errors << "client_secret is required (--client-secret or TICKTICK_CLIENT_SECRET)" unless options[:client_secret]
|
|
30
|
+
|
|
31
|
+
unless errors.empty?
|
|
32
|
+
errors.each { |e| warn "Error: #{e}" }
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
flow = Ticktick::Auth::OauthFlow.new(
|
|
38
|
+
client_id: options[:client_id],
|
|
39
|
+
client_secret: options[:client_secret],
|
|
40
|
+
port: options[:port]
|
|
41
|
+
)
|
|
42
|
+
token = flow.run
|
|
43
|
+
puts "TICKTICK_ACCESS_TOKEN=#{token}"
|
|
44
|
+
rescue Ticktick::Auth::OauthFlow::TimeoutError => e
|
|
45
|
+
warn "Error: #{e.message}"
|
|
46
|
+
exit 1
|
|
47
|
+
rescue Ticktick::Auth::OauthFlow::DeniedError => e
|
|
48
|
+
warn "Error: #{e.message}"
|
|
49
|
+
exit 1
|
|
50
|
+
rescue Ticktick::Auth::OauthFlow::NetworkError => e
|
|
51
|
+
warn "Error: #{e.message}"
|
|
52
|
+
exit 1
|
|
53
|
+
rescue Ticktick::Auth::OauthFlow::Error => e
|
|
54
|
+
warn "Error: #{e.message}"
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webrick"
|
|
4
|
+
|
|
5
|
+
module Ticktick
|
|
6
|
+
module Auth
|
|
7
|
+
class CallbackServer
|
|
8
|
+
CALLBACK_PATH = "/callback"
|
|
9
|
+
SUCCESS_HTML = <<~HTML
|
|
10
|
+
<!DOCTYPE html>
|
|
11
|
+
<html>
|
|
12
|
+
<head><meta charset="utf-8"><title>TickTick Authorization</title></head>
|
|
13
|
+
<body>
|
|
14
|
+
<h1>Authorization successful!</h1>
|
|
15
|
+
<p>You can close this browser tab and return to the terminal.</p>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
HTML
|
|
19
|
+
ERROR_HTML = <<~HTML
|
|
20
|
+
<!DOCTYPE html>
|
|
21
|
+
<html>
|
|
22
|
+
<head><meta charset="utf-8"><title>TickTick Authorization</title></head>
|
|
23
|
+
<body>
|
|
24
|
+
<h1>Authorization failed</h1>
|
|
25
|
+
<p>Access was denied. Please try again.</p>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
HTML
|
|
29
|
+
|
|
30
|
+
def initialize(port:)
|
|
31
|
+
@port = port
|
|
32
|
+
@queue = Queue.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def wait_for_code(timeout: 300)
|
|
36
|
+
server = build_server
|
|
37
|
+
server_thread = Thread.new { server.start }
|
|
38
|
+
|
|
39
|
+
result = poll_for_result(timeout)
|
|
40
|
+
server.shutdown
|
|
41
|
+
server_thread.join(5)
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_server
|
|
48
|
+
logger = WEBrick::Log.new(nil, WEBrick::Log::FATAL)
|
|
49
|
+
access_log = [[nil, ""]]
|
|
50
|
+
|
|
51
|
+
server = WEBrick::HTTPServer.new(
|
|
52
|
+
Port: @port,
|
|
53
|
+
Logger: logger,
|
|
54
|
+
AccessLog: access_log
|
|
55
|
+
)
|
|
56
|
+
server.mount_proc(CALLBACK_PATH) { |req, res| handle_callback(req, res) }
|
|
57
|
+
server
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_callback(req, res)
|
|
61
|
+
code = req.query["code"]
|
|
62
|
+
error = req.query["error"]
|
|
63
|
+
state = req.query["state"]
|
|
64
|
+
|
|
65
|
+
if code
|
|
66
|
+
@queue.push({ code: code, state: state })
|
|
67
|
+
write_response(res, SUCCESS_HTML)
|
|
68
|
+
else
|
|
69
|
+
@queue.push({ error: error || "unknown_error" })
|
|
70
|
+
write_response(res, ERROR_HTML)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def write_response(res, html)
|
|
75
|
+
res.status = 200
|
|
76
|
+
res.content_type = "text/html; charset=utf-8"
|
|
77
|
+
res.body = html
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def poll_for_result(timeout)
|
|
81
|
+
deadline = Time.now + timeout
|
|
82
|
+
loop do
|
|
83
|
+
return @queue.pop(true) unless @queue.empty?
|
|
84
|
+
return nil if Time.now >= deadline
|
|
85
|
+
|
|
86
|
+
sleep 0.1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "base64"
|
|
7
|
+
require_relative "callback_server"
|
|
8
|
+
|
|
9
|
+
module Ticktick
|
|
10
|
+
module Auth
|
|
11
|
+
class OauthFlow
|
|
12
|
+
AUTHORIZE_URL = "https://ticktick.com/oauth/authorize"
|
|
13
|
+
TOKEN_URL = "https://ticktick.com/oauth/token"
|
|
14
|
+
SCOPE = "tasks:read tasks:write"
|
|
15
|
+
|
|
16
|
+
Error = Class.new(StandardError)
|
|
17
|
+
TimeoutError = Class.new(Error)
|
|
18
|
+
DeniedError = Class.new(Error)
|
|
19
|
+
NetworkError = Class.new(Error)
|
|
20
|
+
|
|
21
|
+
def initialize(client_id:, client_secret:, port: 8585)
|
|
22
|
+
@client_id = client_id
|
|
23
|
+
@client_secret = client_secret
|
|
24
|
+
@port = port
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
state = SecureRandom.hex(16)
|
|
29
|
+
redirect_uri = "http://localhost:#{@port}/callback"
|
|
30
|
+
|
|
31
|
+
puts build_authorize_message(state, redirect_uri)
|
|
32
|
+
|
|
33
|
+
server = CallbackServer.new(port: @port)
|
|
34
|
+
result = server.wait_for_code(timeout: 300)
|
|
35
|
+
|
|
36
|
+
raise TimeoutError, "Timed out waiting for authorization. Please try again." if result.nil?
|
|
37
|
+
raise DeniedError, "Authorization was denied: #{result[:error]}" if result[:error]
|
|
38
|
+
raise Error, "State mismatch. Possible CSRF attack." if result[:state] != state
|
|
39
|
+
|
|
40
|
+
exchange_code_for_token(result[:code], redirect_uri)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def build_authorize_message(state, redirect_uri)
|
|
46
|
+
url = build_authorize_url(state, redirect_uri)
|
|
47
|
+
|
|
48
|
+
<<~MSG
|
|
49
|
+
Open the following URL in your browser to authorize TickTick:
|
|
50
|
+
|
|
51
|
+
#{url}
|
|
52
|
+
|
|
53
|
+
Waiting for authorization (timeout: 5 minutes)...
|
|
54
|
+
MSG
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_authorize_url(state, redirect_uri)
|
|
58
|
+
params = URI.encode_www_form(
|
|
59
|
+
client_id: @client_id,
|
|
60
|
+
response_type: "code",
|
|
61
|
+
scope: SCOPE,
|
|
62
|
+
redirect_uri: redirect_uri,
|
|
63
|
+
state: state
|
|
64
|
+
)
|
|
65
|
+
"#{AUTHORIZE_URL}?#{params}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def exchange_code_for_token(code, redirect_uri)
|
|
69
|
+
conn = Faraday.new(url: TOKEN_URL)
|
|
70
|
+
credentials = Base64.strict_encode64("#{@client_id}:#{@client_secret}")
|
|
71
|
+
|
|
72
|
+
response = conn.post { |req| configure_token_request(req, credentials, code, redirect_uri) }
|
|
73
|
+
handle_token_response(response)
|
|
74
|
+
rescue Faraday::Error => e
|
|
75
|
+
raise NetworkError, "Network error during token exchange: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def configure_token_request(req, credentials, code, redirect_uri)
|
|
79
|
+
req.headers["Authorization"] = "Basic #{credentials}"
|
|
80
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
81
|
+
req.body = URI.encode_www_form(
|
|
82
|
+
grant_type: "authorization_code",
|
|
83
|
+
code: code,
|
|
84
|
+
redirect_uri: redirect_uri
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def handle_token_response(response)
|
|
89
|
+
raise Error, "Token exchange failed (HTTP #{response.status}): #{response.body}" unless response.success?
|
|
90
|
+
|
|
91
|
+
data = JSON.parse(response.body)
|
|
92
|
+
token = data["access_token"]
|
|
93
|
+
raise Error, "No access_token in response: #{response.body}" unless token
|
|
94
|
+
|
|
95
|
+
token
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ticktick-mcp-server
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- j-o-lantern0422
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: faraday
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,11 +51,26 @@ dependencies:
|
|
|
37
51
|
- - ">="
|
|
38
52
|
- !ruby/object:Gem::Version
|
|
39
53
|
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: webrick
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
40
68
|
description: A Model Context Protocol (MCP) server that provides tools to interact
|
|
41
69
|
with the TickTick API
|
|
42
70
|
email:
|
|
43
71
|
- j.o.lantern0422@gmail.com
|
|
44
72
|
executables:
|
|
73
|
+
- ticktick-auth
|
|
45
74
|
- ticktick-mcp-server
|
|
46
75
|
extensions: []
|
|
47
76
|
extra_rdoc_files: []
|
|
@@ -49,10 +78,15 @@ files:
|
|
|
49
78
|
- ".rspec"
|
|
50
79
|
- ".rubocop.yml"
|
|
51
80
|
- CHANGELOG.md
|
|
81
|
+
- Dockerfile
|
|
82
|
+
- Dockerfile.dev
|
|
52
83
|
- LICENSE.txt
|
|
53
84
|
- README.md
|
|
54
85
|
- Rakefile
|
|
86
|
+
- exe/ticktick-auth
|
|
55
87
|
- exe/ticktick-mcp-server
|
|
88
|
+
- lib/ticktick/auth/callback_server.rb
|
|
89
|
+
- lib/ticktick/auth/oauth_flow.rb
|
|
56
90
|
- lib/ticktick/client.rb
|
|
57
91
|
- lib/ticktick/errors.rb
|
|
58
92
|
- lib/ticktick/http_connection.rb
|