Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
pull-requests: write
issues: read
id-token: write

Expand All @@ -35,7 +35,31 @@ jobs:
with:
fetch-depth: 1

- name: Check workflow parity with default branch
id: workflow-parity
run: |
DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
git fetch --depth=1 origin "${DEFAULT_BRANCH}"

if ! git cat-file -e "origin/${DEFAULT_BRANCH}:.github/workflows/claude-code-review.yml" 2>/dev/null; then
echo "can_run=false" >> "${GITHUB_OUTPUT}"
exit 0
fi

if git diff --quiet "origin/${DEFAULT_BRANCH}" -- .github/workflows/claude-code-review.yml; then
echo "can_run=true" >> "${GITHUB_OUTPUT}"
else
echo "can_run=false" >> "${GITHUB_OUTPUT}"
fi

- name: Skip Claude Code Review
if: steps.workflow-parity.outputs.can_run != 'true'
run: |
echo "Skipping Claude Code Review because this workflow file differs from the default branch."
echo "The Claude action cannot exchange its app token unless the workflow file matches the default branch exactly."

- name: Run Claude Code Review
if: steps.workflow-parity.outputs.can_run == 'true'
id: claude-review
uses: anthropics/claude-code-action@v1
with:
Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ jobs:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@v2
uses: astral-sh/setup-uv@v6
with:
version: "0.5.13"
enable-cache: true

- name: Install dependencies
Expand Down Expand Up @@ -96,9 +95,8 @@ jobs:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@v2
uses: astral-sh/setup-uv@v6
with:
version: "0.5.13"
enable-cache: true

- name: Install dependencies
Expand Down
21 changes: 17 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

Expand All @@ -45,14 +45,27 @@ jobs:
uv build
ls -la dist/

- name: Validate version matches tag
if: contains(github.ref, 'tags')
run: |
WHEEL=$(ls dist/*.whl | head -1)
WHEEL_VERSION=$(echo "$WHEEL" | sed -n 's/.*-\([0-9][0-9.]*\)-.*/\1/p')
TAG_VERSION="${GITHUB_REF_NAME#v}"
echo "Wheel version: $WHEEL_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$WHEEL_VERSION" != "$TAG_VERSION" ]; then
echo "ERROR: Version mismatch! Wheel=$WHEEL_VERSION Tag=$TAG_VERSION"
exit 1
fi

- name: Check build artifacts
run: |
uv pip install --system twine
twine check dist/*

- name: Create GitHub Release
if: contains(github.ref, 'tags') && !inputs.dry_run
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: dist/*
generate_release_notes: true
Expand All @@ -72,5 +85,5 @@ jobs:
echo "🔍 Dry run complete - artifacts built but not published"
echo "Built artifacts:"
ls -la dist/
echo "Version info:"
cat dist/*.whl | head -c 500
echo "Wheel contents:"
ls -lh dist/
50 changes: 35 additions & 15 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-suffix: "smoke"
Comment on lines 43 to 47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check available tags for setup-uv action
gh api repos/astral-sh/setup-uv/tags --jq '.[].name' | head -20

Repository: royerlab/napari-mcp

Length of output: 223


🌐 Web query:

astral-sh/setup-uv action releases versions v6

💡 Result:

astral-sh/setup-uv does have a v6 major line (usable as uses: astral-sh/setup-uv@v6, which floats to the latest v6.x.y). The last v6 release is v6.8.0 (“Add **/*.py.lock to cache-dependency-glob”, dated Sep 30). [1]

As of Mar 16, 2026, the latest overall major is v7 (latest release shown: v7.6.0), and Astral’s docs now show uses: astral-sh/setup-uv@v7 in examples. [2], [3]

Sources: [1] [2] [3]


Upgrade setup-uv action from v6 to v7.

The workflow currently uses astral-sh/setup-uv@v6, but v7 is now the latest stable release and is recommended in Astral's official documentation. This appears in lines 44 and 96.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test.yml around lines 43 - 47, Update the GitHub Actions
usage of the astral-sh/setup-uv action by changing the version tag from
"astral-sh/setup-uv@v6" to "astral-sh/setup-uv@v7" wherever it appears (e.g.,
the occurrences around the name: Install uv step and the second occurrence near
line 96); keep the existing inputs (enable-cache, cache-suffix) unchanged.

