# Install MCP Forge

Install the latest stable MCP Forge binary or run the published container.
No account is required.

MCP Forge is the governed publishing layer for MCP tools. Version `v1`
connects PostgreSQL, MySQL and SQL Server; its connector model keeps the
product identity independent from any one source type.

> API connectors are part of the product direction, but they are not included
> in `v1`. Current installation and configuration examples therefore use
> database connectors only.

Documentation is versioned by major release (`v1`). The `latest` download and
image tags follow the newest stable `v1` release. For repeatable production
deployments, record the version reported by `runku version` and pin that exact
release during the next maintenance window.

For the guided first-run experience, use the
[MCP Forge quickstart](/docs/v1/mcp-forge-quickstart). This page documents
the manual installation path and the operational choices the quick installer
makes on your behalf.

## Installation contract

A complete binary deployment has five independent parts:

| Component | Purpose | Must persist |
|---|---|---|
| `runku` binary | Serves the portal and MCP endpoints | Replace only during upgrades |
| `config.yaml` | Listener, store location, connectors and OIDC verification | Yes; back up with deployment configuration |
| Service environment | Admin-store and connector DSNs | Yes; keep in a secrets manager or restricted file |
| Admin store | Policies, identities, roles and API-key hashes | Yes; PostgreSQL is recommended for production |
| Audit store | Local portal/report history and shipping outbox | Yes when local history matters |

The target databases configured under `connectors[]` are not installation
state and are never interchangeable with Runku's private admin store.

## Binary installation

### Linux x86-64

```bash
curl --fail --location --output runku.tar.gz \
  https://downloads.runku.dev/latest/runku_linux_amd64.tar.gz
curl --fail --location --output checksums.txt \
  https://downloads.runku.dev/latest/checksums.txt
grep " runku_linux_amd64.tar.gz$" checksums.txt | sha256sum --check
tar -xzf runku.tar.gz
sudo install -m 0755 runku /usr/local/bin/runku
runku version
```

### macOS Apple silicon

```bash
curl --fail --location --output runku.tar.gz \
  https://downloads.runku.dev/latest/runku_darwin_arm64.tar.gz
curl --fail --location --output checksums.txt \
  https://downloads.runku.dev/latest/checksums.txt
grep " runku_darwin_arm64.tar.gz$" checksums.txt | shasum -a 256 --check
tar -xzf runku.tar.gz
sudo install -m 0755 runku /usr/local/bin/runku
runku version
```

### Windows x86-64

Run PowerShell as Administrator:

```powershell
$BaseUrl = "https://downloads.runku.dev/latest"
Invoke-WebRequest "$BaseUrl/runku_windows_amd64.zip" -OutFile runku.zip
Invoke-WebRequest "$BaseUrl/checksums.txt" -OutFile checksums.txt
$Expected = ((Select-String " runku_windows_amd64.zip$" checksums.txt).Line -split " ")[0]
$Actual = (Get-FileHash runku.zip -Algorithm SHA256).Hash.ToLower()
if ($Actual -ne $Expected.ToLower()) { throw "Checksum verification failed" }
New-Item -ItemType Directory -Force "C:\Program Files\Runku" | Out-Null
Expand-Archive runku.zip -DestinationPath "C:\Program Files\Runku" -Force
& "C:\Program Files\Runku\runku.exe" version
```

Add `C:\Program Files\Runku` to the system `PATH`, then open a new terminal.

### Direct downloads

