* feat: MCP Server v1.0.0rc0 - Complete refactoring with modular architecture This is a major refactoring of the MCP Server to support multiple providers through a YAML-based configuration system with factory pattern implementation. ## Key Changes ### Architecture Improvements - Modular configuration system with YAML-based settings - Factory pattern for LLM, Embedder, and Database providers - Support for multiple database backends (Neo4j, FalkorDB, KuzuDB) - Clean separation of concerns with dedicated service modules ### Provider Support - **LLM**: OpenAI, Anthropic, Gemini, Groq - **Embedders**: OpenAI, Voyage, Gemini, Anthropic, Sentence Transformers - **Databases**: Neo4j, FalkorDB, KuzuDB (new default) - Azure OpenAI support with AD authentication ### Configuration - YAML configuration with environment variable expansion - CLI argument overrides for runtime configuration - Multiple pre-configured Docker Compose setups - Proper boolean handling in environment variables ### Testing & CI - Comprehensive test suite with unit and integration tests - GitHub Actions workflows for linting and testing - Multi-database testing support ### Docker Support - Updated Docker images with multi-stage builds - Database-specific docker-compose configurations - Persistent volume support for all databases ### Bug Fixes - Fixed KuzuDB connectivity checks - Corrected Docker command paths - Improved error handling and logging - Fixed boolean environment variable expansion Co-authored-by: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01PmXwij9S976CQk798DJ4PH * fix: Improve MCP server configuration and initialization - Fix API key detection: Remove hardcoded OpenAI checks, let factories handle provider-specific validation - Fix .env file loading: Search for .env in mcp_server directory first - Change default transport to SSE for broader compatibility (was stdio) - Add proper error handling with warnings for failed client initialization - Model already defaults to gpt-4o as requested These changes ensure the MCP server properly loads API keys from .env files and creates the appropriate LLM/embedder clients based on configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01PCGAWzQUbmh7hAKBvadbYN * chore: Update default transport from SSE to HTTP - Changed default transport to 'http' as SSE is deprecated - Updated all configuration files to use HTTP transport - Updated Docker compose commands to use HTTP transport - Updated comments to reflect HTTP transport usage This change ensures the MCP server uses the recommended HTTP transport instead of the deprecated SSE transport. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01FErZjFG5iWrvbdUD2acQwQ * chore: Update default OpenAI model to gpt-4o-mini Changed the default LLM model from gpt-4o to gpt-4o-mini across all configuration files for better cost efficiency while maintaining quality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01FETp6u9mWAMjJAeT6WFgAf * conductor-checkpoint-msg_01AJJ48RbkPaZ99G2GmE6HUi * fix: Correct default OpenAI model to gpt-4.1 Changed the default LLM model from gpt-4o-mini to gpt-4.1 as requested. This is the latest GPT-4 series model. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_013HP1MYHKZ5wdTHHxrpBaT9 * fix: Update hardcoded default model to gpt-4.1 and fix config path - Changed hardcoded default in schema.py from gpt-4o to gpt-4.1 - Fixed default config path to look in config/config.yaml relative to mcp_server directory - This ensures the server uses gpt-4.1 as the default model everywhere 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01EaN8GZtehm8LV3a7CdWJ8u * feat: Add detailed server URL logging and improve access information - Added comprehensive logging showing exact URLs to access the MCP server - Display localhost instead of 0.0.0.0 for better usability - Show MCP endpoint, transport type, and status endpoint information - Added visual separators to make server info stand out in logs This helps users understand exactly how to connect to the MCP server and troubleshoot connection issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01SNpbaZMdxWbefo2zsLprcW * fix: Correct MCP HTTP endpoint path from / to /mcp/ - Remove incorrect /status endpoint reference - Update logging to show correct MCP endpoint at /mcp/ - Align with FastMCP documentation standards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01417YVh3s6afJadN5AM5Ahk * fix: Configure consistent logging format between uvicorn and MCP server - Use simplified format matching uvicorn's default (LEVEL message) - Remove timestamps from custom logger format - Suppress verbose MCP and uvicorn access logs - Improve readability of server startup output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_014BF6Kzdy7qXc5AgC7eeVa5 * conductor-checkpoint-msg_01TscHXmijzkqcTJX5sGTYP8 * conductor-checkpoint-msg_01Q7VLFTJrtmpkaB7hfUzZLP * fix: Improve test runner to load API keys from .env file - Add dotenv loading support in test runner - Fix duplicate os import issue - Improve prerequisite checking with helpful hints - Update error messages to guide users 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01NfviLNeAhFDA1G5841YKCS * conductor-checkpoint-msg_015EewQhKbqAGSQkasWtRQjp * fix: Fix all linting errors in test suite - Replace bare except with except Exception - Remove unused imports and variables - Fix type hints to use modern syntax - Apply ruff formatting for line length - Ensure all tests pass linting checks * conductor-checkpoint-msg_01RedNheKT4yWyXcM83o3Nmv * fix: Use contextlib.suppress instead of try-except-pass (SIM105) - Replace try-except-pass with contextlib.suppress in test_async_operations.py - Replace try-except-pass with contextlib.suppress in test_fixtures.py - Fixes ruff SIM105 linting errors * conductor-checkpoint-msg_01GuBj69k2CsqgojBsGJ2zFT * fix: Move README back to mcp_server root folder The main README for the MCP server should be in the root of the mcp_server folder for better discoverability * conductor-checkpoint-msg_01VsJQ3MgDxPwyb4ynvswZfb * docs: Update README with comprehensive features and database options - Add comprehensive features list including all supported databases, LLM providers, and transports - Document Kuzu as the default database with explanation of its benefits and archived status - Add detailed instructions for running with different databases (Kuzu, Neo4j, FalkorDB) - Update transport references from SSE to HTTP (default transport) - Add database-specific Docker Compose instructions - Update MCP client configurations to use /mcp/ endpoint - Clarify prerequisites to reflect optional nature of external databases - Add detailed database configuration examples for all supported backends * conductor-checkpoint-msg_018Z7AjbTkuDfhqdjB9iGbbD * docs: Address README review comments - Shorten Kuzu database description to be more concise - Update Ollama model example to use 'gpt-oss:120b' - Restore Azure OpenAI environment variables documentation - Remove implementation details from Docker section (irrelevant to container users) - Clarify mcp-remote supports both HTTP and SSE transports Addresses review comments #1-7 on the PR * conductor-checkpoint-msg_01QMeMEMe9rTVDgd8Ce5hmXp * docs: Remove SSE transport reference from Claude Desktop section Since the MCP server no longer supports SSE transport, removed the mention of SSE from the mcp-remote description. The server only uses HTTP transport. Addresses review comment on line 514 * conductor-checkpoint-msg_01DNn76rvpx7rmTBwsUQd1De * docs: Remove telemetry from features list Telemetry is not a feature but a notice about data collection, so it shouldn't be listed as a feature. Addresses review comment on line 29 * conductor-checkpoint-msg_01Jcb8sm9bpqB9Ksz1W6YrSz * feat: Update default embedding model to text-embedding-3-small Replace outdated text-embedding-ada-002 with the newer, more efficient text-embedding-3-small model as the default embedder. The new model offers better performance and is more cost-effective. Updated: - config/config.yaml: Changed default model - README.md: Updated documentation to reflect new default * conductor-checkpoint-msg_016AXAH98nYKTj5WCueBubmA * fix: Resolve database connection and episode processing errors Fixed two critical runtime errors: 1. Database connection check for KuzuDB - KuzuDB session.run() returns None, causing async iteration error - Added special handling for KuzuDB (in-memory, no query needed) - Other databases (Neo4j, FalkorDB) still perform connection test 2. Episode processing parameter error - Changed 'episode_type' parameter to 'source' to match Graphiti API - Added required 'reference_time' parameter with current timestamp - Added datetime imports (UTC, datetime) Errors fixed: - 'async for' requires an object with __aiter__ method, got NoneType - Graphiti.add_episode() got an unexpected keyword argument 'episode_type' * conductor-checkpoint-msg_01JvcW97a4s3icDWFkhF3kEJ * fix: Use timezone.utc instead of UTC for Python 3.10 compatibility The UTC constant was added in Python 3.11. Changed to use timezone.utc which is available in Python 3.10+. Fixed ImportError: cannot import name 'UTC' from 'datetime' * conductor-checkpoint-msg_01Br69UnYf8QXvtAhJVTuDGD * fix: Convert entity_types from list to dict for Graphiti API The Graphiti add_episode() API expects entity_types as a dict[str, type[BaseModel]], not a list. Changed entity type building to create a dictionary mapping entity names to their Pydantic model classes. Fixed error: 'list' object has no attribute 'items' Changes: - Build entity_types as dict instead of list in config processing - Add fallback to convert ENTITY_TYPES list to dict if needed - Map entity type names to their model classes * conductor-checkpoint-msg_0173SR9CxbBH9jVWp8tLooRp * conductor-checkpoint-msg_0169v3hqZG1Sqb13Kp1Vijms * fix: Remove protected 'name' attribute from entity type models Pydantic BaseModel reserves 'name' as a protected attribute. Removed the 'name' attribute from dynamically created entity type models as it's not needed - the entity type name is already stored as the class name and dict key. Fixed error: name cannot be used as an attribute for Requirement as it is a protected attribute name. * conductor-checkpoint-msg_0118QJWvZLyoZfwb1UWZqrRa * conductor-checkpoint-msg_01B78jtT59YDt1Xm5hJpoqQw * conductor-checkpoint-msg_01MsqeFGoCEXpoNMiDRM3Gjh * conductor-checkpoint-msg_01SwJkCDAScffk8116KPVpTd * conductor-checkpoint-msg_01EBWwDRC8bZ7oLYxsrmVnLH * conductor-checkpoint-msg_01SAcxuF3eqtP4exA47CBqAi * conductor-checkpoint-msg_011dRKwJM31K3ob9Gy4JCmae * conductor-checkpoint-msg_018d52yUXdPF48UBWPQdiB4W * conductor-checkpoint-msg_01MGFAenMDnTX3H9HSZEbj2T * conductor-checkpoint-msg_01MHw4g8TicrXegSK9phncfw * conductor-checkpoint-msg_018YrqWa3c2ZpkxemiiaE9tA * conductor-checkpoint-msg_01SNsax9AwiCBFrC7Fpo7BNe * conductor-checkpoint-msg_01K7QC1X8iPiYaMdvbi7WtR5 * conductor-checkpoint-msg_01KgGgzpbiuM31KWKxQhNBfY * conductor-checkpoint-msg_01KL3wzQUn3gekDmznXVgXne * conductor-checkpoint-msg_016GKc3DYwYUjngGw8pArRJK * conductor-checkpoint-msg_01QLbhPMGDeB5EHbMq5KT86U * conductor-checkpoint-msg_01Qdskq96hJ6Q9DPg1h5Jjgg * conductor-checkpoint-msg_01JhPXYdc6HGsoEW2f1USSyd * conductor-checkpoint-msg_018NLrtFxs5zfcNwQnNCfvNg * conductor-checkpoint-msg_01G1G9J7cbupmLkyiQufj335 * conductor-checkpoint-msg_01BHEPsv2EML14gFa6vkn1NP * conductor-checkpoint-msg_0127MeSvxWk8BLXjB5k3wDJY * conductor-checkpoint-msg_018dRGHW6fPNqJDN6eV6SpoH * conductor-checkpoint-msg_01CPPZ9JKakjsmHpzzoFVhaM * conductor-checkpoint-msg_014jJQ4FkGU4485gF41K2suG * conductor-checkpoint-msg_01MS72hQDCrr1rB6GSd3zy4h * conductor-checkpoint-msg_01P7ur6mQEusfHTYpBrBnpk3 * conductor-checkpoint-msg_01JiEiEuJN3sQXheqMzCa6hX * conductor-checkpoint-msg_01D7XfEJqzTeKGyuE5EFmjND * conductor-checkpoint-msg_01Gn6qZrD3DZd8c6a6fmMap7 * conductor-checkpoint-msg_01Ji7gxCG4jR145rBAupwU49 * conductor-checkpoint-msg_01CYzyiAtLo95iVLeqWSuYiR * conductor-checkpoint-msg_017fAeUG21Ym1EeofanFzFGa * conductor-checkpoint-msg_013rt24pyzMHbrmEQein2dJJ * conductor-checkpoint-msg_016bN3uyAxN28Rh8uvDpExit * conductor-checkpoint-msg_017QV6m73ShaMBdQi7L3kmhP * conductor-checkpoint-msg_01LUZ9XS7C1LCG6A1VFNcRL2 * conductor-checkpoint-msg_0136b9tNU5ko18T3PmRkW3LJ * conductor-checkpoint-msg_018FX6Mibr66cKLnpL84f2Js * conductor-checkpoint-msg_01WRZxPMQYjNEjcFNTMzWYeL * conductor-checkpoint-msg_015Tbxjxrj6dynf7TbZscFD3 * conductor-checkpoint-msg_01ELC9AyZZGry9tN4XKrwEM6 * conductor-checkpoint-msg_01Jk4ugkAqMs4iRYWwnaNAHR * conductor-checkpoint-msg_01NLStrCDq7HZJy3pKyGSqxM * conductor-checkpoint-msg_01BFZEVpXbdxuXJguFH3caek * Remove User and Assistant exception from Preference prioritization * conductor-checkpoint-msg_01JP4eGXZfEjoSXWUwTHNYoJ * Add combined FalkorDB + MCP server Docker image - Created Dockerfile.falkordb-combined extending official FalkorDB image - Added startup script to run both FalkorDB daemon and MCP server - Created docker-compose-falkordb-combined.yml for simplified deployment - Added comprehensive README-falkordb-combined.md documentation - Updated main README with Option 4 for combined image - Single container solution for development and single-node deployments * conductor-checkpoint-msg_01PRJ1fre9d6J4qgBmCBQhCu * Fix Dockerfile syntax version and Python compatibility - Set Dockerfile syntax to version 1 as requested - Use Python 3.11 from Debian Bookworm instead of 3.12 - Add comment explaining Bookworm ships with Python 3.11 - Python 3.11 meets project requirement of >=3.10 - Build tested successfully * conductor-checkpoint-msg_011Thrsv6CjZKRCXvordMWeb * Fix combined FalkorDB image to run both services successfully - Override FalkorDB ENTRYPOINT to use custom startup script - Use correct FalkorDB module path: /var/lib/falkordb/bin/falkordb.so - Create config-docker-falkordb-combined.yaml with localhost URI - Create /var/lib/falkordb/data directory for persistence - Both FalkorDB and MCP server now start successfully - Tested: FalkorDB ready, MCP server running on port 8000 * conductor-checkpoint-msg_01FT3bsTuv7466EvCeRtgDsD * Fix health check to eliminate 404 errors - Changed health check to only verify FalkorDB (redis-cli ping) - Removed non-existent /health endpoint check - MCP server startup is visible in logs - Container now runs without health check errors * conductor-checkpoint-msg_01KWBc5S8vWzyovUTWLvPYNw * Replace Kuzu with FalkorDB as default database BREAKING CHANGE: Kuzu is no longer supported. FalkorDB is now the default. - Renamed Dockerfile.falkordb-combined to Dockerfile (default) - Renamed docker-compose-falkordb-combined.yml to docker-compose.yml (default) - Updated config.yaml to use FalkorDB with localhost:6379 as default - Removed Kuzu from pyproject.toml dependencies (now only falkordb extra) - Updated Dockerfile to use graphiti-core[falkordb] instead of [kuzu,falkordb] - Completely removed all Kuzu references from README - Updated README to document FalkorDB combined container as default - Docker Compose now starts single container with FalkorDB + MCP server - Prerequisites now require Docker instead of Python for default setup - Removed old Kuzu docker-compose files Running from command line now requires external FalkorDB instance at localhost:6379 * conductor-checkpoint-msg_014wBY9WG9GRXP7cUZ2JiqGz * Complete Kuzu removal from MCP server Removed all remaining Kuzu references from: - Test fixtures (test_fixtures.py): Changed default database to falkordb, removed kuzu configuration - Test runner (run_tests.py): Removed kuzu from database choices, checks, and markers - Integration tests (test_comprehensive_integration.py): Removed kuzu from parameterized tests and environment setup - Test README: Updated all examples and documentation to reflect falkordb as default - Docker README: Completely rewrote to remove KuzuDB section, updated with FalkorDB combined image as default All Kuzu support has been completely removed from the MCP server codebase. FalkorDB (via combined container) is now the default database backend. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01FAgmoDFBPETezbBr18Bpir * Fix Anthropic client temperature type error Fixed pyright type error where temperature parameter (float | None) was being passed directly to Anthropic's messages.create() method which expects (float | Omit). Changes: - Build message creation parameters as a dictionary - Conditionally include temperature only when not None - Use dictionary unpacking to pass parameters This allows temperature to be properly omitted when None, rather than passing None as a value. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_01KEuAQucnvsH94BwFgCAQXg * Fix critical PR review issues Fixed high-impact issues from PR #1024 code review: 1. **Boolean conversion bug (schema.py)** - Fixed _expand_env_vars returning strings 'true'/'false' instead of booleans - Now properly converts boolean-like strings (true/false/1/0/yes/no/on/off) to actual booleans - Simplified logic by removing redundant string-to-string conversions - Added support for common boolean string variations 2. **Dependency management (pyproject.toml)** - Removed pytest from main dependencies (now only in dev dependencies) - Moved azure-identity to optional dependencies under new [azure] group - Prevents forcing Azure and testing dependencies on all users 3. **Conditional Azure imports (utils.py)** - Made azure-identity import conditional in create_azure_credential_token_provider() - Raises helpful ImportError with installation instructions if not available - Follows lazy-import pattern for optional dependencies 4. **Documentation fix (graphiti_mcp_server.py)** - Fixed confusing JSON escaping in add_memory docstring example - Changed from triple-backslash escaping to standard JSON string - Updated comment to clarify standard JSON escaping is used Issues verified as already fixed: - Docker build context (all docker-compose files use context: ..) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_013aEa7tUV8rEmfw38BzJatc * Add comprehensive SEMAPHORE_LIMIT documentation Added detailed documentation for SEMAPHORE_LIMIT configuration to help users optimize episode processing concurrency based on their LLM provider's rate limits. Changes: 1. **graphiti_mcp_server.py** - Expanded inline comments from 3 lines to 26 lines - Added provider-specific tuning guidelines (OpenAI, Anthropic, Azure, Ollama) - Documented symptoms of too-high/too-low settings - Added monitoring recommendations 2. **README.md** - Expanded "Concurrency and LLM Provider 429 Rate Limit Errors" section - Added tier-specific recommendations for each provider - Explained relationship between episode concurrency and LLM request rates - Added troubleshooting symptoms and monitoring guidance - Included example .env configuration 3. **config.yaml** - Added header comment referencing detailed documentation - Noted default value and suitable use case 4. **.env.example** - Added SEMAPHORE_LIMIT with inline tuning guidelines - Quick reference for all major LLM provider tiers - Cross-reference to README for full details Benefits: - Users can now make informed decisions about concurrency settings - Reduces likelihood of 429 rate limit errors from misconfiguration - Helps users maximize throughput within their rate limits - Provides clear troubleshooting guidance Addresses PR #1024 review comment about magic number documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_017netxNYzmam5Cu8PM2uQXW * conductor-checkpoint-msg_012B8ESfBFcMeG3tFimpjbce * conductor-checkpoint-msg_01Xe46bzgCGV4c8g4piPtSMQ * conductor-checkpoint-start * conductor-checkpoint-msg_01QPZK2pa2vUMpURRFmX93Jt * conductor-checkpoint-msg_01UU5jQcfrW5btRJB3zy5KQZ * conductor-checkpoint-msg_01884eN3wprtCkrEgEaRDzko * conductor-checkpoint-msg_01GC2fQiu9gLGPGf8SvG5VW8 * conductor-checkpoint-msg_018ZD567wd4skoiAz7oML7WX * conductor-checkpoint-msg_01C3AxzcQQSNZxJcuVxAMYpG * conductor-checkpoint-msg_014w5iHAnv7mVkKfTroeNkuM * docs: Add current LLM model reference to CLAUDE.md Added comprehensive model reference section documenting valid model names for OpenAI, Anthropic, and Google Gemini as of January 2025. OpenAI Models: - GPT-5 family (reasoning models): gpt-5-mini, gpt-5-nano - GPT-4.1 family (standard models): gpt-4.1, gpt-4.1-mini, gpt-4.1-nano - Legacy models: gpt-4o, gpt-4o-mini Anthropic Models: - Claude 3.7 family (latest) - Claude 3.5 family - Legacy Claude 3 models Google Gemini Models: - Gemini 2.5 family (latest) - Gemini 2.0 family (experimental) - Gemini 1.5 family (stable) This documents that model names like gpt-5-mini, gpt-4.1, and gpt-4.1-mini used throughout the codebase are valid OpenAI model identifiers, not errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * conductor-checkpoint-msg_014JsovjGyTM1mGwR1nVWLvX * conductor-checkpoint-msg_013ooHLBEhPccaSY4cFse8vK * conductor-checkpoint-msg_01WfmUCwXhWxEFtV7R3zJLwT * conductor-checkpoint-msg_01SbjZ9mm9YwqeJHTDUDoKU8 * conductor-checkpoint-msg_01T2cR1aXUjNSegqzXQcW2jC * conductor-checkpoint-msg_01EnQy5A9dMFD8F11hWKvzGo * conductor-checkpoint-msg_01R1zsLmxvwjZ9SwKNhSnQAv * refactor: Remove duplicate is_reasoning_model calculation in factories.py * conductor-checkpoint-msg_015oLk8qck3TbfaCryY9gngJ * conductor-checkpoint-msg_018YAxG5GsLq1dBMuGE6kwEJ * conductor-checkpoint-msg_014fda5sUsvofb537BvqkuBY * fix: Change default transport to http, mark SSE as deprecated * conductor-checkpoint-msg_01S3x8oHkFTM2x4ZiT81QetV * conductor-checkpoint-msg_01AVxUgejEA9piS6narw4omz * conductor-checkpoint-msg_019W9KoNBmkobBguViYUj18s * conductor-checkpoint-msg_01S2mYUmqLohxEmoZaNqsm2f * conductor-checkpoint-msg_013ZGKfZjdDsqiCkAjAiuEk7 * fix: Handle default config path and empty env vars correctly - Change default config path from 'config.yaml' to 'config/config.yaml' - Fix env var expansion to return None for empty strings instead of False - Prevents validation errors when optional string fields have unset env vars * conductor-checkpoint-msg_01Bx1BqH3BaBxHMrnsbUQXww * fix: Allow None for episode_id_prefix and convert to empty string - Change episode_id_prefix type to str | None to accept None from YAML - Add model_post_init to convert None to empty string for backward compatibility * conductor-checkpoint-msg_01CXVkHJC8gp5i395MQMhp6D * feat: Add helpful error message for database connection failures - Catch Redis/database connection errors during initialization - Provide clear, formatted error messages with startup instructions - Include provider-specific guidance (FalkorDB vs Neo4j) - Improves developer experience when database is not running * conductor-checkpoint-msg_01Cd9u1z7pqmX1EG7vXXo4GA * feat: Add specific Neo4j connection error message with startup instructions * conductor-checkpoint-msg_01XgbmgFaUMPopni4Q8EhG23 * fix: Remove obsolete KuzuDB check from status endpoint - Remove dead code checking for 'kuzu' provider (was removed) - Simplify status check to use configured database provider directly - Status now correctly reports neo4j or falkordb based on config * conductor-checkpoint-msg_01WLjwygBwfvbJcVoUMDV3h6 * fix: Use service config instead of global config in status endpoint - Changed status check to use graphiti_service.config.database.provider - Ensures status reports the actual running database, not potentially stale global - Fixes issue where status always reported falkordb regardless of config * conductor-checkpoint-msg_01DoLD51xqrrdFvq3AgkYuQi * conductor-checkpoint-msg_01EUW7ArnNM6kHCgFDrQZrro * conductor-checkpoint-msg_01LqYK6nj1ZFfRNBRP15FMLo * feat: Add standalone Dockerfile for external database deployments - Create Dockerfile.standalone for MCP server without embedded FalkorDB - Supports both Neo4j and FalkorDB via DATABASE_PROVIDER build arg - Update docker-compose-neo4j.yml to use standalone Dockerfile - Update docker-compose-falkordb.yml to use standalone Dockerfile - Fixes issue where Neo4j compose was starting embedded FalkorDB - Separate images: standalone-neo4j and standalone-falkordb * conductor-checkpoint-msg_01QSHNgVZvF1id5UtLhpzuUa * refactor: Unified standalone image with both Neo4j and FalkorDB drivers - Modified Dockerfile.standalone to install both neo4j and falkordb extras - Both compose files now use the same standalone image - Config file determines which database to connect to at runtime - Added build-standalone.sh script for building and pushing to DockerHub - Image tags: standalone, {version}-standalone, {version}-graphiti-{core}-standalone * conductor-checkpoint-msg_01H4isP3oHK25sGpVWzXq9kX * fix: Correct config file paths in compose files - Fix CONFIG_PATH env var: /app/config/config.yaml -> /app/mcp/config/config.yaml - Fix volume mount path: /app/config/config.yaml -> /app/mcp/config/config.yaml - Matches WORKDIR /app/mcp in Dockerfile.standalone - Fixes issue where wrong config was being loaded * conductor-checkpoint-msg_01Pv3Qj9UJJat288xZTsfCm3 * conductor-checkpoint-msg_01DkBq4kQA5Fdmxfikm8aBYG * conductor-checkpoint-msg_01EBqphY68KNzRWei4QNpcYg * feat: Add /health endpoint for Docker healthchecks - Add @mcp.custom_route for /health endpoint using FastMCP - Returns {status: 'healthy', service: 'graphiti-mcp'} - Update Dockerfile.standalone healthcheck to use /health instead of / - Eliminates 404 errors in logs from healthcheck pings - Follows FastMCP best practices for operational monitoring * conductor-checkpoint-msg_01UpNeurS45bREPEeGkV3uCx * feat: Add logging to verify entity types are loaded from config Added INFO level logging during GraphitiService initialization to confirm that custom entity types from the configuration file are properly loaded. This helps debug issues where the entity ontology may not be applied. Logs the entity type names when custom types are present: INFO - Using custom entity types: Preference, Requirement, Procedure, ... * fix: Correct logging message for entity types and add embedder logging Fixed copy-paste error where entity types else clause was logging about embedder client. Also added missing else clause for embedder client logging for consistency. - Fixed: "No Embedder client configured" -> "Using default entity types" - Added: Missing embedder client else clause logging * conductor-checkpoint-msg_01WMuxAzUnkpsa5WSXKMyLLP * fix: Return JSONResponse from health check endpoint Fixed TypeError in health check endpoint by returning a proper Starlette JSONResponse object instead of a plain dict. Starlette custom routes require ASGI-compatible response objects. Error was: TypeError: 'dict' object is not callable * conductor-checkpoint-msg_01CSgKFQaLsKVrBJAYCFoSGa * conductor-checkpoint-msg_01SFY9xCnHxeCFGf53FESncs * feat: Return complete node properties and exclude all embeddings Enhanced node search results to include all relevant properties: - Added `attributes` dict for custom entity properties - Changed from single `type` to full `labels` array - Added `group_id` for partition information - Added safety filter to strip any keys containing "embedding" from attributes Added format_node_result() helper function for consistent node formatting that excludes name_embedding vectors, matching the pattern used for edges. Embeddings are now explicitly excluded in all data returns: - EntityNode: name_embedding excluded + attributes filtered - EntityEdge: fact_embedding excluded (existing) - EpisodicNode: No embeddings to exclude This ensures clients receive complete metadata while keeping payload sizes manageable and avoiding exposure of internal vector representations. --------- Co-authored-by: Claude <noreply@anthropic.com>
489 lines
18 KiB
Python
489 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Asynchronous operation tests for Graphiti MCP Server.
|
|
Tests concurrent operations, queue management, and async patterns.
|
|
"""
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import json
|
|
import time
|
|
|
|
import pytest
|
|
from test_fixtures import (
|
|
TestDataGenerator,
|
|
graphiti_test_client,
|
|
)
|
|
|
|
|
|
class TestAsyncQueueManagement:
|
|
"""Test asynchronous queue operations and episode processing."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sequential_queue_processing(self):
|
|
"""Verify episodes are processed sequentially within a group."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Add multiple episodes quickly
|
|
episodes = []
|
|
for i in range(5):
|
|
result = await session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': f'Sequential Test {i}',
|
|
'episode_body': f'Episode {i} with timestamp {time.time()}',
|
|
'source': 'text',
|
|
'source_description': 'sequential test',
|
|
'group_id': group_id,
|
|
'reference_id': f'seq_{i}', # Add reference for tracking
|
|
},
|
|
)
|
|
episodes.append(result)
|
|
|
|
# Wait for processing
|
|
await asyncio.sleep(10) # Allow time for sequential processing
|
|
|
|
# Retrieve episodes and verify order
|
|
result = await session.call_tool('get_episodes', {'group_id': group_id, 'last_n': 10})
|
|
|
|
processed_episodes = json.loads(result.content[0].text)['episodes']
|
|
|
|
# Verify all episodes were processed
|
|
assert len(processed_episodes) >= 5, (
|
|
f'Expected at least 5 episodes, got {len(processed_episodes)}'
|
|
)
|
|
|
|
# Verify sequential processing (timestamps should be ordered)
|
|
timestamps = [ep.get('created_at') for ep in processed_episodes]
|
|
assert timestamps == sorted(timestamps), 'Episodes not processed in order'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_group_processing(self):
|
|
"""Test that different groups can process concurrently."""
|
|
async with graphiti_test_client() as (session, _):
|
|
groups = [f'group_{i}_{time.time()}' for i in range(3)]
|
|
tasks = []
|
|
|
|
# Create tasks for different groups
|
|
for group_id in groups:
|
|
for j in range(2):
|
|
task = session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': f'Group {group_id} Episode {j}',
|
|
'episode_body': f'Content for {group_id}',
|
|
'source': 'text',
|
|
'source_description': 'concurrent test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
tasks.append(task)
|
|
|
|
# Execute all tasks concurrently
|
|
start_time = time.time()
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
execution_time = time.time() - start_time
|
|
|
|
# Verify all succeeded
|
|
failures = [r for r in results if isinstance(r, Exception)]
|
|
assert not failures, f'Concurrent operations failed: {failures}'
|
|
|
|
# Check that execution was actually concurrent (should be faster than sequential)
|
|
# Sequential would take at least 6 * processing_time
|
|
assert execution_time < 30, f'Concurrent execution too slow: {execution_time}s'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_overflow_handling(self):
|
|
"""Test behavior when queue reaches capacity."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Attempt to add many episodes rapidly
|
|
tasks = []
|
|
for i in range(100): # Large number to potentially overflow
|
|
task = session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': f'Overflow Test {i}',
|
|
'episode_body': f'Episode {i}',
|
|
'source': 'text',
|
|
'source_description': 'overflow test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
tasks.append(task)
|
|
|
|
# Execute with gathering to catch any failures
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# Count successful queuing
|
|
successful = sum(1 for r in results if not isinstance(r, Exception))
|
|
|
|
# Should handle overflow gracefully
|
|
assert successful > 0, 'No episodes were queued successfully'
|
|
|
|
# Log overflow behavior
|
|
if successful < 100:
|
|
print(f'Queue overflow: {successful}/100 episodes queued')
|
|
|
|
|
|
class TestConcurrentOperations:
|
|
"""Test concurrent tool calls and operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_search_operations(self):
|
|
"""Test multiple concurrent search operations."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# First, add some test data
|
|
data_gen = TestDataGenerator()
|
|
|
|
add_tasks = []
|
|
for _ in range(5):
|
|
task = session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': 'Search Test Data',
|
|
'episode_body': data_gen.generate_technical_document(),
|
|
'source': 'text',
|
|
'source_description': 'search test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
add_tasks.append(task)
|
|
|
|
await asyncio.gather(*add_tasks)
|
|
await asyncio.sleep(15) # Wait for processing
|
|
|
|
# Now perform concurrent searches
|
|
search_queries = [
|
|
'architecture',
|
|
'performance',
|
|
'implementation',
|
|
'dependencies',
|
|
'latency',
|
|
]
|
|
|
|
search_tasks = []
|
|
for query in search_queries:
|
|
task = session.call_tool(
|
|
'search_memory_nodes',
|
|
{
|
|
'query': query,
|
|
'group_id': group_id,
|
|
'limit': 10,
|
|
},
|
|
)
|
|
search_tasks.append(task)
|
|
|
|
start_time = time.time()
|
|
results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
|
search_time = time.time() - start_time
|
|
|
|
# Verify all searches completed
|
|
failures = [r for r in results if isinstance(r, Exception)]
|
|
assert not failures, f'Search operations failed: {failures}'
|
|
|
|
# Verify concurrent execution efficiency
|
|
assert search_time < len(search_queries) * 2, 'Searches not executing concurrently'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mixed_operation_concurrency(self):
|
|
"""Test different types of operations running concurrently."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
operations = []
|
|
|
|
# Add memory operation
|
|
operations.append(
|
|
session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': 'Mixed Op Test',
|
|
'episode_body': 'Testing mixed operations',
|
|
'source': 'text',
|
|
'source_description': 'test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
)
|
|
|
|
# Search operation
|
|
operations.append(
|
|
session.call_tool(
|
|
'search_memory_nodes',
|
|
{
|
|
'query': 'test',
|
|
'group_id': group_id,
|
|
'limit': 5,
|
|
},
|
|
)
|
|
)
|
|
|
|
# Get episodes operation
|
|
operations.append(
|
|
session.call_tool(
|
|
'get_episodes',
|
|
{
|
|
'group_id': group_id,
|
|
'last_n': 10,
|
|
},
|
|
)
|
|
)
|
|
|
|
# Get status operation
|
|
operations.append(session.call_tool('get_status', {}))
|
|
|
|
# Execute all concurrently
|
|
results = await asyncio.gather(*operations, return_exceptions=True)
|
|
|
|
# Check results
|
|
for i, result in enumerate(results):
|
|
assert not isinstance(result, Exception), f'Operation {i} failed: {result}'
|
|
|
|
|
|
class TestAsyncErrorHandling:
|
|
"""Test async error handling and recovery."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timeout_recovery(self):
|
|
"""Test recovery from operation timeouts."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Create a very large episode that might timeout
|
|
large_content = 'x' * 1000000 # 1MB of data
|
|
|
|
with contextlib.suppress(asyncio.TimeoutError):
|
|
await asyncio.wait_for(
|
|
session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': 'Timeout Test',
|
|
'episode_body': large_content,
|
|
'source': 'text',
|
|
'source_description': 'timeout test',
|
|
'group_id': group_id,
|
|
},
|
|
),
|
|
timeout=2.0, # Short timeout - expected to timeout
|
|
)
|
|
|
|
# Verify server is still responsive after timeout
|
|
status_result = await session.call_tool('get_status', {})
|
|
assert status_result is not None, 'Server unresponsive after timeout'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancellation_handling(self):
|
|
"""Test proper handling of cancelled operations."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Start a long-running operation
|
|
task = asyncio.create_task(
|
|
session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': 'Cancellation Test',
|
|
'episode_body': TestDataGenerator.generate_technical_document(),
|
|
'source': 'text',
|
|
'source_description': 'cancel test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
)
|
|
|
|
# Cancel after a short delay
|
|
await asyncio.sleep(0.1)
|
|
task.cancel()
|
|
|
|
# Verify cancellation was handled
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|
|
|
|
# Server should still be operational
|
|
result = await session.call_tool('get_status', {})
|
|
assert result is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exception_propagation(self):
|
|
"""Test that exceptions are properly propagated in async context."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Call with invalid arguments
|
|
with pytest.raises(ValueError):
|
|
await session.call_tool(
|
|
'add_memory',
|
|
{
|
|
# Missing required fields
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
|
|
# Server should remain operational
|
|
status = await session.call_tool('get_status', {})
|
|
assert status is not None
|
|
|
|
|
|
class TestAsyncPerformance:
|
|
"""Performance tests for async operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_throughput(self, performance_benchmark):
|
|
"""Measure throughput of async operations."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
num_operations = 50
|
|
start_time = time.time()
|
|
|
|
# Create many concurrent operations
|
|
tasks = []
|
|
for i in range(num_operations):
|
|
task = session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': f'Throughput Test {i}',
|
|
'episode_body': f'Content {i}',
|
|
'source': 'text',
|
|
'source_description': 'throughput test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
tasks.append(task)
|
|
|
|
# Execute all
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
total_time = time.time() - start_time
|
|
|
|
# Calculate metrics
|
|
successful = sum(1 for r in results if not isinstance(r, Exception))
|
|
throughput = successful / total_time
|
|
|
|
performance_benchmark.record('async_throughput', throughput)
|
|
|
|
# Log results
|
|
print('\nAsync Throughput Test:')
|
|
print(f' Operations: {num_operations}')
|
|
print(f' Successful: {successful}')
|
|
print(f' Total time: {total_time:.2f}s')
|
|
print(f' Throughput: {throughput:.2f} ops/s')
|
|
|
|
# Assert minimum throughput
|
|
assert throughput > 1.0, f'Throughput too low: {throughput:.2f} ops/s'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_latency_under_load(self, performance_benchmark):
|
|
"""Test operation latency under concurrent load."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Create background load
|
|
background_tasks = []
|
|
for i in range(10):
|
|
task = asyncio.create_task(
|
|
session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': f'Background {i}',
|
|
'episode_body': TestDataGenerator.generate_technical_document(),
|
|
'source': 'text',
|
|
'source_description': 'background',
|
|
'group_id': f'background_{group_id}',
|
|
},
|
|
)
|
|
)
|
|
background_tasks.append(task)
|
|
|
|
# Measure latency of operations under load
|
|
latencies = []
|
|
for _ in range(5):
|
|
start = time.time()
|
|
await session.call_tool('get_status', {})
|
|
latency = time.time() - start
|
|
latencies.append(latency)
|
|
performance_benchmark.record('latency_under_load', latency)
|
|
|
|
# Clean up background tasks
|
|
for task in background_tasks:
|
|
task.cancel()
|
|
|
|
# Analyze latencies
|
|
avg_latency = sum(latencies) / len(latencies)
|
|
max_latency = max(latencies)
|
|
|
|
print('\nLatency Under Load:')
|
|
print(f' Average: {avg_latency:.3f}s')
|
|
print(f' Max: {max_latency:.3f}s')
|
|
|
|
# Assert acceptable latency
|
|
assert avg_latency < 2.0, f'Average latency too high: {avg_latency:.3f}s'
|
|
assert max_latency < 5.0, f'Max latency too high: {max_latency:.3f}s'
|
|
|
|
|
|
class TestAsyncStreamHandling:
|
|
"""Test handling of streaming responses and data."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_large_response_streaming(self):
|
|
"""Test handling of large streamed responses."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Add many episodes
|
|
for i in range(20):
|
|
await session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': f'Stream Test {i}',
|
|
'episode_body': f'Episode content {i}',
|
|
'source': 'text',
|
|
'source_description': 'stream test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
|
|
# Wait for processing
|
|
await asyncio.sleep(30)
|
|
|
|
# Request large result set
|
|
result = await session.call_tool(
|
|
'get_episodes',
|
|
{
|
|
'group_id': group_id,
|
|
'last_n': 100, # Request all
|
|
},
|
|
)
|
|
|
|
# Verify response handling
|
|
episodes = json.loads(result.content[0].text)['episodes']
|
|
assert len(episodes) >= 20, f'Expected at least 20 episodes, got {len(episodes)}'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_incremental_processing(self):
|
|
"""Test incremental processing of results."""
|
|
async with graphiti_test_client() as (session, group_id):
|
|
# Add episodes incrementally
|
|
for batch in range(3):
|
|
batch_tasks = []
|
|
for i in range(5):
|
|
task = session.call_tool(
|
|
'add_memory',
|
|
{
|
|
'name': f'Batch {batch} Item {i}',
|
|
'episode_body': f'Content for batch {batch}',
|
|
'source': 'text',
|
|
'source_description': 'incremental test',
|
|
'group_id': group_id,
|
|
},
|
|
)
|
|
batch_tasks.append(task)
|
|
|
|
# Process batch
|
|
await asyncio.gather(*batch_tasks)
|
|
|
|
# Wait for this batch to process
|
|
await asyncio.sleep(10)
|
|
|
|
# Verify incremental results
|
|
result = await session.call_tool(
|
|
'get_episodes',
|
|
{
|
|
'group_id': group_id,
|
|
'last_n': 100,
|
|
},
|
|
)
|
|
|
|
episodes = json.loads(result.content[0].text)['episodes']
|
|
expected_min = (batch + 1) * 5
|
|
assert len(episodes) >= expected_min, (
|
|
f'Batch {batch}: Expected at least {expected_min} episodes'
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__, '-v', '--asyncio-mode=auto'])
|