Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,25 @@ The recommended way is `uvx` with `--refresh` — every restart pulls the latest
"mcpServers": {
"synapto": {
"command": "uvx",
"args": ["--refresh", "synapto", "serve"]
"args": ["--refresh", "synapto", "serve"],
"env": {
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1"
}
}
}
}
```

Set `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1` for Claude Code so Synapto remains the single memory sink instead of duplicating new memories into Claude's flat-file auto-memory.

Existing Claude Code users can upgrade their MCP config in place:

```bash
uvx --refresh synapto configure-mcp --client claude-code --yes
```

Restart Claude Code after running the command so the MCP subprocess receives the new environment.

**Cursor** (`.cursor/mcp.json`):

```json
Expand Down
18 changes: 17 additions & 1 deletion docs/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ synapto init
"mcpServers": {
"synapto": {
"command": "uvx",
"args": ["--refresh", "synapto", "serve"]
"args": ["--refresh", "synapto", "serve"],
"env": {
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1"
}
}
}
}
Expand All @@ -34,6 +37,7 @@ synapto init
"command": "uvx",
"args": ["--refresh", "synapto", "serve"],
"env": {
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1",
"SYNAPTO_DEFAULT_TENANT": "my-project"
}
}
Expand All @@ -43,6 +47,18 @@ synapto init

> **Why `--refresh`?** Without it, `uvx` reuses the cached environment across restarts, so a new Synapto release on PyPI will not be picked up until the cache expires or you run `uv cache clean synapto` manually. `--refresh` tells `uv` to re-resolve the package on every launch, adding 1–3 seconds to Claude Code's MCP startup in exchange for "always on the latest version" — the right default while Synapto is shipping fast. Drop the flag (or pin a version like `"synapto==0.2.0"`) once you want to freeze a known-good build.

> **Why `CLAUDE_CODE_DISABLE_AUTO_MEMORY`?** When Synapto is the active memory layer, this tells Claude Code to skip its flat-file auto-memory extraction so new memories do not get duplicated outside Synapto.

### Upgrading an existing MCP config

If you already use Synapto with Claude Code, upgrade your config once after installing this release:

```bash
uvx --refresh synapto configure-mcp --client claude-code --yes
```

The command preserves your existing Synapto `command` and `args`, adds `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`, and keeps any existing `SYNAPTO_DEFAULT_TENANT` unless you pass a new `--tenant`.

### Upgrading mid-session

If a new Synapto release lands while Claude Code is already running, the existing MCP subprocess keeps using the version it started with. To pick up the new release, **fully quit Claude Code (`Cmd+Q`) and relaunch** — the MCP server is a child process of Claude Code, so a window-close is not enough. With `--refresh` in place, the relaunch will pull the new version automatically.
Expand Down
107 changes: 94 additions & 13 deletions src/synapto/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
import json
import logging
from pathlib import Path

import click

Expand All @@ -14,13 +15,26 @@

# Memory migration: how many texts to embed per provider call.
EMBEDDING_BATCH_SIZE = 64
CLAUDE_CODE_DISABLE_AUTO_MEMORY_ENV = "CLAUDE_CODE_DISABLE_AUTO_MEMORY"
MCP_CLIENT_ALL = "all"
MCP_CLIENT_CLAUDE_CODE = "claude-code"
MCP_CLIENT_CURSOR = "cursor"


def _run(coro):
"""Run an async function synchronously."""
return asyncio.run(coro)


def _mcp_client_slug(client: dict) -> str:
name = client["name"].lower()
if "claude" in name:
return MCP_CLIENT_CLAUDE_CODE
if "cursor" in name:
return MCP_CLIENT_CURSOR
return name.replace(" ", "-")


@click.group()
@click.version_option(version=__version__, prog_name="synapto")
@click.option("--verbose", "-v", is_flag=True, help="enable debug logging")
Expand Down Expand Up @@ -144,6 +158,48 @@ def serve() -> None:
mcp.run(show_banner=False)


