|
1 | | -"""Hindsight MCP Server implementation using FastMCP.""" |
| 1 | +"""Hindsight MCP Server implementation using FastMCP (HTTP transport).""" |
2 | 2 |
|
3 | 3 | import json |
4 | 4 | import logging |
|
8 | 8 | from fastmcp import FastMCP |
9 | 9 |
|
10 | 10 | 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 |
13 | 12 |
|
14 | 13 | # Configure logging from HINDSIGHT_API_LOG_LEVEL environment variable |
15 | 14 | _log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower() |
@@ -52,194 +51,15 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP: |
52 | 51 | # Use stateless_http=True for Claude Code compatibility |
53 | 52 | mcp = FastMCP("hindsight-mcp-server", stateless_http=True) |
54 | 53 |
|
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) |
243 | 63 |
|
244 | 64 | return mcp |
245 | 65 |
|
|
0 commit comments