@@ -54,12 +54,14 @@ def get_current_api_key() -> str | None:
5454 return _current_api_key .get ()
5555
5656
57- def create_mcp_server (memory : MemoryEngine ) -> FastMCP :
57+ def create_mcp_server (memory : MemoryEngine , multi_bank : bool = True ) -> FastMCP :
5858 """
5959 Create and configure the Hindsight MCP server.
6060
6161 Args:
6262 memory: MemoryEngine instance (required)
63+ multi_bank: If True, expose all tools with bank_id parameters (default).
64+ If False, only expose bank-scoped tools without bank_id parameters.
6365
6466 Returns:
6567 Configured FastMCP server instance with stateless_http enabled
@@ -71,8 +73,8 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
7173 config = MCPToolsConfig (
7274 bank_id_resolver = get_current_bank_id ,
7375 api_key_resolver = get_current_api_key , # Propagate API key for tenant auth
74- include_bank_id_param = True , # HTTP MCP supports multi-bank via parameter
75- tools = None , # All tools
76+ include_bank_id_param = multi_bank ,
77+ tools = None if multi_bank else { "retain" , "recall" , "reflect" }, # Scoped tools for single-bank mode
7678 retain_fire_and_forget = False , # HTTP MCP supports sync/async modes
7779 )
7880
@@ -88,20 +90,33 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
8890
8991
9092class MCPMiddleware :
91- """ASGI middleware that handles authentication and extracts bank_id from header or path .
93+ """ASGI middleware that handles authentication and routes to appropriate MCP server .
9294
9395 Authentication:
9496 1. If HINDSIGHT_API_MCP_AUTH_TOKEN is set (legacy), validates against that token
9597 2. Otherwise, uses TenantExtension.authenticate_mcp() from the MemoryEngine
9698 - DefaultTenantExtension: no auth required (local dev)
9799 - ApiKeyTenantExtension: validates against env var
98100
99- Bank ID can be provided via:
100- 1. X-Bank-Id header (recommended for Claude Code)
101- 2. URL path: /mcp/{bank_id}/
102- 3. Environment variable HINDSIGHT_MCP_BANK_ID (fallback default)
101+ Two modes based on URL structure:
103102
104- For Claude Code, configure with:
103+ 1. Multi-bank mode (for /mcp/ root endpoint):
104+ - Exposes all tools: retain, recall, reflect, list_banks, create_bank
105+ - All tools include optional bank_id parameter for cross-bank operations
106+ - Bank ID from: X-Bank-Id header or HINDSIGHT_MCP_BANK_ID env var
107+
108+ 2. Single-bank mode (for /mcp/{bank_id}/ endpoints):
109+ - Exposes bank-scoped tools only: retain, recall, reflect
110+ - No bank_id parameter (comes from URL)
111+ - No bank management tools (list_banks, create_bank)
112+ - Recommended for agent isolation
113+
114+ Examples:
115+ # Single-bank mode (recommended for agent isolation)
116+ claude mcp add --transport http my-agent http://localhost:8888/mcp/my-agent-bank/ \\
117+ --header "Authorization: Bearer <token>"
118+
119+ # Multi-bank mode (for cross-bank operations)
105120 claude mcp add --transport http hindsight http://localhost:8888/mcp \\
106121 --header "X-Bank-Id: my-bank" --header "Authorization: Bearer <token>"
107122 """
@@ -110,10 +125,23 @@ def __init__(self, app, memory: MemoryEngine):
110125 self .app = app
111126 self .memory = memory
112127 self .tenant_extension = memory ._tenant_extension
113- self .mcp_server = create_mcp_server (memory )
114- self .mcp_app = self .mcp_server .http_app (path = "/" )
115- # Expose the lifespan for the parent app to chain
116- self .lifespan = self .mcp_app .lifespan_handler if hasattr (self .mcp_app , "lifespan_handler" ) else None
128+
129+ # Create two server instances:
130+ # 1. Multi-bank server (for /mcp/ root endpoint)
131+ self .multi_bank_server = create_mcp_server (memory , multi_bank = True )
132+ self .multi_bank_app = self .multi_bank_server .http_app (path = "/" )
133+
134+ # 2. Single-bank server (for /mcp/{bank_id}/ endpoints)
135+ self .single_bank_server = create_mcp_server (memory , multi_bank = False )
136+ self .single_bank_app = self .single_bank_server .http_app (path = "/" )
137+
138+ # Backward compatibility: expose multi_bank_app as mcp_app
139+ self .mcp_app = self .multi_bank_app
140+
141+ # Expose the lifespan for the parent app to chain (use multi-bank as default)
142+ self .lifespan = (
143+ self .multi_bank_app .lifespan_handler if hasattr (self .multi_bank_app , "lifespan_handler" ) else None
144+ )
117145
118146 def _get_header (self , scope : dict , name : str ) -> str | None :
119147 """Extract a header value from ASGI scope."""
@@ -125,7 +153,7 @@ def _get_header(self, scope: dict, name: str) -> str | None:
125153
126154 async def __call__ (self , scope , receive , send ):
127155 if scope ["type" ] != "http" :
128- await self .mcp_app (scope , receive , send )
156+ await self .multi_bank_app (scope , receive , send )
129157 return
130158
131159 # Extract auth token from header (for tenant auth propagation)
@@ -173,8 +201,13 @@ async def __call__(self, scope, receive, send):
173201 elif path == "/mcp" :
174202 path = "/"
175203
204+ # Ensure path has leading slash (needed after stripping mount path)
205+ if path and not path .startswith ("/" ):
206+ path = "/" + path
207+
176208 # Try to get bank_id from header first (for Claude Code compatibility)
177209 bank_id = self ._get_header (scope , "X-Bank-Id" )
210+ bank_id_from_path = False
178211
179212 # MCP endpoint paths that should not be treated as bank_ids
180213 MCP_ENDPOINTS = {"sse" , "messages" }
@@ -187,13 +220,19 @@ async def __call__(self, scope, receive, send):
187220 if parts [0 ] and parts [0 ] not in MCP_ENDPOINTS :
188221 # First segment looks like a bank_id
189222 bank_id = parts [0 ]
223+ bank_id_from_path = True
190224 new_path = "/" + parts [1 ] if len (parts ) > 1 else "/"
191225
192226 # Fall back to default bank_id
193227 if not bank_id :
194228 bank_id = DEFAULT_BANK_ID
195229 logger .debug (f"Using default bank_id: { bank_id } " )
196230
231+ # Select the appropriate MCP app based on how bank_id was provided:
232+ # - Path-based bank_id → single-bank app (no bank_id param, scoped tools)
233+ # - Header/env bank_id → multi-bank app (bank_id param, all tools)
234+ target_app = self .single_bank_app if bank_id_from_path else self .multi_bank_app
235+
197236 # Set bank_id and api_key context
198237 bank_id_token = _current_bank_id .set (bank_id )
199238 # Store the auth token for tenant extension to validate
@@ -206,15 +245,15 @@ async def __call__(self, scope, receive, send):
206245
207246 # Wrap send to rewrite the SSE endpoint URL to include bank_id if using path-based routing
208247 async def send_wrapper (message ):
209- if message ["type" ] == "http.response.body" :
248+ if message ["type" ] == "http.response.body" and bank_id_from_path :
210249 body = message .get ("body" , b"" )
211250 if body and b"/messages" in body :
212251 # Rewrite /messages to /{bank_id}/messages in SSE endpoint event
213252 body = body .replace (b"data: /messages" , f"data: /{ bank_id } /messages" .encode ())
214253 message = {** message , "body" : body }
215254 await send (message )
216255
217- await self . mcp_app (new_scope , receive , send_wrapper )
256+ await target_app (new_scope , receive , send_wrapper )
218257 finally :
219258 _current_bank_id .reset (bank_id_token )
220259 if api_key_token is not None :
@@ -242,15 +281,23 @@ async def _send_error(self, send, status: int, message: str):
242281
243282def create_mcp_app (memory : MemoryEngine ):
244283 """
245- Create an ASGI app that handles MCP requests.
284+ Create an ASGI app that handles MCP requests with dynamic tool exposure .
246285
247286 Authentication:
248287 Uses the TenantExtension from the MemoryEngine (same auth as REST API).
249288
250- Bank ID can be provided via:
251- 1. X-Bank-Id header: claude mcp add --transport http hindsight http://localhost:8888/mcp --header "X-Bank-Id: my-bank"
252- 2. URL path: /mcp/{bank_id}/
253- 3. Environment variable HINDSIGHT_MCP_BANK_ID (fallback, default: "default")
289+ Two modes based on URL structure:
290+
291+ 1. Single-bank mode (recommended for agent isolation):
292+ - URL: /mcp/{bank_id}/
293+ - Tools: retain, recall, reflect (no bank_id parameter)
294+ - Example: claude mcp add --transport http my-agent http://localhost:8888/mcp/my-agent-bank/
295+
296+ 2. Multi-bank mode (for cross-bank operations):
297+ - URL: /mcp/
298+ - Tools: retain, recall, reflect, list_banks, create_bank (all with bank_id parameter)
299+ - Bank ID from: X-Bank-Id header or HINDSIGHT_MCP_BANK_ID env var (default: "default")
300+ - Example: claude mcp add --transport http hindsight http://localhost:8888/mcp --header "X-Bank-Id: my-bank"
254301
255302 Args:
256303 memory: MemoryEngine instance
0 commit comments