@main.command(name="configure-mcp")
@click.option(
"--client",
type=click.Choice([MCP_CLIENT_ALL, MCP_CLIENT_CLAUDE_CODE, MCP_CLIENT_CURSOR], case_sensitive=False),
default=MCP_CLIENT_ALL,
show_default=True,
help="MCP client config to update",
)
@click.option("--tenant", default=None, help="set or update SYNAPTO_DEFAULT_TENANT")
@click.option("--yes", "-y", is_flag=True, help="update detected MCP configs without prompting")
@click.option("--home", default=None, type=click.Path(exists=True), hidden=True)
def configure_mcp(client: str, tenant: str | None, yes: bool, home: str | None) -> None:
"""Configure detected MCP clients for Synapto."""
clients = _detect_mcp_clients(home=Path(home) if home else None)
target = client.lower()
selected = [
c for c in clients
if target == MCP_CLIENT_ALL or _mcp_client_slug(c) == target
]

if not selected:
click.echo("no matching MCP clients detected")
return

click.echo("synapto MCP configuration\n")
for detected in selected:
slug = _mcp_client_slug(detected)
if not yes and not click.confirm(f"configure {detected['name']} for Synapto?", default=True):
click.echo(f" skipped: {detected['name']}")
continue

_write_mcp_config(
detected["path"],
tenant=tenant,
disable_claude_auto_memory=slug == MCP_CLIENT_CLAUDE_CODE,
preserve_existing_synapto=True,
)
click.echo(f" written: {detected['path']}")

click.echo("\nrestart your MCP client so the updated environment is loaded")


@main.command()
@click.argument("query")
@click.option("--tenant", "-t", default=None, help="tenant/project scope")
Expand Down Expand Up @@ -539,8 +595,6 @@ async def _import():

def _detect_mcp_clients(home=None) -> list[dict]:
"""Detect installed MCP clients and their config paths."""
from pathlib import Path

home = home or Path.home()
clients = []

Expand All @@ -558,30 +612,53 @@ def _detect_mcp_clients(home=None) -> list[dict]:
return clients


def _write_mcp_config(config_path, tenant: str = "default") -> None:
def _write_mcp_config(
config_path,
tenant: str | None = "default",
*,
disable_claude_auto_memory: bool = False,
preserve_existing_synapto: bool = False,
) -> None:
"""Write synapto MCP config using uvx for auto-updates."""
from pathlib import Path

path = Path(config_path)
existing = {}
if path.exists():
with open(path) as f:
existing = json.loads(f.read())

servers = existing.get("mcpServers", {})
server_config: dict = {
"command": "uvx",
"args": ["synapto", "serve"],
}
if tenant != "default":
server_config["env"] = {"SYNAPTO_DEFAULT_TENANT": tenant}
current_server = servers.get("synapto")
if preserve_existing_synapto and isinstance(current_server, dict):
server_config: dict = dict(current_server)
else:
server_config = {
"command": "uvx",
"args": ["--refresh", "synapto", "serve"],
}

existing_env = server_config.get("env")
env = dict(existing_env) if isinstance(existing_env, dict) else {}
if tenant is not None and tenant != "default":
env["SYNAPTO_DEFAULT_TENANT"] = tenant
elif tenant == "default":
env.pop("SYNAPTO_DEFAULT_TENANT", None)

if disable_claude_auto_memory:
env[CLAUDE_CODE_DISABLE_AUTO_MEMORY_ENV] = "1"
else:
env.pop(CLAUDE_CODE_DISABLE_AUTO_MEMORY_ENV, None)

if env:
server_config["env"] = env
else:
server_config.pop("env", None)

servers["synapto"] = server_config
existing["mcpServers"] = servers

path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
f.write(json.dumps(existing, indent=2))
f.write(json.dumps(existing, indent=2) + "\n")


def _offer_mcp_config(tenant: str = "default") -> None:
Expand All @@ -593,7 +670,11 @@ def _offer_mcp_config(tenant: str = "default") -> None:
click.echo("\n--- mcp client configuration ---")
for client in clients:
if click.confirm(f"configure {client['name']} with auto-update (uvx)?", default=True):
_write_mcp_config(client["path"], tenant)
_write_mcp_config(
client["path"],
tenant,
disable_claude_auto_memory=client["name"] == "Claude Code",
)
click.echo(f" written: {client['path']}")
else:
click.echo(f" skipped: {client['name']}")
Expand Down
Loading