feat: Merge Cloud auth (#291) · basicmachines-co/basic-memory@3a6baf8 · GitHub
Skip to content

Commit 3a6baf8

Browse files
jope-bmphernandezclaude
authored
feat: Merge Cloud auth (#291)
Signed-off-by: phernandez <paul@basicmachines.co> Signed-off-by: Joe P <joe@basicmemory.com> Co-authored-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec2fa07 commit 3a6baf8

42 files changed

Lines changed: 1237 additions & 953 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 1 addition & 1 deletion

src/basic_memory/api/routers/project_router.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515

1616
# Router for resources in a specific project
17+
# The ProjectPathDep is used in the path as a prefix, so the request path is like /{project}/project/info
1718
project_router = APIRouter(prefix="/project", tags=["project"])
1819

1920
# Router for managing project resources
@@ -29,6 +30,25 @@ async def get_project_info(
2930
return await project_service.get_project_info(project)
3031

3132

33+
@project_router.get("/item", response_model=ProjectItem)
34+
async def get_project(
35+
project_service: ProjectServiceDep,
36+
project: ProjectPathDep,
37+
) -> ProjectItem:
38+
"""Get bassic info about the specified Basic Memory project."""
39+
found_project = await project_service.get_project(project)
40+
if not found_project:
41+
raise HTTPException(
42+
status_code=404, detail=f"Project: '{project}' does not exist"
43+
) # pragma: no cover
44+
45+
return ProjectItem(
46+
name=found_project.name,
47+
path=found_project.path,
48+
is_default=found_project.is_default or False,
49+
)
50+
51+
3252
# Update a project
3353
@project_router.patch("/{name}", response_model=ProjectStatusResponse)
3454
async def update_project(
@@ -217,6 +237,27 @@ async def set_default_project(
217237
raise HTTPException(status_code=400, detail=str(e))
218238

219239

240+
# Get the default project
241+
@project_resource_router.get("/default", response_model=ProjectItem)
242+
async def get_default_project(
243+
project_service: ProjectServiceDep,
244+
) -> ProjectItem:
245+
"""Get the default project.
246+
247+
Returns:
248+
Response with project default information
249+
"""
250+
# Get the old default project
251+
default_name = project_service.default_project
252+
default_project = await project_service.get_project(default_name)
253+
if not default_project: # pragma: no cover
254+
raise HTTPException( # pragma: no cover
255+
status_code=404, detail=f"Default Project: '{default_name}' does not exist"
256+
)
257+
258+
return ProjectItem(name=default_project.name, path=default_project.path, is_default=True)
259+
260+
220261
# Synchronize projects between config and database
221262
@project_resource_router.post("/sync", response_model=ProjectStatusResponse)
222263
async def synchronize_projects(

src/basic_memory/cli/app.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import typer
44

55
from basic_memory.config import get_project_config, ConfigManager
6-
from basic_memory.mcp.project_session import session
76

87

98
def version_callback(value: bool) -> None:
@@ -28,7 +27,7 @@ def app_callback(
2827
None,
2928
"--project",
3029
"-p",
31-
help="Specify which project to use 1",
30+
help="Specify which project to use",
3231
envvar="BASIC_MEMORY_PROJECT",
3332
),
3433
version: Optional[bool] = typer.Option(
@@ -51,18 +50,11 @@ def app_callback(
5150

5251
# Initialize MCP session with the specified project or default
5352
if project: # pragma: no cover
54-
# Use the project specified via --project flag
55-
current_project_config = get_project_config(project)
56-
session.set_current_project(current_project_config.name)
57-
5853
# Update the global config to use this project
5954
from basic_memory.config import update_current_project
6055

56+
# TODO set active project via cli
6157
update_current_project(project)
62-
else:
63-
# Use the default project
64-
current_project = app_config.default_project
65-
session.set_current_project(current_project)
6658

6759

6860
# Register sub-command groups

src/basic_memory/cli/commands/mcp.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from basic_memory.cli.app import app
77
from basic_memory.config import ConfigManager
8+
from basic_memory.mcp.middleware import ProjectContextMiddleware
89

910
# Import mcp instance
1011
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
@@ -63,6 +64,9 @@ def run_file_sync():
6364
# Now run the MCP server (blocks)
6465
logger.info(f"Starting MCP server with {transport.upper()} transport")
6566

67+
# middleware to store the active project
68+
mcp_server.add_middleware(ProjectContextMiddleware())
69+
6670
if transport == "stdio":
6771
mcp_server.run(
6872
transport=transport,

src/basic_memory/cli/commands/project.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from rich.table import Table
1010

1111
from basic_memory.cli.app import app
12-
from basic_memory.mcp.project_session import session
1312
from basic_memory.mcp.resources.project_info import project_info
1413
import json
1514
from datetime import datetime
@@ -52,13 +51,11 @@ def list_projects() -> None:
5251
table = Table(title="Basic Memory Projects")
5352
table.add_column("Name", style="cyan")
5453
table.add_column("Path", style="green")
55-
table.add_column("Default", style="yellow")
56-
table.add_column("Active", style="magenta")
54+
table.add_column("Default", style="magenta")
5755

5856
for project in result.projects:
5957
is_default = "✓" if project.is_default else ""
60-
is_active = "✓" if session.get_current_project() == project.name else ""
61-
table.add_row(project.name, format_path(project.path), is_default, is_active)
58+
table.add_row(project.name, format_path(project.path), is_default)
6259

6360
console.print(table)
6461
except Exception as e:
@@ -161,9 +158,10 @@ def move_project(
161158
data = {"path": resolved_path}
162159

163160
project_permalink = generate_permalink(name)
164-
current_project = session.get_current_project()
161+
162+
# TODO fix route to use ProjectPathDep
165163
response = asyncio.run(
166-
call_patch(client, f"/{current_project}/project/{project_permalink}", json=data)
164+
call_patch(client, f"/{name}/project/{project_permalink}", json=data)
167165
)
168166
result = ProjectStatusResponse.model_validate(response.json())
169167

src/basic_memory/mcp/async_client.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1+
import os
12
from httpx import ASGITransport, AsyncClient
23
from loguru import logger
34

45
from basic_memory.api.app import app as fastapi_app
5-
from basic_memory.config import ConfigManager
66

77

88
def create_client() -> AsyncClient:
99
"""Create an HTTP client based on configuration.
1010
1111
Returns:
12-
AsyncClient configured for either local ASGI or remote HTTP transport
12+
AsyncClient configured for either local ASGI or remote proxy
1313
"""
14-
config_manager = ConfigManager()
15-
config = config_manager.load_config()
14+
proxy_base_url = os.getenv("BASIC_MEMORY_PROXY_URL", None)
15+
logger.info(f"BASIC_MEMORY_PROXY_URL: {proxy_base_url}")
1616

17-
if config.api_url:
18-
# Use HTTP transport for remote API
19-
logger.info(f"Creating HTTP client for remote Basic Memory API: {config.api_url}")
20-
return AsyncClient(base_url=config.api_url)
17+
if proxy_base_url:
18+
# Use HTTP transport to proxy endpoint
19+
logger.info(f"Creating HTTP client for proxy at: {proxy_base_url}")
20+
return AsyncClient(base_url=proxy_base_url)
2121
else:
22-
# Use ASGI transport for local API
22+
# Default: use ASGI transport for local API (development mode)
2323
logger.debug("Creating ASGI client for local Basic Memory API")
2424
return AsyncClient(transport=ASGITransport(app=fastapi_app), base_url="http://test")
2525

src/basic_memory/mcp/middleware.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from typing import Any
2+
from loguru import logger
3+
from fastmcp.server.middleware import Middleware, MiddlewareContext
4+
5+
from basic_memory.config import ConfigManager
6+
from basic_memory.schemas.project_info import ProjectItem
7+
8+
9+
class ProjectContextMiddleware(Middleware):
10+
def __init__(self):
11+
config = ConfigManager().config
12+
default_project_name = config.default_project
13+
default_project_path = config.projects[default_project_name]
14+
15+
self.default_project = ProjectItem(
16+
name=default_project_name, path=default_project_path, is_default=True
17+
)
18+
self.sessions: dict[str, ProjectItem] = {}
19+
20+
async def on_call_tool(self, context: MiddlewareContext, call_next) -> Any:
21+
# Initialize context with default project if not set
22+
if context.fastmcp_context:
23+
session_id = context.fastmcp_context.session_id
24+
project = self.sessions.get(session_id)
25+
26+
active_project = project or self.default_project
27+
28+
# store in sessions
29+
if not project:
30+
self.sessions[session_id] = active_project
31+
32+
# set active project on context
33+
context.fastmcp_context.set_state("active_project", active_project)
34+
logger.debug(
35+
f"project context set: session_id={session_id}, active_project={active_project}"
36+
)
37+
38+
# call tool
39+
result = await call_next(context)
40+
41+
# set global state for if the project changed during tool call
42+
if context.fastmcp_context:
43+
session_id = context.fastmcp_context.session_id
44+
active_project = context.fastmcp_context.get_state("active_project")
45+
project = self.sessions.get(session_id)
46+
if project != active_project:
47+
logger.debug(
48+
f"project session saved: session_id={session_id}, active_project={active_project}"
49+
)
50+
self.sessions[session_id] = active_project
51+
52+
return result
Lines changed: 99 additions & 0 deletions

0 commit comments

Comments
 (0)