Skip to content

Commit 0d02eb8

Browse files
Gh-Novelclaude
andcommitted
feat(server): persistent SQLite memory, env fixes, HTTP transport fix
- memory tools now persist to SQLite (THINKMCP_MEMORY_DB), surviving server restarts and shared across MCP clients - actions: read THINKMCP_REPORTS_DIR at call time, not import time - fix --http mode crash: FastMCP.run() takes no host/port kwargs; set them via mcp.settings instead - drop ANTHROPIC_API_KEY from client config template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b1cefa0 commit 0d02eb8

6 files changed

Lines changed: 78 additions & 39 deletions

File tree

‎mcp_config.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
"command": "python",
55
"args": ["/ABSOLUTE/PATH/TO/thinkmcp/server/mcp_server.py"],
66
"env": {
7-
"ANTHROPIC_API_KEY": "your_anthropic_api_key",
87
"TAVILY_API_KEY": "your_tavily_api_key",
98
"GITHUB_TOKEN": "your_github_token_optional",
10-
"THINKMCP_REPORTS_DIR": "/ABSOLUTE/PATH/TO/thinkmcp/reports"
9+
"THINKMCP_REPORTS_DIR": "/ABSOLUTE/PATH/TO/thinkmcp/reports",
10+
"THINKMCP_MEMORY_DB": "/ABSOLUTE/PATH/TO/thinkmcp/thinkmcp_memory.db"
1111
}
1212
}
1313
}

‎server/mcp_server.py‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
HTTP — for remote / Streamlit deployment (use --http flag)
77
"""
88

9-
import sys
109
import os
10+
import sys
1111

1212
# Allow running as: python server/mcp_server.py [--http] [--port PORT]
1313
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
1414

1515
from mcp.server.fastmcp import FastMCP
1616

17-
from server.tools.research import web_search, fetch_url, search_papers, search_code
18-
from server.tools.reasoning import think, critique, plan
19-
from server.tools.memory import remember, recall, list_memory
20-
from server.tools.actions import write_report, create_summary, compare
17+
from server.tools.actions import compare, create_summary, write_report
18+
from server.tools.memory import list_memory, recall, remember
19+
from server.tools.reasoning import critique, plan, think
20+
from server.tools.research import fetch_url, search_code, search_papers, web_search
2121

2222
mcp = FastMCP(
2323
name="ThinkMCP",
@@ -114,7 +114,7 @@ def plan_tool(goal: str) -> dict:
114114
@mcp.tool()
115115
def remember_tool(key: str, value: str) -> dict:
116116
"""
117-
Persist a key-value pair in session memory for later recall.
117+
Persist a key-value pair in durable memory (SQLite) for later recall.
118118
Use descriptive keys like 'ddim_key_claim' or 'source_A_summary'.
119119
Returns: {status, key, value_preview, total_keys}
120120
"""
@@ -133,7 +133,7 @@ def recall_tool(key: str) -> dict:
133133
@mcp.tool()
134134
def list_memory_tool() -> dict:
135135
"""
136-
Show all key-value pairs currently stored in session memory.
136+
Show all key-value pairs currently stored in memory.
137137
Returns: {total, entries: {key: value_preview}}
138138
"""
139139
return list_memory()
@@ -184,7 +184,9 @@ def compare_tool(item_a: str, item_b: str) -> dict:
184184

185185
if args.http:
186186
print(f"[ThinkMCP] Starting HTTP server on {args.host}:{args.port}", file=sys.stderr)
187-
mcp.run(transport="streamable-http", host=args.host, port=args.port)
187+
mcp.settings.host = args.host
188+
mcp.settings.port = args.port
189+
mcp.run(transport="streamable-http")
188190
else:
189191
print("[ThinkMCP] Starting stdio server", file=sys.stderr)
190192
mcp.run(transport="stdio")

‎server/tools/actions.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,24 @@
22

33
import os
44
import re
5-
import textwrap
65
from datetime import datetime
76

87

9-
REPORTS_DIR = os.environ.get("THINKMCP_REPORTS_DIR", "./reports")
8+
def _reports_dir() -> str:
9+
return os.environ.get("THINKMCP_REPORTS_DIR", "./reports")
1010

1111

1212
def write_report(title: str, content: str) -> dict:
1313
"""Format content as a markdown report and save it to disk."""
14-
os.makedirs(REPORTS_DIR, exist_ok=True)
14+
reports_dir = _reports_dir()
15+
os.makedirs(reports_dir, exist_ok=True)
1516

1617
# Sanitize filename
1718
safe_title = re.sub(r"[^\w\s-]", "", title).strip()
1819
safe_title = re.sub(r"\s+", "_", safe_title)[:60]
1920
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
2021
filename = f"{timestamp}_{safe_title}.md"
21-
filepath = os.path.join(REPORTS_DIR, filename)
22+
filepath = os.path.join(reports_dir, filename)
2223

2324
report_body = f"""# {title}
2425

