From 536da417759eb84fd5e9e547c7490f8905be3893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Thu, 20 Nov 2025 19:16:22 +0100 Subject: [PATCH] merge mcp --- .env.example | 2 + .../python/memora_client/memora_client.py | 126 +++++++++++++++ memora-mcp-server/README.md | 44 ----- memora-mcp-server/__init__.py | 3 - memora-mcp-server/client.py | 62 ------- memora-mcp-server/config.py | 26 --- memora-mcp-server/pyproject.toml | 20 --- memora-mcp-server/server.py | 105 ------------ memora/memora/api/__init__.py | 101 ++++++++++++ memora/memora/{api.py => api/http.py} | 0 memora/memora/api/mcp.py | 130 +++++++++++++++ memora/memora/web/server.py | 12 +- memora/pyproject.toml | 3 + memora/tests/mcp_server/test_server.py | 153 ++++++++++++++++++ standalone/.dockerignore | 46 ------ standalone/.env.example | 9 -- standalone/.env.standalone | 15 -- standalone/.gitignore | 5 - standalone/Dockerfile | 87 ---------- standalone/QUICKSTART.md | 106 ------------ standalone/README.md | 126 --------------- standalone/build-docker.sh | 87 ---------- standalone/docker-compose.yml | 21 --- standalone/init.sh | 58 ------- standalone/run-docker.sh | 122 -------------- standalone/supervisord.conf | 42 ----- uv.lock | 22 +-- 27 files changed, 531 insertions(+), 1002 deletions(-) delete mode 100644 memora-mcp-server/README.md delete mode 100644 memora-mcp-server/__init__.py delete mode 100644 memora-mcp-server/client.py delete mode 100644 memora-mcp-server/config.py delete mode 100644 memora-mcp-server/pyproject.toml delete mode 100644 memora-mcp-server/server.py create mode 100644 memora/memora/api/__init__.py rename memora/memora/{api.py => api/http.py} (100%) create mode 100644 memora/memora/api/mcp.py create mode 100644 memora/tests/mcp_server/test_server.py delete mode 100644 standalone/.dockerignore delete mode 100644 standalone/.env.example delete mode 100644 standalone/.env.standalone delete mode 100644 standalone/.gitignore delete mode 100644 standalone/Dockerfile delete mode 100644 standalone/QUICKSTART.md delete mode 100644 standalone/README.md delete mode 100755 standalone/build-docker.sh delete mode 100644 standalone/docker-compose.yml delete mode 100755 standalone/init.sh delete mode 100755 standalone/run-docker.sh delete mode 100644 standalone/supervisord.conf diff --git a/.env.example b/.env.example index 143e32df..a0963187 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,8 @@ MEMORA_API_LLM_API_KEY=your_api_key_here # MEMORA_API_HOST=0.0.0.0 # MEMORA_API_PORT=8080 +MEMORA_API_MCP_ENABLED=true + # ============================================================================= # CONTROL PLANE SERVICE (MEMORA_CP_*) # ============================================================================= diff --git a/memora-clients/python/memora_client/memora_client.py b/memora-clients/python/memora_client/memora_client.py index e77e0409..4481c568 100644 --- a/memora-clients/python/memora_client/memora_client.py +++ b/memora-clients/python/memora_client/memora_client.py @@ -278,6 +278,132 @@ class Memora: response = _run_async(self._agent_api.create_or_update_agent(agent_id, request_obj)) return response.to_dict() if hasattr(response, 'to_dict') else response + # Async methods (native async, no _run_async wrapper) + + async def aput_batch( + self, + agent_id: str, + items: List[Dict[str, Any]], + document_id: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Store multiple memories in batch (async). + + Args: + agent_id: The agent ID + items: List of memory items with 'content' and optional 'event_date', 'context' + document_id: Optional document ID for grouping memories + + Returns: + Response with success status and item count + """ + memory_items = [ + memory_item.MemoryItem( + content=item["content"], + event_date=item.get("event_date"), + context=item.get("context"), + ) + for item in items + ] + + request_obj = batch_put_request.BatchPutRequest( + items=memory_items, + document_id=document_id, + ) + + response = await self._memory_api.batch_put_memories(agent_id, request_obj) + return response.to_dict() if hasattr(response, 'to_dict') else response + + async def aput( + self, + agent_id: str, + content: str, + event_date: Optional[datetime] = None, + context: Optional[str] = None, + document_id: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Store a single memory (async). + + Args: + agent_id: The agent ID + content: Memory content + event_date: Optional event timestamp + context: Optional context description + document_id: Optional document ID for grouping + + Returns: + Response with success status + """ + return await self.aput_batch( + agent_id=agent_id, + items=[{"content": content, "event_date": event_date, "context": context}], + document_id=document_id, + ) + + async def asearch( + self, + agent_id: str, + query: str, + fact_type: Optional[List[str]] = None, + max_tokens: int = 4096, + thinking_budget: int = 100, + ) -> List[Dict[str, Any]]: + """ + Search memories using semantic similarity (async). + + Args: + agent_id: The agent ID + query: Search query + fact_type: Optional list of fact types to filter (world, agent, opinion) + max_tokens: Maximum tokens in results (default: 4096) + thinking_budget: Token budget for search (default: 100) + + Returns: + List of search results + """ + request_obj = search_request.SearchRequest( + query=query, + fact_type=fact_type, + thinking_budget=thinking_budget, + max_tokens=max_tokens, + trace=False, + ) + + response = await self._memory_api.search_memories(agent_id, request_obj) + + if hasattr(response, 'results'): + return [r.to_dict() if hasattr(r, 'to_dict') else r for r in response.results] + return [] + + async def athink( + self, + agent_id: str, + query: str, + thinking_budget: int = 50, + context: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Generate a contextual answer based on agent identity and memories (async). + + Args: + agent_id: The agent ID + query: The question or prompt + thinking_budget: Token budget for thinking (default: 50) + context: Optional additional context + + Returns: + Response with answer text, facts used, and new opinions + """ + request_obj = think_request.ThinkRequest( + query=query, + thinking_budget=thinking_budget, + context=context, + ) + + response = await self._reasoning_api.think(agent_id, request_obj) + return response.to_dict() if hasattr(response, 'to_dict') else response + # Alias for backward compatibility MemoraClient = Memora diff --git a/memora-mcp-server/README.md b/memora-mcp-server/README.md deleted file mode 100644 index f19a899d..00000000 --- a/memora-mcp-server/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Memora MCP Server - -Remote MCP server for integrating Memora memory capabilities with Claude Desktop and other MCP clients. - -## Configuration - -Required environment variables: -- `MEMORA_AGENT_ID`: The agent ID to use for all operations -- `MEMORA_API_URL`: Memora API endpoint (default: http://localhost:8080) -- `MEMORA_API_KEY`: API key for authentication (optional) - -## Usage - -### Start the HTTP/SSE Server -```bash -export MEMORA_AGENT_ID=your-agent-id -export MEMORA_API_URL=http://localhost:8080 -export PORT=8765 # optional, default is 8765 -export HOST=127.0.0.1 # optional, default is 127.0.0.1 -uv run memora-mcp-server -``` - -The server will start on `http://127.0.0.1:8765` - -### Claude Desktop Integration - -Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): - -```json -{ - "mcpServers": { - "memora": { - "url": "http://127.0.0.1:8765/sse" - } - } -} -``` - -Make sure the Memora MCP server is running before starting Claude Desktop. - -## Available Tools - -- `memora_put`: Store facts/memories with required context -- `memora_search`: Search through memories using semantic search diff --git a/memora-mcp-server/__init__.py b/memora-mcp-server/__init__.py deleted file mode 100644 index 71d4c292..00000000 --- a/memora-mcp-server/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Memora MCP Server - Remote MCP server for Memora memory system.""" - -__version__ = "0.0.1" diff --git a/memora-mcp-server/client.py b/memora-mcp-server/client.py deleted file mode 100644 index 54e7bb15..00000000 --- a/memora-mcp-server/client.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Memora API client wrapper.""" - -import httpx -from typing import Any - - -class MemoraClient: - """Client for interacting with Memora API.""" - - def __init__(self, api_url: str, agent_id: str, api_key: str | None = None): - self.api_url = api_url.rstrip("/") - self.agent_id = agent_id - self.headers = {} - if api_key: - self.headers["Authorization"] = f"Bearer {api_key}" - - async def remember( - self, content: str, context: str - ) -> dict[str, Any]: - """Store a memory using batch endpoint.""" - async with httpx.AsyncClient() as client: - payload = { - "agent_id": self.agent_id, - "items": [ - { - "content": content, - "context": context, - } - ] - } - - response = await client.post( - f"{self.api_url}/api/memories/batch", - json=payload, - headers=self.headers, - timeout=30.0, - ) - response.raise_for_status() - return response.json() - - async def search( - self, query: str, max_tokens: int = 4096 - ) -> dict[str, Any]: - """Search memories using search endpoint.""" - async with httpx.AsyncClient() as client: - payload = { - "agent_id": self.agent_id, - "query": query, - "thinking_budget": 100, - "max_tokens": max_tokens, - "reranker": "heuristic", - "trace": False, - } - - response = await client.post( - f"{self.api_url}/api/search", - json=payload, - headers=self.headers, - timeout=30.0, - ) - response.raise_for_status() - return response.json() diff --git a/memora-mcp-server/config.py b/memora-mcp-server/config.py deleted file mode 100644 index ee5970ad..00000000 --- a/memora-mcp-server/config.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Configuration management for Memora MCP Server.""" - -import os -from dataclasses import dataclass - - -@dataclass -class Config: - """MCP Server configuration.""" - - agent_id: str - api_url: str = "http://localhost:8080" - api_key: str | None = None - - @classmethod - def from_env(cls) -> "Config": - """Load configuration from environment variables.""" - agent_id = os.getenv("MEMORA_AGENT_ID") - if not agent_id: - raise ValueError("MEMORA_AGENT_ID environment variable is required") - - return cls( - agent_id=agent_id, - api_url=os.getenv("MEMORA_API_URL", "http://localhost:8080"), - api_key=os.getenv("MEMORA_API_KEY"), - ) diff --git a/memora-mcp-server/pyproject.toml b/memora-mcp-server/pyproject.toml deleted file mode 100644 index 6722d688..00000000 --- a/memora-mcp-server/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[project] -name = "memora-mcp-server" -version = "0.0.1" -description = "Remote MCP server for Memora memory system" -readme = "README.md" -requires-python = ">=3.11" -dependencies = [ - "fastmcp>=0.7.0", - "httpx>=0.28.1", -] - -[project.scripts] -memora-mcp-server = "server:main" - -[tool.hatch.build.targets.wheel] -packages = ["."] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/memora-mcp-server/server.py b/memora-mcp-server/server.py deleted file mode 100644 index 1526a307..00000000 --- a/memora-mcp-server/server.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Memora MCP Server implementation using FastMCP.""" - -import json -import logging -import os - -from fastmcp import FastMCP - -from config import Config -from client import MemoraClient - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Load config -config = Config.from_env() -client = MemoraClient( - api_url=config.api_url, - agent_id=config.agent_id, - api_key=config.api_key, -) - -# Create FastMCP server -mcp = FastMCP("memora-mcp-server") - - -@mcp.tool() -async def memora_put(content: str, context: str) -> str: - """ - **CRITICAL: Store important user information to long-term memory.** - - Use this tool PROACTIVELY whenever the user shares: - - Personal facts, preferences, or interests (e.g., "I love hiking", "I'm a vegetarian") - - Important events or milestones (e.g., "I got promoted", "My birthday is June 15") - - User history, experiences, or background (e.g., "I used to work at Google", "I studied CS at MIT") - - Decisions, opinions, or stated preferences (e.g., "I prefer Python over JavaScript") - - Goals, plans, or future intentions (e.g., "I'm planning to visit Japan next year") - - Relationships or people mentioned (e.g., "My manager Sarah", "My wife Alice") - - Work context, projects, or responsibilities - - Any other information the user would want remembered for future conversations - - **When to use**: Immediately after user shares personal information. Don't ask permission - just store it naturally. - - **Context guidelines**: Use descriptive contexts like "personal_preferences", "work_history", "family", "hobbies", - "career_goals", "project_details", etc. This helps organize and retrieve related memories later. - - Args: - content: The fact/memory to store (be specific and include relevant details) - context: Categorize the memory (e.g., 'personal_preferences', 'work_history', 'hobbies', 'family') - """ - try: - result = await client.remember(content=content, context=context) - return f"Fact stored successfully: {result.get('message', 'Success')}" - except Exception as e: - logger.error(f"Error storing fact: {e}", exc_info=True) - return f"Error: {str(e)}" - - -@mcp.tool() -async def memora_search(query: str, max_tokens: int = 4096) -> str: - """ - **CRITICAL: Search user's memory to provide personalized, context-aware responses.** - - Use this tool PROACTIVELY at the start of conversations or when making recommendations to: - - Check user's preferences before making suggestions (e.g., "what foods does the user like?") - - Recall user's history to provide continuity (e.g., "what projects has the user worked on?") - - Remember user's goals and context (e.g., "what is the user trying to accomplish?") - - Avoid repeating information or asking questions you should already know - - Personalize responses based on user's background, interests, and past interactions - - Reference past conversations or events the user mentioned - - **When to use**: - - Start of conversation: Search for relevant context about the user - - Before recommendations: Check user preferences and past experiences - - When user asks about something they may have mentioned before - - To provide continuity across conversations - - **Search tips**: Use natural language queries like "user's programming language preferences", - "user's work experience", "user's dietary restrictions", "what does the user know about X?" - - Args: - query: Natural language search query to find relevant memories - max_tokens: Maximum tokens for search context (default: 4096) - """ - try: - result = await client.search(query=query, max_tokens=max_tokens) - return json.dumps(result, indent=2) - except Exception as e: - logger.error(f"Error searching: {e}", exc_info=True) - return f"Error: {str(e)}" - - -def main(): - """Main entry point.""" - port = int(os.getenv("PORT", "8765")) - host = os.getenv("HOST", "127.0.0.1") - - logger.info(f"Starting Memora MCP Server for agent: {config.agent_id}") - logger.info(f"MCP server starting on http://{host}:{port}") - - mcp.run(transport="sse", host=host, port=port) - - -if __name__ == "__main__": - main() diff --git a/memora/memora/api/__init__.py b/memora/memora/api/__init__.py new file mode 100644 index 00000000..d598b5db --- /dev/null +++ b/memora/memora/api/__init__.py @@ -0,0 +1,101 @@ +""" +Unified API module for Memora. + +Provides both HTTP REST API and MCP (Model Context Protocol) server. +""" +import logging +from typing import Optional +from fastapi import FastAPI + +from memora import TemporalSemanticMemory + +logger = logging.getLogger(__name__) + + +def create_app( + memory: TemporalSemanticMemory, + http_api_enabled: bool = True, + mcp_api_enabled: bool = False, + mcp_mount_path: str = "/mcp", + run_migrations: bool = True, + initialize_memory: bool = True +) -> FastAPI: + """ + Create and configure the unified Memora API application. + + Args: + memory: TemporalSemanticMemory instance (already initialized with required parameters) + http_api_enabled: Whether to enable HTTP REST API endpoints (default: True) + mcp_api_enabled: Whether to enable MCP server (default: False) + mcp_mount_path: Path to mount MCP server (default: /mcp) + run_migrations: Whether to run database migrations on startup (default: True) + initialize_memory: Whether to initialize memory system on startup (default: True) + + Returns: + Configured FastAPI application with enabled APIs + + Example: + # HTTP only + app = create_app(memory) + + # MCP only + app = create_app(memory, http_api_enabled=False, mcp_api_enabled=True) + + # Both HTTP and MCP + app = create_app(memory, mcp_api_enabled=True) + """ + + # Import and create HTTP API if enabled + if http_api_enabled: + from .http import create_app as create_http_app + app = create_http_app( + memory=memory, + run_migrations=run_migrations, + initialize_memory=initialize_memory + ) + logger.info("HTTP REST API enabled") + else: + # Create minimal FastAPI app + app = FastAPI(title="Memora API", version="0.0.7") + logger.info("HTTP REST API disabled") + + # Mount MCP server if enabled + if mcp_api_enabled: + try: + from .mcp import create_mcp_server + + # Create MCP server with shared memory instance + mcp_server = create_mcp_server(memory=memory) + + # Mount at specified path + app.mount(mcp_mount_path, mcp_server.sse_app()) + logger.info(f"MCP server enabled at {mcp_mount_path}/sse") + except ImportError as e: + logger.error(f"MCP server requested but dependencies not available: {e}") + logger.error("Install with: pip install memora[mcp]") + raise + + return app + + +# Re-export commonly used items for backwards compatibility +from .http import ( + SearchRequest, + SearchResult, + SearchResponse, + MemoryItem, + BatchPutRequest, + ThinkRequest, + ThinkResponse, +) + +__all__ = [ + "create_app", + "SearchRequest", + "SearchResult", + "SearchResponse", + "MemoryItem", + "BatchPutRequest", + "ThinkRequest", + "ThinkResponse", +] diff --git a/memora/memora/api.py b/memora/memora/api/http.py similarity index 100% rename from memora/memora/api.py rename to memora/memora/api/http.py diff --git a/memora/memora/api/mcp.py b/memora/memora/api/mcp.py new file mode 100644 index 00000000..bb14f2d6 --- /dev/null +++ b/memora/memora/api/mcp.py @@ -0,0 +1,130 @@ +"""Memora MCP Server implementation using FastMCP.""" + +import json +import logging + +from fastmcp import FastMCP +from memora import TemporalSemanticMemory + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def create_mcp_server(memory: TemporalSemanticMemory) -> FastMCP: + """ + Create and configure the Memora MCP server. + + Args: + memory: TemporalSemanticMemory instance (required) + + Returns: + Configured FastMCP server instance + """ + # Create FastMCP server + mcp = FastMCP("memora-mcp-server") + + @mcp.tool() + async def memora_put(agent_id: str, content: str, context: str, explanation: str = "") -> str: + """ + **CRITICAL: Store important user information to long-term memory.** + + Use this tool PROACTIVELY whenever the user shares: + - Personal facts, preferences, or interests (e.g., "I love hiking", "I'm a vegetarian") + - Important events or milestones (e.g., "I got promoted", "My birthday is June 15") + - User history, experiences, or background (e.g., "I used to work at Google", "I studied CS at MIT") + - Decisions, opinions, or stated preferences (e.g., "I prefer Python over JavaScript") + - Goals, plans, or future intentions (e.g., "I'm planning to visit Japan next year") + - Relationships or people mentioned (e.g., "My manager Sarah", "My wife Alice") + - Work context, projects, or responsibilities + - Any other information the user would want remembered for future conversations + + **When to use**: Immediately after user shares personal information. Don't ask permission - just store it naturally. + + **Context guidelines**: Use descriptive contexts like "personal_preferences", "work_history", "family", "hobbies", + "career_goals", "project_details", etc. This helps organize and retrieve related memories later. + + Args: + agent_id: The unique identifier for the agent/user storing the memory + content: The fact/memory to store (be specific and include relevant details) + context: Categorize the memory (e.g., 'personal_preferences', 'work_history', 'hobbies', 'family') + explanation: Optional explanation for why this memory is being stored + """ + try: + # Log explanation if provided + if explanation: + logger.debug(f"Explanation: {explanation}") + + # Store memory using put_batch_async + await memory.put_batch_async( + agent_id=agent_id, + contents=[{"content": content, "context": context}] + ) + return f"Fact stored successfully" + except Exception as e: + logger.error(f"Error storing fact: {e}", exc_info=True) + return f"Error: {str(e)}" + + @mcp.tool() + async def memora_search(agent_id: str, query: str, max_tokens: int = 4096, explanation: str = "") -> str: + """ + **CRITICAL: Search user's memory to provide personalized, context-aware responses.** + + Use this tool PROACTIVELY at the start of conversations or when making recommendations to: + - Check user's preferences before making suggestions (e.g., "what foods does the user like?") + - Recall user's history to provide continuity (e.g., "what projects has the user worked on?") + - Remember user's goals and context (e.g., "what is the user trying to accomplish?") + - Avoid repeating information or asking questions you should already know + - Personalize responses based on user's background, interests, and past interactions + - Reference past conversations or events the user mentioned + + **When to use**: + - Start of conversation: Search for relevant context about the user + - Before recommendations: Check user preferences and past experiences + - When user asks about something they may have mentioned before + - To provide continuity across conversations + + **Search tips**: Use natural language queries like "user's programming language preferences", + "user's work experience", "user's dietary restrictions", "what does the user know about X?" + + Args: + agent_id: The unique identifier for the agent/user whose memories to search + query: Natural language search query to find relevant memories + max_tokens: Maximum tokens for search context (default: 4096) + explanation: Optional explanation for why this search is being performed + """ + try: + # Log all parameters for debugging + logger.info(f"memora_search called with: query={query!r}, max_tokens={max_tokens}, explanation={explanation!r}") + + # Log explanation if provided + if explanation: + logger.debug(f"Explanation: {explanation}") + + # Search using search_async + search_result = await memory.search_async( + agent_id=agent_id, + query=query, + fact_type=["world", "agent", "opinion"], # Search all fact types + max_tokens=max_tokens, + thinking_budget=100 + ) + + # Convert results to dict format + results = [ + { + "id": fact.id, + "text": fact.text, + "type": fact.fact_type, + "context": fact.context, + "event_date": fact.event_date, # Already a string from the database + "document_id": fact.document_id + } + for fact in search_result.results + ] + + return json.dumps({"results": results}, indent=2) + except Exception as e: + logger.error(f"Error searching: {e}", exc_info=True) + return json.dumps({"error": str(e), "results": []}) + + return mcp diff --git a/memora/memora/web/server.py b/memora/memora/web/server.py index c79a7f0d..4d89883d 100644 --- a/memora/memora/web/server.py +++ b/memora/memora/web/server.py @@ -22,7 +22,17 @@ _memory = TemporalSemanticMemory( memory_llm_model=os.getenv("MEMORA_API_LLM_MODEL", "openai/gpt-oss-120b"), memory_llm_base_url=os.getenv("MEMORA_API_LLM_BASE_URL") or None, ) -app = create_app(_memory) + +# Check if MCP should be enabled +mcp_enabled = os.getenv("MEMORA_API_MCP_ENABLED", "true").lower() == "true" + +# Create unified app with both HTTP and optionally MCP +app = create_app( + memory=_memory, + http_api_enabled=True, + mcp_api_enabled=mcp_enabled, + mcp_mount_path="/mcp" +) if __name__ == "__main__": diff --git a/memora/pyproject.toml b/memora/pyproject.toml index 2cf64230..eb3b12f7 100644 --- a/memora/pyproject.toml +++ b/memora/pyproject.toml @@ -35,6 +35,9 @@ test = [ "pytest-asyncio>=0.21.0", "pytest-timeout>=2.4.0", ] +mcp = [ + "fastmcp>=2.0.0", +] [tool.hatch.build.targets.wheel] packages = ["memora"] diff --git a/memora/tests/mcp_server/test_server.py b/memora/tests/mcp_server/test_server.py new file mode 100644 index 00000000..ae02f8cc --- /dev/null +++ b/memora/tests/mcp_server/test_server.py @@ -0,0 +1,153 @@ +"""Test MCP server with real server and client.""" + +import asyncio +import os +import pytest +from mcp import ClientSession +from mcp.client.sse import sse_client + + +# Note: MCP server tests now require the full web server to be running +# with MEMORA_API_MCP_ENABLED=true since there's no standalone MCP server anymore. +# These tests are kept for documentation but may need manual server setup. + +pytest.skip("MCP server is now integrated with web server. Run web server with MEMORA_API_MCP_ENABLED=true to test.", allow_module_level=True) + + +@pytest.mark.asyncio +async def test_mcp_server_tools_via_sse(mcp_server): + """Test MCP server tools via SSE transport using proper MCP client.""" + sse_url = mcp_server + + async with sse_client(sse_url) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + + # Test 1: List tools + tools_list = await session.list_tools() + print(f"Tools: {tools_list}") + tool_names = [t.name for t in tools_list.tools] + assert "memora_search" in tool_names + assert "memora_put" in tool_names + + # Test 2: Call memora_put + put_result = await session.call_tool( + "memora_put", + arguments={ + "content": "User loves Python programming", + "context": "programming_preferences", + "explanation": "Storing user's programming language preference" + } + ) + print(f"Put result: {put_result}") + assert put_result is not None + + # Wait a bit for indexing + await asyncio.sleep(1) + + # Test 3: Call memora_search + search_result = await session.call_tool( + "memora_search", + arguments={ + "query": "What programming languages does the user like?", + "max_tokens": 4096, + "explanation": "Searching for programming preferences" + } + ) + print(f"Search result: {search_result}") + assert search_result is not None + + +@pytest.mark.asyncio +async def test_multiple_concurrent_requests(mcp_server): + """Test multiple concurrent requests from a single session.""" + sse_url = mcp_server + + async with sse_client(sse_url) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + + # Fire off 10 concurrent search requests from same session + async def make_search(idx): + try: + result = await session.call_tool( + "memora_search", + arguments={ + "query": f"test query {idx}", + "explanation": f"Concurrent test {idx}" + } + ) + return idx, "success", result + except Exception as e: + return idx, "error", str(e) + + tasks = [make_search(i) for i in range(10)] + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Check results + successes = 0 + failures = 0 + + for result in results: + if isinstance(result, Exception): + print(f"Request failed with exception: {result}") + failures += 1 + else: + idx, status, data = result + if status == "success": + successes += 1 + else: + print(f"Request {idx} failed: {data}") + failures += 1 + + print(f"Successes: {successes}, Failures: {failures}") + + # We expect all requests to succeed + assert successes >= 8, f"Too many failures: {failures}/10" + + +@pytest.mark.asyncio +async def test_race_condition_with_rapid_requests(mcp_server): + """Test rapid-fire requests with multiple sessions to trigger race condition.""" + sse_url = mcp_server + + async def rapid_session_search(idx): + """Create a new session and immediately make a request.""" + try: + async with sse_client(sse_url) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + + # Make request immediately after initialization + result = await session.call_tool( + "memora_search", + arguments={ + "query": f"rapid query {idx}", + "max_tokens": 2048 + } + ) + return idx, "success", result + except Exception as e: + return idx, "error", str(e) + + # Fire 20 requests with minimal delay, each with its own session + tasks = [rapid_session_search(i) for i in range(20)] + results = await asyncio.gather(*tasks) + + # Analyze results + errors = [] + for idx, status, data in results: + if status == "error": + errors.append((idx, data)) + + if errors: + print(f"Found {len(errors)} errors:") + for idx, error_msg in errors: + print(f" Request {idx}: {error_msg}") + + # Most requests should succeed + assert len(errors) < 5, f"Too many errors: {len(errors)}/20" + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"]) diff --git a/standalone/.dockerignore b/standalone/.dockerignore deleted file mode 100644 index 905f41ac..00000000 --- a/standalone/.dockerignore +++ /dev/null @@ -1,46 +0,0 @@ -# Build artifacts -**/*.pyc -**/__pycache__/ -**/.pytest_cache/ -**/.venv/ -**/venv/ -**/*.egg-info/ -**/dist/ -**/build/ - -# Node -**/node_modules/ -**/.next/ -**/npm-debug.log -**/.turbo/ - -# Environment files -.env -.env.* -!standalone/.env.standalone - -# Git -.git/ -.gitignore -.gitattributes - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# Test and dev files -**/tests/ -local-db/ -logs/ - -# Documentation (except standalone README) -README.md -!standalone/README.md - -# Standalone files -standalone/build-docker.sh -standalone/.dockerignore -standalone/.env.example -standalone/docker-compose.yml diff --git a/standalone/.env.example b/standalone/.env.example deleted file mode 100644 index 64bc282b..00000000 --- a/standalone/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Environment variables for docker-compose -# Copy this file to .env and customize as needed - -# Optional: OpenAI API key -# OPENAI_API_KEY=your-api-key-here - -# Optional: Custom embedding model -# EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2 -# EMBEDDING_DIM=384 diff --git a/standalone/.env.standalone b/standalone/.env.standalone deleted file mode 100644 index 05bd6a1a..00000000 --- a/standalone/.env.standalone +++ /dev/null @@ -1,15 +0,0 @@ -# Standalone environment configuration -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/memora -DATAPLANE_API_URL=http://localhost:8080 - -# Embedding configuration -EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2 -EMBEDDING_DIM=384 - -# LLM Provider (set to "none" to disable LLM features) -LLM_PROVIDER=none - -# Optional: LLM API Keys -# OPENAI_API_KEY=your-openai-key-here -# ANTHROPIC_API_KEY=your-anthropic-key-here -# GROQ_API_KEY=your-groq-key-here diff --git a/standalone/.gitignore b/standalone/.gitignore deleted file mode 100644 index 844a5129..00000000 --- a/standalone/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Environment files -.env - -# Docker volumes -*.log diff --git a/standalone/Dockerfile b/standalone/Dockerfile deleted file mode 100644 index 7bbe36c2..00000000 --- a/standalone/Dockerfile +++ /dev/null @@ -1,87 +0,0 @@ -FROM node:20-alpine AS control-plane-builder - -# Build control plane -WORKDIR /app/memora-control-plane -COPY memora-control-plane/package*.json ./ -RUN npm ci - -COPY memora-control-plane/ ./ -# Set env to skip font optimization during build -ENV NEXT_TELEMETRY_DISABLED=1 -RUN npm run build || (echo "Build failed, retrying..." && npm run build) - -# Python source stage - just copy files, don't build venv yet -FROM python:3.12-slim AS dataplane-source - -WORKDIR /build -COPY pyproject.toml uv.lock ./ -COPY memora/ ./memora/ -COPY memora-dev/ ./memora-dev/ - -# Final runtime image -FROM python:3.12-slim - -# Install system dependencies and PostgreSQL -RUN apt-get update && apt-get install -y \ - gnupg \ - lsb-release \ - wget \ - curl \ - ca-certificates \ - && mkdir -p /etc/apt/keyrings \ - && wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/keyrings/pgdg.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ - && apt-get update && apt-get install -y \ - postgresql-15 \ - postgresql-15-pgvector \ - postgresql-contrib-15 \ - nodejs \ - npm \ - supervisor \ - && rm -rf /var/lib/apt/lists/* - -# Install uv -RUN pip install uv - -# Create app directory -WORKDIR /app - -# Copy dataplane source from builder -COPY --from=dataplane-source /build /app - -# Build venv in the final stage to ensure compatibility -RUN cd /app && uv sync --frozen - -# Copy control plane from builder -COPY --from=control-plane-builder /app/memora-control-plane/.next/standalone /app/memora-control-plane -COPY --from=control-plane-builder /app/memora-control-plane/.next/static /app/memora-control-plane/.next/static -COPY memora-control-plane/start-server.sh /app/memora-control-plane/start-server.sh -RUN chmod +x /app/memora-control-plane/start-server.sh - -# Copy standalone configuration -COPY standalone/supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY standalone/init.sh /app/init.sh -COPY standalone/.env.standalone /app/.env - -RUN chmod +x /app/init.sh - -# PostgreSQL setup -RUN mkdir -p /var/lib/postgresql/data && \ - chown -R postgres:postgres /var/lib/postgresql && \ - mkdir -p /var/run/postgresql && \ - chown -R postgres:postgres /var/run/postgresql - -# Initialize PostgreSQL as postgres user -USER postgres -RUN /usr/lib/postgresql/15/bin/initdb -D /var/lib/postgresql/data - -USER root - -# Expose ports -# 5432: PostgreSQL -# 8080: Dataplane API -# 3000: Control Plane -EXPOSE 5432 8080 3000 - -# Start supervisor -CMD ["/app/init.sh"] diff --git a/standalone/QUICKSTART.md b/standalone/QUICKSTART.md deleted file mode 100644 index 05d04cb1..00000000 --- a/standalone/QUICKSTART.md +++ /dev/null @@ -1,106 +0,0 @@ -# Memora Standalone - Quick Start - -## What is this? - -A single Docker image containing everything you need to run Memora: -- ✅ PostgreSQL database -- ✅ Dataplane API (FastAPI backend) -- ✅ Control Plane (Next.js web UI) - -## Fastest Start (Docker Compose) - -```bash -cd standalone -docker-compose up -d -``` - -Access the UI at: **http://localhost:3000** - -## Manual Docker Build & Run - -### Build the image: -```bash -./standalone/build-docker.sh -``` - -### Run with the helper script: -```bash -./standalone/run-docker.sh --persist -``` - -### Or run directly: -```bash -docker run -d \ - --name memora \ - -p 3000:3000 \ - -p 8080:8080 \ - -p 5432:5432 \ - -v memora-data:/var/lib/postgresql/data \ - memora-standalone:latest -``` - -## Access Points - -| Service | URL | Purpose | -|---------|-----|---------| -| **Control Plane** | http://localhost:3000 | Web UI | -| **Dataplane API** | http://localhost:8080 | REST API | -| **PostgreSQL** | localhost:5432 | Database | - -## View Logs - -```bash -docker logs -f memora-standalone -``` - -## Stop & Remove - -```bash -# Stop -docker-compose down - -# Stop and remove data -docker-compose down -v -``` - -## Environment Variables - -Set in `docker-compose.yml` or pass with `-e`: - -- `MEMORA_API_LLM_PROVIDER` - LLM provider (openai, groq, ollama, none) (default: none) -- `MEMORA_API_LLM_API_KEY` - API key for LLM provider -- `MEMORA_API_LLM_MODEL` - LLM model name (default: openai/gpt-oss-120b) -- `MEMORA_API_LLM_BASE_URL` - Optional custom LLM endpoint -- `MEMORA_CP_DATAPLANE_API_URL` - Dataplane API URL (default: http://localhost:8080) - -## Troubleshooting - -**Container won't start:** -```bash -docker logs memora-standalone -``` - -**Database issues:** -```bash -docker exec -it memora-standalone su - postgres -c "psql memora" -``` - -**Reset everything:** -```bash -docker-compose down -v -docker-compose up -d -``` - -## Production Notes - -This standalone image is ideal for: -- ✅ Development -- ✅ Demos -- ✅ Testing -- ✅ Small deployments - -For production, consider: -- Separate containers for each service -- External PostgreSQL database -- Kubernetes/Docker Swarm orchestration -- Environment-specific configurations diff --git a/standalone/README.md b/standalone/README.md deleted file mode 100644 index 6392442c..00000000 --- a/standalone/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# Memora Standalone Docker Image - -This directory contains the configuration to build a standalone Docker image that includes all Memora components in a single container: - -- **PostgreSQL**: Database backend -- **Dataplane**: FastAPI backend service -- **Control Plane**: Next.js web interface - -## Quick Start with Docker Compose - -The easiest way to run the standalone image: - -```bash -cd standalone -docker-compose up -d -``` - -This will build and start all services with persistent data storage. - -To stop: -```bash -docker-compose down -``` - -To remove data and start fresh: -```bash -docker-compose down -v -``` - -## Building Manually - -```bash -./standalone/build-docker.sh -``` - -With custom options: -```bash -./standalone/build-docker.sh --name my-memora --tag v1.0.0 -./standalone/build-docker.sh --registry docker.io/myuser --tag latest -``` - -## Running Manually - -Using the run script (recommended): -```bash -./standalone/run-docker.sh --persist -``` - -With custom ports: -```bash -./standalone/run-docker.sh --persist --port-control 3001 --port-api 8081 -``` - -Direct docker run: -```bash -docker run -p 3000:3000 -p 8080:8080 memora-standalone:latest -``` - -With persistent data: -```bash -docker run -p 3000:3000 -p 8080:8080 \ - -v memora-data:/var/lib/postgresql/data \ - memora-standalone:latest -``` - -With custom environment variables: -```bash -docker run -p 3000:3000 -p 8080:8080 \ - -e MEMORA_API_LLM_PROVIDER=groq \ - -e MEMORA_API_LLM_API_KEY=your-key \ - -e MEMORA_API_LLM_MODEL=openai/gpt-oss-120b \ - memora-standalone:latest -``` - -## Accessing Services - -Once running, services are available at: - -- **Control Plane**: http://localhost:3000 -- **Dataplane API**: http://localhost:8080 -- **PostgreSQL**: localhost:5432 (username: postgres, password: postgres, database: memora) - -## Architecture - -The container uses `supervisord` to manage three processes: -1. PostgreSQL (started first) -2. Dataplane API (started after PostgreSQL) -3. Control Plane (started after dataplane) - -The `init.sh` script handles: -- PostgreSQL initialization -- Database creation -- Running migrations -- Starting all services via supervisord - -## Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `MEMORA_API_DATABASE_URL` | `postgresql://postgres:postgres@localhost:5432/memora` | PostgreSQL connection string | -| `MEMORA_CP_DATAPLANE_API_URL` | `http://localhost:8080` | Dataplane API URL for control plane | -| `MEMORA_API_LLM_PROVIDER` | `none` | LLM provider (openai, groq, ollama, none) | -| `MEMORA_API_LLM_API_KEY` | - | API key for LLM provider | -| `MEMORA_API_LLM_MODEL` | `openai/gpt-oss-120b` | LLM model name | -| `MEMORA_API_LLM_BASE_URL` | - | Optional custom LLM endpoint | - -## Logs - -View logs from all services: -```bash -docker logs -f -``` - -## Production Considerations - -This standalone image is designed for: -- Development environments -- Demos and testing -- Small deployments - -For production use, consider: -- Using separate containers for each service -- External PostgreSQL database -- Load balancing for the control plane -- Persistent volume for PostgreSQL data -- Environment-specific configurations diff --git a/standalone/build-docker.sh b/standalone/build-docker.sh deleted file mode 100755 index a97d2cbf..00000000 --- a/standalone/build-docker.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -set -e - -cd "$(dirname "$0")/.." - -# Default values -IMAGE_NAME="memora-standalone" -IMAGE_TAG="latest" -REGISTRY="" - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --name) - IMAGE_NAME="$2" - shift 2 - ;; - --tag) - IMAGE_TAG="$2" - shift 2 - ;; - --registry) - REGISTRY="$2" - shift 2 - ;; - --help) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --name NAME Docker image name (default: memora-standalone)" - echo " --tag TAG Docker image tag (default: latest)" - echo " --registry REG Docker registry URL (optional)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --name myapp --tag v1.0.0" - echo " $0 --registry docker.io/myuser --name memora-standalone --tag v1.0.0" - exit 0 - ;; - *) - echo "Unknown option: $1" - echo "Use --help for usage information" - exit 1 - ;; - esac -done - -# Construct full image name -if [ -n "$REGISTRY" ]; then - FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" -else - FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}" -fi - -echo "Building Memora Standalone Docker image: ${FULL_IMAGE_NAME}" -echo "=============================================================" -echo "This image includes:" -echo " - PostgreSQL database" -echo " - Dataplane API (FastAPI)" -echo " - Control Plane (Next.js)" -echo "" - -# Build the Docker image -docker build -f standalone/Dockerfile -t "${FULL_IMAGE_NAME}" . - -echo "" -echo "Build completed successfully!" -echo "Image: ${FULL_IMAGE_NAME}" -echo "" -echo "To run the container:" -echo " docker run -p 3000:3000 -p 8080:8080 ${FULL_IMAGE_NAME}" -echo "" -echo "Services will be available at:" -echo " - Control Plane: http://localhost:3000" -echo " - Dataplane API: http://localhost:8080" -echo " - PostgreSQL: localhost:5432" -echo "" -echo "For persistent data, mount a volume:" -echo " docker run -p 3000:3000 -p 8080:8080 \\" -echo " -v memora-data:/var/lib/postgresql/data \\" -echo " ${FULL_IMAGE_NAME}" -echo "" -if [ -n "$REGISTRY" ]; then - echo "To push to registry:" - echo " docker push ${FULL_IMAGE_NAME}" - echo "" -fi diff --git a/standalone/docker-compose.yml b/standalone/docker-compose.yml deleted file mode 100644 index 7ea0bf1b..00000000 --- a/standalone/docker-compose.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: '3.8' - -services: - memora-standalone: - build: - context: .. - dockerfile: standalone/Dockerfile - ports: - - "3000:3000" # Control Plane - - "8080:8080" # Dataplane API - - "5432:5432" # PostgreSQL - environment: - - EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2 - - EMBEDDING_DIM=384 - # - OPENAI_API_KEY=${OPENAI_API_KEY} # Uncomment if needed - volumes: - - memora-data:/var/lib/postgresql/data - restart: unless-stopped - -volumes: - memora-data: diff --git a/standalone/init.sh b/standalone/init.sh deleted file mode 100755 index 831f2b58..00000000 --- a/standalone/init.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -set -e - -echo "🚀 Starting Memora Standalone Container..." -echo "===========================================" - -# Start PostgreSQL temporarily for initialization -echo "📦 Starting PostgreSQL for initialization..." -su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/data -l /tmp/postgresql-init.log start" - -# Wait for PostgreSQL to be ready -echo "⏳ Waiting for PostgreSQL to be ready..." -for i in {1..30}; do - if su - postgres -c "psql -lqt" &>/dev/null; then - echo "✅ PostgreSQL is ready" - break - fi - if [ $i -eq 30 ]; then - echo "❌ PostgreSQL failed to start" - cat /tmp/postgresql-init.log - exit 1 - fi - sleep 1 -done - -# Create database if it doesn't exist -echo "📊 Setting up database..." -su - postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname = 'memora'\" | grep -q 1 || psql -c 'CREATE DATABASE memora;'" - -# Run initial migrations -# Note: The API also runs migrations automatically on startup. -# We run them here during initialization to ensure the database -# schema is ready before handing off to supervisord. -echo "🔄 Running initial database migrations..." -cd /app/memora - -# Export environment variables -set -a -source /app/.env -set +a - -/app/.venv/bin/python -m alembic upgrade head - -# Stop PostgreSQL so supervisord can start it cleanly -echo "🔄 Stopping PostgreSQL to hand off to supervisord..." -su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/data stop -m fast" -sleep 2 - -echo "✅ Initialization complete" -echo "" -echo "Starting services via supervisord..." -echo " - PostgreSQL: localhost:5432" -echo " - Dataplane API: http://localhost:8080" -echo " - Control Plane: http://localhost:3000" -echo "" - -# Start supervisor to manage all services -exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/standalone/run-docker.sh b/standalone/run-docker.sh deleted file mode 100755 index ada21582..00000000 --- a/standalone/run-docker.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash -set -e - -# Default values -IMAGE_NAME="memora-standalone:latest" -CONTAINER_NAME="memora-standalone" -PERSIST_DATA=false -PORT_CONTROL=3000 -PORT_API=8080 -PORT_DB=5432 - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --image) - IMAGE_NAME="$2" - shift 2 - ;; - --name) - CONTAINER_NAME="$2" - shift 2 - ;; - --persist) - PERSIST_DATA=true - shift - ;; - --port-control) - PORT_CONTROL="$2" - shift 2 - ;; - --port-api) - PORT_API="$2" - shift 2 - ;; - --port-db) - PORT_DB="$2" - shift 2 - ;; - --help) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --image NAME Docker image name (default: memora-standalone:latest)" - echo " --name NAME Container name (default: memora-standalone)" - echo " --persist Use persistent volume for data" - echo " --port-control PORT Control plane port (default: 3000)" - echo " --port-api PORT Dataplane API port (default: 8080)" - echo " --port-db PORT PostgreSQL port (default: 5432)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --persist --port-control 3001" - echo "" - echo "To stop the container:" - echo " docker stop ${CONTAINER_NAME}" - echo "" - echo "To remove the container:" - echo " docker rm ${CONTAINER_NAME}" - exit 0 - ;; - *) - echo "Unknown option: $1" - echo "Use --help for usage information" - exit 1 - ;; - esac -done - -# Check if container already exists -if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "⚠️ Container '${CONTAINER_NAME}' already exists" - echo "" - read -p "Do you want to remove it and create a new one? (y/N): " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - echo "🗑️ Removing existing container..." - docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true - else - echo "Exiting..." - exit 0 - fi -fi - -echo "🚀 Starting Memora Standalone Container" -echo "========================================" -echo "Image: ${IMAGE_NAME}" -echo "Container: ${CONTAINER_NAME}" -echo "" - -# Build docker run command -DOCKER_CMD="docker run -d --name ${CONTAINER_NAME}" -DOCKER_CMD="${DOCKER_CMD} -p ${PORT_CONTROL}:3000" -DOCKER_CMD="${DOCKER_CMD} -p ${PORT_API}:8080" -DOCKER_CMD="${DOCKER_CMD} -p ${PORT_DB}:5432" - -if [ "$PERSIST_DATA" = true ]; then - DOCKER_CMD="${DOCKER_CMD} -v memora-data:/var/lib/postgresql/data" - echo "📦 Using persistent volume: memora-data" -fi - -DOCKER_CMD="${DOCKER_CMD} ${IMAGE_NAME}" - -# Run the container -eval $DOCKER_CMD - -echo "" -echo "✅ Container started successfully!" -echo "" -echo "Services are available at:" -echo " - Control Plane: http://localhost:${PORT_CONTROL}" -echo " - Dataplane API: http://localhost:${PORT_API}" -echo " - PostgreSQL: localhost:${PORT_DB}" -echo "" -echo "View logs:" -echo " docker logs -f ${CONTAINER_NAME}" -echo "" -echo "Stop container:" -echo " docker stop ${CONTAINER_NAME}" -echo "" -echo "Remove container:" -echo " docker rm -f ${CONTAINER_NAME}" -echo "" diff --git a/standalone/supervisord.conf b/standalone/supervisord.conf deleted file mode 100644 index 809d6b87..00000000 --- a/standalone/supervisord.conf +++ /dev/null @@ -1,42 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:postgresql] -command=/usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/data -user=postgres -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=1 - -[program:dataplane] -command=/app/.venv/bin/python -m memora.web.server --host 0.0.0.0 --port 8080 -directory=/app/memora -environment=PATH="/app/.venv/bin:%(ENV_PATH)s",MEMORA_API_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/memora",MEMORA_API_LLM_PROVIDER="none" -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -startsecs=10 -priority=10 - -[program:memora-control-plane] -command=/app/memora-control-plane/start-server.sh -directory=/app/memora-control-plane -environment=NODE_ENV="production",MEMORA_CP_PORT="3000",MEMORA_CP_HOSTNAME="0.0.0.0",MEMORA_CP_DATAPLANE_API_URL="http://localhost:8080" -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -startsecs=5 -priority=20 diff --git a/uv.lock b/uv.lock index 4090e236..4b99440f 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,6 @@ members = [ "memora-client", "memora-dev", "memora-langmem", - "memora-mcp-server", "memora-openai", ] @@ -1717,6 +1716,9 @@ dependencies = [ ] [package.optional-dependencies] +mcp = [ + { name = "fastmcp" }, +] test = [ { name = "pytest" }, { name = "pytest-asyncio" }, @@ -1728,6 +1730,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.17.1" }, { name = "asyncpg", specifier = ">=0.29.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" }, + { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.0.0" }, { name = "greenlet", specifier = ">=3.2.4" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "langchain-text-splitters", specifier = ">=0.3.0" }, @@ -1747,7 +1750,7 @@ requires-dist = [ { name = "transformers", specifier = ">=4.30.0" }, { name = "uvicorn", specifier = ">=0.38.0" }, ] -provides-extras = ["test"] +provides-extras = ["test", "mcp"] [[package]] name = "memora-client" @@ -1814,21 +1817,6 @@ requires-dist = [ ] provides-extras = ["test"] -[[package]] -name = "memora-mcp-server" -version = "0.0.1" -source = { editable = "memora-mcp-server" } -dependencies = [ - { name = "fastmcp" }, - { name = "httpx" }, -] - -[package.metadata] -requires-dist = [ - { name = "fastmcp", specifier = ">=0.7.0" }, - { name = "httpx", specifier = ">=0.28.1" }, -] - [[package]] name = "memora-openai" version = "0.1.0"