diff --git a/.crush.json b/.crush.json new file mode 100644 index 0000000..1d29e78 --- /dev/null +++ b/.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 + } + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b29a06d --- /dev/null +++ b/.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 diff --git a/.gitignore b/.gitignore index a073752..35c2e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.roo/mcp.json b/.roo/mcp.json new file mode 100644 index 0000000..d173c56 --- /dev/null +++ b/.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 + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..565e5ab --- /dev/null +++ b/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 # register a project for code indexing (idempotent; starts stack) +./add-project.sh --remove +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: `//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//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. diff --git a/Dockerfile b/Dockerfile index b0feda0..e69595c 100644 --- a/Dockerfile +++ b/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/" - -# create venv +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 + +# 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 diff --git a/README.md b/README.md index b4a1ba1..baa96a8 100644 --- a/README.md +++ b/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//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//mcp`. + +## Quick start (once per machine) + +```bash +git clone && 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 up -d +docker compose ps # both containers should be "Up" +curl http://localhost:8000/healthz # -> ok +curl http://localhost:8001/healthz # -> ok ``` -3. Point any MCP-compatible client at the exposed SSE endpoints (one per service/port defined in - `docker-compose.yml`). +## 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. -## Client configuration +Rules of the game: -Example of connecting an SSE-capable MCP client: +- 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" } + } +} +``` + +crush picks up each project's `.crush.json` automatically (restart crush in that +directory). + +### Cline / Roo Code / Kilo Code (VS Code) + +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//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: ":"` on the + `mcp-codebase-index` service in `docker-compose.yml`. diff --git a/add-project.sh b/add-project.sh new file mode 100755 index 0000000..7926b04 --- /dev/null +++ b/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 [slug] +# ./add-project.sh --remove +# +# 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//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 -> 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 (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 + 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}//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 + 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 + 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 " + 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 [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 -> 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 diff --git a/codebase-index.config.json b/codebase-index.config.json new file mode 100644 index 0000000..811d168 --- /dev/null +++ b/codebase-index.config.json @@ -0,0 +1,11 @@ +{ + "scope": "project", + "indexing": { + "mode": "structural", + "autoIndex": false, + "watchFiles": true + }, + "search": { + "maxResults": 20 + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 4bd217d..1f8256d 100644 --- a/docker-compose.yml +++ b/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//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//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 /.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 - # ports: - # - "8007:8000" - # env_file: - # - .env - # working_dir: /app - # volumes: - # - ./:/app - # command: --stdio "npx -y mcptelegram" - # restart: unless-stopped - # mcp-filesystem: - # 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: - # - "8008:8000" - # env_file: - # - .env - # working_dir: /app + # - "11434:11434" # volumes: - # - ./:/app - # command: --stdio "npx -y @modelcontextprotocol/server-filesystem /app" + # - ollama:/root/.ollama # 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 +# volumes: +# ollama: diff --git a/mcp_settings.json.roo_example b/mcp_settings.json.roo_example deleted file mode 100644 index 9ded61c..0000000 --- a/mcp_settings.json.roo_example +++ /dev/null @@ -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 - } - } -} diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..d83e139 --- /dev/null +++ b/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 + } + } +} diff --git a/supergateway.config.jsonc b/supergateway.config.jsonc new file mode 100644 index 0000000..79bc52c --- /dev/null +++ b/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//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" + // } + } +}