Skip to content

Commit da541b4

Browse files
committed
feat(docker): add multi-stage secure Alpine image for stdio MCP
closes #4 CHANGES - Add optimized Dockerfile with uv-based Alpine multi-stage build - Implement .dockerignore for lean image builds - Configure non-root user (appuser) for container security - Set up stdio-specific environment variables and transport - Update README.md with Docker usage and stdio transport focus - Rename project to py_dep_man_companion in pyproject.toml - Prepare workflows for Docker Hub publish automation IMPACT - Enables containerized deployment of MCP server - Reduces image size through multi-stage Alpine build - Enhances security with non-root execution - Optimizes for stdio transport pattern (no HTTP endpoints) TECHNICAL NOTES - Uses ghcr.io/astral-sh/uv:python3.12-alpine base images - Includes Rust toolchain for tantivy dependency compilation - Implements build caching for faster rebuilds - Stdio containers are ephemeral and client-initiated
1 parent fcca940 commit da541b4

File tree

12 files changed

+317
-64
lines changed

12 files changed

+317
-64
lines changed

.dockerignore

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Git
2+
.git
3+
.gitignore
4+
5+
# Python
6+
__pycache__
7+
*.pyc
8+
*.pyo
9+
*.pyd
10+
.Python
11+
*.so
12+
.pytest_cache
13+
.coverage
14+
htmlcov
15+
.tox
16+
.cache
17+
nosetests.xml
18+
coverage.xml
19+
*.cover
20+
*.log
21+
.mypy_cache
22+
.dmypy.json
23+
dmypy.json
24+
25+
# Virtual environments
26+
venv/
27+
.venv/
28+
ENV/
29+
env/
30+
.env
31+
32+
# IDEs
33+
.vscode
34+
.idea
35+
*.swp
36+
*.swo
37+
*~
38+
39+
# OS
40+
.DS_Store
41+
Thumbs.db
42+
43+
# Documentation
44+
*.md
45+
docs/
46+
47+
# Development files
48+
.editorconfig
49+
.pre-commit-config.yaml
50+
51+
# Build artifacts
52+
build/
53+
dist/
54+
*.egg-info/
55+
56+
# Lock files are now needed for uv sync --frozen
57+
# uv.lock
58+
59+
# Docker
60+
.dockerignore
61+
docker-compose*.yml

