mcp-sqlite-memory-bank 0.1.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.
@@ -0,0 +1,165 @@
1
+ """
2
+ Type definitions for SQLite Memory Bank.
3
+
4
+ This module contains type definitions and custom error classes used throughout the project.
5
+ Designed for explicit, discoverable use by LLMs and FastMCP clients.
6
+ """
7
+
8
+ from typing import TypedDict, Dict, Any, Literal, List, Union, Optional
9
+ from dataclasses import dataclass
10
+ from enum import Enum, auto
11
+
12
+
13
+ class SqliteType(str, Enum):
14
+ """Valid SQLite column types."""
15
+ TEXT = "TEXT"
16
+ INTEGER = "INTEGER"
17
+ REAL = "REAL"
18
+ BLOB = "BLOB"
19
+ NULL = "NULL"
20
+ TIMESTAMP = "TIMESTAMP" # Common extension
21
+ BOOLEAN = "BOOLEAN" # Common extension
22
+
23
+
24
+ class ErrorCategory(Enum):
25
+ """Categories for SQLite Memory Bank errors."""
26
+ VALIDATION = auto()
27
+ DATABASE = auto()
28
+ SCHEMA = auto()
29
+ DATA = auto()
30
+ SYSTEM = auto()
31
+
32
+
33
+ @dataclass
34
+ class MemoryBankError(Exception):
35
+ """Base class for SQLite Memory Bank errors."""
36
+ message: str
37
+ category: ErrorCategory
38
+ details: Optional[Dict[str, Any]] = None
39
+
40
+ def to_dict(self) -> Dict[str, Any]:
41
+ """Convert error to a dict format suitable for FastMCP responses."""
42
+ return {
43
+ "success": False,
44
+ "error": self.message,
45
+ "category": self.category.name,
46
+ "details": self.details or {}
47
+ }
48
+
49
+
50
+ class ValidationError(MemoryBankError):
51
+ """Error for invalid inputs (table names, column names, data types, etc)."""
52
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
53
+ super().__init__(message, ErrorCategory.VALIDATION, details)
54
+
55
+
56
+ class DatabaseError(MemoryBankError):
57
+ """Error for SQLite database operations."""
58
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
59
+ super().__init__(message, ErrorCategory.DATABASE, details)
60
+
61
+
62
+ class SchemaError(MemoryBankError):
63
+ """Error for schema-related operations (create/alter table, etc)."""
64
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
65
+ super().__init__(message, ErrorCategory.SCHEMA, details)
66
+
67
+
68
+ class DataError(MemoryBankError):
69
+ """Error for data operations (insert/update/delete)."""
70
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
71
+ super().__init__(message, ErrorCategory.DATA, details)
72
+
73
+
74
+ class TableColumn(TypedDict):
75
+ """Structure for table column definitions."""
76
+ name: str
77
+ type: str
78
+ notnull: bool
79
+ default: Any
80
+ pk: bool
81
+
82
+
83
+ class SuccessResponse(TypedDict):
84
+ """Base structure for successful responses."""
85
+ success: Literal[True]
86
+
87
+
88
+ class ErrorResponse(TypedDict):
89
+ """Structure for error responses."""
90
+ success: Literal[False]
91
+ error: str
92
+ category: str
93
+ details: Dict[str, Any]
94
+
95
+
96
+ class CreateTableResponse(SuccessResponse):
97
+ """Response for create_table tool."""
98
+ pass
99
+
100
+
101
+ class DropTableResponse(SuccessResponse):
102
+ """Response for drop_table tool."""
103
+ pass
104
+
105
+
106
+ class RenameTableResponse(SuccessResponse):
107
+ """Response for rename_table tool."""
108
+ pass
109
+
110
+
111
+ class ListTablesResponse(SuccessResponse):
112
+ """Response for list_tables tool."""
113
+ tables: List[str]
114
+
115
+
116
+ class DescribeTableResponse(SuccessResponse):
117
+ """Response for describe_table tool."""
118
+ columns: List[TableColumn]
119
+
120
+
121
+ class ListAllColumnsResponse(SuccessResponse):
122
+ """Response for list_all_columns tool."""
123
+ schemas: Dict[str, List[str]]
124
+
125
+
126
+ class CreateRowResponse(SuccessResponse):
127
+ """Response for create_row tool."""
128
+ id: int
129
+
130
+
131
+ class ReadRowsResponse(SuccessResponse):
132
+ """Response for read_rows tool."""
133
+ rows: List[Dict[str, Any]]
134
+
135
+
136
+ class UpdateRowsResponse(SuccessResponse):
137
+ """Response for update_rows tool."""
138
+ rows_affected: int
139
+
140
+
141
+ class DeleteRowsResponse(SuccessResponse):
142
+ """Response for delete_rows tool."""
143
+ rows_affected: int
144
+
145
+
146
+ class SelectQueryResponse(SuccessResponse):
147
+ """Response for run_select_query tool."""
148
+ rows: List[Dict[str, Any]]
149
+
150
+
151
+ # Type alias for all possible responses
152
+ ToolResponse = Union[
153
+ CreateTableResponse,
154
+ DropTableResponse,
155
+ RenameTableResponse,
156
+ ListTablesResponse,
157
+ DescribeTableResponse,
158
+ ListAllColumnsResponse,
159
+ CreateRowResponse,
160
+ ReadRowsResponse,
161
+ UpdateRowsResponse,
162
+ DeleteRowsResponse,
163
+ SelectQueryResponse,
164
+ ErrorResponse
165
+ ]
@@ -0,0 +1,195 @@
1
+ """
2
+ Utility functions for SQLite Memory Bank.
3
+
4
+ This module provides common functionality used across the project,
5
+ including validation, error handling, and database utilities.
6
+ """
7
+
8
+ import re
9
+ import os
10
+ import sqlite3
11
+ import logging
12
+ from functools import wraps
13
+ from typing import Any, Callable, Dict, List, TypeVar, cast, Union, Tuple
14
+ from .types import (
15
+ ValidationError,
16
+ DatabaseError,
17
+ SchemaError,
18
+ DataError,
19
+ MemoryBankError,
20
+ ToolResponse
21
+ )
22
+
23
+ T = TypeVar('T', bound=Callable[..., ToolResponse])
24
+
25
+
26
+ def catch_errors(f: T) -> T:
27
+ """
28
+ Decorator to standardize error handling across tools.
29
+ Catches exceptions and converts them to appropriate error responses.
30
+ """
31
+ @wraps(f)
32
+ def wrapper(*args: Any, **kwargs: Any) -> ToolResponse:
33
+ try:
34
+ return f(*args, **kwargs)
35
+ except MemoryBankError as e:
36
+ logging.error(f"{f.__name__} error: {e}")
37
+ return cast(ToolResponse, e.to_dict())
38
+ except sqlite3.Error as e:
39
+ logging.error(f"{f.__name__} database error: {e}")
40
+ return cast(ToolResponse, DatabaseError(
41
+ f"Database error in {f.__name__}: {e}",
42
+ {"sqlite_error": str(e)}
43
+ ).to_dict())
44
+ except Exception as e:
45
+ logging.error(f"Unexpected error in {f.__name__}: {e}")
46
+ return cast(ToolResponse, DatabaseError(
47
+ f"Unexpected error in {f.__name__}: {e}"
48
+ ).to_dict())
49
+ return cast(T, wrapper)
50
+
51
+
52
+ def validate_identifier(name: str, context: str = "identifier") -> None:
53
+ """
54
+ Validate that a string is a valid SQLite identifier (table/column name).
55
+ Raises ValidationError if invalid.
56
+
57
+ Args:
58
+ name: The identifier to validate
59
+ context: Description of what's being validated (for error messages)
60
+ """
61
+ if not bool(re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', name)):
62
+ raise ValidationError(
63
+ f"Invalid {context}: {name}. Must start with letter/underscore and contain only letters, numbers, underscores.",
64
+ {"invalid_name": name}
65
+ )
66
+
67
+
68
+ def validate_column_definition(column: Dict[str, Any]) -> None:
69
+ """
70
+ Validate a column definition dictionary.
71
+ Raises ValidationError if invalid.
72
+
73
+ Args:
74
+ column: Dictionary with column definition (must have 'name' and 'type' keys)
75
+ """
76
+ if not isinstance(column, dict):
77
+ raise ValidationError(
78
+ "Column definition must be a dictionary",
79
+ {"received": str(type(column))}
80
+ )
81
+ if "name" not in column or "type" not in column:
82
+ raise ValidationError(
83
+ "Column definition must have 'name' and 'type' keys",
84
+ {"missing_keys": [k for k in ["name", "type"] if k not in column]}
85
+ )
86
+ validate_identifier(column["name"], "column name")
87
+
88
+
89
+ def get_table_columns(conn: sqlite3.Connection, table_name: str) -> List[str]:
90
+ """
91
+ Get list of column names for a table.
92
+ Raises SchemaError if table doesn't exist.
93
+
94
+ Args:
95
+ conn: SQLite connection
96
+ table_name: Name of table to check
97
+
98
+ Returns:
99
+ List of column names
100
+ """
101
+ validate_identifier(table_name, "table name")
102
+ cur = conn.cursor()
103
+ cur.execute(f"PRAGMA table_info({table_name})")
104
+ columns = [row[1] for row in cur.fetchall()]
105
+ if not columns:
106
+ raise SchemaError(
107
+ f"Table does not exist: {table_name}",
108
+ {"table_name": table_name}
109
+ )
110
+ return columns
111
+
112
+ # Compatibility function for direct table_name usage
113
+ def get_table_columns_by_name(table_name: str) -> Union[List[str], Dict[str, Any]]:
114
+ """
115
+ Get list of column names for a table by name.
116
+ Compatibility function for the old implementation.
117
+
118
+ Args:
119
+ table_name: Name of table to check
120
+
121
+ Returns:
122
+ List of column names or error dict
123
+ """
124
+ try:
125
+ validate_identifier(table_name, "table name")
126
+ with sqlite3.connect(os.environ.get("DB_PATH", "./test.db")) as conn:
127
+ cur = conn.cursor()
128
+ cur.execute(
129
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
130
+ (table_name,)
131
+ )
132
+ if not cur.fetchone():
133
+ return {"success": False, "error": f"Table '{table_name}' does not exist"}
134
+
135
+ # Get column information
136
+ cur.execute(f"PRAGMA table_info({table_name})")
137
+ columns = [col[1] for col in cur.fetchall()]
138
+ return columns
139
+ except MemoryBankError as e:
140
+ return e.to_dict()
141
+ except Exception as e:
142
+ return {"success": False, "error": f"Exception in get_table_columns: {e}"}
143
+
144
+
145
+ def validate_table_exists(conn: sqlite3.Connection, table_name: str) -> None:
146
+ """
147
+ Validate that a table exists.
148
+ Raises SchemaError if it doesn't.
149
+
150
+ Args:
151
+ conn: SQLite connection
152
+ table_name: Name of table to check
153
+ """
154
+ cur = conn.cursor()
155
+ cur.execute(
156
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
157
+ (table_name,)
158
+ )
159
+ if not cur.fetchone():
160
+ raise SchemaError(
161
+ f"Table does not exist: {table_name}",
162
+ {"table_name": table_name}
163
+ )
164
+
165
+
166
+ def build_where_clause(where: Dict[str, Any], valid_columns: List[str]) -> Union[Tuple[str, list], Dict[str, Any]]:
167
+ """
168
+ Build a WHERE clause from a dictionary of column-value pairs.
169
+
170
+ Args:
171
+ where: Dictionary of {column: value} pairs
172
+ valid_columns: List of valid column names for validation
173
+
174
+ Returns:
175
+ Tuple of (where_clause, parameter_values) or error dict if validation fails
176
+ """
177
+ if not where:
178
+ return "", []
179
+
180
+ try:
181
+ # Validate column names
182
+ conditions = []
183
+ values = []
184
+
185
+ for col, val in where.items():
186
+ if col not in valid_columns:
187
+ return {"success": False, "error": f"Invalid column in where clause: {col}"}
188
+ conditions.append(f"{col}=?")
189
+ values.append(val)
190
+
191
+ clause = ' AND '.join(conditions)
192
+ return clause, values
193
+ except Exception as e:
194
+ logging.error(f"Error in build_where_clause: {e}")
195
+ return {"success": False, "error": f"Error building WHERE clause: {e}"}