chore(deps): upgrade dependencies and bump ReMe to 0.4.2.2 (#692)

* chore(deps): upgrade dependencies and bump ReMe to 0.4.2.2

* test(mcp): disable camelCase compatibility in CI

* fix(mcp): normalize implicit job input schemas

---------

Co-authored-by: huangsen <huangsen.huang@alibaba-inc.com>
This commit is contained in:
jinliyl 2026-10-28 16:36:17 +08:00 • committed by GitHub
parent b0991db725
commit 80b85dcfd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 151 additions and 72 deletions

View file

@ -48,6 +48,8 @@ jobs:
pip install coverage
- name: Run unit tests
env:
FASTMCP_MCP_CAMELCASE_COMPAT: "false"
run: |
python -c "import tokenizers; print('tokenizers', tokenizers.__version__)"
coverage run -m pytest tests/unit plugins/auto-fin plugins/daily_paper plugins/lme plugins/beam plugins/home/tests \
@ -62,6 +64,6 @@ jobs:
- name: Verify minimum tokenizer version
if: matrix.python-version == '3.11'
run: |
python -m pip install "tokenizers==0.21.0"
python -m pip install "tokenizers==0.23.3"
python -c "import tokenizers; print('tokenizers', tokenizers.__version__)"
python -m pytest tests/unit/test_qwen_token_tools.py tests/unit/test_package_versions.py -q

View file

@ -116,7 +116,7 @@ as `auto_memory`, `auto_resource`, `auto_dream`, and proactive refresh do.
## Decision Model Configuration
`as_decision_model` is a provider-independent interface for probabilistic decisions. The current `agentscope` adapter
calls the TypeSafe API through AgentScope `2.0.10`. It evaluates named binary, choice, or score questions against shared text or a JSON object.
calls the TypeSafe API through AgentScope `2.0.11`. It evaluates named binary, choice, or score questions against shared text or a JSON object.
Install the optional dependencies:

View file

@ -53,6 +53,10 @@ MCP does not expose Stream Jobs.
## MCP
ReMe 0.4.2.2 uses FastMCP 4 and MCP Python SDK 2. Upgrade ReMe and host-side MCP bridges together;
standalone bridge environments require `fastmcp>=4.1.4,<5`. CLI tool catalogs use SDK 2 snake_case fields,
including `input_schema`, `output_schema`, and annotation hints. Update consumers that read the old camelCase fields.
The default HTTP service mounts streamable HTTP MCP at `/mcp`:
```yaml

View file

@ -119,7 +119,7 @@ components:
## 决策模型配置
`as_decision_model` 是不依赖供应商的概率决策模型接口;当前的 `agentscope` 适配器通过 AgentScope `2.0.10` 调用 TypeSafe API。
`as_decision_model` 是不依赖供应商的概率决策模型接口;当前的 `agentscope` 适配器通过 AgentScope `2.0.11` 调用 TypeSafe API。
它接受共享的文本或 JSON object 与命名问题,返回二分类概率、选项分类或评分结果。
安装可选依赖:

View file

@ -59,6 +59,10 @@ MCP 不暴露 Stream Job。
## MCP
ReMe 0.4.2.2 使用 FastMCP 4 与 MCP Python SDK 2。升级时请同步更新 ReMe 和宿主侧 MCP bridge;
独立 bridge 环境需要 `fastmcp>=4.1.4,<5`。CLI 工具列表统一使用 SDK 2 的 snake_case 字段,
包括 `input_schema`、`output_schema` 和 annotation hints;读取旧驼峰字段的调用方需要同步更新。
默认 HTTP 服务会在 `/mcp` 挂载 streamable HTTP MCP:
```yaml

View file

@ -107,7 +107,7 @@ sends old pending conversations to the new service. Remove a field to restore it
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.
run `python3 -m pip install "fastmcp>=4.1.4,<5"` in the host environment. Hooks use only the standard library.
## Lifecycle and failure behavior

View file

@ -99,7 +99,7 @@ MCP 和 Hook 从同一份用户配置读取地址。Hook 在下一次调用时
插件通过宿主管理的 stdio 连接进程转发 MCP,请确保启动宿主时的 `python3` 环境包含 FastMCP。
如果 ReMe 服务运行在另一台机器或另一个 Python 环境,在宿主环境执行
`python3 -m pip install "fastmcp>=3.1"`。Hook 本身仅使用标准库。
`python3 -m pip install "fastmcp>=4.1.4,<5"`。Hook 本身仅使用标准库。
## 生命周期与失败行为

View file

@ -12,7 +12,7 @@ def main() -> None:
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).",
"ReMe MCP requires fastmcp>=4.1.4,<5 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")

