feat(integrations): configure plugin connections from user settings

This commit is contained in:
huangsen 2026-09-28 18:49:53 +08:00
parent 2ce7b25d94
commit 8691778a44
16 changed files with 332 additions and 61 deletions

View file

@ -15,7 +15,7 @@ Start ReMe → Install the ReMe plugin → Use memory in Claude Code
## Requirements
- Python 3.11+ available as `python3` in the host's environment; the plugin uses only the standard library.
- Python 3.11+ available as `python3` in the host's environment, with FastMCP >= 3.1 (included with ReMe).
- A current Claude Code version supporting plugin command hooks, `async`, and Stop `last_assistant_message`.
- A running ReMe HTTP service exposing `search`, `auto_memory`, and `health_check`, with MCP enabled.
- A working model configuration on the ReMe server for memory extraction. Default BM25 recall does not need embeddings.
@ -58,7 +58,10 @@ Set `auto_memory` to `false` for recall-only use.
The default service address works immediately after plugin installation. Create a configuration
file only when you want to change the defaults below.
Optional settings live at `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`. Start from the bundled example:
Edit the user configuration file directly to change addresses and behavior. Plugin upgrades preserve
this file, and only overrides need to be saved.
Settings live at `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`. For a new file, copy the bundled example; edit an existing file in place:
```bash
mkdir -p "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme"
@ -67,22 +70,44 @@ cp integrations/claude_code/reme/config.example.json "${CLAUDE_CONFIG_DIR:-$HOME
| Option | Default | Meaning |
| --- | --- | --- |
| `mcp_url` | `http://127.0.0.1:2333/mcp` | MCP address; removing the trailing `/mcp` gives the default HTTP base for hooks |
| `api_url` | Empty string | Optional HTTP Job API base; required for custom MCP paths |
| `auto_recall` | `true` | Search before a user prompt |
| `auto_memory` | `true` | Queue and submit completed turns; false also disables retries |
| `recall_limit` | `5` | Search result limit |
| `recall_min_score` | `0` | Minimum recall score |
| `recall_timeout` | `5` | Foreground HTTP timeout in seconds, at most 10 |
| `request_timeout` | `600` | Background HTTP timeout in seconds, at most 600 |
| `request_timeout` | `600` | Memory-write and MCP request timeout in seconds, at most 600 |
| `memory_interval` | `5` | Completed turns per batch; use 1 for immediate per-turn writes |
| `shutdown_timeout` | `2` | Total best-effort exit drain budget in seconds, at most 2 |
| `context_max_chars` | `8000` | Maximum recalled payload characters |
| `timezone` | `Asia/Shanghai` | Daily batch timezone; match the ReMe workspace |
Unknown fields and invalid values fail validation. Changes apply on the next hook invocation.
The plugin's `.mcp.json` is the single endpoint setting: both MCP and hooks use its `reme.url`,
which must end in `/mcp`. For a different port or protected proxy, edit that source file **before
installation**, then refresh/reinstall the plugin. Do not edit a versioned installation cache.
There is no separate `REME_HOST`/`REME_PORT` hook override.
For example, use another port and disable automatic recording:
```json
{
"mcp_url": "http://127.0.0.1:2444/mcp",
"auto_memory": false,
"auto_recall": true,
"memory_interval": 1
}
```
Both MCP and hooks read addresses from this user configuration. Hooks pick up edits on their next
invocation. After changing an address or the MCP timeout, reconnect MCP or restart the host so the
existing MCP process also reads the new settings. No source edits, cache edits, or reinstall are needed.
For a custom MCP path such as `https://memory.example.com/tools`, also set `api_url` to the HTTP Job
API base, for example `https://memory.example.com/reme`.
Unknown fields and invalid values fail validation; hooks skip work and log `hook_failed` instead of
falling back to another service. Disabling `auto_memory` stops automatic capture and retries while
preserving pending batches; explicit MCP tools remain available. Changing the service address never
sends old pending conversations to the new service. Remove a field to restore its default.
The plugin uses a host-managed stdio process to forward MCP. Start the host with a `python3`
environment containing FastMCP. If ReMe runs on another machine or in another Python environment,
run `python3 -m pip install "fastmcp>=3.1"` in the host environment. Hooks use only the standard library.
## Lifecycle and failure behavior

