From 4ab47c097ea7a4c1a7dfb220be6e9d95b35ea0b4 Mon Sep 17 00:00:00 2001 From: donbr Date: Sat, 6 Dec 2025 23:25:31 -0800 Subject: [PATCH] feat(mcp_server): Add FastMCP Cloud deployment support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tooling and configuration for deploying the Graphiti MCP server to FastMCP Cloud with Neo4j or FalkorDB backends. Changes: - Add DATABASE_PROVIDER env var support in config.yaml for runtime database selection (neo4j or falkordb) - Add EMBEDDING_DIM, EMBEDDER_PROVIDER, EMBEDDER_MODEL env var support for embedder configuration - Add python-dotenv and pydantic to pyproject.toml dependencies - Bump version to 1.0.2 - Rewrite .env.example with comprehensive documentation for both local development and FastMCP Cloud deployment - Add verification script (scripts/verify_fastmcp_cloud_readiness.py) that checks 6 deployment prerequisites - Add deployment guide (docs/FASTMCP_CLOUD_DEPLOYMENT.md) The server can now be configured entirely via environment variables, making it compatible with FastMCP Cloud which ignores config files. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- mcp_server/.env.example | 133 ++++-- mcp_server/config/config.yaml | 8 +- mcp_server/docs/FASTMCP_CLOUD_DEPLOYMENT.md | 451 ++++++++++++++++++ mcp_server/pyproject.toml | 4 +- .../scripts/verify_fastmcp_cloud_readiness.py | 350 ++++++++++++++ 5 files changed, 914 insertions(+), 32 deletions(-) create mode 100644 mcp_server/docs/FASTMCP_CLOUD_DEPLOYMENT.md create mode 100644 mcp_server/scripts/verify_fastmcp_cloud_readiness.py diff --git a/mcp_server/.env.example b/mcp_server/.env.example index dd4677b2..1ebd6b5e 100644 --- a/mcp_server/.env.example +++ b/mcp_server/.env.example @@ -1,25 +1,99 @@ # Graphiti MCP Server Environment Configuration +# ============================================= +# +# For LOCAL development: Copy this file to .env and fill in your values +# For FASTMCP CLOUD: Set these in the FastMCP Cloud UI (NOT in .env files) +# +# FastMCP Cloud ignores .env files - you MUST set secrets in the Cloud UI -# Neo4j Database Configuration -# These settings are used to connect to your Neo4j database +# ============================================================================= +# DATABASE CONFIGURATION (Choose ONE provider) +# ============================================================================= + +# Database Provider Selection +# Options: neo4j, falkordb +DATABASE_PROVIDER=neo4j + +# --- Neo4j Configuration --- +# For local development: bolt://localhost:7687 +# For Neo4j Aura (cloud): neo4j+s://xxxxx.databases.neo4j.io NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j -NEO4J_PASSWORD=demodemo +NEO4J_PASSWORD=your_neo4j_password_here +NEO4J_DATABASE=neo4j -# OpenAI API Configuration -# Required for LLM operations +# --- FalkorDB Configuration --- +# For local development: redis://localhost:6379 +# For FalkorDB Cloud: redis://username:password@host:port +# FALKORDB_URI=redis://localhost:6379 +# FALKORDB_PASSWORD= +# FALKORDB_DATABASE=default_db +# FALKORDB_USER= + +# ============================================================================= +# LLM PROVIDER CONFIGURATION (Required) +# ============================================================================= + +# OpenAI (Default) OPENAI_API_KEY=your_openai_api_key_here -MODEL_NAME=gpt-4.1-mini -# Optional: Only needed for non-standard OpenAI endpoints -# OPENAI_BASE_URL=https://api.openai.com/v1 +# Optional: Override default model +# LLM_MODEL=gpt-4.1-mini -# Optional: Group ID for namespacing graph data -# GROUP_ID=my_project +# --- Alternative LLM Providers --- + +# Anthropic +# ANTHROPIC_API_KEY=sk-ant-... + +# Google Gemini +# GOOGLE_API_KEY=... +# GOOGLE_PROJECT_ID= +# GOOGLE_LOCATION=us-central1 + +# Groq +# GROQ_API_KEY=... + +# Azure OpenAI +# AZURE_OPENAI_API_KEY=... +# AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com +# AZURE_OPENAI_DEPLOYMENT=your-deployment-name +# AZURE_OPENAI_API_VERSION=2024-10-21 +# USE_AZURE_AD=false + +# ============================================================================= +# EMBEDDER CONFIGURATION (Optional - defaults to OpenAI) +# ============================================================================= + +# Voyage AI (recommended by Anthropic for Claude integrations) +# VOYAGE_API_KEY=... +# Note: Voyage AI uses 1024 dimensions by default + +# Embedding dimensions (must match your embedding model) +# OpenAI text-embedding-3-small: 1536 +# Voyage AI voyage-3: 1024 +# EMBEDDING_DIM=1536 + +# ============================================================================= +# GRAPHITI CONFIGURATION +# ============================================================================= + +# Group ID for namespacing graph data +GRAPHITI_GROUP_ID=main + +# User ID for tracking operations +USER_ID=mcp_user + +# Episode ID prefix (optional) +# EPISODE_ID_PREFIX= + +# ============================================================================= +# PERFORMANCE TUNING +# ============================================================================= # Concurrency Control # Controls how many episodes can be processed simultaneously # Default: 10 (suitable for OpenAI Tier 3, mid-tier Anthropic) +# # Adjust based on your LLM provider's rate limits: # - OpenAI Tier 1 (free): 1-2 # - OpenAI Tier 2: 5-8 @@ -27,23 +101,28 @@ MODEL_NAME=gpt-4.1-mini # - OpenAI Tier 4: 20-50 # - Anthropic default: 5-8 # - Anthropic high tier: 15-30 -# - Ollama (local): 1-5 +# # See README.md "Concurrency and LLM Provider 429 Rate Limit Errors" for details SEMAPHORE_LIMIT=10 -# Optional: Path configuration for Docker -# PATH=/root/.local/bin:${PATH} - -# Optional: Memory settings for Neo4j (used in Docker Compose) -# NEO4J_server_memory_heap_initial__size=512m -# NEO4J_server_memory_heap_max__size=1G -# NEO4J_server_memory_pagecache_size=512m - -# Azure OpenAI configuration -# Optional: Only needed for Azure OpenAI endpoints -# AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint_here -# AZURE_OPENAI_API_VERSION=2025-01-01-preview -# AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-gpt-4o-mini-deployment -# AZURE_OPENAI_EMBEDDING_API_VERSION=2023-05-15 -# AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-3-large-deployment -# AZURE_OPENAI_USE_MANAGED_IDENTITY=false +# ============================================================================= +# FASTMCP CLOUD DEPLOYMENT CHECKLIST +# ============================================================================= +# When deploying to FastMCP Cloud, set these in the Cloud UI: +# +# REQUIRED: +# - OPENAI_API_KEY (or your chosen LLM provider key) +# - Database credentials (Neo4j or FalkorDB) +# - For Neo4j Aura: NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD +# - For FalkorDB Cloud: FALKORDB_URI, FALKORDB_USER, FALKORDB_PASSWORD +# +# OPTIONAL: +# - DATABASE_PROVIDER (defaults to falkordb) +# - SEMAPHORE_LIMIT (defaults to 10) +# - GRAPHITI_GROUP_ID (defaults to main) +# +# IMPORTANT: FastMCP Cloud IGNORES: +# - .env files +# - config.yaml files +# - if __name__ == "__main__" blocks +# ============================================================================= diff --git a/mcp_server/config/config.yaml b/mcp_server/config/config.yaml index 91f72377..1ea7bc40 100644 --- a/mcp_server/config/config.yaml +++ b/mcp_server/config/config.yaml @@ -43,9 +43,9 @@ llm: api_url: ${GROQ_API_URL:https://api.groq.com/openai/v1} embedder: - provider: "openai" # Options: openai, azure_openai, gemini, voyage - model: "text-embedding-3-small" - dimensions: 1536 + provider: ${EMBEDDER_PROVIDER:openai} # Options: openai, azure_openai, gemini, voyage + model: ${EMBEDDER_MODEL:text-embedding-3-small} + dimensions: ${EMBEDDING_DIM:1536} # OpenAI: 1536, Voyage: 1024 providers: openai: @@ -71,7 +71,7 @@ embedder: model: "voyage-3" database: - provider: "falkordb" # Default: falkordb. Options: neo4j, falkordb + provider: ${DATABASE_PROVIDER:falkordb} # Options: neo4j, falkordb. Set DATABASE_PROVIDER env var to override providers: falkordb: diff --git a/mcp_server/docs/FASTMCP_CLOUD_DEPLOYMENT.md b/mcp_server/docs/FASTMCP_CLOUD_DEPLOYMENT.md new file mode 100644 index 00000000..4ba96df5 --- /dev/null +++ b/mcp_server/docs/FASTMCP_CLOUD_DEPLOYMENT.md @@ -0,0 +1,451 @@ +# FastMCP Cloud Deployment Guide + +This guide covers deploying the Graphiti MCP server to FastMCP Cloud, a managed hosting platform for MCP servers. + +## Overview + +FastMCP Cloud is a managed platform that: +- Automatically builds and deploys your MCP server from GitHub +- Provides a unique HTTPS URL for your server +- Handles SSL certificates and authentication +- Auto-redeploys on pushes to `main` branch +- Creates preview deployments for pull requests + +**Note:** FastMCP Cloud is currently **free while in beta**. + +## Prerequisites + +Before deploying to FastMCP Cloud, you need: + +1. **GitHub Account** - FastMCP Cloud integrates with GitHub repos +2. **Cloud Database** - Neo4j Aura or FalkorDB Cloud (must be internet-accessible) +3. **API Keys** - OpenAI (required), Anthropic (optional) +4. **Verified Repo** - Run the verification script first + +### Pre-Deployment Verification + +Run the verification script to check your server is ready: + +```bash +cd mcp_server +uv run python scripts/verify_fastmcp_cloud_readiness.py +``` + +This checks: +- Server is discoverable via `fastmcp inspect` +- Dependencies are properly declared in `pyproject.toml` +- Environment variables are documented +- No secrets are committed to git +- Server can be imported successfully +- Entrypoint format is correct + +All checks should pass before deploying. + +## Deployment Steps + +### Step 0: Validate Your Server Locally + +**Before deploying to FastMCP Cloud, validate with BOTH static and runtime checks.** + +#### Static Validation: `fastmcp inspect` + +Checks that your server module can be imported and tools are registered: + +```bash +cd mcp_server +uv run fastmcp inspect src/graphiti_mcp_server.py:mcp +``` + +**Expected successful output:** + +``` +Name: Graphiti Agent Memory +Version: +Tools: 9 found + - add_memory: Add an episode to memory + - search_nodes: Search for nodes in the graph memory + - search_memory_facts: Search the graph memory for relevant facts + - get_episodes: Get episodes from the graph memory + - get_entity_edge: Get an entity edge from the graph memory by its UUID + - delete_episode: Delete an episode from the graph memory + - delete_entity_edge: Delete an entity edge from the graph memory + - clear_graph: Clear all data from the graph for specified group IDs + - get_status: Get the status of the Graphiti MCP server +``` + +#### Runtime Validation: `fastmcp dev` (ESSENTIAL!) + +**The `inspect` command only checks imports - it does NOT catch runtime initialization issues.** + +Run the interactive inspector to test actual server initialization: + +```bash +cd mcp_server +uv run fastmcp dev src/graphiti_mcp_server.py:mcp +``` + +This starts your server and opens an interactive web UI at `http://localhost:6274`. + +**Critical test in the web UI:** + +1. Open `http://localhost:6274` in your browser +2. Click the "get_status" tool +3. Click "Execute" +4. **Expected:** `{"status": "ok", "message": "Graphiti MCP server is running and connected to Neo4j database"}` +5. **If you see:** `{"status": "error", "message": "Graphiti service not initialized"}` - **DO NOT DEPLOY** + +### Step 1: Set Up Cloud Database + +#### Option A: Neo4j Aura (Recommended for Neo4j users) + +1. Visit [Neo4j Aura](https://neo4j.com/cloud/aura/) +2. Create a free instance +3. Note your connection details: + - URI: `neo4j+s://xxxxx.databases.neo4j.io` + - Username: `neo4j` + - Password: (generated) + +#### Option B: FalkorDB Cloud + +1. Visit [FalkorDB Cloud](https://cloud.falkordb.com) +2. Create an instance +3. Note your connection details: + - URI: `redis://username:password@host:port` + - Database: `default_db` + +**Important:** Local databases (localhost) will NOT work with FastMCP Cloud. You must use a cloud-hosted database. + +### Step 2: Prepare Your Repository + +1. **Ensure `pyproject.toml` is complete** + + FastMCP Cloud automatically detects dependencies from `pyproject.toml`: + ```toml + [project] + dependencies = [ + "fastmcp>=2.13.3", + "graphiti-core[falkordb]>=0.23.1", + "pydantic>=2.0.0", + "pydantic-settings>=2.0.0", + "python-dotenv>=1.0.0", + # ... other dependencies + ] + ``` + +2. **Verify `.env` is in `.gitignore`** + + ```bash + git check-ignore -v .env + # Should output: .gitignore:XX:.env .env + ``` + +3. **Commit and push your code** + + ```bash + git add . + git commit -m "Prepare for FastMCP Cloud deployment" + git push origin main + ``` + +### Step 3: Create FastMCP Cloud Project + +1. **Visit [fastmcp.cloud](https://fastmcp.cloud)** + +2. **Sign in with your GitHub account** + +3. **Create a new project:** + - Click "Create Project" + - Select your repository + - Repository can be public or private + +4. **Configure project settings:** + + | Setting | Value | Notes | + |---------|-------|-------| + | **Name** | `graphiti-mcp` | Used in your deployment URL | + | **Entrypoint** | `mcp_server/src/graphiti_mcp_server.py:mcp` | Points to module-level server instance | + | **Authentication** | Enabled | Recommended for production | + + **Important:** The entrypoint must point to a **module-level** `FastMCP` instance. + +### Step 4: Configure Environment Variables + +Set these environment variables in the FastMCP Cloud UI (**NOT** in `.env` files): + +#### For Neo4j: + +```bash +# Required +OPENAI_API_KEY=sk-... +NEO4J_URI=neo4j+s://xxxxx.databases.neo4j.io +NEO4J_USER=neo4j +NEO4J_PASSWORD=your-password +DATABASE_PROVIDER=neo4j + +# Optional +SEMAPHORE_LIMIT=10 +GRAPHITI_GROUP_ID=main +``` + +#### For FalkorDB: + +```bash +# Required +OPENAI_API_KEY=sk-... +FALKORDB_URI=redis://host:port +FALKORDB_USER=your-username +FALKORDB_PASSWORD=your-password +FALKORDB_DATABASE=default_db +DATABASE_PROVIDER=falkordb + +# Optional +SEMAPHORE_LIMIT=10 +GRAPHITI_GROUP_ID=main +``` + +**Security Note:** Environment variables set in FastMCP Cloud UI are encrypted at rest and never logged. + +### Step 5: Deploy + +1. **Click "Deploy"** + + FastMCP Cloud will: + 1. Clone your repository + 2. Detect dependencies from `pyproject.toml` + 3. Install dependencies using `uv` + 4. Build your FastMCP server + 5. Deploy to a unique URL + 6. Make it immediately available + +2. **Monitor the build** + + Watch the build logs in the FastMCP Cloud UI. The build typically takes 2-5 minutes. + +3. **Note your deployment URL** + + Your server will be accessible at: + ``` + https://your-project-name.fastmcp.app/mcp + ``` + +### Step 6: Verify Deployment + +1. **Test with `fastmcp inspect`** + + ```bash + fastmcp inspect https://your-project-name.fastmcp.app/mcp + ``` + + You should see your server info and 9 tools. + +2. **Connect from Claude Desktop** + + FastMCP Cloud provides auto-generated configuration. Click "Connect" in the UI and copy the configuration. + +3. **Test add_memory tool** + + Use Claude Desktop or an MCP client to test: + ``` + Add a memory: "John prefers dark mode UI" + ``` + +## Configuration Differences + +### FastMCP Cloud vs Local Development + +| Aspect | FastMCP Cloud | Local Development | +|--------|---------------|-------------------| +| **Entry point** | Module-level instance only | `if __name__ == "__main__"` runs | +| **Dependencies** | Auto-detected from `pyproject.toml` | Installed via `uv sync` | +| **Environment** | Set in Cloud UI | Loaded from `.env` file | +| **Transport** | Managed by platform | Configured via CLI args | +| **HTTPS** | Automatic | Manual setup | +| **Authentication** | Built-in OAuth | Configure manually | + +### What Gets Ignored + +FastMCP Cloud **ignores**: + +- `if __name__ == "__main__"` blocks +- `.env` files (use Cloud UI instead) +- `fastmcp.json` config files (use Cloud UI) +- YAML config files (use environment variables) +- Docker configurations +- CLI arguments + +FastMCP Cloud **uses**: + +- Module-level `FastMCP` instance (`mcp = FastMCP(...)`) +- `pyproject.toml` or `requirements.txt` +- Environment variables from Cloud UI +- Code from your `main` branch + +## Troubleshooting + +### Build Failures + +**Issue:** Dependencies fail to install + +``` +Solution: +1. Verify pyproject.toml syntax +2. Check dependency versions are available on PyPI +3. Remove any local-only dependencies (like editable installs) +4. Check that python-dotenv is included +``` + +**Issue:** Module import errors + +``` +Solution: +1. Ensure all imports use relative paths from src/ +2. Check that config/, models/, etc. have __init__.py files +3. Verify the entrypoint format: mcp_server/src/graphiti_mcp_server.py:mcp +``` + +### Runtime Errors + +**Issue:** "API key is not configured" + +``` +Solution: +1. Verify environment variables are set in FastMCP Cloud UI +2. Check variable names match exactly (case-sensitive) +3. Redeploy after adding environment variables +``` + +**Issue:** Database connection failures + +``` +Solution: +1. Verify database is internet-accessible (not localhost!) +2. Check credentials are correct +3. For Neo4j Aura: Use neo4j+s:// protocol +4. For FalkorDB: Check firewall allows FastMCP Cloud IPs +``` + +**Issue:** 429 Rate Limit Errors + +``` +Solution: +1. Lower SEMAPHORE_LIMIT based on your API tier: + - OpenAI Tier 1: SEMAPHORE_LIMIT=1-2 + - OpenAI Tier 2: SEMAPHORE_LIMIT=5-8 + - OpenAI Tier 3: SEMAPHORE_LIMIT=10-15 +``` + +**Issue:** "Graphiti service not initialized" + +``` +Solution: +1. This means initialization failed silently +2. Check database credentials +3. Check LLM API key +4. Run fastmcp dev locally to debug +``` + +## Best Practices + +### 1. Use Environment Variables + +All configuration should use environment variables: + +```python +# Good - FastMCP Cloud compatible +import os +api_key = os.environ.get('OPENAI_API_KEY') + +# Bad - Won't work on FastMCP Cloud +api_key = 'sk-hardcoded-key' +``` + +### 2. Module-Level Server Instance + +```python +# Good - FastMCP Cloud can discover this +from fastmcp import FastMCP +mcp = FastMCP("Graphiti Agent Memory") + +if __name__ == "__main__": + # This block is IGNORED by FastMCP Cloud + mcp.run() +``` + +### 3. Test Locally First + +Always test locally before deploying: + +```bash +# Run verification script +cd mcp_server +uv run python scripts/verify_fastmcp_cloud_readiness.py + +# Test with fastmcp dev +uv run fastmcp dev src/graphiti_mcp_server.py:mcp +``` + +### 4. Monitor Resource Usage + +- **Neo4j Aura free tier:** Limited connections +- **FalkorDB free tier:** 100 MB limit +- **OpenAI rate limits:** Tier-dependent +- **SEMAPHORE_LIMIT:** Tune based on API tier + +## Security Considerations + +### Secrets Management + +- **DO:** Set secrets in FastMCP Cloud UI +- **DO:** Add `.env` to `.gitignore` +- **DO:** Use `.env.example` for documentation +- **DON'T:** Commit `.env` files +- **DON'T:** Hardcode API keys +- **DON'T:** Store secrets in YAML configs + +### Authentication + +FastMCP Cloud provides built-in authentication: + +- **Enabled:** Only org members can connect (recommended) +- **Disabled:** Public access (use for demos only) + +Enable authentication for production deployments. + +## Summary Checklist + +Before deploying to FastMCP Cloud: + +- [ ] Run `uv run python scripts/verify_fastmcp_cloud_readiness.py` +- [ ] All checks pass +- [ ] Cloud database is running (Neo4j Aura or FalkorDB Cloud) +- [ ] API keys are ready (OpenAI required) +- [ ] Code is pushed to GitHub `main` branch +- [ ] `.env` is in `.gitignore` +- [ ] No secrets committed to repo + +During deployment: + +- [ ] Create project on fastmcp.cloud +- [ ] Configure entrypoint: `mcp_server/src/graphiti_mcp_server.py:mcp` +- [ ] Enable authentication +- [ ] Set all required environment variables in UI +- [ ] Deploy and monitor build logs + +After deployment: + +- [ ] Test with `fastmcp inspect ` +- [ ] Connect from Claude Desktop +- [ ] Test add_memory and search tools +- [ ] Monitor database usage +- [ ] Monitor API rate limits + +## Resources + +- **FastMCP Cloud:** [fastmcp.cloud](https://fastmcp.cloud) +- **FastMCP Docs:** [gofastmcp.com](https://gofastmcp.com) +- **FastMCP Discord:** [discord.com/invite/aGsSC3yDF4](https://discord.com/invite/aGsSC3yDF4) +- **Neo4j Aura:** [neo4j.com/cloud/aura](https://neo4j.com/cloud/aura) +- **FalkorDB Cloud:** [cloud.falkordb.com](https://cloud.falkordb.com) +- **Verification Script:** [`scripts/verify_fastmcp_cloud_readiness.py`](../scripts/verify_fastmcp_cloud_readiness.py) + +You're now ready to deploy! diff --git a/mcp_server/pyproject.toml b/mcp_server/pyproject.toml index 2946f895..72308fdd 100644 --- a/mcp_server/pyproject.toml +++ b/mcp_server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-server" -version = "1.0.1" +version = "1.0.2" description = "Graphiti MCP Server" readme = "README.md" requires-python = ">=3.10,<4" @@ -8,8 +8,10 @@ dependencies = [ "fastmcp>=2.13.3", "openai>=1.91.0", "graphiti-core[falkordb]>=0.23.1", + "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "pyyaml>=6.0", + "python-dotenv>=1.0.0", "typing-extensions>=4.0.0", ] diff --git a/mcp_server/scripts/verify_fastmcp_cloud_readiness.py b/mcp_server/scripts/verify_fastmcp_cloud_readiness.py new file mode 100644 index 00000000..dc185919 --- /dev/null +++ b/mcp_server/scripts/verify_fastmcp_cloud_readiness.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +""" +Verify FastMCP Cloud deployment readiness. + +This script checks that your Graphiti MCP server is ready for FastMCP Cloud deployment. +Run this before deploying to catch common issues. + +Usage: + cd mcp_server + uv run python scripts/verify_fastmcp_cloud_readiness.py +""" + +import os +import subprocess +import sys +from pathlib import Path + + +def print_header(title: str) -> None: + """Print a section header.""" + print(f'\n{"=" * 60}') + print(f' {title}') + print('=' * 60) + + +def print_result(check: str, passed: bool, details: str = '') -> None: + """Print a check result.""" + status = 'āœ… PASSED' if passed else 'āŒ FAILED' + print(f'{status}: {check}') + if details: + for line in details.split('\n'): + print(f' {line}') + + +def check_server_discoverability() -> bool: + """Check if fastmcp inspect can discover the server.""" + print_header('Check 1: Server Discoverability') + + # Find the server file + script_dir = Path(__file__).parent + mcp_server_dir = script_dir.parent + server_file = mcp_server_dir / 'src' / 'graphiti_mcp_server.py' + + if not server_file.exists(): + print_result('Server file exists', False, f'Not found: {server_file}') + return False + + print_result('Server file exists', True, str(server_file)) + + # Try to run fastmcp inspect + try: + result = subprocess.run( + ['uv', 'run', 'fastmcp', 'inspect', f'{server_file}:mcp'], + capture_output=True, + text=True, + cwd=mcp_server_dir, + timeout=30, + ) + + if result.returncode == 0: + # Count tools in output + output = result.stdout + tool_count = output.count('- ') if '- ' in output else 0 + print_result( + 'fastmcp inspect succeeds', + True, + f'Server discovered with {tool_count} tools', + ) + return True + else: + print_result( + 'fastmcp inspect succeeds', + False, + f'Error: {result.stderr[:200] if result.stderr else result.stdout[:200]}', + ) + return False + except subprocess.TimeoutExpired: + print_result('fastmcp inspect succeeds', False, 'Command timed out') + return False + except FileNotFoundError: + print_result( + 'fastmcp inspect succeeds', + False, + 'fastmcp not found. Run: uv sync', + ) + return False + except Exception as e: + print_result('fastmcp inspect succeeds', False, f'Error: {e}') + return False + + +def check_dependencies() -> bool: + """Check that all dependencies are declared in pyproject.toml.""" + print_header('Check 2: Dependencies') + + script_dir = Path(__file__).parent + mcp_server_dir = script_dir.parent + pyproject_file = mcp_server_dir / 'pyproject.toml' + + if not pyproject_file.exists(): + print_result('pyproject.toml exists', False) + return False + + print_result('pyproject.toml exists', True) + + # Read and check for critical dependencies + content = pyproject_file.read_text() + critical_deps = [ + 'fastmcp', + 'graphiti-core', + 'pydantic', + 'pydantic-settings', + 'python-dotenv', + ] + + missing = [] + for dep in critical_deps: + if dep not in content: + missing.append(dep) + + if missing: + print_result( + 'Critical dependencies declared', + False, + f'Missing: {", ".join(missing)}', + ) + return False + + print_result('Critical dependencies declared', True, ', '.join(critical_deps)) + return True + + +def check_env_documentation() -> bool: + """Check that environment variables are documented.""" + print_header('Check 3: Environment Variable Documentation') + + script_dir = Path(__file__).parent + mcp_server_dir = script_dir.parent + env_example = mcp_server_dir / '.env.example' + + if not env_example.exists(): + print_result('.env.example exists', False) + return False + + content = env_example.read_text() + required_vars = ['OPENAI_API_KEY', 'NEO4J_URI', 'FALKORDB_URI'] + + documented = [var for var in required_vars if var in content] + + print_result('.env.example exists', True) + print_result( + 'Required vars documented', + len(documented) == len(required_vars), + f'Found: {", ".join(documented)}', + ) + + return len(documented) == len(required_vars) + + +def check_secrets_safety() -> bool: + """Check that no secrets are committed.""" + print_header('Check 4: Secrets Safety') + + script_dir = Path(__file__).parent + mcp_server_dir = script_dir.parent + env_file = mcp_server_dir / '.env' + gitignore_file = mcp_server_dir.parent / '.gitignore' + + # Check if .env exists (it shouldn't be committed) + if env_file.exists(): + # Check if it's in gitignore + if gitignore_file.exists(): + gitignore_content = gitignore_file.read_text() + if '.env' in gitignore_content: + print_result('.env in .gitignore', True) + else: + print_result('.env in .gitignore', False, 'Add .env to .gitignore!') + return False + else: + print_result('.gitignore exists', False) + return False + else: + print_result('.env not present (good for cloud)', True) + + # Check for hardcoded secrets in server file + server_file = mcp_server_dir / 'src' / 'graphiti_mcp_server.py' + if server_file.exists(): + content = server_file.read_text() + secret_patterns = ['sk-', 'api_key=', 'password='] + found_secrets = [] + for pattern in secret_patterns: + if pattern in content.lower() and f"'{pattern}" in content: + found_secrets.append(pattern) + + if found_secrets: + print_result( + 'No hardcoded secrets in server', + False, + f'Found patterns: {found_secrets}', + ) + return False + print_result('No hardcoded secrets in server', True) + + return True + + +def check_server_import() -> bool: + """Check that the server can be imported successfully.""" + print_header('Check 5: Server Import') + + script_dir = Path(__file__).parent + mcp_server_dir = script_dir.parent + src_dir = mcp_server_dir / 'src' + + # Add src to path temporarily + sys.path.insert(0, str(src_dir)) + + try: + # Try importing the module + import importlib.util + + spec = importlib.util.spec_from_file_location( + 'graphiti_mcp_server', + src_dir / 'graphiti_mcp_server.py', + ) + if spec and spec.loader: + module = importlib.util.module_from_spec(spec) + # Don't actually execute - just check if it can be loaded + print_result('Server module loadable', True) + + # Check for mcp object + server_content = (src_dir / 'graphiti_mcp_server.py').read_text() + if 'mcp = FastMCP(' in server_content: + print_result('Module-level mcp instance found', True) + else: + print_result( + 'Module-level mcp instance found', + False, + 'Need: mcp = FastMCP(...) at module level', + ) + return False + + return True + else: + print_result('Server module loadable', False, 'Could not create module spec') + return False + except Exception as e: + print_result('Server module loadable', False, f'Import error: {e}') + return False + finally: + sys.path.pop(0) + + +def check_entrypoint_format() -> bool: + """Check that the entrypoint format is correct for FastMCP Cloud.""" + print_header('Check 6: Entrypoint Format') + + script_dir = Path(__file__).parent + mcp_server_dir = script_dir.parent + server_file = mcp_server_dir / 'src' / 'graphiti_mcp_server.py' + + if not server_file.exists(): + print_result('Server file exists', False) + return False + + content = server_file.read_text() + + # Check for module-level FastMCP instance + has_module_level_mcp = 'mcp = FastMCP(' in content + + # Check for if __name__ == "__main__" block (should exist but is ignored) + has_main_block = "if __name__ == '__main__':" in content or 'if __name__ == "__main__":' in content + + print_result( + 'Module-level mcp instance', + has_module_level_mcp, + 'Required for FastMCP Cloud discovery', + ) + + if has_main_block: + print_result( + '__main__ block present', + True, + 'Note: This is IGNORED by FastMCP Cloud', + ) + + # Print the expected entrypoint + if has_module_level_mcp: + print(f'\n Expected entrypoint for FastMCP Cloud:') + print(f' src/graphiti_mcp_server.py:mcp') + + return has_module_level_mcp + + +def main() -> None: + """Run all verification checks.""" + print('\n' + '=' * 60) + print(' FastMCP Cloud Deployment Readiness Check') + print(' Graphiti MCP Server') + print('=' * 60) + + checks = [ + ('Server Discoverability', check_server_discoverability), + ('Dependencies', check_dependencies), + ('Environment Documentation', check_env_documentation), + ('Secrets Safety', check_secrets_safety), + ('Server Import', check_server_import), + ('Entrypoint Format', check_entrypoint_format), + ] + + results = [] + for name, check_fn in checks: + try: + passed = check_fn() + results.append((name, passed)) + except Exception as e: + print(f'\nāŒ Error in {name}: {e}') + results.append((name, False)) + + # Summary + print_header('Summary') + passed_count = sum(1 for _, passed in results if passed) + total_count = len(results) + + for name, passed in results: + status = 'āœ…' if passed else 'āŒ' + print(f' {status} {name}') + + print(f'\n Total: {passed_count}/{total_count} checks passed') + + if passed_count == total_count: + print('\n' + '=' * 60) + print(' šŸš€ READY FOR FASTMCP CLOUD DEPLOYMENT!') + print('=' * 60) + print('\nNext steps:') + print(' 1. Push code to GitHub') + print(' 2. Visit https://fastmcp.cloud') + print(' 3. Create project with entrypoint: src/graphiti_mcp_server.py:mcp') + print(' 4. Set environment variables in FastMCP Cloud UI') + print(' 5. Deploy!') + print('\nSee docs/FASTMCP_CLOUD_DEPLOYMENT.md for detailed instructions.') + else: + print('\n' + '=' * 60) + print(' āš ļø NOT READY - Please fix the failing checks above') + print('=' * 60) + sys.exit(1) + + +if __name__ == '__main__': + main()