Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
05c6685
feat: add log scale, data point customization, and bean plot median/m…
jwildenhain Apr 6, 2026
d546930
Refactor Shiny application: modernize UI framework, optimize data loa…
jwildenhain Apr 8, 2026
58b8ce1
update with docker
jwildenhain Apr 8, 2026
f233ad7
Add Docker publish workflow
jwildenhain Apr 14, 2026
571a56a
refactor: update UI to fluidPage, optimize data processing, and impro…
jwildenhain Apr 16, 2026
aadd24f
fixed docker file with dependencies
jwildenhain Apr 16, 2026
e1dd2a5
Version 2.0 with Excel support and Docker
jwildenhain Apr 16, 2026
196438f
Modernize UI, fix selectize dropdown layout glitches, expand MCP serv…
jwildenhain May 30, 2026
b5c87c4
Fix reactive timing length-zero crashes on initial load during plot r…
jwildenhain May 31, 2026
b680cd4
Reorganize MCP server schema into nested, structured JSON Schema with…
jwildenhain May 31, 2026
3f2a116
Add rich JSON Schema example payload to MCP generate_boxplot tool spe…
jwildenhain May 31, 2026
92c6072
docs: Update FAQ with correct wide-format JSON example and single-lin…
jwildenhain May 31, 2026
88e6f95
docs: Add RELEASE_v2.0.0.md release notes detailing modernized BoxPlo…
jwildenhain May 31, 2026
a75beb9
docs: update README with statistical capabilities, formulas, and visu…
jwildenhain May 31, 2026
b993d41
Fix MCP server app-relative R source paths
Aug 10, 2026
9d35b96
Return generated plot files from MCP server
Aug 10, 2026
e15afff
Harden and containerize public BoxPlotR MCP service
jwildenhain Sep 13, 2026
bce2e5d
Ignore generated Python bytecode
jwildenhain Sep 13, 2026
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
Binary file added .RData
Binary file not shown.
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
assets/
examples/
www/
*.png
*.xlsx
60 changes: 60 additions & 0 deletions .github/workflows/boxplotr-mcp-container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: BoxPlotR MCP container

on:
push:
paths:
- "Dockerfile.mcp"
- "requirements-mcp.txt"
- "boxplotr_mcp_server.py"
- "*.R"
- "deploy/boxplotr_mcp/**"
- "tests/test_mcp_container.py"
- ".github/workflows/boxplotr-mcp-container.yml"
pull_request:
paths:
- "Dockerfile.mcp"
- "requirements-mcp.txt"
- "boxplotr_mcp_server.py"
- "*.R"
- "deploy/boxplotr_mcp/**"
- "tests/test_mcp_container.py"
- ".github/workflows/boxplotr-mcp-container.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
build-and-test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build MCP image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.mcp
load: true
push: false
tags: boxplotr-mcp:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Start MCP container
run: docker run --detach --name boxplotr-mcp --env MCP_IDENTITY_SECRET=docker-ci-runtime-only --publish 8765:8765 boxplotr-mcp:test
- name: Wait for health
run: |
for attempt in $(seq 1 24); do
if curl --fail --silent http://127.0.0.1:8765/health >/dev/null; then
exit 0
fi
sleep 5
done
docker logs boxplotr-mcp
exit 1
- name: Test health, MCP and rendering
run: python3 tests/test_mcp_container.py
- name: Show logs on failure
if: failure()
run: docker logs boxplotr-mcp
98 changes: 98 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Docker

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

on:
schedule:
- cron: '24 21 * * *'
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]

env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}


jobs:
build:

runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
with:
cosign-release: 'v2.2.4'

# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0

# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

# Example code in package build process
*-Ex.R

# Python generated files
__pycache__/
*.py[cod]
60 changes: 60 additions & 0 deletions BoxPlotR_functions.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Specifies the arrangement of data points (normal or jittered).
# point_type: 0 = normal, 2 = jittered
jittered_points <- function(data_matrix, my_horizontal = FALSE, point_type,
point_colors, point_transparency, point_size) {
# normal boxplots
if (my_horizontal) {
for (i in seq_len(ncol(data_matrix))) {
alpha_val <- 255 * (point_transparency / 100)
col_rgb <- rgb(
t(col2rgb(point_colors[i])),
maxColorValue = 255,
alpha = alpha_val
)
if (point_type == 0) {
points(
data_matrix[, i],
rep(i, nrow(data_matrix)),
col = col_rgb,
pch = 16,
cex = point_size
)
} else {
points(
data_matrix[, i],
jitter(rep(i, nrow(data_matrix)), amount = 0.25),
col = col_rgb,
pch = 16,
cex = point_size
)
}
}
} else {
# horizontal boxplots
for (i in seq_len(ncol(data_matrix))) {
alpha_val <- 255 * (point_transparency / 100)
col_rgb <- rgb(
t(col2rgb(point_colors[i])),
maxColorValue = 255,
alpha = alpha_val
)
if (point_type == 0) {
points(
rep(i, nrow(data_matrix)),
data_matrix[, i],
col = col_rgb,
pch = 16,
cex = point_size
)
} else {
points(
jitter(rep(i, nrow(data_matrix)), amount = 0.25),
data_matrix[, i],
col = col_rgb,
pch = 16,
cex = point_size
)
}
}
}
}
Binary file added Boxplot_testData3.xlsx
Binary file not shown.
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Use the official lightweight rocker shiny image
FROM rocker/shiny:latest


RUN R -e "install.packages(c('beeswarm', 'vioplot', 'beanplot', 'RColorBrewer', 'readxl', 'sm', 'testthat', 'ggplot2'), repos='https://cloud.r-project.org/')"

# Remove default Shiny apps
RUN rm -rf /srv/shiny-server/*

# Copy the application files to the container
COPY . /srv/shiny-server/

# Ensure proper ownership
RUN chown -R shiny:shiny /srv/shiny-server/

# Expose the shiny server port
EXPOSE 3838

# Run the Shiny server
CMD ["/usr/bin/shiny-server"]
32 changes: 32 additions & 0 deletions Dockerfile.mcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM rocker/r-ver:4.5.1

RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-venv python3-pip \
&& rm -rf /var/lib/apt/lists/*

RUN install2.r --error --skipinstalled \
beanplot beeswarm ggplot2 RColorBrewer sm tidyr vioplot

WORKDIR /opt/boxplotr-mcp
COPY requirements-mcp.txt ./
RUN python3 -m venv .venv \
&& .venv/bin/pip install --no-cache-dir --requirement requirements-mcp.txt

COPY deploy/boxplotr_mcp/app.py ./
COPY boxplotr_mcp_server.py BoxPlotR_functions.R boxplot_stats_Function.R \
MyBeanplot.R MyVioplot.R /srv/shiny-server/boxplotr/

RUN groupadd --system shiny \
&& useradd --system --gid shiny --home-dir /nonexistent --shell /usr/sbin/nologin shiny \
&& mkdir -p /var/lib/boxplotr-mcp/output /etc/boxplotr-mcp \
&& printf '{}\n' > /etc/boxplotr-mcp/keys.json \
&& chown -R shiny:shiny /var/lib/boxplotr-mcp \
&& chmod -R a-w /opt/boxplotr-mcp /srv/shiny-server/boxplotr

USER shiny
EXPOSE 8765

HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=6 \
CMD .venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health', timeout=2)"

CMD [".venv/bin/uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8765", "--workers", "1", "--limit-concurrency", "24", "--timeout-keep-alive", "5"]
Loading