View file

@ -14,7 +14,7 @@
## 环境要求
- 宿主环境中有 Python 3.11+,可通过 `python3` 执行;插件仅使用标准库。
- 宿主环境中有 Python 3.11+,可通过 `python3` 执行,并已安装 FastMCP >= 3.1(ReMe 自带此依赖)。
- 当前 Claude Code 版本支持插件、`UserPromptSubmit`、`Stop`、原生异步命令 Hook 和 `last_assistant_message`。
- 已启动 ReMe HTTP 服务,开放 `search`、`auto_memory`、`health_check`,并启用 MCP。
- ReMe 服务端有可用于自动记忆提取的模型配置。默认 BM25 召回不要求 Embedding。
@ -53,7 +53,9 @@ reme start workspace_dir=/absolute/path/to/workspace service.backend=http
使用默认服务地址时,安装插件后即可使用。需要调整下列默认行为时,再创建配置文件。
可选配置位于 `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`。从仓库根目录复制示例:
直接编辑用户配置文件即可调整地址和行为,升级插件会保留配置。文件中只需写入要覆盖的选项。
配置位于 `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`。首次创建时,可从仓库根目录复制示例;已有文件请直接编辑:
```bash
mkdir -p "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme"
@ -62,21 +64,42 @@ cp integrations/claude_code/reme/config.example.json "${CLAUDE_CONFIG_DIR:-$HOME
| 配置项 | 默认值 | 作用 |
| --- | --- | --- |
| `mcp_url` | `http://127.0.0.1:2333/mcp` | ReMe MCP 地址;默认去掉末尾 `/mcp` 得到 Hook 的 HTTP 地址 |
| `api_url` | 空字符串 | 可选 HTTP Job API 基址;MCP 使用自定义路径时需显式填写 |
| `auto_recall` | `true` | 用户提交消息时自动检索 |
| `auto_memory` | `true` | 自动捕获与提交;关闭后也不重试已有队列 |
| `recall_limit` | `5` | 检索结果上限 |
| `recall_min_score` | `0` | 自动召回最低分数 |
| `recall_timeout` | `5` | 前台 HTTP 超时秒数,最大 10 |
| `request_timeout` | `600` | 后台 HTTP 超时秒数,最大 600 |
| `request_timeout` | `600` | 写入与 MCP 请求超时秒数,最大 600 |
| `memory_interval` | `5` | 每批完成的对话轮数;设为 1 则逐轮提交 |
| `shutdown_timeout` | `2` | 退出时尽力刷新队列的总秒数,最大 2 |
| `context_max_chars` | `8000` | 召回正文字符上限 |
| `timezone` | `Asia/Shanghai` | 批次按此时区分日,应与 ReMe workspace 一致 |
未知字段或无效值会使配置校验失败;配置修改在下一次 Hook 调用生效。
插件 `.mcp.json` 中的 `reme.url` 是 MCP 和 Hook 共用的唯一服务地址,必须以 `/mcp` 结尾。
自定义端口或代理时,在安装前修改源码中的此文件,再刷新或重新安装插件,不要修改版本化安装缓存。
不再提供单独的 `REME_HOST` / `REME_PORT` Hook 地址覆盖。
例如,改用另一个端口并关闭自动记录:
```json
{
"mcp_url": "http://127.0.0.1:2444/mcp",
"auto_memory": false,
"auto_recall": true,
"memory_interval": 1
}
```
MCP 和 Hook 从同一份用户配置读取地址。Hook 在下一次调用时读取修改;更改地址或 MCP 超时后,
请重新连接 MCP 或重启宿主,确保现有 MCP 进程也使用新配置。无需修改插件源码、安装缓存或重装插件。
使用自定义 MCP 路径时,例如 `https://memory.example.com/tools`,同时设置
`api_url` 为 HTTP Job API 基址,例如 `https://memory.example.com/reme`。
未知字段或无效值会使配置校验失败,Hook 会跳过并记录 `hook_failed`,不会回退到其他服务。
关闭 `auto_memory` 会停止自动捕获和重试,保留已有队列;显式 MCP 工具仍可使用。
修改服务地址不会把旧地址下的待提交对话发送到新服务。移除配置项即可恢复该项默认值。
插件通过宿主管理的 stdio 连接进程转发 MCP,请确保启动宿主时的 `python3` 环境包含 FastMCP。
如果 ReMe 服务运行在另一台机器或另一个 Python 环境,在宿主环境执行
`python3 -m pip install "fastmcp>=3.1"`。Hook 本身仅使用标准库。
## 生命周期与失败行为

