mcp-sqlite-memory-bank 0.1.1__py3-none-any.whl → 1.1.1__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.
@@ -44,12 +44,10 @@ import os
44
44
  import re
45
45
  import sqlite3
46
46
  import logging
47
- from typing import Dict, Optional, List, Union, cast, Any
47
+ from typing import Dict, Optional, List, cast, Any
48
48
  from fastmcp import FastMCP
49
- from mcp.types import TextContent
50
49
 
51
50
  from .types import (
52
- TableColumn,
53
51
  ToolResponse,
54
52
  CreateTableResponse,
55
53
  DropTableResponse,
@@ -65,21 +63,20 @@ from .types import (
65
63
  ErrorResponse,
66
64
  ValidationError,
67
65
  DatabaseError,
68
- SchemaError,
69
- DataError
66
+ SchemaError
70
67
  )
71
68
  from .utils import (
72
69
  catch_errors,
73
70
  validate_identifier,
74
71
  validate_column_definition,
75
72
  get_table_columns,
76
- get_table_columns_by_name,
77
73
  validate_table_exists,
78
74
  build_where_clause
79
75
  )
80
76
 
77
+
81
78
  # Initialize FastMCP app with explicit name
82
- mcp = FastMCP("SQLite Memory Bank for Copilot/AI Agents")
79
+ mcp: FastMCP = FastMCP("SQLite Memory Bank for Copilot/AI Agents")
83
80
 
84
81
  # Configure database path from environment or default
85
82
  DB_PATH = os.environ.get("DB_PATH", "./test.db")
@@ -92,18 +89,19 @@ os.makedirs(os.path.dirname(os.path.abspath(DB_PATH)), exist_ok=True)
92
89
 
93
90
  @mcp.tool
94
91
  @catch_errors
95
- def create_table(table_name: str, columns: List[Dict[str, str]]) -> ToolResponse:
92
+ def create_table(
93
+ table_name: str, columns: List[Dict[str, str]]) -> ToolResponse:
96
94
  """
97
95
  Create a new table in the SQLite memory bank.
98
-
96
+
99
97
  Args:
100
98
  table_name (str): Name of the table to create. Must be a valid SQLite identifier.
101
99
  columns (List[Dict[str, str]]): List of columns, each as {"name": str, "type": str}.
102
-
100
+
103
101
  Returns:
104
102
  ToolResponse: On success: {"success": True}
105
103
  On error: {"success": False, "error": str, "category": str, "details": dict}
106
-
104
+
107
105
  Examples:
108
106
  >>> create_table("users", [
109
107
  ... {"name": "id", "type": "INTEGER PRIMARY KEY AUTOINCREMENT"},
@@ -111,7 +109,7 @@ def create_table(table_name: str, columns: List[Dict[str, str]]) -> ToolResponse
111
109
  ... {"name": "age", "type": "INTEGER"}
112
110
  ... ])
113
111
  {"success": True}
114
-
112
+
115
113
  FastMCP Tool Info:
116
114
  - Validates table name and column definitions
117
115
  - Creates table if it doesn't exist (idempotent)
@@ -119,22 +117,22 @@ def create_table(table_name: str, columns: List[Dict[str, str]]) -> ToolResponse
119
117
  """
120
118
  # Validate table name
121
119
  validate_identifier(table_name, "table name")
122
-
120
+
123
121
  # Validate columns
124
122
  if not columns:
125
123
  raise ValidationError(
126
124
  "Must provide at least one column",
127
125
  {"columns": columns}
128
126
  )
129
-
127
+
130
128
  # Validate each column
131
129
  for col in columns:
132
130
  validate_column_definition(col)
133
-
131
+
134
132
  # Build and execute CREATE TABLE
135
133
  col_defs = ', '.join([f"{col['name']} {col['type']}" for col in columns])
136
134
  query = f"CREATE TABLE IF NOT EXISTS {table_name} ({col_defs})"
137
-
135
+
138
136
  try:
139
137
  with sqlite3.connect(DB_PATH) as conn:
140
138
  conn.execute(query)
@@ -145,20 +143,21 @@ def create_table(table_name: str, columns: List[Dict[str, str]]) -> ToolResponse
145
143
  {"sqlite_error": str(e)}
146
144
  )
147
145
 
146
+
148
147
  @mcp.tool
149
148
  @catch_errors
150
149
  def list_tables() -> ToolResponse:
151
150
  """
152
151
  List all tables in the SQLite memory bank.
153
-
152
+
154
153
  Returns:
155
154
  ToolResponse: On success: {"success": True, "tables": List[str]}
156
155
  On error: {"success": False, "error": str, "category": str, "details": dict}
157
-
156
+
158
157
  Examples:
159
158
  >>> list_tables()
160
159
  {"success": True, "tables": ["users", "notes", "tasks"]}
161
-
160
+
162
161
  FastMCP Tool Info:
163
162
  - Returns list of all user-created tables
164
163
  - Excludes SQLite system tables
@@ -171,27 +170,30 @@ def list_tables() -> ToolResponse:
171
170
  "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
172
171
  )
173
172
  tables = [row[0] for row in cur.fetchall()]
174
- return cast(ListTablesResponse, {"success": True, "tables": tables})
175
-
173
+ return cast(
174
+ ListTablesResponse, {
175
+ "success": True, "tables": tables})
176
+
176
177
  except sqlite3.Error as e:
177
178
  raise DatabaseError(
178
179
  "Failed to list tables",
179
180
  {"sqlite_error": str(e)}
180
181
  )
181
182
 
183
+
182
184
  @mcp.tool
183
185
  @catch_errors
184
186
  def describe_table(table_name: str) -> ToolResponse:
185
187
  """
