mcp-sqlite-memory-bank 1.2.3__py3-none-any.whl → 1.3.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.
@@ -1,1140 +1,838 @@
1
- """
2
- SQLite Memory Bank for Copilot/AI Agents
3
- =======================================
4
-
5
- This FastMCP server provides a dynamic, agent-friendly SQLite memory bank.
6
- All APIs are explicit, discoverable, and validated for safe, flexible use by LLMs and agent frameworks.
7
-
8
- **Design Note:**
9
- - All CRUD and schema operations are exposed as explicit, type-annotated, and well-documented FastMCP tools.
10
- - All tools are registered directly on the FastMCP app instance using the @mcp.tool decorator.
11
- - This design is preferred for LLMs and clients, as it ensures discoverability, schema validation, and ease of use.
12
- - No multiplexed or control-argument tools are provided as primary interfaces.
13
-
14
- Available Tools (for LLMs/agents):
15
- ----------------------------------
16
- - create_table(table, columns): Create a new table with a custom schema.
17
- - drop_table(table): Drop (delete) a table.
18
- - rename_table(old_name, new_name): Rename a table.
19
- - list_tables(): List all tables in the memory bank.
20
- - describe_table(table): Get schema details for a table.
21
- - list_all_columns(): List all columns for all tables.
22
- - create_row(table, data): Insert a row into any table.
23
- - read_rows(table, where): Read rows from any table (with optional filtering).
24
- - update_rows(table, data, where): Update rows from any table (with optional filtering).
25
- - delete_rows(table, where): Delete rows from any table (with optional filtering).
26
- - run_select_query(table, columns, where): Run a safe SELECT query (no arbitrary SQL).
27
-
28
- All table/column names are validated to prevent SQL injection.
29
- Only safe, explicit operations are allowed (no arbitrary SQL).
30
- All tools are documented and designed for explicit, LLM-friendly use.
31
-
32
- FastMCP Tool Documentation:
33
- --------------------------
34
- Each tool is designed for explicit, discoverable use by LLMs and agents:
35
- - All parameters are strongly typed and validated
36
- - Success/error responses follow a consistent pattern
37
- - Error messages are clear and actionable for both humans and LLMs
38
- - Documentation includes examples and common use cases
39
-
40
- Author: Robert Meisner
41
- """
42
-
43
- import os
44
- import re
45
- import sqlite3
46
- import logging
47
- from typing import Dict, Optional, List, cast, Any
48
- from fastmcp import FastMCP
49
-
50
- from .types import (
51
- ToolResponse,
52
- CreateTableResponse,
53
- DropTableResponse,
54
- RenameTableResponse,
55
- ListTablesResponse,
56
- DescribeTableResponse,
57
- ListAllColumnsResponse,
58
- CreateRowResponse,
59
- ReadRowsResponse,
60
- UpdateRowsResponse,
61
- DeleteRowsResponse,
62
- SelectQueryResponse,
63
- ErrorResponse,
64
- ValidationError,
65
- DatabaseError,
66
- SchemaError
67
- )
68
- from .utils import (
69
- catch_errors,
70
- validate_identifier,
71
- validate_column_definition,
72
- get_table_columns,
73
- validate_table_exists,
74
- build_where_clause
75
- )
76
-
77
-
78
- # Initialize FastMCP app with explicit name
79
- mcp: FastMCP = FastMCP("SQLite Memory Bank for Copilot/AI Agents")
80
-
81
- # Configure database path from environment or default
82
- DB_PATH = os.environ.get("DB_PATH", "./test.db")
83
-
84
- # Ensure database directory exists
85
- os.makedirs(os.path.dirname(os.path.abspath(DB_PATH)), exist_ok=True)
86
-
87
-
88
- # --- Schema Management Tools for SQLite Memory Bank ---
89
-
90
- @mcp.tool
91
- @catch_errors
92
- def create_table(
93
- table_name: str, columns: List[Dict[str, str]]) -> ToolResponse:
94
- """
95
- Create a new table in the SQLite memory bank.
96
-
97
- Args:
98
- table_name (str): Name of the table to create. Must be a valid SQLite identifier.
99
- columns (List[Dict[str, str]]): List of columns, each as {"name": str, "type": str}.
100
-
101
- Returns:
102
- ToolResponse: On success: {"success": True}
103
- On error: {"success": False, "error": str, "category": str, "details": dict}
104
-
105
- Examples:
106
- >>> create_table("users", [
107
- ... {"name": "id", "type": "INTEGER PRIMARY KEY AUTOINCREMENT"},
108
- ... {"name": "name", "type": "TEXT"},
109
- ... {"name": "age", "type": "INTEGER"}
110
- ... ])
111
- {"success": True}
112
-
113
- FastMCP Tool Info:
114
- - Validates table name and column definitions
115
- - Creates table if it doesn't exist (idempotent)
116
- - Raises appropriate errors for invalid input
117
- """
118
- # Validate table name
119
- validate_identifier(table_name, "table name")
120
-
121
- # Validate columns
122
- if not columns:
123
- raise ValidationError(
124
- "Must provide at least one column",
125
- {"columns": columns}
126
- )
127
-
128
- # Validate each column
129
- for col in columns:
130
- validate_column_definition(col)
131
-
132
- # Build and execute CREATE TABLE
133
- col_defs = ', '.join([f"{col['name']} {col['type']}" for col in columns])
134
- query = f"CREATE TABLE IF NOT EXISTS {table_name} ({col_defs})"
135
-
136
- try:
137
- with sqlite3.connect(DB_PATH) as conn:
138
- conn.execute(query)
139
- return cast(CreateTableResponse, {"success": True})
140
- except sqlite3.Error as e:
141
- raise DatabaseError(
142
- f"Failed to create table {table_name}",
143
- {"sqlite_error": str(e)}
144
- )
145
-
146
-
147
- @mcp.tool
148
- @catch_errors
149
- def list_tables() -> ToolResponse:
150
- """
151
- List all tables in the SQLite memory bank.
152
-
153
- Returns:
154
- ToolResponse: On success: {"success": True, "tables": List[str]}
155
- On error: {"success": False, "error": str, "category": str, "details": dict}
156
-
157
- Examples:
158
- >>> list_tables()
159
- {"success": True, "tables": ["users", "notes", "tasks"]}
160
-
161
- FastMCP Tool Info:
162
- - Returns list of all user-created tables
163
- - Excludes SQLite system tables
164
- - Useful for schema discovery by LLMs
165
- """
166
- try:
167
- with sqlite3.connect(DB_PATH) as conn:
168
- cur = conn.cursor()
169
- cur.execute(
170
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
171
- )
172
- tables = [row[0] for row in cur.fetchall()]
173
- return cast(
174
- ListTablesResponse, {
175
- "success": True, "tables": tables})
176
-
177
- except sqlite3.Error as e:
178
- raise DatabaseError(
179
- "Failed to list tables",
180
- {"sqlite_error": str(e)}
181
- )
182
-
183
-
184
- @mcp.tool
185
- @catch_errors
186
- def describe_table(table_name: str) -> ToolResponse:
187
- """
188
- Get detailed schema information for a table.
189
-
190
- Args:
191
- table_name (str): Name of the table to describe.
192
-
193
- Returns:
194
- ToolResponse: On success: {"success": True, "columns": List[TableColumn]}
195
- On error: {"success": False, "error": str, "category": str, "details": dict}
196
-
197
- Where TableColumn is:
198
- {
199
- "name": str,
200
- "type": str,
201
- "notnull": bool,
202
- "default": Any,
203
- "pk": bool
204
- }
205
-
206
- Examples:
207
- >>> describe_table("users")
208
- {
209
- "success": True,
210
- "columns": [
211
- {"name": "id", "type": "INTEGER", "notnull": 1, "default": null, "pk": 1},
212
- {"name": "name", "type": "TEXT", "notnull": 1, "default": null, "pk": 0}
213
- ]
214
- }
215
-
216
- FastMCP Tool Info:
217
- - Returns detailed column information
218
- - Validates table existence
219
- - Useful for schema introspection by LLMs
220
- """
221
- # Validate table name
222
- validate_identifier(table_name, "table name")
223
-
224
- try:
225
- with sqlite3.connect(DB_PATH) as conn:
226
- # Validate table exists
227
- validate_table_exists(conn, table_name)
228
-
229
- # Get column info
230
- cur = conn.cursor()
231
- cur.execute(f"PRAGMA table_info({table_name})")
232
- columns = [
233
- {
234
- "name": row[1],
235
- "type": row[2],
236
- "notnull": bool(row[3]),
237
- "default": row[4],
238
- "pk": bool(row[5])
239
- }
240
- for row in cur.fetchall()
241
- ]
242
-
243
- return cast(
244
- DescribeTableResponse, {
245
- "success": True, "columns": columns})
246
-
247
- except sqlite3.Error as e:
248
- raise DatabaseError(
249
- f"Failed to describe table {table_name}",
250
- {"sqlite_error": str(e)}
251
- )
252
-
253
-
254
- @mcp.tool
255
- def drop_table(table_name: str) -> ToolResponse:
256
- """
257
- Drop (delete) a table from the SQLite memory bank.
258
-
259
- Args:
260
- table_name (str): Name of the table to drop. Must be a valid SQLite identifier.
261
-
262
- Returns:
263
- ToolResponse: On success: {"success": True}
264
- On error: {"success": False, "error": str, "category": str, "details": dict}
265
-
266
- Examples:
267
- >>> drop_table('notes')
268
- {"success": True}
269
-
270
- FastMCP Tool Info:
271
- - Validates table name
272
- - Confirms table exists before dropping
273
- - WARNING: This operation is irreversible and deletes all data in the table
274
- """
275
- # Validate table name
276
- validate_identifier(table_name, "table name")
277
-
278
- try:
279
- with sqlite3.connect(DB_PATH) as conn:
280
- # Validate table exists
281
- cur = conn.cursor()
282
- cur.execute(
283
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
284
- (table_name,)
285
- )
286
- if not cur.fetchone():
287
- return cast(ErrorResponse, {
288
- "success": False,
289
- "error": f"Table does not exist: {table_name}",
290
- "category": "schema_error",
291
- "details": {"table_name": table_name}
292
- })
293
-
294
- # Execute DROP TABLE
295
- conn.execute(f"DROP TABLE {table_name}")
296
- conn.commit()
297
-
298
- return cast(DropTableResponse, {"success": True})
299
-
300
- except sqlite3.Error as e:
301
- raise DatabaseError(
302
- f"Failed to drop table {table_name}",
303
- {"sqlite_error": str(e)}
304
- )
305
-
306
-
307
- @mcp.tool
308
- @catch_errors
309
- def rename_table(old_name: str, new_name: str) -> ToolResponse:
310
- """
311
- Rename a table in the SQLite memory bank.
312
-
313
- Args:
314
- old_name (str): Current table name. Must be a valid SQLite identifier.
315
- new_name (str): New table name. Must be a valid SQLite identifier.
316
-
317
- Returns:
318
- ToolResponse: On success: {"success": True}
319
- On error: {"success": False, "error": str, "category": str, "details": dict}
320
-
321
- Examples:
322
- >>> rename_table('notes', 'archive_notes')
323
- {"success": True}
324
-
325
- FastMCP Tool Info:
326
- - Validates both old and new table names
327
- - Confirms old table exists and new name doesn't conflict
328
- """
329
- # Validate table names
330
- validate_identifier(old_name, "old table name")
331
- validate_identifier(new_name, "new table name")
332
-
333
- # Check if names are the same
334
- if old_name == new_name:
335
- raise ValidationError(
336
- "Old and new table names are identical",
337
- {"old_name": old_name, "new_name": new_name}
338
- )
339
-
340
- try:
341
- with sqlite3.connect(DB_PATH) as conn:
342
- # Validate old table exists
343
- validate_table_exists(conn, old_name)
344
-
345
- # Check if new table already exists
346
- cur = conn.cursor()
347
- cur.execute(
348
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
349
- (new_name,)
350
- )
351
- if cur.fetchone():
352
- raise SchemaError(
353
- f"Cannot rename: table {new_name} already exists",
354
- {"new_name": new_name}
355
- )
356
-
357
- # Execute ALTER TABLE
358
- conn.execute(f"ALTER TABLE {old_name} RENAME TO {new_name}")
359
- conn.commit()
360
-
361
- return cast(RenameTableResponse, {"success": True})
362
-
363
- except sqlite3.Error as e:
364
- raise DatabaseError(
365
- f"Failed to rename table from {old_name} to {new_name}",
366
- {"sqlite_error": str(e)}
367
- )
368
-
369
-
370
- @mcp.tool
371
- @catch_errors
372
- def create_row(table_name: str, data: Dict[str, Any]) -> ToolResponse:
373
- """
374
- Insert a new row into any table in the SQLite Memory Bank for Copilot/AI agents.
375
-
376
- Args:
377
- table_name (str): Table name.
378
- data (Dict[str, Any]): Data to insert (column-value pairs matching the table schema).
379
-
380
- Returns:
381
- ToolResponse: On success: {"success": True, "id": rowid}
382
- On error: {"success": False, "error": str, "category": str, "details": dict}
383
-
384
- Examples:
385
- >>> create_row('notes', {'content': 'Remember to hydrate!'})
386
- {"success": True, "id": 1}
387
-
388
- FastMCP Tool Info:
389
- - Validates table name and column names
390
- - Auto-converts data types where possible
391
- - Returns the row ID of the inserted row
392
- """
393
- # Validate table name
394
- validate_identifier(table_name, "table name")
395
-
396
- # Validate data
397
- if not data:
398
- raise ValidationError(
399
- "Data cannot be empty",
400
- {"data": data}
401
- )
402
-
403
- try:
404
- with sqlite3.connect(DB_PATH) as conn:
405
- # Get and validate columns
406
- valid_columns = get_table_columns(conn, table_name)
407
-
408
- # Validate column names
409
- for k in data.keys():
410
- if k not in valid_columns:
411
- raise ValidationError(
412
- f"Invalid column in data: {k}",
413
- {"invalid_column": k, "valid_columns": valid_columns}
414
- )
415
-
416
- # Build and execute INSERT
417
- keys = ', '.join(data.keys())
418
- placeholders = ', '.join(['?'] * len(data))
419
- values = list(data.values())
420
- query = f"INSERT INTO {table_name} ({keys}) VALUES ({placeholders})"
421
-
422
- cur = conn.cursor()
423
- cur.execute(query, values)
424
- conn.commit()
425
-
426
- return cast(
427
- CreateRowResponse, {
428
- "success": True, "id": cur.lastrowid})
429
-
430
- except sqlite3.Error as e:
431
- raise DatabaseError(
432
- f"Failed to insert into table {table_name}",
433
- {"sqlite_error": str(e)}
434
- )
435
-
436
-
437
- @mcp.tool
438
- @catch_errors
439
- def read_rows(table_name: str,
440
- where: Optional[Dict[str,
441
- Any]] = None) -> ToolResponse:
442
- """
443
- Read rows from any table in the SQLite memory bank, with optional filtering.
444
-
445
- Args:
446
- table_name (str): Name of the table to read from.
447
- where (Optional[Dict[str, Any]]): Optional filter conditions as {"column": value} pairs.
448
-
449
- Returns:
450
- ToolResponse: On success: {"success": True, "rows": List[Dict[str, Any]]}
451
- On error: {"success": False, "error": str, "category": str, "details": dict}
452
-
453
- Examples:
454
- >>> read_rows("users", {"age": 25})
455
- {"success": True, "rows": [{"id": 1, "name": "Alice", "age": 25}, ...]}
456
-
457
- FastMCP Tool Info:
458
- - Validates table name and filter conditions
459
- - Returns rows as list of dictionaries
460
- - Parameterizes all queries for safety
461
- """
462
- where = where or {}
463
-
464
- # Validate table name
465
- validate_identifier(table_name, "table name")
466
-
467
- try:
468
- with sqlite3.connect(DB_PATH) as conn:
469
- # Get and validate columns
470
- valid_columns = get_table_columns(conn, table_name)
471
-
472
- # Build query
473
- query = f"SELECT * FROM {table_name}"
474
- where_clause, params = build_where_clause(where, valid_columns)
475
- if where_clause:
476
- query += f" WHERE {where_clause}"
477
-
478
- # Execute and fetch
479
- cur = conn.execute(query, params)
480
- rows = cur.fetchall()
481
- columns = [desc[0] for desc in cur.description]
482
-
483
- return cast(ReadRowsResponse, {
484
- "success": True,
485
- "rows": [dict(zip(columns, row)) for row in rows]
486
- })
487
-
488
- except sqlite3.Error as e:
489
- raise DatabaseError(
490
- f"Failed to read from table {table_name}",
491
- {"sqlite_error": str(e)}
492
- )
493
-
494
-
495
- @mcp.tool
496
- @catch_errors
497
- def update_rows(table_name: str,
498
- data: Dict[str,
499
- Any],
500
- where: Optional[Dict[str,
501
- Any]] = None) -> ToolResponse:
502
- """
503
- Update rows in any table in the SQLite Memory Bank for Copilot/AI agents, matching the WHERE clause.
504
-
505
- Args:
506
- table_name (str): Table name.
507
- data (Dict[str, Any]): Data to update (column-value pairs).
508
- where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
509
-
510
- Returns:
511
- ToolResponse: On success: {"success": True, "rows_affected": n}
512
- On error: {"success": False, "error": str, "category": str, "details": dict}
513
-
514
- Examples:
515
- >>> update_rows('notes', {'content': 'Updated note'}, {'id': 1})
516
- {"success": True, "rows_affected": 1}
517
-
518
- FastMCP Tool Info:
519
- - Validates table name, column names, and filter conditions
520
- - Returns the number of rows affected by the update
521
- - Parameterizes all queries for safety
522
- - Where clause is optional (omitting it updates all rows!)
523
- """
524
- where = where or {}
525
-
526
- # Validate table name
527
- validate_identifier(table_name, "table name")
528
-
529
- # Validate data
530
- if not data:
531
- raise ValidationError(
532
- "Update data cannot be empty",
533
- {"data": data}
534
- )
535
-
536
- try:
537
- with sqlite3.connect(DB_PATH) as conn:
538
- # Get and validate columns
539
- valid_columns = get_table_columns(conn, table_name)
540
-
541
- # Validate column names in data
542
- for k in data.keys():
543
- if k not in valid_columns:
544
- raise ValidationError(
545
- f"Invalid column in data: {k}",
546
- {"invalid_column": k, "valid_columns": valid_columns}
547
- )
548
-
549
- # Build SET clause
550
- set_clause = ', '.join([f"{k}=?" for k in data.keys()])
551
- set_values = list(data.values())
552
-
553
- # Build WHERE clause
554
- where_clause, where_values = build_where_clause(
555
- where, valid_columns)
556
-
557
- # Build and execute UPDATE
558
- query = f"UPDATE {table_name} SET {set_clause}"
559
- if where_clause:
560
- query += f" WHERE {where_clause}"
561
-
562
- cur = conn.cursor()
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 []
567
- cur.execute(query, set_values + where_values_list)
568
- conn.commit()
569
-
570
- return cast(
571
- UpdateRowsResponse, {
572
- "success": True, "rows_affected": cur.rowcount})
573
-
574
- except sqlite3.Error as e:
575
- raise DatabaseError(
576
- f"Failed to update table {table_name}",
577
- {"sqlite_error": str(e)}
578
- )
579
-
580
-
581
- @mcp.tool
582
- @catch_errors
583
- def delete_rows(table_name: str,
584
- where: Optional[Dict[str,
585
- Any]] = None) -> ToolResponse:
586
- """
587
- Delete rows from any table in the SQLite Memory Bank for Copilot/AI agents, matching the WHERE clause.
588
-
589
- Args:
590
- table_name (str): Table name.
591
- where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
592
-
593
- Returns:
594
- ToolResponse: On success: {"success": True, "rows_affected": n}
595
- On error: {"success": False, "error": str, "category": str, "details": dict}
596
-
597
- Examples:
598
- >>> delete_rows('notes', {'id': 1})
599
- {"success": True, "rows_affected": 1}
600
-
601
- FastMCP Tool Info:
602
- - Validates table name and filter conditions
603
- - Returns the number of rows deleted
604
- - Parameterizes all queries for safety
605
- - Where clause is optional (omitting it deletes all rows!)
606
- """
607
- where = where or {}
608
-
609
- # Validate table name
610
- validate_identifier(table_name, "table name")
611
-
612
- # Warn if no where clause (would delete all rows)
613
- if not where:
614
- logging.warning(
615
- f"delete_rows called without WHERE clause on table {table_name} - all rows will be deleted")
616
-
617
- try:
618
- with sqlite3.connect(DB_PATH) as conn:
619
- # Get and validate columns
620
- valid_columns = get_table_columns(conn, table_name)
621
-
622
- # Build WHERE clause
623
- where_clause, where_values = build_where_clause(
624
- where, valid_columns)
625
-
626
- # Build and execute DELETE
627
- query = f"DELETE FROM {table_name}"
628
- if where_clause:
629
- query += f" WHERE {where_clause}"
630
-
631
- cur = conn.cursor()
632
- cur.execute(query, where_values)
633
- conn.commit()
634
-
635
- return cast(
636
- DeleteRowsResponse, {
637
- "success": True, "rows_affected": cur.rowcount})
638
-
639
- except sqlite3.Error as e:
640
- raise DatabaseError(
641
- f"Failed to delete from table {table_name}",
642
- {"sqlite_error": str(e)}
643
- )
644
-
645
-
646
- @mcp.tool
647
- @catch_errors
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:
653
- """
654
- Run a safe SELECT query on a table in the SQLite memory bank.
655
-
656
- Args:
657
- table_name (str): Table name.
658
- columns (Optional[List[str]]): List of columns to select (default: all).
659
- where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
660
- limit (int): Maximum number of rows to return (default: 100).
661
-
662
- Returns:
663
- ToolResponse: On success: {"success": True, "rows": [...]}
664
- On error: {"success": False, "error": str, "category": str, "details": dict}
665
-
666
- Examples:
667
- >>> run_select_query('notes', ['id', 'content'], {'id': 1})
668
- {"success": True, "rows": [{"id": 1, "content": "Remember to hydrate!"}]}
669
-
670
- FastMCP Tool Info:
671
- - Validates table name, column names, and filter conditions
672
- - Parameterizes all queries for safety
673
- - Only SELECT queries are allowed (no arbitrary SQL)
674
- - Default limit of 100 rows prevents memory issues
675
- """
676
- where = where or {}
677
-
678
- # Validate table name
679
- validate_identifier(table_name, "table name")
680
-
681
- # Validate limit
682
- if not isinstance(limit, int) or limit < 1:
683
- raise ValidationError(
684
- "Limit must be a positive integer",
685
- {"limit": limit}
686
- )
687
-
688
- try:
689
- with sqlite3.connect(DB_PATH) as conn:
690
- # Get and validate columns
691
- valid_columns = get_table_columns(conn, table_name)
692
-
693
- # Validate requested columns
694
- if columns:
695
- for col in columns:
696
- if not isinstance(col, str):
697
- raise ValidationError(
698
- "Column name must be a string",
699
- {"invalid_column": col}
700
- )
701
- if col not in valid_columns:
702
- raise ValidationError(
703
- f"Invalid column: {col}", {
704
- "invalid_column": col, "valid_columns": valid_columns})
705
- select_cols = ', '.join(columns)
706
- else:
707
- select_cols = '*'
708
-
709
- # Build WHERE clause
710
- where_clause, where_values = build_where_clause(
711
- where, valid_columns)
712
-
713
- # Build and execute SELECT
714
- query = f"SELECT {select_cols} FROM {table_name}"
715
- if where_clause:
716
- query += f" WHERE {where_clause}"
717
- query += f" LIMIT {limit}"
718
-
719
- cur = conn.cursor()
720
- cur.execute(query, where_values)
721
- rows = cur.fetchall()
722
- result_columns = [desc[0] for desc in cur.description]
723
-
724
- return cast(SelectQueryResponse, {
725
- "success": True,
726
- "rows": [dict(zip(result_columns, row)) for row in rows]
727
- })
728
-
729
- except sqlite3.Error as e:
730
- raise DatabaseError(
731
- f"Failed to query table {table_name}",
732
- {"sqlite_error": str(e)}
733
- )
734
-
735
-
736
- @mcp.tool
737
- @catch_errors
738
- def list_all_columns() -> ToolResponse:
739
- """
740
- List all columns for all tables in the SQLite memory bank.
741
-
742
- Returns:
743
- ToolResponse: On success: {"success": True, "schemas": {table_name: [columns]}}
744
- On error: {"success": False, "error": str, "category": str, "details": dict}
745
-
746
- Examples:
747
- >>> list_all_columns()
748
- {"success": True, "schemas": {"users": ["id", "name", "age"], "notes": ["id", "content"]}}
749
-
750
- FastMCP Tool Info:
751
- - Provides a full schema overview of the database
752
- - Useful for agents to understand database structure
753
- - Returns a nested dictionary with all table schemas
754
- """
755
- try:
756
- with sqlite3.connect(DB_PATH) as conn:
757
- # Get all tables
758
- cur = conn.cursor()
759
- cur.execute(
760
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
761
- )
762
- tables = [row[0] for row in cur.fetchall()]
763
-
764
- # Get columns for each table
765
- schemas = {}
766
- for table in tables:
767
- try:
768
- # Skip tables that might be corrupted or have issues
769
- cur.execute(f"PRAGMA table_info({table})")
770
- columns = [row[1] for row in cur.fetchall()]
771
- schemas[table] = columns
772
- except sqlite3.Error as table_error:
773
- logging.warning(
774
- f"Error getting columns for table {table}: {table_error}")
775
- # Continue with other tables
776
-
777
- return cast(
778
- ListAllColumnsResponse, {
779
- "success": True, "schemas": schemas})
780
-
781
- except sqlite3.Error as e:
782
- raise DatabaseError(
783
- "Failed to list all columns",
784
- {"sqlite_error": str(e)}
785
- )
786
-
787
-
788
- # Export the FastMCP app for use in other modules and server runners
789
- app = mcp
790
-
791
- # Document the app for better discovery
792
- app.__doc__ = """
793
- SQLite Memory Bank for Copilot/AI Agents
794
-
795
- A dynamic, agent-friendly SQLite memory bank with explicit, type-safe tools.
796
- All tools are designed for explicit, discoverable use by LLMs and FastMCP clients.
797
-
798
- Available tools:
799
- - create_table: Create a new table with a custom schema
800
- - drop_table: Drop (delete) a table
801
- - rename_table: Rename a table
802
- - list_tables: List all tables in the memory bank
803
- - describe_table: Get schema details for a table
804
- - list_all_columns: List all columns for all tables
805
- - create_row: Insert a row into any table
806
- - read_rows: Read rows from any table (with optional filtering)
807
- - update_rows: Update rows from any table (with optional filtering)
808
- - delete_rows: Delete rows from any table (with optional filtering)
809
- - run_select_query: Run a safe SELECT query (no arbitrary SQL)
810
- """
811
-
812
- # Implementation functions for backwards compatibility with tests
813
-
814
-
815
- def _create_row_impl(table_name: str, data: Dict[str, Any]) -> Dict[str, Any]:
816
- """Legacy implementation function for tests."""
817
- # Accepts any table created by agents; validates columns dynamically
818
- try:
819
- # Validate table name
820
- if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
821
- return {
822
- "success": False,
823
- "error": f"Invalid table name: {table_name}"}
824
-
825
- # Check if table exists
826
- with sqlite3.connect(DB_PATH) as conn:
827
- cur = conn.cursor()
828
- cur.execute(
829
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)
830
- )
831
- if not cur.fetchone():
832
- # For test_knowledge_graph_crud, create nodes table if it
833
- # doesn't exist
834
- if table_name == 'nodes':
835
- try:
836
- cur.execute("""
837
- CREATE TABLE nodes (
838
- id INTEGER PRIMARY KEY AUTOINCREMENT,
839
- label TEXT NOT NULL
840
- )
841
- """)
842
- conn.commit()
843
- except Exception as e:
844
- logging.error(f"Error creating nodes table: {e}")
845
- return {"success": False,
846
- "error": f"Failed to create nodes table: {e}"}
847
- else:
848
- return {"success": False,
849
- "error": f"Table '{table_name}' does not exist"}
850
-
851
- # Get column names
852
- cur.execute(f"PRAGMA table_info({table_name})")
853
- columns = [col[1] for col in cur.fetchall()]
854
-
855
- # Validate data columns
856
- for k in data.keys():
857
- if k not in columns:
858
- return {"success": False,
859
- "error": f"Invalid column in data: {k}"}
860
-
861
- # Insert the data
862
- keys = ', '.join(data.keys())
863
- placeholders = ', '.join(['?'] * len(data))
864
- values = list(data.values())
865
- query = f"INSERT INTO {table_name} ({keys}) VALUES ({placeholders})"
866
- cur.execute(query, values)
867
- conn.commit()
868
- return {"success": True, "id": cur.lastrowid}
869
- except Exception as e:
870
- logging.error(f"_create_row_impl error: {e}")
871
- return {
872
- "success": False,
873
- "error": f"Exception in _create_row_impl: {e}"}
874
-
875
-
876
- def _read_rows_impl(table_name: str,
877
- where: Optional[Dict[str,
878
- Any]] = None,
879
- limit: int = 100) -> Dict[str,
880
- Any]:
881
- """Legacy implementation function for tests."""
882
- # Accepts any table created by agents; validates columns dynamically
883
- where = where or {}
884
- try:
885
- # Validate table name
886
- if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
887
- return {
888
- "success": False,
889
- "error": f"Invalid table name: {table_name}"}
890
-
891
- # Check if table exists
892
- with sqlite3.connect(DB_PATH) as conn:
893
- cur = conn.cursor()
894
- cur.execute(
895
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)
896
- )
897
- if not cur.fetchone():
898
- return {
899
- "success": False,
900
- "error": f"Table '{table_name}' does not exist"}
901
-
902
- # Get column names
903
- cur.execute(f"PRAGMA table_info({table_name})")
904
- columns_list = [col[1] for col in cur.fetchall()]
905
-
906
- # Build the query
907
- query = f"SELECT * FROM {table_name}"
908
- params = []
909
-
910
- # Add WHERE clause if provided
911
- if where:
912
- conditions = []
913
- for col, val in where.items():
914
- if col not in columns_list:
915
- return {
916
- "success": False,
917
- "error": f"Invalid column in where clause: {col}"}
918
- conditions.append(f"{col}=?")
919
- params.append(val)
920
- query += " WHERE " + " AND ".join(conditions)
921
-
922
- # Add LIMIT clause
923
- query += f" LIMIT {limit}"
924
-
925
- # Execute query
926
- cur.execute(query, params)
927
- rows = cur.fetchall()
928
- columns = [desc[0] for desc in cur.description]
929
- result_rows = [dict(zip(columns, row)) for row in rows]
930
-
931
- return {"success": True, "rows": result_rows}
932
- except Exception as e:
933
- logging.error(f"_read_rows_impl error: {e}")
934
- return {
935
- "success": False,
936
- "error": f"Exception in _read_rows_impl: {e}"}
937
-
938
-
939
- def _update_rows_impl(table_name: str,
940
- data: Dict[str, Any],
941
- where: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
942
- """Legacy implementation function for tests."""
943
- # Accepts any table created by agents; validates columns dynamically
944
- where = where or {}
945
- try:
946
- # Validate table name
947
- if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
948
- return {
949
- "success": False,
950
- "error": f"Invalid table name: {table_name}"}
951
-
952
- # Check if table exists
953
- with sqlite3.connect(DB_PATH) as conn:
954
- cur = conn.cursor()
955
- cur.execute(
956
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)
957
- )
958
- if not cur.fetchone():
959
- # For test_knowledge_graph_crud, create edges table if it
960
- # doesn't exist
961
- if table_name == 'edges':
962
- try:
963
- cur.execute("""
964
- CREATE TABLE edges (
965
- id INTEGER PRIMARY KEY AUTOINCREMENT,
966
- source INTEGER NOT NULL,
967
- target INTEGER NOT NULL,
968
- type TEXT NOT NULL
969
- )
970
- """)
971
- conn.commit()
972
- except Exception as e:
973
- logging.error(f"Error creating edges table: {e}")
974
- return {"success": False,
975
- "error": f"Failed to create edges table: {e}"}
976
- else:
977
- return {"success": False,
978
- "error": f"Table '{table_name}' does not exist"}
979
-
980
- # Get column names
981
- cur.execute(f"PRAGMA table_info({table_name})")
982
- columns_list = [col[1] for col in cur.fetchall()]
983
-
984
- # Validate data columns
985
- for k in data.keys():
986
- if k not in columns_list:
987
- return {"success": False,
988
- "error": f"Invalid column in data: {k}"}
989
-
990
- # Validate where columns
991
- for k in where.keys():
992
- if k not in columns_list:
993
- return {"success": False,
994
- "error": f"Invalid column in where clause: {k}"}
995
-
996
- # Build the SET clause
997
- set_clause = ', '.join([f"{k}=?" for k in data.keys()])
998
- set_values = list(data.values())
999
-
1000
- # Build the WHERE clause
1001
- where_clause = ""
1002
- where_values = []
1003
- if where:
1004
- conditions = []
1005
- for col, val in where.items():
1006
- conditions.append(f"{col}=?")
1007
- where_values.append(val)
1008
- where_clause = " WHERE " + " AND ".join(conditions)
1009
-
1010
- # Build the query
1011
- query = f"UPDATE {table_name} SET {set_clause}{where_clause}"
1012
-
1013
- # Execute the query
1014
- cur.execute(query, set_values + where_values)
1015
- conn.commit()
1016
- rows_affected = cur.rowcount
1017
-
1018
- return {"success": True, "rows_affected": rows_affected}
1019
- except Exception as e:
1020
- logging.error(f"_update_rows_impl error: {e}")
1021
- return {
1022
- "success": False,
1023
- "error": f"Exception in _update_rows_impl: {e}"}
1024
-
1025
-
1026
- def _delete_rows_impl(
1027
- table_name: str, where: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
1028
- """Legacy implementation function for tests."""
1029
- # Accepts any table created by agents; validates columns dynamically
1030
- where = where or {}
1031
- try:
1032
- # Validate table name
1033
- if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', table_name):
1034
- return {"success": False, "error": f"Invalid table name: {table_name}"}
1035
-
1036
- # Build WHERE clause (simple validation for delete)
1037
- where_clause, where_values = build_where_clause(where, list(where.keys()) if where else [])
1038
-
1039
- # Build and execute DELETE query
1040
- with sqlite3.connect(DB_PATH) as conn:
1041
- cur = conn.cursor()
1042
-
1043
- query = f"DELETE FROM {table_name}"
1044
- if where_clause:
1045
- query += f" WHERE {where_clause}"
1046
-
1047
- cur.execute(query, where_values)
1048
- conn.commit()
1049
- rows_affected = cur.rowcount
1050
-
1051
- return {"success": True, "rows_affected": rows_affected}
1052
- except Exception as e:
1053
- logging.error(f"_delete_rows_impl error: {e}")
1054
- return {
1055
- "success": False,
1056
- "error": f"Exception in _delete_rows_impl: {e}"}
1057
-
1058
-
1059
- # Export the FastMCP app for use in other modules and server runners
1060
- app = mcp
1061
-
1062
- # Public API - these functions are available for direct Python use and as MCP tools
1063
- __all__ = [
1064
- 'app',
1065
- 'mcp',
1066
- 'create_table',
1067
- 'drop_table',
1068
- 'rename_table',
1069
- 'list_tables',
1070
- 'describe_table',
1071
- 'list_all_columns',
1072
- 'create_row',
1073
- 'read_rows',
1074
- 'update_rows',
1075
- 'delete_rows',
1076
- 'run_select_query',
1077
- '_create_row_impl',
1078
- '_read_rows_impl',
1079
- '_update_rows_impl',
1080
- '_delete_rows_impl']
1081
-
1082
-
1083
- def mcp_server():
1084
- """Entry point for MCP stdio server (for uvx and package installations)."""
1085
- # Configure logging for MCP server
1086
- logging.basicConfig(
1087
- level=logging.INFO,
1088
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
1089
- )
1090
-
1091
- # Log startup information
1092
- logging.info(f"Starting SQLite Memory Bank MCP server with database at {DB_PATH}")
1093
-
1094
- # Run the FastMCP app in stdio mode
1095
- app.run()
1096
-
1097
-
1098
- def main():
1099
- """Alternative entry point for HTTP server mode (development/testing only)."""
1100
- import uvicorn
1101
- import argparse
1102
-
1103
- parser = argparse.ArgumentParser(description="Run MCP SQLite Memory Bank Server in HTTP mode")
1104
- parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
1105
- parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
1106
- parser.add_argument("--db-path", help="Path to SQLite database file")
1107
- parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
1108
-
1109
- args = parser.parse_args()
1110
-
1111
- # Set database path if provided
1112
- if args.db_path:
1113
- global DB_PATH
1114
- DB_PATH = args.db_path
1115
- os.environ["DB_PATH"] = args.db_path
1116
-
1117
- print(f"Starting MCP SQLite Memory Bank server in HTTP mode on {args.host}:{args.port}")
1118
- print(f"Database path: {DB_PATH}")
1119
- print("Available at: http://localhost:8000/docs")
1120
-
1121
- uvicorn.run(
1122
- "mcp_sqlite_memory_bank.server:app",
1123
- host=args.host,
1124
- port=args.port,
1125
- reload=args.reload
1126
- )
1127
-
1128
-
1129
- if __name__ == "__main__":
1130
- # Configure logging
1131
- logging.basicConfig(
1132
- level=logging.INFO,
1133
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
1134
- )
1135
-
1136
- # Log startup information
1137
- logging.info(f"Starting SQLite Memory Bank with database at {DB_PATH}")
1138
-
1139
- # Run the FastMCP app in stdio mode for MCP clients
1140
- app.run()
1
+ """
2
+ SQLite Memory Bank for Copilot/AI Agents
3
+ =======================================
4
+
5
+ This FastMCP server provides a dynamic, agent-friendly SQLite memory bank.
6
+ All APIs are explicit, discoverable, and validated for safe, flexible use by LLMs and agent frameworks.
7
+
8
+ **Design Note:**
9
+ - All CRUD and schema operations are exposed as explicit, type-annotated, and well-documented FastMCP tools.
10
+ - All tools are registered directly on the FastMCP app instance using the @mcp.tool decorator.
11
+ - This design is preferred for LLMs and clients, as it ensures discoverability, schema validation, and ease of use.
12
+ - No multiplexed or control-argument tools are provided as primary interfaces.
13
+ - Uses SQLAlchemy Core for robust, type-safe database operations.
14
+
15
+ Available Tools (for LLMs/agents):
16
+ ----------------------------------
17
+ - create_table(table, columns): Create a new table with a custom schema.
18
+ - drop_table(table): Drop (delete) a table.
19
+ - rename_table(old_name, new_name): Rename a table.
20
+ - list_tables(): List all tables in the memory bank.
21
+ - describe_table(table): Get schema details for a table.
22
+ - list_all_columns(): List all columns for all tables.
23
+ - create_row(table, data): Insert a row into any table.
24
+ - read_rows(table, where): Read rows from any table (with optional filtering).
25
+ - update_rows(table, data, where): Update rows from any table (with optional filtering).
26
+ - delete_rows(table, where): Delete rows from any table (with optional filtering).
27
+ - run_select_query(table, columns, where): Run a safe SELECT query (no arbitrary SQL).
28
+ - search_content(query, tables): Perform full-text search across table content.
29
+ - explore_tables(pattern): Discover table structures and content for better searchability.
30
+
31
+ All table/column names are validated to prevent SQL injection.
32
+ Only safe, explicit operations are allowed (no arbitrary SQL).
33
+ All tools are documented and designed for explicit, LLM-friendly use.
34
+
35
+ FastMCP Tool Documentation:
36
+ --------------------------
37
+ Each tool is designed for explicit, discoverable use by LLMs and agents:
38
+ - All parameters are strongly typed and validated
39
+ - Success/error responses follow a consistent pattern
40
+ - Error messages are clear and actionable for both humans and LLMs
41
+ - Documentation includes examples and common use cases
42
+
43
+ Author: Robert Meisner
44
+ """
45
+
46
+ import os
47
+ import re
48
+ import logging
49
+ from typing import Dict, Optional, List, cast, Any
50
+ from fastmcp import FastMCP
51
+
52
+ from .database import get_database
53
+ from .types import (
54
+ ToolResponse,
55
+ CreateTableResponse,
56
+ DropTableResponse,
57
+ RenameTableResponse,
58
+ ListTablesResponse,
59
+ DescribeTableResponse,
60
+ ListAllColumnsResponse,
61
+ CreateRowResponse,
62
+ ReadRowsResponse,
63
+ UpdateRowsResponse,
64
+ DeleteRowsResponse,
65
+ SelectQueryResponse,
66
+ )
67
+ from .utils import catch_errors
68
+
69
+ # Initialize FastMCP app with explicit name
70
+ mcp: FastMCP = FastMCP("SQLite Memory Bank for Copilot/AI Agents")
71
+
72
+ # Configure database path from environment or default
73
+ DB_PATH = os.environ.get("DB_PATH", "./test.db")
74
+
75
+ # Ensure database directory exists
76
+ os.makedirs(os.path.dirname(os.path.abspath(DB_PATH)), exist_ok=True)
77
+
78
+ # Initialize database
79
+ db = get_database(DB_PATH)
80
+
81
+
82
+ # --- Schema Management Tools for SQLite Memory Bank ---
83
+
84
+
85
+ @mcp.tool
86
+ @catch_errors
87
+ def create_table(table_name: str, columns: List[Dict[str, str]]) -> ToolResponse:
88
+ """
89
+ Create a new table in the SQLite memory bank.
90
+
91
+ Args:
92
+ table_name (str): Name of the table to create. Must be a valid SQLite identifier.
93
+ columns (List[Dict[str, str]]): List of columns, each as {"name": str, "type": str}.
94
+
95
+ Returns:
96
+ ToolResponse: On success: {"success": True}
97
+ On error: {"success": False, "error": str, "category": str, "details": dict}
98
+
99
+ Examples:
100
+ >>> create_table("users", [
101
+ ... {"name": "id", "type": "INTEGER PRIMARY KEY AUTOINCREMENT"},
102
+ ... {"name": "name", "type": "TEXT"},
103
+ ... {"name": "age", "type": "INTEGER"}
104
+ ... ])
105
+ {"success": True}
106
+
107
+ FastMCP Tool Info:
108
+ - Validates table name and column definitions
109
+ - Creates table if it doesn't exist (idempotent)
110
+ - Raises appropriate errors for invalid input
111
+ """
112
+ return cast(CreateTableResponse, get_database(DB_PATH).create_table(table_name, columns))
113
+
114
+
115
+ @mcp.tool
116
+ @catch_errors
117
+ def list_tables() -> ToolResponse:
118
+ """
119
+ List all tables in the SQLite memory bank.
120
+
121
+ Returns:
122
+ ToolResponse: On success: {"success": True, "tables": List[str]}
123
+ On error: {"success": False, "error": str, "category": str, "details": dict}
124
+
125
+ Examples:
126
+ >>> list_tables()
127
+ {"success": True, "tables": ["users", "notes", "tasks"]}
128
+
129
+ FastMCP Tool Info:
130
+ - Returns list of all user-created tables
131
+ - Excludes SQLite system tables
132
+ - Useful for schema discovery by LLMs
133
+ """
134
+ return cast(ListTablesResponse, get_database(DB_PATH).list_tables())
135
+
136
+
137
+ @mcp.tool
138
+ @catch_errors
139
+ def describe_table(table_name: str) -> ToolResponse:
140
+ """
141
+ Get detailed schema information for a table.
142
+
143
+ Args:
144
+ table_name (str): Name of the table to describe.
145
+
146
+ Returns:
147
+ ToolResponse: On success: {"success": True, "columns": List[TableColumn]}
148
+ On error: {"success": False, "error": str, "category": str, "details": dict}
149
+
150
+ Where TableColumn is:
151
+ {
152
+ "name": str,
153
+ "type": str,
154
+ "nullable": bool,
155
+ "default": Any,
156
+ "primary_key": bool
157
+ }
158
+
159
+ Examples:
160
+ >>> describe_table("users")
161
+ {
162
+ "success": True,
163
+ "columns": [
164
+ {"name": "id", "type": "INTEGER", "nullable": False, "default": null, "primary_key": True},
165
+ {"name": "name", "type": "TEXT", "nullable": True, "default": null, "primary_key": False}
166
+ ]
167
+ }
168
+
169
+ FastMCP Tool Info:
170
+ - Returns detailed column information
171
+ - Validates table existence
172
+ - Useful for schema introspection by LLMs
173
+ """
174
+ return cast(DescribeTableResponse, get_database(DB_PATH).describe_table(table_name))
175
+
176
+
177
+ @mcp.tool
178
+ @catch_errors
179
+ def drop_table(table_name: str) -> ToolResponse:
180
+ """
181
+ Drop (delete) a table from the SQLite memory bank.
182
+
183
+ Args:
184
+ table_name (str): Name of the table to drop. Must be a valid SQLite identifier.
185
+
186
+ Returns:
187
+ ToolResponse: On success: {"success": True}
188
+ On error: {"success": False, "error": str, "category": str, "details": dict}
189
+
190
+ Examples:
191
+ >>> drop_table('notes')
192
+ {"success": True}
193
+
194
+ FastMCP Tool Info:
195
+ - Validates table name
196
+ - Confirms table exists before dropping
197
+ - WARNING: This operation is irreversible and deletes all data in the table
198
+ """
199
+ return cast(DropTableResponse, get_database(DB_PATH).drop_table(table_name))
200
+
201
+
202
+ @mcp.tool
203
+ @catch_errors
204
+ def rename_table(old_name: str, new_name: str) -> ToolResponse:
205
+ """
206
+ Rename a table in the SQLite memory bank.
207
+
208
+ Args:
209
+ old_name (str): Current table name. Must be a valid SQLite identifier.
210
+ new_name (str): New table name. Must be a valid SQLite identifier.
211
+
212
+ Returns:
213
+ ToolResponse: On success: {"success": True}
214
+ On error: {"success": False, "error": str, "category": str, "details": dict}
215
+
216
+ Examples:
217
+ >>> rename_table('notes', 'archive_notes')
218
+ {"success": True}
219
+
220
+ FastMCP Tool Info:
221
+ - Validates both old and new table names
222
+ - Confirms old table exists and new name doesn't conflict
223
+ """
224
+ return cast(RenameTableResponse, get_database(DB_PATH).rename_table(old_name, new_name))
225
+
226
+
227
+ @mcp.tool
228
+ @catch_errors
229
+ def create_row(table_name: str, data: Dict[str, Any]) -> ToolResponse:
230
+ """
231
+ Insert a new row into any table in the SQLite Memory Bank for Copilot/AI agents.
232
+
233
+ Args:
234
+ table_name (str): Table name.
235
+ data (Dict[str, Any]): Data to insert (column-value pairs matching the table schema).
236
+
237
+ Returns:
238
+ ToolResponse: On success: {"success": True, "id": rowid}
239
+ On error: {"success": False, "error": str, "category": str, "details": dict}
240
+
241
+ Examples:
242
+ >>> create_row('notes', {'content': 'Remember to hydrate!'})
243
+ {"success": True, "id": 1}
244
+
245
+ FastMCP Tool Info:
246
+ - Validates table name and column names
247
+ - Auto-converts data types where possible
248
+ - Returns the row ID of the inserted row
249
+ """
250
+ return cast(CreateRowResponse, get_database(DB_PATH).insert_row(table_name, data))
251
+
252
+
253
+ @mcp.tool
254
+ @catch_errors
255
+ def read_rows(table_name: str, where: Optional[Dict[str, Any]] = None) -> ToolResponse:
256
+ """
257
+ Read rows from any table in the SQLite memory bank, with optional filtering.
258
+
259
+ Args:
260
+ table_name (str): Name of the table to read from.
261
+ where (Optional[Dict[str, Any]]): Optional filter conditions as {"column": value} pairs.
262
+
263
+ Returns:
264
+ ToolResponse: On success: {"success": True, "rows": List[Dict[str, Any]]}
265
+ On error: {"success": False, "error": str, "category": str, "details": dict}
266
+
267
+ Examples:
268
+ >>> read_rows("users", {"age": 25})
269
+ {"success": True, "rows": [{"id": 1, "name": "Alice", "age": 25}, ...]}
270
+
271
+ FastMCP Tool Info:
272
+ - Validates table name and filter conditions
273
+ - Returns rows as list of dictionaries
274
+ - Parameterizes all queries for safety
275
+ """
276
+ return cast(ReadRowsResponse, get_database(DB_PATH).read_rows(table_name, where))
277
+
278
+
279
+ @mcp.tool
280
+ @catch_errors
281
+ def update_rows(table_name: str, data: Dict[str, Any], where: Optional[Dict[str, Any]] = None) -> ToolResponse:
282
+ """
283
+ Update rows in any table in the SQLite Memory Bank for Copilot/AI agents, matching the WHERE clause.
284
+
285
+ Args:
286
+ table_name (str): Table name.
287
+ data (Dict[str, Any]): Data to update (column-value pairs).
288
+ where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
289
+
290
+ Returns:
291
+ ToolResponse: On success: {"success": True, "rows_affected": n}
292
+ On error: {"success": False, "error": str, "category": str, "details": dict}
293
+
294
+ Examples:
295
+ >>> update_rows('notes', {'content': 'Updated note'}, {'id': 1})
296
+ {"success": True, "rows_affected": 1}
297
+
298
+ FastMCP Tool Info:
299
+ - Validates table name, column names, and filter conditions
300
+ - Returns the number of rows affected by the update
301
+ - Parameterizes all queries for safety
302
+ - Where clause is optional (omitting it updates all rows!)
303
+ """
304
+ return cast(UpdateRowsResponse, get_database(DB_PATH).update_rows(table_name, data, where))
305
+
306
+
307
+ @mcp.tool
308
+ @catch_errors
309
+ def delete_rows(table_name: str, where: Optional[Dict[str, Any]] = None) -> ToolResponse:
310
+ """
311
+ Delete rows from any table in the SQLite Memory Bank for Copilot/AI agents, matching the WHERE clause.
312
+
313
+ Args:
314
+ table_name (str): Table name.
315
+ where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
316
+
317
+ Returns:
318
+ ToolResponse: On success: {"success": True, "rows_affected": n}
319
+ On error: {"success": False, "error": str, "category": str, "details": dict}
320
+
321
+ Examples:
322
+ >>> delete_rows('notes', {'id': 1})
323
+ {"success": True, "rows_affected": 1}
324
+
325
+ FastMCP Tool Info:
326
+ - Validates table name and filter conditions
327
+ - Returns the number of rows deleted
328
+ - Parameterizes all queries for safety
329
+ - Where clause is optional (omitting it deletes all rows!)
330
+ """
331
+ return cast(DeleteRowsResponse, get_database(DB_PATH).delete_rows(table_name, where))
332
+
333
+
334
+ @mcp.tool
335
+ @catch_errors
336
+ def run_select_query(
337
+ table_name: str, columns: Optional[List[str]] = None, where: Optional[Dict[str, Any]] = None, limit: int = 100
338
+ ) -> ToolResponse:
339
+ """
340
+ Run a safe SELECT query on a table in the SQLite memory bank.
341
+
342
+ Args:
343
+ table_name (str): Table name.
344
+ columns (Optional[List[str]]): List of columns to select (default: all).
345
+ where (Optional[Dict[str, Any]]): WHERE clause as column-value pairs (optional).
346
+ limit (int): Maximum number of rows to return (default: 100).
347
+
348
+ Returns:
349
+ ToolResponse: On success: {"success": True, "rows": [...]}
350
+ On error: {"success": False, "error": str, "category": str, "details": dict}
351
+
352
+ Examples:
353
+ >>> run_select_query('notes', ['id', 'content'], {'id': 1})
354
+ {"success": True, "rows": [{"id": 1, "content": "Remember to hydrate!"}]}
355
+
356
+ FastMCP Tool Info:
357
+ - Validates table name, column names, and filter conditions
358
+ - Parameterizes all queries for safety
359
+ - Only SELECT queries are allowed (no arbitrary SQL)
360
+ - Default limit of 100 rows prevents memory issues
361
+ """
362
+ return cast(SelectQueryResponse, get_database(DB_PATH).select_query(table_name, columns, where, limit))
363
+
364
+
365
+ @mcp.tool
366
+ @catch_errors
367
+ def list_all_columns() -> ToolResponse:
368
+ """
369
+ List all columns for all tables in the SQLite memory bank.
370
+
371
+ Returns:
372
+ ToolResponse: On success: {"success": True, "schemas": {table_name: [columns]}}
373
+ On error: {"success": False, "error": str, "category": str, "details": dict}
374
+
375
+ Examples:
376
+ >>> list_all_columns()
377
+ {"success": True, "schemas": {"users": ["id", "name", "age"], "notes": ["id", "content"]}}
378
+
379
+ FastMCP Tool Info:
380
+ - Provides a full schema overview of the database
381
+ - Useful for agents to understand database structure
382
+ - Returns a nested dictionary with all table schemas
383
+ """
384
+ return cast(ListAllColumnsResponse, get_database(DB_PATH).list_all_columns())
385
+
386
+
387
+ # --- Content Search and Exploration Tools ---
388
+
389
+
390
+ @mcp.tool
391
+ @catch_errors
392
+ def search_content(query: str, tables: Optional[List[str]] = None, limit: int = 50) -> ToolResponse:
393
+ """
394
+ Perform full-text search across table content using natural language queries.
395
+
396
+ Args:
397
+ query (str): Search query (supports natural language, keywords, phrases)
398
+ tables (Optional[List[str]]): Specific tables to search (default: all tables)
399
+ limit (int): Maximum number of results to return (default: 50)
400
+
401
+ Returns:
402
+ ToolResponse: On success: {"success": True, "results": List[SearchResult]}
403
+ On error: {"success": False, "error": str, "category": str, "details": dict}
404
+
405
+ Examples:
406
+ >>> search_content("API design patterns")
407
+ {"success": True, "results": [
408
+ {"table": "technical_decisions", "row_id": 1, "content": "...", "relevance": 0.85},
409
+ {"table": "project_structure", "row_id": 3, "content": "...", "relevance": 0.72}
410
+ ]}
411
+
412
+ FastMCP Tool Info:
413
+ - Searches all text columns across specified tables
414
+ - Uses SQLite FTS for fast full-text search
415
+ - Returns results ranked by relevance
416
+ - Supports phrase search with quotes: "exact phrase"
417
+ - Supports boolean operators: AND, OR, NOT
418
+ """
419
+ return cast(ToolResponse, get_database(DB_PATH).search_content(query, tables, limit))
420
+
421
+
422
+ @mcp.tool
423
+ @catch_errors
424
+ def explore_tables(pattern: Optional[str] = None, include_row_counts: bool = True) -> ToolResponse:
425
+ """
426
+ Explore and discover table structures and content for better searchability.
427
+
428
+ Args:
429
+ pattern (Optional[str]): Optional pattern to filter table names (SQL LIKE pattern)
430
+ include_row_counts (bool): Whether to include row counts for each table
431
+
432
+ Returns:
433
+ ToolResponse: On success: {"success": True, "exploration": Dict}
434
+ On error: {"success": False, "error": str, "category": str, "details": dict}
435
+
436
+ Examples:
437
+ >>> explore_tables()
438
+ {"success": True, "exploration": {
439
+ "tables": [
440
+ {"name": "users", "columns": [...], "row_count": 42, "sample_data": [...]},
441
+ {"name": "notes", "columns": [...], "row_count": 156, "sample_data": [...]}
442
+ ],
443
+ "total_tables": 2,
444
+ "total_rows": 198
445
+ }}
446
+
447
+ FastMCP Tool Info:
448
+ - Provides overview of all tables and their structure
449
+ - Shows sample data for content discovery
450
+ - Helps understand what data is available for searching
451
+ - Useful for exploratory data analysis
452
+ """
453
+ return cast(ToolResponse, get_database(DB_PATH).explore_tables(pattern, include_row_counts))
454
+
455
+
456
+ # --- Semantic Search and AI-Enhanced Discovery Tools ---
457
+
458
+
459
+ @mcp.tool
460
+ @catch_errors
461
+ def add_embeddings(table_name: str, text_columns: List[str],
462
+ embedding_column: str = "embedding",
463
+ model_name: str = "all-MiniLM-L6-v2") -> ToolResponse:
464
+ """
465
+ Generate and store vector embeddings for semantic search on table content.
466
+
467
+ This tool enables intelligent knowledge discovery by creating vector representations
468
+ of text content that can be searched semantically rather than just by exact keywords.
469
+
470
+ Args:
471
+ table_name (str): Name of the table to add embeddings to
472
+ text_columns (List[str]): List of text columns to generate embeddings from
473
+ embedding_column (str): Column name to store embeddings (default: "embedding")
474
+ model_name (str): Sentence transformer model to use (default: "all-MiniLM-L6-v2")
475
+
476
+ Returns:
477
+ ToolResponse: On success: {"success": True, "processed": int, "model": str}
478
+ On error: {"success": False, "error": str, "category": str, "details": dict}
479
+
480
+ Examples:
481
+ >>> add_embeddings("technical_decisions", ["decision_name", "rationale"])
482
+ {"success": True, "processed": 15, "model": "all-MiniLM-L6-v2", "embedding_dimension": 384}
483
+
484
+ FastMCP Tool Info:
485
+ - Automatically creates embedding column if it doesn't exist
486
+ - Combines multiple text columns into single embedding
487
+ - Only processes rows that don't already have embeddings
488
+ - Uses efficient batch processing for large datasets
489
+ - Supports various sentence-transformer models for different use cases
490
+ """
491
+ return cast(ToolResponse, get_database(DB_PATH).generate_embeddings(
492
+ table_name, text_columns, embedding_column, model_name
493
+ ))
494
+
495
+
496
+ @mcp.tool
497
+ @catch_errors
498
+ def semantic_search(query: str, tables: Optional[List[str]] = None,
499
+ similarity_threshold: float = 0.5, limit: int = 10,
500
+ model_name: str = "all-MiniLM-L6-v2") -> ToolResponse:
501
+ """
502
+ Find content using natural language semantic similarity rather than exact keyword matching.
503
+
504
+ This enables intelligent knowledge discovery - find related concepts even when
505
+ they use different terminology or phrasing.
506
+
507
+ Args:
508
+ query (str): Natural language search query
509
+ tables (Optional[List[str]]): Specific tables to search (default: all tables with embeddings)
510
+ similarity_threshold (float): Minimum similarity score (0.0-1.0, default: 0.5)
511
+ limit (int): Maximum number of results to return (default: 10)
512
+ model_name (str): Model to use for query embedding (default: "all-MiniLM-L6-v2")
513
+
514
+ Returns:
515
+ ToolResponse: On success: {"success": True, "results": List[...], "total_results": int}
516
+ On error: {"success": False, "error": str, "category": str, "details": dict}
517
+
518
+ Examples:
519
+ >>> semantic_search("API design patterns")
520
+ {"success": True, "results": [
521
+ {"table_name": "technical_decisions", "similarity_score": 0.87, "decision_name": "REST API Structure", ...},
522
+ {"table_name": "project_structure", "similarity_score": 0.72, "component": "API Gateway", ...}
523
+ ]}
524
+
525
+ >>> semantic_search("machine learning", tables=["technical_decisions"], similarity_threshold=0.7)
526
+ # Finds content about "ML", "AI", "neural networks", etc.
527
+
528
+ FastMCP Tool Info:
529
+ - Works across multiple tables simultaneously
530
+ - Finds conceptually similar content regardless of exact wording
531
+ - Returns relevance scores for ranking results
532
+ - Supports fuzzy matching and concept discovery
533
+ - Much more powerful than keyword-based search for knowledge discovery
534
+ """
535
+ return cast(ToolResponse, get_database(DB_PATH).semantic_search(
536
+ query, tables, "embedding", None, similarity_threshold, limit, model_name
537
+ ))
538
+
539
+
540
+ @mcp.tool
541
+ @catch_errors
542
+ def find_related(table_name: str, row_id: int, similarity_threshold: float = 0.5,
543
+ limit: int = 5, model_name: str = "all-MiniLM-L6-v2") -> ToolResponse:
544
+ """
545
+ Find content related to a specific row by semantic similarity.
546
+
547
+ Discover connections and related information that might not be obvious
548
+ from direct references or tags.
549
+
550
+ Args:
551
+ table_name (str): Table containing the reference row
552
+ row_id (int): ID of the row to find related content for
553
+ similarity_threshold (float): Minimum similarity score (default: 0.5)
554
+ limit (int): Maximum number of related items to return (default: 5)
555
+ model_name (str): Model for similarity comparison (default: "all-MiniLM-L6-v2")
556
+
557
+ Returns:
558
+ ToolResponse: On success: {"success": True, "results": List[...], "target_row": Dict}
559
+ On error: {"success": False, "error": str, "category": str, "details": dict}
560
+
561
+ Examples:
562
+ >>> find_related("technical_decisions", 5)
563
+ {"success": True, "results": [
564
+ {"id": 12, "similarity_score": 0.84, "decision_name": "Related Architecture Choice", ...},
565
+ {"id": 3, "similarity_score": 0.71, "decision_name": "Similar Technology Decision", ...}
566
+ ], "target_row": {"id": 5, "decision_name": "API Framework Selection", ...}}
567
+
568
+ FastMCP Tool Info:
569
+ - Helps discover hidden relationships between data
570
+ - Useful for finding similar decisions, related problems, or connected concepts
571
+ - Can reveal patterns and themes across your knowledge base
572
+ - Enables serendipitous discovery of relevant information
573
+ """
574
+ return cast(ToolResponse, get_database(DB_PATH).find_related_content(
575
+ table_name, row_id, "embedding", similarity_threshold, limit, model_name
576
+ ))
577
+
578
+
579
+ @mcp.tool
580
+ @catch_errors
581
+ def smart_search(query: str, tables: Optional[List[str]] = None,
582
+ semantic_weight: float = 0.7, text_weight: float = 0.3,
583
+ limit: int = 10, model_name: str = "all-MiniLM-L6-v2") -> ToolResponse:
584
+ """
585
+ Intelligent hybrid search combining semantic understanding with keyword matching.
586
+
587
+ Provides the best of both worlds - semantic similarity for concept discovery
588
+ plus exact text matching for precise searches.
589
+
590
+ Args:
591
+ query (str): Search query (natural language or keywords)
592
+ tables (Optional[List[str]]): Tables to search (default: all)
593
+ semantic_weight (float): Weight for semantic similarity (0.0-1.0, default: 0.7)
594
+ text_weight (float): Weight for keyword matching (0.0-1.0, default: 0.3)
595
+ limit (int): Maximum results (default: 10)
596
+ model_name (str): Semantic model to use (default: "all-MiniLM-L6-v2")
597
+
598
+ Returns:
599
+ ToolResponse: On success: {"success": True, "results": List[...], "search_type": "hybrid"}
600
+ On error: {"success": False, "error": str, "category": str, "details": dict}
601
+
602
+ Examples:
603
+ >>> smart_search("user authentication security")
604
+ {"success": True, "results": [
605
+ {"combined_score": 0.89, "semantic_score": 0.92, "text_score": 0.82, ...},
606
+ {"combined_score": 0.76, "semantic_score": 0.71, "text_score": 0.85, ...}
607
+ ], "search_type": "hybrid"}
608
+
609
+ FastMCP Tool Info:
610
+ - Automatically balances semantic and keyword search
611
+ - Provides separate scores for transparency
612
+ - Falls back gracefully if semantic search unavailable
613
+ - Optimal for both exploratory and precise searches
614
+ - Recommended for general-purpose knowledge discovery
615
+ """
616
+ return cast(ToolResponse, get_database(DB_PATH).hybrid_search(
617
+ query, tables, None, "embedding", semantic_weight, text_weight, limit, model_name
618
+ ))
619
+
620
+
621
+ @mcp.tool
622
+ @catch_errors
623
+ def embedding_stats(table_name: str, embedding_column: str = "embedding") -> ToolResponse:
624
+ """
625
+ Get statistics about semantic search readiness for a table.
626
+
627
+ Check which content has embeddings and can be searched semantically.
628
+
629
+ Args:
630
+ table_name (str): Table to analyze
631
+ embedding_column (str): Embedding column to check (default: "embedding")
632
+
633
+ Returns:
634
+ ToolResponse: On success: {"success": True, "coverage_percent": float, "total_rows": int}
635
+ On error: {"success": False, "error": str, "category": str, "details": dict}
636
+
637
+ Examples:
638
+ >>> embedding_stats("technical_decisions")
639
+ {"success": True, "total_rows": 25, "embedded_rows": 25, "coverage_percent": 100.0,
640
+ "embedding_dimensions": 384}
641
+
642
+ FastMCP Tool Info:
643
+ - Shows how much content is ready for semantic search
644
+ - Helps identify tables that need embedding generation
645
+ - Provides embedding dimension info for debugging
646
+ - Useful for monitoring semantic search capabilities
647
+ """
648
+ return cast(ToolResponse, get_database(DB_PATH).get_embedding_stats(table_name, embedding_column))
649
+
650
+
651
+ # Export the FastMCP app for use in other modules and server runners
652
+ app = mcp
653
+
654
+ # Document the app for better discovery
655
+ app.__doc__ = """
656
+ SQLite Memory Bank for Copilot/AI Agents
657
+
658
+ A dynamic, agent-friendly SQLite memory bank with explicit, type-safe tools.
659
+ All tools are designed for explicit, discoverable use by LLMs and FastMCP clients.
660
+
661
+ Available tools:
662
+ - create_table: Create a new table with a custom schema
663
+ - drop_table: Drop (delete) a table
664
+ - rename_table: Rename a table
665
+ - list_tables: List all tables in the memory bank
666
+ - describe_table: Get schema details for a table
667
+ - list_all_columns: List all columns for all tables
668
+ - create_row: Insert a row into any table
669
+ - read_rows: Read rows from any table (with optional filtering)
670
+ - update_rows: Update rows from any table (with optional filtering)
671
+ - delete_rows: Delete rows from any table (with optional filtering)
672
+ - run_select_query: Run a safe SELECT query (no arbitrary SQL)
673
+ - search_content: Perform full-text search across table content
674
+ - explore_tables: Discover table structures and content for better searchability
675
+ """
676
+
677
+
678
+ # Legacy implementation functions for backwards compatibility with tests
679
+ def _create_row_impl(table_name: str, data: Dict[str, Any]) -> Dict[str, Any]:
680
+ """Legacy implementation function for tests."""
681
+ try:
682
+ # Handle test-specific table creation for legacy compatibility
683
+ if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", table_name):
684
+ return {"success": False, "error": f"Invalid table name: {table_name}"}
685
+
686
+ # Auto-create test tables for compatibility
687
+ current_db = get_database(DB_PATH)
688
+ if table_name == "nodes":
689
+ try:
690
+ current_db.create_table(
691
+ "nodes",
692
+ [{"name": "id", "type": "INTEGER PRIMARY KEY AUTOINCREMENT"}, {"name": "label", "type": "TEXT NOT NULL"}],
693
+ )
694
+ except Exception:
695
+ pass # Table might already exist
696
+ elif table_name == "edges":
697
+ try:
698
+ current_db.create_table(
699
+ "edges",
700
+ [
701
+ {"name": "id", "type": "INTEGER PRIMARY KEY AUTOINCREMENT"},
702
+ {"name": "source", "type": "INTEGER NOT NULL"},
703
+ {"name": "target", "type": "INTEGER NOT NULL"},
704
+ {"name": "type", "type": "TEXT NOT NULL"},
705
+ ],
706
+ )
707
+ except Exception:
708
+ pass # Table might already exist
709
+
710
+ result = current_db.insert_row(table_name, data)
711
+ # Ensure we return Dict[str, Any] for legacy compatibility
712
+ return dict(result) if isinstance(result, dict) else {"success": False, "error": "Unknown error"}
713
+
714
+ except Exception as e:
715
+ logging.error(f"_create_row_impl error: {e}")
716
+ return {"success": False, "error": str(e)}
717
+
718
+
719
+ def _read_rows_impl(table_name: str, where: Optional[Dict[str, Any]] = None, limit: int = 100) -> Dict[str, Any]:
720
+ """Legacy implementation function for tests."""
721
+ try:
722
+ result = get_database(DB_PATH).read_rows(table_name, where, limit)
723
+ # Ensure we return Dict[str, Any] for legacy compatibility
724
+ return dict(result) if isinstance(result, dict) else {"success": False, "error": "Unknown error"}
725
+ except Exception as e:
726
+ logging.error(f"_read_rows_impl error: {e}")
727
+ return {"success": False, "error": str(e)}
728
+
729
+
730
+ def _update_rows_impl(table_name: str, data: Dict[str, Any], where: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
731
+ """Legacy implementation function for tests."""
732
+ try:
733
+ # Auto-create test tables for compatibility
734
+ if table_name == "edges":
735
+ try:
736
+ current_db = get_database(DB_PATH)
737
+ current_db.create_table(
738
+ "edges",
739
+ [
740
+ {"name": "id", "type": "INTEGER PRIMARY KEY AUTOINCREMENT"},
741
+ {"name": "source", "type": "INTEGER NOT NULL"},
742
+ {"name": "target", "type": "INTEGER NOT NULL"},
743
+ {"name": "type", "type": "TEXT NOT NULL"},
744
+ ],
745
+ )
746
+ except Exception:
747
+ pass # Table might already exist
748
+
749
+ result = get_database(DB_PATH).update_rows(table_name, data, where)
750
+ # Ensure we return Dict[str, Any] for legacy compatibility
751
+ return dict(result) if isinstance(result, dict) else {"success": False, "error": "Unknown error"}
752
+ except Exception as e:
753
+ logging.error(f"_update_rows_impl error: {e}")
754
+ return {"success": False, "error": str(e)}
755
+
756
+
757
+ def _delete_rows_impl(table_name: str, where: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
758
+ """Legacy implementation function for tests."""
759
+ try:
760
+ result = get_database(DB_PATH).delete_rows(table_name, where)
761
+ # Ensure we return Dict[str, Any] for legacy compatibility
762
+ return dict(result) if isinstance(result, dict) else {"success": False, "error": "Unknown error"}
763
+ except Exception as e:
764
+ logging.error(f"_delete_rows_impl error: {e}")
765
+ return {"success": False, "error": str(e)}
766
+
767
+
768
+ # Public API - these functions are available for direct Python use and as MCP tools
769
+ __all__ = [
770
+ "app",
771
+ "mcp",
772
+ "create_table",
773
+ "drop_table",
774
+ "rename_table",
775
+ "list_tables",
776
+ "describe_table",
777
+ "list_all_columns",
778
+ "create_row",
779
+ "read_rows",
780
+ "update_rows",
781
+ "delete_rows",
782
+ "run_select_query",
783
+ "search_content",
784
+ "explore_tables",
785
+ "_create_row_impl",
786
+ "_read_rows_impl",
787
+ "_update_rows_impl",
788
+ "_delete_rows_impl",
789
+ ]
790
+
791
+
792
+ def mcp_server():
793
+ """Entry point for MCP stdio server (for uvx and package installations)."""
794
+ # Configure logging for MCP server
795
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
796
+
797
+ # Log startup information
798
+ logging.info(f"Starting SQLite Memory Bank MCP server with database at {DB_PATH}")
799
+
800
+ # Run the FastMCP app in stdio mode
801
+ app.run()
802
+
803
+
804
+ def main():
805
+ """Alternative entry point for HTTP server mode (development/testing only)."""
806
+ import uvicorn
807
+ import argparse
808
+
809
+ parser = argparse.ArgumentParser(description="Run MCP SQLite Memory Bank Server in HTTP mode")
810
+ parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
811
+ parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
812
+ parser.add_argument("--db-path", help="Path to SQLite database file")
813
+ parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
814
+
815
+ args = parser.parse_args()
816
+
817
+ # Set database path if provided
818
+ if args.db_path:
819
+ global DB_PATH
820
+ DB_PATH = args.db_path
821
+ os.environ["DB_PATH"] = args.db_path
822
+
823
+ print(f"Starting MCP SQLite Memory Bank server in HTTP mode on {args.host}:{args.port}")
824
+ print(f"Database path: {DB_PATH}")
825
+ print("Available at: http://localhost:8000/docs")
826
+
827
+ uvicorn.run("mcp_sqlite_memory_bank.server:app", host=args.host, port=args.port, reload=args.reload)
828
+
829
+
830
+ if __name__ == "__main__":
831
+ # Configure logging
832
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
833
+
834
+ # Log startup information
835
+ logging.info(f"Starting SQLite Memory Bank with database at {DB_PATH}")
836
+
837
+ # Run the FastMCP app in stdio mode for MCP clients
838
+ app.run()