View file

@ -1,8 +1,10 @@
{
"mcpServers": {
"reme": {
"type": "http",
"url": "http://127.0.0.1:2333/mcp"
"command": "python3",
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/mcp_bridge.py"
]
}
}
}

View file

@ -1,4 +1,6 @@
{
"mcp_url": "http://127.0.0.1:2333/mcp",
"api_url": "",
"auto_recall": true,
"auto_memory": true,
"recall_limit": 5,

View file

@ -22,6 +22,8 @@ HOST = "claude-code"
HOME_ENV = "CLAUDE_CONFIG_DIR"
HOME_DEFAULT = "~/.claude"
DEFAULTS = {
"mcp_url": "http://127.0.0.1:2333/mcp",
"api_url": "",
"auto_recall": True,
"auto_memory": True,
"recall_limit": 5,
@ -41,9 +43,14 @@ def data_dir() -> Path:
def load_config() -> dict:
"""Read host-local settings; resolve HTTP from the bundled MCP URL."""
"""Read the same persistent settings used by the MCP bridge and lifecycle hooks."""
path = data_dir() / "config.json"
values = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
return validate_config(values)
def validate_config(values: dict) -> dict:
"""Validate user overrides before either transport can use them."""
if not isinstance(values, dict) or values.keys() - DEFAULTS.keys():
raise ValueError("Unknown ReMe configuration fields")
config = {**DEFAULTS, **values}
@ -52,6 +59,9 @@ def load_config() -> dict:
if isinstance(default, bool):
if not isinstance(value, bool):
raise ValueError(f"{key} must be a boolean")
elif isinstance(default, str):
if not isinstance(value, str):
raise ValueError(f"{key} must be a string")
elif isinstance(default, (int, float)):
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
raise ValueError(f"{key} must be a finite number")
@ -61,20 +71,33 @@ def load_config() -> dict:
raise ValueError(f"{key} must be an integer")
if config["recall_timeout"] > 10 or config["request_timeout"] > 600 or config["shutdown_timeout"] > 2:
raise ValueError("Timeout exceeds the lifecycle hook budget")
ZoneInfo(config["timezone"])
mcp = json.loads((Path(__file__).resolve().parents[1] / ".mcp.json").read_text(encoding="utf-8"))
url = mcp["mcpServers"]["reme"]["url"].rstrip("/")
try:
ZoneInfo(config["timezone"])
except (KeyError, ValueError) as exc:
raise ValueError("timezone must be an IANA timezone") from exc
config["mcp_url"] = validate_url(config["mcp_url"])
if config["api_url"]:
config["endpoint"] = validate_url(config["api_url"])
elif config["mcp_url"].endswith("/mcp"):
config["endpoint"] = config["mcp_url"][:-4]
else:
raise ValueError("Set api_url explicitly when mcp_url does not end in /mcp")
return config
def validate_url(url: str) -> str:
"""Accept HTTP endpoints without embedded credentials or ambiguous URL suffixes."""
url = url.rstrip("/")
parsed = urlsplit(url)
if (
parsed.scheme not in {"http", "https"}
or not parsed.hostname
or any((parsed.username is not None, parsed.password is not None, parsed.query, parsed.fragment))
or not parsed.path.endswith("/mcp")
or any(character.isspace() for character in url)
):
raise ValueError("MCP URL must be an absolute HTTP(S) URL ending in /mcp")
raise ValueError("ReMe URLs must be absolute HTTP(S) URLs without credentials, query, or fragment")
_ = parsed.port
config["endpoint"] = url[:-4]
return config
return url
def call(config: dict, action: str, payload: dict, timeout: float) -> dict:

View file

@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""Host-managed stdio connection to the user's independently running ReMe service."""
from auto_memory import load_config
def main() -> None:
"""Resolve persistent settings on connection; leave MCP transport handling to FastMCP."""
config = load_config()
try:
from fastmcp.server import create_proxy
from fastmcp.server.providers.proxy import ProxyClient
except ImportError as exc:
raise SystemExit(
"ReMe MCP requires fastmcp>=3.1 in the host's python3 environment (included in reme-ai).",
) from exc
upstream = ProxyClient(config["mcp_url"], timeout=config["request_timeout"])
proxy = create_proxy(upstream, name="ReMe")
proxy.run(transport="stdio", show_banner=False)
if __name__ == "__main__":
main()

View file

@ -27,8 +27,10 @@ mean the plugin is disabled, the MCP connection failed, or the server's job allo
Check the plugin and `/mcp` status before concluding the service is stopped. The default service is
started with `reme start workspace_dir=/absolute/path/to/workspace service.backend=http`.
The plugin's `.mcp.json` is the endpoint source for both MCP and automatic hooks. Hook settings and
content-free logs live in the host's `reme/` directory. Hooks batch five completed turns by default;
The host's `reme/config.json` supplies `mcp_url` and hook settings to both the MCP bridge and
automatic hooks. Users can edit this file directly. Reconnect MCP after changing its address;
hook changes apply on the next invocation. Content-free logs live in the host's `reme/` directory.
Hooks batch five completed turns by default;
short batches flush at session boundaries, subject to a bounded shutdown budget. Failed batches
remain for a later attempt. Do not manually submit the same conversation just because a background
write has not finished. For an explicit user request to store a fact, use `auto_memory` with only

View file

@ -15,7 +15,7 @@ Start ReMe → Install the ReMe plugin → Use memory in Codex
## Requirements
- Python 3.11+ available as `python3` in the host's environment; the plugin uses only the standard library.
- Python 3.11+ available as `python3` in the host's environment, with FastMCP >= 3.1 (included with ReMe).
- Codex CLI 0.145.0 or a compatible client supporting local plugins, command hooks, and hook trust review.
- A running ReMe HTTP service exposing `search`, `auto_memory`, and `health_check`, with MCP enabled.
- A working model configuration on the ReMe server for memory extraction. Default BM25 recall does not need embeddings.
@ -59,7 +59,10 @@ Set `auto_memory` to `false` for recall-only use.
The default service address works immediately after plugin installation. Create a configuration
file only when you want to change the defaults below.
Optional settings live at `${CODEX_HOME:-$HOME/.codex}/reme/config.json`. Start from the bundled example:
Edit the user configuration file directly to change addresses and behavior. Plugin upgrades preserve
this file, and only overrides need to be saved.
Settings live at `${CODEX_HOME:-$HOME/.codex}/reme/config.json`. For a new file, copy the bundled example; edit an existing file in place:
```bash
mkdir -p "${CODEX_HOME:-$HOME/.codex}/reme"
@ -68,22 +71,44 @@ cp integrations/codex/plugins/reme/config.example.json "${CODEX_HOME:-$HOME/.cod
| Option | Default | Meaning |
| --- | --- | --- |
| `mcp_url` | `http://127.0.0.1:2333/mcp` | MCP address; removing the trailing `/mcp` gives the default HTTP base for hooks |
| `api_url` | Empty string | Optional HTTP Job API base; required for custom MCP paths |
| `auto_recall` | `true` | Search before a user prompt |
| `auto_memory` | `true` | Queue and submit completed turns; false also disables retries |
| `recall_limit` | `5` | Search result limit |
| `recall_min_score` | `0` | Minimum recall score |
| `recall_timeout` | `5` | Foreground HTTP timeout in seconds, at most 10 |
| `request_timeout` | `600` | Memory-write HTTP timeout in seconds, at most 600 |
| `request_timeout` | `600` | Memory-write and MCP request timeout in seconds, at most 600 |
| `memory_interval` | `5` | Completed turns per batch; use 1 for immediate per-turn writes |
| `shutdown_timeout` | `2` | Total best-effort exit drain budget in seconds, at most 2 |
| `context_max_chars` | `8000` | Maximum recalled payload characters |
| `timezone` | `Asia/Shanghai` | Daily batch timezone; match the ReMe workspace |
Unknown fields and invalid values fail validation. Changes apply on the next hook invocation.
The plugin's `.mcp.json` is the single endpoint setting: both MCP and hooks use its `reme.url`,
which must end in `/mcp`. For a different port or protected proxy, edit that source file **before
installation**, then refresh/reinstall the plugin. Do not edit a versioned installation cache.
There is no separate `REME_HOST`/`REME_PORT` hook override.
For example, use another port and disable automatic recording:
```json
{
"mcp_url": "http://127.0.0.1:2444/mcp",
"auto_memory": false,
"auto_recall": true,
"memory_interval": 1
}
```
Both MCP and hooks read addresses from this user configuration. Hooks pick up edits on their next
invocation. After changing an address or the MCP timeout, reconnect MCP or restart the host so the
existing MCP process also reads the new settings. No source edits, cache edits, or reinstall are needed.
For a custom MCP path such as `https://memory.example.com/tools`, also set `api_url` to the HTTP Job
API base, for example `https://memory.example.com/reme`.
Unknown fields and invalid values fail validation; hooks skip work and log `hook_failed` instead of
falling back to another service. Disabling `auto_memory` stops automatic capture and retries while
preserving pending batches; explicit MCP tools remain available. Changing the service address never
sends old pending conversations to the new service. Remove a field to restore its default.
The plugin uses a host-managed stdio process to forward MCP. Start the host with a `python3`
environment containing FastMCP. If ReMe runs on another machine or in another Python environment,
run `python3 -m pip install "fastmcp>=3.1"` in the host environment. Hooks use only the standard library.
## Lifecycle and failure behavior

View file

@ -14,7 +14,7 @@
## 环境要求
- 宿主环境中有 Python 3.11+,可通过 `python3` 执行;插件仅使用标准库。
- 宿主环境中有 Python 3.11+,可通过 `python3` 执行,并已安装 FastMCP >= 3.1(ReMe 自带此依赖)。
- Codex CLI 0.145.0 或兼容版本,支持插件、`UserPromptSubmit`、`Stop`、同步命令 Hook 和 `last_assistant_message`。
- 已启动 ReMe HTTP 服务,开放 `search`、`auto_memory`、`health_check`,并启用 MCP。
- ReMe 服务端有可用于自动记忆提取的模型配置。默认 BM25 召回不要求 Embedding。
@ -53,7 +53,9 @@ codex plugin add reme@reme-codex
使用默认服务地址时,安装插件后即可使用。需要调整下列默认行为时,再创建配置文件。
可选配置位于 `${CODEX_HOME:-$HOME/.codex}/reme/config.json`。从仓库根目录复制示例:
直接编辑用户配置文件即可调整地址和行为,升级插件会保留配置。文件中只需写入要覆盖的选项。
配置位于 `${CODEX_HOME:-$HOME/.codex}/reme/config.json`。首次创建时,可从仓库根目录复制示例;已有文件请直接编辑:
```bash
mkdir -p "${CODEX_HOME:-$HOME/.codex}/reme"
@ -62,21 +64,42 @@ cp integrations/codex/plugins/reme/config.example.json "${CODEX_HOME:-$HOME/.cod
| 配置项 | 默认值 | 作用 |
| --- | --- | --- |
| `mcp_url` | `http://127.0.0.1:2333/mcp` | ReMe MCP 地址;默认去掉末尾 `/mcp` 得到 Hook 的 HTTP 地址 |
| `api_url` | 空字符串 | 可选 HTTP Job API 基址;MCP 使用自定义路径时需显式填写 |
| `auto_recall` | `true` | 用户提交消息时自动检索 |
| `auto_memory` | `true` | 自动捕获与提交;关闭后也不重试已有队列 |
| `recall_limit` | `5` | 检索结果上限 |
| `recall_min_score` | `0` | 自动召回最低分数 |
| `recall_timeout` | `5` | 前台 HTTP 超时秒数,最大 10 |
| `request_timeout` | `600` | 写入 HTTP 超时秒数,最大 600 |
| `request_timeout` | `600` | 写入与 MCP 请求超时秒数,最大 600 |
| `memory_interval` | `5` | 每批完成的对话轮数;设为 1 则逐轮提交 |
| `shutdown_timeout` | `2` | 退出时尽力刷新队列的总秒数,最大 2 |
| `context_max_chars` | `8000` | 召回正文字符上限 |
| `timezone` | `Asia/Shanghai` | 批次按此时区分日,应与 ReMe workspace 一致 |
未知字段或无效值会使配置校验失败;配置修改在下一次 Hook 调用生效。
插件 `.mcp.json` 中的 `reme.url` 是 MCP 和 Hook 共用的唯一服务地址,必须以 `/mcp` 结尾。
自定义端口或代理时,在安装前修改源码中的此文件,再刷新或重新安装插件,不要修改版本化安装缓存。
不再提供单独的 `REME_HOST` / `REME_PORT` Hook 地址覆盖。
例如,改用另一个端口并关闭自动记录:
```json
{
"mcp_url": "http://127.0.0.1:2444/mcp",
"auto_memory": false,
"auto_recall": true,
"memory_interval": 1
}
```
MCP 和 Hook 从同一份用户配置读取地址。Hook 在下一次调用时读取修改;更改地址或 MCP 超时后,
请重新连接 MCP 或重启宿主,确保现有 MCP 进程也使用新配置。无需修改插件源码、安装缓存或重装插件。
使用自定义 MCP 路径时,例如 `https://memory.example.com/tools`,同时设置
`api_url` 为 HTTP Job API 基址,例如 `https://memory.example.com/reme`。
未知字段或无效值会使配置校验失败,Hook 会跳过并记录 `hook_failed`,不会回退到其他服务。
关闭 `auto_memory` 会停止自动捕获和重试,保留已有队列;显式 MCP 工具仍可使用。
修改服务地址不会把旧地址下的待提交对话发送到新服务。移除配置项即可恢复该项默认值。
插件通过宿主管理的 stdio 连接进程转发 MCP,请确保启动宿主时的 `python3` 环境包含 FastMCP。
如果 ReMe 服务运行在另一台机器或另一个 Python 环境,在宿主环境执行
`python3 -m pip install "fastmcp>=3.1"`。Hook 本身仅使用标准库。
## 生命周期与失败行为

View file

@ -1,6 +1,6 @@
{
"name": "reme",
"version": "0.1.0",
"version": "0.1.0+codex.20260928104620",
"description": "Automatic recall, batched conversation recording, and explicit MCP memory tools for Codex.",
"author": {
"name": "EconML team of Alibaba Tongyi Lab",

View file

@ -1,8 +1,12 @@
{
"mcpServers": {
"reme": {
"type": "http",
"url": "http://127.0.0.1:2333/mcp"
"command": "python3",
"args": [
"hooks/mcp_bridge.py"
],
"cwd": ".",
"env_vars": ["CODEX_HOME"]
}
}
}

View file

@ -1,4 +1,6 @@
{
"mcp_url": "http://127.0.0.1:2333/mcp",
"api_url": "",
"auto_recall": true,
"auto_memory": true,
"recall_limit": 5,

View file

@ -22,6 +22,8 @@ HOST = "codex"
HOME_ENV = "CODEX_HOME"
HOME_DEFAULT = "~/.codex"
DEFAULTS = {
"mcp_url": "http://127.0.0.1:2333/mcp",
"api_url": "",
"auto_recall": True,
"auto_memory": True,
"recall_limit": 5,
@ -41,9 +43,14 @@ def data_dir() -> Path:
def load_config() -> dict:
"""Read host-local settings; resolve HTTP from the bundled MCP URL."""
"""Read the same persistent settings used by the MCP bridge and lifecycle hooks."""
path = data_dir() / "config.json"
values = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
return validate_config(values)
def validate_config(values: dict) -> dict:
"""Validate user overrides before either transport can use them."""
if not isinstance(values, dict) or values.keys() - DEFAULTS.keys():
raise ValueError("Unknown ReMe configuration fields")
config = {**DEFAULTS, **values}
@ -52,6 +59,9 @@ def load_config() -> dict:
if isinstance(default, bool):
if not isinstance(value, bool):
raise ValueError(f"{key} must be a boolean")
elif isinstance(default, str):
if not isinstance(value, str):
raise ValueError(f"{key} must be a string")
elif isinstance(default, (int, float)):
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
raise ValueError(f"{key} must be a finite number")
@ -61,20 +71,33 @@ def load_config() -> dict:
raise ValueError(f"{key} must be an integer")
if config["recall_timeout"] > 10 or config["request_timeout"] > 600 or config["shutdown_timeout"] > 2:
raise ValueError("Timeout exceeds the lifecycle hook budget")
ZoneInfo(config["timezone"])
mcp = json.loads((Path(__file__).resolve().parents[1] / ".mcp.json").read_text(encoding="utf-8"))
url = mcp["mcpServers"]["reme"]["url"].rstrip("/")
try:
ZoneInfo(config["timezone"])
except (KeyError, ValueError) as exc:
raise ValueError("timezone must be an IANA timezone") from exc
config["mcp_url"] = validate_url(config["mcp_url"])
if config["api_url"]:
config["endpoint"] = validate_url(config["api_url"])
elif config["mcp_url"].endswith("/mcp"):
config["endpoint"] = config["mcp_url"][:-4]
else:
raise ValueError("Set api_url explicitly when mcp_url does not end in /mcp")
return config
def validate_url(url: str) -> str:
"""Accept HTTP endpoints without embedded credentials or ambiguous URL suffixes."""
url = url.rstrip("/")
parsed = urlsplit(url)
if (
parsed.scheme not in {"http", "https"}
or not parsed.hostname
or any((parsed.username is not None, parsed.password is not None, parsed.query, parsed.fragment))
or not parsed.path.endswith("/mcp")
or any(character.isspace() for character in url)
):
raise ValueError("MCP URL must be an absolute HTTP(S) URL ending in /mcp")
raise ValueError("ReMe URLs must be absolute HTTP(S) URLs without credentials, query, or fragment")
_ = parsed.port
config["endpoint"] = url[:-4]
return config
return url
def call(config: dict, action: str, payload: dict, timeout: float) -> dict:

View file

@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""Host-managed stdio connection to the user's independently running ReMe service."""
from auto_memory import load_config
def main() -> None:
"""Resolve persistent settings on connection; leave MCP transport handling to FastMCP."""
config = load_config()
try:
from fastmcp.server import create_proxy
from fastmcp.server.providers.proxy import ProxyClient
except ImportError as exc:
raise SystemExit(
"ReMe MCP requires fastmcp>=3.1 in the host's python3 environment (included in reme-ai).",
) from exc
upstream = ProxyClient(config["mcp_url"], timeout=config["request_timeout"])
proxy = create_proxy(upstream, name="ReMe")
proxy.run(transport="stdio", show_banner=False)
if __name__ == "__main__":
main()

View file

@ -27,8 +27,10 @@ mean the plugin is disabled, the MCP connection failed, or the server's job allo
Check the plugin and `/mcp` status before concluding the service is stopped. The default service is
started with `reme start workspace_dir=/absolute/path/to/workspace service.backend=http`.
The plugin's `.mcp.json` is the endpoint source for both MCP and automatic hooks. Hook settings and
content-free logs live in the host's `reme/` directory. Hooks batch five completed turns by default;
The host's `reme/config.json` supplies `mcp_url` and hook settings to both the MCP bridge and
automatic hooks. Users can edit this file directly. Reconnect MCP after changing its address;
hook changes apply on the next invocation. Content-free logs live in the host's `reme/` directory.
Hooks batch five completed turns by default;
short batches flush at session boundaries, subject to a bounded shutdown budget. Failed batches
remain for a later attempt. Do not manually submit the same conversation just because a queued
write has not finished. For an explicit user request to store a fact, use `auto_memory` with only

View file

@ -70,6 +70,15 @@ def test_native_manifests_resolve_within_their_own_plugin():
assert manifest["name"] == root.name == "reme"
assert (root / ".mcp.json").is_file()
assert (root / "skills/reme-memory/SKILL.md").is_file()
mcp = json.loads((root / ".mcp.json").read_text())["mcpServers"]["reme"]
assert mcp["command"] == "python3"
if host == "claude-code":
assert mcp["args"] == ["${CLAUDE_PLUGIN_ROOT}/hooks/mcp_bridge.py"]
else:
assert mcp["args"] == ["hooks/mcp_bridge.py"]
assert mcp["cwd"] == "."
assert "CODEX_HOME" in mcp["env_vars"]
assert (root / "hooks/mcp_bridge.py").is_file()
hooks = json.loads((root / "hooks/hooks.json").read_text())["hooks"]
assert "SubagentStop" not in hooks
for event, groups in hooks.items():
@ -100,6 +109,18 @@ def test_defaults_and_mcp_endpoint_agree(adapter):
{"recall_limit": True},
{"memory_interval": 1.5},
{"shutdown_timeout": 3},
{"timezone": False},
{"timezone": "Unknown/Timezone"},
{"mcp_url": False},
{"mcp_url": "file:///tmp/mcp"},
{"mcp_url": "http://name:secret@example.com/mcp"},
{"mcp_url": "http://example.com/mcp?token=secret"},
{"mcp_url": "http://example.com/mcp#fragment"},
{"mcp_url": "http://example.com:invalid/mcp"},
{"mcp_url": "http://example.com/other"},
{"mcp_url": "http://example.com/\nmcp"},
{"api_url": 12},
{"api_url": "relative/path"},
],
)
def test_invalid_config_fails_explicitly(adapter, values):
@ -108,12 +129,60 @@ def test_invalid_config_fails_explicitly(adapter, values):
adapter.load_config()
def test_custom_endpoint_has_one_source(adapter, tmp_path, monkeypatch):
root = tmp_path / "plugin"
root.mkdir()
(root / ".mcp.json").write_text(json.dumps({"mcpServers": {"reme": {"url": "https://example.com/reme/mcp"}}}))
monkeypatch.setattr(adapter, "__file__", str(root / "hooks/auto_memory.py"))
assert adapter.load_config()["endpoint"] == "https://example.com/reme"
def test_custom_endpoint_has_one_source(adapter):
adapter.write_json(adapter.data_dir() / "config.json", {"mcp_url": "https://example.com/reme/mcp/"})
config = adapter.load_config()
assert config["mcp_url"] == "https://example.com/reme/mcp"
assert config["endpoint"] == "https://example.com/reme"
def test_custom_mcp_path_and_http_prefix(adapter):
adapter.write_json(
adapter.data_dir() / "config.json",
{
"mcp_url": "https://example.com/tools/reme",
"api_url": "https://example.com/jobs/reme/",
},
)
assert adapter.load_config()["endpoint"] == "https://example.com/jobs/reme"
def test_config_edit_takes_effect_on_next_hook(adapter, monkeypatch):
call = Mock(return_value={"success": True, "answer": "daily/fact.md"})
monkeypatch.setattr(adapter, "call", call)
adapter.write_json(adapter.data_dir() / "config.json", {"auto_recall": False, "auto_memory": False})
assert adapter.handle_event({"hook_event_name": "UserPromptSubmit", "prompt": "fact"}) == {}
adapter.handle_event({"hook_event_name": "SessionStart"})
call.assert_not_called()
adapter.write_json(
adapter.data_dir() / "config.json",
{"auto_recall": True, "mcp_url": "http://127.0.0.1:2444/mcp"},
)
assert adapter.handle_event({"hook_event_name": "UserPromptSubmit", "prompt": "fact"})
assert call.call_args.args[0]["endpoint"] == "http://127.0.0.1:2444"
def test_mcp_bridge_uses_persistent_configuration(adapter, monkeypatch):
import fastmcp.server
import fastmcp.server.providers.proxy
adapter.write_json(
adapter.data_dir() / "config.json",
{
"mcp_url": "http://127.0.0.1:2444/mcp",
"request_timeout": 42,
},
)
monkeypatch.setitem(sys.modules, "auto_memory", adapter)
spec = importlib.util.spec_from_file_location("reme_test_bridge", Path(adapter.__file__).with_name("mcp_bridge.py"))
bridge = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bridge)
client, proxy = Mock(), Mock()
monkeypatch.setattr(fastmcp.server.providers.proxy, "ProxyClient", client)
monkeypatch.setattr(fastmcp.server, "create_proxy", proxy)
bridge.main()
client.assert_called_once_with("http://127.0.0.1:2444/mcp", timeout=42)
proxy.return_value.run.assert_called_once_with(transport="stdio", show_banner=False)
def test_recall_is_bounded_and_cannot_close_evidence_wrapper(adapter, monkeypatch):