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
43 changes: 43 additions & 0 deletions .github/setup-node/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: "Setup Node.js"
description: "Sets up Node.js environment and installs dependencies."
inputs:
node_version:
description: "Node.js version to use, e.g. 20.x"
required: false
default: "24.12.0"
pnpm_version:
description: "pnpm version to use, e.g. 10.x"
required: false
default: "10.x"

runs:
using: "composite"
steps:
- name: Use Node.js ${{ inputs.node_version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ inputs.pnpm_version }}

- name: Get pnpm store location
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies (pnpm)
shell: bash
run: |
pnpm install --frozen-lockfile
# equivalent to npm ci
23 changes: 23 additions & 0 deletions .github/workflows/pr-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI

on:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: ./.github/setup-node

- name: Build server
run: pnpm run build:server

- name: Run tests
run: pnpm test
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
13 changes: 9 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Build argument for base image
# Base image built from Dockerfile-base and published to GHCR.
# Update both the tag and the digest together when publishing a new base image.
# apt/rustup runs inside the pinned base build — accepted.
ARG BASE_IMAGE=ghcr.io/g-core/fastedge-mcp-server-base:latest

# Build stage
Expand Down Expand Up @@ -35,9 +37,12 @@ ENV WORKSPACE_ROOT=/workspace
# Set up a volume mount point for workspace data
VOLUME [ "/workspace" ]

# Entrypoint drops privileges to the host user (owner of the /workspace mount,
# or HOST_UID/HOST_GID if set) so generated files are not root-owned. Falls
# back to running as root when the resolved UID is 0 (e.g. no mount or root-owned mount).
# Entrypoint resolves the target UID/GID from the /workspace mount owner, then
# drops privileges via setpriv so generated files are owned by that user. When
# the mount is missing, root-owned, or Docker Desktop-virtualized (uid 0 inside
# the container), it falls back to uid/gid 10001 instead of running as root.
# Override with -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) on docker run when
# automatic detection does not produce the right owner.
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
Expand Down
11 changes: 10 additions & 1 deletion Dockerfile-base
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM rust:1.95-slim
# rust:1.95-slim — update digest if the tag is ever re-released
FROM rust:1.95-slim@sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0

RUN rustup target add wasm32-wasip1 wasm32-wasip2 && \
rm -rf /usr/local/cargo/registry /usr/local/cargo/git /usr/local/rustup/tmp/* && \
Expand Down Expand Up @@ -72,3 +73,11 @@ SHELL ["/bin/bash", "-c"]
# Set environment variables for better shell experience
ENV SHELL=/bin/bash
ENV TERM=xterm-256color

# Baked-in unprivileged fallback user (UID/GID 10001).
# The entrypoint starts as root for privilege-drop machinery; when it cannot
# resolve a real workspace owner (root-owned mount, Docker Desktop), it drops
# to this user instead of staying root. Do NOT add a USER directive here —
# the entrypoint must start as root to stat the mount and call setpriv.
RUN groupadd -g 10001 fastedge && \
useradd -u 10001 -g 10001 -m -d /home/fastedge fastedge
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ Make sure to set the following environment variables:

See [DEVELOPMENT.md](./DEVELOPMENT.md) for the full env var table and the preprod build recipe.

## Permissions

The container entrypoint automatically detects the owner of the `/workspace` mount and drops privileges to that UID/GID so generated files are not root-owned. If builds fail with "Permission denied", pass `-e HOST_UID=$(id -u) -e HOST_GID=$(id -g)` to `docker run` to override the detected user:

```bash
docker run --rm -i \
-v "$(pwd):/workspace" \
-e WORKSPACE_ROOT=/workspace \
-e HOST_UID=$(id -u) -e HOST_GID=$(id -g) \
-e GCORE_API_KEY=your_api_key \
ghcr.io/g-core/fastedge-mcp-server:latest
```

## Supported FastEdge Templates

The MCP server includes the following templates:
Expand Down
7 changes: 6 additions & 1 deletion STANDALONE-SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Create a file called `.vscode/mcp.json` in your workspace with the following con
"command": "bash",
"args": [
"-c",
"docker run --rm -i --pull=always -v ${workspaceFolder}:/workspace -e WORKSPACE_ROOT=/workspace -e \"GCORE_API_KEY=$GCORE_API_KEY\" ghcr.io/g-core/fastedge-mcp-server:latest"
"docker run --rm -i --pull=always -v ${workspaceFolder}:/workspace -e WORKSPACE_ROOT=/workspace -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) -e \"GCORE_API_KEY=$GCORE_API_KEY\" ghcr.io/g-core/fastedge-mcp-server:latest"
],
"env": {
"GCORE_API_KEY": "your_api_key_here"
Expand Down Expand Up @@ -51,10 +51,15 @@ You can test the Docker image manually:
docker run --rm -i --pull=always \
-v "$(pwd):/workspace" \
-e "WORKSPACE_ROOT=/workspace" \
-e HOST_UID=$(id -u) -e HOST_GID=$(id -g) \
-e "GCORE_API_KEY=your_api_key" \
ghcr.io/g-core/fastedge-mcp-server:latest
```

## Permissions

The container entrypoint automatically detects the owner of the `/workspace` mount and drops privileges to that UID/GID so generated files are not root-owned. If builds fail with "Permission denied", pass `-e HOST_UID=$(id -u) -e HOST_GID=$(id -g)` to `docker run` (both `docker run` examples above already include these flags).

## Requirements

- Docker installed and running
Expand Down
40 changes: 31 additions & 9 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
#!/bin/sh
# Drop privileges to the host user so files created in the bind-mounted
# workspace are owned by that user instead of root.
# Resolve the UID/GID to run as, then drop privileges before exec-ing the
# server process.
#
# Target UID/GID resolution order:
# 1. Explicit HOST_UID / HOST_GID environment variables
# Resolution order:
# 1. HOST_UID / HOST_GID environment variables (explicit override)
# 2. Owner of the mounted workspace directory ($WORKSPACE_ROOT)
# 3. Fall back to running as-is (root) for backward compatibility
# 3. If the resolved UID is 0 (no mount, root-owned mount, or Docker Desktop
# where bind-mount ownership appears as uid 0 inside the container), fall
# back to the baked-in uid/gid 10001 to avoid running as container root.
#
# This keeps the container backward-compatible: with no writable mount, or on
# Docker Desktop (macOS/Windows) where bind-mount ownership is virtualized and
# typically appears as uid 0 inside the container, the workspace owner resolves
# to 0 and we stay root.
# Pass -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) to docker run when the
# workspace mount is owned by a non-root user but the above detection does not
# pick it up correctly (e.g. userns-remap setups).
#
# The API key is passed to the Node process via fd 3 (a heredoc opened below)
# and removed from the environment before exec so it does not appear in
# /proc/<pid>/environ of child processes.
set -e

WORKSPACE_ROOT="${WORKSPACE_ROOT:-/workspace}"
Expand All @@ -35,6 +40,14 @@ fi
target_uid="${target_uid:-0}"
target_gid="${target_gid:-$target_uid}"

# When the resolved owner is root (root-owned or absent mount, Docker Desktop
# virtualized ownership), drop to the baked-in fallback user instead of
# staying root. This prevents untrusted build code from running as container root.
if [ "$target_uid" = "0" ]; then
target_uid=10001
target_gid=10001
fi

if [ "$(id -u)" = "0" ] && [ "$target_uid" != "0" ] && command -v setpriv >/dev/null 2>&1; then
# Give the unprivileged user a writable HOME for tool caches
# (npm / pnpm / create-fastedge-app). The cargo registry already lives in a
Expand All @@ -49,7 +62,16 @@ if [ "$(id -u)" = "0" ] && [ "$target_uid" != "0" ] && command -v setpriv >/dev/
chmod 0700 "$HOME"
chown "$target_uid:$target_gid" "$HOME"
export HOME
exec 3<<EOF
${GCORE_API_KEY:-${FASTEDGE_API_KEY:-}}
EOF
unset GCORE_API_KEY FASTEDGE_API_KEY
exec setpriv --reuid="$target_uid" --regid="$target_gid" --clear-groups "$@"
fi

if [ "$(id -u)" = "0" ]; then echo "Warning: running as root — setpriv not found" >&2; fi
exec 3<<EOF
${GCORE_API_KEY:-${FASTEDGE_API_KEY:-}}
EOF
unset GCORE_API_KEY FASTEDGE_API_KEY
exec "$@"
23 changes: 19 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,46 @@
"build:watch": "tsc -p ./tsconfig.build.json --watch",
"generate:schemas:prod": "SPEC_BASE_URL=${SPEC_BASE_URL:-https://api.gcore.com} tsx scripts/generate-schemas.ts",
"generate:schemas:preprod": "SPEC_BASE_URL=https://api.preprod.world tsx scripts/generate-schemas.ts",
"test": "npm run test:api && npm run test:compiler-injection && npm run test:reference-index",
"test": "npm run test:api && npm run test:compiler-injection && npm run test:path-confinement && npm run test:subprocess-bounds && npm run test:subprocess-env && npm run test:scaffold-pin && npm run test:permissions && npm run test:reference-index",
"test:api": "tsx --test scripts/tests/test-api.ts",
"test:reference-index": "bash scripts/tests/test-reference-index.sh",
"server:dev": "tsx src/server.ts",
"server:inspect": "export DANGEROUSLY_OMIT_AUTH=true && npx @modelcontextprotocol/inspector npm run server:dev",
"test:compiler-injection": "tsx --test scripts/tests/test-compiler-injection.ts"
"test:compiler-injection": "tsx --test scripts/tests/test-compiler-injection.ts",
"test:path-confinement": "tsx --test scripts/tests/test-path-confinement.ts",
"test:subprocess-bounds": "tsx --test scripts/tests/test-subprocess-bounds.ts",
"test:subprocess-env": "tsx --test scripts/tests/test-subprocess-env.ts",
"test:scaffold-pin": "tsx --test scripts/tests/test-scaffold-pin.ts",
"test:permissions": "tsx --test scripts/tests/test-permissions.ts"
},
"author": "Gcore",
"license": "Apache-2.0",
"dependencies": {
"@gcoredev/fastedge-sdk-js": "^2.5.1",
"@modelcontextprotocol/sdk": "^1.30.0",
"dedent": "^1.7.0",
"qs": "^6.14.0",
"toml": "^3.0.0",
"zod": "^4.3.5"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^12.1.0",
"@modelcontextprotocol/inspector": "^0.18.0",
"@types/node": "^24.10.9",
"@types/qs": "^6.14.0",
"esbuild": "^0.27.2",
"npm-run-all2": "^8.0.4",
"tsx": "^4.20.3",
"typescript": "^5.9.3"
},
"pnpm": {
"overrides": {
"@hono/node-server@<1.19.10": ">=1.19.10",
"path-to-regexp@>=8.0.0 <8.4.0": ">=8.4.0",
"fast-uri@>=3.0.0 <=3.1.3": ">=3.1.4",
"hono@<4.11.7": ">=4.11.7",
"ajv@>=7.0.0-alpha.0 <8.18.0": ">=8.18.0",
"picomatch@>=4.0.0 <4.0.4": ">=4.0.4",
"qs@>=6.7.0 <=6.14.1": ">=6.14.2",
"body-parser@>=2.0.0 <2.3.0": ">=2.3.0"
}
}
}
Loading
Loading