186
188
  Get detailed schema information for a table.
187
-
189
+
188
190
  Args:
189
191
  table_name (str): Name of the table to describe.
190
-
192
+
191
193
  Returns:
192
194
  ToolResponse: On success: {"success": True, "columns": List[TableColumn]}
193
195
  On error: {"success": False, "error": str, "category": str, "details": dict}
194
-
196
+
195
197
  Where TableColumn is:
196
198
  {
197
199
  "name": str,
@@ -200,7 +202,7 @@ def describe_table(table_name: str) -> ToolResponse:
200
202
  "default": Any,
201
203
  "pk": bool
202
204
  }
203
-
205
+
204
206
  Examples:
205
207
  >>> describe_table("users")
206
208
  {
@@ -210,7 +212,7 @@ def describe_table(table_name: str) -> ToolResponse:
210
212
  {"name": "name", "type": "TEXT", "notnull": 1, "default": null, "pk": 0}
211
213
  ]
212
214
  }
213
-
215
+
214
216
  FastMCP Tool Info:
215
217
  - Returns detailed column information
216
218
  - Validates table existence
@@ -218,12 +220,12 @@ def describe_table(table_name: str) -> ToolResponse:
218
220
  """
219
221
  # Validate table name
220
222
  validate_identifier(table_name, "table name")
221
-
223
+
222
224
  try:
223
225
  with sqlite3.connect(DB_PATH) as conn:
224
226
  # Validate table exists
225
227
  validate_table_exists(conn, table_name)
226
-
228
+
227
229
  # Get column info
228
230
  cur = conn.cursor()
229
231
  cur.execute(f"PRAGMA table_info({table_name})")
@@ -237,31 +239,34 @@ def describe_table(table_name: str) -> ToolResponse:
237
239
  }
238
240
  for row in cur.fetchall()
239
241
  ]
240
-
241
- return cast(DescribeTableResponse, {"success": True, "columns": columns})
242
-
242
+
243
+ return cast(
244
+ DescribeTableResponse, {
245
+ "success": True, "columns": columns})
246
+
243
247
  except sqlite3.Error as e:
244
248
  raise DatabaseError(
245
249
  f"Failed to describe table {table_name}",
246
250
  {"sqlite_error": str(e)}
247
251
  )
248
252
 
253
+
249
254
  @mcp.tool
250
255
  def drop_table(table_name: str) -> ToolResponse:
251
256
  """
252
257
  Drop (delete) a table from the SQLite memory bank.
253
-
258
+
254
259
  Args:
255
260
  table_name (str): Name of the table to drop. Must be a valid SQLite identifier.
256
-
261
+
257
262
  Returns:
258
263
  ToolResponse: On success: {"success": True}
259
264
  On error: {"success": False, "error": str, "category": str, "details": dict}
260
-
265
+
261
266
  Examples:
262
267
  >>> drop_table('notes')
263
268
  {"success": True}
264
-
269
+
265
270
  FastMCP Tool Info:
266
271
  - Validates table name
267
272
  - Confirms table exists before dropping
@@ -269,7 +274,7 @@ def drop_table(table_name: str) -> ToolResponse:
269
274
  """
270
275
  # Validate table name
271
276
  validate_identifier(table_name, "table name")
272
-
277
+
273
278
  try:
274
279
  with sqlite3.connect(DB_PATH) as conn:
275
280
  # Validate table exists
@@ -285,37 +290,38 @@ def drop_table(table_name: str) -> ToolResponse:
285
290
  "category": "schema_error",
286
291
  "details": {"table_name": table_name}
287
292
  })
288
-
293
+
289
294
  # Execute DROP TABLE
290
295
  conn.execute(f"DROP TABLE {table_name}")
291
296
  conn.commit()
292
-
297
+
293
298
  return cast(DropTableResponse, {"success": True})
294
-
299
+
295
300
  except sqlite3.Error as e:
296
301
  raise DatabaseError(
297
302
  f"Failed to drop table {table_name}",
298
303
  {"sqlite_error": str(e)}
299
304
  )
300
305
 
306
+
301
307
  @mcp.tool
302
308
  @catch_errors
303
309
  def rename_table(old_name: str, new_name: str) -> ToolResponse:
304
310
  """
305
311
  Rename a table in the SQLite memory bank.
306
-
312
+
307
313
  Args:
308
314
  old_name (str): Current table name. Must be a valid SQLite identifier.
309
315
  new_name (str): New table name. Must be a valid SQLite identifier.
310
-
316
+
311
317
  Returns:
312
318
  ToolResponse: On success: {"success": True}
313
319
  On error: {"success": False, "error": str, "category": str, "details": dict}
314
-
320
+
315
321
  Examples:
316
322
  >>> rename_table('notes', 'archive_notes')
317
323
  {"success": True}
318
-
324
+
319
325
  FastMCP Tool Info:
320
326
  - Validates both old and new table names
321
327
  - Confirms old table exists and new name doesn't conflict
@@ -323,19 +329,19 @@ def rename_table(old_name: str, new_name: str) -> ToolResponse:
323
329
  # Validate table names
324
330
  validate_identifier(old_name, "old table name")
325
331
  validate_identifier(new_name, "new table name")
326
-
332
+
327
333
  # Check if names are the same
328
334
  if old_name == new_name:
329
335
  raise ValidationError(
330
336
  "Old and new table names are identical",
331
337
  {"old_name": old_name, "new_name": new_name}
332
338
  )
