Browse Source

Modernize MCP stack: streamable HTTP, multi-project code index

master
chodak166 5 days ago
parent
commit
79da59253f
  1. 10
      .crush.json
  2. 10
      .env.example
  3. 7
      .gitignore
  4. 24
      .roo/mcp.json
  5. 56
      AGENTS.md
  6. 86
      Dockerfile
  7. 268
      README.md
  8. 325
      add-project.sh
  9. 11
      codebase-index.config.json
  10. 148
      docker-compose.yml
  11. 71
      mcp_settings.json.roo_example
  12. 11
      opencode.json
  13. 38
      supergateway.config.jsonc

10
.crush.json

@ -0,0 +1,10 @@
{
"$schema": "https://charm.land/crush.json",
"mcp": {
"codebase-index": {
"type": "http",
"url": "http://localhost:8001/supergateway/mcp",
"timeout": 600000
}
}
}

10
.env.example

@ -0,0 +1,10 @@
# API key for the brave-search MCP server (https://brave.com/search/api/)
BRAVE_API_KEY=
# Optional: comma-separated list of brave search tools to expose
# BRAVE_MCP_ENABLED_TOOLS=
# Root directory that contains all repositories you want code-indexed.
# add-project.sh sets this automatically (default: $HOME/src); projects
# outside this directory need an extra volume mount (see README).
# PROJECTS_ROOT=/home/you/src

7
.gitignore vendored

@ -298,3 +298,10 @@ cython_debug/
# Built Visual Studio Code Extensions
*.vsix
# opencode codebase index data
.opencode/
# machine-specific project registration (managed by add-project.sh)
projects.env
supergateway.projects.jsonc

24
.roo/mcp.json

@ -0,0 +1,24 @@
{
"mcpServers": {
"fetch": {
"type": "streamableHttp",
"url": "http://localhost:8000/fetch/mcp",
"disabled": false
},
"context7": {
"type": "streamableHttp",
"url": "http://localhost:8000/context7/mcp",
"disabled": false
},
"brave-search": {
"type": "streamableHttp",
"url": "http://localhost:8000/brave-search/mcp",
"disabled": false
},
"codebase-index": {
"type": "streamableHttp",
"url": "http://localhost:8001/supergateway/mcp",
"disabled": false
}
}
}

56
AGENTS.md

@ -0,0 +1,56 @@
# AGENTS.md
This repo is not an application. It is a **local MCP server stack**: a docker-compose file
that runs AI-coding-tool helpers in Docker so opencode, crush, and VS Code plugins
(Cline/Roo/Kilo) can all share the same local services over Streamable HTTP. There is no
app source, no test suite, no linter — verification is manual (health endpoints, MCP
handshakes, `opencode mcp list`).
## Commands
```bash
cp .env.example .env # once; add BRAVE_API_KEY
./add-project.sh <repo> # register a project for code indexing (idempotent; starts stack)
./add-project.sh --remove <repo-or-slug>
docker compose up -d --build
docker compose ps / logs -f mcp / logs -f mcp-codebase-index
opencode reload && sleep 3 && opencode mcp list # acceptance check
```
## Architecture
Two services, one image (`Dockerfile`, `node:24-slim`: supergateway@4 +
open-codebase-index + context7/brave/`mcp-server-fetch` preinstalled, glibc because the
index ships NAPI binaries — do not switch the base to alpine):
- `mcp-gateway` (:8000) — supergateway `--config supergateway.config.jsonc`; one endpoint
per shared tool: `/fetch/mcp`, `/context7/mcp`, `/brave-search/mcp` (edit the jsonc to
add servers, then `docker compose restart mcp`).
- `mcp-codebase-index` (:8001) — supergateway `--config supergateway.projects.jsonc`
(GENERATED by add-project.sh from `projects.env`; do not hand-edit, re-run the script).
One endpoint per registered project: `/<slug>/mcp`. Mounts `PROJECTS_ROOT` (default
`~/src`, kept in `.env`) at the identical container path so index results return valid
host paths. Runs as `user: node` (uid 1000) so index data is host-user owned. Index
data lives in each project at `.opencode/index/` (structural mode: BM25 + call graph,
no embedding provider; ollama upgrade documented in README).
Per-project client configs (written into each project by add-project.sh): `opencode.json`,
`.crush.json`, `.roo/mcp.json`, each pointing at `http://localhost:8001/<slug>/mcp`.
Global client configs only carry the shared tools (fetch/context7/brave-search).
## Gotchas
- **Host docker networking is partly broken**: the default docker0 bridge has NO egress
(DNS/TCP fail); custom compose networks work fine. That is why builds use
`network: host` in the compose file — keep it.
- `opencode mcp list` can print "No MCP servers configured" for a few seconds after
`opencode reload` (daemon reconnects asynchronously). Retry before assuming breakage.
- After changing client configs, crush/opencode sessions must be restarted/reloaded;
VS Code plugins need the workspace reopened.
- `supergateway.projects.jsonc`, `projects.env`, `.env` are gitignored and
machine-specific. `docker-compose.override.yml` (if present) is the documented way to
mount extra project roots.
- supergateway runs stateful Streamable HTTP; each client session spawns its own stdio
server process (nothing idles; index data persists on disk between sessions).
- Add brief imperative commit messages; keep disabled blocks commented rather than
deleted.

86
Dockerfile

@ -1,72 +1,28 @@
FROM node:24.0.1-alpine
# Install Node.js package supergateway
RUN npm install -g supergateway
# Install system dependencies including build tools
RUN apk update && \
apk add --no-cache \
python3 \
py3-pip \
python3-dev \
bash \
screen \
rust \
cargo \
cmake \
make \
g++ \
clang \
llvm-dev \
clang-dev
### Python-based tools
# Install bindgen-cli required by aws-lc-sys (dependency of uvx)
RUN cargo install --force --locked bindgen-cli
# Add cargo bin to PATH
ENV PATH="/root/.cargo/bin:${PATH}"
ENV LIBCLANG_PATH="/usr/lib/llvm19/lib/"
FROM node:24-slim
# git: used by the codebase index for change detection / branch awareness
# python3: runtime for mcp-server-fetch
RUN apt-get update && \
apt-get install -y --no-install-recommends git python3 python3-venv ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Repos mounted from the host are owned by another uid; make git trust them all.
RUN git config --global --add safe.directory '*'
# Gateway + MCP servers. Global installs put each server's CLI on PATH,
# so supergateway config entries can reference them directly (no npx).
RUN npm install -g --omit=dev \
supergateway@4 \
open-codebase-index \
@upstash/context7-mcp \
@brave/brave-search-mcp-server \
@modelcontextprotocol/server-filesystem
# create venv
# Python-based MCP servers
RUN python3 -m venv /venv && \
echo "source /venv/bin/activate" >> /root/.profile && \
/venv/bin/pip install --no-cache-dir uvx uvenv mcp-server-fetch matplotlib numpy pyyaml
/venv/bin/pip install --no-cache-dir mcp-server-fetch
ENV PATH="/venv/bin:${PATH}"
### Node-based tools
# Clone, install dependencies, and build google-search-mcp-server
RUN apk update && apk add --no-cache git
RUN git clone https://github.com/ayush-rudani/google-search-mcp-server.git /opt/google-search-mcp-server && \
apk add --no-cache git && \
cd /opt/google-search-mcp-server && \
npm install && \
npm run build
# Install global dependencies for Node.js-based MCP servers
RUN npm install -g \
@modelcontextprotocol/server-google-maps \
@modelcontextprotocol/server-filesystem \
@upstash/context7-mcp \
@modelcontextprotocol/server-puppeteer \
@wonderwhy-er/desktop-commander@latest \
mcp-communicator-telegram \
mcp-cli-exec
RUN npx @wonderwhy-er/desktop-commander@latest setup
# chromium and x11 buffer (xvfb) for puppeteer
RUN apk add chromium
RUN apk add xvfb xvfb-run
# Clean up apk cache
RUN rm -rf /var/cache/apk/*
# Ensure Python user binaries are in PATH
ENV PATH="/root/.local/bin:${PATH}"
WORKDIR /app
EXPOSE 8000

268
README.md

@ -1,36 +1,268 @@
# mcp-supergateway
Dockerized [supergateway](https://www.npmjs.com/package/supergateway) image bundling several popular
MCP servers (fetch, google-maps, context7, puppeteer, excel, brave-search, ...) so each one can be
exposed as an SSE endpoint via docker-compose.
A local toolbox for AI coding assistants, running in Docker.
## Usage
AI coding tools — **opencode**, **crush**, VS Code's **Cline**, **Roo Code**, **Kilo Code** —
can all use extra abilities (fetch a web page, search the web, read library docs, search a
codebase) through a standard plug system called **MCP**. Every ability is a small server
program that normally has to be installed and run separately for each tool.
1. Copy `.env.example` (or create `.env`) and fill in the API keys the services you enable require.
2. Start the stack:
This repo runs those servers **once per machine, in Docker**, and gives each one a simple
local web address. Every AI tool on your computer then shares the same services — no
Node.js, no Python, nothing to install on the host.
## The idea in one picture
```
your AI tools this docker-compose stack
┌─────────────┐
│ opencode │───┐
│ crush │ │ http://localhost:8000/fetch/mcp ┌────────────────────┐
│ Cline │ ├───►http://localhost:8000/context7/mcp ──► │ mcp-gateway │
│ Roo / Kilo │ │ http://localhost:8000/brave-search/mcp │ (shared tools) │
└─────────────┘ │ └────────────────────┘
│ http://localhost:8001/supergateway/mcp
└───►http://localhost:8001/yazi/mcp ──► ┌────────────────────┐
http://localhost:8001/<project>/mcp │ mcp-codebase-index│
│ (one endpoint per │
│ code project) │
└────────────────────┘
```
Two containers do all the work:
- **`mcp-gateway`** (port 8000) — always-useful helpers, shared by every tool:
[web fetch](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch),
[library docs via context7](https://github.com/upstash/context7),
[web search via brave](https://github.com/brave/brave-search-mcp-server) (needs a free API key).
- **`mcp-codebase-index`** (port 8001) — a private search index for each of your code
projects, built with [open-codebase-index](https://github.com/Helweg/open-codebase-index).
It lets the AI find things in a large codebase quickly (keyword search, "where is this
defined", "who calls this") instead of reading files one by one. Every project gets its
own address: `http://localhost:8001/<project>/mcp`.
## Quick start (once per machine)
```bash
docker compose up -d
git clone <this repo> && cd supergateway
cp .env.example .env # put your BRAVE_API_KEY in it (https://brave.com/search/api/)
./add-project.sh . # registers this repo as the first indexed project and starts everything
```
`./add-project.sh .` builds the Docker image, starts both containers, indexes this
repository, and writes a few small config files into it (see below). Check that it worked:
```bash
docker compose ps # both containers should be "Up"
curl http://localhost:8000/healthz # -> ok
curl http://localhost:8001/healthz # -> ok
```
## Indexing your own projects — the one command you'll actually use
From this repo, run:
```bash
./add-project.sh ~/src/git/my-project
```
That single command:
1. remembers the project in `projects.env` (the list of indexed repositories),
2. gives it its own address: `http://localhost:8001/my-project/mcp`,
3. builds its search index (stored inside the project at `.opencode/index/` — disposable,
rebuilt anytime, gitignored),
4. writes three tiny config files **into the project**, each pointing at that address:
- `opencode.json` — opencode reads this automatically when started in that directory
- `.crush.json` — crush reads this automatically when started in that directory
- `.roo/mcp.json` — Roo Code / Cline / Kilo Code workspace config
(commit them — teammates who run this stack get the same behavior for free),
5. adds `.opencode/` to the project's `.gitignore`.
**Why this is nice:** you never re-configure your tools when switching projects. Open a
project → the AI tool automatically finds *that project's* index. Work on three projects
at the same time — each has its own address and its own index, nothing interferes.
Rules of the game:
- the project must be a **git repository** (the index is branch-aware and needs git),
- it must live under `PROJECTS_ROOT` (default `~/src`, set in `.env` — the script
configures this for you). Projects elsewhere need one extra volume mount, see
[Troubleshooting](#troubleshooting).
To stop indexing a project: `./add-project.sh --remove ~/src/git/my-project` (index data
and the small config files are left in the project; delete them if you want).
For "how does repo X use repo Y" questions you can also attach neighboring repositories
to the current project as **knowledge bases** (the `add_knowledge_base` tool) — they get
searched alongside the project's own code.
## Pointing your tools at the stack (once per tool)
The shared tools (port 8000) are configured once, globally. Per-project indexes (port
8001) need **no global configuration at all** — each project carries its own config files,
written by `add-project.sh`.
### opencode
`~/.config/opencode/opencode.jsonc`:
```jsonc
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"fetch": { "type": "remote", "url": "http://localhost:8000/fetch/mcp", "enabled": true },
"context7": { "type": "remote", "url": "http://localhost:8000/context7/mcp", "enabled": true },
"brave-search": { "type": "remote", "url": "http://localhost:8000/brave-search/mcp", "enabled": true }
}
}
```
Verify from any indexed project directory: `opencode mcp list` → four servers connected
(three above + `codebase-index` from the project's own `opencode.json`). If opencode was
already running there, run `opencode reload` first and give it a few seconds.
### crush
`~/.config/crush/crush.json`:
```json
{
"$schema": "https://charm.land/crush.json",
"mcp": {
"fetch": { "type": "http", "url": "http://localhost:8000/fetch/mcp" },
"context7": { "type": "http", "url": "http://localhost:8000/context7/mcp" },
"brave-search": { "type": "http", "url": "http://localhost:8000/brave-search/mcp" }
}
}
```
3. Point any MCP-compatible client at the exposed SSE endpoints (one per service/port defined in
`docker-compose.yml`).
crush picks up each project's `.crush.json` automatically (restart crush in that
directory).
## Client configuration
### Cline / Roo Code / Kilo Code (VS Code)
Example of connecting an SSE-capable MCP client:
Each indexed project already contains a workspace `.roo/mcp.json` (written by
`add-project.sh`), so opening the project in Roo Code just works. To also get the shared
tools everywhere, add them once to your plugin's global settings file:
| Plugin | Global config file (Linux) |
| --------- | --------------------------------------------------------------------------------------------------------- |
| Cline | `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` |
| Roo Code | `~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json` |
| Kilo Code | `~/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json` |
```json
{
"mcpServers": {
"fetch": {
"type": "sse",
"url": "http://localhost:8001/sse"
},
"brave-search": {
"type": "sse",
"url": "http://localhost:8010/sse"
"fetch": { "type": "streamableHttp", "url": "http://localhost:8000/fetch/mcp", "disabled": false },
"context7": { "type": "streamableHttp", "url": "http://localhost:8000/context7/mcp", "disabled": false },
"brave-search": { "type": "streamableHttp", "url": "http://localhost:8000/brave-search/mcp", "disabled": false }
}
}
```
## What the code index actually searches
By default the index runs in **structural mode**: keyword (BM25) search, symbol
definitions, call graphs, branch awareness — entirely **offline**, no AI provider needed.
This is usually plenty and it works out of the box.
To add **semantic search** ("find code by meaning"), plug in an embedding provider:
1. Start local Ollama embeddings: `docker compose --profile embeddings up -d ollama`, then
`docker compose exec ollama ollama pull nomic-embed-text`.
2. In `codebase-index.config.json` switch `"mode": "structural"` to `"hybrid"` and add:
```json
{
"embeddingProvider": "custom",
"customProvider": {
"baseUrl": "http://ollama:11434/v1",
"model": "nomic-embed-text",
"dimensions": 768
}
}
```
3. Rebuild each project's index:
```bash
docker compose exec mcp-codebase-index ocbi index --project ~/src/git/my-project --config /etc/codebase-index/config.json --force
```
(Or ask the AI to run the `index_codebase` / `index_status` tools — rebuilding and
refreshing indexes is something the assistants can do themselves through MCP.)
## Everyday commands
```bash
docker compose ps # status
docker compose logs -f mcp # shared-tools gateway logs
docker compose logs -f mcp-codebase-index # index gateway logs (per-project lines prefixed)
docker compose restart mcp mcp-codebase-index # after editing supergateway.config.jsonc / codebase-index.config.json
docker compose down # stop everything (index data survives, it lives in the projects)
```
## Adding or removing shared tools (port 8000)
Edit `supergateway.config.jsonc` — each entry becomes
`http://localhost:8000/<name>/mcp`. Any stdio MCP server works:
```jsonc
"memory": {
"stdio": "npx -y @modelcontextprotocol/server-memory"
}
```
Then `docker compose restart mcp`. Servers started via `npx` need outbound network from
the container on first use.
## How it works under the hood (curious readers)
- One image (built from `Dockerfile`, `node:24-slim`) runs both services:
[supergateway](https://github.com/supercorp-ai/supergateway) v4 plus the bundled MCP
servers, and [open-codebase-index](https://github.com/Helweg/open-codebase-index).
- Both gateways speak **Streamable HTTP** (the current MCP transport) with sessions, so
any modern client can connect.
- The index gateway mounts your `PROJECTS_ROOT` into the container **at the same path it
has on the host** — so every file path the index reports is a real path you (and your
AI tool) can open.
- Each project's index server process is started **per client session** and dies when the
client disconnects; the index data on disk persists, so reconnects are incremental.
Registering 20 projects does not mean 20 idle processes.
- Index data is stored per project in `.opencode/index/` — the same layout the native
open-codebase-index uses — and is safe to delete at any time (it gets rebuilt).
## Troubleshooting
```bash
curl http://localhost:8000/healthz # -> ok (shared tools gateway)
curl http://localhost:8001/healthz # -> ok (index gateway)
docker compose ps # both services "Up"
```
- **`opencode mcp list` says "No MCP servers configured" right after `opencode reload`** —
the background service reconnects asynchronously; wait 2–3 seconds and run it again.
- **Project lives outside `PROJECTS_ROOT`** — create a `docker-compose.override.yml` next
to `docker-compose.yml` in this repo, mounting the extra root at its own path:
```yaml
services:
mcp-codebase-index:
volumes:
- /opt/code:/opt/code
```
then run `docker compose up -d mcp-codebase-index` and register the project with
`./add-project.sh /opt/code/whatever`.
- **Docker build fails with `Temporary failure resolving ...`** — on this host the default
docker bridge has no internet access; builds already run with `network: host` in the
compose file. If it still fails, check your firewall.
- **`brave-search` tools fail** — missing/invalid `BRAVE_API_KEY` in `.env`.
- **Index seems stale** — re-run the project's index:
`./add-project.sh ~/src/git/my-project` (safe to repeat), or ask the AI to run
`index_codebase`.
- **Index data owned by root / permission errors** — the index container runs as uid
1000. If your user has a different uid, set `user: "<uid>:<gid>"` on the
`mcp-codebase-index` service in `docker-compose.yml`.

325
add-project.sh

@ -0,0 +1,325 @@
#!/usr/bin/env bash
#
# Register (or remove) a project repository with the local MCP codebase-index
# gateway managed by this repo's docker-compose stack.
#
# ./add-project.sh <path-to-repo> [slug]
# ./add-project.sh --remove <path-or-slug>
#
# What "register" does:
# 1. records the project in projects.env (slug=path) - the list of indexed repos
# 2. regenerates supergateway.projects.jsonc - one MCP endpoint per project:
# http://localhost:8001/<slug>/mcp
# 3. recreates the mcp-codebase-index container with the updated config
# 4. builds the initial code index (keyword + call graph, works offline)
# 5. writes AI-client config snippets INTO the project repo so every tool
# finds its index automatically when opened in that directory:
# opencode.json (opencode project config)
# .crush.json (crush project config)
# .roo/mcp.json (Roo Code / Cline / Kilo Code workspace config)
# and adds ".opencode/" to the project's .gitignore (index data lives there)
#
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$REPO_DIR"
PROJECTS_ENV="$REPO_DIR/projects.env"
GENERATED_CONFIG="$REPO_DIR/supergateway.projects.jsonc"
GATEWAY_PORT=8001
INDEX_CONFIG=/etc/codebase-index/config.json
SERVICE=mcp-codebase-index
die() { echo "ERROR: $*" >&2; exit 1; }
note() { echo; echo "== $*"; }
http_get() {
if command -v curl >/dev/null 2>&1; then
curl -fsS -m 3 "$1" 2>/dev/null || true
elif command -v wget >/dev/null 2>&1; then
wget -qO- -T 3 "$1" 2>/dev/null || true
fi
}
slugify() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//'
}
json_escape() { sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' <<<"$1"; }
# ---------------------------------------------------------------------------
# projects.env helpers
# ---------------------------------------------------------------------------
env_get() { # env_get <path> -> slug (or empty)
local p line key val
p="$1"
[[ -f "$PROJECTS_ENV" ]] || return 0
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" == \#* ]] && continue
key="${line%%=*}"; val="${line#*=}"
[[ "$val" == "$p" ]] && { echo "$key"; return 0; }
done < "$PROJECTS_ENV"
return 0
}
env_set() { # env_set <slug> <path> (idempotent, keeps order)
local slug="$1" path="$2" tmp line key val
tmp="$(mktemp)"
[[ -f "$PROJECTS_ENV" ]] || touch "$PROJECTS_ENV"
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ -z "$line" || "$line" == \#* ]]; then printf '%s\n' "$line" >> "$tmp"; continue; fi
key="${line%%=*}"; val="${line#*=}"
# drop lines that match either the same path or the same slug
[[ "$val" == "$path" || "$key" == "$slug" ]] && continue
printf '%s\n' "$line" >> "$tmp"
done < "$PROJECTS_ENV"
printf '%s=%s\n' "$slug" "$path" >> "$tmp"
mv "$tmp" "$PROJECTS_ENV"
}
env_del() { # env_del <slug-or-path>
local target="$1" tmp line key val
tmp="$(mktemp)"
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ -z "$line" || "$line" == \#* ]]; then printf '%s\n' "$line" >> "$tmp"; continue; fi
key="${line%%=*}"; val="${line#*=}"
[[ "$val" == "$target" || "$key" == "$target" ]] && continue
printf '%s\n' "$line" >> "$tmp"
done < "$PROJECTS_ENV"
mv "$tmp" "$PROJECTS_ENV"
}
regenerate_config() {
local line slug path first=1
{
echo "// GENERATED by add-project.sh from projects.env - do not edit by hand."
echo "// Each project is served at http://localhost:${GATEWAY_PORT}/<slug>/mcp"
echo "// Re-run ./add-project.sh after editing projects.env."
echo "{"
echo " \"outputTransport\": \"streamableHttp\","
echo " \"stateful\": true,"
echo " \"sessionTimeout\": 3600000,"
echo " \"mcpServers\": {"
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" == \#* ]] && continue
slug="${line%%=*}"; path="${line#*=}"
[[ -z "$slug" || -z "$path" ]] && continue
[[ $first -eq 0 ]] && echo ","
first=0
printf ' "%s": { "stdio": "open-codebase-index-mcp --config %s --project %s" }' \
"$(json_escape "$slug")" "$INDEX_CONFIG" "$(json_escape "$path")"
done < "$PROJECTS_ENV"
echo ""
echo " }"
echo "}"
} > "$GENERATED_CONFIG"
}
# ---------------------------------------------------------------------------
# .env bootstrap
# ---------------------------------------------------------------------------
ensure_env() {
if [[ ! -f "$REPO_DIR/.env" ]]; then
cp "$REPO_DIR/.env.example" "$REPO_DIR/.env" 2>/dev/null || touch "$REPO_DIR/.env"
echo "Created .env (remember to set BRAVE_API_KEY there)."
fi
if ! grep -qE '^PROJECTS_ROOT=' "$REPO_DIR/.env"; then
printf 'PROJECTS_ROOT=%s\n' "${PROJECTS_ROOT:-$HOME/src}" >> "$REPO_DIR/.env"
echo "Set PROJECTS_ROOT=${PROJECTS_ROOT:-$HOME/src} in .env"
fi
PROJECTS_ROOT="$(grep -E '^PROJECTS_ROOT=' "$REPO_DIR/.env" | tail -1 | cut -d= -f2-)"
[[ -d "$PROJECTS_ROOT" ]] || die "PROJECTS_ROOT=$PROJECTS_ROOT does not exist (edit .env)."
}
# ---------------------------------------------------------------------------
# client config snippets written into the target project
# ---------------------------------------------------------------------------
write_client_config() { # write_client_config <file> <fresh-content> <jq-merge-expr> <url>
local file="$1" fresh="$2" merge="$3" url="$4"
mkdir -p "$(dirname "$file")"
if [[ -f "$file" ]]; then
if command -v jq >/dev/null 2>&1; then
if jq --arg u "$url" "$merge" "$file" > "$file.tmp" 2>/dev/null; then
mv "$file.tmp" "$file"
echo " updated $file"
else
rm -f "$file.tmp"
echo " NOTE: could not merge into $file (invalid JSON?) - add manually:"
echo " codebase-index -> $url"
fi
else
echo " NOTE: $file exists and jq is not installed - merge manually:"
echo " codebase-index -> $url"
fi
else
printf '%s\n' "$fresh" > "$file"
echo " created $file"
fi
}
write_project_configs() { # write_project_configs <project-dir> <slug>
local dir="$1" slug="$2"
local url="http://localhost:${GATEWAY_PORT}/${slug}/mcp"
write_client_config "$dir/opencode.json" \
"{
\"\$schema\": \"https://opencode.ai/config.json\",
\"mcp\": {
\"codebase-index\": {
\"type\": \"remote\",
\"url\": \"$url\",
\"enabled\": true,
\"timeout\": 600000
}
}
}" \
'.mcp = ((.mcp // {}) | ."codebase-index" = {type:"remote", url:$u, enabled:true, timeout:600000})' \
"$url"
write_client_config "$dir/.crush.json" \
"{
\"\$schema\": \"https://charm.land/crush.json\",
\"mcp\": {
\"codebase-index\": {
\"type\": \"http\",
\"url\": \"$url\",
\"timeout\": 600000
}
}
}" \
'.mcp = ((.mcp // {}) | ."codebase-index" = {type:"http", url:$u, timeout:600000})' \
"$url"
write_client_config "$dir/.roo/mcp.json" \
"{
\"mcpServers\": {
\"codebase-index\": {
\"type\": \"streamableHttp\",
\"url\": \"$url\",
\"disabled\": false
}
}
}" \
'.mcpServers = ((.mcpServers // {}) | ."codebase-index" = {type:"streamableHttp", url:$u, disabled:false})' \
"$url"
if [[ -f "$dir/.gitignore" ]] && ! grep -qE '^\.opencode/?$' "$dir/.gitignore"; then
printf '\n# codebase index data (dockerized open-codebase-index)\n.opencode/\n' >> "$dir/.gitignore"
echo " updated .gitignore (.opencode/)"
elif [[ ! -f "$dir/.gitignore" ]]; then
printf '.opencode/\n' > "$dir/.gitignore"
echo " created .gitignore (.opencode/)"
fi
}
# ---------------------------------------------------------------------------
# commands
# ---------------------------------------------------------------------------
cmd_remove() {
local target="${1:-}"
[[ -n "$target" ]] || die "usage: $0 --remove <path-or-slug>"
if [[ -d "$target" ]]; then target="$(realpath -e "$target")"; fi
[[ -f "$PROJECTS_ENV" ]] || die "no projects registered yet"
env_del "$target"
regenerate_config
note "Recreating the index gateway"
docker compose up -d --build >/dev/null
echo "Removed '$target' from the index gateway."
echo "Left in place (safe to delete by hand): client config snippets in the"
echo "project (opencode.json / .crush.json / .roo/mcp.json) and its .opencode/ index data."
}
cmd_add() {
local target="${1:-}" slug="${2:-}"
[[ -n "$target" ]] || die "usage: $0 <path-to-repo> [slug]"
target="$(realpath -e "$target")" || die "path not found: $target"
[[ -d "$target/.git" ]] || die "not a git repository: $target (the index is branch-aware and needs git)"
ensure_env
[[ "$target" == "$PROJECTS_ROOT" || "$target" == "$PROJECTS_ROOT"/* ]] || {
die "$target is not under PROJECTS_ROOT=$PROJECTS_ROOT.
Either move the project under that directory, or expose its parent too:
create a docker-compose.override.yml next to this compose file containing
services:
mcp-codebase-index:
volumes:
- /extra/root:/extra/root
then add 'PROJECTS_ROOT' style entries by hand in projects.env and re-run this script."
}
# slug: explicit, or from directory name, deduped against existing entries
if [[ -z "$slug" ]]; then
slug="$(slugify "$(basename "$target")")"
[[ -n "$slug" ]] || slug="project"
if [[ -n "$(env_get_other_slug "$slug" "$target")" ]]; then
slug="$(slugify "$(basename "$(dirname "$target")")")-$slug"
fi
else
slug="$(slugify "$slug")"
[[ -n "$slug" ]] || die "invalid slug"
fi
local existing; existing="$(env_get "$target")"
if [[ -n "$existing" && "$existing" != "$slug" ]]; then
echo "Project already registered as '$existing' - renaming to '$slug'."
elif [[ -n "$existing" ]]; then
echo "Project already registered as '$existing' - refreshing."
fi
env_set "$slug" "$target"
regenerate_config
note "Starting the stack (first build can take a few minutes)"
local log; log="$(mktemp)"
if ! docker compose up -d --build >"$log" 2>&1; then
tail -n 40 "$log"
die "docker compose up failed"
fi
rm -f "$log"
echo "Waiting for the index gateway..."
local i ok=""
for i in $(seq 1 30); do
[[ "$(http_get "http://localhost:${GATEWAY_PORT}/healthz")" == "ok" ]] && { ok=1; break; }
sleep 1
done
[[ -n "$ok" ]] || die "index gateway did not become healthy (see: docker compose logs $SERVICE)"
note "Building the code index for $target (keyword + call graph, offline)"
docker compose exec -T "$SERVICE" ocbi index --project "$target" --config "$INDEX_CONFIG"
note "Writing AI-client config snippets into $target"
write_project_configs "$target" "$slug"
note "Done"
echo " MCP endpoint : http://localhost:${GATEWAY_PORT}/${slug}/mcp"
echo
echo "Open $target with your AI tool and it picks the config up automatically:"
echo " opencode / crush : (re)start in that directory, or run 'opencode reload'"
echo " VS Code (Cline/Roo/Kilo): reopen the workspace"
}
env_get_other_slug() { # env_get_other_slug <slug> <path> -> prints slug if taken by another path
local slug="$1" path="$2" line key val
[[ -f "$PROJECTS_ENV" ]] || return 0
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" == \#* ]] && continue
key="${line%%=*}"; val="${line#*=}"
[[ "$key" == "$slug" && "$val" != "$path" ]] && { echo "$key"; return 0; }
done < "$PROJECTS_ENV"
return 0
}
case "${1:-}" in
--remove|-r) shift; cmd_remove "$@" ;;
-h|--help) sed -n '2,20p' "$0" ;;
*) cmd_add "$@" ;;
esac

11
codebase-index.config.json

@ -0,0 +1,11 @@
{
"scope": "project",
"indexing": {
"mode": "structural",
"autoIndex": false,
"watchFiles": true
},
"search": {
"maxResults": 20
}
}

148
docker-compose.yml

@ -1,107 +1,75 @@
version: '3.8'
name: mcp-supergateway
# NOTE: remember to fill your .env
# NOTE: copy this file to .env and fill in the keys for the servers you enable.
services:
mcp-fetch:
image: chodak166/mcp-supergateway
container_name: mcp-fetch
# Multi-server MCP gateway (supergateway --config).
# One port, one Streamable HTTP endpoint per server: http://localhost:8000/<name>/mcp
# Servers are defined in supergateway.config.jsonc.
mcp:
build:
context: .
network: host
image: chodak166/mcp-supergateway:latest
container_name: mcp-gateway
ports:
- "8001:8000"
command: --stdio "mcp-server-fetch --ignore-robots-txt"
restart: unless-stopped
# mcp-google-search:
# image: chodak166/mcp-supergateway
# ports:
# - "8002:8000"
# env_file:
# - .env
# command: --stdio "node /opt/google-search-mcp-server/dist/index.js"
# restart: unless-stopped
mcp-google-maps:
image: chodak166/mcp-supergateway
ports:
- "8003:8000"
- "8000:8000"
env_file:
- .env
command: --stdio "npx -y @modelcontextprotocol/server-google-maps"
restart: unless-stopped
mcp-context7:
image: chodak166/mcp-supergateway
ports:
- "8004:8000"
env_file:
- .env
command: --stdio "npx -y @upstash/context7-mcp@latest"
volumes:
- ./supergateway.config.jsonc:/etc/supergateway/servers.jsonc:ro
command:
- --config
- /etc/supergateway/servers.jsonc
- --healthEndpoint
- /healthz
restart: unless-stopped
# mcp-commander:
# image: chodak166/mcp-supergateway
# ports:
# - "8005:8000"
# volumes:
# - ./volumes/mcp/workspace:/app
# command: --stdio "npx -y @wonderwhy-er/desktop-commander@latest"
# restart: unless-stopped
mcp-puppeteer:
image: chodak166/mcp-supergateway
# open-codebase-index (https://github.com/Helweg/open-codebase-index)
# Serves ONE endpoint per registered project: http://localhost:8001/<slug>/mcp
# Projects are registered with ./add-project.sh (list kept in projects.env,
# gateway config generated into supergateway.projects.jsonc). All projects must
# live under PROJECTS_ROOT (set in .env by add-project.sh), which is mounted at
# its own host path so tool results return paths valid on the host.
# Each project's index data lives in <project>/.opencode/index/.
# The index server process is started per client session; nothing idles.
mcp-codebase-index:
build:
context: .
network: host
image: chodak166/mcp-supergateway:latest
container_name: mcp-codebase-index
# Run as the image's `node` user (uid 1000) so index data written into the
# mounted repos is owned by the host user.
user: node
ports:
- "8006:8000"
command: --stdio "xvfb-run --server-args='-screen 0 1920x1080x24' -a npx -y @modelcontextprotocol/server-puppeteer"
- "8001:8000"
volumes:
- ${PROJECTS_ROOT:?run ./add-project.sh first}:${PROJECTS_ROOT:?run ./add-project.sh first}
- ./codebase-index.config.json:/etc/codebase-index/config.json:ro
- ./supergateway.projects.jsonc:/etc/supergateway/projects.jsonc:ro
command:
- --config
- /etc/supergateway/projects.jsonc
- --healthEndpoint
- /healthz
restart: unless-stopped
env_file:
- .env
# mcp-telegram:
# image: chodak166/mcp-supergateway
# Optional: local embeddings for semantic (hybrid) codebase indexing.
# Start with: docker compose --profile embeddings up -d
# Then switch indexing.mode to "hybrid" in codebase-index.config.json and set
# embeddingProvider "custom" with baseUrl http://ollama:11434/v1,
# model nomic-embed-text, dimensions 768, and run:
# docker compose exec mcp-codebase-index ocbi index --project $PWD --config /etc/codebase-index/config.json --force
# ollama:
# image: ollama/ollama:latest
# profiles: ["embeddings"]
# ports:
# - "8007:8000"
# env_file:
# - .env
# working_dir: /app
# - "11434:11434"
# volumes:
# - ./:/app
# command: --stdio "npx -y mcptelegram"
# - ollama:/root/.ollama
# restart: unless-stopped
# mcp-filesystem:
# image: chodak166/mcp-supergateway
# ports:
# - "8008:8000"
# env_file:
# - .env
# working_dir: /app
# volumes:
# - ./:/app
# command: --stdio "npx -y @modelcontextprotocol/server-filesystem /app"
# restart: unless-stopped
mcp-excel:
image: chodak166/mcp-supergateway
ports:
- "8009:8000"
volumes:
- ~/src/mcp-excel:/app
command: --stdio "npx -y @negokaz/excel-mcp-server"
restart: unless-stopped
#
# mcp-inspector:
# image: chodak166/mcp-supergateway
# container_name: mcp-inspector
# network_mode: host
# command: --stdio "npx -y @modelcontextprotocol/inspector"
# restart: unless-stopped
mcp-brave-search:
image: chodak166/mcp-supergateway
ports:
- "8010:8000"
env_file:
- .env
command: --stdio "npx -y @brave/brave-search-mcp-server"
restart: unless-stopped
# ollama:

71
mcp_settings.json.roo_example

@ -1,71 +0,0 @@
{
"mcpServers": {
"fetch": {
"type": "sse",
"url": "http://localhost:8001/sse",
"disabled": false,
"alwaysAllow": [
"fetch"
]
},
"google-search": {
"url": "http://localhost:8002/sse",
"disabled": true,
"alwaysAllow": [
"google_search"
]
},
"telegram": {
"url": "http://localhost:8003/sse",
"disabled": true,
"alwaysAllow": []
},
"context7": {
"url": "http://localhost:8004/sse",
"disabled": false,
"alwaysAllow": []
},
"excel": {
"url": "http://localhost:8009/sse",
"disabled": true,
"alwaysAllow": []
},
"brave-search": {
"url": "http://localhost:8010/sse",
"disabled": false,
"alwaysAllow": [
"brave_web_search"
]
},
"blender-mcp": {
"disabled": true,
"command": "uvx",
"args": [
"/home/chodak/src/git/blender-mcp"
],
"alwaysAllow": [
"get_scene_info",
"get_object_info",
"execute_blender_code"
]
},
"gimp": {
"command": "uv",
"args": [
"run",
"--directory",
"/home/chodak/src/git/gimp-mcp",
"gimp_mcp_server.py"
],
"alwaysAllow": [
"get_gimp_info"
],
"disabled": true
},
"cloudflare-docs": {
"type": "sse",
"url": "https://docs.mcp.cloudflare.com/sse",
"disabled": true
}
}
}

11
opencode.json

@ -0,0 +1,11 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"codebase-index": {
"type": "remote",
"url": "http://localhost:8001/supergateway/mcp",
"enabled": true,
"timeout": 600000
}
}
}

38
supergateway.config.jsonc

@ -0,0 +1,38 @@
// Supergateway multi-server configuration.
//
// Every server below is served over Streamable HTTP on the gateway port:
// http://localhost:8000/<name>/mcp
//
// Restart the gateway after changes: docker compose restart mcp
// Env vars (${VAR}) are read from the container environment (see .env / env_file).
{
"outputTransport": "streamableHttp",
"stateful": true,
"sessionTimeout": 3600000,
"mcpServers": {
"fetch": {
"stdio": "mcp-server-fetch --ignore-robots-txt"
},
"context7": {
"stdio": "context7-mcp"
},
"brave-search": {
"stdio": "brave-search-mcp-server",
"env": {
"BRAVE_API_KEY": "${BRAVE_API_KEY:-}"
}
}
// Examples (uncomment and adjust):
//
// "memory": {
// "stdio": "npx -y @modelcontextprotocol/server-memory"
// },
// "filesystem": {
// "stdio": "npx -y @modelcontextprotocol/server-filesystem /absolute/path"
// },
// "everything": {
// "stdio": "npx -y @modelcontextprotocol/server-everything"
// }
}
}
Loading…
Cancel
Save