Skip to content

Commit b9247ed

Browse files
committed
fix: lifecyle handling during startup of mcp, other cli services
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent ca5fa55 commit b9247ed

File tree

20 files changed

+314
-461
lines changed

20 files changed

+314
-461
lines changed

src/basic_memory/api/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
async def lifespan(app: FastAPI): # pragma: no cover
1717
"""Lifecycle manager for the FastAPI app."""
1818
# Initialize database and file sync services
19-
sync_service, watch_service, watch_task = await initialize_app(project_config)
19+
watch_task = await initialize_app(project_config)
2020

2121
# proceed with startup
2222
yield

src/basic_memory/cli/app.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ def version_callback(value: bool) -> None:
77
"""Show version and exit."""
88
if value: # pragma: no cover
99
import basic_memory
10+
from basic_memory.config import config
1011

1112
typer.echo(f"Basic Memory version: {basic_memory.__version__}")
13+
typer.echo(f"Current project: {config.project}")
14+
typer.echo(f"Project path: {config.home}")
1215
raise typer.Exit()
1316

1417

@@ -17,11 +20,12 @@ def version_callback(value: bool) -> None:
1720

1821
@app.callback()
1922
def app_callback(
23+
ctx: typer.Context,
2024
project: Optional[str] = typer.Option(
2125
None,
2226
"--project",
2327
"-p",
24-
help="Specify which project to use",
28+
help="Specify which project to use 1",
2529
envvar="BASIC_MEMORY_PROJECT",
2630
),
2731
version: Optional[bool] = typer.Option(
@@ -34,6 +38,7 @@ def app_callback(
3438
),
3539
) -> None:
3640
"""Basic Memory - Local-first personal knowledge management."""
41+
3742
# We use the project option to set the BASIC_MEMORY_PROJECT environment variable
3843
# The config module will pick this up when loading
3944
if project: # pragma: no cover
@@ -53,6 +58,13 @@ def app_callback(
5358

5459
config = new_config
5560

61+
# Run migrations for every command unless --version was specified
62+
if not version and ctx.invoked_subcommand is not None:
63+
from basic_memory.config import config
64+
from basic_memory.services.initialization import ensure_initialize_database
65+
66+
ensure_initialize_database(config)
67+
5668

5769
# Register sub-command groups
5870
import_app = typer.Typer(help="Import data from various sources")
Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
"""MCP server command."""
22

3-
import asyncio
4-
from loguru import logger
5-
63
import basic_memory
74
from basic_memory.cli.app import app
8-
from basic_memory.config import config, config_manager
9-
from basic_memory.services.initialization import initialize_app
105

116
# Import mcp instance
127
from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover
@@ -17,31 +12,24 @@
1712

1813
@app.command()
1914
def mcp(): # pragma: no cover
20-
"""Run the MCP server for Claude Desktop integration."""
21-
home_dir = config.home
22-
project_name = config.project
15+
"""Run the MCP server"""
16+
from basic_memory.config import config
17+
import asyncio
18+
from basic_memory.services.initialization import initialize_database
19+
20+
# First, run just the database migrations synchronously
21+
asyncio.run(initialize_database(config))
22+
23+
# Load config to check if sync is enabled
24+
from basic_memory.config import config_manager
2325

24-
# app config
2526
basic_memory_config = config_manager.load_config()
2627

27-
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
28-
logger.info(f"Project: {project_name}")
29-
logger.info(f"Project directory: {home_dir}")
30-
31-
# Run initialization before starting MCP server
32-
# Note: Although we already run migrations via ensure_migrations in the CLI callback,
33-
# we do a full initialization here to ensure file sync is properly set up too
34-
try:
35-
loop = asyncio.new_event_loop()
36-
asyncio.set_event_loop(loop)
37-
loop.run_until_complete(initialize_app(config))
38-
except Exception as e:
39-
logger.error(f"Error during app initialization: {e}")
40-
logger.info("Continuing with server startup despite initialization error")
41-
42-
logger.info(f"Sync changes enabled: {basic_memory_config.sync_changes}")
43-
logger.info(
44-
f"Update permalinks on move enabled: {basic_memory_config.update_permalinks_on_move}"
45-
)
28+
if basic_memory_config.sync_changes:
29+
# For now, we'll just log that sync will be handled by the MCP server
30+
from loguru import logger
31+
32+
logger.info("File sync will be handled by the MCP server")
4633

34+
# Start the MCP server
4735
mcp_server.run()

src/basic_memory/cli/commands/sync.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,14 @@ async def run_sync(verbose: bool = False, watch: bool = False, console_status: b
179179
)
180180

181181
# full sync - no progress bars in watch mode
182-
await sync_service.sync(config.home, show_progress=False)
182+
await sync_service.sync(config.home)
183183

184184
# watch changes
185185
await watch_service.run() # pragma: no cover
186186
else:
187-
# one time sync - use progress bars for better UX
187+
# one time sync
188188
logger.info("Running one-time sync")
189-
knowledge_changes = await sync_service.sync(config.home, show_progress=True)
189+
knowledge_changes = await sync_service.sync(config.home)
190190

191191
# Log results
192192
duration_ms = int((time.time() - start_time) * 1000)
@@ -237,11 +237,11 @@ def sync(
237237
if not isinstance(e, typer.Exit):
238238
logger.exception(
239239
"Sync command failed",
240-
project=config.project,
241-
error=str(e),
242-
error_type=type(e).__name__,
243-
watch_mode=watch,
244-
directory=str(config.home),
240+
f"project={config.project},"
241+
f"error={str(e)},"
242+
f"error_type={type(e).__name__},"
243+
f"watch_mode={watch},"
244+
f"directory={str(config.home)}",
245245
)
246246
typer.echo(f"Error during sync: {e}", err=True)
247247
raise typer.Exit(1)

src/basic_memory/cli/main.py

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
"""Main CLI entry point for basic-memory.""" # pragma: no cover
22

3-
import asyncio
4-
5-
import typer
6-
from loguru import logger
7-
83
from basic_memory.cli.app import app # pragma: no cover
94

105
# Register commands
@@ -23,55 +18,8 @@
2318
from basic_memory.config import config
2419
from basic_memory.services.initialization import ensure_initialization
2520

26-
27-
# Helper function to run database migrations
28-
def ensure_migrations(): # pragma: no cover
29-
"""Ensure database migrations are run before executing commands."""
30-
ensure_initialization(config)
31-
32-
33-
# Version command
34-
@app.callback(invoke_without_command=True)
35-
def main(
36-
ctx: typer.Context,
37-
project: str = typer.Option( # noqa
38-
"main",
39-
"--project",
40-
"-p",
41-
help="Specify which project to use",
42-
envvar="BASIC_MEMORY_PROJECT",
43-
),
44-
version: bool = typer.Option(
45-
False,
46-
"--version",
47-
"-V",
48-
help="Show version information and exit.",
49-
is_eager=True,
50-
),
51-
):
52-
"""Basic Memory - Local-first personal knowledge management system."""
53-
if version: # pragma: no cover
54-
from basic_memory import __version__
55-
from basic_memory.config import config
56-
57-
typer.echo(f"Basic Memory v{__version__}")
58-
typer.echo(f"Current project: {config.project}")
59-
typer.echo(f"Project path: {config.home}")
60-
raise typer.Exit()
61-
62-
# Handle project selection via environment variable
63-
if project:
64-
import os
65-
66-
os.environ["BASIC_MEMORY_PROJECT"] = project
67-
68-
# Run migrations for every command unless --version was specified
69-
if not version and ctx.invoked_subcommand is not None:
70-
ensure_migrations()
71-
72-
7321
if __name__ == "__main__": # pragma: no cover
74-
# Run initialization
22+
# Run initialization if we are starting as a module
7523
ensure_initialization(config)
7624

7725
# start the app

src/basic_memory/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ProjectConfig(BaseSettings):
3535

3636
# Watch service configuration
3737
sync_delay: int = Field(
38-
default=500, description="Milliseconds to wait after changes before syncing", gt=0
38+
default=1000, description="Milliseconds to wait after changes before syncing", gt=0
3939
)
4040

4141
# update permalinks on move
@@ -274,7 +274,7 @@ def setup_basic_memory_logging(): # pragma: no cover
274274
console=False,
275275
)
276276

277-
logger.info(f"Starting Basic Memory {basic_memory.__version__} (Project: {config.project})")
277+
logger.info(f"Basic Memory {basic_memory.__version__} (Project: {config.project})")
278278
_LOGGING_SETUP = True
279279

280280

src/basic_memory/mcp/server.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
11
"""Enhanced FastMCP server instance for Basic Memory."""
22

3+
import asyncio
4+
from contextlib import asynccontextmanager
5+
from typing import AsyncIterator, Optional
6+
37
from mcp.server.fastmcp import FastMCP
4-
from mcp.server.fastmcp.utilities.logging import configure_logging
8+
from mcp.server.fastmcp.utilities.logging import configure_logging as mcp_configure_logging
9+
from dataclasses import dataclass
10+
11+
from basic_memory.config import config as project_config
12+
from basic_memory.services.initialization import initialize_app
513

614
# mcp console logging
7-
configure_logging(level="ERROR")
15+
mcp_configure_logging(level="ERROR")
16+
17+
18+
@dataclass
19+
class AppContext:
20+
watch_task: Optional[asyncio.Task]
21+
22+
23+
@asynccontextmanager
24+
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma: no cover
25+
"""Manage application lifecycle with type-safe context"""
26+
# Initialize on startup
27+
watch_task = await initialize_app(project_config)
28+
try:
29+
yield AppContext(watch_task=watch_task)
30+
finally:
31+
# Cleanup on shutdown
32+
if watch_task:
33+
watch_task.cancel()
834

935

1036
# Create the shared server instance
11-
mcp = FastMCP("Basic Memory", log_level="ERROR")
37+
mcp = FastMCP("Basic Memory", log_level="ERROR", lifespan=app_lifespan)

src/basic_memory/repository/repository.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,6 @@ async def find_by_ids(self, ids: List[int]) -> Sequence[T]:
137137

138138
async def find_one(self, query: Select[tuple[T]]) -> Optional[T]:
139139
"""Execute a query and retrieve a single record."""
140-
logger.debug(f"Finding one {self.Model.__name__} with query: {query}")
141-
142140
# add in load options
143141
query = query.options(*self.get_load_options())
144142
result = await self.execute_query(query)
@@ -270,11 +268,9 @@ async def execute_query(self, query: Executable, use_query_options: bool = True)
270268
"""Execute a query asynchronously."""
271269

272270
query = query.options(*self.get_load_options()) if use_query_options else query
273-
274271
logger.debug(f"Executing query: {query}")
275272
async with db.scoped_session(self.session_maker) as session:
276273
result = await session.execute(query)
277-
logger.debug("Query executed successfully")
278274
return result
279275

280276
def get_load_options(self) -> List[LoaderOption]:

src/basic_memory/services/file_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ async def read_entity_content(self, entity: EntityModel) -> str:
6060
Returns:
6161
Raw content string without metadata sections
6262
"""
63-
logger.debug("Reading entity content", entity_id=entity.id, permalink=entity.permalink)
63+
logger.debug(f"Reading entity content, entity_id={entity.id}, permalink={entity.permalink}")
6464

6565
file_path = self.get_entity_path(entity)
6666
markdown = await self.markdown_processor.read_file(file_path)

0 commit comments

Comments
 (0)