Skip to content

Commit b378f68

Browse files
authored
feat(mcp): add timestamp to retain (#190)
* feat(mcp): add timestamp to retain * ci
1 parent 9c2df9d commit b378f68

File tree

9 files changed

+644
-299
lines changed

9 files changed

+644
-299
lines changed

hindsight-api/hindsight_api/api/mcp.py

Lines changed: 11 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Hindsight MCP Server implementation using FastMCP."""
1+
"""Hindsight MCP Server implementation using FastMCP (HTTP transport)."""
22

33
import json
44
import logging
@@ -8,8 +8,7 @@
88
from fastmcp import FastMCP
99

1010
from hindsight_api import MemoryEngine
11-
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
12-
from hindsight_api.models import RequestContext
11+
from hindsight_api.mcp_tools import MCPToolsConfig, register_mcp_tools
1312

1413
# Configure logging from HINDSIGHT_API_LOG_LEVEL environment variable
1514
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
@@ -52,194 +51,15 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
5251
# Use stateless_http=True for Claude Code compatibility
5352
mcp = FastMCP("hindsight-mcp-server", stateless_http=True)
5453

55-
@mcp.tool()
56-
async def retain(
57-
content: str,
58-
context: str = "general",
59-
async_processing: bool = True,
60-
bank_id: str | None = None,
61-
) -> str:
62-
"""
63-
Store important information to long-term memory.
64-
65-
Use this tool PROACTIVELY whenever the user shares:
66-
- Personal facts, preferences, or interests
67-
- Important events or milestones
68-
- User history, experiences, or background
69-
- Decisions, opinions, or stated preferences
70-
- Goals, plans, or future intentions
71-
- Relationships or people mentioned
72-
- Work context, projects, or responsibilities
73-
74-
Args:
75-
content: The fact/memory to store (be specific and include relevant details)
76-
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
77-
async_processing: If True, queue for background processing and return immediately. If False, wait for completion. Default: True
78-
bank_id: Optional bank to store in (defaults to session bank). Use for cross-bank operations.
79-
"""
80-
try:
81-
target_bank = bank_id or get_current_bank_id()
82-
if target_bank is None:
83-
return "Error: No bank_id configured"
84-
contents = [{"content": content, "context": context}]
85-
if async_processing:
86-
# Queue for background processing and return immediately
87-
result = await memory.submit_async_retain(
88-
bank_id=target_bank, contents=contents, request_context=RequestContext()
89-
)
90-
return f"Memory queued for background processing (operation_id: {result.get('operation_id', 'N/A')})"
91-
else:
92-
# Wait for completion
93-
await memory.retain_batch_async(
94-
bank_id=target_bank,
95-
contents=contents,
96-
request_context=RequestContext(),
97-
)
98-
return f"Memory stored successfully in bank '{target_bank}'"
99-
except Exception as e:
100-
logger.error(f"Error storing memory: {e}", exc_info=True)
101-
return f"Error: {str(e)}"
102-
103-
@mcp.tool()
104-
async def recall(query: str, max_tokens: int = 4096, bank_id: str | None = None) -> str:
105-
"""
106-
Search memories to provide personalized, context-aware responses.
107-
108-
Use this tool PROACTIVELY to:
109-
- Check user's preferences before making suggestions
110-
- Recall user's history to provide continuity
111-
- Remember user's goals and context
112-
- Personalize responses based on past interactions
113-
114-
Args:
115-
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
116-
max_tokens: Maximum tokens in the response (default: 4096)
117-
bank_id: Optional bank to search in (defaults to session bank). Use for cross-bank operations.
118-
"""
119-
try:
120-
target_bank = bank_id or get_current_bank_id()
121-
if target_bank is None:
122-
return "Error: No bank_id configured"
123-
from hindsight_api.engine.memory_engine import Budget
124-
125-
recall_result = await memory.recall_async(
126-
bank_id=target_bank,
127-
query=query,
128-
fact_type=list(VALID_RECALL_FACT_TYPES),
129-
budget=Budget.HIGH,
130-
max_tokens=max_tokens,
131-
request_context=RequestContext(),
132-
)
133-
134-
# Use model's JSON serialization
135-
return recall_result.model_dump_json(indent=2)
136-
except Exception as e:
137-
logger.error(f"Error searching: {e}", exc_info=True)
138-
return f'{{"error": "{e}", "results": []}}'
139-
140-
@mcp.tool()
141-
async def reflect(query: str, context: str | None = None, budget: str = "low", bank_id: str | None = None) -> str:
142-
"""
143-
Generate thoughtful analysis by synthesizing stored memories with the bank's personality.
144-
145-
WHEN TO USE THIS TOOL:
146-
Use reflect when you need reasoned analysis, not just fact retrieval. This tool
147-
thinks through the question using everything the bank knows and its personality traits.
148-
149-
EXAMPLES OF GOOD QUERIES:
150-
- "What patterns have emerged in how I approach debugging?"
151-
- "Based on my past decisions, what architectural style do I prefer?"
152-
- "What might be the best approach for this problem given what you know about me?"
153-
- "How should I prioritize these tasks based on my goals?"
154-
155-
HOW IT DIFFERS FROM RECALL:
156-
- recall: Returns raw facts matching your search (fast lookup)
157-
- reflect: Reasons across memories to form a synthesized answer (deeper analysis)
158-
159-
Use recall for "what did I say about X?" and reflect for "what should I do about X?"
160-
161-
Args:
162-
query: The question or topic to reflect on
163-
context: Optional context about why this reflection is needed
164-
budget: Search budget - 'low', 'mid', or 'high' (default: 'low')
165-
bank_id: Optional bank to reflect in (defaults to session bank). Use for cross-bank operations.
166-
"""
167-
try:
168-
target_bank = bank_id or get_current_bank_id()
169-
if target_bank is None:
170-
return "Error: No bank_id configured"
171-
from hindsight_api.engine.memory_engine import Budget
172-
173-
# Map string budget to enum
174-
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
175-
budget_enum = budget_map.get(budget.lower(), Budget.LOW)
176-
177-
reflect_result = await memory.reflect_async(
178-
bank_id=target_bank,
179-
query=query,
180-
budget=budget_enum,
181-
context=context,
182-
request_context=RequestContext(),
183-
)
184-
185-
return reflect_result.model_dump_json(indent=2)
186-
except Exception as e:
187-
logger.error(f"Error reflecting: {e}", exc_info=True)
188-
return f'{{"error": "{e}", "text": ""}}'
189-
190-
@mcp.tool()
191-
async def list_banks() -> str:
192-
"""
193-
List all available memory banks.
194-
195-
Use this tool to discover what memory banks exist in the system.
196-
Each bank is an isolated memory store (like a separate "brain").
197-
198-
Returns:
199-
JSON list of banks with their IDs, names, dispositions, and missions.
200-
"""
201-
try:
202-
banks = await memory.list_banks(request_context=RequestContext())
203-
return json.dumps({"banks": banks}, indent=2)
204-
except Exception as e:
205-
logger.error(f"Error listing banks: {e}", exc_info=True)
206-
return f'{{"error": "{e}", "banks": []}}'
207-
208-
@mcp.tool()
209-
async def create_bank(bank_id: str, name: str | None = None, mission: str | None = None) -> str:
210-
"""
211-
Create a new memory bank or get an existing one.
212-
213-
Memory banks are isolated stores - each one is like a separate "brain" for a user/agent.
214-
Banks are auto-created with default settings if they don't exist.
215-
216-
Args:
217-
bank_id: Unique identifier for the bank (e.g., 'user-123', 'agent-alpha')
218-
name: Optional human-friendly name for the bank
219-
mission: Optional mission describing who the agent is and what they're trying to accomplish
220-
"""
221-
try:
222-
# get_bank_profile auto-creates bank if it doesn't exist
223-
profile = await memory.get_bank_profile(bank_id, request_context=RequestContext())
224-
225-
# Update name/mission if provided
226-
if name is not None or mission is not None:
227-
await memory.update_bank(
228-
bank_id,
229-
name=name,
230-
mission=mission,
231-
request_context=RequestContext(),
232-
)
233-
# Fetch updated profile
234-
profile = await memory.get_bank_profile(bank_id, request_context=RequestContext())
235-
236-
# Serialize disposition if it's a Pydantic model
237-
if "disposition" in profile and hasattr(profile["disposition"], "model_dump"):
238-
profile["disposition"] = profile["disposition"].model_dump()
239-
return json.dumps(profile, indent=2)
240-
except Exception as e:
241-
logger.error(f"Error creating bank: {e}", exc_info=True)
242-
return f'{{"error": "{e}"}}'
54+
# Configure and register tools using shared module
55+
config = MCPToolsConfig(
56+
bank_id_resolver=get_current_bank_id,
57+
include_bank_id_param=True, # HTTP MCP supports multi-bank via parameter
58+
tools=None, # All tools
59+
retain_fire_and_forget=False, # HTTP MCP supports sync/async modes
60+
)
61+
62+
register_mcp_tools(mcp, memory, config)
24363

24464
return mcp
24565

hindsight-api/hindsight_api/mcp_local.py

Lines changed: 12 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import sys
4545

4646
from mcp.server.fastmcp import FastMCP
47-
from mcp.types import Icon
4847

4948
from hindsight_api.config import (
5049
DEFAULT_MCP_LOCAL_BANK_ID,
@@ -53,6 +52,7 @@
5352
ENV_MCP_INSTRUCTIONS,
5453
ENV_MCP_LOCAL_BANK_ID,
5554
)
55+
from hindsight_api.mcp_tools import MCPToolsConfig, register_mcp_tools
5656

5757
# Configure logging - default to warning to avoid polluting stderr during MCP init
5858
# MCP clients interpret stderr output as errors, so we suppress INFO logs by default
@@ -85,9 +85,6 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
8585
"""
8686
# Import here to avoid slow startup if just checking --help
8787
from hindsight_api import MemoryEngine
88-
from hindsight_api.engine.memory_engine import Budget
89-
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
90-
from hindsight_api.models import RequestContext
9188

9289
# Create memory engine with pg0 embedded database if not provided
9390
if memory is None:
@@ -105,55 +102,17 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
105102

106103
mcp = FastMCP("hindsight")
107104

108-
@mcp.tool(description=retain_description)
109-
async def retain(content: str, context: str = "general") -> dict:
110-
"""
111-
Args:
112-
content: The fact/memory to store (be specific and include relevant details)
113-
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
114-
"""
115-
import asyncio
116-
117-
async def _retain():
118-
try:
119-
await memory.retain_batch_async(
120-
bank_id=bank_id,
121-
contents=[{"content": content, "context": context}],
122-
request_context=RequestContext(),
123-
)
124-
except Exception as e:
125-
logger.error(f"Error storing memory: {e}", exc_info=True)
126-
127-
# Fire and forget - don't block on memory storage
128-
asyncio.create_task(_retain())
129-
return {"status": "accepted", "message": "Memory storage initiated"}
130-
131-
@mcp.tool(description=recall_description)
132-
async def recall(query: str, max_tokens: int = 4096, budget: str = "low") -> dict:
133-
"""
134-
Args:
135-
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
136-
max_tokens: Maximum tokens to return in results (default: 4096)
137-
budget: Search budget level - "low", "mid", or "high" (default: "low")
138-
"""
139-
try:
140-
# Map string budget to enum
141-
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
142-
budget_enum = budget_map.get(budget.lower(), Budget.LOW)
143-
144-
search_result = await memory.recall_async(
145-
bank_id=bank_id,
146-
query=query,
147-
fact_type=list(VALID_RECALL_FACT_TYPES),
148-
budget=budget_enum,
149-
max_tokens=max_tokens,
150-
request_context=RequestContext(),
151-
)
152-
153-
return search_result.model_dump()
154-
except Exception as e:
155-
logger.error(f"Error searching: {e}", exc_info=True)
156-
return {"error": str(e), "results": []}
105+
# Configure and register tools using shared module
106+
config = MCPToolsConfig(
107+
bank_id_resolver=lambda: bank_id,
108+
include_bank_id_param=False, # Local MCP uses fixed bank_id
109+
tools={"retain", "recall"}, # Local MCP only has retain and recall
110+
retain_description=retain_description,
111+
recall_description=recall_description,
112+
retain_fire_and_forget=True, # Local MCP uses fire-and-forget pattern
113+
)
114+
115+
register_mcp_tools(mcp, memory, config)
157116

158117
return mcp
159118

0 commit comments

Comments
 (0)