Expand All @@ -51,9 +51,9 @@ jobs:

- name: Install dependencies
run: |
uv sync --extra test
uv sync --group testing

- name: Test with tox
- name: Run smoke tests
uses: aganders3/headless-gui@v1
with:
run: uv run pytest tests/test_tools.py::test_all_tools_end_to_end -v -x
Expand Down Expand Up @@ -88,7 +88,6 @@ jobs:
with:
path: |
.pytest_cache
.coverage.*
key: test-cache-${{ matrix.os }}-py${{ matrix.python-version }}-${{ hashFiles('tests/**/*.py') }}
restore-keys: |
test-cache-${{ matrix.os }}-py${{ matrix.python-version }}-
Expand All @@ -113,14 +112,13 @@ jobs:

- name: Install dependencies
run: |
uv sync --all-extras
uv pip install pytest-qt pytest-asyncio pytest-xdist pytest-timeout
uv sync --all-extras --group testing

- name: Test with tox
- name: Run full test suite
uses: aganders3/headless-gui@v1
with:
run: |
uv run pytest tests/ --verbose --dist loadscope --cov=napari_mcp --cov-report=xml --cov-report=term-missing --cov-fail-under=65 --durations=20 --timeout=60
uv run pytest tests/ --verbose --cov=napari_mcp --cov-report=xml --cov-report=term-missing --cov-fail-under=80 --durations=20 --timeout=60

- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
Expand Down Expand Up @@ -160,7 +158,7 @@ jobs:

- name: Install dependencies
run: |
uv pip install --system ruff mypy types-Pillow bandit "safety<4.0"
uv pip install --system ruff mypy types-Pillow types-toml

- name: Run quality checks
run: |
Expand All @@ -173,18 +171,37 @@ jobs:
echo "::endgroup::"

echo "::group::Type Checking"
mypy src/napari_mcp/ --ignore-missing-imports || true
mypy src/napari_mcp/ --ignore-missing-imports
echo "::endgroup::"

echo "::group::Security Scan"
bandit -r src/ --skip B110,B101,B102,B307 -f json || true
safety check || true
echo "::endgroup::"
# Advisory security scan (non-blocking)
security:
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install security tools
run: pip install bandit "safety<4.0"

- name: Bandit scan
continue-on-error: true
run: bandit -r src/ --skip B110,B101 -f json

- name: Safety check
continue-on-error: true
run: safety check

# Final status check
test-status:
if: always()
needs: [smoke-tests, test-all-platforms, quality]
needs: [smoke-tests, test-all-platforms, quality, security]
runs-on: ubuntu-latest
steps:
- name: Check test status
Expand All @@ -195,4 +212,7 @@ jobs:
echo "Tests failed!"
exit 1
fi
if [[ "${{ needs.security.result }}" == "failure" ]]; then
echo "::warning::Security checks failed (advisory)"
fi
echo "All tests passed!"
83 changes: 53 additions & 30 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Thank you for your interest in contributing to napari-mcp! This document provide
source .venv/bin/activate # On Windows: .venv\Scripts\activate

# Install in development mode
uv pip install -e ".[test]"
uv pip install -e ".[dev]"
```

3. **Install pre-commit hooks**
Expand All @@ -35,7 +35,7 @@ Thank you for your interest in contributing to napari-mcp! This document provide
uv run pytest

# Run with coverage
uv run pytest --cov=napari_mcp_server --cov-report=html
uv run pytest --cov=src --cov-report=html

# Run only fast tests (no GUI)
uv run pytest -m "not realgui"
Expand All @@ -56,7 +56,7 @@ ruff format src/ tests/
ruff check src/ tests/ --fix

# Type checking
mypy src/napari_mcp_server.py --ignore-missing-imports
mypy src/napari_mcp/ --ignore-missing-imports

# Security scanning
bandit -r src/
Expand Down Expand Up @@ -174,32 +174,41 @@ Brief description of the changes
### Specific Guidelines

```python
# ✅ Good: Clear function signature with type hints
async def add_image(
path: str,
name: Optional[str] = None,
colormap: Optional[str] = None
) -> Dict[str, Any]:
"""Add an image layer from a file path.

Args:
path: Path to an image readable by imageio
name: Optional layer name
colormap: Optional napari colormap name

Returns:
Dict with status, name, and shape information
# ✅ Good: Clear function signature with type hints and NumPy docstrings
async def add_layer(
layer_type: str,
path: str | None = None,
name: str | None = None,
colormap: str | None = None,
) -> dict[str, Any]:
"""Add a layer to the viewer.

