things-mcp 0.7.0__py3-none-any.whl
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.
- things_mcp/__init__.py +5 -0
- things_mcp/__main__.py +6 -0
- things_mcp/formatters.py +280 -0
- things_mcp/server.py +477 -0
- things_mcp/url_scheme.py +233 -0
- things_mcp-0.7.0.dist-info/METADATA +413 -0
- things_mcp-0.7.0.dist-info/RECORD +10 -0
- things_mcp-0.7.0.dist-info/WHEEL +4 -0
- things_mcp-0.7.0.dist-info/entry_points.txt +2 -0
- things_mcp-0.7.0.dist-info/licenses/LICENSE +21 -0
things_mcp/__init__.py
ADDED
things_mcp/__main__.py
ADDED
things_mcp/formatters.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import things
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
def _calculate_age(date_str: str) -> str:
|
|
8
|
+
"""Helper function to calculate human-readable age from a date string.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
date_str: ISO format date string
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Human-readable age string (e.g., "3 days ago", "2 weeks ago")
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
ValueError: If date string cannot be parsed
|
|
18
|
+
TypeError: If date_str is not a string
|
|
19
|
+
"""
|
|
20
|
+
date_obj = datetime.fromisoformat(str(date_str))
|
|
21
|
+
age = datetime.now() - date_obj
|
|
22
|
+
days = age.days
|
|
23
|
+
|
|
24
|
+
if days == 0:
|
|
25
|
+
return "today"
|
|
26
|
+
elif days == 1:
|
|
27
|
+
return "1 day ago"
|
|
28
|
+
elif days < 7:
|
|
29
|
+
return f"{days} days ago"
|
|
30
|
+
elif days < 30:
|
|
31
|
+
weeks = days // 7
|
|
32
|
+
return f"{weeks} week{'s' if weeks > 1 else ''} ago"
|
|
33
|
+
elif days < 365:
|
|
34
|
+
months = days // 30
|
|
35
|
+
return f"{months} month{'s' if months > 1 else ''} ago"
|
|
36
|
+
else:
|
|
37
|
+
years = days // 365
|
|
38
|
+
return f"{years} year{'s' if years > 1 else ''} ago"
|
|
39
|
+
|
|
40
|
+
def format_todo(todo: dict) -> str:
|
|
41
|
+
"""Helper function to format a single todo into a readable string."""
|
|
42
|
+
logger.debug(f"Formatting todo: {todo}")
|
|
43
|
+
todo_text = f"Title: {todo['title']}"
|
|
44
|
+
|
|
45
|
+
# Add UUID for reference
|
|
46
|
+
todo_text += f"\nUUID: {todo['uuid']}"
|
|
47
|
+
|
|
48
|
+
# Add type
|
|
49
|
+
todo_text += f"\nType: {todo['type']}"
|
|
50
|
+
|
|
51
|
+
# Add status if present
|
|
52
|
+
if todo.get('status'):
|
|
53
|
+
todo_text += f"\nStatus: {todo['status']}"
|
|
54
|
+
|
|
55
|
+
# Add start/list location
|
|
56
|
+
if todo.get('start'):
|
|
57
|
+
todo_text += f"\nList: {todo['start']}"
|
|
58
|
+
|
|
59
|
+
# Add dates
|
|
60
|
+
if todo.get('start_date'):
|
|
61
|
+
todo_text += f"\nStart Date: {todo['start_date']}"
|
|
62
|
+
if todo.get('deadline'):
|
|
63
|
+
todo_text += f"\nDeadline: {todo['deadline']}"
|
|
64
|
+
if todo.get('stop_date'): # Completion date
|
|
65
|
+
todo_text += f"\nCompleted: {todo['stop_date']}"
|
|
66
|
+
|
|
67
|
+
# Add creation and modification dates
|
|
68
|
+
if todo.get('created'):
|
|
69
|
+
todo_text += f"\nCreated: {todo['created']}"
|
|
70
|
+
# Calculate age since creation
|
|
71
|
+
try:
|
|
72
|
+
age_text = _calculate_age(todo['created'])
|
|
73
|
+
todo_text += f"\nAge: {age_text}"
|
|
74
|
+
except (ValueError, TypeError):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
if todo.get('modified'):
|
|
78
|
+
todo_text += f"\nModified: {todo['modified']}"
|
|
79
|
+
# Calculate time since last modification
|
|
80
|
+
try:
|
|
81
|
+
modified_age = _calculate_age(todo['modified'])
|
|
82
|
+
todo_text += f"\nLast modified: {modified_age}"
|
|
83
|
+
except (ValueError, TypeError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Add notes if present
|
|
87
|
+
if todo.get('notes'):
|
|
88
|
+
todo_text += f"\nNotes: {todo['notes']}"
|
|
89
|
+
|
|
90
|
+
# Add project info if present
|
|
91
|
+
if todo.get('project'):
|
|
92
|
+
try:
|
|
93
|
+
project = things.get(todo['project'])
|
|
94
|
+
if project:
|
|
95
|
+
todo_text += f"\nProject: {project['title']}"
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Add heading info if present
|
|
100
|
+
if todo.get('heading'):
|
|
101
|
+
try:
|
|
102
|
+
heading = things.get(todo['heading'])
|
|
103
|
+
if heading:
|
|
104
|
+
todo_text += f"\nHeading: {heading['title']}"
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Add area info if present
|
|
109
|
+
if todo.get('area'):
|
|
110
|
+
try:
|
|
111
|
+
area = things.get(todo['area'])
|
|
112
|
+
if area:
|
|
113
|
+
todo_text += f"\nArea: {area['title']}"
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# Add tags if present
|
|
118
|
+
if todo.get('tags'):
|
|
119
|
+
todo_text += f"\nTags: {', '.join(todo['tags'])}"
|
|
120
|
+
|
|
121
|
+
# Add checklist if present and contains items
|
|
122
|
+
if isinstance(todo.get('checklist'), list):
|
|
123
|
+
todo_text += "\nChecklist:"
|
|
124
|
+
for item in todo['checklist']:
|
|
125
|
+
checkbox = "✓" if item.get('status') == 'completed' else "☐"
|
|
126
|
+
todo_text += f"\n {checkbox} {item['title']}"
|
|
127
|
+
|
|
128
|
+
return todo_text
|
|
129
|
+
|
|
130
|
+
def format_project(project: dict, include_items: bool = False) -> str:
|
|
131
|
+
"""Helper function to format a single project."""
|
|
132
|
+
project_text = f"Title: {project['title']}\nUUID: {project['uuid']}"
|
|
133
|
+
|
|
134
|
+
if project.get('area'):
|
|
135
|
+
try:
|
|
136
|
+
area = things.get(project['area'])
|
|
137
|
+
if area:
|
|
138
|
+
project_text += f"\nArea: {area['title']}"
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
if project.get('notes'):
|
|
143
|
+
project_text += f"\nNotes: {project['notes']}"
|
|
144
|
+
|
|
145
|
+
# Add creation and modification dates
|
|
146
|
+
if project.get('created'):
|
|
147
|
+
project_text += f"\nCreated: {project['created']}"
|
|
148
|
+
# Calculate age since creation
|
|
149
|
+
try:
|
|
150
|
+
age_text = _calculate_age(project['created'])
|
|
151
|
+
project_text += f"\nAge: {age_text}"
|
|
152
|
+
except (ValueError, TypeError):
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
if project.get('modified'):
|
|
156
|
+
project_text += f"\nModified: {project['modified']}"
|
|
157
|
+
# Calculate time since last modification
|
|
158
|
+
try:
|
|
159
|
+
modified_age = _calculate_age(project['modified'])
|
|
160
|
+
project_text += f"\nLast modified: {modified_age}"
|
|
161
|
+
except (ValueError, TypeError):
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Always show headings for projects
|
|
165
|
+
headings = things.tasks(type='heading', project=project['uuid'])
|
|
166
|
+
if headings:
|
|
167
|
+
project_text += "\n\nHeadings:"
|
|
168
|
+
for heading in headings:
|
|
169
|
+
project_text += f"\n- {heading['title']}"
|
|
170
|
+
|
|
171
|
+
if include_items:
|
|
172
|
+
todos = things.todos(project=project['uuid'])
|
|
173
|
+
if todos:
|
|
174
|
+
project_text += "\n\nTasks:"
|
|
175
|
+
for todo in todos:
|
|
176
|
+
project_text += f"\n- {todo['title']}"
|
|
177
|
+
|
|
178
|
+
return project_text
|
|
179
|
+
|
|
180
|
+
def format_area(area: dict, include_items: bool = False) -> str:
|
|
181
|
+
"""Helper function to format a single area."""
|
|
182
|
+
area_text = f"Title: {area['title']}\nUUID: {area['uuid']}"
|
|
183
|
+
|
|
184
|
+
if area.get('notes'):
|
|
185
|
+
area_text += f"\nNotes: {area['notes']}"
|
|
186
|
+
|
|
187
|
+
# Add creation and modification dates
|
|
188
|
+
if area.get('created'):
|
|
189
|
+
area_text += f"\nCreated: {area['created']}"
|
|
190
|
+
try:
|
|
191
|
+
age_text = _calculate_age(area['created'])
|
|
192
|
+
area_text += f"\nAge: {age_text}"
|
|
193
|
+
except (ValueError, TypeError):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
if area.get('modified'):
|
|
197
|
+
area_text += f"\nModified: {area['modified']}"
|
|
198
|
+
try:
|
|
199
|
+
modified_age = _calculate_age(area['modified'])
|
|
200
|
+
area_text += f"\nLast modified: {modified_age}"
|
|
201
|
+
except (ValueError, TypeError):
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
if include_items:
|
|
205
|
+
projects = things.projects(area=area['uuid'])
|
|
206
|
+
if projects:
|
|
207
|
+
area_text += "\n\nProjects:"
|
|
208
|
+
for project in projects:
|
|
209
|
+
area_text += f"\n- {project['title']}"
|
|
210
|
+
|
|
211
|
+
todos = things.todos(area=area['uuid'])
|
|
212
|
+
if todos:
|
|
213
|
+
area_text += "\n\nTasks:"
|
|
214
|
+
for todo in todos:
|
|
215
|
+
area_text += f"\n- {todo['title']}"
|
|
216
|
+
|
|
217
|
+
return area_text
|
|
218
|
+
|
|
219
|
+
def format_tag(tag: dict, include_items: bool = False) -> str:
|
|
220
|
+
"""Helper function to format a single tag."""
|
|
221
|
+
tag_text = f"Title: {tag['title']}\nUUID: {tag['uuid']}"
|
|
222
|
+
|
|
223
|
+
if tag.get('shortcut'):
|
|
224
|
+
tag_text += f"\nShortcut: {tag['shortcut']}"
|
|
225
|
+
|
|
226
|
+
if include_items:
|
|
227
|
+
todos = things.todos(tag=tag['title'])
|
|
228
|
+
if todos:
|
|
229
|
+
tag_text += "\n\nTagged Items:"
|
|
230
|
+
for todo in todos:
|
|
231
|
+
tag_text += f"\n- {todo['title']}"
|
|
232
|
+
|
|
233
|
+
return tag_text
|
|
234
|
+
|
|
235
|
+
def format_heading(heading: dict, include_items: bool = False) -> str:
|
|
236
|
+
"""Helper function to format a single heading."""
|
|
237
|
+
heading_text = f"Title: {heading['title']}\nUUID: {heading['uuid']}"
|
|
238
|
+
heading_text += f"\nType: heading"
|
|
239
|
+
|
|
240
|
+
# Add project info if present
|
|
241
|
+
if heading.get('project'):
|
|
242
|
+
if heading.get('project_title'):
|
|
243
|
+
heading_text += f"\nProject: {heading['project_title']}"
|
|
244
|
+
else:
|
|
245
|
+
try:
|
|
246
|
+
project = things.get(heading['project'])
|
|
247
|
+
if project:
|
|
248
|
+
heading_text += f"\nProject: {project['title']}"
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
# Add dates
|
|
253
|
+
if heading.get('created'):
|
|
254
|
+
heading_text += f"\nCreated: {heading['created']}"
|
|
255
|
+
try:
|
|
256
|
+
age_text = _calculate_age(heading['created'])
|
|
257
|
+
heading_text += f"\nAge: {age_text}"
|
|
258
|
+
except (ValueError, TypeError):
|
|
259
|
+
pass
|
|
260
|
+
if heading.get('modified'):
|
|
261
|
+
heading_text += f"\nModified: {heading['modified']}"
|
|
262
|
+
try:
|
|
263
|
+
modified_age = _calculate_age(heading['modified'])
|
|
264
|
+
heading_text += f"\nLast modified: {modified_age}"
|
|
265
|
+
except (ValueError, TypeError):
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
# Add notes if present
|
|
269
|
+
if heading.get('notes'):
|
|
270
|
+
heading_text += f"\nNotes: {heading['notes']}"
|
|
271
|
+
|
|
272
|
+
if include_items:
|
|
273
|
+
# Get todos under this heading
|
|
274
|
+
todos = things.todos(heading=heading['uuid'])
|
|
275
|
+
if todos:
|
|
276
|
+
heading_text += "\n\nTasks under heading:"
|
|
277
|
+
for todo in todos:
|
|
278
|
+
heading_text += f"\n- {todo['title']}"
|
|
279
|
+
|
|
280
|
+
return heading_text
|