| Platform | Architecture | Latest stable | Immutable v1.0.0 |
|---|---|---|---|
| Linux | x86-64 | [Download](https://downloads.runku.dev/latest/runku_linux_amd64.tar.gz) | [Download](https://downloads.runku.dev/releases/v1.0.0/runku_linux_amd64.tar.gz) |
| Linux | ARM64 | [Download](https://downloads.runku.dev/latest/runku_linux_arm64.tar.gz) | [Download](https://downloads.runku.dev/releases/v1.0.0/runku_linux_arm64.tar.gz) |
| macOS | Intel | [Download](https://downloads.runku.dev/latest/runku_darwin_amd64.tar.gz) | [Download](https://downloads.runku.dev/releases/v1.0.0/runku_darwin_amd64.tar.gz) |
| macOS | Apple silicon | [Download](https://downloads.runku.dev/latest/runku_darwin_arm64.tar.gz) | [Download](https://downloads.runku.dev/releases/v1.0.0/runku_darwin_arm64.tar.gz) |
| Windows | x86-64 | [Download](https://downloads.runku.dev/latest/runku_windows_amd64.zip) | [Download](https://downloads.runku.dev/releases/v1.0.0/runku_windows_amd64.zip) |

[SHA-256 checksums](https://downloads.runku.dev/latest/checksums.txt) are
published with every channel update. Production automation should use the
immutable release URLs.

## Choose the filesystem layout

The examples below use predictable system locations:

| Platform | Binary | Bootstrap configuration | Persistent data | Restricted environment |
|---|---|---|---|---|
| Linux/macOS | `/usr/local/bin/runku` | `/etc/runku/config.yaml` | `/var/lib/runku` | `/etc/runku/runku.env` |
| Windows | `C:\Program Files\Runku\runku.exe` | `C:\ProgramData\Runku\config.yaml` | `C:\ProgramData\Runku` | Service manager or protected wrapper |

On Linux, create the unprivileged service account and directories before
writing configuration:

```bash
# The service account cannot log in interactively.
sudo useradd --system --home /var/lib/runku --shell /usr/sbin/nologin runku
# Configuration and state are separate so permissions and backups are clear.
sudo install -d -m 0750 /etc/runku
sudo install -d -o runku -g runku -m 0750 /var/lib/runku
```

On macOS, create the same directories and use the non-root account that will
own the launchd process:

```bash
# launchd will run Runku as the current account, not as root.
sudo install -d -m 0750 /etc/runku /var/lib/runku
sudo chown "$(id -un)" /var/lib/runku
```

On Windows:

```powershell
# ProgramData persists configuration and runtime state outside Program Files.
New-Item -ItemType Directory -Force "C:\ProgramData\Runku" | Out-Null
```

## Create the bootstrap configuration

This production-oriented example uses a dedicated PostgreSQL admin store, one
PostgreSQL source connector, local audit history and API-key authentication.
The same connector section accepts `mysql` or `sqlserver`.

```yaml
# HTTP listener shared by the local portal and all MCP connector routes.
server:
  # Place a TLS reverse proxy in front before exposing this port externally.
  port: 8080

# Runku's private operational database. It contains policy and identities,
# never rows from the source configured under connectors[].
store:
  # PostgreSQL is recommended for production and multiple Runku instances.
  driver: postgres
  # Keep the dedicated admin-store credential outside this file.
  dsn: ${RUNKU_MCP_STORE_DSN}

# External sources published through independent governed MCP endpoints.
connectors:
  # Stable identifier used by the URL, policy and operational commands.
  - id: primary
    # MCP clients for this source connect to /mcp/primary.
    path: /mcp/primary
    database:
      # Supported values in v1: postgres, mysql or sqlserver.
      driver: postgres
      # This credential belongs to the source, not the Runku admin store.
      dsn: ${RUNKU_CONNECTOR_DSN}

# Optional local reporting buffer, separate from both databases above.
audit:
  # Persist this path and include it in backups when local history matters.
  sqlite_path: /var/lib/runku/runku-audit.db

# Require API keys and/or verified OIDC tokens on protected routes.
auth:
  enabled: true
```

On Windows, change `audit.sqlite_path` to
`C:/ProgramData/Runku/runku-audit.db`. YAML accepts forward slashes and avoids
Windows escaping ambiguity.

For a small single-instance installation, replace the `store` block with:

```yaml
# Embedded admin store; suitable for evaluation and small single-node setups.
store:
  # No external admin database server is required.
  driver: sqlite
  # Keep this file on persistent storage and include it in backups.
  path: /var/lib/runku/runku.db
```

## Supply secrets outside YAML

Create `/etc/runku/runku.env` on Linux or macOS:

```bash
# Store and connector credentials have different users and responsibilities.
sudo sh -c 'umask 077; cat > /etc/runku/runku.env' <<'EOF'
RUNKU_MCP_STORE_DSN='postgres://runku:<password>@<admin-db>:5432/runku?sslmode=require'
RUNKU_CONNECTOR_DSN='postgres://reader:<password>@<source-db>:5432/app?sslmode=require'
EOF
```

Restrict it to the account that will run Runku. For a `runku` system account:

```bash
# The service needs read access; other local users must not see DSNs.
sudo chown root:runku /etc/runku/config.yaml /etc/runku/runku.env
sudo chmod 0640 /etc/runku/config.yaml
sudo chmod 0600 /etc/runku/runku.env
```

On macOS, make the configuration, environment and launcher readable only by
the non-root account selected for launchd:

```bash
# Replace ownership without exposing DSNs to other local accounts.
sudo chown "$(id -un)" /etc/runku/config.yaml /etc/runku/runku.env
sudo chmod 0600 /etc/runku/config.yaml /etc/runku/runku.env
```

On Windows, load the values into the elevated shell used for initialization;
the service manager must receive the same values later:

```powershell
# These values exist only in the current PowerShell process.
$env:RUNKU_MCP_STORE_DSN = "postgres://runku:<password>@<admin-db>:5432/runku?sslmode=require"
$env:RUNKU_CONNECTOR_DSN = "postgres://reader:<password>@<source-db>:5432/app?sslmode=require"
```

## Configure OIDC instead of bootstrap API keys

OIDC is optional. It verifies tokens from an identity provider you operate; it
does not store roles in YAML. Add this under `auth` before initialization:

```yaml
# Authentication verifies the token; authorization remains in the admin store.
auth:
  # Reject requests without a valid API key or OIDC bearer.
  enabled: true
  oidc:
    # Keycloak realm, Google issuer or Entra issuer ending in /v2.0.
    issuer: https://id.example.com/realms/company
    # Required token audience; configure the same value in the IdP.
    audience: runku-mcp
    # Public Authorization Code + PKCE client used by the local portal.
    portal_client_id: runku-portal
    # Google normally uses true; Entra and Keycloak normally use false.
    prefer_id_token: false
```

Use the provider-specific values from
[Google Workspace and Entra ID](/docs/v1/idp-google-entra) or
[Keycloak and generic OIDC](/docs/v1/idp-keycloak).

## Initialize the admin store

Load the service environment, then run the supported first-boot initializer:

```bash
# Export restricted environment values without copying them into config.yaml.
set -a
. /etc/runku/runku.env
set +a
# Tests every configured source and initializes the empty admin store.
sudo -u runku --preserve-env=RUNKU_MCP_STORE_DSN,RUNKU_CONNECTOR_DSN \
  runku auth init --config /etc/runku/config.yaml
```

On macOS, run initialization as the same account declared in the launchd
definition:

```bash
# The current account owns the restricted config and service environment.
set -a
. /etc/runku/runku.env
set +a
runku auth init --config /etc/runku/config.yaml
```

On Windows:

```powershell
# Uses the environment values loaded in the previous step.
& "C:\Program Files\Runku\runku.exe" auth init `
  --config "C:\ProgramData\Runku\config.yaml"
```

When OIDC is absent, initialization creates one unrestricted bootstrap API key,
prints it once and writes `.runku-init-token` beside `config.yaml`. Copy the
token into a secrets manager, then delete that marker. Runku intentionally
refuses to start while the plaintext marker remains. When OIDC is present, the
initializer skips bootstrap-key generation.

Initialization does not expose tables or enable mutations. Those decisions are
stored per connector after installation.

## Run once in the foreground

Before registering a service, start Runku in the foreground and verify the
exact configuration that the service will use:

```bash
# Linux: keep this terminal open while performing the first health checks.
sudo -u runku --preserve-env=RUNKU_MCP_STORE_DSN,RUNKU_CONNECTOR_DSN \
  runku serve --config /etc/runku/config.yaml

# macOS: run as the account selected for launchd.
set -a; . /etc/runku/runku.env; set +a
runku serve --config /etc/runku/config.yaml
```

```powershell
# Press Ctrl+C after the portal and connector checks succeed.
& "C:\Program Files\Runku\runku.exe" serve `
  --config "C:\ProgramData\Runku\config.yaml"
```

Confirm that the portal loads at `/portal/`, the expected connector route is
present, authentication rejects a request without credentials, and startup
logs show successful source introspection. Stop the foreground process before
registering the managed service.

## Register a Linux systemd service

Confirm the unprivileged account created during layout preparation has only the
required files:

```bash
sudo chown -R runku:runku /var/lib/runku
sudo chown root:runku /etc/runku/config.yaml /etc/runku/runku.env
```

Create `/etc/systemd/system/runku.service`:

```ini
[Unit]
# Start only after the host considers networking available.
Description=Runku MCP Forge
After=network-online.target
Wants=network-online.target

[Service]
# Run the published binary in the foreground; systemd owns its lifecycle.
Type=simple
User=runku
Group=runku
EnvironmentFile=/etc/runku/runku.env
ExecStart=/usr/local/bin/runku serve --config /etc/runku/config.yaml
# Recover from process failures without creating a tight restart loop.
Restart=on-failure
RestartSec=5
# Reduce access to host facilities not required by Runku.
NoNewPrivileges=true
PrivateTmp=true

[Install]
# Start automatically during normal multi-user boot.
WantedBy=multi-user.target
```

Enable and inspect it:

```bash
# Reload units after every service-file change.
sudo systemctl daemon-reload
sudo systemctl enable --now runku.service
sudo systemctl status runku --no-pager
sudo journalctl -u runku -n 100 --no-pager
```

## Register a macOS launchd service

Create a small restricted launcher that loads `/etc/runku/runku.env` and then
replaces itself with the Runku process. Save it as `/etc/runku/service.sh`:

```bash
#!/bin/sh
# Export values loaded from the restricted environment file.
set -a
. /etc/runku/runku.env
set +a
# launchd monitors this foreground process and restarts it when required.
exec /usr/local/bin/runku serve --config /etc/runku/config.yaml
```

Set the launcher and secret-file permissions to the macOS account that will run
Runku, then create `/Library/LaunchDaemons/dev.runku.mcp.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <!-- Stable launchd service identifier. -->
  <key>Label</key><string>dev.runku.mcp</string>
  <!-- Replace YOUR_ACCOUNT with the non-root account that owns runtime data. -->
  <key>UserName</key><string>YOUR_ACCOUNT</string>
  <key>ProgramArguments</key>
  <array><string>/etc/runku/service.sh</string></array>
  <!-- Start during boot and recover if the foreground process exits. -->
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <!-- Keep service output in the persistent data directory. -->
  <key>StandardOutPath</key><string>/var/lib/runku/runku.log</string>
  <key>StandardErrorPath</key><string>/var/lib/runku/runku-error.log</string>
</dict></plist>
```

Register and inspect it:

```bash
# Validate the plist before installing it.
plutil -lint dev.runku.mcp.plist
sudo install -m 0644 dev.runku.mcp.plist /Library/LaunchDaemons/dev.runku.mcp.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/dev.runku.mcp.plist
sudo launchctl print system/dev.runku.mcp
```

## Register a managed Windows startup task

The published binary runs in the foreground. On Windows, the supported script
uses Task Scheduler as the process supervisor: it runs as `SYSTEM`, starts at
boot and retries after failure. Store the DSNs in a PowerShell wrapper whose ACL
allows only `SYSTEM` and local administrators, then register that wrapper with
`New-ScheduledTaskAction`, `New-ScheduledTaskTrigger -AtStartup` and
`Register-ScheduledTask`.

The complete, reviewed implementation is the versioned
[Windows installer script](https://runku.dev/install/v1/install.ps1). For a
manual Windows deployment, download it and inspect the section titled
`Register and start the managed background service`; it creates no external
account and installs no third-party service wrapper.

Check or restart the managed process with:

```powershell
# Inspect the supervisor state and last result.
Get-ScheduledTask -TaskName "Runku MCP Forge"
Get-ScheduledTaskInfo -TaskName "Runku MCP Forge"
# Apply configuration changes with a controlled restart.
Stop-ScheduledTask -TaskName "Runku MCP Forge"
Start-ScheduledTask -TaskName "Runku MCP Forge"
```

## Docker Compose installation

Create `compose.yaml`:

```yaml
services:
  # Dedicated PostgreSQL database for Runku's internal admin store.
  # Do not point this service at a customer/source database.
  postgres:
    # Pin an exact major/minor image in controlled production environments.
    image: postgres:17-alpine
    # Restart after host or process failures unless explicitly stopped.
    restart: unless-stopped
    environment:
      # Database, login and secret used only by the Runku admin store.
      POSTGRES_DB: runku
      POSTGRES_USER: runku
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
    volumes:
      # Persist policies, roles, identities and API-key hashes across restarts.
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      # Prevent Runku from starting before its admin database accepts traffic.
      test: ["CMD-SHELL", "pg_isready -U runku -d runku"]
      interval: 10s
      timeout: 5s
      retries: 10

  # Free self-hosted MCP Forge distribution.
  runku:
    # `latest` tracks the stable channel; pin a version for controlled upgrades.
    image: registry.runku.dev/runku:latest
    restart: unless-stopped
    depends_on:
      postgres:
        # Start only after the internal store is ready.
        condition: service_healthy
    ports:
      # Host port : container port for portal and MCP endpoints.
      - "8080:8080"
    # Source connector DSNs and other secrets can be supplied from this file.
    env_file: .env
    environment:
      # Internal-store DSN. This is separate from connector DSNs in config.yaml.
      RUNKU_MCP_STORE_DSN: postgres://runku:${POSTGRES_PASSWORD}@postgres:5432/runku?sslmode=disable
    volumes:
      # Persist bootstrap config and the temporary first-token safety marker.
      - ./runku-config:/etc/runku
      # Persistent local runtime data such as the optional audit buffer.
      - runku-data:/var/lib/runku
    # Start the published binary with the mounted bootstrap configuration.
    command: ["serve", "--config", "/etc/runku/config.yaml"]

volumes:
  # Named volumes survive container replacement and image upgrades.
  postgres-data:
  runku-data:
```

Create `runku-config/config.yaml` beside `compose.yaml`:

```yaml
# Container listener exposed by compose.yaml on host port 8080.
server:
  port: 8080

# Private admin state stored in the dedicated postgres Compose service.
store:
  # PostgreSQL data persists in the postgres-data named volume.
  driver: postgres
  # Compose injects this complete DSN into the Runku container.
  dsn: ${RUNKU_MCP_STORE_DSN}

# External business source; it is not the postgres service above.
connectors:
  - id: primary
    path: /mcp/primary
    database:
      # Change to mysql or sqlserver when appropriate.
      driver: postgres
      # Source credential loaded from the Compose environment.
      dsn: ${RUNKU_CONNECTOR_DSN}

# Local report/audit buffer persisted in the runku-data volume.
audit:
  sqlite_path: /var/lib/runku/runku-audit.db

# First boot creates an API key unless an oidc block is added here.
auth:
  enabled: true
```

Create `.env` with restrictive permissions. This file is used for Compose
substitution and is passed to the Runku container through `env_file`:

```dotenv
# Password for the dedicated PostgreSQL admin-store container.
POSTGRES_PASSWORD=replace-with-a-long-random-password
# DSN for the external source; use a least-privilege database account.
RUNKU_CONNECTOR_DSN=postgres://reader:password@source.example:5432/app?sslmode=require
```

Do not commit or publish `.env`. On Unix, use `chmod 0600 .env`.

```bash
docker compose pull
docker compose run --rm runku auth init --config /etc/runku/config.yaml
# API-key mode only: save the token, then remove its plaintext safety marker.
if [ -f runku-config/.runku-init-token ]; then
  cat runku-config/.runku-init-token
  rm runku-config/.runku-init-token
fi
docker compose up -d
docker compose exec runku runku version
docker compose logs --tail=100 runku
```

If initialization generated `.runku-init-token`, the service intentionally
refuses to start until that token is copied to secure storage and the marker is
removed from `runku-config`. Remove only the marker after confirming the token
is safely stored; do not delete the PostgreSQL admin store or its volume.

For production, replace `latest` with the exact version printed by the last
command, for example `registry.runku.dev/runku:v1.0.0`.

## Production storage

MCP Forge supports SQLite and PostgreSQL admin stores. PostgreSQL is the
recommended production mode and supports multiple service instances sharing
the same roles, identities, API keys and connector policies. SQLite remains a
simple option for evaluation and small single-node installations.

The admin store is separate from connected PostgreSQL, MySQL or SQL Server
sources. Back up the PostgreSQL admin database, `config.yaml` and the audit
store according to the same recovery point.

## Post-installation verification

Do not treat a running process as a complete verification. Check each boundary:

1. `runku version` reports the expected pinned release.
2. Startup logs show successful admin-store connection and source introspection.
3. `/portal/` loads through the intended hostname or local URL.
4. An unauthenticated request to a protected route is rejected.
5. The initial API key or OIDC login reaches the portal.
6. Each connector appears at its configured `/mcp/<id>` route.
7. An exposed, masked table omits the protected columns from both tool schema
   and results.
8. A table or operation not allowed by policy is blocked and audited.

Keep the service bound to a trusted network until authentication, TLS and the
first policy have passed these checks. If a reverse proxy terminates TLS, make
sure it forwards long-lived MCP connections without buffering or an
aggressive request timeout.

## Logs and common failures

| Symptom | Check |
|---|---|
| Startup rejects an unknown YAML field | Compare the versioned configuration reference; parsing is intentionally strict |
| Admin store connection fails | Verify `RUNKU_MCP_STORE_DSN`, DNS, TLS mode and that the database is dedicated to Runku |
| Connector connects but introspection fails | Grant catalog visibility and read access required for the selected schemas |
| Service refuses to start after initialization | Save the initial token and remove only `.runku-init-token` beside `config.yaml` |
| Portal opens but login fails | Verify issuer discovery, token audience, portal client ID and redirect URI |
| Connector has no tools | Apply an explicit policy; installation intentionally exposes nothing |
| Service works in a shell but not after boot | Confirm the service account receives the same DSN environment and can read configuration/data paths |

Log locations depend on the process supervisor:

- Linux: `journalctl -u runku`.
- macOS example: `/var/lib/runku/runku.log` and `runku-error.log`.
- Windows: Task Scheduler history plus the Runku process output destination
  configured by your wrapper or monitoring agent.
- Compose: `docker compose logs runku` and `docker compose logs postgres`.

## Upgrade and rollback

1. Record `runku version`, the configuration checksum and the currently pinned
   container/binary version.
2. Back up the PostgreSQL or SQLite admin store, local audit store,
   `config.yaml` and service definition in the same maintenance window.
3. Read the target version's release notes before changing the binary.
4. Download and checksum the exact immutable release, or pull its exact image
   tag. Do not leave `latest` in a controlled production definition.
5. Stop the service, replace only the binary/image, and keep the previous
   artifact available for rollback.
6. Start the service and repeat the post-installation verification, including
   one allowed and one blocked tool call.
7. If verification fails, stop the new process, restore the previous artifact
   and restore the matching admin-store backup only when the release notes say
   the store format changed.

For a systemd binary deployment, replacement is typically:

```bash
# Preserve the old binary as the immediate rollback artifact.
sudo systemctl stop runku
sudo cp /usr/local/bin/runku /usr/local/bin/runku.previous
sudo install -m 0755 ./runku /usr/local/bin/runku
sudo systemctl start runku
sudo systemctl status runku --no-pager
```

## Remove the managed process

Unregistering the process does not automatically delete configuration, secrets,
admin state or audit history. This protects against accidental data loss.

```bash
# Linux: stop automatic execution and remove only the unit definition.
sudo systemctl disable --now runku
sudo rm /etc/systemd/system/runku.service
sudo systemctl daemon-reload

# macOS: unregister and remove only the launchd definition.
sudo launchctl bootout system/dev.runku.mcp
sudo rm /Library/LaunchDaemons/dev.runku.mcp.plist
```

```powershell
# Windows: remove only the managed startup task.
Unregister-ScheduledTask -TaskName "Runku MCP Forge" -Confirm:$false
```

After taking a final backup, remove the binary, `/etc/runku` or
`C:\ProgramData\Runku`, `/var/lib/runku`, and the PostgreSQL database only if
you explicitly intend to erase the installation. Those destructive removals
are deliberately not bundled into a single command.

Continue with [MCP Forge configuration](/docs/v1/mcp-configuration).
