added gitea, n8n and invokeai MCP servers, plus docs/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
code
2026-07-14 20:47:29 +00:00
parent 06fcf6bab7
commit bcd7e012ba
9 changed files with 375 additions and 1 deletions
+24
View File
@@ -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
+26
View File
@@ -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
+18
View File
@@ -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
+14
View File
@@ -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.
+96
View File
@@ -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 <command>
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.
+89
View File
@@ -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.
+78
View File
@@ -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.
+10 -1
View File
@@ -23,4 +23,13 @@ alias tl='tmux list-sessions'
alias gs='git status'
alias btw='fastfetch'
alias cd='z'
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'
+20
View File
@@ -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