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.
- mcp_sqlite_memory_bank/__init__.py +112 -0
- mcp_sqlite_memory_bank/py.typed +0 -0
- mcp_sqlite_memory_bank/server.py +1015 -0
- mcp_sqlite_memory_bank/types.py +165 -0
- mcp_sqlite_memory_bank/utils.py +195 -0
- mcp_sqlite_memory_bank-0.1.0.dist-info/METADATA +696 -0
- mcp_sqlite_memory_bank-0.1.0.dist-info/RECORD +10 -0
- mcp_sqlite_memory_bank-0.1.0.dist-info/WHEEL +5 -0
- mcp_sqlite_memory_bank-0.1.0.dist-info/licenses/LICENSE +21 -0
- mcp_sqlite_memory_bank-0.1.0.dist-info/top_level.txt +1 -0
@@ -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}"}
|