vector-task-mcp 1.2.7__tar.gz → 1.2.9__tar.gz

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.
Files changed (26) hide show
  1. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/PKG-INFO +2 -1
  2. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/main.py +3 -2
  3. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/pyproject.toml +2 -1
  4. vector_task_mcp-1.2.9/src/embeddings.py +151 -0
  5. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/src/models.py +5 -1
  6. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/src/task_store.py +124 -5
  7. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/vector_task_mcp.egg-info/PKG-INFO +2 -1
  8. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/vector_task_mcp.egg-info/requires.txt +1 -0
  9. vector_task_mcp-1.2.7/src/embeddings.py +0 -68
  10. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/.mcp.json +0 -0
  11. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/.python-version +0 -0
  12. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/CLAUDE.md +0 -0
  13. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/LICENSE +0 -0
  14. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/MANIFEST.in +0 -0
  15. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/README.md +0 -0
  16. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/claude-desktop-config.example.json +0 -0
  17. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/requirements.txt +0 -0
  18. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/run-arm64.sh +0 -0
  19. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/setup.cfg +0 -0
  20. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/src/__init__.py +0 -0
  21. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/src/security.py +0 -0
  22. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/tests/test_task_store.py +0 -0
  23. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/vector_task_mcp.egg-info/SOURCES.txt +0 -0
  24. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/vector_task_mcp.egg-info/dependency_links.txt +0 -0
  25. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/vector_task_mcp.egg-info/entry_points.txt +0 -0
  26. {vector_task_mcp-1.2.7 → vector_task_mcp-1.2.9}/vector_task_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vector-task-mcp
3
- Version: 1.2.7
3
+ Version: 1.2.9
4
4
  Summary: A secure, vector-based task management server for Claude Desktop using sqlite-vec and sentence-transformers
5
5
  Author-email: Xsaven <xsaven@gmail.com>
6
6
  License: MIT
@@ -23,6 +23,7 @@ License-File: LICENSE
23
23
  Requires-Dist: mcp>=0.3.0
24
24
  Requires-Dist: sqlite-vec>=0.1.6
25
25
  Requires-Dist: sentence-transformers>=2.2.2
26
+ Requires-Dist: requests>=2.28.0
26
27
  Dynamic: license-file
27
28
 
28
29
  # Vector Task MCP Server
@@ -4,9 +4,10 @@
4
4
  # dependencies = [
5
5
  # "mcp>=0.3.0",
6
6
  # "sqlite-vec>=0.1.6",
7
- # "sentence-transformers>=2.2.2"
7
+ # "sentence-transformers>=2.2.2",
8
+ # "requests>=2.28.0"
8
9
  # ]
9
- # requires-python = ">=3.8"
10
+ # requires-python = ">=3.10"
10
11
  # ///
11
12
 
