From bcd7e012ba755f0164508b71c15443ee4f20088c Mon Sep 17 00:00:00 2001 From: code Date: Tue, 14 Jul 2026 20:47:29 +0000 Subject: [PATCH] added gitea, n8n and invokeai MCP servers, plus docs/ Co-Authored-By: Claude Fable 5 --- AGENTS.md | 24 +++++++++++ Dockerfile | 26 +++++++++++ README.md | 18 ++++++++ docs/README.md | 14 ++++++ docs/claudaris.md | 96 +++++++++++++++++++++++++++++++++++++++++ docs/getting-started.md | 89 ++++++++++++++++++++++++++++++++++++++ docs/mcp.md | 78 +++++++++++++++++++++++++++++++++ files/bash_aliases | 11 ++++- files/entrypoint.sh | 20 +++++++++ 9 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 docs/README.md create mode 100644 docs/claudaris.md create mode 100644 docs/getting-started.md create mode 100644 docs/mcp.md diff --git a/AGENTS.md b/AGENTS.md index c865e1f..bbd7fec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,25 @@ Docker scaffold for running Claude Code in an isolated container (see README.md) individual host files onto `~/.claude/.credentials.json` (the OAuth token) and `~/.claude.json` (account/onboarding state) — see `claudaris start` below for why these are per-file mounts rather than a directory volume. + Three MCP servers for Claude Code are also installed at build time: + `n8n-mcp` (npm, `/usr/local/bin/n8n-mcp`), the official `gitea-mcp` + (latest release binary, `/usr/local/bin/gitea-mcp`), and + `invokeai-mcp-server` (PyPI via `uv tool install`; upstream hardcodes the + InvokeAI URL, so the Dockerfile sed-patches it to honor + `$INVOKEAI_BASE_URL`, with a `grep` that fails the build if upstream + changes and the patch stops landing). They're only *installed* by the + Dockerfile — *registration* happens in `files/entrypoint.sh` on every + container start (`claude mcp add --scope user`, skipped per-server if + already present), because user-scope MCP config lives in `~/.claude.json`, + which is bind-mounted from the host and would shadow anything registered + at build time. Consequence of the "add if missing" guard: hand edits to a + server's config survive restarts, but a server removed with + `claude mcp remove` is re-added on the next container start. The servers + read their instance URLs/tokens (`GITEA_HOST`, `GITEA_ACCESS_TOKEN`, + `N8N_API_URL`, `N8N_API_KEY`, `INVOKEAI_BASE_URL`) from the environment + Claude Code runs in — commented `export` templates live in + `files/bash_aliases`, which is host-persisted, so tokens stay out of the + image and the repo. `git config --system --add safe.directory '*'` is set so git works on `/projects` repos bind-mounted from the host (which the container, always running as root, would otherwise treat as untrusted). `files/entrypoint.sh` @@ -74,6 +93,11 @@ Docker scaffold for running Claude Code in an isolated container (see README.md) `bash_aliases` are also seeded onto the host by `claudaris start` so their bind-mounted copies can diverge without a rebuild. - `README.md` — usage instructions. +- `docs/` — user documentation in markdown: `README.md` (index), + `getting-started.md` (setup and daily use), `claudaris.md` (the executable, + subcommand by subcommand, including the mount table), `mcp.md` (the baked-in + MCP servers and their env vars). Keep these in sync when changing the + script, Dockerfile, or entrypoint. ## Common commands ```bash diff --git a/Dockerfile b/Dockerfile index 37e5b05..fe789c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,6 +56,32 @@ RUN curl -fsSL https://claude.ai/install.sh | bash RUN PATH="/root/.local/bin:$PATH" bash -c \ 'curl -fsSL https://raw.githubusercontent.com/leeguooooo/claude-code-usage-bar/main/web-install.sh | bash' +# MCP servers for Claude Code. Only installed here — they're registered at +# user scope by entrypoint.sh on container start, because user-scope MCP +# config lives in ~/.claude.json, which is bind-mounted from the host and +# would shadow anything written to it at build time. Each server picks up +# its instance URL/token from the environment (see files/bash_aliases). + +# n8n — https://github.com/czlonkowski/n8n-mcp +RUN npm install -g --prefix /usr/local n8n-mcp + +# Gitea — official server, latest release binary +# https://gitea.com/gitea/gitea-mcp +RUN tag="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/gitea-mcp/releases/latest \ + | python -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" && \ + curl -fsSL "https://gitea.com/gitea/gitea-mcp/releases/download/${tag}/gitea-mcp_Linux_x86_64.tar.gz" \ + | tar -xz -C /usr/local/bin gitea-mcp && \ + chmod 755 /usr/local/bin/gitea-mcp + +# InvokeAI — https://github.com/coinstax/invokeai-mcp-server. Upstream +# hardcodes the InvokeAI URL, so patch it to honor $INVOKEAI_BASE_URL; the +# grep fails the build if upstream changes shape and the patch stops landing. +RUN uv tool install invokeai-mcp-server && \ + sed -i 's|^INVOKEAI_BASE_URL = .*|import os\nINVOKEAI_BASE_URL = os.environ.get("INVOKEAI_BASE_URL", "http://127.0.0.1:9090")|' \ + /root/.local/share/uv/tools/invokeai-mcp-server/lib/python*/site-packages/invokeai_mcp_server.py && \ + grep -q 'INVOKEAI_BASE_URL = os.environ.get' \ + /root/.local/share/uv/tools/invokeai-mcp-server/lib/python*/site-packages/invokeai_mcp_server.py + # tmux wrapper COPY files/tmux /usr/local/bin/tmux COPY files/tmux.conf /etc/tmux.conf diff --git a/README.md b/README.md index 5d3d6a9..f2a93fa 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Claudaris (klaw-DAR-iss): A moveable docker container for Claude code. using Arch btw. +Full documentation lives in [`docs/`](docs/README.md): [getting started](docs/getting-started.md), +the [`claudaris` executable](docs/claudaris.md), and [MCP servers](docs/mcp.md). + ## Configure Run the interactive wizard and answer its prompts (press enter to keep a @@ -49,6 +52,21 @@ To let the container SSH out to other machines, create `$DATA_DIR/ssh` on the host and populate it with keys/config *before* running `./claudaris start`. If present, it's bind-mounted read-only to `/root/.ssh`. It's off by default. +## MCP servers + +MCP servers for [Gitea](https://gitea.com/gitea/gitea-mcp), +[n8n](https://github.com/czlonkowski/n8n-mcp), and +[InvokeAI](https://github.com/coinstax/invokeai-mcp-server) are baked into +the image and registered with Claude Code (user scope) automatically on +container start. To point them at your instances, uncomment and fill in the +`export` lines at the bottom of your host-persisted +`$DATA_DIR/home/bash_aliases` (`GITEA_HOST`, `GITEA_ACCESS_TOKEN`, +`N8N_API_URL`, `N8N_API_KEY`, `INVOKEAI_BASE_URL`) — if yours was seeded +before this feature existed, copy the block from `files/bash_aliases`. +Tokens stay on the host, out of the image and repo. Note the services run +elsewhere (not in this container), so use URLs reachable from inside it — +`127.0.0.1` here means the container itself. + ## Notes Claude Code is installed at build time under `/root`, so a rebuild always diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9e46fb1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,14 @@ +# claudaris docs + +Claudaris (klaw-DAR-iss) is a movable Docker container for running +[Claude Code](https://code.claude.com/docs) in isolation, built on Arch Linux. +Your login, dotfiles, and MCP configuration live on the host, so the image can +be rebuilt at any time (always pulling the latest Claude Code release) without +losing anything. + +- **[Getting started](getting-started.md)** — prerequisites, first-time setup, + and daily use. +- **[The `claudaris` executable](claudaris.md)** — every subcommand, what it + does, and the files and mounts behind it. +- **[MCP servers](mcp.md)** — the Gitea, n8n, and InvokeAI MCP servers baked + into the image, and how to point them at your instances. diff --git a/docs/claudaris.md b/docs/claudaris.md new file mode 100644 index 0000000..2ee115e --- /dev/null +++ b/docs/claudaris.md @@ -0,0 +1,96 @@ +# The `claudaris` executable + +`./claudaris` is a single bash script at the repo root that wraps every +Docker operation. It `cd`s to its own directory first, so it can be invoked +from anywhere. Run it with no arguments (or `help`) for usage. + +``` +Usage: ./claudaris + +Commands: + config, configure Interactive wizard to write .env with your settings + build Build the container image (always --no-cache --pull) + start Start (or create) the container + connect Start the container if needed, then attach a tmux session + remove, stop, rm Stop and remove the container + help Show this message +``` + +All commands source `.env` if present (gitignored, written by `config`), +falling back to `NAME=claudaris` and the defaults listed below. + +## `config` (alias: `configure`) + +Interactive wizard: prompts for `NAME`, `DATA_DIR`, and `WORKSPACE_DIR`, +showing the current/default value for each (enter keeps it), then writes all +three to `.env`. Safe to re-run any time. See +[Getting started](getting-started.md#1-configure) for what each variable +means. + +## `build` + +```bash +sudo ./claudaris build +``` + +Runs `docker build --no-cache --pull -t "$NAME" .`. Always uncached so a +rebuild picks up the latest Arch packages, the latest Claude Code release +(installed straight into `/root` in the image), and the latest +[MCP server](mcp.md) releases. + +## `start` + +Creates and starts the container — or just `docker start`s it if a container +named `$NAME` already exists (in that case none of the mount setup below is +re-evaluated; use `remove` first to pick up mount changes). + +On the host side it first seeds, without overwriting anything that already +exists: + +- `$DATA_DIR/home/.bashrc` and `$DATA_DIR/home/bash_aliases` — copied from + `files/bashrc` and `files/bash_aliases`. +- `$DATA_DIR/claude/credentials.json` and `$DATA_DIR/claude/claude.json` — + created empty. Seeding them as files matters: if Docker had to create the + mount targets itself it would make directories, breaking the login mounts. + +Then it runs the container with: + +| Mount / flag | Purpose | +| --- | --- | +| `--name "$NAME" --hostname "$NAME"` | Prompt reads `root@$NAME` instead of a random container ID. | +| `$DATA_DIR/home/.bashrc` → `/root/.bashrc` | Per-host shell config, editable without a rebuild. | +| `$DATA_DIR/home/bash_aliases` → `/opt/dotfiles/bash_aliases` | Per-host aliases and [MCP env vars](mcp.md); sourced by `.bashrc`. | +| `$DATA_DIR/claude/credentials.json` → `/root/.claude/.credentials.json` | Claude Code OAuth token. | +| `$DATA_DIR/claude/claude.json` → `/root/.claude.json` | Claude Code account/onboarding state and user-scope MCP config. | +| `$WORKSPACE_DIR` → `/projects` | Your project workspace. | +| `$DATA_DIR/ssh` → `/root/.ssh` (read-only, only if the host dir exists) | Opt-in SSH access to other machines. | + +The login files are individual file mounts rather than a directory volume on +purpose: Claude Code saves them atomically (write a temp file, then +`rename()` over the target), and `rename()` onto a symlink replaces the +symlink instead of writing through it — which would silently break +persistence. A bind mount doesn't have that failure mode. + +## `connect` + +```bash +sudo ./claudaris connect +``` + +Runs `docker start "$NAME"` (a no-op if already running) and then +`docker exec -it "$NAME" tmux`. The in-container `tmux` is a wrapper that +attaches to the `main` session if it exists and creates it otherwise, so +`connect` always lands you in the same session. + +The explicit `docker start` matters because the container stops itself: the +entrypoint creates the `main` tmux session and exits once that session ends +(tmux defaults — a window closes when its shell exits, the session closes +with its last window). There's no `--restart` policy, so after a Ctrl+D the +container sits stopped until `connect` (or `start`) brings it back. + +## `remove` (aliases: `stop`, `rm`) + +Stops (ignoring errors if already stopped) and removes the container, so the +next `start` recreates it fresh — needed after a `build` to actually run the +new image, or after changing mounts. Nothing under `$DATA_DIR` or +`$WORKSPACE_DIR` is touched. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..d7392de --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,89 @@ +# Getting started + +Claudaris runs Claude Code inside an Arch Linux container, with your project +workspace bind-mounted in and your login persisted on the host. Everything is +driven by the [`./claudaris` executable](claudaris.md) at the repo root. + +## Prerequisites + +- Docker installed and running on the host. +- Root (or equivalent Docker) access — the examples below use `sudo`. +- A Claude account to log in with on first run. + +## 1. Configure + +From the repo root, run the interactive wizard and answer its prompts +(press enter to keep a default): + +```bash +./claudaris config +``` + +It asks for three values and writes them to `.env` (gitignored — every user +of a shared checkout keeps their own): + +| Variable | Default | Meaning | +| --------------- | ---------------------- | -------------------------------------------------------------- | +| `NAME` | `claudaris` | Image *and* container name. Pick something unique to you (e.g. `chris-claude`) if several people run containers from the same repo checkout. | +| `DATA_DIR` | `/data/$NAME` | Host directory for everything that must survive rebuilds: dotfiles, Claude login, optional SSH keys. | +| `WORKSPACE_DIR` | `/home/$USER/projects` | Host directory mounted as `/projects` inside the container — this is where Claude Code works. | + +Re-run `config` any time to change these. + +## 2. Build the image + +```bash +sudo ./claudaris build +``` + +The build always runs with `--no-cache --pull`, so every rebuild picks up the +latest Arch packages and the latest Claude Code release. Claude Code is +installed into the image itself (not a volume) — rebuilding is how you +upgrade it. + +## 3. Start and connect + +```bash +sudo ./claudaris start +sudo ./claudaris connect +``` + +`start` creates the container (seeding host-side copies of the dotfiles and +login files on first run); `connect` attaches you to its tmux session. You +land in a shell at `/projects` as `root@$NAME`. + +On first connect, run `claude` and log in. The two files that carry the login +(`~/.claude/.credentials.json` and `~/.claude.json`) are bind-mounted from +`$DATA_DIR/claude/` on the host, so you stay logged in across container +rebuilds and re-creations. + +## 4. Daily use + +- **Attach:** `sudo ./claudaris connect` — also restarts the container if it + stopped. The container exits on its own when the tmux session ends (last + shell exits via Ctrl+D, `exit`, or a crash); there is no Docker restart + policy, so `connect` bringing it back up is the normal flow. +- **Detach without stopping anything:** standard tmux detach + (`Ctrl+b d`). +- **Upgrade Claude Code / packages:** `sudo ./claudaris remove`, then + `sudo ./claudaris build` and `sudo ./claudaris start`. Login, dotfiles, and + MCP registration all survive this. + +## Customizing your shell + +`~/.bashrc` and the aliases file are bind-mounted single files living at +`$DATA_DIR/home/.bashrc` and `$DATA_DIR/home/bash_aliases` on the host. Edit +them there (or inside the container) and the changes persist across rebuilds — +no image change needed. They're seeded from `files/bashrc` and +`files/bash_aliases` the first time `start` runs and never overwritten after +that. + +This is also where you configure the [MCP servers](mcp.md): the bottom of +`bash_aliases` has commented `export` lines for the Gitea/n8n/InvokeAI URLs +and tokens. + +## Optional: SSH access to other machines + +To let the container SSH out, create `$DATA_DIR/ssh` on the host and populate +it with keys/config *before* running `start`. If the directory exists, it's +bind-mounted read-only at `/root/.ssh`. It's off by default. diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..4162926 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,78 @@ +# MCP servers + +The image ships three [MCP](https://modelcontextprotocol.io/) servers for +Claude Code, all speaking stdio: + +| Server | Name in `/mcp` | What it gives Claude | Source | +| --- | --- | --- | --- | +| Gitea | `gitea` | Repos, issues, PRs, releases on your Gitea instance | [gitea/gitea-mcp](https://gitea.com/gitea/gitea-mcp) (official, latest release binary) | +| n8n | `n8n` | n8n node documentation, plus workflow creation/management when the API env vars are set | [czlonkowski/n8n-mcp](https://github.com/czlonkowski/n8n-mcp) (npm) | +| InvokeAI | `invokeai` | Text-to-image, img2img, and upscaling against your InvokeAI instance | [coinstax/invokeai-mcp-server](https://github.com/coinstax/invokeai-mcp-server) (PyPI) | + +## How install and registration are split + +The Dockerfile only *installs* the servers (`/usr/local/bin/n8n-mcp`, +`/usr/local/bin/gitea-mcp`, and a `uv` tool venv for InvokeAI). +*Registration* happens in `files/entrypoint.sh` on every container start, +via `claude mcp add --scope user`. + +It has to work that way: user-scope MCP config lives in `~/.claude.json`, +which [`claudaris start`](claudaris.md#start) bind-mounts from the host — so +anything registered at build time would be shadowed by the mounted file. The +runtime registration writes into the mounted file instead, which also means +it persists across rebuilds like the login does. + +Registration is guarded per server: a server that's already present is left +alone, so hand edits to its config survive restarts. The flip side is that a +server deleted with `claude mcp remove` comes back on the next container +start — to change one permanently, edit its entry rather than removing it. + +## Pointing the servers at your instances + +The servers read their URLs and tokens from the environment Claude Code runs +in (Claude Code passes its environment through to stdio MCP servers). Set +them in your host-persisted aliases file, `$DATA_DIR/home/bash_aliases`, +where a commented template already exists at the bottom: + +```bash +export GITEA_HOST='https://gitea.example.com' +export GITEA_ACCESS_TOKEN='...' +export N8N_API_URL='https://n8n.example.com' # optional: enables workflow management tools +export N8N_API_KEY='...' +export INVOKEAI_BASE_URL='http://invokeai.example.com:9090' +``` + +Then start a new shell (or `source ~/.bashrc`) and launch `claude`. Because +the file lives on the host, tokens never end up in the image or the repo. If +your `bash_aliases` was seeded before the MCP servers existed, copy the +template block from `files/bash_aliases`. + +Notes: + +- **URLs must be reachable from inside the container.** `127.0.0.1` means + the container itself, not the host. Use the host's LAN address or service + hostname. +- **Gitea:** create the token in Gitea under *Settings → Applications → + Generate New Token*. +- **n8n:** without `N8N_API_URL`/`N8N_API_KEY` the server still works as a + node-documentation reference; the API pair unlocks creating and managing + workflows. Create the key in n8n under *Settings → n8n API*. +- **InvokeAI:** defaults to `http://127.0.0.1:9090` if unset. Upstream + hardcodes that URL; the Dockerfile patches the package so + `INVOKEAI_BASE_URL` is honored (and the build fails loudly if a future + upstream release breaks the patch). Registration uses + `python -m invokeai_mcp_server` because the package's console script is + broken upstream. + +## Checking it works + +Inside the container: + +```bash +claude mcp list # all three should show as configured +``` + +Or in a Claude Code session, run `/mcp` to see connection status, and try +something like *"list my Gitea repos"*. A server whose env vars are unset +will show as failed/erroring until you set them — the other servers are +unaffected. diff --git a/files/bash_aliases b/files/bash_aliases index 5ab690f..ad0adc0 100644 --- a/files/bash_aliases +++ b/files/bash_aliases @@ -23,4 +23,13 @@ alias tl='tmux list-sessions' alias gs='git status' alias btw='fastfetch' -alias cd='z' \ No newline at end of file +alias cd='z' + +# MCP servers (see Dockerfile / entrypoint.sh). Uncomment and point at your +# instances — Claude Code passes its environment through to the stdio +# servers, so exporting these here is all the configuration they need. +#export GITEA_HOST='https://gitea.example.com' +#export GITEA_ACCESS_TOKEN='changeme' +#export N8N_API_URL='https://n8n.example.com' # optional: enables workflow management tools +#export N8N_API_KEY='changeme' +#export INVOKEAI_BASE_URL='http://127.0.0.1:9090' \ No newline at end of file diff --git a/files/entrypoint.sh b/files/entrypoint.sh index 9141e16..7a08048 100644 --- a/files/entrypoint.sh +++ b/files/entrypoint.sh @@ -5,6 +5,26 @@ set -euo pipefail # which bind-mounts ~/.claude/.credentials.json and ~/.claude.json individually # from the host (see claudaris) — nothing to seed or symlink here. +# Register the MCP servers baked into the image (see Dockerfile) at user +# scope. Done here rather than at build time because user-scope MCP config +# lives in ~/.claude.json, which is bind-mounted from the host. Each server +# is only added if missing, so hand edits survive restarts — but a server +# removed via `claude mcp remove` comes back on the next container start. +[ -s /root/.claude.json ] || echo '{}' > /root/.claude.json +register_mcp() { + local name="$1"; shift + /root/.local/bin/claude mcp get "$name" >/dev/null 2>&1 || + /root/.local/bin/claude mcp add --scope user "$name" "$@" || true +} +register_mcp gitea -- /usr/local/bin/gitea-mcp -t stdio +register_mcp n8n --env MCP_MODE=stdio --env LOG_LEVEL=error \ + --env DISABLE_CONSOLE_OUTPUT=true -- /usr/local/bin/n8n-mcp +# Not the `invokeai-mcp-server` console script — it's broken upstream +# (calls the async main() without asyncio.run); `python -m` is the +# invocation upstream documents. +register_mcp invokeai -- \ + /root/.local/share/uv/tools/invokeai-mcp-server/bin/python -m invokeai_mcp_server + # Create the main tmux session if it does not exist. /usr/bin/tmux new-session -d -s main 2>/dev/null || true