Skip to content

Commit 8730067

Browse files
phernandezclaude
andauthored
feat: Feature/517 local mcp cloud mode (#522)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e14ba92 commit 8730067

12 files changed

Lines changed: 876 additions & 181 deletions

File tree

CLAUDE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,26 @@ Basic Memory now supports cloud synchronization and storage (requires active sub
310310
- Background relation resolution (non-blocking startup)
311311
- API performance optimizations (SPEC-11)
312312

313+
**CLI Routing Flags:**
314+
315+
When cloud mode is enabled, CLI commands route to the cloud API by default. Use `--local` and `--cloud` flags to override:
316+
317+
```bash
318+
# Force local routing (ignore cloud mode)
319+
basic-memory status --local
320+
basic-memory project list --local
321+
322+
# Force cloud routing (when cloud mode is disabled)
323+
basic-memory status --cloud
324+
basic-memory project info my-project --cloud
325+
```
326+
327+
Key behaviors:
328+
- The local MCP server (`basic-memory mcp`) automatically uses local routing
329+
- This allows simultaneous use of local Claude Desktop and cloud-based clients
330+
- Some commands (like `project default`, `project sync-config`, `project move`) require `--local` in cloud mode since they modify local configuration
331+
- Environment variable `BASIC_MEMORY_FORCE_LOCAL=true` forces local routing globally
332+
313333
## AI-Human Collaborative Development
314334

315335
Basic Memory emerged from and enables a new kind of development process that combines human and AI capabilities. Instead

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,22 @@ basic-memory cloud check
357357
basic-memory cloud mount
358358
```
359359

360+
**Routing Flags** (for users with cloud subscriptions):
361+
362+
When cloud mode is enabled, CLI commands communicate with the cloud API by default. Use routing flags to override this:
363+
364+
```bash
365+
# Force local routing (useful for local MCP server while cloud mode is enabled)
366+
basic-memory status --local
367+
basic-memory project list --local
368+
369+
# Force cloud routing (when cloud mode is disabled but you want cloud access)
370+
basic-memory status --cloud
371+
basic-memory project info my-project --cloud
372+
```
373+
374+
The local MCP server (`basic-memory mcp`) automatically uses local routing, so you can use both local Claude Desktop and cloud-based clients simultaneously.
375+
360376
4. In Claude Desktop, the LLM can now use these tools:
361377

362378
**Content Management:**
@@ -433,6 +449,7 @@ Basic Memory uses [Loguru](https://github.com/Delgan/loguru) for logging. The lo
433449
|----------|---------|-------------|
434450
| `BASIC_MEMORY_LOG_LEVEL` | `INFO` | Log level: DEBUG, INFO, WARNING, ERROR |
435451
| `BASIC_MEMORY_CLOUD_MODE` | `false` | When `true`, API logs to stdout with structured context |
452+
| `BASIC_MEMORY_FORCE_LOCAL` | `false` | When `true`, forces local API routing (ignores cloud mode) |
436453
| `BASIC_MEMORY_ENV` | `dev` | Set to `test` for test mode (stderr only) |
437454

438455
### Examples

src/basic_memory/cli/commands/mcp.py

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,60 +17,64 @@
1717
import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover
1818
from loguru import logger
1919

20-
config = ConfigManager().config
21-
22-
if not config.cloud_mode_enabled:
23-
24-
@app.command()
25-
def mcp(
26-
transport: str = typer.Option(
27-
"stdio", help="Transport type: stdio, streamable-http, or sse"
28-
),
29-
host: str = typer.Option(
30-
"0.0.0.0", help="Host for HTTP transports (use 0.0.0.0 to allow external connections)"
31-
),
32-
port: int = typer.Option(8000, help="Port for HTTP transports"),
33-
path: str = typer.Option("/mcp", help="Path prefix for streamable-http transport"),
34-
project: Optional[str] = typer.Option(None, help="Restrict MCP server to single project"),
35-
): # pragma: no cover
36-
"""Run the MCP server with configurable transport options.
37-
38-
This command starts an MCP server using one of three transport options:
39-
40-
- stdio: Standard I/O (good for local usage)
41-
- streamable-http: Recommended for web deployments (default)
42-
- sse: Server-Sent Events (for compatibility with existing clients)
43-
44-
Initialization, file sync, and cleanup are handled by the MCP server's lifespan.
45-
"""
46-
# Initialize logging for MCP (file only, stdout breaks protocol)
47-
init_mcp_logging()
48-
49-
# Validate and set project constraint if specified
50-
if project:
51-
config_manager = ConfigManager()
52-
project_name, _ = config_manager.get_project(project)
53-
if not project_name:
54-
typer.echo(f"No project found named: {project}", err=True)
55-
raise typer.Exit(1)
56-
57-
# Set env var with validated project name
58-
os.environ["BASIC_MEMORY_MCP_PROJECT"] = project_name
59-
logger.info(f"MCP server constrained to project: {project_name}")
60-
61-
# Run the MCP server (blocks)
62-
# Lifespan handles: initialization, migrations, file sync, cleanup
63-
logger.info(f"Starting MCP server with {transport.upper()} transport")
64-
65-
if transport == "stdio":
66-
mcp_server.run(
67-
transport=transport,
68-
)
69-
elif transport == "streamable-http" or transport == "sse":
70-
mcp_server.run(
71-
transport=transport,
72-
host=host,
73-
port=port,
74-
path=path,
75-
log_level="INFO",
76-
)
20+
21+
@app.command()
22+
def mcp(
23+
transport: str = typer.Option("stdio", help="Transport type: stdio, streamable-http, or sse"),
24+
host: str = typer.Option(
25+
"0.0.0.0", help="Host for HTTP transports (use 0.0.0.0 to allow external connections)"
26+
),
27+
port: int = typer.Option(8000, help="Port for HTTP transports"),
28+
path: str = typer.Option("/mcp", help="Path prefix for streamable-http transport"),
29+
project: Optional[str] = typer.Option(None, help="Restrict MCP server to single project"),
30+
): # pragma: no cover
31+
"""Run the MCP server with configurable transport options.
32+
33+
This command starts an MCP server using one of three transport options:
34+
35+
- stdio: Standard I/O (good for local usage)
36+
- streamable-http: Recommended for web deployments (default)
37+
- sse: Server-Sent Events (for compatibility with existing clients)
38+
39+
Initialization, file sync, and cleanup are handled by the MCP server's lifespan.
40+
41+
Note: This command is available regardless of cloud mode setting.
42+
Users who have cloud mode enabled can still use local MCP for Claude Code
43+
and Claude Desktop while using cloud MCP for web and mobile access.
44+
"""
45+
# Force local routing for local MCP server
46+
# Why: The local MCP server should always talk to the local API, not the cloud proxy.
47+
# Even when cloud_mode_enabled is True, stdio MCP runs locally and needs local API access.
48+
os.environ["BASIC_MEMORY_FORCE_LOCAL"] = "true"
49+
50+
# Initialize logging for MCP (file only, stdout breaks protocol)
51+
init_mcp_logging()
52+
53+
# Validate and set project constraint if specified
54+
if project:
55+
config_manager = ConfigManager()
56+
project_name, _ = config_manager.get_project(project)
57+
if not project_name:
58+
typer.echo(f"No project found named: {project}", err=True)
59+
raise typer.Exit(1)
60+
61+
# Set env var with validated project name
62+
os.environ["BASIC_MEMORY_MCP_PROJECT"] = project_name
63+
logger.info(f"MCP server constrained to project: {project_name}")
64+
65+
# Run the MCP server (blocks)
66+
# Lifespan handles: initialization, migrations, file sync, cleanup
67+
logger.info(f"Starting MCP server with {transport.upper()} transport")
68+
69+
if transport == "stdio":
70+
mcp_server.run(
71+
transport=transport,
72+
)
73+
elif transport == "streamable-http" or transport == "sse":
74+
mcp_server.run(
75+
transport=transport,
76+
host=host,
77+
port=port,
78+
path=path,
79+
log_level="INFO",
80+
)

0 commit comments

Comments
 (0)