graphiti/mcp_server/scripts/verify_fastmcp_cloud_readiness.py
donbr 4ab47c097e feat(mcp_server): Add FastMCP Cloud deployment support
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 <noreply@anthropic.com>
2025-12-06 23:25:31 -08:00

350 lines
11 KiB
Python

#!/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()