vector-task-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.
main.py ADDED
@@ -0,0 +1,913 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # -*- coding: utf-8 -*-
3
+ # /// script
4
+ # dependencies = [
5
+ # "mcp>=0.3.0",
6
+ # "sqlite-vec>=0.1.6",
7
+ # "sentence-transformers>=2.2.2"
8
+ # ]
9
+ # requires-python = ">=3.8"
10
+ # ///
11
+
12
+ """
13
+ Vector Task MCP Server - Main Entry Point
14
+ ==========================================
15
+
16
+ A secure, vector-based task management server using sqlite-vec for semantic search.
17
+ Stores and retrieves tasks with vector embeddings for intelligent task retrieval.
18
+
19
+ Usage:
20
+ python main.py --working-dir /path/to/project
21
+
22
+ Task database stored in: {working_dir}/memory/tasks.db
23
+ """
24
+
25
+ import sys
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+ from typing import Dict, Any
29
+
30
+ # Add src to path for imports
31
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
32
+
33
+ from mcp.server.fastmcp import FastMCP
34
+
35
+ # Import our modules
36
+ from src.models import Config
37
+ from src.security import validate_working_dir, SecurityError, validate_task_list_params
38
+ from src.task_store import TaskStore
39
+
40
+
41
+ def get_working_dir() -> Path:
42
+ """Get working directory from command line arguments"""
43
+ if "--working-dir" in sys.argv:
44
+ idx = sys.argv.index("--working-dir")
45
+ if idx + 1 < len(sys.argv):
46
+ return validate_working_dir(sys.argv[idx + 1])
47
+ # Default to current directory
48
+ return validate_working_dir(".")
49
+
50
+
51
+ def create_server() -> FastMCP:
52
+ """Create and configure the MCP server"""
53
+
54
+ # Initialize task store
55
+ try:
56
+ working_dir = get_working_dir()
57
+ memory_dir = working_dir / "memory"
58
+ memory_dir.mkdir(parents=True, exist_ok=True)
59
+ task_db_path = memory_dir / "tasks.db"
60
+ task_store = TaskStore(task_db_path)
61
+ print(f"Task database initialized: {task_db_path}", file=sys.stderr)
62
+ except Exception as e:
63
+ print(f"Failed to initialize task store: {e}", file=sys.stderr)
64
+ sys.exit(1)
65
+
66
+ # Create FastMCP server
67
+ mcp = FastMCP(Config.SERVER_NAME)
68
+
69
+ # ===============================================================================
70
+ # TASK MANAGEMENT TOOLS
71
+ # ===============================================================================
72
+
73
+ @mcp.tool()
74
+ def task_create(
75
+ title: str,
76
+ content: str,
77
+ parent_id: int = None,
78
+ comment: str = None,
79
+ priority: str = None,
80
+ tags: list[str] = None
81
+ ) -> dict[str, Any]:
82
+ """
83
+ Create new task with vector embedding for semantic search.
84
+
85
+ Args:
86
+ title: Task title (max 200 chars)
87
+ content: Task description/details (max 10K chars)
88
+ parent_id: Optional parent task ID for subtasks
89
+ comment: Optional comment/note for the task
90
+ priority: Optional task priority (low, medium, high, critical, default: medium)
91
+ tags: Optional list of tags for organization (max 10)
92
+ """
93
+ try:
94
+ result = task_store.create_task(title, content, parent_id, comment, priority, tags)
95
+ return result
96
+
97
+ except SecurityError as e:
98
+ return {
99
+ "success": False,
100
+ "error": "Security validation failed",
101
+ "message": str(e)
102
+ }
103
+ except Exception as e:
104
+ return {
105
+ "success": False,
106
+ "error": "Task creation failed",
107
+ "message": str(e)
108
+ }
109
+
110
+ @mcp.tool()
111
+ def task_create_bulk(tasks: list[dict]) -> dict[str, Any]:
112
+ """
113
+ Create multiple tasks in bulk with vector embeddings.
114
+
115
+ Args:
116
+ tasks: List of task objects with fields:
117
+ - title (required): Task title (max 200 chars)
118
+ - content (required): Task description (max 10K chars)
119
+ - parent_id (optional): Parent task ID for subtasks
120
+ - comment (optional): Comment/note for the task
121
+ - tags (optional): List of tags for organization (max 10)
122
+
123
+ Example:
124
+ tasks = [
125
+ {"title": "Task 1", "content": "Description", "parent_id": None, "comment": "Note", "tags": ["backend", "api"]},
126
+ {"title": "Task 2", "content": "Description", "parent_id": 1, "comment": None, "tags": ["frontend"]}
127
+ ]
128
+ """
129
+ try:
130
+ result = task_store.create_tasks_bulk(tasks)
131
+ return result
132
+
133
+ except SecurityError as e:
134
+ return {
135
+ "success": False,
136
+ "error": "Security validation failed",
137
+ "message": str(e)
138
+ }
139
+ except Exception as e:
140
+ return {
141
+ "success": False,
142
+ "error": "Bulk task creation failed",
143
+ "message": str(e)
144
+ }
145
+
146
+ @mcp.tool()
147
+ def task_update(
148
+ task_id: int,
149
+ title: str | None = None,
150
+ content: str | None = None,
151
+ status: str | None = None,
152
+ parent_id: int | None = None,
153
+ comment: str | None = None,
154
+ start_at: str | None = None,
155
+ finish_at: str | None = None,
156
+ priority: str | None = None,
157
+ tags: list[str] | None = None
158
+ ) -> dict[str, Any]:
159
+ """
160
+ Update task fields by ID.
161
+
162
+ Args:
163
+ task_id: Task ID to update
164
+ title: Optional new title
165
+ content: Optional new content
166
+ status: Optional new status (pending, in_progress, completed, stopped)
167
+ parent_id: Optional new parent task ID
168
+ comment: Optional comment to add or replace
169
+ start_at: Optional start timestamp (ISO 8601 format)
170
+ finish_at: Optional finish timestamp (ISO 8601 format)
171
+ priority: Optional new priority (low, medium, high, critical)
172
+ tags: Optional list of tags to replace existing tags
173
+ """
174
+ try:
175
+ if not isinstance(task_id, int) or task_id < 1:
176
+ return {
177
+ "success": False,
178
+ "error": "Invalid parameter",
179
+ "message": "task_id must be a positive integer"
180
+ }
181
+
182
+ # Build kwargs from provided parameters
183
+ kwargs = {}
184
+ if title is not None:
185
+ kwargs['title'] = title
186
+ if content is not None:
187
+ kwargs['content'] = content
188
+ if status is not None:
189
+ kwargs['status'] = status
190
+ if parent_id is not None:
191
+ kwargs['parent_id'] = parent_id
192
+ if comment is not None:
193
+ kwargs['comment'] = comment
194
+ if start_at is not None:
195
+ kwargs['start_at'] = start_at
196
+ if finish_at is not None:
197
+ kwargs['finish_at'] = finish_at
198
+ if priority is not None:
199
+ kwargs['priority'] = priority
200
+ if tags is not None:
201
+ kwargs['tags'] = tags
202
+
203
+ result = task_store.update_task(task_id, **kwargs)
204
+ return result
205
+
206
+ except SecurityError as e:
207
+ return {
208
+ "success": False,
209
+ "error": "Security validation failed",
210
+ "message": str(e)
211
+ }
212
+ except Exception as e:
213
+ return {
214
+ "success": False,
215
+ "error": "Task update failed",
216
+ "message": str(e)
217
+ }
218
+
219
+ @mcp.tool()
220
+ def task_delete(task_id: int) -> dict[str, Any]:
221
+ """
222
+ Delete task by ID (permanent, cannot be undone).
223
+
224
+ Args:
225
+ task_id: Task ID to delete
226
+ """
227
+ try:
228
+ if not isinstance(task_id, int) or task_id < 1:
229
+ return {
230
+ "success": False,
231
+ "error": "Invalid parameter",
232
+ "message": "task_id must be a positive integer"
233
+ }
234
+
235
+ deleted = task_store.delete_task(task_id)
236
+
237
+ if not deleted:
238
+ return {
239
+ "success": False,
240
+ "error": "Not found",
241
+ "message": f"Task with ID {task_id} not found"
242
+ }
243
+
244
+ return {
245
+ "success": True,
246
+ "task_id": task_id,
247
+ "message": "Task deleted successfully"
248
+ }
249
+
250
+ except Exception as e:
251
+ return {
252
+ "success": False,
253
+ "error": "Deletion failed",
254
+ "message": str(e)
255
+ }
256
+
257
+ @mcp.tool()
258
+ def task_delete_bulk(task_ids: list[int]) -> dict[str, Any]:
259
+ """
260
+ Delete multiple tasks by IDs (permanent, cannot be undone).
261
+
262
+ Args:
263
+ task_ids: List of task IDs to delete
264
+ """
265
+ try:
266
+ result = task_store.delete_tasks_bulk(task_ids)
267
+ return result
268
+
269
+ except SecurityError as e:
270
+ return {
271
+ "success": False,
272
+ "error": "Security validation failed",
273
+ "message": str(e)
274
+ }
275
+ except Exception as e:
276
+ return {
277
+ "success": False,
278
+ "error": "Bulk deletion failed",
279
+ "message": str(e)
280
+ }
281
+
282
+ @mcp.tool()
283
+ def task_last() -> dict[str, Any]:
284
+ """Get last created task."""
285
+ try:
286
+ task = task_store.get_last_task()
287
+
288
+ if task is None:
289
+ return {
290
+ "success": False,
291
+ "error": "Not found",
292
+ "message": "No tasks found in database"
293
+ }
294
+
295
+ return {
296
+ "success": True,
297
+ "task": task.to_dict(),
298
+ "message": "Last task retrieved successfully"
299
+ }
300
+
301
+ except Exception as e:
302
+ return {
303
+ "success": False,
304
+ "error": "Retrieval failed",
305
+ "message": str(e)
306
+ }
307
+
308
+ @mcp.tool()
309
+ def task_start(task_id: int) -> dict[str, Any]:
310
+ """
311
+ Start task (set status to in_progress, record start time).
312
+
313
+ Args:
314
+ task_id: Task ID to start
315
+ """
316
+ try:
317
+ if not isinstance(task_id, int) or task_id < 1:
318
+ return {
319
+ "success": False,
320
+ "error": "Invalid parameter",
321
+ "message": "task_id must be a positive integer"
322
+ }
323
+
324
+ # Fetch current task to validate status transition
325
+ current_task = task_store.get_task_by_id(task_id)
326
+ if current_task is None:
327
+ return {
328
+ "success": False,
329
+ "error": "Not found",
330
+ "message": f"Task {task_id} not found"
331
+ }
332
+
333
+ # Validate status transition
334
+ if current_task.status == "completed":
335
+ raise SecurityError("Cannot start completed task. Task already finished.")
336
+
337
+ if current_task.status == "in_progress":
338
+ raise SecurityError("Task already in progress")
339
+
340
+ # Only pending and stopped tasks can be started
341
+ result = task_store.update_task(
342
+ task_id,
343
+ status="in_progress",
344
+ start_at=datetime.now(timezone.utc).isoformat(),
345
+ finish_at=None
346
+ )
347
+
348
+ return result
349
+
350
+ except SecurityError as e:
351
+ return {
352
+ "success": False,
353
+ "error": "Security validation failed",
354
+ "message": str(e)
355
+ }
356
+ except Exception as e:
357
+ return {
358
+ "success": False,
359
+ "error": "Failed to start task",
360
+ "message": str(e)
361
+ }
362
+
363
+ @mcp.tool()
364
+ def task_stop(task_id: int) -> dict[str, Any]:
365
+ """
366
+ Stop task (set status to stopped).
367
+
368
+ Args:
369
+ task_id: Task ID to stop
370
+ """
371
+ try:
372
+ if not isinstance(task_id, int) or task_id < 1:
373
+ return {
374
+ "success": False,
375
+ "error": "Invalid parameter",
376
+ "message": "task_id must be a positive integer"
377
+ }
378
+
379
+ result = task_store.update_task(task_id, status="stopped")
380
+
381
+ return result
382
+
383
+ except SecurityError as e:
384
+ return {
385
+ "success": False,
386
+ "error": "Security validation failed",
387
+ "message": str(e)
388
+ }
389
+ except Exception as e:
390
+ return {
391
+ "success": False,
392
+ "error": "Failed to stop task",
393
+ "message": str(e)
394
+ }
395
+
396
+ @mcp.tool()
397
+ def task_finish(task_id: int) -> dict[str, Any]:
398
+ """
399
+ Finish task (set status to completed, record finish time).
400
+
401
+ Args:
402
+ task_id: Task ID to finish
403
+ """
404
+ try:
405
+ if not isinstance(task_id, int) or task_id < 1:
406
+ return {
407
+ "success": False,
408
+ "error": "Invalid parameter",
409
+ "message": "task_id must be a positive integer"
410
+ }
411
+
412
+ result = task_store.update_task(
413
+ task_id,
414
+ status="completed",
415
+ finish_at=datetime.now(timezone.utc).isoformat()
416
+ )
417
+
418
+ return result
419
+
420
+ except SecurityError as e:
421
+ return {
422
+ "success": False,
423
+ "error": "Security validation failed",
424
+ "message": str(e)
425
+ }
426
+ except Exception as e:
427
+ return {
428
+ "success": False,
429
+ "error": "Failed to finish task",
430
+ "message": str(e)
431
+ }
432
+
433
+ @mcp.tool()
434
+ def task_resume(task_id: int) -> dict[str, Any]:
435
+ """
436
+ Resume stopped task (set status back to in_progress).
437
+
438
+ Args:
439
+ task_id: Task ID to resume
440
+ """
441
+ try:
442
+ if not isinstance(task_id, int) or task_id < 1:
443
+ return {
444
+ "success": False,
445
+ "error": "Invalid parameter",
446
+ "message": "task_id must be a positive integer"
447
+ }
448
+
449
+ result = task_store.update_task(task_id, status="in_progress", finish_at=None)
450
+
451
+ return result
452
+
453
+ except SecurityError as e:
454
+ return {
455
+ "success": False,
456
+ "error": "Security validation failed",
457
+ "message": str(e)
458
+ }
459
+ except Exception as e:
460
+ return {
461
+ "success": False,
462
+ "error": "Failed to resume task",
463
+ "message": str(e)
464
+ }
465
+
466
+ @mcp.tool()
467
+ def task_list(
468
+ query: str = None,
469
+ limit: int = 10,
470
+ offset: int = 0,
471
+ status: str = None,
472
+ parent_id: int = None,
473
+ tags: list[str] = None
474
+ ) -> dict[str, Any]:
475
+ """
476
+ List tasks with optional filters and vector semantic search.
477
+
478
+ Args:
479
+ query: Optional semantic search query for title/content
480
+ limit: Max results (1-50, default 10)
481
+ offset: Starting position for pagination (default 0)
482
+ status: Optional status filter (pending, in_progress, completed, stopped)
483
+ parent_id: Optional parent task ID filter (for subtasks)
484
+ tags: Optional list of tags to filter by (matches tasks containing ANY of the specified tags)
485
+ """
486
+ try:
487
+ # Validate parameters
488
+ limit, offset, status, parent_id, validated_tags = validate_task_list_params(
489
+ limit=limit,
490
+ offset=offset,
491
+ status=status,
492
+ parent_id=parent_id,
493
+ tags=tags
494
+ )
495
+
496
+ # Search tasks
497
+ tasks, total = task_store.search_tasks(
498
+ query=query,
499
+ limit=limit,
500
+ offset=offset,
501
+ status=status,
502
+ parent_id=parent_id,
503
+ tags=validated_tags
504
+ )
505
+
506
+ if not tasks:
507
+ return {
508
+ "success": True,
509
+ "tasks": [],
510
+ "total": total,
511
+ "count": 0,
512
+ "message": "No tasks found matching filters"
513
+ }
514
+
515
+ # Convert Task objects to dictionaries
516
+ task_dicts = [task.to_dict() for task in tasks]
517
+
518
+ return {
519
+ "success": True,
520
+ "query": query,
521
+ "tasks": task_dicts,
522
+ "total": total,
523
+ "count": len(task_dicts),
524
+ "message": f"Retrieved {len(task_dicts)} of {total} tasks"
525
+ }
526
+
527
+ except SecurityError as e:
528
+ return {
529
+ "success": False,
530
+ "error": "Security validation failed",
531
+ "message": str(e)
532
+ }
533
+ except Exception as e:
534
+ return {
535
+ "success": False,
536
+ "error": "Task list failed",
537
+ "message": str(e)
538
+ }
539
+
540
+ @mcp.tool()
541
+ def task_next() -> dict[str, Any]:
542
+ """
543
+ Get next task to work on (smart selection).
544
+
545
+ Returns in_progress task if any exists, otherwise returns
546
+ next pending task after last completed task.
547
+ """
548
+ try:
549
+ task = task_store.get_next_task()
550
+
551
+ if task is None:
552
+ return {
553
+ "success": False,
554
+ "error": "Not found",
555
+ "message": "No pending or in-progress tasks found"
556
+ }
557
+
558
+ return {
559
+ "success": True,
560
+ "task": task.to_dict(),
561
+ "message": f"Next task: {task.status}"
562
+ }
563
+
564
+ except Exception as e:
565
+ return {
566
+ "success": False,
567
+ "error": "Failed to get next task",
568
+ "message": str(e)
569
+ }
570
+
571
+ @mcp.tool()
572
+ def task_get(task_id: int) -> dict[str, Any]:
573
+ """
574
+ Get task by ID.
575
+
576
+ Args:
577
+ task_id: Task ID to retrieve
578
+ """
579
+ try:
580
+ if not isinstance(task_id, int) or task_id < 1:
581
+ return {
582
+ "success": False,
583
+ "error": "Invalid parameter",
584
+ "message": "task_id must be a positive integer"
585
+ }
586
+
587
+ task = task_store.get_task_by_id(task_id)
588
+
589
+ if task is None:
590
+ return {
591
+ "success": False,
592
+ "error": "Not found",
593
+ "message": f"Task with ID {task_id} not found"
594
+ }
595
+
596
+ return {
597
+ "success": True,
598
+ "task": task.to_dict(),
599
+ "message": "Task retrieved successfully"
600
+ }
601
+
602
+ except Exception as e:
603
+ return {
604
+ "success": False,
605
+ "error": "Retrieval failed",
606
+ "message": str(e)
607
+ }
608
+
609
+ @mcp.tool()
610
+ def task_stats() -> dict[str, Any]:
611
+ """
612
+ Get task statistics (total, completed, pending, in_progress, stopped, next_task_id, etc.).
613
+
614
+ Returns comprehensive task statistics including:
615
+ - Total tasks count
616
+ - Count by status (pending, in_progress, completed, stopped)
617
+ - Tasks with subtasks count
618
+ - Next task ID (from smart selection logic)
619
+ """
620
+ try:
621
+ # Get stats from TaskStore
622
+ stats = task_store.get_stats()
623
+
624
+ # Get next task ID
625
+ next_task = task_store.get_next_task()
626
+ next_task_id = next_task.id if next_task else None
627
+
628
+ # Build response with stats
629
+ result = stats.to_dict()
630
+ result["success"] = True
631
+ result["next_task_id"] = next_task_id
632
+ result["message"] = f"Statistics for {result['total_tasks']} tasks"
633
+
634
+ return result
635
+
636
+ except Exception as e:
637
+ return {
638
+ "success": False,
639
+ "error": "Failed to get statistics",
640
+ "message": str(e)
641
+ }
642
+
643
+ @mcp.tool()
644
+ def task_comment(task_id: int, comment: str, append: bool = True) -> dict[str, Any]:
645
+ """
646
+ Add or replace task comment.
647
+
648
+ Args:
649
+ task_id: Task ID to update
650
+ comment: Comment text to add or set
651
+ append: If True, append to existing comment with \\n\\n separator. If False, replace entirely.
652
+ """
653
+ try:
654
+ # Parameter validation
655
+ if not isinstance(task_id, int) or task_id < 1:
656
+ return {
657
+ "success": False,
658
+ "error": "Invalid parameter",
659
+ "message": "task_id must be a positive integer"
660
+ }
661
+
662
+ if not isinstance(comment, str) or not comment.strip():
663
+ return {
664
+ "success": False,
665
+ "error": "Invalid parameter",
666
+ "message": "comment cannot be empty"
667
+ }
668
+
669
+ # Fetch existing task
670
+ existing_task = task_store.get_task_by_id(task_id)
671
+
672
+ if existing_task is None:
673
+ return {
674
+ "success": False,
675
+ "error": "Not found",
676
+ "message": f"Task with ID {task_id} not found"
677
+ }
678
+
679
+ # Build new comment
680
+ if append and existing_task.comment:
681
+ new_comment = existing_task.comment + "\n\n" + comment
682
+ else:
683
+ new_comment = comment
684
+
685
+ # Update task
686
+ result = task_store.update_task(task_id, comment=new_comment)
687
+ return result
688
+
689
+ except SecurityError as e:
690
+ return {
691
+ "success": False,
692
+ "error": "Security validation failed",
693
+ "message": str(e)
694
+ }
695
+ except Exception as e:
696
+ return {
697
+ "success": False,
698
+ "error": "Comment update failed",
699
+ "message": str(e)
700
+ }
701
+
702
+ @mcp.tool()
703
+ def task_add_tag(task_id: int, tag: str) -> dict[str, Any]:
704
+ """
705
+ Add a single tag to a task (appends to existing tags).
706
+
707
+ Args:
708
+ task_id: Task ID to update
709
+ tag: Tag to add (will be sanitized and lowercased)
710
+ """
711
+ try:
712
+ # Validate task_id
713
+ if not isinstance(task_id, int) or task_id < 1:
714
+ return {
715
+ "success": False,
716
+ "error": "Invalid parameter",
717
+ "message": "task_id must be a positive integer"
718
+ }
719
+
720
+ # Validate tag
721
+ if not isinstance(tag, str) or not tag.strip():
722
+ return {
723
+ "success": False,
724
+ "error": "Invalid parameter",
725
+ "message": "tag cannot be empty"
726
+ }
727
+
728
+ # Get existing task
729
+ task = task_store.get_task_by_id(task_id)
730
+
731
+ if task is None:
732
+ return {
733
+ "success": False,
734
+ "error": "Not found",
735
+ "message": f"Task with ID {task_id} not found"
736
+ }
737
+
738
+ # Get current tags and add new tag
739
+ current_tags = task.tags if task.tags else []
740
+
741
+ # Sanitize and normalize new tag
742
+ from src.security import validate_tags
743
+ validated_tags = validate_tags([tag])
744
+
745
+ if not validated_tags:
746
+ return {
747
+ "success": False,
748
+ "error": "Validation failed",
749
+ "message": "Tag validation failed (must be alphanumeric + hyphens/underscores)"
750
+ }
751
+
752
+ new_tag = validated_tags[0]
753
+
754
+ # Check if tag already exists
755
+ if new_tag in current_tags:
756
+ return {
757
+ "success": False,
758
+ "error": "Already exists",
759
+ "message": f"Tag '{new_tag}' already exists on task {task_id}"
760
+ }
761
+
762
+ # Check max tags limit
763
+ if len(current_tags) >= 10:
764
+ return {
765
+ "success": False,
766
+ "error": "Limit exceeded",
767
+ "message": "Task already has maximum of 10 tags"
768
+ }
769
+
770
+ # Add tag
771
+ updated_tags = current_tags + [new_tag]
772
+ result = task_store.update_task(task_id, tags=updated_tags)
773
+
774
+ return result
775
+
776
+ except SecurityError as e:
777
+ return {
778
+ "success": False,
779
+ "error": "Security validation failed",
780
+ "message": str(e)
781
+ }
782
+ except Exception as e:
783
+ return {
784
+ "success": False,
785
+ "error": "Failed to add tag",
786
+ "message": str(e)
787
+ }
788
+
789
+ @mcp.tool()
790
+ def task_remove_tag(task_id: int, tag: str) -> dict[str, Any]:
791
+ """
792
+ Remove a single tag from a task.
793
+
794
+ Args:
795
+ task_id: Task ID to update
796
+ tag: Tag to remove (case-insensitive match)
797
+ """
798
+ try:
799
+ # Validate task_id
800
+ if not isinstance(task_id, int) or task_id < 1:
801
+ return {
802
+ "success": False,
803
+ "error": "Invalid parameter",
804
+ "message": "task_id must be a positive integer"
805
+ }
806
+
807
+ # Validate tag
808
+ if not isinstance(tag, str) or not tag.strip():
809
+ return {
810
+ "success": False,
811
+ "error": "Invalid parameter",
812
+ "message": "tag cannot be empty"
813
+ }
814
+
815
+ # Get existing task
816
+ task = task_store.get_task_by_id(task_id)
817
+
818
+ if task is None:
819
+ return {
820
+ "success": False,
821
+ "error": "Not found",
822
+ "message": f"Task with ID {task_id} not found"
823
+ }
824
+
825
+ # Get current tags
826
+ current_tags = task.tags if task.tags else []
827
+
828
+ # Normalize tag for comparison (lowercase)
829
+ tag_normalized = tag.lower().strip()
830
+
831
+ # Check if tag exists
832
+ if tag_normalized not in current_tags:
833
+ return {
834
+ "success": False,
835
+ "error": "Not found",
836
+ "message": f"Tag '{tag_normalized}' not found on task {task_id}"
837
+ }
838
+
839
+ # Remove tag
840
+ updated_tags = [t for t in current_tags if t != tag_normalized]
841
+ result = task_store.update_task(task_id, tags=updated_tags)
842
+
843
+ return result
844
+
845
+ except SecurityError as e:
846
+ return {
847
+ "success": False,
848
+ "error": "Security validation failed",
849
+ "message": str(e)
850
+ }
851
+ except Exception as e:
852
+ return {
853
+ "success": False,
854
+ "error": "Failed to remove tag",
855
+ "message": str(e)
856
+ }
857
+
858
+ @mcp.tool()
859
+ def task_get_all_tags() -> dict[str, Any]:
860
+ """
861
+ Get all unique tags across all tasks.
862
+
863
+ Returns sorted list of unique tags from the task database.
864
+ """
865
+ try:
866
+ tags = task_store.get_all_tags()
867
+
868
+ return {
869
+ "success": True,
870
+ "tags": tags,
871
+ "count": len(tags),
872
+ "message": f"Retrieved {len(tags)} unique tags"
873
+ }
874
+
875
+ except Exception as e:
876
+ return {
877
+ "success": False,
878
+ "error": "Failed to retrieve tags",
879
+ "message": str(e)
880
+ }
881
+
882
+ return mcp
883
+
884
+
885
+ def main():
886
+ """Main entry point"""
887
+ print(f"Starting {Config.SERVER_NAME} v{Config.SERVER_VERSION}", file=sys.stderr)
888
+
889
+ try:
890
+ # Get working directory and config
891
+ working_dir = get_working_dir()
892
+ memory_dir = working_dir / "memory"
893
+ task_db_path = memory_dir / "tasks.db"
894
+
895
+ print(f"Working directory: {working_dir}", file=sys.stderr)
896
+ print(f"Task database: {task_db_path}", file=sys.stderr)
897
+ print(f"Embedding model: {Config.EMBEDDING_MODEL}", file=sys.stderr)
898
+ print("=" * 50, file=sys.stderr)
899
+
900
+ # Create and run server
901
+ server = create_server()
902
+ print("Server ready for connections...", file=sys.stderr)
903
+ server.run()
904
+
905
+ except KeyboardInterrupt:
906
+ print("\nServer stopped by user", file=sys.stderr)
907
+ except Exception as e:
908
+ print(f"Server failed to start: {e}", file=sys.stderr)
909
+ sys.exit(1)
910
+
911
+
912
+ if __name__ == "__main__":
913
+ main()