iflow-mcp_excelsier-things3-enhanced-mcp 1.0.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.
- iflow_mcp_excelsier_things3_enhanced_mcp-1.0.0.dist-info/METADATA +444 -0
- iflow_mcp_excelsier_things3_enhanced_mcp-1.0.0.dist-info/RECORD +20 -0
- iflow_mcp_excelsier_things3_enhanced_mcp-1.0.0.dist-info/WHEEL +4 -0
- iflow_mcp_excelsier_things3_enhanced_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_excelsier_things3_enhanced_mcp-1.0.0.dist-info/licenses/LICENSE +24 -0
- things_mcp/__init__.py +5 -0
- things_mcp/applescript_bridge.py +335 -0
- things_mcp/cache.py +240 -0
- things_mcp/config.py +120 -0
- things_mcp/fast_server.py +633 -0
- things_mcp/formatters.py +128 -0
- things_mcp/handlers.py +601 -0
- things_mcp/logging_config.py +218 -0
- things_mcp/mcp_tools.py +465 -0
- things_mcp/simple_server.py +687 -0
- things_mcp/simple_url_scheme.py +230 -0
- things_mcp/tag_handler.py +111 -0
- things_mcp/things_server.py +106 -0
- things_mcp/url_scheme.py +318 -0
- things_mcp/utils.py +360 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Things MCP Server implementation using the FastMCP pattern.
|
|
4
|
+
This provides a more modern and maintainable approach to the Things integration.
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
import asyncio
|
|
8
|
+
import traceback
|
|
9
|
+
from typing import Dict, Any, Optional, List, Union
|
|
10
|
+
import things
|
|
11
|
+
|
|
12
|
+
from mcp.server.fastmcp import FastMCP
|
|
13
|
+
import mcp.types as types
|
|
14
|
+
|
|
15
|
+
# Import supporting modules
|
|
16
|
+
from .formatters import format_todo, format_project, format_area, format_tag
|
|
17
|
+
from .utils import app_state, circuit_breaker, dead_letter_queue, rate_limiter
|
|
18
|
+
from .url_scheme import (
|
|
19
|
+
add_todo, add_project, update_todo, update_project, show,
|
|
20
|
+
search, launch_things, execute_url
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Import and configure enhanced logging
|
|
24
|
+
from .logging_config import setup_logging, get_logger, log_operation_start, log_operation_end
|
|
25
|
+
# Import caching
|
|
26
|
+
from .cache import cached, invalidate_caches_for, get_cache_stats, CACHE_TTL
|
|
27
|
+
|
|
28
|
+
# Configure enhanced logging
|
|
29
|
+
setup_logging(console_level="INFO", file_level="DEBUG", structured_logs=True)
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
# Create the FastMCP server
|
|
33
|
+
mcp = FastMCP("Things")
|
|
34
|
+
|
|
35
|
+
# LIST VIEWS
|
|
36
|
+
|
|
37
|
+
@mcp.tool(name="get-inbox")
|
|
38
|
+
def get_inbox() -> str:
|
|
39
|
+
"""Get todos from Inbox"""
|
|
40
|
+
import time
|
|
41
|
+
start_time = time.time()
|
|
42
|
+
log_operation_start("get-inbox")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
todos = things.inbox()
|
|
46
|
+
|
|
47
|
+
if not todos:
|
|
48
|
+
log_operation_end("get-inbox", True, time.time() - start_time, count=0)
|
|
49
|
+
return "No items found in Inbox"
|
|
50
|
+
|
|
51
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
52
|
+
log_operation_end("get-inbox", True, time.time() - start_time, count=len(todos))
|
|
53
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
log_operation_end("get-inbox", False, time.time() - start_time, error=str(e))
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
@mcp.tool(name="get-today")
|
|
59
|
+
@cached(ttl=CACHE_TTL.get("today", 30))
|
|
60
|
+
def get_today() -> str:
|
|
61
|
+
"""Get todos due today"""
|
|
62
|
+
import time
|
|
63
|
+
start_time = time.time()
|
|
64
|
+
log_operation_start("get-today")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
todos = things.today()
|
|
68
|
+
|
|
69
|
+
if not todos:
|
|
70
|
+
log_operation_end("get-today", True, time.time() - start_time, count=0)
|
|
71
|
+
return "No items due today"
|
|
72
|
+
|
|
73
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
74
|
+
log_operation_end("get-today", True, time.time() - start_time, count=len(todos))
|
|
75
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
log_operation_end("get-today", False, time.time() - start_time, error=str(e))
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
@mcp.tool(name="get-upcoming")
|
|
81
|
+
def get_upcoming() -> str:
|
|
82
|
+
"""Get upcoming todos"""
|
|
83
|
+
todos = things.upcoming()
|
|
84
|
+
|
|
85
|
+
if not todos:
|
|
86
|
+
return "No upcoming items"
|
|
87
|
+
|
|
88
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
89
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
90
|
+
|
|
91
|
+
@mcp.tool(name="get-anytime")
|
|
92
|
+
def get_anytime() -> str:
|
|
93
|
+
"""Get todos from Anytime list"""
|
|
94
|
+
todos = things.anytime()
|
|
95
|
+
|
|
96
|
+
if not todos:
|
|
97
|
+
return "No items in Anytime list"
|
|
98
|
+
|
|
99
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
100
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
101
|
+
|
|
102
|
+
@mcp.tool(name="get-someday")
|
|
103
|
+
def get_someday() -> str:
|
|
104
|
+
"""Get todos from Someday list"""
|
|
105
|
+
todos = things.someday()
|
|
106
|
+
|
|
107
|
+
if not todos:
|
|
108
|
+
return "No items in Someday list"
|
|
109
|
+
|
|
110
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
111
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
112
|
+
|
|
113
|
+
@mcp.tool(name="get-logbook")
|
|
114
|
+
def get_logbook(period: str = "7d", limit: int = 50) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Get completed todos from Logbook, defaults to last 7 days
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
period: Time period to look back (e.g., '3d', '1w', '2m', '1y'). Defaults to '7d'
|
|
120
|
+
limit: Maximum number of entries to return. Defaults to 50
|
|
121
|
+
"""
|
|
122
|
+
todos = things.last(period, status='completed')
|
|
123
|
+
|
|
124
|
+
if not todos:
|
|
125
|
+
return "No completed items found"
|
|
126
|
+
|
|
127
|
+
if todos and len(todos) > limit:
|
|
128
|
+
todos = todos[:limit]
|
|
129
|
+
|
|
130
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
131
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
132
|
+
|
|
133
|
+
@mcp.tool(name="get-trash")
|
|
134
|
+
def get_trash() -> str:
|
|
135
|
+
"""Get trashed todos"""
|
|
136
|
+
todos = things.trash()
|
|
137
|
+
|
|
138
|
+
if not todos:
|
|
139
|
+
return "No items in trash"
|
|
140
|
+
|
|
141
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
142
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
143
|
+
|
|
144
|
+
# BASIC TODO OPERATIONS
|
|
145
|
+
|
|
146
|
+
@mcp.tool(name="get-todos")
|
|
147
|
+
def get_todos(project_uuid: Optional[str] = None, include_items: bool = True) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Get todos from Things, optionally filtered by project
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
project_uuid: Optional UUID of a specific project to get todos from
|
|
153
|
+
include_items: Include checklist items
|
|
154
|
+
"""
|
|
155
|
+
if project_uuid:
|
|
156
|
+
project = things.get(project_uuid)
|
|
157
|
+
if not project or project.get('type') != 'project':
|
|
158
|
+
return f"Error: Invalid project UUID '{project_uuid}'"
|
|
159
|
+
|
|
160
|
+
todos = things.todos(project=project_uuid, start=None)
|
|
161
|
+
|
|
162
|
+
if not todos:
|
|
163
|
+
return "No todos found"
|
|
164
|
+
|
|
165
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
166
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
167
|
+
|
|
168
|
+
@mcp.tool(name="get-projects")
|
|
169
|
+
def get_projects(include_items: bool = False) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Get all projects from Things
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
include_items: Include tasks within projects
|
|
175
|
+
"""
|
|
176
|
+
projects = things.projects()
|
|
177
|
+
|
|
178
|
+
if not projects:
|
|
179
|
+
return "No projects found"
|
|
180
|
+
|
|
181
|
+
formatted_projects = [format_project(project, include_items) for project in projects]
|
|
182
|
+
return "\n\n---\n\n".join(formatted_projects)
|
|
183
|
+
|
|
184
|
+
@mcp.tool(name="get-areas")
|
|
185
|
+
def get_areas(include_items: bool = False) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Get all areas from Things
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
include_items: Include projects and tasks within areas
|
|
191
|
+
"""
|
|
192
|
+
areas = things.areas()
|
|
193
|
+
|
|
194
|
+
if not areas:
|
|
195
|
+
return "No areas found"
|
|
196
|
+
|
|
197
|
+
formatted_areas = [format_area(area, include_items) for area in areas]
|
|
198
|
+
return "\n\n---\n\n".join(formatted_areas)
|
|
199
|
+
|
|
200
|
+
# TAG OPERATIONS
|
|
201
|
+
|
|
202
|
+
@mcp.tool(name="get-tags")
|
|
203
|
+
def get_tags(include_items: bool = False) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Get all tags
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
include_items: Include items tagged with each tag
|
|
209
|
+
"""
|
|
210
|
+
tags = things.tags()
|
|
211
|
+
|
|
212
|
+
if not tags:
|
|
213
|
+
return "No tags found"
|
|
214
|
+
|
|
215
|
+
formatted_tags = [format_tag(tag, include_items) for tag in tags]
|
|
216
|
+
return "\n\n---\n\n".join(formatted_tags)
|
|
217
|
+
|
|
218
|
+
@mcp.tool(name="get-tagged-items")
|
|
219
|
+
def get_tagged_items(tag: str) -> str:
|
|
220
|
+
"""
|
|
221
|
+
Get items with a specific tag
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
tag: Tag title to filter by
|
|
225
|
+
"""
|
|
226
|
+
todos = things.todos(tag=tag)
|
|
227
|
+
|
|
228
|
+
if not todos:
|
|
229
|
+
return f"No items found with tag '{tag}'"
|
|
230
|
+
|
|
231
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
232
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
233
|
+
|
|
234
|
+
# SEARCH OPERATIONS
|
|
235
|
+
|
|
236
|
+
@mcp.tool(name="search-todos")
|
|
237
|
+
def search_todos(query: str) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Search todos by title or notes
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
query: Search term to look for in todo titles and notes
|
|
243
|
+
"""
|
|
244
|
+
todos = things.search(query)
|
|
245
|
+
|
|
246
|
+
if not todos:
|
|
247
|
+
return f"No todos found matching '{query}'"
|
|
248
|
+
|
|
249
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
250
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
251
|
+
|
|
252
|
+
@mcp.tool(name="search-advanced")
|
|
253
|
+
def search_advanced(
|
|
254
|
+
status: Optional[str] = None,
|
|
255
|
+
start_date: Optional[str] = None,
|
|
256
|
+
deadline: Optional[str] = None,
|
|
257
|
+
tag: Optional[str] = None,
|
|
258
|
+
area: Optional[str] = None,
|
|
259
|
+
type: Optional[str] = None
|
|
260
|
+
) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Advanced todo search with multiple filters
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
status: Filter by todo status (incomplete/completed/canceled)
|
|
266
|
+
start_date: Filter by start date (YYYY-MM-DD)
|
|
267
|
+
deadline: Filter by deadline (YYYY-MM-DD)
|
|
268
|
+
tag: Filter by tag
|
|
269
|
+
area: Filter by area UUID
|
|
270
|
+
type: Filter by item type (to-do/project/heading)
|
|
271
|
+
"""
|
|
272
|
+
# Build filter parameters
|
|
273
|
+
kwargs = {}
|
|
274
|
+
|
|
275
|
+
# Add filters that are provided
|
|
276
|
+
if status:
|
|
277
|
+
kwargs['status'] = status
|
|
278
|
+
if deadline:
|
|
279
|
+
kwargs['deadline'] = deadline
|
|
280
|
+
if start_date:
|
|
281
|
+
kwargs['start'] = start_date
|
|
282
|
+
if tag:
|
|
283
|
+
kwargs['tag'] = tag
|
|
284
|
+
if area:
|
|
285
|
+
kwargs['area'] = area
|
|
286
|
+
if type:
|
|
287
|
+
kwargs['type'] = type
|
|
288
|
+
|
|
289
|
+
# Execute search with applicable filters
|
|
290
|
+
try:
|
|
291
|
+
todos = things.todos(**kwargs)
|
|
292
|
+
|
|
293
|
+
if not todos:
|
|
294
|
+
return "No items found matching your search criteria"
|
|
295
|
+
|
|
296
|
+
formatted_todos = [format_todo(todo) for todo in todos]
|
|
297
|
+
return "\n\n---\n\n".join(formatted_todos)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
return f"Error in advanced search: {str(e)}"
|
|
300
|
+
|
|
301
|
+
# MODIFICATION OPERATIONS
|
|
302
|
+
|
|
303
|
+
@mcp.tool(name="add-todo")
|
|
304
|
+
def add_task(
|
|
305
|
+
title: str,
|
|
306
|
+
notes: Optional[str] = None,
|
|
307
|
+
when: Optional[str] = None,
|
|
308
|
+
deadline: Optional[str] = None,
|
|
309
|
+
tags: Optional[List[str]] = None,
|
|
310
|
+
checklist_items: Optional[List[str]] = None,
|
|
311
|
+
list_id: Optional[str] = None,
|
|
312
|
+
list_title: Optional[str] = None,
|
|
313
|
+
heading: Optional[str] = None
|
|
314
|
+
) -> str:
|
|
315
|
+
"""
|
|
316
|
+
Create a new todo in Things
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
title: Title of the todo
|
|
320
|
+
notes: Notes for the todo
|
|
321
|
+
when: When to schedule the todo (today, tomorrow, evening, anytime, someday, or YYYY-MM-DD)
|
|
322
|
+
deadline: Deadline for the todo (YYYY-MM-DD)
|
|
323
|
+
tags: Tags to apply to the todo
|
|
324
|
+
checklist_items: Checklist items to add
|
|
325
|
+
list_id: ID of project/area to add to
|
|
326
|
+
list_title: Title of project/area to add to
|
|
327
|
+
heading: Heading to add under
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
# Ensure Things app is running
|
|
331
|
+
if not app_state.update_app_state():
|
|
332
|
+
if not launch_things():
|
|
333
|
+
return "Error: Unable to launch Things app"
|
|
334
|
+
|
|
335
|
+
# Execute the add_todo URL command
|
|
336
|
+
result = add_todo(
|
|
337
|
+
title=title,
|
|
338
|
+
notes=notes,
|
|
339
|
+
when=when,
|
|
340
|
+
deadline=deadline,
|
|
341
|
+
tags=tags,
|
|
342
|
+
checklist_items=checklist_items,
|
|
343
|
+
list_id=list_id,
|
|
344
|
+
list_title=list_title,
|
|
345
|
+
heading=heading
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if not result:
|
|
349
|
+
return "Error: Failed to create todo"
|
|
350
|
+
|
|
351
|
+
# Invalidate relevant caches after creating a todo
|
|
352
|
+
invalidate_caches_for(["get-inbox", "get-today", "get-upcoming", "get-todos"])
|
|
353
|
+
|
|
354
|
+
return f"Successfully created todo: {title}"
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.error(f"Error creating todo: {str(e)}")
|
|
357
|
+
return f"Error creating todo: {str(e)}"
|
|
358
|
+
|
|
359
|
+
@mcp.tool(name="add-project")
|
|
360
|
+
def add_new_project(
|
|
361
|
+
title: str,
|
|
362
|
+
notes: Optional[str] = None,
|
|
363
|
+
when: Optional[str] = None,
|
|
364
|
+
deadline: Optional[str] = None,
|
|
365
|
+
tags: Optional[List[str]] = None,
|
|
366
|
+
area_id: Optional[str] = None,
|
|
367
|
+
area_title: Optional[str] = None,
|
|
368
|
+
todos: Optional[List[str]] = None
|
|
369
|
+
) -> str:
|
|
370
|
+
"""
|
|
371
|
+
Create a new project in Things
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
title: Title of the project
|
|
375
|
+
notes: Notes for the project
|
|
376
|
+
when: When to schedule the project
|
|
377
|
+
deadline: Deadline for the project
|
|
378
|
+
tags: Tags to apply to the project
|
|
379
|
+
area_id: ID of area to add to
|
|
380
|
+
area_title: Title of area to add to
|
|
381
|
+
todos: Initial todos to create in the project
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
# Ensure Things app is running
|
|
385
|
+
if not app_state.update_app_state():
|
|
386
|
+
if not launch_things():
|
|
387
|
+
return "Error: Unable to launch Things app"
|
|
388
|
+
|
|
389
|
+
# Execute the add_project URL command
|
|
390
|
+
result = add_project(
|
|
391
|
+
title=title,
|
|
392
|
+
notes=notes,
|
|
393
|
+
when=when,
|
|
394
|
+
deadline=deadline,
|
|
395
|
+
tags=tags,
|
|
396
|
+
area_id=area_id,
|
|
397
|
+
area_title=area_title,
|
|
398
|
+
todos=todos
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if not result:
|
|
402
|
+
return "Error: Failed to create project"
|
|
403
|
+
|
|
404
|
+
return f"Successfully created project: {title}"
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.error(f"Error creating project: {str(e)}")
|
|
407
|
+
return f"Error creating project: {str(e)}"
|
|
408
|
+
|
|
409
|
+
@mcp.tool(name="update-todo")
|
|
410
|
+
def update_task(
|
|
411
|
+
id: str,
|
|
412
|
+
title: Optional[str] = None,
|
|
413
|
+
notes: Optional[str] = None,
|
|
414
|
+
when: Optional[str] = None,
|
|
415
|
+
deadline: Optional[str] = None,
|
|
416
|
+
tags: Optional[List[str]] = None,
|
|
417
|
+
completed: Optional[bool] = None,
|
|
418
|
+
canceled: Optional[bool] = None
|
|
419
|
+
) -> str:
|
|
420
|
+
"""
|
|
421
|
+
Update an existing todo in Things
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
id: ID of the todo to update
|
|
425
|
+
title: New title
|
|
426
|
+
notes: New notes
|
|
427
|
+
when: New schedule
|
|
428
|
+
deadline: New deadline
|
|
429
|
+
tags: New tags
|
|
430
|
+
completed: Mark as completed
|
|
431
|
+
canceled: Mark as canceled
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
# Ensure Things app is running
|
|
435
|
+
if not app_state.update_app_state():
|
|
436
|
+
if not launch_things():
|
|
437
|
+
return "Error: Unable to launch Things app"
|
|
438
|
+
|
|
439
|
+
# Execute the update_todo URL command
|
|
440
|
+
result = update_todo(
|
|
441
|
+
id=id,
|
|
442
|
+
title=title,
|
|
443
|
+
notes=notes,
|
|
444
|
+
when=when,
|
|
445
|
+
deadline=deadline,
|
|
446
|
+
tags=tags,
|
|
447
|
+
completed=completed,
|
|
448
|
+
canceled=canceled
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if not result:
|
|
452
|
+
return "Error: Failed to update todo"
|
|
453
|
+
|
|
454
|
+
return f"Successfully updated todo with ID: {id}"
|
|
455
|
+
except Exception as e:
|
|
456
|
+
logger.error(f"Error updating todo: {str(e)}")
|
|
457
|
+
return f"Error updating todo: {str(e)}"
|
|
458
|
+
|
|
459
|
+
@mcp.tool(name="update-project")
|
|
460
|
+
def update_existing_project(
|
|
461
|
+
id: str,
|
|
462
|
+
title: Optional[str] = None,
|
|
463
|
+
notes: Optional[str] = None,
|
|
464
|
+
when: Optional[str] = None,
|
|
465
|
+
deadline: Optional[str] = None,
|
|
466
|
+
tags: Optional[List[str]] = None,
|
|
467
|
+
completed: Optional[bool] = None,
|
|
468
|
+
canceled: Optional[bool] = None
|
|
469
|
+
) -> str:
|
|
470
|
+
"""
|
|
471
|
+
Update an existing project in Things
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
id: ID of the project to update
|
|
475
|
+
title: New title
|
|
476
|
+
notes: New notes
|
|
477
|
+
when: New schedule
|
|
478
|
+
deadline: New deadline
|
|
479
|
+
tags: New tags
|
|
480
|
+
completed: Mark as completed
|
|
481
|
+
canceled: Mark as canceled
|
|
482
|
+
"""
|
|
483
|
+
try:
|
|
484
|
+
# Ensure Things app is running
|
|
485
|
+
if not app_state.update_app_state():
|
|
486
|
+
if not launch_things():
|
|
487
|
+
return "Error: Unable to launch Things app"
|
|
488
|
+
|
|
489
|
+
# Execute the update_project URL command
|
|
490
|
+
result = update_project(
|
|
491
|
+
id=id,
|
|
492
|
+
title=title,
|
|
493
|
+
notes=notes,
|
|
494
|
+
when=when,
|
|
495
|
+
deadline=deadline,
|
|
496
|
+
tags=tags,
|
|
497
|
+
completed=completed,
|
|
498
|
+
canceled=canceled
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if not result:
|
|
502
|
+
return "Error: Failed to update project"
|
|
503
|
+
|
|
504
|
+
return f"Successfully updated project with ID: {id}"
|
|
505
|
+
except Exception as e:
|
|
506
|
+
logger.error(f"Error updating project: {str(e)}")
|
|
507
|
+
return f"Error updating project: {str(e)}"
|
|
508
|
+
|
|
509
|
+
@mcp.tool(name="show-item")
|
|
510
|
+
def show_item(
|
|
511
|
+
id: str,
|
|
512
|
+
query: Optional[str] = None,
|
|
513
|
+
filter_tags: Optional[List[str]] = None
|
|
514
|
+
) -> str:
|
|
515
|
+
"""
|
|
516
|
+
Show a specific item or list in Things
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
id: ID of item to show, or one of: inbox, today, upcoming, anytime, someday, logbook
|
|
520
|
+
query: Optional query to filter by
|
|
521
|
+
filter_tags: Optional tags to filter by
|
|
522
|
+
"""
|
|
523
|
+
try:
|
|
524
|
+
# Ensure Things app is running
|
|
525
|
+
if not app_state.update_app_state():
|
|
526
|
+
if not launch_things():
|
|
527
|
+
return "Error: Unable to launch Things app"
|
|
528
|
+
|
|
529
|
+
# Execute the show URL command
|
|
530
|
+
result = show(
|
|
531
|
+
id=id,
|
|
532
|
+
query=query,
|
|
533
|
+
filter_tags=filter_tags
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if not result:
|
|
537
|
+
return f"Error: Failed to show item/list '{id}'"
|
|
538
|
+
|
|
539
|
+
return f"Successfully opened '{id}' in Things"
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.error(f"Error showing item: {str(e)}")
|
|
542
|
+
return f"Error showing item: {str(e)}"
|
|
543
|
+
|
|
544
|
+
@mcp.tool(name="search-items")
|
|
545
|
+
def search_all_items(query: str) -> str:
|
|
546
|
+
"""
|
|
547
|
+
Search for items in Things
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
query: Search query
|
|
551
|
+
"""
|
|
552
|
+
try:
|
|
553
|
+
# Ensure Things app is running
|
|
554
|
+
if not app_state.update_app_state():
|
|
555
|
+
if not launch_things():
|
|
556
|
+
return "Error: Unable to launch Things app"
|
|
557
|
+
|
|
558
|
+
# Execute the search URL command
|
|
559
|
+
result = search(query=query)
|
|
560
|
+
|
|
561
|
+
if not result:
|
|
562
|
+
return f"Error: Failed to search for '{query}'"
|
|
563
|
+
|
|
564
|
+
return f"Successfully searched for '{query}' in Things"
|
|
565
|
+
except Exception as e:
|
|
566
|
+
logger.error(f"Error searching: {str(e)}")
|
|
567
|
+
return f"Error searching: {str(e)}"
|
|
568
|
+
|
|
569
|
+
@mcp.tool(name="get-recent")
|
|
570
|
+
def get_recent(period: str) -> str:
|
|
571
|
+
"""
|
|
572
|
+
Get recently created items
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
period: Time period (e.g., '3d', '1w', '2m', '1y')
|
|
576
|
+
"""
|
|
577
|
+
try:
|
|
578
|
+
# Check if period format is valid
|
|
579
|
+
if not period or not any(period.endswith(unit) for unit in ['d', 'w', 'm', 'y']):
|
|
580
|
+
return "Error: Period must be in format '3d', '1w', '2m', '1y'"
|
|
581
|
+
|
|
582
|
+
# Get recent items
|
|
583
|
+
items = things.last(period)
|
|
584
|
+
|
|
585
|
+
if not items:
|
|
586
|
+
return f"No items found in the last {period}"
|
|
587
|
+
|
|
588
|
+
formatted_items = []
|
|
589
|
+
for item in items:
|
|
590
|
+
if item.get('type') == 'to-do':
|
|
591
|
+
formatted_items.append(format_todo(item))
|
|
592
|
+
elif item.get('type') == 'project':
|
|
593
|
+
formatted_items.append(format_project(item, include_items=False))
|
|
594
|
+
|
|
595
|
+
return "\n\n---\n\n".join(formatted_items)
|
|
596
|
+
except Exception as e:
|
|
597
|
+
logger.error(f"Error getting recent items: {str(e)}")
|
|
598
|
+
return f"Error getting recent items: {str(e)}"
|
|
599
|
+
|
|
600
|
+
@mcp.tool(name="get-cache-stats")
|
|
601
|
+
def get_cache_statistics() -> str:
|
|
602
|
+
"""Get cache performance statistics"""
|
|
603
|
+
stats = get_cache_stats()
|
|
604
|
+
|
|
605
|
+
return f"""Cache Statistics:
|
|
606
|
+
- Total entries: {stats['entries']}
|
|
607
|
+
- Cache hits: {stats['hits']}
|
|
608
|
+
- Cache misses: {stats['misses']}
|
|
609
|
+
- Hit rate: {stats['hit_rate']}
|
|
610
|
+
- Total requests: {stats['total_requests']}"""
|
|
611
|
+
|
|
612
|
+
# Main entry point
|
|
613
|
+
def run_things_mcp_server():
|
|
614
|
+
"""Run the Things MCP server"""
|
|
615
|
+
# Check if Things app is available
|
|
616
|
+
if not app_state.update_app_state():
|
|
617
|
+
logger.warning("Things app is not running at startup. MCP will attempt to launch it when needed.")
|
|
618
|
+
try:
|
|
619
|
+
# Try to launch Things
|
|
620
|
+
if launch_things():
|
|
621
|
+
logger.info("Successfully launched Things app")
|
|
622
|
+
else:
|
|
623
|
+
logger.error("Unable to launch Things app. Some operations may fail.")
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.error(f"Error launching Things app: {str(e)}")
|
|
626
|
+
else:
|
|
627
|
+
logger.info("Things app is running and ready for operations")
|
|
628
|
+
|
|
629
|
+
# Run the MCP server
|
|
630
|
+
mcp.run()
|
|
631
|
+
|
|
632
|
+
if __name__ == "__main__":
|
|
633
|
+
run_things_mcp_server()
|