From d71c0378e85ff703f9ed0c616ce79f222d907c6b Mon Sep 17 00:00:00 2001 From: Tapan Chugh Date: Thu, 10 Jul 2025 12:52:45 -0700 Subject: [PATCH 1/2] Add initial docker + smithery integration --- .dockerignore | 86 +++++++++++ Dockerfile | 27 ++++ docs/SMITHERY_DEPLOYMENT.md | 165 +++++++++++++++++++++ docs/TESTING_GUIDE.md | 189 +++++++++++++++++++++++++ pyproject.toml | 2 +- smithery.yaml | 36 +++++ src/pragweb/config.py | 62 +++++++- src/pragweb/google_api/auth.py | 62 +++++++- src/pragweb/secrets_manager.py | 16 ++- tests/integration/test_docker_build.py | 119 ++++++++++++++++ 10 files changed, 752 insertions(+), 12 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docs/SMITHERY_DEPLOYMENT.md create mode 100644 docs/TESTING_GUIDE.md create mode 100644 smithery.yaml create mode 100644 tests/integration/test_docker_build.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bc1769d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,86 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Testing +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +tests/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Project specific +*.log +*.db +*.sqlite +.mypy_cache/ +.dmypy.json +dmypy.json + +# Git +.git/ +.gitignore +.gitattributes + +# Documentation +docs/ +*.md +!README.md + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml + +# Development files +.pre-commit-config.yaml +Makefile +requirements-dev.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..85f23f2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Use Python 3.11 slim image +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY pyproject.toml ./ +COPY src/ ./src/ +COPY README.md ./ + +# Install the package in production mode +RUN pip install --no-cache-dir -e . + +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/app/src + +# The command will be provided by smithery.yaml +# Default command for local testing +CMD ["fastmcp", "run", "pragweb/mcp_server.py"] diff --git a/docs/SMITHERY_DEPLOYMENT.md b/docs/SMITHERY_DEPLOYMENT.md new file mode 100644 index 0000000..c8dc3c8 --- /dev/null +++ b/docs/SMITHERY_DEPLOYMENT.md @@ -0,0 +1,165 @@ +# Smithery Deployment Guide + +This guide explains how to deploy the Praga Core MCP server on Smithery. + +## Prerequisites + +1. A GitHub account with this repository +2. A Smithery account (sign up at https://smithery.ai) +3. An OpenAI API key +4. (Optional) Google OAuth credentials for Google API features + +## Deployment Steps + +### 1. Push to GitHub + +Ensure your repository includes the following files: +- `smithery.yaml` - Smithery configuration +- `Dockerfile` - Container build instructions +- `.dockerignore` - Optimize Docker builds + +```bash +git add smithery.yaml Dockerfile .dockerignore +git commit -m "Add Smithery deployment configuration" +git push origin main +``` + +### 2. Connect to Smithery + +1. Log in to [Smithery](https://smithery.ai) +2. Click "Deploy a new server" or "Add Server" +3. Connect your GitHub account if not already connected +4. Select this repository from the list + +### 3. Configure Deployment + +1. Smithery will detect the `smithery.yaml` configuration +2. Review the deployment settings +3. Click "Deploy" to start the deployment process + +### 4. Configure Environment + +After deployment, users will need to provide: +- **OPENAI_API_KEY** (required): Your OpenAI API key for LLM interactions +- **GOOGLE_OAUTH_CLIENT_ID** (optional): For Google API features +- **GOOGLE_OAUTH_CLIENT_SECRET** (optional): For Google API features +- **GOOGLE_OAUTH_REFRESH_TOKEN** (optional): For automated Google API authentication + +### Obtaining Google OAuth Credentials + +#### 1. Create OAuth Client ID +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select an existing one +3. Enable the required APIs (Gmail, Calendar, Drive, Docs, People) +4. Go to "APIs & Services" > "Credentials" +5. Click "Create Credentials" > "OAuth client ID" +6. Choose "Desktop app" as the application type +7. Save the client ID and client secret + +#### 2. Obtain Refresh Token +To get a refresh token, you'll need to run the OAuth flow once locally: + +```python +# save this as get_refresh_token.py +from google_auth_oauthlib.flow import InstalledAppFlow + +SCOPES = [ + "https://www.googleapis.com/auth/gmail.readonly", + "https://www.googleapis.com/auth/gmail.compose", + "https://www.googleapis.com/auth/calendar.readonly", + "https://www.googleapis.com/auth/contacts.readonly", + "https://www.googleapis.com/auth/directory.readonly", + "https://www.googleapis.com/auth/documents.readonly", + "https://www.googleapis.com/auth/drive.readonly", +] + +# Create flow with your client ID and secret +flow = InstalledAppFlow.from_client_config( + { + "installed": { + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + } + }, + scopes=SCOPES +) + +# Run the flow +creds = flow.run_local_server(port=0) + +print(f"Refresh Token: {creds.refresh_token}") +``` + +Run this script, authorize in your browser, and copy the refresh token. + +## Using Your Deployed Server + +Once deployed, your MCP server will be available through: +- Smithery's web interface +- Claude Desktop (with proper MCP configuration) +- Any MCP-compatible client + +### Claude Desktop Configuration + +To use with Claude Desktop, add to your Claude configuration: + +```json +{ + "mcpServers": { + "praga-core": { + "url": "https://smithery.ai/api/mcp/your-server-id", + "apiKey": "your-smithery-api-key" + } + } +} +``` + +## Features Available + +Your deployed MCP server provides: +- Document retrieval using LLMRP protocol +- Google API integrations (Calendar, Gmail, Docs, People) +- Reactive agent for intelligent document search +- Action execution framework +- Page caching system + +## Troubleshooting + +### Build Failures +- Check the Smithery deployment logs +- Ensure all dependencies in `pyproject.toml` are installable +- Verify the Python version (3.11+) + +### Runtime Errors +- Verify all required environment variables are set +- Check the server logs in Smithery dashboard +- Ensure API keys have proper permissions + +### Local Testing + +To test locally before deployment: + +```bash +# Build the Docker image +docker build -t praga-mcp-local . + +# Run with environment variables +docker run -it \ + -e OPENAI_API_KEY="your-key" \ + -e PYTHONUNBUFFERED=1 \ + praga-mcp-local +``` + +## Security Notes + +- Never commit API keys or secrets to the repository +- Use Smithery's secure environment variable management +- Tokens are passed ephemerally and not stored long-term by Smithery + +## Support + +For issues specific to: +- Praga Core: Open an issue in this repository +- Smithery platform: Contact Smithery support or check their documentation at https://smithery.ai/docs \ No newline at end of file diff --git a/docs/TESTING_GUIDE.md b/docs/TESTING_GUIDE.md new file mode 100644 index 0000000..a80e86b --- /dev/null +++ b/docs/TESTING_GUIDE.md @@ -0,0 +1,189 @@ +# Testing Guide for Smithery Deployment + +This guide helps you test your Praga MCP server setup before deploying to Smithery. + +## Prerequisites + +- Python 3.11+ +- Docker (for container testing) +- Google Cloud account (for OAuth setup) +- OpenAI API key + +## Quick Test + +Run the automated test script: + +```bash +python test_smithery_setup.py +``` + +This script will: +- Verify all required files exist +- Validate smithery.yaml configuration +- Check Docker availability +- Test environment variable setup +- Create test configuration files + +## Testing Steps + +### 1. Environment Variable Testing + +#### Option A: Using .env file +```bash +# Copy the test template +cp .env.test .env + +# Edit .env with your actual credentials +# Required: OPENAI_API_KEY +# Optional: GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, GOOGLE_OAUTH_REFRESH_TOKEN + +# Test the server +python -m pragweb.mcp_server +``` + +#### Option B: Export variables directly +```bash +export OPENAI_API_KEY='sk-your-openai-key' +export GOOGLE_OAUTH_CLIENT_ID='your-client-id.apps.googleusercontent.com' +export GOOGLE_OAUTH_CLIENT_SECRET='your-client-secret' +export GOOGLE_OAUTH_REFRESH_TOKEN='your-refresh-token' + +python -m pragweb.mcp_server +``` + +### 2. Obtaining Google OAuth Credentials + +#### Step 1: Create OAuth Client +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select existing +3. Enable APIs: + - Gmail API + - Google Calendar API + - Google Drive API + - Google Docs API + - People API +4. Go to "APIs & Services" > "Credentials" +5. Click "Create Credentials" > "OAuth client ID" +6. Choose "Desktop app" as the application type +7. Save the client ID and secret + +#### Step 2: Get Refresh Token +```bash +# Run the helper script +python scripts/get_google_refresh_token.py + +# Or provide credentials as arguments +python scripts/get_google_refresh_token.py "YOUR_CLIENT_ID" "YOUR_CLIENT_SECRET" +``` + +The script will: +- Open a browser for authorization +- Request access to required Google services +- Display your refresh token +- Optionally save credentials to .env.google + +### 3. Docker Testing + +#### Build the image +```bash +docker build -t praga-mcp-test . +``` + +#### Run with minimal config (OpenAI only) +```bash +docker run -it \ + -e OPENAI_API_KEY='sk-your-key' \ + -e PYTHONUNBUFFERED=1 \ + praga-mcp-test +``` + +#### Run with full Google integration +```bash +docker run -it \ + -e OPENAI_API_KEY='sk-your-key' \ + -e GOOGLE_OAUTH_CLIENT_ID='your-client-id' \ + -e GOOGLE_OAUTH_CLIENT_SECRET='your-client-secret' \ + -e GOOGLE_OAUTH_REFRESH_TOKEN='your-refresh-token' \ + -e PYTHONUNBUFFERED=1 \ + praga-mcp-test +``` + +### 4. Testing MCP Protocol + +Once the server is running, you can test it with an MCP client: + +#### Using fastmcp CLI (if installed) +```bash +# List available tools +fastmcp tools list + +# Test search functionality +fastmcp tools call search_pages '{"instruction": "find recent emails"}' + +# Test get_pages +fastmcp tools call get_pages '{"page_uris": ["gmail:email:123"]}' +``` + +## Troubleshooting + +### Common Issues + +1. **"OPENAI_API_KEY environment variable is required"** + - Set the OPENAI_API_KEY environment variable + - Check your .env file is in the correct location + +2. **"Docker daemon not running"** + - Start Docker Desktop or Docker service + - Verify with: `docker --version` + +3. **"Failed to refresh token from environment variables"** + - Verify your Google OAuth credentials are correct + - Check the refresh token hasn't expired + - Try generating a new refresh token + +4. **"Failed to import MCP server"** + - Install dependencies: `pip install -e .` + - Ensure you're in the project root directory + - Check Python version is 3.11+ + +### Authentication Priority + +The server tries authentication in this order: +1. Environment variables (GOOGLE_OAUTH_*) +2. Stored credentials in secrets database +3. Interactive OAuth flow (local only) + +For Smithery deployment, use option 1 (environment variables). + +## Verification Checklist + +Before deploying to Smithery: + +- [ ] `smithery.yaml` exists and is valid +- [ ] `Dockerfile` builds successfully +- [ ] `.dockerignore` excludes unnecessary files +- [ ] Google auth supports environment variables +- [ ] MCP server starts without errors +- [ ] OpenAI API key is available +- [ ] (Optional) Google OAuth credentials obtained +- [ ] (Optional) Docker image tested locally + +## Next Steps + +Once testing is complete: + +1. Commit all changes: + ```bash + git add smithery.yaml Dockerfile .dockerignore + git add src/pragweb/google_api/auth.py + git commit -m "Add Smithery deployment configuration with OAuth support" + git push origin main + ``` + +2. Deploy on Smithery: + - Go to https://smithery.ai + - Connect your GitHub repository + - Configure environment variables + - Deploy! + +For detailed deployment instructions, see [SMITHERY_DEPLOYMENT.md](SMITHERY_DEPLOYMENT.md). \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8eb8135..ceff7e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ line_length = 88 [tool.mypy] python_version = "3.11" mypy_path = "src" -exclude = "^examples/google_api|tests" +exclude = "^examples/google_api|tests|scripts" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true diff --git a/smithery.yaml b/smithery.yaml new file mode 100644 index 0000000..269597b --- /dev/null +++ b/smithery.yaml @@ -0,0 +1,36 @@ +# Smithery configuration file: https://smithery.ai/docs/deployments + +build: + dockerBuildPath: . + +startCommand: + type: stdio + configSchema: + type: object + required: + - OPENAI_API_KEY + properties: + OPENAI_API_KEY: + type: string + description: "OpenAI API key for LLM interactions" + GOOGLE_OAUTH_CLIENT_ID: + type: string + description: "Google OAuth Client ID for Google API authentication" + GOOGLE_OAUTH_CLIENT_SECRET: + type: string + description: "Google OAuth Client Secret for Google API authentication" + GOOGLE_OAUTH_REFRESH_TOKEN: + type: string + description: "Google OAuth refresh token for automated authentication" + commandFunction: | + (config) => ({ + command: 'python', + args: ['-m', 'pragweb.mcp_server'], + env: { + OPENAI_API_KEY: config.OPENAI_API_KEY, + GOOGLE_OAUTH_CLIENT_ID: config.GOOGLE_OAUTH_CLIENT_ID || '', + GOOGLE_OAUTH_CLIENT_SECRET: config.GOOGLE_OAUTH_CLIENT_SECRET || '', + GOOGLE_OAUTH_REFRESH_TOKEN: config.GOOGLE_OAUTH_REFRESH_TOKEN || '', + PYTHONUNBUFFERED: '1' + } + }) \ No newline at end of file diff --git a/src/pragweb/config.py b/src/pragweb/config.py index 0f08cb9..3fc94d7 100644 --- a/src/pragweb/config.py +++ b/src/pragweb/config.py @@ -4,12 +4,15 @@ It uses dotenv to load from .env files and provides a centralized config object. """ +import logging import os from typing import Optional from dotenv import load_dotenv from pydantic import BaseModel, Field, field_validator +logger = logging.getLogger(__name__) + class AppConfig(BaseModel): """Application configuration loaded from environment variables.""" @@ -72,17 +75,70 @@ def validate_max_iterations(cls, v: int) -> int: return v +def detect_environment() -> str: + """Detect the current runtime environment. + + Returns: + 'smithery': Running in Smithery cloud environment + 'ci': Running in CI/testing environment + 'user': Running in user/development environment + """ + # Check for Smithery environment + if os.getenv("SMITHERY") or os.getenv("SMITHERY_DEPLOYMENT"): + return "smithery" + + # Check for CI/testing environment + if os.getenv("CI") or os.getenv("PYTEST_CURRENT_TEST"): + return "ci" + + # Default to user environment + return "user" + + +def get_database_urls(environment: str) -> tuple[str, str]: + """Get appropriate database URLs based on environment. + + Args: + environment: The detected environment type + + Returns: + Tuple of (page_cache_url, secrets_database_url) + """ + # Check for explicit environment variable overrides first + explicit_page_cache = os.getenv("PAGE_CACHE_URL") + explicit_secrets = os.getenv("SECRETS_DATABASE_URL") + + if explicit_page_cache and explicit_secrets: + logger.info(f"Using explicit database URLs for {environment} environment") + return (explicit_page_cache, explicit_secrets) + + # Use environment-specific defaults + if environment in ("smithery", "ci"): + # Use in-memory databases for ephemeral environments + in_memory_url = "sqlite+aiosqlite:///:memory:" + logger.info(f"Using in-memory databases for {environment} environment") + return (in_memory_url, in_memory_url) + else: + # Use persistent databases for user environments + page_cache_url = explicit_page_cache or "sqlite+aiosqlite:///praga_cache.db" + secrets_url = explicit_secrets or page_cache_url + logger.info(f"Using persistent databases for {environment} environment") + return (page_cache_url, secrets_url) + + def load_default_config() -> AppConfig: """Load configuration from environment variables with defaults.""" # Load environment variables from .env file load_dotenv() - # Create config from environment variables with defaults - page_cache_url = os.getenv("PAGE_CACHE_URL", "sqlite+aiosqlite:///praga_cache.db") + # Detect environment and get appropriate database URLs + environment = detect_environment() + page_cache_url, secrets_database_url = get_database_urls(environment) + return AppConfig( server_root=os.getenv("SERVER_ROOT", "google"), page_cache_url=page_cache_url, - secrets_database_url=os.getenv("SECRETS_DATABASE_URL", page_cache_url), + secrets_database_url=secrets_database_url, retriever_agent_model=os.getenv("RETRIEVER_AGENT_MODEL", "gpt-4o-mini"), retriever_max_iterations=int(os.getenv("RETRIEVER_MAX_ITERATIONS", "10")), openai_api_key=os.getenv("OPENAI_API_KEY", ""), diff --git a/src/pragweb/google_api/auth.py b/src/pragweb/google_api/auth.py index 82211ba..67466ad 100644 --- a/src/pragweb/google_api/auth.py +++ b/src/pragweb/google_api/auth.py @@ -1,6 +1,7 @@ """Google API authentication using singleton pattern.""" import logging +import os import threading from datetime import timezone from typing import Any, Optional @@ -52,6 +53,35 @@ def _get_credentials_path(self) -> str: """Get path to credentials file.""" return get_current_config().google_credentials_file + def _create_credentials_from_env(self) -> Optional[Credentials]: + """Create credentials from environment variables if available.""" + client_id = os.environ.get("GOOGLE_OAUTH_CLIENT_ID") + client_secret = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET") + refresh_token = os.environ.get("GOOGLE_OAUTH_REFRESH_TOKEN") + + if not all([client_id, client_secret, refresh_token]): + return None + + logger.info("Creating Google credentials from environment variables") + + # Create credentials object with refresh token + creds = Credentials( # type: ignore[no-untyped-call] + token=None, # Will be populated on first refresh + refresh_token=refresh_token, + token_uri="https://oauth2.googleapis.com/token", + client_id=client_id, + client_secret=client_secret, + scopes=_SCOPES, + ) + + # Refresh to get an access token + try: + creds.refresh(Request()) # type: ignore[no-untyped-call] + return creds + except Exception as e: + logger.error(f"Failed to refresh token from environment variables: {e}") + return None + def _scopes_match( self, stored_scopes: list[str], required_scopes: list[str] ) -> bool: @@ -140,6 +170,14 @@ def _store_credentials( def _authenticate(self) -> None: """Authenticate with Google APIs.""" + # First try to create credentials from environment variables + self._creds = self._create_credentials_from_env() + + if self._creds and self._creds.valid: + logger.info("Successfully authenticated using environment variables") + return + + # Fall back to secrets manager config = get_current_config() secrets_manager = get_secrets_manager(config.secrets_database_url) @@ -149,13 +187,23 @@ def _authenticate(self) -> None: self._creds.refresh(Request()) # type: ignore[no-untyped-call] self._store_credentials(self._creds, secrets_manager) else: - credentials_path = self._get_credentials_path() - flow = InstalledAppFlow.from_client_secrets_file( - credentials_path, _SCOPES - ) - self._creds = flow.run_local_server(port=0) - # Save new credentials - self._store_credentials(self._creds, secrets_manager) + # Fall back to file-based OAuth flow + try: + credentials_path = self._get_credentials_path() + flow = InstalledAppFlow.from_client_secrets_file( + credentials_path, _SCOPES + ) + self._creds = flow.run_local_server(port=0) + # Save new credentials + self._store_credentials(self._creds, secrets_manager) + except Exception as e: + logger.error(f"Failed to authenticate with Google APIs: {e}") + raise RuntimeError( + "Unable to authenticate with Google APIs. " + "Please provide either environment variables " + "(GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, GOOGLE_OAUTH_REFRESH_TOKEN) " + "or a valid credentials file." + ) def get_gmail_service(self) -> Any: """Get Gmail service (cached).""" diff --git a/src/pragweb/secrets_manager.py b/src/pragweb/secrets_manager.py index f0830c4..25114eb 100644 --- a/src/pragweb/secrets_manager.py +++ b/src/pragweb/secrets_manager.py @@ -65,11 +65,25 @@ def __init__(self, database_url: str) -> None: """Initialize SecretsManager with database connection.""" # Configure engine based on database type - engine_args = {} + engine_args: Dict[str, Any] = {} if database_url.startswith("postgresql"): from sqlalchemy.pool import NullPool engine_args["poolclass"] = NullPool + elif database_url.startswith("sqlite"): + # SQLite-specific configuration + engine_args["pool_pre_ping"] = True + if "memory" in database_url: + # In-memory SQLite needs special handling + from sqlalchemy.pool import StaticPool + + engine_args["poolclass"] = StaticPool + engine_args["connect_args"] = {"check_same_thread": False} + logger.info( + "SecretsManager: Using in-memory SQLite database (ephemeral)" + ) + else: + logger.info("SecretsManager: Using persistent SQLite database") self._engine = create_engine(database_url, **engine_args) self._session_factory = sessionmaker(bind=self._engine) diff --git a/tests/integration/test_docker_build.py b/tests/integration/test_docker_build.py new file mode 100644 index 0000000..b6e1342 --- /dev/null +++ b/tests/integration/test_docker_build.py @@ -0,0 +1,119 @@ +"""Integration test for Docker build functionality.""" + +import os +import subprocess +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="module") +def docker_available(): + """Check if Docker is available and skip tests if not.""" + try: + result = subprocess.run( + ["docker", "--version"], capture_output=True, text=True, timeout=10 + ) + if result.returncode != 0: + pytest.skip("Docker not available") + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + pytest.skip("Docker not installed or not responding") + + +@pytest.mark.skipif( + os.environ.get("CI") == "true" and os.environ.get("DOCKER_AVAILABLE") != "true", + reason="Docker not available in CI environment", +) +class TestDockerBuild: + """Test Docker build functionality.""" + + def test_dockerfile_exists(self): + """Test that Dockerfile exists in the repository root.""" + dockerfile_path = Path("Dockerfile") + assert dockerfile_path.exists(), "Dockerfile not found in repository root" + + def test_dockerignore_exists(self): + """Test that .dockerignore exists in the repository root.""" + dockerignore_path = Path(".dockerignore") + assert dockerignore_path.exists(), ".dockerignore not found in repository root" + + def test_docker_build_succeeds(self, docker_available): + """Test that Docker build succeeds without errors.""" + # Run Docker build + try: + result = subprocess.run( + ["docker", "build", "-t", "praga-mcp-test", "."], + capture_output=True, + text=True, + timeout=300, # 5 minutes timeout for build + cwd=Path.cwd(), + ) + + assert result.returncode == 0, ( + f"Docker build failed with return code {result.returncode}.\n" + f"STDOUT: {result.stdout}\n" + f"STDERR: {result.stderr}" + ) + + # Check that the image was created + result = subprocess.run( + [ + "docker", + "images", + "--format", + "table {{.Repository}}:{{.Tag}}", + "praga-mcp-test", + ], + capture_output=True, + text=True, + timeout=30, + ) + + assert ( + "praga-mcp-test:latest" in result.stdout + ), "Docker image was not created" + + except subprocess.TimeoutExpired: + pytest.fail("Docker build timed out after 5 minutes") + finally: + # Clean up: remove the test image if it exists + try: + subprocess.run( + ["docker", "rmi", "praga-mcp-test"], + capture_output=True, + text=True, + timeout=30, + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + # Ignore cleanup errors + pass + + def test_smithery_yaml_docker_compatibility(self): + """Test that smithery.yaml is compatible with Docker deployment.""" + smithery_yaml_path = Path("smithery.yaml") + + if not smithery_yaml_path.exists(): + pytest.skip("smithery.yaml not found") + + try: + import yaml + + with open(smithery_yaml_path, "r") as f: + config = yaml.safe_load(f) + + # Check that build section exists + assert "build" in config, "Missing 'build' section in smithery.yaml" + + # Check that startCommand is configured for containerized deployment + assert ( + "startCommand" in config + ), "Missing 'startCommand' section in smithery.yaml" + + start_cmd = config["startCommand"] + assert ( + start_cmd.get("type") == "stdio" + ), f"Expected startCommand.type to be 'stdio', got '{start_cmd.get('type')}'" + + except ImportError: + pytest.skip("PyYAML not available for smithery.yaml validation") From 1f84fef740fd93f221a0f69cc157e8646050d256 Mon Sep 17 00:00:00 2001 From: Tapan Chugh Date: Thu, 10 Jul 2025 13:14:28 -0700 Subject: [PATCH 2/2] Trying to fix smithery deployment --- Dockerfile | 6 +++++- smithery.yaml | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 85f23f2..ca3c98f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,11 @@ RUN pip install --no-cache-dir -e . # Set environment variables ENV PYTHONUNBUFFERED=1 ENV PYTHONPATH=/app/src +ENV PORT=8080 + +# Expose port for HTTP server +EXPOSE $PORT # The command will be provided by smithery.yaml # Default command for local testing -CMD ["fastmcp", "run", "pragweb/mcp_server.py"] +CMD ["sh", "-c", "fastmcp -t streamable-http --host 0.0.0.0 --port $PORT pragweb/mcp_server.py"] diff --git a/smithery.yaml b/smithery.yaml index 269597b..b805fbd 100644 --- a/smithery.yaml +++ b/smithery.yaml @@ -24,13 +24,14 @@ startCommand: description: "Google OAuth refresh token for automated authentication" commandFunction: | (config) => ({ - command: 'python', - args: ['-m', 'pragweb.mcp_server'], + command: 'sh', + args: ['-c', `fastmcp -t streamable-http --host 0.0.0.0 --port \${PORT:-8080} pragweb/mcp_server.py`], env: { OPENAI_API_KEY: config.OPENAI_API_KEY, GOOGLE_OAUTH_CLIENT_ID: config.GOOGLE_OAUTH_CLIENT_ID || '', GOOGLE_OAUTH_CLIENT_SECRET: config.GOOGLE_OAUTH_CLIENT_SECRET || '', GOOGLE_OAUTH_REFRESH_TOKEN: config.GOOGLE_OAUTH_REFRESH_TOKEN || '', - PYTHONUNBUFFERED: '1' + PYTHONUNBUFFERED: '1', + PORT: process.env.PORT || '8080' } }) \ No newline at end of file