View file

@ -13,14 +13,14 @@ The screenshots below were captured in Codex desktop. Click an image to view it
- **Codex CLI >= 0.159.2** for installation and CLI use. Run `codex --version` to check.
- **Codex desktop >= 26.924.22138** for the MCP settings form and status panel. Check the version in **About**.
These are the versions validated for this guide; older releases have not been verified.
- Make Python 3.11+ and `fastmcp>=3.4.2` available as `python3` in the environment that launches Codex,
- Make Python 3.11+ and `fastmcp>=4.1.4,<5` available as `python3` in the environment that launches Codex,
even when ReMe runs on another machine. Check with:
```bash
python3 -c "import sys, fastmcp; print(sys.version); print(fastmcp.__version__)"
```
If needed, install FastMCP there with `python3 -m pip install "fastmcp>=3.4.2"`.
If needed, install FastMCP there with `python3 -m pip install "fastmcp>=4.1.4,<5"`.
- Configure ReMe's model for memory extraction using the [configuration guide](../../docs/en/configuration.md).
Default BM25 search does not require an embedding model.
- Have a local checkout of this repository for installation.

View file

@ -12,14 +12,14 @@ ReMe 可以自动记录、召回记忆,并每天整理。记忆文件保存在
- **Codex CLI >= 0.159.2**:用于安装和命令行使用,运行 `codex --version` 查看版本。
- **Codex 桌面端 >= 26.924.22138**:用于打开 MCP 设置表单和状态面板,可在 **关于(About)** 中查看版本。
以上为本指南已验证的版本基线,更早版本未经验证。
- 启动 Codex 的环境中,`python3` 需要是 Python 3.11+,并安装 `fastmcp>=3.4.2`。
- 启动 Codex 的环境中,`python3` 需要是 Python 3.11+,并安装 `fastmcp>=4.1.4,<5`。
即使 ReMe 在远程运行,Codex 所在机器也需要这个环境。检查命令:
```bash
python3 -c "import sys, fastmcp; print(sys.version); print(fastmcp.__version__)"
```
如未安装,执行 `python3 -m pip install "fastmcp>=3.4.2"`。
如未安装,执行 `python3 -m pip install "fastmcp>=4.1.4,<5"`。
- 按[配置指南](../../docs/zh/configuration.md)配置 ReMe 用于提取记忆的模型。默认 BM25 检索不需要 embedding 模型。
- 准备本仓库的本地副本,用于安装。

View file

@ -198,7 +198,7 @@ def create_server() -> FastMCP:
},
"required": ["schema", "values"],
},
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
),
)
server.add_tool(
@ -217,10 +217,10 @@ def create_server() -> FastMCP:
},
output_schema={"type": "object", "properties": {"values": SCHEMA}, "required": ["values"]},
annotations=ToolAnnotations(
readOnlyHint=False,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
read_only_hint=False,
destructive_hint=False,
idempotent_hint=True,
open_world_hint=False,
),
),
)

View file

@ -11,6 +11,7 @@ from typing import Annotated
from fastmcp import FastMCP
from fastmcp.resources import ResourceContent, ResourceResult
from fastmcp.tools import ToolResult
from mcp.types import ToolAnnotations
from pydantic import Field
from reme_config import load_config
@ -82,7 +83,7 @@ def register_tools(server: FastMCP) -> None:
status_html = (root / "ui/status.html").read_text(encoding="utf-8")
plugin_version = json.loads((root / ".codex-plugin/plugin.json").read_text(encoding="utf-8"))["version"]
@server.tool(name=SEARCH, title="Search ReMe memory", annotations={"readOnlyHint": True})
@server.tool(name=SEARCH, title="Search ReMe memory", annotations=ToolAnnotations(read_only_hint=True))
async def search(
query: Annotated[str, Field(min_length=1, description="Focused memory search query")],
limit: Annotated[int | None, Field(ge=1, le=50)] = None,
@ -119,7 +120,7 @@ def register_tools(server: FastMCP) -> None:
@server.tool(
name=STATUS,
title="ReMe status",
annotations={"readOnlyHint": True},
annotations=ToolAnnotations(read_only_hint=True),
app={"resourceUri": STATUS_URI, "visibility": ["app", "model"]},
)
async def status() -> ToolResult:
@ -142,7 +143,7 @@ def register_tools(server: FastMCP) -> None:
@server.tool(
name=RUN_DREAM,
title="Consolidate ReMe memory now",
annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": False},
annotations=ToolAnnotations(read_only_hint=False, destructive_hint=True, idempotent_hint=False),
)
async def run_dream(date: str = "", hint: str | None = None) -> str:
"""Only on user request: consolidate daily notes into digest files without changing the daily schedule."""

View file

@ -21,66 +21,70 @@ classifiers = [
]
dependencies = [
"aiofiles>=24.1.0",
"croniter>=2.0",
"fastapi>=0.135.1",
"fastmcp>=3.1.0",
"aiofiles>=25.1.0",
"croniter>=6.2.4",
"fastapi>=0.162.0",
"fastmcp>=4.1.4,<5",
"httpx[socks]>=0.28.1",
"loguru>=0.7.3",
"mistletoe>=1.5.1",
"numpy>=2.2.6",
"openai>=2.26.0",
"psutil>=5.9",
"pydantic>=2.12.5",
"python-frontmatter>=1.1.0",
"mcp>=2.3.0,<3",
"mistletoe>=1.6.0",
"numpy>=2.4.6; python_version < '3.12'",
"numpy>=2.5.4; python_version >= '3.12'",
"openai>=3.40.0",
"psutil>=7.2.2",
"pydantic>=2.14.1",
"python-frontmatter>=1.3.0",
"pyyaml>=6.0.3",
"rich>=14.3.3",
"uvicorn>=0.41.0",
"watchfiles>=1.1.1",
"zstandard>=0.23.0",
"rich>=15.0.0",
"uvicorn>=0.54.0",
"watchfiles>=1.3.0",
"zstandard>=0.25.0",
]
[project.optional-dependencies]
as = [
"agentscope[model-ollama]==2.0.10",
"agentscope[model-ollama]==2.0.11",
]
decision = [
"agentscope[classifier-jev]==2.0.10",
"agentscope[classifier-jev]==2.0.11",
]
web = [
"reme_studio",
]
core = [
"reme-ai[as]",
"pillow>=10.0.0",
"claude-agent-sdk>=0.2.126",
"openai-codex>=0.144.4",
"pillow>=13.0.0",
"claude-agent-sdk>=0.2.174",
"openai-codex>=0.163.0",
"pproxy>=2.7.9,<3",
"faiss-cpu>=1.13.2",
"zvec>=0.6.0",
"faiss-cpu>=1.15.1",
"zvec>=0.8.0",
"jieba>=0.42.1",
"rjieba>=0.2.1",
"neo4j>=6.2.0",
"networkx>=3.4.2",
"polars>=1.43.0",
"rjieba>=0.2.2",
"neo4j>=6.4.1",
"networkx>=3.6.1; python_version < '3.12'",
"networkx>=3.6; python_full_version == '3.14.1'",
"networkx>=3.7; python_version >= '3.12' and python_full_version != '3.14.1'",
"polars>=2.1.0",
"reme_studio",
]
qwen-tokenizer = [
"tokenizers>=0.21,<1",
"tokenizers>=0.23.3,<1",
]
image-heif = [
"pillow>=10.0.0",
"pillow-heif>=0.13.0",
"pillow>=13.0.0",
"pillow-heif>=1.8.1",
]
dev = [
"packaging>=24.2",
"pre-commit>=4.6.1",
"pytest>=9.1.1",
"pytest-asyncio>=0.23",
"packaging>=26.3",
"pre-commit>=4.7.0",
"pytest>=9.2.0",
"pytest-asyncio>=1.4.1",
]
benchmark-docker = [
"reme-ai[as]",
"aiodocker",
"aiodocker>=0.27.0",
]
full = [
"reme-ai[core]",
@ -111,7 +115,7 @@ include-package-data = true
version = { attr = "reme.__version__" }
[build-system]
requires = ["setuptools>=45", "wheel"]
requires = ["setuptools>=84.2.1", "wheel>=0.48.0"]
build-backend = "setuptools.build_meta"
[tool.pytest.ini_options]

View file

@ -1,6 +1,6 @@
"""ReMe CLI package."""
__version__ = "0.4.2.1"
__version__ = "0.4.2.2"
from . import config
from . import constants

View file

@ -92,7 +92,7 @@ class MCPClient(BaseClient):
yield self._extract_text(result)
async def list_actions(self) -> list[dict]:
"""Return raw MCP Tool dumps; each dict gets an `action` key (the tool name)."""
"""Return MCP tool descriptors using the SDK's snake_case field names."""
if self.client is None:
raise RuntimeError("Client not initialized. Call _start() first.")
tools = await self.client.list_tools()

View file

@ -51,7 +51,7 @@ class ZvecLocalFileStore(LocalFileStore):
zvec is imported lazily inside ``__init__`` so that merely importing this
module does not require the optional dependency; the backend targets the
zvec version declared in ``pyproject.toml`` (``zvec>=0.6.0``).
zvec version declared in ``pyproject.toml`` (``zvec>=0.8.0``).
"""
def __init__(
@ -75,7 +75,7 @@ class ZvecLocalFileStore(LocalFileStore):
import zvec
except ImportError as e:
raise ImportError(
"zvec is required for ZvecLocalFileStore. Install with `pip install 'zvec>=0.6.0'`.",
"zvec is required for ZvecLocalFileStore. Install with `pip install 'zvec>=0.8.0'`.",
) from e
return zvec

View file

@ -33,6 +33,8 @@ def add_mcp_job(
return response.answer
parameters = dict(job.parameters or {})
# MCP requires object inputs even when a job leaves its schema implicit.
parameters.setdefault("type", "object")
injected_names = injected_job_kwargs.keys()
if "properties" in parameters:
parameters["properties"] = {

View file

@ -13,7 +13,7 @@ from unittest.mock import AsyncMock
import pytest
from fastmcp import Client, FastMCP
from fastmcp.exceptions import ToolError
from mcp.shared.exceptions import McpError
from mcp.shared.exceptions import MCPError
PLUGIN = Path(__file__).resolve().parents[2] / "integrations/codex/plugins/reme"
@ -60,7 +60,7 @@ async def test_settings_remain_editable_offline(plugin, monkeypatch):
connect = AsyncMock(side_effect=ConnectionError("offline"))
monkeypatch.setattr(module.ReMeProvider, "_list_tools", connect)
async with Client(module.create_server()) as client:
capability = client.initialize_result.capabilities.experimental["openai/settings"]
capability = client.server_capabilities.experimental["openai/settings"]
assert capability == {"readTool": module.SETTINGS_READ, "updateTool": module.SETTINGS_UPDATE}
tools = {tool.name: tool for tool in await client.list_tools()}
assert {module.SETTINGS_READ, module.SETTINGS_UPDATE, module.STATUS} <= tools.keys()
@ -71,8 +71,8 @@ async def test_settings_remain_editable_offline(plugin, monkeypatch):
assert settings["schema"]["properties"].keys() == config.DEFAULTS.keys()
assert settings["layout"][0]["items"][1]["tool"] == module.STATUS
assert len(settings["layout"][0]["items"]) == 2
assert tools[module.SETTINGS_READ].annotations.readOnlyHint
assert tools[module.SETTINGS_READ].outputSchema
assert tools[module.SETTINGS_READ].annotations.read_only_hint
assert tools[module.SETTINGS_READ].output_schema
assert not (config.data_dir() / "config.json").exists()
saved = await client.call_tool(capability["updateTool"], {"set": {"autoMemoryEnabled": False}})
assert saved.structured_content["values"] == {**config.DEFAULTS, "autoMemoryEnabled": False}
@ -169,7 +169,7 @@ async def test_hooks_call_mcp_and_preserve_structured_answers(plugin, monkeypatc
assert calls == ["fact"]
with pytest.raises(ToolError, match="write failed"):
await transport.call_async(config.load_config(), "auto_memory", {}, 2)
with pytest.raises((TimeoutError, McpError)):
with pytest.raises((TimeoutError, MCPError)):
await transport.call_async(config.load_config(), "delayed", {}, 0.05)
@ -208,7 +208,7 @@ async def test_status_opens_a_self_contained_fullscreen_app_resource(plugin):
ui = tools["reme_status"].meta["ui"]
assert ui["visibility"] == ["app", "model"]
content = (await client.read_resource(ui["resourceUri"]))[0]
assert content.mimeType == "text/html;profile=mcp-app"
assert content.mime_type == "text/html;profile=mcp-app"
assert content.meta["openai/ui"] == {
"preferredDisplayMode": "fullscreen",
"availableDisplayModes": ["fullscreen"],
@ -323,7 +323,7 @@ async def test_manual_dream_uses_saved_settings_serializes_processes_and_tracks_
monkeypatch.setattr(features, "call_async", dream)
async with Client(module.create_server()) as first, Client(module.create_server()) as second:
tools = {tool.name: tool for tool in await first.list_tools()}
assert tools["reme_run_dream"].annotations.destructiveHint
assert tools["reme_run_dream"].annotations.destructive_hint
task = asyncio.create_task(first.call_tool("reme_run_dream", {"date": "2026-09-30"}))
await asyncio.wait_for(started.wait(), 2)
assert state.local_status(current)["auto_dream"]["last_run"]["status"] == "running"

View file

@ -173,8 +173,8 @@ def test_extra_reuses_agentscope_pin():
"""Installing core and decision models together must not request different AgentScope versions."""
config = tomllib.loads((Path(__file__).resolve().parents[2] / "pyproject.toml").read_text())
extras = config["project"]["optional-dependencies"]
assert extras["decision"] == ["agentscope[classifier-jev]==2.0.10"]
assert extras["as"] == ["agentscope[model-ollama]==2.0.10"]
assert extras["decision"] == ["agentscope[classifier-jev]==2.0.11"]
assert extras["as"] == ["agentscope[model-ollama]==2.0.11"]
assert "reme-ai[decision]" in extras["full"]
assert "reme-ai[decision]" not in extras["core"]

View file

@ -1,4 +1,10 @@
"""MCP client address defaults."""
"""MCP client addresses and SDK tool descriptors."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
from mcp.types import Tool, ToolAnnotations
import pytest
from reme.components.client.mcp_client import MCPClient
from reme.constants import REME_DEFAULT_HOST, REME_DEFAULT_PORT
@ -18,3 +24,35 @@ def test_mcp_client_converts_wildcard_bind_address_to_loopback():
assert client.host == "127.0.0.1"
assert client.port == 8123
@pytest.mark.asyncio
async def test_mcp_catalog_uses_sdk_field_names():
"""Tool catalogs expose SDK 2 fields while preserving schemas and user metadata."""
parameters = {
"type": "object",
"$defs": {"Query": {"type": "string"}},
"properties": {"query": {"$ref": "#/$defs/Query"}},
"required": ["query"],
}
output = {"type": "object", "properties": {"answer": {"type": "string"}}}
metadata = {"input_schema": "user-owned metadata", "_meta": {"keep": True}}
tool = Tool(
name="search",
input_schema=parameters,
output_schema=output,
annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
meta=metadata,
)
client = MCPClient()
client.client = SimpleNamespace(list_tools=AsyncMock(return_value=[tool]))
[action] = await client.list_actions()
assert action["name"] == "search"
assert action["input_schema"] == parameters
assert action["output_schema"] == output
assert action["annotations"]["read_only_hint"] is True
assert action["annotations"]["open_world_hint"] is False
assert action["meta"] == metadata
assert {"inputSchema", "outputSchema", "_meta"}.isdisjoint(action)

View file

@ -261,7 +261,7 @@ def test_qwen_tokenizer_dependency_is_optional() -> None:
"""Exact Qwen tokenization is opt-in and included in the full install."""
config = tomllib.loads((REPOSITORY / "pyproject.toml").read_text(encoding="utf-8"))
extras = config["project"]["optional-dependencies"]
assert extras["qwen-tokenizer"] == ["tokenizers>=0.21,<1"]
assert extras["qwen-tokenizer"] == ["tokenizers>=0.23.3,<1"]
assert "tokenizers" not in {Requirement(value).name for value in config["project"]["dependencies"]}
assert "tokenizers" not in {Requirement(value).name for value in extras["core"]}
assert "reme-ai[qwen-tokenizer]" in extras["full"]

View file

@ -14,6 +14,12 @@ from reme.constants import REME_SERVICE_INFO
from reme.schema import Response
@pytest.fixture(autouse=True)
def _isolate_service_info(monkeypatch):
"""Restore discovery state after service lifespans modify the environment."""
monkeypatch.setenv(REME_SERVICE_INFO, "")
def _dummy_app():
"""Minimal object needed by MCPService.build_service."""
@ -91,14 +97,32 @@ def test_explicit_service_jobs_reject_missing_disabled_and_unsupported_jobs(opti
stream_service.add_jobs(_app_with_jobs(stream=StreamJob(name="stream")))
def test_mcp_service_registers_job_with_empty_parameters():
"""Empty job parameters must remain a dict for FastMCP FunctionTool validation."""
service = MCPService()
@pytest.mark.asyncio
@pytest.mark.parametrize("service_class", [MCPService, HttpService])
@pytest.mark.parametrize("mode", ["auto", "legacy"])
@pytest.mark.parametrize("parameters", [None, {}, {"properties": {"query": {"type": "string"}}}])
async def test_mcp_services_list_and_call_jobs_without_explicit_object_schema(service_class, mode, parameters):
"""Optional job schemas must not break discovery or calls in either protocol era."""
from fastmcp import Client
service = service_class()
service.build_service(_dummy_app())
job = BaseJob(name="empty_params", parameters={})
job = BaseJob(name="optional_params", parameters=parameters)
original_parameters = dict(job.parameters)
assert service.add_job(job) is True
assert service.add_job(BaseJob(name="typed_params", parameters={"type": "object"})) is True
server = service.service if isinstance(service, MCPService) else service.mcp_server
async with Client(server, mode=mode) as client:
tools = {tool.name: tool for tool in await client.list_tools()}
assert set(tools) == {"optional_params", "typed_params"}
assert tools[job.name].input_schema == {"type": "object", **original_parameters}
assert tools["typed_params"].input_schema == {"type": "object"}
assert not (await client.call_tool(job.name, {"query": "hello"})).is_error
assert not (await client.call_tool("typed_params", {})).is_error
assert job.parameters == original_parameters
def test_mcp_service_reports_stream_job_skipped():