12
13
  """
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vector-task-mcp"
3
- version = "1.2.7"
3
+ version = "1.2.9"
4
4
  description = "A secure, vector-based task management server for Claude Desktop using sqlite-vec and sentence-transformers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "mcp>=0.3.0",
35
35
  "sqlite-vec>=0.1.6",
36
36
  "sentence-transformers>=2.2.2",
37
+ "requests>=2.28.0",
37
38
  ]
38
39
 
39
40
  [project.urls]
@@ -0,0 +1,151 @@
1
+ """
2
+ Embedding Model Module
3
+ ======================
4
+
5
+ Provides sentence transformer embedding functionality for semantic task search.
6
+ Uses lazy loading with background preload for fast server startup.
7
+ """
8
+
9
+ import threading
10
+ from typing import List, Union, Optional
11
+ import numpy as np
12
+
13
+
14
+ class EmbeddingModelNotReadyError(Exception):
15
+ """Raised when embedding model not loaded and timeout exceeded."""
16
+ pass
17
+
18
+
19
+ class LazyEmbeddingModel:
20
+ """Lazy-loading embedding model with background initialization."""
21
+
22
+ def __init__(self, model_name: str, preload: bool = True):
23
+ """
24
+ Initialize lazy embedding model.
25
+
26
+ Args:
27
+ model_name: HuggingFace model name (e.g., 'sentence-transformers/all-MiniLM-L6-v2')
28
+ preload: If True, start loading model in background immediately
29
+ """
30
+ self.model_name = model_name
31
+ self._model = None
32
+ self._lock = threading.Lock()
33
+ self._ready_event = threading.Event()
34
+ self._load_error: Optional[Exception] = None
35
+
36
+ if preload:
37
+ self._start_background_load()
38
+
39
+ def _start_background_load(self) -> None:
40
+ """Start model loading in background thread."""
41
+ thread = threading.Thread(target=self._load_model, daemon=True)
42
+ thread.start()
43
+
44
+ def _load_model(self) -> None:
45
+ """Load the model (runs in background thread)."""
46
+ try:
47
+ # DEFERRED IMPORT - only happens in background thread
48
+ # This is the key optimization: sentence_transformers import is slow
49
+ from sentence_transformers import SentenceTransformer
50
+ model = SentenceTransformer(self.model_name)
51
+ with self._lock:
52
+ self._model = model
53
+ self._ready_event.set()
54
+ except Exception as e:
55
+ with self._lock:
56
+ self._load_error = e
57
+ self._ready_event.set()
58
+
59
+ def _ensure_model(self, timeout: float = 30.0):
60
+ """
61
+ Ensure model is loaded, waiting up to timeout seconds.
62
+
63
+ Args:
64
+ timeout: Maximum seconds to wait for model
65
+
66
+ Returns:
67
+ Loaded SentenceTransformer model
68
+
69
+ Raises:
70
+ EmbeddingModelNotReadyError: If model not ready within timeout
71
+ """
72
+ if not self._ready_event.wait(timeout=timeout):
73
+ raise EmbeddingModelNotReadyError(
74
+ f"Embedding model not ready after {timeout}s. Try again shortly."
75
+ )
76
+ if self._load_error:
77
+ raise EmbeddingModelNotReadyError(
78
+ f"Failed to load embedding model: {self._load_error}"
79
+ )
80
+ return self._model
81
+
82
+ def encode(self, text: Union[str, List[str]], timeout: float = 30.0) -> np.ndarray:
83
+ """
84
+ Encode text to embedding vector(s).
85
+
86
+ Args:
87
+ text: Single text string or list of texts
88
+ timeout: Maximum seconds to wait for model
89
+
90
+ Returns:
91
+ Numpy array of embeddings (1D for single text, 2D for list)
92
+
93
+ Raises:
94
+ EmbeddingModelNotReadyError: If model not ready within timeout
95
+ """
96
+ model = self._ensure_model(timeout)
97
+ embeddings = model.encode(text, convert_to_numpy=True)
98
+ # Ensure float32 for sqlite-vec compatibility
99
+ return embeddings.astype(np.float32)
100
+
101
+ def encode_single(self, text: str, timeout: float = 30.0) -> np.ndarray:
102
+ """
103
+ Encode single text to embedding vector.
104
+
105
+ Args:
106
+ text: Single text string
107
+ timeout: Maximum seconds to wait for model
108
+
109
+ Returns:
110
+ Numpy array of embedding (1D float32)
111
+
112
+ Raises:
113
+ EmbeddingModelNotReadyError: If model not ready within timeout
114
+ """
115
+ return self.encode(text, timeout)
116
+
117
+ def get_embedding_dim(self, timeout: float = 30.0) -> int:
118
+ """
119
+ Get embedding dimension.
120
+
121
+ Args:
122
+ timeout: Maximum seconds to wait for model
123
+
124
+ Returns:
125
+ Embedding dimension (e.g., 384 for all-MiniLM-L6-v2)
126
+ """
127
+ model = self._ensure_model(timeout)
128
+ return model.get_sentence_embedding_dimension()
129
+
130
+ def is_ready(self) -> bool:
131
+ """
132
+ Check if model is loaded and ready.
133
+
134
+ Returns:
135
+ True if model is ready for encoding
136
+ """
137
+ return self._ready_event.is_set() and self._model is not None and self._load_error is None
138
+
139
+
140
+ def get_embedding_model(model_name: str, preload: bool = True) -> LazyEmbeddingModel:
141
+ """
142
+ Factory function to get embedding model instance.
143
+
144
+ Args:
145
+ model_name: HuggingFace model name
146
+ preload: If True, start loading model in background immediately
147
+
148
+ Returns:
149
+ Initialized LazyEmbeddingModel instance
150
+ """
151
+ return LazyEmbeddingModel(model_name, preload=preload)
@@ -328,4 +328,8 @@ class Config:
328
328
  # Database configuration
329
329
  DB_NAME = "vector_tasks.db"
330
330
  EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
331
- EMBEDDING_DIM = 384
331
+ EMBEDDING_DIM = 384
332
+
333
+ # Embedding model configuration
334
+ EMBEDDING_LOAD_TIMEOUT = 30.0 # Seconds to wait for model during operations
335
+ EMBEDDING_PRELOAD = True # Start loading model in background on init
@@ -22,7 +22,7 @@ from .security import (
22
22
  generate_content_hash, validate_file_path, validate_parent_id,
23
23
  validate_bulk_tasks_params, validate_bulk_task_ids
24
24
  )
25
- from .embeddings import get_embedding_model
25
+ from .embeddings import get_embedding_model, EmbeddingModelNotReadyError
26
26
 
27
27
 
28
28
  class TaskStore:
@@ -261,6 +261,76 @@ class TaskStore:
261
261
  # Recursively propagate to grandparent
262
262
  self._propagate_pending_to_parents(conn, parent_id)
263
263
 
264
+ def _propagate_in_progress_to_parents(self, conn: sqlite3.Connection, task_id: int) -> None:
265
+ """
266
+ Recursively propagate 'in_progress' status to parent tasks when ANY child starts work.
267
+
268
+ Rules:
269
+ - Parent gets 'in_progress' when ANY child becomes 'in_progress'
270
+ - Only propagates if parent is currently 'pending' (not already in_progress/completed)
271
+ - Recursively propagates up the parent chain
272
+
273
+ Args:
274
+ conn: Active database connection (must be within transaction)
275
+ task_id: Current task ID whose status just changed to 'in_progress'
276
+ """
277
+ # Get parent_id and parent status
278
+ cursor = conn.execute('SELECT parent_id FROM tasks WHERE id = ?', (task_id,))
279
+ row = cursor.fetchone()
280
+ if not row or not row[0]: # No parent
281
+ return
282
+
283
+ parent_id = row[0]
284
+
285
+ # Get parent's current status
286
+ parent_status_row = conn.execute('SELECT status FROM tasks WHERE id = ?', (parent_id,)).fetchone()
287
+ if not parent_status_row:
288
+ return
289
+
290
+ parent_status = parent_status_row[0]
291
+
292
+ # Only update parent if it's pending (not already working or completed)
293
+ if parent_status == 'pending':
294
+ conn.execute('UPDATE tasks SET status = ? WHERE id = ?', ('in_progress', parent_id))
295
+ # Recursively propagate to grandparent
296
+ self._propagate_in_progress_to_parents(conn, parent_id)
297
+
298
+ def _propagate_stopped_to_parents(self, conn: sqlite3.Connection, task_id: int) -> None:
299
+ """
300
+ Recursively propagate status to parent tasks when a child is stopped.
301
+
302
+ Rules:
303
+ - When a child is stopped and parent has no other in_progress children,
304
+ parent reverts to 'pending' (no active work)
305
+ - Recursively propagates up the parent chain
306
+
307
+ Args:
308
+ conn: Active database connection (must be within transaction)
309
+ task_id: Current task ID whose status just changed to 'stopped'
310
+ """
311
+ # Get parent_id
312
+ cursor = conn.execute('SELECT parent_id FROM tasks WHERE id = ?', (task_id,))
313
+ row = cursor.fetchone()
314
+ if not row or not row[0]: # No parent
315
+ return
316
+
317
+ parent_id = row[0]
318
+
319
+ # Check if parent has any other in_progress children
320
+ in_progress_children = conn.execute(
321
+ 'SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status = ? AND id != ?',
322
+ (parent_id, 'in_progress', task_id)
323
+ ).fetchone()[0]
324
+
325
+ # If no in_progress children remain, set parent to pending
326
+ if in_progress_children == 0:
327
+ # Get parent's current status
328
+ parent_status_row = conn.execute('SELECT status FROM tasks WHERE id = ?', (parent_id,)).fetchone()
329
+ if parent_status_row and parent_status_row[0] == 'in_progress':
330
+ conn.execute('UPDATE tasks SET status = ? WHERE id = ?', ('pending', parent_id))
331
+ # Recursively propagate to grandparent
332
+ self._propagate_stopped_to_parents(conn, parent_id)
333
+
264
334
  def _start_time_session(self, conn: sqlite3.Connection, task_id: int, start_status: str) -> None:
265
335
  """
266
336
  Create new time session record in task_time_log table.
@@ -403,7 +473,16 @@ class TaskStore:
403
473
  }
404
474
 
405
475
  # Generate embedding from title + content
406
- embedding = self.embedding_model.encode_single(combined)
476
+ try:
477
+ embedding = self.embedding_model.encode_single(combined, timeout=Config.EMBEDDING_LOAD_TIMEOUT)
478
+ except EmbeddingModelNotReadyError as e:
479
+ conn.close()
480
+ return {
481
+ "success": False,
482
+ "error": "model_loading",
483
+ "message": str(e),
484
+ "retry_after": 5
485
+ }
407
486
 
408
487
  # Calculate or validate order
409
488
  if validated_order is None:
@@ -556,7 +635,16 @@ class TaskStore:
556
635
  }
557
636
 
558
637
  # Batch generate embeddings for all valid tasks
559
- embeddings = self.embedding_model.encode(combined_texts)
638
+ try:
639
+ embeddings = self.embedding_model.encode(combined_texts, timeout=Config.EMBEDDING_LOAD_TIMEOUT)
640
+ except EmbeddingModelNotReadyError as e:
641
+ conn.close()
642
+ return {
643
+ "success": False,
644
+ "error": "model_loading",
645
+ "message": str(e),
646
+ "retry_after": 5
647
+ }
560
648
 
561
649
  # Second pass: insert tasks and vectors in single transaction
562
650
  for idx, metadata in enumerate(task_metadata):
@@ -840,7 +928,17 @@ class TaskStore:
840
928
  update_values.append(new_hash)
841
929
 
842
930
  # Generate new embedding
843
- embedding = self.embedding_model.encode_single(combined)
931
+ try:
932
+ embedding = self.embedding_model.encode_single(combined, timeout=Config.EMBEDDING_LOAD_TIMEOUT)
933
+ except EmbeddingModelNotReadyError as e:
934
+ conn.rollback()
935
+ conn.close()
936
+ return {
937
+ "success": False,
938
+ "error": "model_loading",
939
+ "message": str(e),
940
+ "retry_after": 5
941
+ }
844
942
  embedding_blob = sqlite_vec.serialize_float32(embedding)
845
943
 
846
944
  # Update vector
@@ -871,6 +969,10 @@ class TaskStore:
871
969
  elif old_status == 'in_progress' and new_status != 'in_progress':
872
970
  self._finish_time_session(conn, task_id, time_delta, new_status)
873
971
 
972
+ # Propagate 'in_progress' status to parent when ANY child starts work
973
+ if status_changed and new_status == 'in_progress':
974
+ self._propagate_in_progress_to_parents(conn, task_id)
975
+
874
976
  # Propagate 'completed' status to parent when ALL children are finished
875
977
  if status_changed and new_status in TaskStatus.finish_statuses():
876
978
  self._propagate_completed_to_parents(conn, task_id)
@@ -879,6 +981,10 @@ class TaskStore:
879
981
  if status_changed and new_status == 'pending':
880
982
  self._propagate_pending_to_parents(conn, task_id)
881
983
 
984
+ # Propagate 'pending' to parent when child is stopped and no other in_progress children
985
+ if status_changed and new_status == 'stopped':
986
+ self._propagate_stopped_to_parents(conn, task_id)
987
+
882
988
  conn.commit()
883
989
 
884
990
  # Fetch updated task
@@ -1356,7 +1462,20 @@ class TaskStore:
1356
1462
  query = sanitize_input(query)
1357
1463
 
1358
1464
  # Generate query embedding
1359
- query_embedding = self.embedding_model.encode_single(query)
1465
+ try:
1466
+ query_embedding = self.embedding_model.encode_single(query, timeout=Config.EMBEDDING_LOAD_TIMEOUT)
1467
+ except EmbeddingModelNotReadyError as e:
1468
+ conn.close()
1469
+ return {
1470
+ "tasks": [],
1471
+ "total_count": 0,
1472
+ "returned_count": 0,
1473
+ "offset": offset,
1474
+ "success": False,
1475
+ "error": "model_loading",
1476
+ "message": str(e),
1477
+ "retry_after": 5
1478
+ }
1360
1479
  query_blob = sqlite_vec.serialize_float32(query_embedding)
1361
1480
 
1362
1481
  # Build search query
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vector-task-mcp
3
- Version: 1.2.7
3
+ Version: 1.2.9
4
4
  Summary: A secure, vector-based task management server for Claude Desktop using sqlite-vec and sentence-transformers
5
5
  Author-email: Xsaven <xsaven@gmail.com>
6
6
  License: MIT
@@ -23,6 +23,7 @@ License-File: LICENSE
23
23
  Requires-Dist: mcp>=0.3.0
24
24
  Requires-Dist: sqlite-vec>=0.1.6
25
25
  Requires-Dist: sentence-transformers>=2.2.2
26
+ Requires-Dist: requests>=2.28.0
26
27
  Dynamic: license-file
27
28
 
28
29
  # Vector Task MCP Server
@@ -1,3 +1,4 @@
1
1
  mcp>=0.3.0
2
2
  sqlite-vec>=0.1.6
3
3
  sentence-transformers>=2.2.2
4
+ requests>=2.28.0
@@ -1,68 +0,0 @@
1
- """
2
- Embedding Model Module
3
- ======================
4
-
5
- Provides sentence transformer embedding functionality for semantic task search.
6
- """
7
-
8
- from typing import List, Union
9
- import numpy as np
10
- from sentence_transformers import SentenceTransformer
11
-
12
-
13
- class EmbeddingModel:
14
- """Wrapper for sentence transformer embedding model."""
15
-
16
- def __init__(self, model_name: str):
17
- """
18
- Initialize embedding model.
19
-
20
- Args:
21
- model_name: HuggingFace model name (e.g., 'sentence-transformers/all-MiniLM-L6-v2')
22
- """
23
- self.model_name = model_name
24
- self.model = SentenceTransformer(model_name)
25
-
26
- def encode(self, text: Union[str, List[str]]) -> np.ndarray:
27
- """
28
- Encode text to embedding vector(s).
29
-
30
- Args:
31
- text: Single text string or list of texts
32
-
33
- Returns:
34
- Numpy array of embeddings (1D for single text, 2D for list)
35
- """
36
- embeddings = self.model.encode(text, convert_to_numpy=True)
37
-
38
- # Ensure float32 for sqlite-vec compatibility
39
- return embeddings.astype(np.float32)
40
-
41
- def encode_single(self, text: str) -> np.ndarray:
42
- """
43
- Encode single text to embedding vector.
44
-
45
- Args:
46
- text: Single text string
47
-
48
- Returns:
49
- Numpy array of embedding (1D float32)
50
- """
51
- return self.encode(text)
52
-
53
- def get_embedding_dim(self) -> int:
54
- """Get embedding dimension."""
55
- return self.model.get_sentence_embedding_dimension()
56
-
57
-
58
- def get_embedding_model(model_name: str) -> EmbeddingModel:
59
- """
60
- Factory function to get embedding model instance.
61
-
62
- Args:
63
- model_name: HuggingFace model name
64
-
65
- Returns:
66
- Initialized EmbeddingModel instance
67
- """
68
- return EmbeddingModel(model_name)
File without changes