diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e281c6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitignore +images/ +docs/ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.venv/ +venv/ +env/ +logs/ +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..93befc4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim AS builder + +WORKDIR /app + +# Install build dependencies (none needed for pure Python, but kept for compatibility) +RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* + +COPY pyproject.toml ./ +COPY src/ ./src/ + +RUN pip install --no-cache-dir . + +# ---- Runtime stage ---- +FROM python:3.12-slim + +WORKDIR /app + +# Create non-root user with home directory +RUN groupadd -r appgroup && useradd -r -g appgroup -m -d /home/appuser appuser + +# Copy installed packages and console script from builder +COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=builder /usr/local/bin/grok-search /usr/local/bin/grok-search + +# Default env for remote HTTP mode +ENV PYTHONUNBUFFERED=1 +ENV MCP_TRANSPORT=http +ENV MCP_HOST=0.0.0.0 +ENV MCP_PORT=8000 + +EXPOSE 8000 + +USER appuser + +CMD ["grok-search"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b5e6e64 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + grok-search: + build: + context: . + dockerfile: Dockerfile + container_name: grok-search + restart: unless-stopped + ports: + - "${MCP_PORT:-8000}:8000" + environment: + # Required + - GROK_API_URL=${GROK_API_URL} + - GROK_API_KEY=${GROK_API_KEY} + # Optional MCP transport settings + - MCP_TRANSPORT=${MCP_TRANSPORT:-http} + - MCP_HOST=0.0.0.0 + - MCP_PORT=8000 + - MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN:-} + # Optional Grok settings + - GROK_MODEL=${GROK_MODEL:-grok-4-fast} + - GROK_DEBUG=${GROK_DEBUG:-false} + - GROK_LOG_LEVEL=${GROK_LOG_LEVEL:-INFO} + # Optional search providers + - TAVILY_ENABLED=${TAVILY_ENABLED:-true} + - TAVILY_API_URL=${TAVILY_API_URL:-https://api.tavily.com} + - TAVILY_API_KEY=${TAVILY_API_KEY:-} + - FIRECRAWL_API_URL=${FIRECRAWL_API_URL:-https://api.firecrawl.dev/v2} + - FIRECRAWL_API_KEY=${FIRECRAWL_API_KEY:-} + # Uncomment if you want switch_model persistence across restarts + # volumes: + # - grok-search-config:/home/appuser/.config/grok-search + +# volumes: +# grok-search-config: diff --git a/src/grok_search/server.py b/src/grok_search/server.py index 7754216..afa3866 100644 --- a/src/grok_search/server.py +++ b/src/grok_search/server.py @@ -25,8 +25,26 @@ from .planning import engine as planning_engine, _split_csv import asyncio +import os -mcp = FastMCP("grok-search") +_mcp_auth_token = os.getenv("MCP_AUTH_TOKEN") +_auth = None +if _mcp_auth_token: + from fastmcp.server.auth.auth import TokenVerifier, AccessToken + + class _StaticBearerAuth(TokenVerifier): + def __init__(self, token: str): + self._token = token + super().__init__() + + async def verify_token(self, token: str) -> AccessToken | None: + if token == self._token: + return AccessToken(token=token, client_id="grok-search", scopes=[]) + return None + + _auth = _StaticBearerAuth(_mcp_auth_token) + +mcp = FastMCP("grok-search", auth=_auth) _SOURCES_CACHE = SourcesCache(max_size=256) _AVAILABLE_MODELS_CACHE: dict[tuple[str, str], list[str]] = {} @@ -844,6 +862,10 @@ def main(): import os import threading + transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower() + host = os.getenv("MCP_HOST", "127.0.0.1") + port = int(os.getenv("MCP_PORT", "8000")) + # 信号处理(仅主线程) if threading.current_thread() is threading.main_thread(): def handle_shutdown(signum, frame): @@ -880,7 +902,10 @@ def monitor_parent(): threading.Thread(target=monitor_parent, daemon=True).start() try: - mcp.run(transport="stdio", show_banner=False) + if transport in {"http", "streamable-http", "sse"}: + mcp.run(transport=transport, host=host, port=port, show_banner=False) + else: + mcp.run(transport="stdio", show_banner=False) except KeyboardInterrupt: pass finally: