diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bbca418..b3f99e1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(chmod:*)" + "Bash(chmod:*)", + "Bash(cargo build:*)" ], "deny": [] } diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md new file mode 100644 index 0000000..6877b31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.md @@ -0,0 +1,58 @@ +--- +name: Release Checklist +about: Checklist for preparing a new release +title: 'Release v[VERSION]' +labels: 'release' +assignees: '' + +--- + +## Pre-release Checklist + +- [ ] All planned features and fixes are merged +- [ ] All tests are passing on main branch +- [ ] Documentation is updated +- [ ] CHANGELOG.md is updated (if maintained separately) +- [ ] Version is updated in Cargo.toml +- [ ] No critical security vulnerabilities in dependencies + +## Release Process + +- [ ] Run `./scripts/release.sh [patch|minor|major|version X.Y.Z]` +- [ ] Verify all CI checks pass +- [ ] Tag is created and pushed +- [ ] GitHub release is created automatically +- [ ] Binaries are built for all platforms +- [ ] Crate is published to crates.io (for stable releases) +- [ ] Docker images are pushed + +## Post-release Tasks + +- [ ] Verify release artifacts are available +- [ ] Test installation from released binaries +- [ ] Update any dependent projects +- [ ] Announce release (if applicable) + +## Release Notes + + + +## Verification Commands + +```bash +# Test the release script (dry run) +./scripts/release.sh patch --dry-run + +# Run pre-release checks +./scripts/release.sh check + +# Create the actual release +./scripts/release.sh patch # or minor/major/version X.Y.Z +``` \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e5cc9df --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,490 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., v1.0.0)' + required: true + type: string + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + validate-release: + name: Validate Release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + tag: ${{ steps.get-version.outputs.tag }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get version from tag or input + id: get-version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + echo "version=${VERSION#v}" >> $GITHUB_OUTPUT + echo "tag=${VERSION}" >> $GITHUB_OUTPUT + else + VERSION="${GITHUB_REF#refs/tags/}" + echo "version=${VERSION#v}" >> $GITHUB_OUTPUT + echo "tag=${VERSION}" >> $GITHUB_OUTPUT + fi + + - name: Validate version format + run: | + VERSION="${{ steps.get-version.outputs.version }}" + if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "❌ Invalid version format: $VERSION" + echo "Expected format: X.Y.Z or X.Y.Z-suffix" + exit 1 + fi + echo "✅ Valid version format: $VERSION" + + - name: Check if version matches Cargo.toml + run: | + CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + INPUT_VERSION="${{ steps.get-version.outputs.version }}" + if [ "$CARGO_VERSION" != "$INPUT_VERSION" ]; then + echo "❌ Version mismatch:" + echo " Cargo.toml: $CARGO_VERSION" + echo " Tag/Input: $INPUT_VERSION" + exit 1 + fi + echo "✅ Version matches Cargo.toml: $INPUT_VERSION" + + test: + name: Run Tests + runs-on: ${{ matrix.os }} + needs: validate-release + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + rust: [stable] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + + - name: Install system dependencies (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libpng-dev + + - name: Install system dependencies (macOS) + if: matrix.os == 'macos-latest' + run: | + brew update + brew install pkg-config freetype jpeg libpng + + - name: Run tests + run: cargo test --all-features --verbose + + build: + name: Build Release Artifacts + runs-on: ${{ matrix.job.os }} + needs: [validate-release, test] + strategy: + matrix: + job: + - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, use-cross: false } + - { os: ubuntu-latest, target: x86_64-unknown-linux-musl, use-cross: true } + - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, use-cross: true } + - { os: ubuntu-latest, target: aarch64-unknown-linux-musl, use-cross: true } + - { os: windows-latest, target: x86_64-pc-windows-msvc, use-cross: false } + - { os: macos-latest, target: x86_64-apple-darwin, use-cross: false } + - { os: macos-latest, target: aarch64-apple-darwin, use-cross: false } + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.job.target }} + + - name: Install cross compilation tools + if: matrix.job.use-cross + run: | + cargo install cross --git https://github.com/cross-rs/cross + + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-${{ matrix.job.target }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + + - name: Install system dependencies (Ubuntu) + if: matrix.job.os == 'ubuntu-latest' && !matrix.job.use-cross + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libpng-dev + + - name: Install system dependencies (macOS) + if: matrix.job.os == 'macos-latest' + run: | + brew update + brew install pkg-config freetype jpeg libpng + + - name: Build release binary + run: | + if [ "${{ matrix.job.use-cross }}" = "true" ]; then + cross build --release --target ${{ matrix.job.target }} --all-features + else + cargo build --release --target ${{ matrix.job.target }} --all-features + fi + + - name: Prepare release archive + shell: bash + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + TARGET="${{ matrix.job.target }}" + + # Create staging directory + mkdir -p staging + + # Copy binary + if [[ "${{ matrix.job.os }}" == "windows-latest" ]]; then + cp "target/${TARGET}/release/docx-mcp.exe" staging/ + BINARY="docx-mcp.exe" + else + cp "target/${TARGET}/release/docx-mcp" staging/ + BINARY="docx-mcp" + fi + + # Copy additional files + cp README.md staging/ + cp LICENSE staging/ + + # Create archive name + ARCHIVE="docx-mcp-${VERSION}-${TARGET}" + + # Create archive + cd staging + if [[ "${{ matrix.job.os }}" == "windows-latest" ]]; then + 7z a "../${ARCHIVE}.zip" * + echo "ARCHIVE=${ARCHIVE}.zip" >> $GITHUB_ENV + else + tar czf "../${ARCHIVE}.tar.gz" * + echo "ARCHIVE=${ARCHIVE}.tar.gz" >> $GITHUB_ENV + fi + cd .. + + # Generate checksums + if [[ "${{ matrix.job.os }}" == "windows-latest" ]]; then + certutil -hashfile "${ARCHIVE}.zip" SHA256 > "${ARCHIVE}.zip.sha256" + else + shasum -a 256 "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256" + fi + + echo "BINARY=${BINARY}" >> $GITHUB_ENV + + - name: Upload release artifacts + uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.job.target }} + path: | + ${{ env.ARCHIVE }} + ${{ env.ARCHIVE }}.sha256 + + create-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [validate-release, build] + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Prepare release assets + run: | + mkdir -p release-assets + find artifacts -type f -name "*.tar.gz" -o -name "*.zip" -o -name "*.sha256" | \ + xargs -I {} cp {} release-assets/ + ls -la release-assets/ + + - name: Generate changelog + id: changelog + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + TAG="${{ needs.validate-release.outputs.tag }}" + + # Get previous tag + PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${TAG}$" | head -1) + + echo "Generating changelog from ${PREV_TAG} to ${TAG}" + + # Generate changelog + if [ -n "$PREV_TAG" ]; then + CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges ${PREV_TAG}..HEAD) + else + CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) + fi + + # Create release notes + cat > release-notes.md << EOF + ## What's Changed + + ${CHANGELOG} + + ## Installation + + ### Pre-built Binaries + + Download the appropriate binary for your system: + + - **Linux x86_64**: \`docx-mcp-${VERSION}-x86_64-unknown-linux-gnu.tar.gz\` + - **Linux x86_64 (musl)**: \`docx-mcp-${VERSION}-x86_64-unknown-linux-musl.tar.gz\` + - **Linux ARM64**: \`docx-mcp-${VERSION}-aarch64-unknown-linux-gnu.tar.gz\` + - **macOS Intel**: \`docx-mcp-${VERSION}-x86_64-apple-darwin.tar.gz\` + - **macOS Apple Silicon**: \`docx-mcp-${VERSION}-aarch64-apple-darwin.tar.gz\` + - **Windows x86_64**: \`docx-mcp-${VERSION}-x86_64-pc-windows-msvc.zip\` + + ### From Source + + \`\`\`bash + cargo install --git https://github.com/hongkongkiwi/docx-mcp --tag ${TAG} + \`\`\` + + ### Verification + + All binaries are provided with SHA256 checksums for verification: + + \`\`\`bash + # Linux/macOS + shasum -a 256 -c docx-mcp-${VERSION}-your-target.tar.gz.sha256 + + # Windows + certutil -hashfile docx-mcp-${VERSION}-x86_64-pc-windows-msvc.zip SHA256 + \`\`\` + + ## Full Changelog + + **Full Changelog**: https://github.com/hongkongkiwi/docx-mcp/compare/${PREV_TAG}...${TAG} + EOF + + echo "CHANGELOG_FILE=release-notes.md" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.validate-release.outputs.tag }} + name: Release ${{ needs.validate-release.outputs.tag }} + body_path: ${{ steps.changelog.outputs.CHANGELOG_FILE }} + files: release-assets/* + draft: false + prerelease: ${{ contains(needs.validate-release.outputs.version, '-') }} + generate_release_notes: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-crate: + name: Publish to crates.io + runs-on: ubuntu-latest + needs: [validate-release, create-release] + if: ${{ !contains(needs.validate-release.outputs.version, '-') }} # Only publish stable releases + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libpng-dev + + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ubuntu-cargo-publish-${{ hashFiles('**/Cargo.lock') }} + + - name: Verify package + run: cargo package --dry-run + + - name: Publish to crates.io + run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + docker-release: + name: Build and Push Docker Images + runs-on: ubuntu-latest + needs: [validate-release, create-release] + permissions: + contents: read + packages: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + if: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + ${{ secrets.DOCKERHUB_USERNAME && format('{0}/docx-mcp', secrets.DOCKERHUB_USERNAME) || '' }} + tags: | + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + update-docs: + name: Update Documentation + runs-on: ubuntu-latest + needs: [validate-release, create-release] + permissions: + contents: write + pages: write + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libpng-dev + + - name: Generate documentation + run: | + cargo doc --all-features --no-deps + echo '' > target/doc/index.html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc + cname: docs.example.com # Replace with your custom domain if you have one + + notify-success: + name: Notify Release Success + runs-on: ubuntu-latest + needs: [create-release, publish-crate, docker-release, update-docs] + if: success() + steps: + - name: Success notification + run: | + echo "🎉 Release ${{ needs.validate-release.outputs.tag }} completed successfully!" + echo "- ✅ GitHub release created" + echo "- ✅ Binaries built for all platforms" + echo "- ✅ Published to crates.io" + echo "- ✅ Docker images pushed" + echo "- ✅ Documentation updated" + + notify-failure: + name: Notify Release Failure + runs-on: ubuntu-latest + needs: [validate-release, test, build, create-release, publish-crate, docker-release, update-docs] + if: failure() + steps: + - name: Failure notification + run: | + echo "❌ Release ${{ needs.validate-release.outputs.tag }} failed!" + echo "Please check the workflow logs for details." + exit 1 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 528d77f..f4936c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,28 @@ name = "docx-mcp" version = "0.1.0" edition = "2021" +authors = ["Your Name "] +description = "A comprehensive Model Context Protocol (MCP) server for Microsoft Word DOCX file manipulation" +documentation = "https://docs.rs/docx-mcp" +homepage = "https://github.com/hongkongkiwi/docx-mcp" +repository = "https://github.com/hongkongkiwi/docx-mcp" +readme = "README.md" +license = "MIT" +keywords = ["mcp", "docx", "word", "document", "pdf"] +categories = ["text-processing", "api-bindings", "command-line-utilities"] +exclude = [ + "/.github/*", + "/tests/fixtures/*", + "/example/*", + "/benches/*", + "/.gitignore", + "/deny.toml" +] [dependencies] # Official MCP SDK -mcp-server = "0.3" -mcp-core = "0.3" +mcp-server = "0.1" +mcp-core = "0.1" # Async runtime tokio = { version = "1.40", features = ["full"] } @@ -26,7 +43,7 @@ lopdf = "0.34" rusttype = "0.9" # Font rendering in pure Rust # Embedded fonts for PDF -include_bytes_plus = "1.0" +include-bytes-plus = "1.0" # Image processing (pure Rust) image = { version = "0.25", features = ["png", "jpeg", "webp", "bmp", "gif"] } @@ -66,6 +83,9 @@ chrono = { version = "0.4", features = ["serde"] } regex = "1.10" once_cell = "1.20" +# Command line argument parsing +clap = { version = "4.5", features = ["derive"] } + # Optional external tool support headless_chrome = { version = "1.0", optional = true } wkhtmltopdf = { version = "0.4", optional = true } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b84d69b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +# Multi-stage Docker build for docx-mcp +FROM rust:1.75-slim as builder + +# Install system dependencies for building +RUN apt-get update && apt-get install -y \ + pkg-config \ + libssl-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libpng-dev \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy manifests +COPY Cargo.toml Cargo.lock ./ +COPY build.rs ./ + +# Copy source code +COPY src/ ./src/ +COPY benches/ ./benches/ +COPY tests/ ./tests/ + +# Build the application +RUN cargo build --release --all-features + +# Runtime stage +FROM debian:bookworm-slim + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + libssl3 \ + libfontconfig1 \ + libfreetype6 \ + libjpeg62-turbo \ + libpng16-16 \ + ca-certificates \ + libreoffice \ + poppler-utils \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN groupadd -r docxmcp && useradd -r -g docxmcp -s /bin/bash -d /app docxmcp + +# Create app directory and set ownership +WORKDIR /app +RUN chown -R docxmcp:docxmcp /app + +# Copy the built binary from builder stage +COPY --from=builder /app/target/release/docx-mcp /usr/local/bin/docx-mcp +RUN chmod +x /usr/local/bin/docx-mcp + +# Copy additional files if needed +COPY README.md LICENSE ./ + +# Switch to non-root user +USER docxmcp + +# Create temp directory for document processing +RUN mkdir -p /tmp/docx-mcp && chmod 755 /tmp/docx-mcp + +# Expose default MCP port (though MCP typically uses stdin/stdout) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD /usr/local/bin/docx-mcp --version || exit 1 + +# Set environment variables +ENV RUST_LOG=info +ENV DOCX_MCP_TEMP_DIR=/tmp/docx-mcp + +# Default command +CMD ["/usr/local/bin/docx-mcp"] \ No newline at end of file diff --git a/README.md b/README.md index a5de9ad..961fb14 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,13 @@ The server includes comprehensive security features for enterprise and restricte ### Readonly Mode ```bash # Enable readonly mode - only allows document viewing and analysis + +# Using environment variables export DOCX_MCP_READONLY=true ./target/release/docx-mcp + +# Using command line arguments +./target/release/docx-mcp --readonly ``` In readonly mode, only these operations are allowed: @@ -74,32 +79,51 @@ In readonly mode, only these operations are allowed: ### Command Filtering ```bash # Whitelist specific commands only + +# Using environment variables export DOCX_MCP_WHITELIST="open_document,extract_text,get_metadata,export_to_markdown" +# Using command line arguments +./target/release/docx-mcp --whitelist open_document,extract_text,get_metadata,export_to_markdown + # Or blacklist dangerous commands + +# Using environment variables export DOCX_MCP_BLACKLIST="save_document,convert_to_pdf,merge_documents" + +# Using command line arguments +./target/release/docx-mcp --blacklist save_document,convert_to_pdf,merge_documents ``` ### Sandbox Mode ```bash # Restrict all file operations to temp directory only + +# Using environment variables export DOCX_MCP_SANDBOX=true ./target/release/docx-mcp + +# Using command line arguments +./target/release/docx-mcp --sandbox ``` ### Resource Limits ```bash # Set maximum document size (100MB default) + +# Using environment variables export DOCX_MCP_MAX_SIZE=52428800 # 50MB - -# Set maximum number of open documents export DOCX_MCP_MAX_DOCS=20 - -# Disable external tools export DOCX_MCP_NO_EXTERNAL_TOOLS=true - -# Disable network operations export DOCX_MCP_NO_NETWORK=true +./target/release/docx-mcp + +# Using command line arguments +./target/release/docx-mcp \ + --max-size 52428800 \ + --max-docs 20 \ + --no-external-tools \ + --no-network ``` ## 🤖 AI Tool Integration @@ -125,6 +149,39 @@ Add to your Claude Desktop configuration file: } ``` +**With Security Options (using command-line arguments):** +```json +{ + "mcpServers": { + "docx": { + "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", + "args": ["--readonly", "--max-size", "52428800", "--no-network"], + "env": { + "RUST_LOG": "info" + } + } + } +} +``` + +**With Security Options (using environment variables):** +```json +{ + "mcpServers": { + "docx": { + "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", + "args": [], + "env": { + "RUST_LOG": "info", + "DOCX_MCP_READONLY": "true", + "DOCX_MCP_MAX_SIZE": "52428800", + "DOCX_MCP_NO_NETWORK": "true" + } + } + } +} +``` + After adding, restart Claude Desktop. You can then ask Claude to: - "Create a new Word document with our Q4 report" - "Convert this DOCX file to PDF" @@ -135,6 +192,7 @@ After adding, restart Claude Desktop. You can then ask Claude to: Add to your Cursor settings (`~/.cursor/config.json` or through Settings UI): +**Basic Configuration:** ```json { "mcp": { @@ -151,10 +209,24 @@ Add to your Cursor settings (`~/.cursor/config.json` or through Settings UI): } ``` -### Windsurf (Codeium) - -Add to your Windsurf configuration (`~/.windsurf/config.json`): +**With Security Options (using command-line arguments):** +```json +{ + "mcp": { + "servers": { + "docx": { + "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", + "args": ["--sandbox", "--whitelist", "open_document,extract_text,export_to_markdown"], + "env": { + "RUST_LOG": "info" + } + } + } + } +} +``` +**With Security Options (using environment variables):** ```json { "mcp": { @@ -162,6 +234,46 @@ Add to your Windsurf configuration (`~/.windsurf/config.json`): "docx": { "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", "args": [], + "env": { + "RUST_LOG": "info", + "DOCX_MCP_SANDBOX": "true", + "DOCX_MCP_WHITELIST": "open_document,extract_text,export_to_markdown" + } + } + } + } +} +``` + +### Windsurf (Codeium) + +Add to your Windsurf configuration (`~/.windsurf/config.json`): + +**Basic Configuration:** +```json +{ + "mcp": { + "servers": { + "docx": { + "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", + "args": [], + "env": { + "RUST_LOG": "info" + } + } + } + } +} +``` + +**With Security Options (using arguments):** +```json +{ + "mcp": { + "servers": { + "docx": { + "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", + "args": ["--readonly", "--no-external-tools"], "env": { "RUST_LOG": "info" } @@ -175,6 +287,7 @@ Add to your Windsurf configuration (`~/.windsurf/config.json`): Add to your Continue configuration (`~/.continue/config.json`): +**Basic Configuration:** ```json { "models": [ @@ -192,10 +305,29 @@ Add to your Continue configuration (`~/.continue/config.json`): } ``` +**With Security Options:** +```json +{ + "models": [ + { + "title": "Your Model", + "provider": "your-provider", + "mcp_servers": { + "docx": { + "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", + "args": ["--sandbox", "--max-size", "10485760"] + } + } + } + ] +} +``` + ### VS Code with MCP Extension If using the MCP extension for VS Code, add to your workspace settings (`.vscode/settings.json`): +**Basic Configuration:** ```json { "mcp.servers": { @@ -210,6 +342,67 @@ If using the MCP extension for VS Code, add to your workspace settings (`.vscode } ``` +**With Security Options:** +```json +{ + "mcp.servers": { + "docx": { + "command": "/absolute/path/to/docx-mcp/target/release/docx-mcp", + "args": ["--readonly", "--blacklist", "save_document,merge_documents"], + "env": { + "RUST_LOG": "info" + } + } + } +} +``` + +## 🔧 Command Line Arguments + +The DOCX MCP server supports the following command-line arguments for configuration: + +```bash +docx-mcp --help +``` + +### Available Arguments + +| Argument | Environment Variable | Description | Example | +|----------|---------------------|-------------|---------| +| `--readonly` | `DOCX_MCP_READONLY=true` | Enable readonly mode - only viewing operations | `--readonly` | +| `--whitelist ` | `DOCX_MCP_WHITELIST` | Comma-separated list of allowed commands | `--whitelist open_document,extract_text` | +| `--blacklist ` | `DOCX_MCP_BLACKLIST` | Comma-separated list of forbidden commands | `--blacklist save_document,convert_to_pdf` | +| `--sandbox` | `DOCX_MCP_SANDBOX=true` | Restrict file operations to temp directory only | `--sandbox` | +| `--no-external-tools` | `DOCX_MCP_NO_EXTERNAL_TOOLS=true` | Disable external tools (LibreOffice, etc.) | `--no-external-tools` | +| `--no-network` | `DOCX_MCP_NO_NETWORK=true` | Disable network operations | `--no-network` | +| `--max-size ` | `DOCX_MCP_MAX_SIZE` | Maximum document size in bytes | `--max-size 52428800` | +| `--max-docs ` | `DOCX_MCP_MAX_DOCS` | Maximum number of open documents | `--max-docs 20` | +| `--help` | - | Show help information | `--help` | +| `--version` | - | Show version information | `--version` | + +### Example Usage + +```bash +# Basic usage +./target/release/docx-mcp + +# Readonly mode with size limit +./target/release/docx-mcp --readonly --max-size 10485760 + +# Sandbox mode with command whitelist +./target/release/docx-mcp --sandbox --whitelist open_document,extract_text,export_to_markdown + +# Multiple security options +./target/release/docx-mcp \ + --readonly \ + --no-external-tools \ + --no-network \ + --max-size 52428800 \ + --max-docs 10 +``` + +**Note:** Command-line arguments take precedence over environment variables when both are specified. + ## 📚 Features ### Document Operations diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..1a26e65 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,249 @@ +# Release Guide + +This document describes the release process for docx-mcp. + +## Overview + +The release process is automated using GitHub Actions and includes: + +- Automated testing on multiple platforms +- Building release binaries for all supported targets +- Publishing to crates.io +- Creating GitHub releases with binaries +- Building and pushing Docker images +- Updating documentation + +## Release Types + +### Semantic Versioning + +We follow [Semantic Versioning](https://semver.org/): + +- **MAJOR**: Incompatible API changes +- **MINOR**: New features (backwards compatible) +- **PATCH**: Bug fixes (backwards compatible) + +### Pre-release Versions + +Pre-release versions can include suffixes like: +- `1.0.0-alpha.1` - Alpha releases +- `1.0.0-beta.1` - Beta releases +- `1.0.0-rc.1` - Release candidates + +## Quick Release Process + +For most releases, use the automated release script: + +```bash +# Patch release (1.0.0 -> 1.0.1) +./scripts/release.sh patch + +# Minor release (1.0.0 -> 1.1.0) +./scripts/release.sh minor + +# Major release (1.0.0 -> 2.0.0) +./scripts/release.sh major + +# Specific version +./scripts/release.sh version 1.5.0 + +# Pre-release +./scripts/release.sh version 1.0.0-beta.1 +``` + +## Manual Release Process + +If you need to create a release manually: + +### 1. Pre-release Checks + +```bash +# Run all checks +./scripts/release.sh check + +# Or manually: +cargo fmt --all -- --check +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all-features +cargo build --release --all-features +cargo package --dry-run +``` + +### 2. Update Version + +Update the version in `Cargo.toml`: + +```toml +[package] +version = "1.2.3" +``` + +Update `Cargo.lock`: + +```bash +cargo update -p docx-mcp +``` + +### 3. Commit and Tag + +```bash +git add Cargo.toml Cargo.lock +git commit -m "Release v1.2.3" +git tag -a "v1.2.3" -m "Release v1.2.3" +git push origin main +git push origin v1.2.3 +``` + +### 4. GitHub Actions + +The release workflow will automatically: + +1. Validate the release +2. Run tests on all platforms +3. Build binaries for all targets +4. Create GitHub release +5. Publish to crates.io (stable releases only) +6. Build and push Docker images +7. Update documentation + +## Supported Platforms + +Release binaries are built for: + +- **Linux**: x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl +- **Linux ARM**: aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl +- **macOS**: x86_64-apple-darwin, aarch64-apple-darwin +- **Windows**: x86_64-pc-windows-msvc + +## Docker Images + +Docker images are published to: + +- GitHub Container Registry: `ghcr.io/hongkongkiwi/docx-mcp` +- Docker Hub: `dockerhub-username/docx-mcp` (if configured) + +Tags include: +- `latest` - Latest stable release +- `v1.2.3` - Specific version +- `1.2.3` - Semantic version +- `1.2` - Major.minor version +- `1` - Major version + +## Publishing to crates.io + +Stable releases (without pre-release suffixes) are automatically published to crates.io. + +### Prerequisites + +1. Set `CARGO_REGISTRY_TOKEN` secret in GitHub repository settings +2. Ensure you have publishing permissions for the crate + +### Manual Publishing + +```bash +# Dry run +cargo publish --dry-run + +# Publish +cargo publish +``` + +## Troubleshooting + +### Release Workflow Fails + +1. Check the Actions tab in GitHub for detailed logs +2. Common issues: + - Version mismatch between tag and Cargo.toml + - Tests failing on specific platforms + - Missing secrets (CARGO_REGISTRY_TOKEN, DOCKERHUB credentials) + +### Version Already Exists + +If you need to recreate a release: + +1. Delete the tag: `git tag -d v1.2.3 && git push origin :v1.2.3` +2. Delete the GitHub release (if created) +3. Create the tag again + +### Docker Build Fails + +1. Check if all dependencies are available in the Docker environment +2. Verify Dockerfile syntax and build context +3. Test locally: `docker build -t docx-mcp:test .` + +### crates.io Publishing Fails + +1. Verify `CARGO_REGISTRY_TOKEN` is set and valid +2. Check if version already exists +3. Ensure all required metadata is in Cargo.toml +4. Run `cargo package --dry-run` to check for issues + +## Security Considerations + +### Signing Releases + +Currently, releases are not cryptographically signed. Consider adding: + +1. GPG signing of Git tags +2. Binary signing with platform-specific tools +3. SBOM (Software Bill of Materials) generation + +### Supply Chain Security + +- Dependencies are audited in CI with `cargo audit` +- Docker images use specific base image versions +- Build reproducibility is enhanced with Rust's deterministic builds + +## Release Checklist + +Use this checklist for important releases: + +- [ ] All planned features are implemented +- [ ] All tests pass locally and in CI +- [ ] Documentation is updated +- [ ] Breaking changes are documented +- [ ] Migration guide is provided (for major releases) +- [ ] Security implications are reviewed +- [ ] Performance regression tests pass +- [ ] Cross-platform compatibility verified +- [ ] Release notes are prepared + +## Post-Release Tasks + +After a release: + +1. **Verify Installation**: Test installation from released binaries +2. **Update Examples**: Update example configurations if needed +3. **Notify Users**: Announce significant releases +4. **Monitor Issues**: Watch for issues related to the new release +5. **Update Dependencies**: Consider updating dependent projects + +## Emergency Releases + +For critical security fixes: + +1. Create a hotfix branch from the affected release tag +2. Apply minimal fix +3. Follow expedited release process +4. Consider yanking affected versions from crates.io if necessary + +```bash +# Yank a version from crates.io (if needed) +cargo yank --version 1.2.3 + +# Un-yank if needed later +cargo yank --version 1.2.3 --undo +``` + +## Release Schedule + +- **Patch releases**: As needed for bug fixes +- **Minor releases**: Monthly or when significant features accumulate +- **Major releases**: Annually or when breaking changes are necessary + +## Getting Help + +- Open an issue for release-related problems +- Check GitHub Actions logs for CI failures +- Review this guide and workflow files for automation details \ No newline at end of file diff --git a/justfile b/justfile index e3199a6..a480816 100644 --- a/justfile +++ b/justfile @@ -169,20 +169,62 @@ flamegraph: changelog: git cliff --output CHANGELOG.md -# Prepare a release +# Release commands using the release script + +# Create a patch release (0.1.0 -> 0.1.1) +release-patch: + ./scripts/release.sh patch + +# Create a minor release (0.1.0 -> 0.2.0) +release-minor: + ./scripts/release.sh minor + +# Create a major release (0.1.0 -> 1.0.0) +release-major: + ./scripts/release.sh major + +# Create a specific version release +release-version version: + ./scripts/release.sh version {{version}} + +# Dry run of patch release (see what would happen) +release-patch-dry: + ./scripts/release.sh patch --dry-run + +# Dry run of minor release +release-minor-dry: + ./scripts/release.sh minor --dry-run + +# Dry run of major release +release-major-dry: + ./scripts/release.sh major --dry-run + +# Dry run of specific version release +release-version-dry version: + ./scripts/release.sh version {{version}} --dry-run + +# Run all pre-release checks +release-check: + ./scripts/release.sh check + +# Generate changelog since last tag +release-changelog: + ./scripts/release.sh changelog + +# Create git tag for current version +release-tag: + ./scripts/release.sh tag + +# Prepare a release (legacy command - use release-* commands above) prepare-release version: - # Update version in Cargo.toml - sed -i 's/^version = ".*"/version = "{{version}}"/' Cargo.toml - # Update dependencies to use new version - cargo update - # Run full test suite - just ci - # Generate changelog - just changelog - # Commit changes - git add . - git commit -m "chore: prepare release {{version}}" - git tag -a "v{{version}}" -m "Release {{version}}" + @echo "âš ī¸ This command is deprecated. Use 'just release-version {{version}}' instead." + @echo "The new release commands provide better automation and safety checks." + @echo "" + @echo "Available release commands:" + @echo " just release-patch - Bump patch version" + @echo " just release-minor - Bump minor version" + @echo " just release-major - Bump major version" + @echo " just release-version X.Y.Z - Set specific version" # Show project statistics stats: @@ -221,4 +263,214 @@ init-hooks: # Remove git hooks remove-hooks: rm -f .git/hooks/pre-commit - echo "Pre-commit hook removed" \ No newline at end of file + echo "Pre-commit hook removed" + +# Docker commands + +# Build multi-platform Docker image +docker-build-multiarch: + docker buildx create --use --name multiarch || true + docker buildx build --platform linux/amd64,linux/arm64 -t docx-mcp:latest . + +# Build and tag Docker image for release +docker-build-release version: + docker buildx build --platform linux/amd64,linux/arm64 \ + -t docx-mcp:{{version}} \ + -t docx-mcp:latest \ + -t ghcr.io/hongkongkiwi/docx-mcp:{{version}} \ + -t ghcr.io/hongkongkiwi/docx-mcp:latest \ + . + +# Push Docker images to registry +docker-push version: + docker push docx-mcp:{{version}} + docker push docx-mcp:latest + docker push ghcr.io/hongkongkiwi/docx-mcp:{{version}} + docker push ghcr.io/hongkongkiwi/docx-mcp:latest + +# Run Docker container with volume mount for testing +docker-test: + docker run --rm -it -v $(pwd)/test-docs:/test-docs docx-mcp:latest + +# Development environment commands + +# Full development setup from scratch +dev-setup: + # Install Rust if not present + @if ! command -v rustup >/dev/null 2>&1; then \ + echo "Installing Rust..."; \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \ + source ~/.cargo/env; \ + fi + # Setup toolchain and tools + just setup + # Initialize git hooks + just init-hooks + # Build project + just build + echo "✅ Development environment ready!" + +# Check system dependencies +check-deps: + @echo "=== System Dependencies Check ===" + @echo "Checking required tools..." + @command -v rustc >/dev/null && echo "✅ Rust compiler found" || echo "❌ Rust compiler not found" + @command -v cargo >/dev/null && echo "✅ Cargo found" || echo "❌ Cargo not found" + @command -v git >/dev/null && echo "✅ Git found" || echo "❌ Git not found" + @command -v docker >/dev/null && echo "✅ Docker found" || echo "❌ Docker not found" + @command -v just >/dev/null && echo "✅ Just found" || echo "❌ Just not found" + @echo "" + @echo "Optional tools:" + @command -v libreoffice >/dev/null && echo "✅ LibreOffice found" || echo "âš ī¸ LibreOffice not found (optional)" + @command -v pdftoppm >/dev/null && echo "✅ pdftoppm found" || echo "âš ī¸ pdftoppm not found (optional)" + @command -v convert >/dev/null && echo "✅ ImageMagick convert found" || echo "âš ī¸ ImageMagick not found (optional)" + +# Cross-compilation commands + +# Build for all supported targets +build-all-targets: + # Install targets if not present + rustup target add x86_64-unknown-linux-gnu + rustup target add x86_64-unknown-linux-musl + rustup target add aarch64-unknown-linux-gnu + rustup target add x86_64-apple-darwin + rustup target add aarch64-apple-darwin + rustup target add x86_64-pc-windows-msvc + # Build for each target + cargo build --release --target x86_64-unknown-linux-gnu --all-features + cargo build --release --target x86_64-unknown-linux-musl --all-features + cargo build --release --target x86_64-apple-darwin --all-features + @echo "✅ Built for all available targets" + +# Build using cross for Linux targets +build-cross-linux: + cargo install cross --git https://github.com/cross-rs/cross + cross build --release --target x86_64-unknown-linux-gnu --all-features + cross build --release --target x86_64-unknown-linux-musl --all-features + cross build --release --target aarch64-unknown-linux-gnu --all-features + cross build --release --target aarch64-unknown-linux-musl --all-features + +# Maintenance commands + +# Update all dependencies to latest versions +update-deps: + cargo update + cargo outdated --depth 1 + +# Check for security vulnerabilities and update +security-update: + cargo audit fix + cargo update + +# Clean everything (including registry cache) +clean-all: + cargo clean + rm -rf ~/.cargo/registry/cache + rm -rf ~/.cargo/git/db + docker system prune -f + +# Backup project (excluding target and build artifacts) +backup: + #!/usr/bin/env bash + BACKUP_NAME="docx-mcp-backup-$(date +%Y%m%d-%H%M%S)" + tar czf "${BACKUP_NAME}.tar.gz" \ + --exclude='target' \ + --exclude='.git' \ + --exclude='*.log' \ + --exclude='*.tmp' \ + . + echo "✅ Backup created: ${BACKUP_NAME}.tar.gz" + +# Development workflows + +# Quick development loop (format, build, test unit, lint) +dev-loop: + just fmt + just build + just test-unit + just clippy + +# Full quality check (everything CI runs) +quality-check: + just fmt-check + just clippy + just test + just docs-check + just audit + just deny + +# Continuous development with file watching +dev-watch: + cargo install cargo-watch + cargo watch -w src -w tests -x "build" -x "test --lib" + +# Performance analysis +perf-analysis: + # Build optimized release + cargo build --release --all-features + # Run criterion benchmarks + cargo bench --all-features + # Generate flamegraph if available + @if command -v flamegraph >/dev/null 2>&1; then \ + echo "Generating flamegraph..."; \ + cargo flamegraph --bin docx-mcp -- --help; \ + fi + +# MCP-specific commands + +# Test MCP server functionality +test-mcp: + @echo "Testing MCP server..." + # Build the server + cargo build --release --all-features + # Run basic functionality test + python3 example/test_client.py || echo "❌ MCP test failed" + +# Generate MCP documentation +mcp-docs: + @echo "Generating MCP server documentation..." + cargo run --bin docx-mcp -- --help > docs/CLI_REFERENCE.md + @echo "✅ CLI reference updated" + +# Example commands + +# Run all examples +run-examples: + @echo "Running all examples..." + @if [ -f example/test_client.py ]; then python3 example/test_client.py; fi + @if [ -f example/automation_example.py ]; then python3 example/automation_example.py; fi + +# Generate test documents +gen-test-docs: + @echo "Generating test documents..." + mkdir -p test-docs + # You could add commands here to generate various test DOCX files + +# Utility commands + +# Show detailed project info +info: + @echo "=== Project Information ===" + @echo "Name: docx-mcp" + @echo "Version: $(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')" + @echo "Rust version: $(rustc --version)" + @echo "Cargo version: $(cargo --version)" + @echo "" + just stats + +# List all available commands with descriptions +help: + @echo "=== Available Commands ===" + @just --list + @echo "" + @echo "=== Release Commands ===" + @echo " release-patch - Create patch release (0.1.0 -> 0.1.1)" + @echo " release-minor - Create minor release (0.1.0 -> 0.2.0)" + @echo " release-major - Create major release (0.1.0 -> 1.0.0)" + @echo " release-version X - Create specific version release" + @echo " release-*-dry - Dry run versions of above commands" + @echo "" + @echo "=== Development Workflows ===" + @echo " dev-loop - Quick development cycle" + @echo " quality-check - Full quality assessment" + @echo " dev-setup - Complete development environment setup" \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..2af1165 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,355 @@ +#!/bin/bash + +# Release script for docx-mcp +# This script helps with version management and release preparation + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +info() { + echo -e "${BLUE}â„šī¸ $1${NC}" +} + +success() { + echo -e "${GREEN}✅ $1${NC}" +} + +warning() { + echo -e "${YELLOW}âš ī¸ $1${NC}" +} + +error() { + echo -e "${RED}❌ $1${NC}" +} + +# Check if we're in a git repository +check_git_repo() { + if ! git rev-parse --git-dir > /dev/null 2>&1; then + error "Not in a git repository" + exit 1 + fi +} + +# Check if working directory is clean +check_clean_working_dir() { + if ! git diff-index --quiet HEAD --; then + error "Working directory is not clean. Please commit or stash your changes." + exit 1 + fi +} + +# Get current version from Cargo.toml +get_current_version() { + grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/' +} + +# Update version in Cargo.toml +update_version() { + local new_version=$1 + info "Updating version to $new_version" + + # Update Cargo.toml + sed -i.bak "s/^version = \".*\"/version = \"$new_version\"/" Cargo.toml + rm Cargo.toml.bak + + # Update Cargo.lock + cargo update -p docx-mcp + + success "Version updated to $new_version" +} + +# Generate changelog since last tag +generate_changelog() { + local last_tag=$(git tag --sort=-version:refname | head -1) + local new_version=$1 + + info "Generating changelog since $last_tag" + + if [ -n "$last_tag" ]; then + git log --pretty=format:"- %s (%h)" --no-merges ${last_tag}..HEAD > CHANGELOG.tmp + else + git log --pretty=format:"- %s (%h)" --no-merges > CHANGELOG.tmp + fi + + echo "## Release $new_version ($(date +%Y-%m-%d))" + echo "" + cat CHANGELOG.tmp + echo "" + rm CHANGELOG.tmp +} + +# Run pre-release checks +run_checks() { + info "Running pre-release checks..." + + # Format check + info "Checking code formatting..." + cargo fmt --all -- --check + success "Code formatting is correct" + + # Clippy check + info "Running Clippy..." + cargo clippy --all-targets --all-features -- -D warnings + success "Clippy checks passed" + + # Tests + info "Running tests..." + cargo test --all-features + success "All tests passed" + + # Build check + info "Testing release build..." + cargo build --release --all-features + success "Release build successful" + + # Package check + info "Testing package..." + cargo package --dry-run + success "Package validation passed" +} + +# Create and push git tag +create_tag() { + local version=$1 + local tag="v$version" + + info "Creating git tag $tag" + + # Create annotated tag + git tag -a "$tag" -m "Release $tag" + + success "Created tag $tag" + + # Ask if user wants to push + echo -n "Push tag to origin? [y/N]: " + read -r response + if [[ "$response" =~ ^[Yy]$ ]]; then + git push origin "$tag" + success "Tag pushed to origin" + else + warning "Tag not pushed. Remember to push it manually: git push origin $tag" + fi +} + +# Show usage information +usage() { + cat << EOF +Usage: $0 [COMMAND] [OPTIONS] + +Commands: + patch Bump patch version (0.1.0 -> 0.1.1) + minor Bump minor version (0.1.0 -> 0.2.0) + major Bump major version (0.1.0 -> 1.0.0) + version X.Y.Z Set specific version + check Run pre-release checks only + changelog Generate changelog since last tag + tag Create git tag for current version + +Options: + --dry-run Show what would be done without making changes + --no-checks Skip pre-release checks (not recommended) + --no-tag Don't create git tag + --help Show this help message + +Examples: + $0 patch # Bump to next patch version + $0 version 1.0.0 # Set version to 1.0.0 + $0 check # Run all pre-release checks + $0 patch --dry-run # Show what patch release would do +EOF +} + +# Parse version bump type +bump_version() { + local current_version=$1 + local bump_type=$2 + + # Split version into components + IFS='.' read -ra VERSION_PARTS <<< "$current_version" + local major=${VERSION_PARTS[0]} + local minor=${VERSION_PARTS[1]} + local patch=${VERSION_PARTS[2]} + + case $bump_type in + "patch") + patch=$((patch + 1)) + ;; + "minor") + minor=$((minor + 1)) + patch=0 + ;; + "major") + major=$((major + 1)) + minor=0 + patch=0 + ;; + *) + error "Invalid bump type: $bump_type" + exit 1 + ;; + esac + + echo "${major}.${minor}.${patch}" +} + +# Validate version format +validate_version() { + local version=$1 + if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + error "Invalid version format: $version" + error "Expected format: X.Y.Z or X.Y.Z-suffix" + exit 1 + fi +} + +# Main script logic +main() { + local command=$1 + local dry_run=false + local no_checks=false + local no_tag=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + dry_run=true + shift + ;; + --no-checks) + no_checks=true + shift + ;; + --no-tag) + no_tag=true + shift + ;; + --help) + usage + exit 0 + ;; + *) + if [ -z "$command" ]; then + command=$1 + elif [ -z "$version_arg" ] && [ "$command" = "version" ]; then + version_arg=$1 + fi + shift + ;; + esac + done + + # Check if command provided + if [ -z "$command" ]; then + usage + exit 1 + fi + + # Basic checks + check_git_repo + + if [ "$dry_run" = false ]; then + check_clean_working_dir + fi + + current_version=$(get_current_version) + info "Current version: $current_version" + + case $command in + "patch"|"minor"|"major") + new_version=$(bump_version "$current_version" "$command") + ;; + "version") + if [ -z "$version_arg" ]; then + error "Version argument required for 'version' command" + exit 1 + fi + new_version=$version_arg + validate_version "$new_version" + ;; + "check") + run_checks + success "All pre-release checks passed!" + exit 0 + ;; + "changelog") + generate_changelog "$current_version" + exit 0 + ;; + "tag") + if [ "$dry_run" = true ]; then + info "Would create tag v$current_version" + else + create_tag "$current_version" + fi + exit 0 + ;; + *) + error "Unknown command: $command" + usage + exit 1 + ;; + esac + + info "New version will be: $new_version" + + if [ "$dry_run" = true ]; then + warning "DRY RUN MODE - No changes will be made" + info "Would update version from $current_version to $new_version" + if [ "$no_checks" = false ]; then + info "Would run pre-release checks" + fi + if [ "$no_tag" = false ]; then + info "Would create git tag v$new_version" + fi + exit 0 + fi + + # Confirm with user + echo -n "Proceed with release $new_version? [y/N]: " + read -r response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + warning "Release cancelled" + exit 0 + fi + + # Run pre-release checks + if [ "$no_checks" = false ]; then + run_checks + fi + + # Update version + update_version "$new_version" + + # Commit version bump + git add Cargo.toml Cargo.lock + git commit -m "Release $new_version" + success "Version bump committed" + + # Create tag + if [ "$no_tag" = false ]; then + create_tag "$new_version" + fi + + # Generate changelog for reference + info "Changelog for release:" + generate_changelog "$new_version" + + success "Release $new_version completed!" + info "Next steps:" + info "1. Push commits: git push origin main" + if [ "$no_tag" = false ]; then + info "2. Push tag: git push origin v$new_version (if not done already)" + fi + info "3. GitHub Actions will automatically create the release" +} + +# Run main function with all arguments +main "$@" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 47745aa..8d68b06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use mcp_server::{Server, ServerBuilder, ServerOptions}; use mcp_core::ToolManager; use tracing::info; use tracing_subscriber::{EnvFilter, fmt, prelude::*}; +use clap::Parser; mod docx_tools; mod docx_handler; @@ -23,8 +24,9 @@ async fn main() -> Result<()> { .with(EnvFilter::from_default_env()) .init(); - // Load security configuration from environment - let security_config = security::SecurityConfig::from_env(); + // Parse command line arguments (which also includes environment variables) + let args = security::Args::parse(); + let security_config = security::SecurityConfig::from_args(args); info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary()); let docx_provider = DocxToolsProvider::new_with_security(security_config); diff --git a/src/security.rs b/src/security.rs index 502622f..582d9d9 100644 --- a/src/security.rs +++ b/src/security.rs @@ -2,6 +2,46 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::env; use tracing::{debug, info, warn}; +use clap::Parser; + +/// Command line arguments for the DOCX MCP server +#[derive(Parser, Debug)] +#[command(name = "docx-mcp")] +#[command(about = "A comprehensive Model Context Protocol (MCP) server for Microsoft Word DOCX file manipulation")] +#[command(version)] +pub struct Args { + /// Enable readonly mode - only allow viewing operations + #[arg(long, env = "DOCX_MCP_READONLY")] + pub readonly: bool, + + /// Comma-separated whitelist of allowed commands + #[arg(long, env = "DOCX_MCP_WHITELIST", value_delimiter = ',')] + pub whitelist: Option>, + + /// Comma-separated blacklist of forbidden commands + #[arg(long, env = "DOCX_MCP_BLACKLIST", value_delimiter = ',')] + pub blacklist: Option>, + + /// Enable sandbox mode - restrict file operations to temp directory only + #[arg(long, env = "DOCX_MCP_SANDBOX")] + pub sandbox: bool, + + /// Disable external tools (LibreOffice, etc.) + #[arg(long, env = "DOCX_MCP_NO_EXTERNAL_TOOLS")] + pub no_external_tools: bool, + + /// Disable network operations + #[arg(long, env = "DOCX_MCP_NO_NETWORK")] + pub no_network: bool, + + /// Maximum document size in bytes + #[arg(long, env = "DOCX_MCP_MAX_SIZE")] + pub max_size: Option, + + /// Maximum number of open documents + #[arg(long, env = "DOCX_MCP_MAX_DOCS")] + pub max_docs: Option, +} /// Security configuration for the MCP server #[derive(Debug, Clone, Serialize, Deserialize)] @@ -47,7 +87,59 @@ impl Default for SecurityConfig { } impl SecurityConfig { - /// Load configuration from environment variables + /// Create configuration from command line arguments + pub fn from_args(args: Args) -> Self { + let mut config = Self::default(); + + // Apply command line arguments + if args.readonly { + config.readonly_mode = true; + info!("Running in READONLY mode - only viewing operations allowed"); + } + + if let Some(whitelist) = args.whitelist { + let commands: HashSet = whitelist.into_iter().collect(); + info!("Command whitelist enabled with {} commands", commands.len()); + config.command_whitelist = Some(commands); + } + + if let Some(blacklist) = args.blacklist { + let commands: HashSet = blacklist.into_iter().collect(); + info!("Command blacklist enabled with {} blocked commands", commands.len()); + config.command_blacklist = Some(commands); + } + + if args.sandbox { + config.sandbox_mode = true; + config.allow_external_tools = false; + config.allow_network = false; + info!("Running in SANDBOX mode - restricted file operations"); + } + + if args.no_external_tools { + config.allow_external_tools = false; + info!("External tools disabled"); + } + + if args.no_network { + config.allow_network = false; + info!("Network operations disabled"); + } + + if let Some(size) = args.max_size { + config.max_document_size = size; + info!("Max document size set to {} bytes", size); + } + + if let Some(max) = args.max_docs { + config.max_open_documents = max; + info!("Max open documents set to {}", max); + } + + config + } + + /// Load configuration from environment variables (deprecated, use from_args instead) pub fn from_env() -> Self { let mut config = Self::default();