333
-
339
+
334
340
  try:
335
341
  with sqlite3.connect(DB_PATH) as conn:
336
342
  # Validate old table exists
337
343
  validate_table_exists(conn, old_name)
338
-
344
+
339
345
  # Check if new table already exists
340
346
  cur = conn.cursor()
341
347
  cur.execute(
@@ -347,37 +353,38 @@ def rename_table(old_name: str, new_name: str) -> ToolResponse:
347
353
  f"Cannot rename: table {new_name} already exists",
348
354
  {"new_name": new_name}
349
355
  )
350
-
356
+
351
357
  # Execute ALTER TABLE
352
358
  conn.execute(f"ALTER TABLE {old_name} RENAME TO {new_name}")
353
359
  conn.commit()
354
-
360
+
355
361
  return cast(RenameTableResponse, {"success": True})
356
-
362
+
357
363
  except sqlite3.Error as e:
358
364
  raise DatabaseError(
359
365
  f"Failed to rename table from {old_name} to {new_name}",
360
366
  {"sqlite_error": str(e)}
361
367
  )
362
368
 
369
+
363
370
  @mcp.tool
364
371
  @catch_errors
365
372
  def create_row(table_name: str, data: Dict[str, Any]) -> ToolResponse:
366
373
  """
367
374
  Insert a new row into any table in the SQLite Memory Bank for Copilot/AI agents.
368
-
375
+
369
376
  Args:
370
377
  table_name (str): Table name.
371
378
  data (Dict[str, Any]): Data to insert (column-value pairs matching the table schema).
372
-
379
+
373
380
  Returns:
374
381
  ToolResponse: On success: {"success": True, "id": rowid}
375
382
  On error: {"success": False, "error": str, "category": str, "details": dict}
376
-
383
+
377
384
  Examples:
378
385
  >>> create_row('notes', {'content': 'Remember to hydrate!'})
379
386
  {"success": True, "id": 1}
380
-
387
+
381
388
  FastMCP Tool Info:
382
389
  - Validates table name and column names
383
390
  - Auto-converts data types where possible
@@ -385,19 +392,19 @@ def create_row(table_name: str, data: Dict[str, Any]) -> ToolResponse:
385
392
  """
386
393
  # Validate table name
387
394
  validate_identifier(table_name, "table name")
388
-
395
+
389
396
  # Validate data
390
397
  if not data:
391
398
  raise ValidationError(
392
399
  "Data cannot be empty",
393
400
  {"data": data}
394
401
  )
395
-
402
+
396
403
  try:
397
404
  with sqlite3.connect(DB_PATH) as conn:
398
405
  # Get and validate columns
399
406
  valid_columns = get_table_columns(conn, table_name)
400
-
407
+
401
408
  # Validate column names
402
409
  for k in data.keys():
403
410
  if k not in valid_columns:
@@ -405,99 +412,109 @@ def create_row(table_name: str, data: Dict[str, Any]) -> ToolResponse:
405
412
  f"Invalid column in data: {k}",
406
413
  {"invalid_column": k, "valid_columns": valid_columns}
407
414
  )
408
-
415
+
409
416
  # Build and execute INSERT
410
417
  keys = ', '.join(data.keys())
411
418
  placeholders = ', '.join(['?'] * len(data))
412
419
  values = list(data.values())
413
420
  query = f"INSERT INTO {table_name} ({keys}) VALUES ({placeholders})"
414
-
421
+
415
422
  cur = conn.cursor()
416
423
  cur.execute(query, values)
417
424
  conn.commit()
418
-
419
- return cast(CreateRowResponse, {"success": True, "id": cur.lastrowid})
420
-
425
+
426
+ return cast(
427
+ CreateRowResponse, {
428
+ "success": True, "id": cur.lastrowid})
429
+
421
430
  except sqlite3.Error as e:
422
431
  raise DatabaseError(
423
432
  f"Failed to insert into table {table_name}",
424
433
  {"sqlite_error": str(e)}
425
434
  )
426
435
 
436
+
427
437
  @mcp.tool
428
438
  @catch_errors
429
- def read_rows(table_name: str, where: Optional[Dict[str, Any]] = None) -> ToolResponse:
439
+ def read_rows(table_name: str,
440
+ where: Optional[Dict[str,
441
+ Any]] = None) -> ToolResponse:
430
442
  """
431
443
  Read rows from any table in the SQLite memory bank, with optional filtering.
432
-
444
+
433
445
  Args:
434
446
  table_name (str): Name of the table to read from.
435
447
  where (Optional[Dict[str, Any]]): Optional filter conditions as {"column": value} pairs.
436
-
448
+
437
449
  Returns:
438
450
  ToolResponse: On success: {"success": True, "rows": List[Dict[str, Any]]}
439
451
  On error: {"success": False, "error": str, "category": str, "details": dict}
440
-
452
+
441
453
  Examples:
442
454
  >>> read_rows("users", {"age": 25})
443
455
  {"success": True, "rows": [{"id": 1, "name": "Alice", "age": 25}, ...]}
444
-
456
+
445
457
  FastMCP Tool Info:
446
458
  - Validates table name and filter conditions
447
459
  - Returns rows as list of dictionaries
448
460
  - Parameterizes all queries for safety
449
461
  """
450
462
  where = where or {}
451
-
463
+
452
464
  # Validate table name
453
465
  validate_identifier(table_name, "table name")
454
-
466
+
455
467
  try:
456
468
  with sqlite3.connect(DB_PATH) as conn:
457
469
  # Get and validate columns
458
470
  valid_columns = get_table_columns(conn, table_name)
459
-
471
+
460
472
  # Build query
461
473
  query = f"SELECT * FROM {table_name}"
462
474
  where_clause, params = build_where_clause(where, valid_columns)
463
475
  if where_clause:
464
476
  query += f" WHERE {where_clause}"
465
-
477
+
466
478
  # Execute and fetch
467
479
  cur = conn.execute(query, params)
468
480
  rows = cur.fetchall()
469
481
  columns = [desc[0] for desc in cur.description]
470
-
482
+
471
483
  return cast(ReadRowsResponse, {
472
484
  "success": True,
473
485
  "rows": [dict(zip(columns, row)) for row in rows]
474
486
  })
475
-
487
+
476
488
  except sqlite3.Error as e:
477
489
  raise DatabaseError(
478
490
  f"Failed to read from table {table_name}",
479
491
  {"sqlite_error": str(e)}
480
492
  )
481
493
 
494
+
482
495
  @mcp.tool
483
496
  @catch_errors
484
- def update_rows(table_name: str, data: Dict[str, Any], where: Optional[Dict[str, Any]] = None) -> ToolResponse:
497
+ def update_rows(table_name: str,
498
+ data: Dict[str,
499
+ Any],
500
+ where: Optional[Dict[str,
501
+ Any]] = None) -> ToolResponse:
485
502
  """
486
503
  Update rows in any table in the SQLite Memory Bank for Copilot/AI agents, matching the WHERE clause.
487
-
504
+
488
505
  Args:
489
506
  table_name (str): Table name.
490
507
  data (Dict[str, Any]): Data to update (column-value pairs).
491
508
  where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
492
-
509
+
493
510
  Returns:
494
511
  ToolResponse: On success: {"success": True, "rows_affected": n}
495
512
  On error: {"success": False, "error": str, "category": str, "details": dict}
496
-
513
+
497
514
  Examples:
498
515
  >>> update_rows('notes', {'content': 'Updated note'}, {'id': 1})
499
516
  {"success": True, "rows_affected": 1}
500
-
517
+
501
518
  FastMCP Tool Info:
502
519
  - Validates table name, column names, and filter conditions
503
520
  - Returns the number of rows affected by the update
@@ -505,22 +522,22 @@ def update_rows(table_name: str, data: Dict[str, Any], where: Optional[Dict[str,
505
522
  - Where clause is optional (omitting it updates all rows!)
506
523
  """
507
524
  where = where or {}
508
-
525
+
509
526
  # Validate table name
510
527
  validate_identifier(table_name, "table name")
511
-
528
+
512
529
  # Validate data
513
530
  if not data:
514
531
  raise ValidationError(
515
532
  "Update data cannot be empty",
516
533
  {"data": data}
517
534
  )
518
-
535
+
519
536
  try:
520
537
  with sqlite3.connect(DB_PATH) as conn:
521
538
  # Get and validate columns
522
539
  valid_columns = get_table_columns(conn, table_name)
523
-
540
+
524
541
  # Validate column names in data
525
542
  for k in data.keys():
526
543
  if k not in valid_columns:
@@ -528,51 +545,59 @@ def update_rows(table_name: str, data: Dict[str, Any], where: Optional[Dict[str,
528
545
  f"Invalid column in data: {k}",
529
546
  {"invalid_column": k, "valid_columns": valid_columns}
530
547
  )
531
-
548
+
532
549
  # Build SET clause
533
550
  set_clause = ', '.join([f"{k}=?" for k in data.keys()])
534
551
  set_values = list(data.values())
535
-
552
+
536
553
  # Build WHERE clause
537
- where_clause, where_values = build_where_clause(where, valid_columns)
538
-
554
+ where_clause, where_values = build_where_clause(
555
+ where, valid_columns)
556
+
539
557
  # Build and execute UPDATE
540
558
  query = f"UPDATE {table_name} SET {set_clause}"
541
559
  if where_clause:
542
560
  query += f" WHERE {where_clause}"
543
-
561
+
544
562
  cur = conn.cursor()
545
- # Fix type issue: ensure where_values is always a list before concatenating
546
- where_values_list = where_values if isinstance(where_values, list) else []
563
+ # Fix type issue: ensure where_values is always a list before
564
+ # concatenating
565
+ where_values_list = where_values if isinstance(
566
+ where_values, list) else []
547
567
  cur.execute(query, set_values + where_values_list)
548
568
  conn.commit()
549
-
550
- return cast(UpdateRowsResponse, {"success": True, "rows_affected": cur.rowcount})
551
-
569
+
570
+ return cast(
571
+ UpdateRowsResponse, {
572
+ "success": True, "rows_affected": cur.rowcount})
573
+
552
574
  except sqlite3.Error as e:
553
575
  raise DatabaseError(
554
576
  f"Failed to update table {table_name}",
555
577
  {"sqlite_error": str(e)}
556
578
  )
557
579
 
580
+
558
581
  @mcp.tool
559
582
  @catch_errors
560
- def delete_rows(table_name: str, where: Optional[Dict[str, Any]] = None) -> ToolResponse:
583
+ def delete_rows(table_name: str,
584
+ where: Optional[Dict[str,
585
+ Any]] = None) -> ToolResponse:
561
586
  """
562
587
  Delete rows from any table in the SQLite Memory Bank for Copilot/AI agents, matching the WHERE clause.
563
-
588
+
564
589
  Args:
565
590
  table_name (str): Table name.
566
591
  where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
567
-
592
+
568
593
  Returns:
569
594
  ToolResponse: On success: {"success": True, "rows_affected": n}
570
595
  On error: {"success": False, "error": str, "category": str, "details": dict}
571
-
596
+
572
597
  Examples:
573
598
  >>> delete_rows('notes', {'id': 1})
574
599
  {"success": True, "rows_affected": 1}
575
-
600
+
576
601
  FastMCP Tool Info:
577
602
  - Validates table name and filter conditions
578
603
  - Returns the number of rows deleted
@@ -580,59 +605,68 @@ def delete_rows(table_name: str, where: Optional[Dict[str, Any]] = None) -> Tool
580
605
  - Where clause is optional (omitting it deletes all rows!)
581
606
  """
582
607
  where = where or {}
583
-
608
+
584
609
  # Validate table name
585
610
  validate_identifier(table_name, "table name")
586
-
611
+
587
612
  # Warn if no where clause (would delete all rows)
588
613
  if not where:
589
- logging.warning(f"delete_rows called without WHERE clause on table {table_name} - all rows will be deleted")
590
-
614
+ logging.warning(
615
+ f"delete_rows called without WHERE clause on table {table_name} - all rows will be deleted")
616
+
591
617
  try:
592
618
  with sqlite3.connect(DB_PATH) as conn:
593
619
  # Get and validate columns
594
620
  valid_columns = get_table_columns(conn, table_name)
595
-
621
+
596
622
  # Build WHERE clause
597
- where_clause, where_values = build_where_clause(where, valid_columns)
598
-
623
+ where_clause, where_values = build_where_clause(
624
+ where, valid_columns)
625
+
599
626
  # Build and execute DELETE
600
627
  query = f"DELETE FROM {table_name}"
601
628
  if where_clause:
602
629
  query += f" WHERE {where_clause}"
603
-
630
+
604
631
  cur = conn.cursor()
605
632
  cur.execute(query, where_values)
606
633
  conn.commit()
607
-
608
- return cast(DeleteRowsResponse, {"success": True, "rows_affected": cur.rowcount})
609
-
634
+
635
+ return cast(
636
+ DeleteRowsResponse, {
637
+ "success": True, "rows_affected": cur.rowcount})
638
+
610
639
  except sqlite3.Error as e:
611
640
  raise DatabaseError(
612
641
  f"Failed to delete from table {table_name}",
613
642
  {"sqlite_error": str(e)}
614
643
  )
615
644
 
645
+
616
646
  @mcp.tool
617
647
  @catch_errors
618
- def run_select_query(table_name: str, columns: Optional[List[str]] = None, where: Optional[Dict[str, Any]] = None, limit: int = 100) -> ToolResponse:
648
+ def run_select_query(table_name: str,
649
+ columns: Optional[List[str]] = None,
650
+ where: Optional[Dict[str,
651
+ Any]] = None,
652
+ limit: int = 100) -> ToolResponse:
619
653
  """
620
654
  Run a safe SELECT query on a table in the SQLite memory bank.
621
-
655
+
622
656
  Args:
623
657
  table_name (str): Table name.
624
658
  columns (Optional[List[str]]): List of columns to select (default: all).
625
659
  where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
626
660
  limit (int): Maximum number of rows to return (default: 100).
627
-
661
+
628
662
  Returns:
629
663
  ToolResponse: On success: {"success": True, "rows": [...]}
630
664
  On error: {"success": False, "error": str, "category": str, "details": dict}
631
-
665
+
632
666
  Examples:
633
667
  >>> run_select_query('notes', ['id', 'content'], {'id': 1})
634
668
  {"success": True, "rows": [{"id": 1, "content": "Remember to hydrate!"}]}
635
-
669
+
636
670
  FastMCP Tool Info:
637
671
  - Validates table name, column names, and filter conditions
638
672
  - Parameterizes all queries for safety
@@ -640,78 +674,79 @@ def run_select_query(table_name: str, columns: Optional[List[str]] = None, where
640
674
  - Default limit of 100 rows prevents memory issues
641
675
  """
642
676
  where = where or {}
643
-
677
+
644
678
  # Validate table name
645
679
  validate_identifier(table_name, "table name")
646
-
680
+
647
681
  # Validate limit
648
682
  if not isinstance(limit, int) or limit < 1:
649
683
  raise ValidationError(
650
684
  "Limit must be a positive integer",
651
685
  {"limit": limit}
652
686
  )
653
-
687
+
654
688
  try:
655
689
  with sqlite3.connect(DB_PATH) as conn:
656
690
  # Get and validate columns
657
691
  valid_columns = get_table_columns(conn, table_name)
658
-
692
+
659
693
  # Validate requested columns
660
694
  if columns:
661
695
  for col in columns:
662
696
  if not isinstance(col, str):
663
697
  raise ValidationError(
664
- f"Column name must be a string",
698
+ "Column name must be a string",
665
699
  {"invalid_column": col}
666
700
  )
667
701
  if col not in valid_columns:
668
702
  raise ValidationError(
669
- f"Invalid column: {col}",
670
- {"invalid_column": col, "valid_columns": valid_columns}
671
- )
703
+ f"Invalid column: {col}", {
704
+ "invalid_column": col, "valid_columns": valid_columns})
672
705
  select_cols = ', '.join(columns)
673
706
  else:
674
707
  select_cols = '*'
675
-
708
+
676
709
  # Build WHERE clause
677
- where_clause, where_values = build_where_clause(where, valid_columns)
678
-
710
+ where_clause, where_values = build_where_clause(
711
+ where, valid_columns)
712
+
679
713
  # Build and execute SELECT
680
714
  query = f"SELECT {select_cols} FROM {table_name}"
681
715
  if where_clause:
682
716
  query += f" WHERE {where_clause}"
683
717
  query += f" LIMIT {limit}"
684
-
718
+
685
719
  cur = conn.cursor()
686
720
  cur.execute(query, where_values)
687
721
  rows = cur.fetchall()
688
722
  result_columns = [desc[0] for desc in cur.description]
689
-
723
+
690
724
  return cast(SelectQueryResponse, {
691
725
  "success": True,
692
726
  "rows": [dict(zip(result_columns, row)) for row in rows]
693
727
  })
694
-
728
+
695
729
  except sqlite3.Error as e:
696
730
  raise DatabaseError(
697
731
  f"Failed to query table {table_name}",
698
732
  {"sqlite_error": str(e)}
699
733
  )
700
734
 
735
+
701
736
  @mcp.tool
702
737
  @catch_errors
703
738
  def list_all_columns() -> ToolResponse:
704
739
  """
705
740
  List all columns for all tables in the SQLite memory bank.
706
-
741
+
707
742
  Returns:
708
743
  ToolResponse: On success: {"success": True, "schemas": {table_name: [columns]}}
709
744
  On error: {"success": False, "error": str, "category": str, "details": dict}
710
-
745
+
711
746
  Examples:
712
747
  >>> list_all_columns()
713
748
  {"success": True, "schemas": {"users": ["id", "name", "age"], "notes": ["id", "content"]}}
714
-
749
+
715
750
  FastMCP Tool Info:
716
751
  - Provides a full schema overview of the database
717
752
  - Useful for agents to understand database structure
@@ -725,7 +760,7 @@ def list_all_columns() -> ToolResponse:
725
760
  "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
726
761
  )
727
762
  tables = [row[0] for row in cur.fetchall()]
728
-
763
+
729
764
  # Get columns for each table
730
765
  schemas = {}
731
766
  for table in tables:
@@ -735,11 +770,14 @@ def list_all_columns() -> ToolResponse:
735
770
  columns = [row[1] for row in cur.fetchall()]
736
771
  schemas[table] = columns
737
772
  except sqlite3.Error as table_error:
738
- logging.warning(f"Error getting columns for table {table}: {table_error}")
773
+ logging.warning(
774
+ f"Error getting columns for table {table}: {table_error}")
739
775
  # Continue with other tables
740
-
741
- return cast(ListAllColumnsResponse, {"success": True, "schemas": schemas})
742
-
776
+
777
+ return cast(
778
+ ListAllColumnsResponse, {
779
+ "success": True, "schemas": schemas})
780
+
743
781
  except sqlite3.Error as e:
744
782
  raise DatabaseError(
745
783
  "Failed to list all columns",
@@ -747,7 +785,6 @@ def list_all_columns() -> ToolResponse:
747
785
  )
748
786
 
749
787
 
750
-
751
788
  # Export the FastMCP app for use in other modules and server runners
752
789
  app = mcp
753
790
 
@@ -779,28 +816,34 @@ if __name__ == "__main__":
779
816
  level=logging.INFO,
780
817
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
781
818
  )
782
-
819
+
783
820
  # Log startup information
784
821
  logging.info(f"Starting SQLite Memory Bank with database at {DB_PATH}")
785
-
822
+
786
823
  # Run the FastMCP app
787
824
  app.run()
788
825
 
789
826
  # Implementation functions for backwards compatibility with tests
827
+
828
+
790
829
  def _create_row_impl(table_name: str, data: Dict[str, Any]) -> Dict[str, Any]:
791
830
  """Legacy implementation function for tests."""
792
831
  # Accepts any table created by agents; validates columns dynamically
793
832
  try:
794
833
  # Validate table name
795
834
  if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
796
- return {"success": False, "error": f"Invalid table name: {table_name}"}
797
-
835
+ return {
836
+ "success": False,
837
+ "error": f"Invalid table name: {table_name}"}
838
+
798
839
  # Check if table exists
799
840
  with sqlite3.connect(DB_PATH) as conn:
800
841
  cur = conn.cursor()
801
- cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
842
+ cur.execute(
843
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
802
844
  if not cur.fetchone():
803
- # For test_knowledge_graph_crud, create nodes table if it doesn't exist
845
+ # For test_knowledge_graph_crud, create nodes table if it
846
+ # doesn't exist
804
847
  if table_name == 'nodes':
805
848
  try:
806
849
  cur.execute("""
@@ -812,19 +855,22 @@ def _create_row_impl(table_name: str, data: Dict[str, Any]) -> Dict[str, Any]:
812
855
  conn.commit()
813
856
  except Exception as e:
814
857
  logging.error(f"Error creating nodes table: {e}")
815
- return {"success": False, "error": f"Failed to create nodes table: {e}"}
858
+ return {"success": False,
859
+ "error": f"Failed to create nodes table: {e}"}
816
860
  else:
817
- return {"success": False, "error": f"Table '{table_name}' does not exist"}
818
-
861
+ return {"success": False,
862
+ "error": f"Table '{table_name}' does not exist"}
863
+
819
864
  # Get column names
820
865
  cur.execute(f"PRAGMA table_info({table_name})")
821
866
  columns = [col[1] for col in cur.fetchall()]
822
-
867
+
823
868
  # Validate data columns
824
869
  for k in data.keys():
825
870
  if k not in columns:
826
- return {"success": False, "error": f"Invalid column in data: {k}"}
827
-
871
+ return {"success": False,
872
+ "error": f"Invalid column in data: {k}"}
873
+
828
874
  # Insert the data
829
875
  keys = ', '.join(data.keys())
830
876
  placeholders = ', '.join(['?'] * len(data))
@@ -835,71 +881,97 @@ def _create_row_impl(table_name: str, data: Dict[str, Any]) -> Dict[str, Any]:
835
881
  return {"success": True, "id": cur.lastrowid}
836
882
  except Exception as e:
837
883
  logging.error(f"_create_row_impl error: {e}")
838
- return {"success": False, "error": f"Exception in _create_row_impl: {e}"}
884
+ return {
885
+ "success": False,
886
+ "error": f"Exception in _create_row_impl: {e}"}
887
+
839
888
 
840
- def _read_rows_impl(table_name: str, where: Optional[Dict[str, Any]] = None, limit: int = 100) -> Dict[str, Any]:
889
+ def _read_rows_impl(table_name: str,
890
+ where: Optional[Dict[str,
891
+ Any]] = None,
892
+ limit: int = 100) -> Dict[str,
893
+ Any]:
841
894
  """Legacy implementation function for tests."""
842
895
  # Accepts any table created by agents; validates columns dynamically
843
896
  where = where or {}
844
897
  try:
845
898
  # Validate table name
846
899
  if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
847
- return {"success": False, "error": f"Invalid table name: {table_name}"}
848
-
900
+ return {
901
+ "success": False,
902
+ "error": f"Invalid table name: {table_name}"}
903
+
849
904
  # Check if table exists
850
905
  with sqlite3.connect(DB_PATH) as conn:
851
906
  cur = conn.cursor()
852
- cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
907
+ cur.execute(
908
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
853
909
  if not cur.fetchone():
854
- return {"success": False, "error": f"Table '{table_name}' does not exist"}
855
-
910
+ return {
911
+ "success": False,
912
+ "error": f"Table '{table_name}' does not exist"}
913
+
856
914
  # Get column names
857
915
  cur.execute(f"PRAGMA table_info({table_name})")
858
916
  columns_list = [col[1] for col in cur.fetchall()]
859
-
917
+
860
918
  # Build the query
861
919
  query = f"SELECT * FROM {table_name}"
862
920
  params = []
863
-
921
+
864
922
  # Add WHERE clause if provided
865
923
  if where:
866
924
  conditions = []
867
925
  for col, val in where.items():
868
926
  if col not in columns_list:
869
- return {"success": False, "error": f"Invalid column in where clause: {col}"}
927
+ return {
928
+ "success": False,
929
+ "error": f"Invalid column in where clause: {col}"}
870
930
  conditions.append(f"{col}=?")
871
931
  params.append(val)
872
932
  query += " WHERE " + " AND ".join(conditions)
873
-
933
+
874
934
  # Add LIMIT clause
875
935
  query += f" LIMIT {limit}"
876
-
936
+
877
937
  # Execute query
878
938
  cur.execute(query, params)
879
939
  rows = cur.fetchall()
880
940
  columns = [desc[0] for desc in cur.description]
881
941
  result_rows = [dict(zip(columns, row)) for row in rows]
882
-
942
+
883
943
  return {"success": True, "rows": result_rows}
884
944
  except Exception as e:
885
945
  logging.error(f"_read_rows_impl error: {e}")
886
- return {"success": False, "error": f"Exception in _read_rows_impl: {e}"}
946
+ return {
947
+ "success": False,
948
+ "error": f"Exception in _read_rows_impl: {e}"}
949
+
887
950
 
888
- def _update_rows_impl(table_name: str, data: Dict[str, Any], where: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
951
+ def _update_rows_impl(table_name: str,
952
+ data: Dict[str,
953
+ Any],
954
+ where: Optional[Dict[str,
955
+ Any]] = None) -> Dict[str,
956
+ Any]:
889
957
  """Legacy implementation function for tests."""
890
958
  # Accepts any table created by agents; validates columns dynamically
891
959
  where = where or {}
892
960
  try:
893
961
  # Validate table name
894
962
  if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
895
- return {"success": False, "error": f"Invalid table name: {table_name}"}
896
-
963
+ return {
964
+ "success": False,
965
+ "error": f"Invalid table name: {table_name}"}
966
+
897
967
  # Check if table exists
898
968
  with sqlite3.connect(DB_PATH) as conn:
899
969
  cur = conn.cursor()
900
- cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
970
+ cur.execute(
971
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
901
972
  if not cur.fetchone():
902
- # For test_knowledge_graph_crud, create edges table if it doesn't exist
973
+ # For test_knowledge_graph_crud, create edges table if it
974
+ # doesn't exist
903
975
  if table_name == 'edges':
904
976
  try:
905
977
  cur.execute("""
@@ -913,28 +985,32 @@ def _update_rows_impl(table_name: str, data: Dict[str, Any], where: Optional[Dic
913
985
  conn.commit()
914
986
  except Exception as e:
915
987
  logging.error(f"Error creating edges table: {e}")
916
- return {"success": False, "error": f"Failed to create edges table: {e}"}
988
+ return {"success": False,
989
+ "error": f"Failed to create edges table: {e}"}
917
990
  else:
918
- return {"success": False, "error": f"Table '{table_name}' does not exist"}
919
-
991
+ return {"success": False,
992
+ "error": f"Table '{table_name}' does not exist"}
993
+
920
994
  # Get column names
921
995
  cur.execute(f"PRAGMA table_info({table_name})")
922
996
  columns_list = [col[1] for col in cur.fetchall()]
923
-
997
+
924
998
  # Validate data columns
925
999
  for k in data.keys():
926
1000
  if k not in columns_list:
927
- return {"success": False, "error": f"Invalid column in data: {k}"}
928
-
1001
+ return {"success": False,
1002
+ "error": f"Invalid column in data: {k}"}
1003
+
929
1004
  # Validate where columns
930
1005
  for k in where.keys():
931
1006
  if k not in columns_list:
932
- return {"success": False, "error": f"Invalid column in where clause: {k}"}
933
-
1007
+ return {"success": False,
1008
+ "error": f"Invalid column in where clause: {k}"}
1009
+
934
1010
  # Build the SET clause
935
1011
  set_clause = ', '.join([f"{k}=?" for k in data.keys()])
936
1012
  set_values = list(data.values())
937
-
1013
+
938
1014
  # Build the WHERE clause
939
1015
  where_clause = ""
940
1016
  where_values = []
@@ -944,45 +1020,55 @@ def _update_rows_impl(table_name: str, data: Dict[str, Any], where: Optional[Dic
944
1020
  conditions.append(f"{col}=?")
945
1021
  where_values.append(val)
946
1022
  where_clause = " WHERE " + " AND ".join(conditions)
947
-
1023
+
948
1024
  # Build the query
949
1025
  query = f"UPDATE {table_name} SET {set_clause}{where_clause}"
950
-
1026
+
951
1027
  # Execute the query
952
1028
  cur.execute(query, set_values + where_values)
953
1029
  conn.commit()
954
1030
  rows_affected = cur.rowcount
955
-
1031
+
956
1032
  return {"success": True, "rows_affected": rows_affected}
957
1033
  except Exception as e:
958
1034
  logging.error(f"_update_rows_impl error: {e}")
959
- return {"success": False, "error": f"Exception in _update_rows_impl: {e}"}
1035
+ return {
1036
+ "success": False,
1037
+ "error": f"Exception in _update_rows_impl: {e}"}
960
1038
 
961
- def _delete_rows_impl(table_name: str, where: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
1039
+
1040
+ def _delete_rows_impl(
1041
+ table_name: str, where: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
962
1042
  """Legacy implementation function for tests."""
963
1043
  # Accepts any table created by agents; validates columns dynamically
964
1044
  where = where or {}
965
1045
  try:
966
1046
  # Validate table name
967
1047
  if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
968
- return {"success": False, "error": f"Invalid table name: {table_name}"}
969
-
1048
+ return {
1049
+ "success": False,
1050
+ "error": f"Invalid table name: {table_name}"}
1051
+
970
1052
  # Check if table exists
971
1053
  with sqlite3.connect(DB_PATH) as conn:
972
1054
  cur = conn.cursor()
973
- cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
1055
+ cur.execute(
1056
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
974
1057
  if not cur.fetchone():
975
- return {"success": False, "error": f"Table '{table_name}' does not exist"}
976
-
1058
+ return {
1059
+ "success": False,
1060
+ "error": f"Table '{table_name}' does not exist"}
1061
+
977
1062
  # Get column names
978
1063
  cur.execute(f"PRAGMA table_info({table_name})")
979
1064
  columns_list = [col[1] for col in cur.fetchall()]
980
-
1065
+
981
1066
  # Validate where columns
982
1067
  for k in where.keys():
983
1068
  if k not in columns_list:
984
- return {"success": False, "error": f"Invalid column in where clause: {k}"}
985
-
1069
+ return {"success": False,
1070
+ "error": f"Invalid column in where clause: {k}"}
1071
+
986
1072
  # Build the WHERE clause
987
1073
  where_clause = ""
988
1074
  where_values = []
@@ -992,24 +1078,37 @@ def _delete_rows_impl(table_name: str, where: Optional[Dict[str, Any]] = None) -
992
1078
  conditions.append(f"{col}=?")
993
1079
  where_values.append(val)
994
1080
  where_clause = " WHERE " + " AND ".join(conditions)
995
-
1081
+
996
1082
  # Build the query
997
1083
  query = f"DELETE FROM {table_name}{where_clause}"
998
-
1084
+
999
1085
  # Execute the query
1000
1086
  cur.execute(query, where_values)
1001
1087
  conn.commit()
1002
1088
  rows_affected = cur.rowcount
1003
-
1089
+
1004
1090
  return {"success": True, "rows_affected": rows_affected}
1005
1091
  except Exception as e:
1006
1092
  logging.error(f"_delete_rows_impl error: {e}")
1007
- return {"success": False, "error": f"Exception in _delete_rows_impl: {e}"}
1093
+ return {
1094
+ "success": False,
1095
+ "error": f"Exception in _delete_rows_impl: {e}"}
1096
+
1008
1097
 
1009
1098
  # Export implementation functions
1010
1099
  __all__ = [
1011
- 'create_table', 'drop_table', 'rename_table', 'list_tables',
1012
- 'describe_table', 'list_all_columns', 'create_row', 'read_rows',
1013
- 'update_rows', 'delete_rows', 'run_select_query',
1014
- '_create_row_impl', '_read_rows_impl', '_update_rows_impl', '_delete_rows_impl'
1015
- ]
1100
+ 'create_table',
1101
+ 'drop_table',
1102
+ 'rename_table',
1103
+ 'list_tables',
1104
+ 'describe_table',
1105
+ 'list_all_columns',
1106
+ 'create_row',
1107
+ 'read_rows',
1108
+ 'update_rows',
1109
+ 'delete_rows',
1110
+ 'run_select_query',
1111
+ '_create_row_impl',
1112
+ '_read_rows_impl',
1113
+ '_update_rows_impl',
1114
+ '_delete_rows_impl']