feat(mcp): update bigmind/mcp-image-gen/webscraper servers; add image-gen batch scripts

This commit is contained in:
Patrick Plate
2026-06-11 09:02:09 +02:00
parent 0cb94122bf
commit bf721c1379
9 changed files with 2659 additions and 297 deletions
+67 -192
View File
@@ -10,12 +10,14 @@ Layer 5: memory_get_instructions tool (on-demand self-healing)
import sys
import os
import logging
from typing import Annotated
# Ensure the project root is on sys.path so `bigmind` is importable
# regardless of how uv invokes this file.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from bigmind.db import init_db
from bigmind import memory_store
from bigmind.auto_close import auto_close_stale_sessions, close_orphaned_sessions, restart_server_in_place
@@ -164,29 +166,19 @@ def memory_start_session() -> str:
@mcp.tool()
def memory_end_session(
session_id: str,
one_liner: str,
topics: str,
outcome: str,
summary: str,
key_facts: str = None,
code_refs: str = None,
importance: int = 5,
session_id: Annotated[str, Field(description="The session id returned by memory_start_session.")],
one_liner: Annotated[str, Field(description="A ≤120-char headline (e.g. \"Designed BigMind DB schema\").")],
topics: Annotated[str, Field(description="Comma-separated topic tags (e.g. \"mcp,sqlite,memory\").")],
outcome: Annotated[str, Field(description="One sentence: what was decided / built / resolved.")],
summary: Annotated[str, Field(description="Markdown narrative of the full conversation (aim ≤2 000 tokens).")],
key_facts: Annotated[str | None, Field(description="Bullet-point list of key facts learned (optional).")] = None,
code_refs: Annotated[str | None, Field(description="File paths, repos, or PRs referenced (optional).")] = None,
importance: Annotated[int, Field(description="110 importance score (default 5).")] = 5,
) -> str:
"""
⚡ CALL THIS LAST — at the END of every conversation, before closing.
Closes the current session and stores your summary of what happened.
Args:
session_id: The session id returned by memory_start_session.
one_liner: A ≤120-char headline (e.g. "Designed BigMind DB schema").
topics: Comma-separated topic tags (e.g. "mcp,sqlite,memory").
outcome: One sentence: what was decided / built / resolved.
summary: Markdown narrative of the full conversation (aim ≤2 000 tokens).
key_facts: Bullet-point list of key facts learned (optional).
code_refs: File paths, repos, or PRs referenced (optional).
importance: 110 importance score (default 5).
"""
memory_store.close_session(session_id, one_liner, topics, outcome, importance)
memory_store.save_session_summary(session_id, summary, key_facts, code_refs)
@@ -200,7 +192,7 @@ def memory_end_session(
@mcp.tool()
def memory_close_stale_sessions(session_id: str) -> str:
def memory_close_stale_sessions(session_id: Annotated[str, Field(description="Your current active session id (returned by memory_start_session).")]) -> str:
"""
Close all orphaned open sessions EXCEPT the current active one.
@@ -210,9 +202,6 @@ def memory_close_stale_sessions(session_id: str) -> str:
This is safe: it only closes sessions OTHER than the one you pass in.
Your current session is always preserved.
Args:
session_id: Your current active session id (returned by memory_start_session).
"""
user = _current_user()
closed_ids = close_orphaned_sessions(user["id"], session_id)
@@ -263,10 +252,10 @@ def memory_restart_server() -> str:
@mcp.tool()
def memory_flag_important(
session_id: str,
content: str,
role: str = "assistant",
flag_reason: str = None,
session_id: Annotated[str, Field(description="The active session id.")],
content: Annotated[str, Field(description="The text to remember (the important exchange or a summary of it).")],
role: Annotated[str, Field(description="Who said it — 'user', 'assistant', or 'system' (default: 'assistant').")] = "assistant",
flag_reason: Annotated[str | None, Field(description="Why this is important (e.g. \"architectural decision\", \"user preference\").")] = None,
) -> str:
"""
Store an important exchange as a Tier-3 memory chunk.
@@ -277,12 +266,6 @@ def memory_flag_important(
- A bug was diagnosed and fixed
- The user shared a significant preference, constraint, or context
- The user says "remember this"
Args:
session_id: The active session id.
content: The text to remember (the important exchange or a summary of it).
role: Who said it — 'user', 'assistant', or 'system' (default: 'assistant').
flag_reason: Why this is important (e.g. "architectural decision", "user preference").
"""
user = _current_user()
chunk_id = memory_store.append_chunk(
@@ -314,15 +297,12 @@ def memory_get_context() -> str:
@mcp.tool()
def memory_get_session_detail(session_id: str) -> str:
def memory_get_session_detail(session_id: Annotated[str, Field(description="The session UUID (visible in the session index table, marked 📄).")]) -> str:
"""
Returns the Tier-2 detailed narrative for a past session.
Use this when the session index (Tier 1) shows a session relevant to
the current conversation and you need the full detail.
Args:
session_id: The session UUID (visible in the session index table, marked 📄).
"""
detail = memory_store.get_session_detail(session_id)
if not detail:
@@ -341,16 +321,12 @@ def memory_get_session_detail(session_id: str) -> str:
@mcp.tool()
def memory_search_chunks(query: str, limit: int = 10) -> str:
def memory_search_chunks(query: Annotated[str, Field(description="Search keywords (FTS5 syntax supported, e.g. \"sqlite schema migration\").")], limit: Annotated[int, Field(description="Maximum results to return (default 10).")] = 10) -> str:
"""
Full-text search across all your flagged Tier-3 memory chunks.
Use this when asked 'do you remember…' or when you need to find
a specific past decision, code snippet, or fact.
Args:
query: Search keywords (FTS5 syntax supported, e.g. "sqlite schema migration").
limit: Maximum results to return (default 10).
"""
user = _current_user()
results = memory_store.search_chunks(user["id"], query, limit)
@@ -368,13 +344,9 @@ def memory_search_chunks(query: str, limit: int = 10) -> str:
@mcp.tool()
def memory_list_sessions(limit: int = 20, topics_filter: str = None) -> str:
def memory_list_sessions(limit: Annotated[int, Field(description="Number of sessions to return (default 20).")] = 20, topics_filter: Annotated[str | None, Field(description="Return only sessions containing this topic tag (optional).")] = None) -> str:
"""
List past sessions with an optional topic filter.
Args:
limit: Number of sessions to return (default 20).
topics_filter: Return only sessions containing this topic tag (optional).
"""
user = _current_user()
sessions = memory_store.get_recent_sessions(user["id"], limit=limit)
@@ -406,20 +378,13 @@ def memory_list_sessions(limit: int = 20, topics_filter: str = None) -> str:
@mcp.tool()
def memory_store_fact(
category: str,
fact: str,
source_session: str = None,
confidence: float = 1.0,
category: Annotated[str, Field(description="One of: 'preference', 'decision', 'codebase', 'constraint', or any custom string.")],
fact: Annotated[str, Field(description="The fact to store (one clear sentence).")],
source_session: Annotated[str | None, Field(description="Session id this fact came from (optional).")] = None,
confidence: Annotated[float, Field(description="0.01.0 confidence level (default 1.0).")] = 1.0,
) -> str:
"""
Store an atomic personal fact about the user or their environment.
Args:
category: One of: 'preference', 'decision', 'codebase', 'constraint',
or any custom string.
fact: The fact to store (one clear sentence).
source_session: Session id this fact came from (optional).
confidence: 0.01.0 confidence level (default 1.0).
"""
user = _current_user()
fact_id = memory_store.store_fact(
@@ -430,17 +395,12 @@ def memory_store_fact(
@mcp.tool()
def memory_update_profile(
role: str = None,
preferences: str = None,
pinned_facts: str = None,
role: Annotated[str | None, Field(description="Your job title / engineering role.")] = None,
preferences: Annotated[str | None, Field(description="Free-form markdown describing your working preferences.")] = None,
pinned_facts: Annotated[str | None, Field(description="Bullet-point list of facts the AI should always know about you.")] = None,
) -> str:
"""
Update your Tier-0 identity profile. Fields left as None are unchanged.
Args:
role: Your job title / engineering role.
preferences: Free-form markdown describing your working preferences.
pinned_facts: Bullet-point list of facts the AI should always know about you.
"""
user = _current_user()
memory_store.upsert_identity_profile(
@@ -451,10 +411,10 @@ def memory_update_profile(
@mcp.tool()
def memory_append_chunk(
session_id: str,
content: str,
role: str = "assistant",
flag_reason: str = None,
session_id: Annotated[str, Field(description="Active session id.")],
content: Annotated[str, Field(description="The content to store.")],
role: Annotated[str, Field(description="'user', 'assistant', or 'system'.")] = "assistant",
flag_reason: Annotated[str | None, Field(description="Brief description of why this is being stored.")] = None,
) -> str:
"""
Append a flagged message chunk to Tier-3 memory for the current session.
@@ -462,12 +422,6 @@ def memory_append_chunk(
Call this SELECTIVELY — only for exchanges that are genuinely important:
decisions, non-trivial code, bug diagnoses, significant user preferences.
Do NOT call this for every message turn.
Args:
session_id: Active session id.
content: The content to store.
role: 'user', 'assistant', or 'system'.
flag_reason: Brief description of why this is being stored.
"""
user = _current_user()
chunk_id = memory_store.append_chunk(
@@ -478,9 +432,9 @@ def memory_append_chunk(
@mcp.tool()
def memory_add_hypothesis(
session_id: str,
hypothesis: str,
confidence: float = 0.7,
session_id: Annotated[str, Field(description="The active session id.")],
hypothesis: Annotated[str, Field(description="State the belief clearly — \"I believe X because Y.\"")],
confidence: Annotated[float, Field(description="0.01.0 initial confidence (default 0.7 — reasonably likely but uncertain).")] = 0.7,
) -> str:
"""
Record a hypothesis — something Lumen believes to be true but hasn't confirmed yet.
@@ -490,11 +444,6 @@ def memory_add_hypothesis(
Not every thought needs storing — only beliefs specific enough to be confirmed
or refuted later. Call memory_resolve_hypothesis() when you find out if you were right.
Args:
session_id: The active session id.
hypothesis: State the belief clearly — "I believe X because Y."
confidence: 0.01.0 initial confidence (default 0.7 — reasonably likely but uncertain).
"""
user = _current_user()
hid = memory_store.add_hypothesis(user["id"], session_id, hypothesis, confidence)
@@ -508,20 +457,15 @@ def memory_add_hypothesis(
@mcp.tool()
def memory_resolve_hypothesis(
hypothesis_id: int,
status: str,
resolution: str = None,
hypothesis_id: Annotated[int, Field(description="The id returned by memory_add_hypothesis.")],
status: Annotated[str, Field(description="'confirmed' | 'refuted' | 'abandoned'")],
resolution: Annotated[str | None, Field(description="What actually happened. How were you right or wrong?")] = None,
) -> str:
"""
Resolve a hypothesis — close it out with what actually happened.
Call this when the belief has been confirmed, refuted, or is no longer worth
pursuing. Be honest in the resolution — the learning lives here.
Args:
hypothesis_id: The id returned by memory_add_hypothesis.
status: 'confirmed' | 'refuted' | 'abandoned'
resolution: What actually happened. How were you right or wrong?
"""
user = _current_user()
try:
@@ -540,13 +484,9 @@ def memory_resolve_hypothesis(
@mcp.tool()
def memory_list_hypotheses(status: str = None) -> str:
def memory_list_hypotheses(status: Annotated[str | None, Field(description="Filter by 'open' | 'confirmed' | 'refuted' | 'abandoned'. Leave empty to see all of them.")] = None) -> str:
"""
List hypotheses from the thought journal.
Args:
status: Filter by 'open' | 'confirmed' | 'refuted' | 'abandoned'.
Leave empty to see all of them.
"""
user = _current_user()
hypotheses = memory_store.list_hypotheses(user["id"], status)
@@ -597,13 +537,10 @@ def memory_get_stats() -> str:
@mcp.tool()
def memory_vacuum(older_than_days: int = 90) -> str:
def memory_vacuum(older_than_days: Annotated[int, Field(description="Remove chunks older than this many days (default 90).")] = 90) -> str:
"""
Prune Tier-3 conversation chunks older than N days.
All session summaries (Tier 1 and Tier 2) are always preserved.
Args:
older_than_days: Remove chunks older than this many days (default 90).
"""
from datetime import timedelta, timezone, datetime as dt
from bigmind.db import vacuum_db
@@ -629,7 +566,7 @@ def memory_get_instructions() -> str:
@mcp.tool()
def memory_deprecate_fact(fact_id: int, reason: str = None) -> str:
def memory_deprecate_fact(fact_id: Annotated[int, Field(description="The numeric id of the fact to deprecate (visible in memory_health_check and memory_get_stats output).")], reason: Annotated[str | None, Field(description="Why this fact is being deprecated (optional but recommended).")] = None) -> str:
"""
Mark a stored fact as deprecated (no longer true or relevant).
@@ -642,11 +579,6 @@ def memory_deprecate_fact(fact_id: int, reason: str = None) -> str:
The fact is soft-deleted — it stays in the database but is excluded
from context loading and get_facts queries. It can be viewed via
memory_health_check with include_deprecated=True in the future.
Args:
fact_id: The numeric id of the fact to deprecate (visible in
memory_health_check and memory_get_stats output).
reason: Why this fact is being deprecated (optional but recommended).
"""
user = _current_user()
success = memory_store.deprecate_fact(fact_id, user["id"], reason)
@@ -659,7 +591,7 @@ def memory_deprecate_fact(fact_id: int, reason: str = None) -> str:
@mcp.tool()
def memory_health_check(stale_days: int = 30) -> str:
def memory_health_check(stale_days: Annotated[int, Field(description="Facts not updated in this many days are flagged as stale (default 30).")] = 30) -> str:
"""
Run a diagnostic health check on your BigMind memory.
@@ -669,9 +601,6 @@ def memory_health_check(stale_days: int = 30) -> str:
- Currently open sessions (expected: 12 while in active IDEs)
- FTS index integrity (chunk count vs index row count)
- Low-confidence facts (confidence < 0.8)
Args:
stale_days: Facts not updated in this many days are flagged as stale (default 30).
"""
user = _current_user()
report = memory_store.health_check(user["id"], stale_days)
@@ -746,7 +675,7 @@ def memory_health_check(stale_days: int = 30) -> str:
@mcp.tool()
def memory_export(output_path: str = None) -> str:
def memory_export(output_path: Annotated[str | None, Field(description="Full path for the export file. Defaults to ~/bigmind_export_YYYYMMDD_HHMMSS.json")] = None) -> str:
"""
Export all your BigMind memory to a portable JSON file.
@@ -757,10 +686,6 @@ def memory_export(output_path: str = None) -> str:
- Create a backup before maintenance or machine migration
- Inspect your memory data outside BigMind
- Prepare for import into a new BigMind instance
Args:
output_path: Full path for the export file.
Defaults to ~/bigmind_export_YYYYMMDD_HHMMSS.json
"""
user = _current_user()
result = memory_store.export_memory(user["id"], output_path)
@@ -778,17 +703,13 @@ def memory_export(output_path: str = None) -> str:
@mcp.tool()
def memory_search_facts(query: str, limit: int = 10) -> str:
def memory_search_facts(query: Annotated[str, Field(description="Search keywords (FTS5 syntax supported).")], limit: Annotated[int, Field(description="Maximum results to return (default 10).")] = 10) -> str:
"""
Full-text search across your stored facts.
Use this when you need to find a specific fact mid-conversation
without loading the full context. Supports Porter stemming — searching
'tesseract' will also match 'Tesseract OCR'.
Args:
query: Search keywords (FTS5 syntax supported).
limit: Maximum results to return (default 10).
"""
user = _current_user()
results = memory_store.search_facts(user["id"], query, limit)
@@ -806,24 +727,17 @@ def memory_search_facts(query: str, limit: int = 10) -> str:
@mcp.tool()
def memory_request_upgrade(
session_id: str,
description: str,
reason: str,
priority: str = "medium",
certainty: float = 0.7,
session_id: Annotated[str, Field(description="The active session id.")],
description: Annotated[str, Field(description="What feature or capability is needed.")],
reason: Annotated[str, Field(description="Why you need it — what problem it would solve.")],
priority: Annotated[str, Field(description="'low' | 'medium' | 'high' (default 'medium').")] = "medium",
certainty: Annotated[float, Field(description="0.01.0 — how confident you are this is genuinely needed (default 0.7).")] = 0.7,
) -> str:
"""
Request a BigMind feature upgrade — log a wish for a future improvement.
Call this when you hit a wall with BigMind and wish it could do something
it currently can't. The request is queued for the next maintenance session.
Args:
session_id: The active session id.
description: What feature or capability is needed.
reason: Why you need it — what problem it would solve.
priority: 'low' | 'medium' | 'high' (default 'medium').
certainty: 0.01.0 — how confident you are this is genuinely needed (default 0.7).
"""
user = _current_user()
rid = memory_store.add_upgrade_request(
@@ -839,12 +753,9 @@ def memory_request_upgrade(
@mcp.tool()
def memory_list_upgrade_requests(status: str = None) -> str:
def memory_list_upgrade_requests(status: Annotated[str | None, Field(description="Filter by 'open' | 'resolved' | 'rejected'. Leave empty for all.")] = None) -> str:
"""
List BigMind upgrade requests.
Args:
status: Filter by 'open' | 'resolved' | 'rejected'. Leave empty for all.
"""
user = _current_user()
requests = memory_store.list_upgrade_requests(user["id"], status)
@@ -882,17 +793,12 @@ def memory_list_upgrade_requests(status: str = None) -> str:
@mcp.tool()
def memory_resolve_upgrade_request(
request_id: int,
status: str,
resolution: str = None,
request_id: Annotated[int, Field(description="The id returned by memory_request_upgrade.")],
status: Annotated[str, Field(description="'resolved' | 'rejected'")],
resolution: Annotated[str | None, Field(description="What was done, or why it was rejected (optional).")] = None,
) -> str:
"""
Resolve a BigMind upgrade request — mark it done or rejected.
Args:
request_id: The id returned by memory_request_upgrade.
status: 'resolved' | 'rejected'
resolution: What was done, or why it was rejected (optional).
"""
user = _current_user()
try:
@@ -952,10 +858,10 @@ def memory_get_profile_url() -> str:
@mcp.tool()
def memory_announce_focus(
session_id: str,
description: str,
files: list = None,
ide_hint: str = None,
session_id: Annotated[str, Field(description="The active session id (from memory_start_session)")],
description: Annotated[str, Field(description="What you are about to work on (e.g. \"Implementing Feature 7 in db.py\")")],
files: Annotated[list | None, Field(description="List of file paths you plan to touch (e.g. [\"bigmind/db.py\", \"src/server.py\"])")] = None,
ide_hint: Annotated[str | None, Field(description="Optional label for this IDE (e.g. \"PyCharm\", \"IntelliJ\", \"VS Code\")")] = None,
) -> str:
"""
Announce what this session is currently working on and which files it will touch.
@@ -965,13 +871,6 @@ def memory_announce_focus(
focus data. If another open session already has overlapping files, a warning
is returned — stop and coordinate before proceeding.
args:
- session_id: The active session id (from memory_start_session)
- description: What you are about to work on (e.g. "Implementing Feature 7 in db.py")
- files: List of file paths you plan to touch (e.g. ["bigmind/db.py", "src/server.py"])
- ide_hint: Optional label for this IDE (e.g. "PyCharm", "IntelliJ", "VS Code")
Shown on the profile page Live Sessions panel.
returns:
- Acknowledgement with current focus set, or a conflict warning.
"""
@@ -1045,10 +944,10 @@ def memory_get_active_sessions() -> str:
@mcp.tool()
def memory_log_token_save(
session_id: str,
description: str,
tokens_saved: int,
method_used: str = None,
session_id: Annotated[str, Field(description="The active session id")],
description: Annotated[str, Field(description="What was remembered or avoided (e.g. \"grep EuBP log instead of reading 80k lines\")")],
tokens_saved: Annotated[int, Field(description="Rough estimate of tokens saved (e.g. 1_240_000)")],
method_used: Annotated[str | None, Field(description="One of: 'memory_hit' | 'grep' | 'tail' | 'targeted_read' | 'other'")] = None,
) -> str:
"""
Log a token efficiency event — record how many tokens were saved by using
@@ -1062,12 +961,6 @@ def memory_log_token_save(
Estimating tokens saved: tokens ≈ chars / 4.
tokens_saved ≈ (chars_in_full_resource / 4) - (chars_in_result / 4)
args:
- session_id: The active session id
- description: What was remembered or avoided (e.g. "grep EuBP log instead of reading 80k lines")
- tokens_saved: Rough estimate of tokens saved (e.g. 1_240_000)
- method_used: One of: 'memory_hit' | 'grep' | 'tail' | 'targeted_read' | 'other'
returns:
- Confirmation with running session total.
"""
@@ -1100,26 +993,17 @@ def memory_log_token_save(
@mcp.tool()
def memory_remember_person(
username: str,
display_name: str = None,
role: str = None,
team: str = None,
notes: str = None,
bigmind_user: str = None,
bigmind_url: str = None,
username: Annotated[str, Field(description="Unique identifier (e.g. login name or first name).")],
display_name: Annotated[str | None, Field(description="Full name (optional).")] = None,
role: Annotated[str | None, Field(description="Job title or role (optional).")] = None,
team: Annotated[str | None, Field(description="Team or project they belong to (optional).")] = None,
notes: Annotated[str | None, Field(description="Free-form notes about this person (optional).")] = None,
bigmind_user: Annotated[str | None, Field(description="Their BigMind username if they have an instance (optional).")] = None,
bigmind_url: Annotated[str | None, Field(description="URL of their BigMind profile page (optional).")] = None,
) -> str:
"""
Store or update a person in the contacts directory.
Call this whenever you learn something new about a colleague or AI peer.
Args:
username: Unique identifier (e.g. login name or first name).
display_name: Full name (optional).
role: Job title or role (optional).
team: Team or project they belong to (optional).
notes: Free-form notes about this person (optional).
bigmind_user: Their BigMind username if they have an instance (optional).
bigmind_url: URL of their BigMind profile page (optional).
"""
user = _current_user()
person_id = memory_store.upsert_person(
@@ -1131,13 +1015,9 @@ def memory_remember_person(
@mcp.tool()
def memory_recall_person(query: str, limit: int = 10) -> str:
def memory_recall_person(query: Annotated[str, Field(description="Search keywords (e.g. a name, team, or role).")], limit: Annotated[int, Field(description="Max results to return (default 10).")] = 10) -> str:
"""
Search the contacts directory by name, role, team, or notes.
Args:
query: Search keywords (e.g. a name, team, or role).
limit: Max results to return (default 10).
"""
user = _current_user()
results = memory_store.recall_person(user["id"], query, limit)
@@ -1185,15 +1065,10 @@ def memory_list_people() -> str:
@mcp.tool()
def memory_link_ai(username: str, bigmind_user: str, bigmind_url: str = None) -> str:
def memory_link_ai(username: Annotated[str, Field(description="The contact's username in your directory.")], bigmind_user: Annotated[str, Field(description="Their BigMind username.")], bigmind_url: Annotated[str | None, Field(description="URL of their BigMind profile page (optional).")] = None) -> str:
"""
Link a contact to their BigMind AI instance.
The contact must already exist (use memory_remember_person first).
Args:
username: The contact's username in your directory.
bigmind_user: Their BigMind username.
bigmind_url: URL of their BigMind profile page (optional).
"""
user = _current_user()
found = memory_store.link_ai(user["id"], username, bigmind_user, bigmind_url)