13 changed files with 824 additions and 247 deletions
@ -0,0 +1,10 @@
|
||||
{ |
||||
"$schema": "https://charm.land/crush.json", |
||||
"mcp": { |
||||
"codebase-index": { |
||||
"type": "http", |
||||
"url": "http://localhost:8001/supergateway/mcp", |
||||
"timeout": 600000 |
||||
} |
||||
} |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
} |
||||
} |
||||
} |
||||
@ -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. |
||||
@ -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`. |
||||
|
||||
@ -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 |
||||
@ -0,0 +1,11 @@
|
||||
{ |
||||
"scope": "project", |
||||
"indexing": { |
||||
"mode": "structural", |
||||
"autoIndex": false, |
||||
"watchFiles": true |
||||
}, |
||||
"search": { |
||||
"maxResults": 20 |
||||
} |
||||
} |
||||
@ -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: |
||||
|
||||
@ -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 |
||||
} |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue