mcp-sqlite-memory-bank 1.5.1__py3-none-any.whl → 1.6.2__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.
@@ -8,7 +8,7 @@ Cursor, and other LLM-powered tools to interact with structured data in a
8
8
  safe, explicit, and extensible way.
9
9
 
10
10
  Author: Robert Meisner
11
- Version: 0.1.0
11
+ Version: 1.6.2
12
12
  License: MIT
13
13
  """
14
14
 
@@ -73,7 +73,7 @@ from .types import (
73
73
  )
74
74
 
75
75
  # Package metadata
76
- __version__ = "0.1.0"
76
+ __version__ = "1.6.2"
77
77
  __author__ = "Robert Meisner"
78
78
  __all__ = [
79
79
  # Core tools
@@ -93,7 +93,7 @@ __all__ = [
93
93
  "explore_tables",
94
94
  "add_embeddings",
95
95
  "semantic_search",
96
- "find_related",
96
+ "find_related",
97
97
  "smart_search",
98
98
  "embedding_stats",
99
99
  "auto_semantic_search",
@@ -17,16 +17,16 @@ if project_root not in sys.path:
17
17
 
18
18
  # Configure logging before any other imports
19
19
  logging.basicConfig(
20
- level=logging.INFO,
21
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
20
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
22
21
  )
23
22
 
24
- def main():
23
+
24
+ def main() -> None:
25
25
  """Main entry point for the MCP server."""
26
26
  try:
27
27
  # Import here to avoid circular import issues
28
28
  from .server import app, DB_PATH
29
-
29
+
30
30
  # Handle help argument
31
31
  if "--help" in sys.argv or "-h" in sys.argv:
32
32
  print("SQLite Memory Bank MCP Server")
@@ -41,13 +41,13 @@ def main():
41
41
  print("Environment variables:")
42
42
  print(" DB_PATH: Override the default database path")
43
43
  return
44
-
44
+
45
45
  # Log startup information
46
46
  logging.info(f"Starting SQLite Memory Bank MCP server with database at {DB_PATH}")
47
-
47
+
48
48
  # Run the FastMCP app in stdio mode for MCP clients
49
49
  app.run(transport="stdio")
50
-
50
+
51
51
  except KeyboardInterrupt:
52
52
  logging.info("Server stopped by user")
53
53
  sys.exit(0)
@@ -55,5 +55,6 @@ def main():
55
55
  logging.error(f"Failed to start MCP server: {e}")
56
56
  sys.exit(1)
57
57
 
58
+
58
59
  if __name__ == "__main__":
59
60
  main()
@@ -12,7 +12,19 @@ import json
12
12
  import logging
13
13
  from functools import wraps
14
14
  from typing import Dict, List, Any, Optional, Callable, cast
15
- from sqlalchemy import create_engine, MetaData, Table, select, insert, update, delete, text, inspect, and_, or_
15
+ from sqlalchemy import (
16
+ create_engine,
17
+ MetaData,
18
+ Table,
19
+ select,
20
+ insert,
21
+ update,
22
+ delete,
23
+ text,
24
+ inspect,
25
+ and_,
26
+ or_,
27
+ )
16
28
  from sqlalchemy.engine import Engine
17
29
  from sqlalchemy.exc import SQLAlchemyError
18
30
  from contextlib import contextmanager
@@ -63,7 +75,7 @@ class SQLiteMemoryDatabase:
63
75
  except Exception as e:
64
76
  logging.warning(f"Error closing database: {e}")
65
77
 
66
- def __del__(self):
78
+ def __del__(self) -> None:
67
79
  """Ensure cleanup when object is garbage collected."""
68
80
  self.close()
69
81
 
@@ -76,7 +88,7 @@ class SQLiteMemoryDatabase:
76
88
  logging.warning(f"Failed to refresh metadata: {e}")
77
89
 
78
90
  @contextmanager
79
- def get_connection(self):
91
+ def get_connection(self) -> Any:
80
92
  """Get a database connection with automatic cleanup."""
81
93
  conn = self.engine.connect()
82
94
  try:
@@ -96,12 +108,16 @@ class SQLiteMemoryDatabase:
96
108
 
97
109
  return self.metadata.tables[table_name]
98
110
 
99
- def _validate_columns(self, table: Table, column_names: List[str], context: str = "operation") -> None:
111
+ def _validate_columns(
112
+ self, table: Table, column_names: List[str], context: str = "operation"
113
+ ) -> None:
100
114
  """Validate that all column names exist in the table."""
101
115
  valid_columns = set(col.name for col in table.columns)
102
116
  for col_name in column_names:
103
117
  if col_name not in valid_columns:
104
- raise ValidationError(f"Invalid column '{col_name}' for table " f"'{table.name}' in {context}")
118
+ raise ValidationError(
119
+ f"Invalid column '{col_name}' for table " f"'{table.name}' in {context}"
120
+ )
105
121
 
106
122
  def _build_where_conditions(self, table: Table, where: Dict[str, Any]) -> List:
107
123
  """Build SQLAlchemy WHERE conditions from a dictionary."""
@@ -170,7 +186,9 @@ class SQLiteMemoryDatabase:
170
186
  try:
171
187
  with self.get_connection() as conn:
172
188
  inspector = inspect(conn)
173
- tables = [name for name in inspector.get_table_names() if not name.startswith("sqlite_")]
189
+ tables = [
190
+ name for name in inspector.get_table_names() if not name.startswith("sqlite_")
191
+ ]
174
192
  return {"success": True, "tables": tables}
175
193
  except SQLAlchemyError as e:
176
194
  raise DatabaseError(f"Failed to list tables: {str(e)}")
@@ -248,7 +266,10 @@ class SQLiteMemoryDatabase:
248
266
  raise DatabaseError(f"Failed to insert into table {table_name}: {str(e)}")
249
267
 
250
268
  def read_rows(
251
- self, table_name: str, where: Optional[Dict[str, Any]] = None, limit: Optional[int] = None
269
+ self,
270
+ table_name: str,
271
+ where: Optional[Dict[str, Any]] = None,
272
+ limit: Optional[int] = None,
252
273
  ) -> ToolResponse:
253
274
  """Read rows from a table with optional filtering."""
254
275
  try:
@@ -275,7 +296,10 @@ class SQLiteMemoryDatabase:
275
296
  raise DatabaseError(f"Failed to read from table {table_name}: {str(e)}")
276
297
 
277
298
  def update_rows(
278
- self, table_name: str, data: Dict[str, Any], where: Optional[Dict[str, Any]] = None
299
+ self,
300
+ table_name: str,
301
+ data: Dict[str, Any],
302
+ where: Optional[Dict[str, Any]] = None,
279
303
  ) -> ToolResponse:
280
304
  """Update rows in a table."""
281
305
  if not data:
@@ -363,13 +387,16 @@ class SQLiteMemoryDatabase:
363
387
  try:
364
388
  self._refresh_metadata()
365
389
  schemas = {
366
- table_name: [col.name for col in table.columns] for table_name, table in self.metadata.tables.items()
390
+ table_name: [col.name for col in table.columns]
391
+ for table_name, table in self.metadata.tables.items()
367
392
  }
368
393
  return {"success": True, "schemas": schemas}
369
394
  except SQLAlchemyError as e:
370
395
  raise DatabaseError(f"Failed to list all columns: {str(e)}")
371
396
 
372
- def search_content(self, query: str, tables: Optional[List[str]] = None, limit: int = 50) -> ToolResponse:
397
+ def search_content(
398
+ self, query: str, tables: Optional[List[str]] = None, limit: int = 50
399
+ ) -> ToolResponse:
373
400
  """Perform full-text search across table content."""
374
401
  if not query or not query.strip():
375
402
  raise ValidationError("Search query cannot be empty")
@@ -413,42 +440,67 @@ class SQLiteMemoryDatabase:
413
440
  if col.name in row_dict and row_dict[col.name]:
414
441
  content = str(row_dict[col.name]).lower()
415
442
  content_length = len(content)
416
-
443
+
417
444
  if query_lower in content:
418
445
  # Factor 1: Exact phrase frequency (weighted higher)
419
446
  exact_frequency = content.count(query_lower)
420
- exact_score = (exact_frequency * 2.0) / content_length if content_length > 0 else 0
421
-
447
+ exact_score = (
448
+ (exact_frequency * 2.0) / content_length
449
+ if content_length > 0
450
+ else 0
451
+ )
452
+
422
453
  # Factor 2: Individual term frequency
423
454
  term_score = 0.0
424
455
  for term in query_terms:
425
456
  if term in content:
426
- term_score += content.count(term) / content_length if content_length > 0 else 0
427
-
457
+ term_score += (
458
+ content.count(term) / content_length
459
+ if content_length > 0
460
+ else 0
461
+ )
462
+
428
463
  # Factor 3: Position bonus (early matches score higher)
429
464
  position_bonus = 0.0
430
465
  first_occurrence = content.find(query_lower)
431
466
  if first_occurrence != -1:
432
- position_bonus = (content_length - first_occurrence) / content_length * 0.1
433
-
467
+ position_bonus = (
468
+ (content_length - first_occurrence)
469
+ / content_length
470
+ * 0.1
471
+ )
472
+
434
473
  # Factor 4: Column importance (title/name columns get bonus)
435
474
  column_bonus = 0.0
436
- if any(keyword in col.name.lower() for keyword in ['title', 'name', 'summary', 'description']):
475
+ if any(
476
+ keyword in col.name.lower()
477
+ for keyword in [
478
+ "title",
479
+ "name",
480
+ "summary",
481
+ "description",
482
+ ]
483
+ ):
437
484
  column_bonus = 0.2
438
-
485
+
439
486
  # Combined relevance score
440
- col_relevance = exact_score + term_score + position_bonus + column_bonus
487
+ col_relevance = (
488
+ exact_score + term_score + position_bonus + column_bonus
489
+ )
441
490
  relevance_scores.append(col_relevance)
442
-
491
+
443
492
  # Enhanced matched content with context
444
493
  snippet_start = max(0, first_occurrence - 50)
445
- snippet_end = min(len(row_dict[col.name]), first_occurrence + len(query) + 50)
494
+ snippet_end = min(
495
+ len(row_dict[col.name]),
496
+ first_occurrence + len(query) + 50,
497
+ )
446
498
  snippet = str(row_dict[col.name])[snippet_start:snippet_end]
447
499
  if snippet_start > 0:
448
500
  snippet = "..." + snippet
449
501
  if snippet_end < len(str(row_dict[col.name])):
450
502
  snippet = snippet + "..."
451
-
503
+
452
504
  matched_content.append(f"{col.name}: {snippet}")
453
505
 
454
506
  total_relevance = sum(relevance_scores)
@@ -460,8 +512,12 @@ class SQLiteMemoryDatabase:
460
512
  "row_data": row_dict,
461
513
  "matched_content": matched_content,
462
514
  "relevance": round(total_relevance, 4),
463
- "match_quality": "high" if total_relevance > 0.5 else "medium" if total_relevance > 0.1 else "low",
464
- "match_count": len(relevance_scores)
515
+ "match_quality": (
516
+ "high"
517
+ if total_relevance > 0.5
518
+ else ("medium" if total_relevance > 0.1 else "low")
519
+ ),
520
+ "match_count": len(relevance_scores),
465
521
  }
466
522
  )
467
523
 
@@ -471,6 +527,7 @@ class SQLiteMemoryDatabase:
471
527
  if isinstance(rel, (int, float)):
472
528
  return float(rel)
473
529
  return 0.0
530
+
474
531
  results.sort(key=get_relevance, reverse=True)
475
532
  results = results[:limit]
476
533
 
@@ -486,7 +543,9 @@ class SQLiteMemoryDatabase:
486
543
  raise e
487
544
  raise DatabaseError(f"Failed to search content: {str(e)}")
488
545
 
489
- def explore_tables(self, pattern: Optional[str] = None, include_row_counts: bool = True) -> ToolResponse:
546
+ def explore_tables(
547
+ self, pattern: Optional[str] = None, include_row_counts: bool = True
548
+ ) -> ToolResponse:
490
549
  """Explore table structures and content."""
491
550
  try:
492
551
  self._refresh_metadata()
@@ -495,7 +554,11 @@ class SQLiteMemoryDatabase:
495
554
  if pattern:
496
555
  table_names = [name for name in table_names if pattern.replace("%", "") in name]
497
556
 
498
- exploration: Dict[str, Any] = {"tables": [], "total_tables": len(table_names), "total_rows": 0}
557
+ exploration: Dict[str, Any] = {
558
+ "tables": [],
559
+ "total_tables": len(table_names),
560
+ "total_rows": 0,
561
+ }
499
562
 
500
563
  with self.get_connection() as conn:
501
564
  for table_name in table_names:
@@ -518,7 +581,11 @@ class SQLiteMemoryDatabase:
518
581
  if "TEXT" in str(col.type).upper() or "VARCHAR" in str(col.type).upper():
519
582
  text_columns.append(col.name)
520
583
 
521
- table_info: Dict[str, Any] = {"name": table_name, "columns": columns, "text_columns": text_columns}
584
+ table_info: Dict[str, Any] = {
585
+ "name": table_name,
586
+ "columns": columns,
587
+ "text_columns": text_columns,
588
+ }
522
589
 
523
590
  # Add row count if requested
524
591
  if include_row_counts:
@@ -538,8 +605,12 @@ class SQLiteMemoryDatabase:
538
605
  content_preview: Dict[str, List[Any]] = {}
539
606
  for col_name in text_columns[:3]: # Limit to first 3 text columns
540
607
  col = table.c[col_name]
541
- preview_result = conn.execute(select(col).distinct().where(col.isnot(None)).limit(5))
542
- unique_values: List[Any] = [row[0] for row in preview_result.fetchall() if row[0]]
608
+ preview_result = conn.execute(
609
+ select(col).distinct().where(col.isnot(None)).limit(5)
610
+ )
611
+ unique_values: List[Any] = [
612
+ row[0] for row in preview_result.fetchall() if row[0]
613
+ ]
543
614
  if unique_values:
544
615
  content_preview[col_name] = unique_values
545
616
 
@@ -554,14 +625,19 @@ class SQLiteMemoryDatabase:
554
625
 
555
626
  # --- Semantic Search Methods ---
556
627
 
557
- def add_embedding_column(self, table_name: str, embedding_column: str = "embedding") -> EmbeddingColumnResponse:
628
+ def add_embedding_column(
629
+ self, table_name: str, embedding_column: str = "embedding"
630
+ ) -> EmbeddingColumnResponse:
558
631
  """Add an embedding column to a table for semantic search."""
559
632
  try:
560
633
  table = self._ensure_table_exists(table_name)
561
634
 
562
635
  # Check if embedding column already exists
563
636
  if embedding_column in [col.name for col in table.columns]:
564
- return {"success": True, "message": f"Embedding column '{embedding_column}' already exists"}
637
+ return {
638
+ "success": True,
639
+ "message": f"Embedding column '{embedding_column}' already exists",
640
+ }
565
641
 
566
642
  # Add embedding column as TEXT (JSON storage)
567
643
  with self.get_connection() as conn:
@@ -569,7 +645,10 @@ class SQLiteMemoryDatabase:
569
645
  conn.commit()
570
646
 
571
647
  self._refresh_metadata()
572
- return {"success": True, "message": f"Added embedding column '{embedding_column}' to table '{table_name}'"}
648
+ return {
649
+ "success": True,
650
+ "message": f"Added embedding column '{embedding_column}' to table '{table_name}'",
651
+ }
573
652
 
574
653
  except (ValidationError, SQLAlchemyError) as e:
575
654
  if isinstance(e, ValidationError):
@@ -586,7 +665,9 @@ class SQLiteMemoryDatabase:
586
665
  ) -> GenerateEmbeddingsResponse:
587
666
  """Generate embeddings for text content in a table."""
588
667
  if not is_semantic_search_available():
589
- raise ValidationError("Semantic search is not available. Please install sentence-transformers.")
668
+ raise ValidationError(
669
+ "Semantic search is not available. Please install sentence-transformers."
670
+ )
590
671
 
591
672
  try:
592
673
  table = self._ensure_table_exists(table_name)
@@ -656,7 +737,9 @@ class SQLiteMemoryDatabase:
656
737
  processed += 1
657
738
 
658
739
  conn.commit()
659
- logging.info(f"Generated embeddings for batch {i//batch_size + 1}, processed {processed} rows")
740
+ logging.info(
741
+ f"Generated embeddings for batch {i//batch_size + 1}, processed {processed} rows"
742
+ )
660
743
 
661
744
  return {
662
745
  "success": True,
@@ -683,7 +766,9 @@ class SQLiteMemoryDatabase:
683
766
  ) -> SemanticSearchResponse:
684
767
  """Perform semantic search across tables using vector embeddings."""
685
768
  if not is_semantic_search_available():
686
- raise ValidationError("Semantic search is not available. Please install sentence-transformers.")
769
+ raise ValidationError(
770
+ "Semantic search is not available. Please install sentence-transformers."
771
+ )
687
772
 
688
773
  if not query or not query.strip():
689
774
  raise ValidationError("Search query cannot be empty")
@@ -704,7 +789,9 @@ class SQLiteMemoryDatabase:
704
789
 
705
790
  # Check if table has embedding column
706
791
  if embedding_column not in [col.name for col in table.columns]:
707
- logging.warning(f"Table '{table_name}' does not have embedding column '{embedding_column}'")
792
+ logging.warning(
793
+ f"Table '{table_name}' does not have embedding column '{embedding_column}'"
794
+ )
708
795
  continue
709
796
 
710
797
  # Get all rows with embeddings
@@ -753,6 +840,11 @@ class SQLiteMemoryDatabase:
753
840
  all_results.sort(key=lambda x: x.get("similarity_score", 0), reverse=True)
754
841
  final_results = all_results[:limit]
755
842
 
843
+ # Remove embedding data from results to keep LLM responses clean
844
+ for result in final_results:
845
+ if embedding_column in result:
846
+ del result[embedding_column]
847
+
756
848
  return {
757
849
  "success": True,
758
850
  "results": final_results,
@@ -779,7 +871,9 @@ class SQLiteMemoryDatabase:
779
871
  ) -> RelatedContentResponse:
780
872
  """Find content related to a specific row by semantic similarity."""
781
873
  if not is_semantic_search_available():
782
- raise ValidationError("Semantic search is not available. Please install sentence-transformers.")
874
+ raise ValidationError(
875
+ "Semantic search is not available. Please install sentence-transformers."
876
+ )
783
877
 
784
878
  try:
785
879
  table = self._ensure_table_exists(table_name)
@@ -862,13 +956,23 @@ class SQLiteMemoryDatabase:
862
956
  for candidate_idx, similarity_score in similar_indices:
863
957
  original_idx = valid_indices[candidate_idx]
864
958
  row_dict = content_data[original_idx].copy()
959
+
960
+ # Remove embedding data to avoid polluting LLM responses
961
+ if embedding_column in row_dict:
962
+ del row_dict[embedding_column]
963
+
865
964
  row_dict["similarity_score"] = round(similarity_score, 3)
866
965
  results.append(row_dict)
867
966
 
967
+ # Remove embedding from target_row as well
968
+ target_dict_clean = target_dict.copy()
969
+ if embedding_column in target_dict_clean:
970
+ del target_dict_clean[embedding_column]
971
+
868
972
  return {
869
973
  "success": True,
870
974
  "results": results,
871
- "target_row": target_dict,
975
+ "target_row": target_dict_clean,
872
976
  "total_results": len(results),
873
977
  "similarity_threshold": similarity_threshold,
874
978
  "model": model_name,
@@ -950,13 +1054,21 @@ class SQLiteMemoryDatabase:
950
1054
  # Enhance with text matching scores
951
1055
  try:
952
1056
  semantic_engine = get_semantic_engine(model_name)
953
-
1057
+
954
1058
  # Verify the engine has the required method
955
- if not hasattr(semantic_engine, 'hybrid_search') or not callable(getattr(semantic_engine, 'hybrid_search')):
1059
+ if not hasattr(semantic_engine, "hybrid_search") or not callable(
1060
+ getattr(semantic_engine, "hybrid_search")
1061
+ ):
956
1062
  raise DatabaseError("Semantic engine hybrid_search method is not callable")
957
-
1063
+
958
1064
  enhanced_results = semantic_engine.hybrid_search(
959
- query, semantic_results, text_columns or [], embedding_column, semantic_weight, text_weight, limit
1065
+ query,
1066
+ semantic_results,
1067
+ text_columns or [],
1068
+ embedding_column,
1069
+ semantic_weight,
1070
+ text_weight,
1071
+ limit,
960
1072
  )
961
1073
  except Exception as e:
962
1074
  # If semantic enhancement fails, return semantic results without text enhancement
@@ -979,7 +1091,9 @@ class SQLiteMemoryDatabase:
979
1091
  raise e
980
1092
  raise DatabaseError(f"Hybrid search failed: {str(e)}")
981
1093
 
982
- def get_embedding_stats(self, table_name: str, embedding_column: str = "embedding") -> ToolResponse:
1094
+ def get_embedding_stats(
1095
+ self, table_name: str, embedding_column: str = "embedding"
1096
+ ) -> ToolResponse:
983
1097
  """Get statistics about embeddings in a table."""
984
1098
  try:
985
1099
  table = self._ensure_table_exists(table_name)
@@ -989,8 +1103,10 @@ class SQLiteMemoryDatabase:
989
1103
  # Return 0% coverage when column doesn't exist (for compatibility with tests)
990
1104
  total_count = 0
991
1105
  with self.get_connection() as conn:
992
- total_count = conn.execute(select(text("COUNT(*)")).select_from(table)).scalar() or 0
993
-
1106
+ total_count = (
1107
+ conn.execute(select(text("COUNT(*)")).select_from(table)).scalar() or 0
1108
+ )
1109
+
994
1110
  return {
995
1111
  "success": True,
996
1112
  "table_name": table_name,
@@ -1003,7 +1119,9 @@ class SQLiteMemoryDatabase:
1003
1119
 
1004
1120
  with self.get_connection() as conn:
1005
1121
  # Count total rows
1006
- total_count = conn.execute(select(text("COUNT(*)")).select_from(table)).scalar() or 0
1122
+ total_count = (
1123
+ conn.execute(select(text("COUNT(*)")).select_from(table)).scalar() or 0
1124
+ )
1007
1125
 
1008
1126
  # Count rows with embeddings
1009
1127
  embedded_count = (
@@ -1072,7 +1190,7 @@ def get_database(db_path: Optional[str] = None) -> SQLiteMemoryDatabase:
1072
1190
  actual_path = db_path or os.environ.get("DB_PATH", "./test.db")
1073
1191
  if actual_path is None:
1074
1192
  actual_path = "./test.db"
1075
-
1193
+
1076
1194
  if _db_instance is None or (db_path and db_path != _db_instance.db_path):
1077
1195
  # Close previous instance if it exists
1078
1196
  if _db_instance is not None: