diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..7b5876a2 --- /dev/null +++ b/manifest.json @@ -0,0 +1,86 @@ +{ + "manifest_version": "0.3", + "name": "memU", + "display_name": "memU - AI Memory Service", + "version": "1.0.0", + "description": "memU is a memory framework built for 24/7 proactive agents. It is designed for long-running use and greatly reduces the LLM token cost of keeping agents always online, making always-on, evolving agents practical in production systems. memU continuously captures and understands user intent. Even without a command, the agent can tell what you are about to do and act on it by itself.", + "author": { + "name": "NevaMind-AI", + "email": "jinshen.yin@nevamind.ai" + }, + "homepage": "https://github.com/NevaMind-AI/memU", + "documentation": "https://memu.pro/docs", + "server": { + "type": "python", + "entry_point": "src/memu/integrations/mcp.py", + "mcp_config": { + "command": "uvx", + "args": [ + "--from", + "memu-py[mcp] @ git+https://github.com/De13ruyne/memU@feat/add_mcp", + "memu-mcp" + ], + "env": { + "OPENAI_API_KEY": "${user_config.api_key}" + } + } + }, + "tools": [ + { + "name": "memorize", + "description": "Save conversations, documents, or knowledge to memory" + }, + { + "name": "retrieve", + "description": "Search for relevant memories based on a query" + }, + { + "name": "list_memories", + "description": "List all stored memory items for a user" + }, + { + "name": "list_categories", + "description": "List all memory categories for a user" + }, + { + "name": "create_memory", + "description": "Manually create a memory item" + }, + { + "name": "update_memory", + "description": "Update an existing memory item" + }, + { + "name": "delete_memory", + "description": "Delete a specific memory item" + }, + { + "name": "clear_memory", + "description": "Clear all memories for a user" + } + ], + "user_config": { + "api_key": { + "type": "string", + "title": "LLM API Key", + "description": "OpenAI-compatible API key for chat and embedding", + "required": true, + "sensitive": true + } + }, + "keywords": [ + "memory", + "ai", + "agent", + "llm", + "conversation", + "knowledge", + "proactive", + "mcp" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/NevaMind-AI/memU" + } +} diff --git a/pyproject.toml b/pyproject.toml index e6bde8da..4cacd00b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ test = [ postgres = ["pgvector>=0.3.4", "sqlalchemy[postgresql-psycopgbinary]>=2.0.36"] langgraph = ["langgraph>=0.0.10", "langchain-core>=0.1.0"] claude = ["claude-agent-sdk>=0.1.24"] +mcp = ["mcp>=1.0", "python-dotenv>=1.0"] [project.urls] "Homepage" = "https://github.com/NevaMind-AI/MemU" @@ -78,10 +79,12 @@ claude = ["claude-agent-sdk>=0.1.24"] [project.scripts] memu-server = "memu.server.cli:main" +memu-mcp = "memu.integrations.mcp:main" [tool.deptry.per_rule_ignores] +DEP001 = ["dotenv"] # Optional dependencies used in examples/ -DEP002 = ["claude-agent-sdk"] +DEP002 = ["claude-agent-sdk", "mcp", "python-dotenv"] [tool.mypy] files = ["src", "tests"] @@ -109,6 +112,14 @@ ignore_missing_imports = true module = ["pgvector.*"] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["mcp.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["dotenv.*"] +ignore_missing_imports = true + [[tool.mypy.overrides]] module = ["memu.client.openai_wrapper"] disallow_untyped_defs = false diff --git a/src/memu/app/crud.py b/src/memu/app/crud.py index cfed7da9..fb9d5236 100644 --- a/src/memu/app/crud.py +++ b/src/memu/app/crud.py @@ -510,6 +510,7 @@ async def _patch_create_memory_item(self, state: WorkflowState, step_context: An content_embedding = (await self._get_step_embedding_client(step_context).embed(embed_payload))[0] item = store.memory_item_repo.create_item( + resource_id="", memory_type=memory_payload["type"], summary=memory_payload["content"], embedding=content_embedding, diff --git a/src/memu/integrations/__init__.py b/src/memu/integrations/__init__.py index dd2390a7..e9a451d3 100644 --- a/src/memu/integrations/__init__.py +++ b/src/memu/integrations/__init__.py @@ -1,3 +1,13 @@ -from .langgraph import MemULangGraphTools +from typing import Any + + +def __getattr__(name: str) -> Any: + if name == "MemULangGraphTools": + from .langgraph import MemULangGraphTools + + return MemULangGraphTools + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) + __all__ = ["MemULangGraphTools"] diff --git a/src/memu/integrations/mcp.py b/src/memu/integrations/mcp.py new file mode 100644 index 00000000..2958dff9 --- /dev/null +++ b/src/memu/integrations/mcp.py @@ -0,0 +1,387 @@ +"""MCP (Model Context Protocol) integration for MemU.""" + +from __future__ import annotations + +import contextlib +import json +import logging +import os +import tempfile +import uuid +from typing import Any + +from pydantic import BaseModel + +from memu.app.service import MemoryService +from memu.database.models import MemoryType + +try: + from mcp.server.fastmcp import FastMCP +except ImportError as e: + msg = "Please install 'mcp' to use the MCP integration: uv sync --extra mcp" + raise ImportError(msg) from e + +logger = logging.getLogger("memu.integrations.mcp") + +mcp_server = FastMCP("memu") + +_service: MemoryService | None = None + + +class MCPUserModel(BaseModel): + """Extended user scope model for MCP with agent and session support.""" + + user_id: str | None = None + agent_id: str | None = None + session_id: str | None = None + + +def init_mcp_server(service: MemoryService) -> Any: + """Bind a MemoryService instance to the MCP server.""" + global _service + _service = service + return mcp_server + + +def _get_service() -> MemoryService: + if _service is None: + msg = "MemoryService not initialized. Call init_mcp_server() or use the memu-mcp CLI." + raise RuntimeError(msg) + return _service + + +def _build_scope( + user_id: str, + agent_id: str | None = None, + session_id: str | None = None, +) -> dict[str, Any]: + scope: dict[str, Any] = {"user_id": user_id} + if agent_id: + scope["agent_id"] = agent_id + if session_id: + scope["session_id"] = session_id + return scope + + +def _json(result: Any) -> str: + return json.dumps(result, default=str, ensure_ascii=False) + + +# --------------------------------------------------------------------------- +# MCP Tools +# --------------------------------------------------------------------------- + + +@mcp_server.tool() +async def memorize( + content: str, + user_id: str, + modality: str = "conversation", + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """Save a conversation, document, or piece of knowledge to memory. + + Args: + content: The text content to memorize. + user_id: User identifier for scoping. + modality: Content type - "conversation", "document", "image", "video", or "audio". + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + + filename = f"memu_mcp_{uuid.uuid4()}.txt" + file_path = os.path.join(tempfile.gettempdir(), filename) + try: + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + result = await service.memorize( + resource_url=file_path, + modality=modality, + user=scope, + ) + return _json(result) + finally: + if os.path.exists(file_path): + with contextlib.suppress(OSError): + os.remove(file_path) + + +@mcp_server.tool() +async def retrieve( + query: str, + user_id: str, + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """Search for relevant memories based on a query. + + Args: + query: The search query to find relevant memories. + user_id: User identifier for scoping. + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + result = await service.retrieve( + queries=[{"role": "user", "content": {"text": query}}], + where=scope, + ) + return _json(result) + + +@mcp_server.tool() +async def list_memories( + user_id: str, + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """List all stored memory items for a user. + + Args: + user_id: User identifier for scoping. + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + result = await service.list_memory_items(where=scope) + return _json(result) + + +@mcp_server.tool() +async def list_categories( + user_id: str, + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """List all memory categories for a user. + + Args: + user_id: User identifier for scoping. + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + result = await service.list_memory_categories(where=scope) + return _json(result) + + +@mcp_server.tool() +async def create_memory( + content: str, + user_id: str, + memory_type: MemoryType = "profile", + categories: list[str] | None = None, + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """Manually create a memory item. + + Args: + content: The memory content to store. + user_id: User identifier for scoping. + memory_type: Type of memory - "profile", "event", "knowledge", "behavior", "skill", or "tool". + categories: List of category names to assign the memory to. + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + result = await service.create_memory_item( + memory_type=memory_type, + memory_content=content, + memory_categories=categories or [], + user=scope, + ) + return _json(result) + + +@mcp_server.tool() +async def update_memory( + memory_id: str, + user_id: str, + content: str | None = None, + memory_type: MemoryType | None = None, + categories: list[str] | None = None, + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """Update an existing memory item. + + Args: + memory_id: The ID of the memory item to update. + user_id: User identifier for scoping. + content: New memory content (optional). + memory_type: New memory type (optional) - "profile", "event", "knowledge", "behavior", "skill", or "tool". + categories: New list of category names (optional). + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + result = await service.update_memory_item( + memory_id=memory_id, + memory_type=memory_type, + memory_content=content, + memory_categories=categories, + user=scope, + ) + return _json(result) + + +@mcp_server.tool() +async def delete_memory( + memory_id: str, + user_id: str, + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """Delete a specific memory item by ID. + + Args: + memory_id: The ID of the memory item to delete. + user_id: User identifier for scoping. + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + result = await service.delete_memory_item( + memory_id=memory_id, + user=scope, + ) + return _json(result) + + +@mcp_server.tool() +async def clear_memory( + user_id: str, + agent_id: str | None = None, + session_id: str | None = None, +) -> str: + """Clear all memories for a user. This action is irreversible. + + Args: + user_id: User identifier for scoping. + agent_id: Optional agent identifier for multi-agent scoping. + session_id: Optional session identifier for session scoping. + """ + service = _get_service() + scope = _build_scope(user_id, agent_id, session_id) + result = await service.clear_memory(where=scope) + return _json(result) + + +# --------------------------------------------------------------------------- +# CLI entry point +# --------------------------------------------------------------------------- + + +def _resolve(cli_value: str | None, env_key: str, default: str | None = None) -> str | None: + """Resolve a config value: CLI arg > env var > default.""" + if cli_value is not None: + return cli_value + return os.environ.get(env_key, default) + + +def _build_database_config(db: str, db_path: str | None, db_dsn: str | None) -> dict[str, Any]: + if db == "sqlite": + dsn = db_path or "sqlite:///memu.db" + if not dsn.startswith("sqlite"): + dsn = f"sqlite:///{dsn}" + return { + "metadata_store": {"provider": "sqlite", "dsn": dsn}, + } + if db == "postgres": + if not db_dsn: + msg = "PostgreSQL requires --db-dsn or MEMU_DB_DSN" + raise ValueError(msg) + return { + "metadata_store": {"provider": "postgres", "dsn": db_dsn}, + } + return {"metadata_store": {"provider": "inmemory"}} + + +def main() -> None: + """CLI entry point for the memU MCP server.""" + import argparse + + with contextlib.suppress(ImportError): + from dotenv import load_dotenv + + load_dotenv() + + parser = argparse.ArgumentParser( + description="memU MCP Server - Expose MemoryService as MCP tools", + ) + parser.add_argument("--api-key", default=None, help="LLM API key (env: OPENAI_API_KEY)") + parser.add_argument("--base-url", default=None, help="LLM base URL (env: OPENAI_BASE_URL)") + parser.add_argument("--chat-model", default=None, help="Chat model name (env: MEMU_CHAT_MODEL)") + parser.add_argument("--embed-model", default=None, help="Embedding model name (env: MEMU_EMBED_MODEL)") + parser.add_argument( + "--embed-api-key", default=None, help="Embedding API key, if different from --api-key (env: MEMU_EMBED_API_KEY)" + ) + parser.add_argument( + "--embed-base-url", + default=None, + help="Embedding base URL, if different from --base-url (env: MEMU_EMBED_BASE_URL)", + ) + parser.add_argument( + "--db", + default=None, + choices=["inmemory", "sqlite", "postgres"], + help="Storage backend (env: MEMU_DB, default: inmemory)", + ) + parser.add_argument("--db-path", default=None, help="SQLite database path (env: MEMU_DB_PATH)") + parser.add_argument("--db-dsn", default=None, help="PostgreSQL DSN (env: MEMU_DB_DSN)") + parser.add_argument( + "--transport", + default="stdio", + choices=["stdio", "sse"], + help="MCP transport (default: stdio)", + ) + + args = parser.parse_args() + + api_key = _resolve(args.api_key, "OPENAI_API_KEY") + if not api_key: + parser.error("LLM API key is required. Set --api-key or OPENAI_API_KEY environment variable.") + + base_url = _resolve(args.base_url, "OPENAI_BASE_URL", "https://api.openai.com/v1") + chat_model = _resolve(args.chat_model, "MEMU_CHAT_MODEL", "gpt-4o-mini") + embed_model = _resolve(args.embed_model, "MEMU_EMBED_MODEL") + embed_api_key = _resolve(args.embed_api_key, "MEMU_EMBED_API_KEY") + embed_base_url = _resolve(args.embed_base_url, "MEMU_EMBED_BASE_URL") + db = _resolve(args.db, "MEMU_DB", "inmemory") or "inmemory" + db_path = _resolve(args.db_path, "MEMU_DB_PATH") + db_dsn = _resolve(args.db_dsn, "MEMU_DB_DSN") + + llm_default: dict[str, Any] = { + "api_key": api_key, + "base_url": base_url, + "chat_model": chat_model, + } + llm_profiles: dict[str, Any] = {"default": llm_default} + if embed_model or embed_api_key or embed_base_url: + llm_profiles["embedding"] = { + "api_key": embed_api_key or api_key, + "base_url": embed_base_url or base_url, + "embed_model": embed_model, + } + + database_config = _build_database_config(db, db_path, db_dsn) + + service = MemoryService( + llm_profiles=llm_profiles, + database_config=database_config, + user_config={"model": MCPUserModel}, + ) + init_mcp_server(service) + + logger.info("Starting memU MCP server (transport=%s, db=%s)", args.transport, db) + mcp_server.run(transport=args.transport) diff --git a/uv.lock b/uv.lock index e7dea60e..25ddcfdc 100644 --- a/uv.lock +++ b/uv.lock @@ -458,7 +458,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -466,7 +465,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -474,7 +472,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, @@ -932,7 +929,7 @@ wheels = [ [[package]] name = "memu-py" -version = "1.3.0" +version = "1.4.0" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -955,6 +952,10 @@ langgraph = [ { name = "langchain-core" }, { name = "langgraph" }, ] +mcp = [ + { name = "mcp" }, + { name = "python-dotenv" }, +] postgres = [ { name = "pgvector" }, { name = "sqlalchemy", extra = ["postgresql-psycopgbinary"] }, @@ -1006,15 +1007,17 @@ requires-dist = [ { name = "langchain-core", marker = "extra == 'langgraph'", specifier = ">=0.1.0" }, { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=0.0.10" }, { name = "lazyllm", specifier = ">=0.7.3" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.0" }, { name = "numpy", specifier = ">=2.3.4" }, { name = "openai", specifier = ">=2.8.0" }, { name = "pendulum", specifier = ">=3.1.0" }, { name = "pgvector", marker = "extra == 'postgres'", specifier = ">=0.3.4" }, { name = "pydantic", specifier = ">=2.12.4" }, + { name = "python-dotenv", marker = "extra == 'mcp'", specifier = ">=1.0" }, { name = "sqlalchemy", extras = ["postgresql-psycopgbinary"], marker = "extra == 'postgres'", specifier = ">=2.0.36" }, { name = "sqlmodel", specifier = ">=0.0.27" }, ] -provides-extras = ["postgres", "langgraph", "claude"] +provides-extras = ["postgres", "langgraph", "claude", "mcp"] [package.metadata.requires-dev] dev = [