Parameters
----------
layer_type : str
One of: "image", "labels", "points", "shapes", etc.
path : str | None, optional
Path to an image readable by imageio.
name : str | None, optional
Layer name.
colormap : str | None, optional
Napari colormap name.

Returns
-------
dict[str, Any]
Dict with status, name, and shape information.
"""

# ❌ Bad: No type hints, unclear parameters
def add_image(path, name=None, colormap=None):
def add_layer(type, path=None, name=None):
# Does stuff
return result
```

### Naming Conventions

- **Functions**: `snake_case` (e.g., `add_image`, `set_layer_properties`)
- **Functions**: `snake_case` (e.g., `add_layer`, `set_layer_properties`)
- **Variables**: `snake_case` (e.g., `layer_name`, `current_zoom`)
- **Constants**: `UPPER_CASE` (e.g., `DEFAULT_TIMEOUT`)
- **Classes**: `PascalCase` (e.g., `LayerManager`)
Expand All @@ -211,27 +220,41 @@ def add_image(path, name=None, colormap=None):
```python
"""
tests/
├── test_tools.py # Main tool functionality tests
├── test_tools_real.py # Real napari GUI tests (marked as 'realgui')
├── test_coverage.py # Edge cases and error conditions
└── test_edge_cases.py # Additional edge cases
├── test_tools.py # E2E smoke test, server factory, tool registration
├── test_server_tools.py # Tool-by-tool unit tests, AUTO_DETECT, invalid inputs
├── test_integration.py # Multi-step workflows and concurrent tool calls
├── test_bridge_server.py # Bridge server and QtBridge tests
├── test_widget.py # MCP control widget tests
├── test_qt_helpers.py # Qt helper functions (ensure_qt_app, process_events)
├── test_state.py # ServerState and ViewerProtocol tests
├── test_output_storage.py # Output truncation and storage tests
├── test_timelapse.py # Timelapse screenshot tests
├── test_performance.py # Benchmarks and performance regression tests
├── test_external_viewer.py# External viewer detection and proxy tests
├── test_property_based.py # Hypothesis property-based tests
├── test_base_installer.py # CLI installer base class tests
├── test_cli_installer.py # CLI install command tests
├── test_cli_utils.py # CLI utility function tests
└── test_cli_installers/ # Platform-specific installer tests
"""
```

### Writing Tests

```python
import pytest
from napari_mcp_server import add_image
from napari_mcp import server as napari_mcp_server

@pytest.mark.asyncio
async def test_add_image_success():
"""Test successful image addition."""
async def test_add_layer_success():
"""Test successful layer addition."""
# Arrange
test_image_path = "test_data/sample.png"

# Act
result = await add_image(test_image_path, name="test_image")
result = await napari_mcp_server.add_layer(
layer_type="image", path=test_image_path, name="test_image"
)

# Assert
assert result["status"] == "ok"
Expand All @@ -240,8 +263,8 @@ async def test_add_image_success():

@pytest.mark.realgui
@pytest.mark.asyncio
async def test_add_image_real_gui():
"""Test image addition with real napari GUI."""
async def test_add_layer_real_gui():
"""Test layer addition with real napari GUI."""
# This test requires RUN_REAL_NAPARI_TESTS=1
# and will be skipped in headless CI
```
Expand Down
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
include LICENSE
include README.md
include CHANGELOG.md
include pyproject.toml

recursive-include src/napari_mcp *.yaml
Expand Down
Loading
Loading