‎server/tools/memory.py‎

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,77 @@
1-
"""Session memory tools: remember, recall, list_memory."""
1+
"""Persistent memory tools: remember, recall, list_memory.
22
3-
# In-process session store (reset per server process)
4-
_store: dict[str, str] = {}
3+
Backed by SQLite so memory survives server restarts (the agent spawns a fresh
4+
MCP server subprocess per run). Override the location with THINKMCP_MEMORY_DB;
5+
it defaults to ./thinkmcp_memory.db in the working directory.
6+
"""
7+
8+
import os
9+
import sqlite3
10+
from datetime import datetime
11+
12+
13+
def _db_path() -> str:
14+
return os.environ.get("THINKMCP_MEMORY_DB", "./thinkmcp_memory.db")
15+
16+
17+
def _connect() -> sqlite3.Connection:
18+
conn = sqlite3.connect(_db_path())
19+
conn.execute(
20+
"CREATE TABLE IF NOT EXISTS memory ("
21+
"key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL)"
22+
)
23+
return conn
24+
25+
26+
def _preview(value: str, limit: int) -> str:
27+
return value[:limit] + ("..." if len(value) > limit else "")
528

629

730
def remember(key: str, value: str) -> dict:
8-
"""Persist a key-value pair for the duration of this session."""
9-
_store[key] = value
31+
"""Persist a key-value pair (upsert)."""
32+
with _connect() as conn:
33+
conn.execute(
34+
"INSERT INTO memory (key, value, updated_at) VALUES (?, ?, ?) "
35+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
36+
(key, value, datetime.now().isoformat()),
37+
)
38+
total = conn.execute("SELECT COUNT(*) FROM memory").fetchone()[0]
1039
return {
1140
"status": "stored",
1241
"key": key,
13-
"value_preview": value[:100] + ("..." if len(value) > 100 else ""),
14-
"total_keys": len(_store),
42+
"value_preview": _preview(value, 100),
43+
"total_keys": total,
1544
}
1645

1746

1847
def recall(key: str) -> dict:
1948
"""Retrieve a previously stored value by key."""
20-
if key not in _store:
21-
available = list(_store.keys())
22-
return {
23-
"status": "not_found",
24-
"key": key,
25-
"value": None,
26-
"available_keys": available,
27-
}
28-
return {"status": "found", "key": key, "value": _store[key]}
49+
with _connect() as conn:
50+
row = conn.execute("SELECT value FROM memory WHERE key = ?", (key,)).fetchone()
51+
if row is None:
52+
available = [r[0] for r in conn.execute("SELECT key FROM memory ORDER BY key")]
53+
return {
54+
"status": "not_found",
55+
"key": key,
56+
"value": None,
57+
"available_keys": available,
58+
}
59+
return {"status": "found", "key": key, "value": row[0]}
2960

3061

3162
def list_memory() -> dict:
32-
"""List all stored key-value pairs."""
63+
"""List all stored key-value pairs (values previewed)."""
64+
with _connect() as conn:
65+
rows = conn.execute("SELECT key, value FROM memory ORDER BY key").fetchall()
3366
return {
34-
"total": len(_store),
35-
"entries": {k: (v[:80] + "..." if len(v) > 80 else v) for k, v in _store.items()},
67+
"total": len(rows),
68+
"entries": {k: _preview(v, 80) for k, v in rows},
3669
}
3770

3871

3972
def clear_memory() -> dict:
40-
"""Clear all stored memory (utility, not exposed as MCP tool)."""
41-
count = len(_store)
42-
_store.clear()
73+
"""Clear all stored memory (utility, not exposed as an MCP tool)."""
74+
with _connect() as conn:
75+
count = conn.execute("SELECT COUNT(*) FROM memory").fetchone()[0]
76+
conn.execute("DELETE FROM memory")
4377
return {"cleared": count}

‎server/tools/research.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""Research tools: web_search, fetch_url, search_papers, search_code."""
22

3+
import html
4+
import json
35
import os
46
import re
57
import urllib.parse
68
import urllib.request
7-
import json
8-
import html
99

1010

1111
def web_search(query: str) -> dict:

‎transport/http_transport.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ def main() -> None:
2626
args = parser.parse_args()
2727

2828
print(f"[ThinkMCP HTTP] Listening on http://{args.host}:{args.port}/mcp", file=sys.stderr)
29-
mcp.run(transport="streamable-http", host=args.host, port=args.port)
29+
mcp.settings.host = args.host
30+
mcp.settings.port = args.port
31+
mcp.run(transport="streamable-http")
3032

3133

3234
if __name__ == "__main__":

0 commit comments

Comments
 (0)