.github/workflows/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,22 @@ graph LR
1212
ORCHESTRATOR["🎯 Orchestrator<br/>auto-update.yml"]
1313
UPDATE_DOCS["📚 Documentation Sync<br/>auto-update-docs.yml"]
1414
UPDATE_INDEX["🔍 Search Index<br/>auto-update-index.yml"]
15+
PUBLISH["🐳 Publish Image<br/>auto-update-publish.yml"]
1516
1617
CRON_TRIGGER --> ORCHESTRATOR
1718
ORCHESTRATOR --> UPDATE_DOCS
1819
UPDATE_DOCS --> UPDATE_INDEX
20+
UPDATE_INDEX --> PUBLISH
1921
```
2022

21-
**Security**: Pinned action hashes, signed commits, sequential execution
23+
**Security**: Pinned 3rd-party actions, signed commits, modular execution
2224

2325
## 🚀 Planned Extensions
2426

25-
- **Workflows**: `auto-build-and-deploy.yml` (Docker)
2627
- **Managers**: pipenv, pdm, pixi
2728
- **Features**: Conditional updates, performance monitoring
2829

2930
## 🔧 Operations
3031

31-
- **Testing**: `workflow_dispatch` on all workflows
32+
- **Testing**: `workflow_dispatch` on `update-docs`, `update-index`, and `publish`
3233
- **Monitoring**: Check Tuesday runs for upstream changes

.github/workflows/auto-update-docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ concurrency:
1111
jobs:
1212
update-docs:
1313
name: Update ${{ matrix.manager }} docs
14+
if: github.repository == 'KemingHe/python-dependency-manager-companion-mcp-server'
1415
runs-on: ubuntu-latest
1516
permissions:
1617
contents: write

.github/workflows/auto-update-index.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
jobs:
88
update-index:
99
name: Update Search Index
10+
if: github.repository == 'KemingHe/python-dependency-manager-companion-mcp-server'
1011
runs-on: ubuntu-latest
1112
permissions:
1213
contents: write
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Publish Image to Docker Hub
2+
3+
on:
4+
workflow_call: {}
5+
workflow_dispatch: {} # Manual trigger for testing
6+
7+
env:
8+
# Docker requires lowercase names for both images and cache references
9+
IMAGE_NAME: py-dep-man-companion
10+
REPO_NAME: ${{ vars.DOCKERHUB_USERNAME }}/py-dep-man-companion
11+
12+
concurrency: ${{ github.workflow }}-${{ github.ref }}
13+
14+
jobs:
15+
build-and-push-image:
16+
if: github.repository == 'KemingHe/python-dependency-manager-companion-mcp-server'
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
attestations: write
21+
id-token: write
22+
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@v4
26+
with:
27+
ref: ${{ github.ref }}
28+
29+
- name: Log in to Docker Hub
30+
# Pinned 3rd party action to commit hash of release v3.3.0 on 08/15/2024 to prevent supply chain attacks
31+
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
32+
with:
33+
username: ${{ vars.DOCKERHUB_USERNAME }}
34+
password: ${{ secrets.DOCKERHUB_PAT }}
35+
36+
- name: Extract metadata (tags, labels) for Docker
37+
id: meta
38+
# Pinned 3rd party action to commit hash of release v5.5.1 on 01/23/2024 to prevent supply chain attacks
39+
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81
40+
with:
41+
images: ${{ env.REPO_NAME }}
42+
tags: |
43+
type=ref,event=branch
44+
type=ref,event=pr
45+
type=schedule,pattern=weekly-{{date 'YYYYMMDD'}}
46+
type=raw,value=latest,enable={{is_default_branch}}
47+
48+
# Add support for more platforms, i.e. linux/arm64 (macOS M1/M2) with QEMU
49+
- name: Set up QEMU
50+
# Pinned 3rd party action to commit hash of release v3.2.0 on 08/15/2024 to prevent supply chain attacks
51+
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf
52+
53+
# Enable advanced build features like cache export
54+
- name: Set up Docker Buildx
55+
# Pinned 3rd party action to commit hash of release v3.6.1 on 08/15/2024 to prevent supply chain attacks
56+
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db
57+
58+
- name: Build and push Docker image
59+
id: push
60+
# Pinned 3rd party action to commit hash of release v6.7.0 on 08/15/2024 to prevent supply chain attacks
61+
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755
62+
with:
63+
context: .
64+
push: true
65+
platforms: linux/amd64,linux/arm64
66+
tags: ${{ steps.meta.outputs.tags }}
67+
labels: ${{ steps.meta.outputs.labels }}
68+
cache-from: type=gha
69+
cache-to: type=gha,mode=max
70+
build-args: |
71+
BUILD_DATE=${{ github.event.head_commit.timestamp }}
72+
VCS_REF=${{ github.sha }}
73+
74+
- name: Generate artifact attestation
75+
uses: actions/attest-build-provenance@v2
76+
with:
77+
subject-name: index.docker.io/${{ env.REPO_NAME }}
78+
subject-digest: ${{ steps.push.outputs.digest }}
79+
push-to-registry: true

.github/workflows/auto-update.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ jobs:
2020
contents: write
2121
uses: ./.github/workflows/auto-update-index.yml
2222

23-
# build-deploy:
24-
# name: Build and Deploy
23+
# publish:
24+
# name: Publish Image
2525
# needs: update-index
26-
# uses: ./.github/workflows/auto-build-and-deploy.yml
26+
# permissions:
27+
# contents: read
28+
# attestations: write
29+
# id-token: write
30+
# uses: ./.github/workflows/auto-update-publish.yml

Dockerfile

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Optimized uv-based Alpine Docker image for MCP stdio transport
2+
# - Uses uv pre-built images for faster dependency management
3+
# - Multi-stage build for minimal final image size
4+
# - Non-root user for security
5+
# - Includes Rust support for tantivy dependency
6+
# - Configured for stdio transport (no HTTP endpoints)
7+
8+
# ==============================================================================
9+
# Stage 1: Dependencies builder
10+
# ==============================================================================
11+
FROM ghcr.io/astral-sh/uv:python3.12-alpine AS builder
12+
13+
# Set environment variables for uv
14+
ENV UV_COMPILE_BYTECODE=1 \
15+
UV_LINK_MODE=copy \
16+
UV_PROJECT_ENVIRONMENT=/app/.venv
17+
18+
# Install system dependencies required for building Python packages
19+
RUN apk add --no-cache \
20+
build-base \
21+
libffi-dev \
22+
rust \
23+
cargo
24+
25+
# Set working directory
26+
WORKDIR /app
27+
28+
# Copy dependency specification files
29+
COPY pyproject.toml ./
30+
COPY uv.lock ./
31+
32+
# Create virtual environment and install dependencies
33+
RUN --mount=type=cache,target=/root/.cache/uv \
34+
uv sync --frozen --no-install-project --no-dev
35+
36+
# Copy application code and install project
37+
COPY . .
38+
RUN --mount=type=cache,target=/root/.cache/uv \
39+
uv sync --frozen --no-dev
40+
41+
# ==============================================================================
42+
# Stage 2: Runtime
43+
# ==============================================================================
44+
FROM ghcr.io/astral-sh/uv:python3.12-alpine AS runtime
45+
46+
# Set environment variables for Python and stdio mode
47+
ENV PYTHONUNBUFFERED=1 \
48+
PYTHONDONTWRITEBYTECODE=1 \
49+
UV_PROJECT_ENVIRONMENT=/app/.venv \
50+
PYTHONPATH="/app" \
51+
TMPDIR="/tmp/app"
52+
53+
# Install runtime dependencies for Rust extensions
54+
RUN apk add --no-cache libgcc
55+
56+
# Create non-root user and directories
57+
RUN addgroup -g 1000 appuser \
58+
&& adduser -u 1000 -G appuser -D appuser \
59+
&& mkdir -p /tmp/app \
60+
&& chown -R appuser:appuser /tmp/app
61+
62+
# Copy application and virtual environment from builder
63+
COPY --from=builder --chown=appuser:appuser /app /app
64+
65+
# Switch to non-root user
66+
USER appuser
67+
68+
# Set working directory
69+
WORKDIR /app
70+
71+
# NOTE: No healthcheck for stdio MCP containers
72+
# Stdio containers are ephemeral and client-initiated - they start on-demand when
73+
# MCP clients connect, process requests via stdin/stdout, then exit when disconnected.
74+
# Traditional health checks don't apply to this usage pattern.
75+
76+
# Start MCP server in stdio mode (default)
77+
CMD ["uv", "run", "--with", "fastmcp>=2.10.5", "--with", "tantivy>=0.24.0", "fastmcp", "run", "src/mcp_server.py"]

README.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Updated on 2025-07-15 by @KemingHe
44
5-
Local MCP server providing unified search across Python dependency manager documentation.
5+
Local stdio MCP server providing unified search across Python dependency managers' latest and official documentation.
66

77
## 📋 Overview
88

@@ -18,7 +18,7 @@ docker pull keminghe/py-dep-man-companion:latest
1818

1919
### Step 2: Configure Your IDE
2020

21-
Add to VSCode User Settings (JSON):
21+
Add to VSCode/Cursor `mcp.json`:
2222

2323
```json
2424
{
@@ -35,31 +35,35 @@ Add to VSCode User Settings (JSON):
3535

3636
### Step 3: Start Searching
3737

38-
Query latest and unified documentation across all supported Python dependency managers directly from your AI assistant.
38+
Query latest and unified documentation across all supported Python dependency managers directly within your agentic chat.
3939

4040
## 📁 Project Structure
4141

4242
```plaintext
4343
python-dep-manager-companion-mcp-server/
44-
├── .github/workflows/ # Automation workflows (see workflows/README.md)
45-
├── docs/ # Project documentation
44+
├── .github/workflows/ # Automation workflows
45+
│ ├── auto-update-docs.yml # Weekly docs update
46+
│ ├── auto-update-index.yml # Search index rebuild
47+
│ ├── auto-update-publish.yml # Multi-arch Docker publish
48+
│ ├── auto-update.yml # Combined automation
49+
│ └── README.md # Workflow documentation
4650
├── src/
47-
│ ├── server.py # FastMCP server implementation
48-
│ ├── index.py # Tantivy search engine
49-
│ └── assets/ # Auto-updated documentation files
51+
│ ├── assets/ # Documentation source files
52+
│ │ ├── conda/ # conda docs
53+
│ │ ├── pip/ # pip docs
54+
│ │ ├── poetry/ # poetry docs
55+
│ │ └── uv/ # uv docs
56+
│ ├── index/ # Pre-built search index
57+
│ ├── build_index.py # Tantivy index builder
58+
│ └── mcp_server.py # FastMCP stdio server
5059
├── Dockerfile # Container build configuration
51-
└── pyproject.toml # Project dependencies and metadata
60+
├── pyproject.toml # Project dependencies and metadata
61+
└── uv.lock # Locked dependencies
5262
```
5363

5464
## 🛠️ Development
5565

56-
**Transport Support**: Stdio (default) and HTTP modes following MCP standards.
57-
58-
**Environment Variables**:
59-
60-
- `TRANSPORT_MODE`: `stdio` or `http` (default: `stdio`)
61-
- `TRANSPORT_PORT`: HTTP server port (default: `8080`)
62-
- `TRANSPORT_HOST`: Host binding (default: `127.0.0.1`)
66+
**Transport**: Stdio only (MCP standard for local tools).
6367

6468
**Local Development**:
6569

@@ -69,8 +73,11 @@ git clone <repo-url>
6973
cd python-dep-manager-companion-mcp-server
7074
uv sync
7175

72-
# Run server
73-
uv run python src/server.py stdio
76+
# Run server locally
77+
uv run --with fastmcp --with tantivy fastmcp run src/mcp_server.py
78+
79+
# Build Docker image
80+
docker build -t py-dep-man-companion .
7481
```
7582

7683
**Roadmap**: Adding support for pipenv, pdm, pixi, and additional Python package managers.

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[project]
2-
name = "python-dep-manager-companion-mcp-server"
2+
name = "py_dep_man_companion"
33
version = "0.1.0"
4-
description = "Local MCP server providing unified search across Python dependency manager documentation"
4+
description = "Local stdio MCP server providing unified search across Python dependency managers' latest and official documentation."
55
readme = "README.md"
66
license = {file = "LICENSE"}
77
requires-python = ">=3.12"
8-
urls.repository = "https://github.com/KemingHe/python-dep-manager-companion-mcp-server"
9-
urls.issues = "https://github.com/KemingHe/python-dep-manager-companion-mcp-server/issues"
8+
urls.repository = "https://github.com/KemingHe/python-dependency-manager-companion-mcp-server"
9+
urls.issues = "https://github.com/KemingHe/python-dependency-manager-companion-mcp-server/issues"
1010
dependencies = [
1111
"fastmcp>=2.10.5",
1212
"tantivy>=0.24.0",

src/build_index.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def build_index():
133133
if INDEX_DIR.exists():
134134
logger.info(f"Removing existing index at {INDEX_DIR}")
135135
import shutil
136+
136137
shutil.rmtree(INDEX_DIR)
137138

138139
# Create index directory

0 commit comments

Comments
 (0)