Skip to main content

elf-mcp-gateway

Elf Express MCPJungle gateway 部署與註冊規範:vendored fork 從源碼 build、docker compose(內建 Postgres + gateway + docs-mcp + 一次性 registrar,零設定可起)、 書本註冊設定由產生器輸出、mcpjungle/registry/*.json 註冊檔格式、registrar 驗證 / 重試 / 冪等、REGISTER_LIST、工具群組、Dockhand / pull 部署、nginx 反向代理(SSE)、 GHCR 發佈(GHCR_PAT fallback)、SHA 釘選的 GitHub Actions、環境變數與安全(SERVER_MODE、bearer token、dashboard 不外露)。 當任務涉及 mcpjungle/ 目錄、compose*.yaml(CI 只允許根目錄 compose.yaml / compose.pull.yaml / compose.attach.yaml 三份,ci.yml:64-66)、 mcpjungle/registry/、registrar.sh、gen-book-configs.mjs、gen-group-configs.mjs、registry/*.json、registry/groups/*.json、 MCPJungle 設定或升級、nginx.example.conf、docker-publish.yml / ci.yml,或排查「gateway 看不到工具 / 註冊失敗 / 連不上」時觸發。

Source facts

Repository
elf-express/skillshare-source
Last source activity
October 19, 2026 at 18:08
Detected SKILL.md language
Chinese
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
9 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
elf-mcp-gateway
description
Elf Express MCPJungle gateway 部署與註冊規範:vendored fork 從源碼 build、docker compose(內建 Postgres + gateway + docs-mcp + 一次性 registrar,零設定可起)、 書本註冊設定由產生器輸出、mcpjungle/registry/*.json 註冊檔格式、registrar 驗證 / 重試 / 冪等、REGISTER_LIST、工具群組、Dockhand / pull 部署、nginx 反向代理(SSE)、 GHCR 發佈(GHCR_PAT fallback)、SHA 釘選的 GitHub Actions、環境變數與安全(SERVER_MODE、bearer token、dashboard 不外露)。 當任務涉及 mcpjungle/ 目錄、compose*.yaml(CI 只允許根目錄 compose.yaml / compose.pull.yaml / compose.attach.yaml 三份,ci.yml:64-66)、 mcpjungle/registry/、registrar.sh、gen-book-configs.mjs、gen-group-configs.mjs、registry/*.json、registry/groups/*.json、 MCPJungle 設定或升級、nginx.example.conf、docker-publish.yml / ci.yml,或排查「gateway 看不到工具 / 註冊失敗 / 連不上」時觸發。
metadata
{"version":"1.3.1","owner":"Elf Express"}
# MCPJungle Gateway 部署與註冊(Elf Express) > 參考實作:`elf-express/mcp-library` 的 `mcpjungle/`、根目錄 `compose.yaml` / `compose.pull.yaml` / `compose.attach.yaml`、`.github/workflows/`。行號以 `origin/release` @ `73b8206` 為準。 > 架構:**AI 工具只連一個 MCPJungle**;gateway 後面掛 9 個 server:`docs-mcp-server` 的 6 份語料(`sqlsugar-zh-tw` / `fc-zh-tw` / `nginx-en` / `opnsense-en` / `opnsense-zh-tw` / `stalwart-en`,每本書一個 server), > 加上官方 stdio server `filesystem` / `fetch` / `time`;registrar 另建工具群組 `claude-tools`(全部書本 + `fetch` + `time`,端點 `/v0/groups/claude-tools/mcp`)(`README.md:62`)。 相關 skill: - **`elf-mcp-knowledge`** — 新增知識庫的主流程(本 skill 是其中「註冊 / 部署 / 驗證」三步的細節) - `elf-mcp-server` — server 本身的寫法 - `elf-cicd-docker`(映像規範)、`elf-cicd-review`(PR AI review)、`elf-cicd-versioning`(版號 / tag)、`elf-stack` --- ## 1. 何時使用 - 部署 / 重佈 gateway stack(`compose.yaml` / `compose.pull.yaml` / `compose.attach.yaml`);換 DB、換 port、改網路 - 新增 / 修改書本(`corpus.json`,由 `gen-book-configs.mjs` 產生註冊設定)、`mcpjungle/registry/*.json`、`registry/optional/*.json`、`REGISTER_LIST`、tool group(`registry/groups/*.json`) - 升級 vendored MCPJungle fork(`mcpjungle/gateway/`)、改 `mcpjungle/gateway/Dockerfile.fullbuild` - 對外開放(nginx / HTTPS / token) - 改 `.github/workflows/docker-publish.yml`、`.github/workflows/ci.yml`、`.github/dependabot.yml` - 排查:registrar 失敗(含註冊前驗證 exit 1)、23505 重名、stdio server 起不來、SSE 收不到回應 --- ## 2. 固定規則 ### 拓撲與 compose 1. **MUST** 以根目錄 `compose.yaml` 為正式入口(單一檔案,沒有 `include`);**四個**服務: `postgres`(`compose.yaml:10-25`,`postgres:16-alpine`,**不對外開 port**,volume `pgdata`)、 `mcpjungle`(`:27-48`,`${MCPJUNGLE_HOST_PORT:-18800}:8080`)、`docs-mcp-server`(`:50-69`,:5690,只在內網)、`registrar`(`:72-91`,一次性,`restart: "no"`)。 container 名由 compose 依 project 產生(如 `mcp-library-mcpjungle-1`),不寫死;進容器用 `docker compose exec <service>`(`AGENTS.md:47`)。 docs-mcp 的 build 是 `context: ./mcpjungle` 加 `dockerfile: docs-mcp-server/Dockerfile`(`compose.yaml:51-53`;`a196fdf` 把 server 與語料搬進 `mcpjungle/`)。 2. **MUST** 維持「**DB 內建、零設定可起**」:stack 自帶 `postgres` service,`mcpjungle` 的 `DATABASE_URL` 預設值是 `postgres://mcpjungle:mcpjungle@postgres:5432/mcpjungle?sslmode=disable`(`compose.yaml:38`;**服務名** `postgres`,在 `mcpjungl` 網路內解析), gateway 以 `depends_on: postgres: condition: service_healthy` 等 DB 就緒(`:33-35`)。`docker compose up -d --build` 不必先 `cp .env.example .env`。 WHY:`878add4`(feat(deploy): bundle Postgres into the stack, drop shared-db)——這條**取代**了早先 `6d25d6f` 的「DB 走外部 IP、stack 不含 Postgres」決策,`shared-db/` 目錄已不存在。 2a. **MUST** 只有要接「既有的外部 DB」才在 `.env` 設 `MCPJUNGLE_DATABASE_URL` 覆寫(host 填該 DB 的 **IP**、port 填它實際監聽的 port); 內建那顆容器仍會起,可 `docker compose stop postgres` 關掉(`.env.example:17-21`)。 2b. **MUST** 改內建 DB 密碼時 **`POSTGRES_PASSWORD` 與 `MCPJUNGLE_DATABASE_URL` 兩處同步改**,否則 gateway 連不上 DB(`compose.yaml:9`)。 `pgdata` volume 的資料 `docker compose down` 不會刪,`down -v` 才會(`compose.yaml:94`)——**MUST NOT** 在正式環境用 `down -v` 當重啟手段。 2c. 網路 `mcpjungl` 由 stack **自建**(實際名 `<project>_mcpjungl`,`compose.yaml:96-98`),不必 `docker network create`。 原本的自包含端到端測試 compose 已在 `e563c9e` 刪除(待確認 8);並存測試改用 `MCPJUNGLE_HOST_PORT=18900 IMAGE_TAG=mcp-test docker compose -p mcp-test up -d --build`(`compose.yaml:6`、`README.md:90-96`)。 3. **MUST** 分清兩個位址:`--registry http://<host>:18800`(CLI → gateway)與書本註冊設定裡的 `${DOCS_MCP_URL}/mcp/<id>`(gateway → server)。 後者由產生器寫入(`mcpjungle/registry/gen-book-configs.mjs:63`),`DOCS_MCP_URL` 預設 `http://docs-mcp-server:5690`(`gen-book-configs.mjs:5`),用**服務名**在 `mcpjungl` 內解析;不寫在任何手寫註冊檔裡。 4. **MUST** gateway image 從 vendored fork 源碼 build:`context: ./mcpjungle/gateway`、`dockerfile: Dockerfile.fullbuild`、`image: mcpjungle-fork:${IMAGE_TAG:-latest}`、`pull_policy: build`(`compose.yaml:28-32`)。 來源是 `75c0649`(vendor fork 源碼);`31722f7` 把源碼搬到 `mcpjungle/gateway`。 5. **MUST** gateway runtime 保持 **Node 22+ 與 uv**(`mcpjungle/gateway/Dockerfile.fullbuild:44-52` stage 3:`ghcr.io/astral-sh/uv:debian` + nodesource `setup_22.x` + `tzdata`)。 WHY:`67b8864`——Node 20 跑不動使用 Node 22 API 的 stdio server(`@smartbear/mcp` 用 `module.enableCompileCache`);`npx` / `uvx` 是 stdio server 的前提。 6. **MUST** `MCP_SERVER_INIT_REQ_TIMEOUT_SEC` 預設 **30**(`compose.yaml:41`)。WHY:`bafd8e8`——`npx -y` 首次執行要下載套件,10 秒不夠。 7. **MUST** 四個服務都設 `TZ: ${TZ:-Asia/Taipei}`(含 `postgres`,`compose.yaml:16,42,61,89`),gateway image 安裝 `tzdata`(`bafd8e8`)。TZ 只影響日誌 / 顯示;DB 時間欄位依 `elf-postgresql` 以 UTC `timestamptz` 儲存。 8. **MUST** 要接「現有」gateway 時用 `compose.attach.yaml`(只起 docs-mcp + registrar,加入 external 網路 `MCPJUNGLE_NETWORK`,預設 `mcp-library_mcpjungl`),**MUST NOT** 再起第二個 gateway。 出處(mcp-library `origin/release` @ `73b8206`):`compose.attach.yaml:1`、`CLAUDE.md:130`、`AGENTS.md:29`、`mcpjungle/README.md:117`。這些檔只寫了規則、沒寫理由,理由列在待確認「為什麼不能再起第二個 gateway」。 ### 註冊 本節是註冊的權威版本;其他規則、第 3 節操作、第 5 節檢查清單只指向這裡,`elf-mcp-knowledge` Step 4/Step 6 的指令與預期輸出跟本節逐字一致。**書本註冊設定只以產生器的輸出為準**:registrar 啟動時由 `mcpjungle/registry/gen-book-configs.mjs` 依各書 `corpus.json` 產生,連同 `registry/*.json` 註冊進 gateway(`registrar.sh:2-3`),不手寫、不進 repo。行號以 mcp-library `origin/release` @ `73b8206` 為準;以下指令都在 mcp-library repo 根目錄執行。 **A. 新增一本書:三步,每步都附指令、預期輸出、停止條件** - **步驟 1(不手寫註冊檔)**:不新增任何註冊檔,`REGISTER_LIST` 保持空白(預設)。 指令 `docker compose config | grep REGISTER_LIST`,預期 `REGISTER_LIST: ""`(接現有 gateway 時加 `-f compose.attach.yaml`);GitOps 工具則看它的 env 編輯器有沒有設 `REGISTER_LIST`。 有值就停,見 B 表第 2 列。 - **步驟 2(部署前驗證)**: 指令 `node mcpjungle/registry/gen-book-configs.mjs mcpjungle/books "$(mktemp -d)"`,預期 exit 0,印出的 id 包含 `<id>`(`73b8206` 印出 6 個 id)。 exit 1 就停,修 `corpus.json`(原因見 B 表第 4 列);這時 registrar 也會整批不註冊。 - **步驟 3(部署後驗證)**:以下指令都要跑,每條都要符合預期: ```bash docker compose logs registrar # 應有「>> 註冊 <id>」 docker compose ps -a registrar # 應為 Exited (0) docker compose exec mcpjungle /mcpjungle list servers # 有 <id> docker compose exec mcpjungle /mcpjungle list tools 2>&1 | grep '<id>__' # 8 個 docker compose exec mcpjungle /mcpjungle invoke <id>__docs_list_corpora --input '{}' docker compose exec mcpjungle /mcpjungle invoke <id>__docs_search --input '{"query":"<該領域關鍵字>"}' ``` Windows Git Bash 下,`exec mcpjungle /mcpjungle …` 要加前綴 `MSYS_NO_PATHCONV=1`:Git Bash 會把 `/mcpjungle` 改寫成 Windows 路徑,指令會失敗(E-316 mcp-test 實跑觀察,2026-10-18)。 `list tools` 的清單印在 stderr(`mcpjungle/gateway/cmd/list.go:195-208` 用 cobra 的 `cmd.Printf`),`list servers` 印在 stdout(`:220-248` 用 `fmt.Println`);少了 `2>&1`,grep 一律 0 筆也不報錯,終端機上卻看得到沒過濾的整份清單(E-338,mcp-test 實跑 mcp-library `2f71743`,2026-10-18:不加時 grep 0 筆、加了 8 筆)。 docs-mcp-server 對每本書一律曝露全部 8 個工具,跟 `capabilities` 無關(`mcpjungle/docs-mcp-server/src/index.ts:145-279`,8 次 `registerTool` 都沒有條件),所以 `list tools` 驗不出能力設定;能力對不對看下一行。 `docs_list_corpora` 在單書端點只會列 `<id>` 自己,且標題行要帶正確 badge(速查表 / 代碼範例 / 符號查);`docs_search` 要有結果。 接現有 gateway(`compose.attach.yaml`)時,`exec mcpjungle` 改在現有 gateway 的 compose 專案執行:`compose.attach.yaml` 只有 `docs-mcp-server`、`registrar` 兩個 service(`compose.attach.yaml:10`、`:16`),`gateway` 是外部網路(`:28-31`),沒有 `mcpjungle`;`logs registrar` 與 `ps -a registrar` 則寫成 `docker compose -f compose.attach.yaml logs registrar`、`docker compose -f compose.attach.yaml ps -a registrar`。 退出碼只能用 `ps -a` 看,`logs` 不會顯示;registrar 在處理群組前就印「registrar: 完成,目前 servers:」(`registrar.sh:87`),之後群組失敗仍會 exit 1(`:90-105`),只看日誌會把失敗的部署誤判為通過。 接現有 gateway 時,所有 `docker compose run --rm … registrar` 都要加 `-f compose.attach.yaml`,所有 `exec mcpjungle …`(含 `deregister`)都在現有 gateway 的 compose 專案執行;否則在預設 `compose.yaml` 上 `run` 會經 `depends_on` 一併起 `mcpjungle` 與 `postgres`(`compose.yaml:78-80`、`:33-35`),等於第二個 gateway,違反規則 8。 不能只看 `list servers`。任一項不符就停,查 B 表。 **B. 症狀 → 原因 → 處理(registrar 的四種行為)** | 症狀 | 原因 | 處理 | |---|---|---| | registrar 印出「…(未註冊任何 server)」並 exit 1,整批都沒註冊 | 註冊前驗證失敗,由 `fail`(`registrar.sh:24`)整批中止:語料設定不合法(`:27`);`registry/*.json` 跟語料 id 同名(`:34`);`REGISTER_LIST` 列了找不到設定檔的名稱(`:54-56`);群組設定不合法(`:29`) | 依訊息修正,再 `docker compose run --rm registrar` | | 新書沒註冊,其他書正常 | `REGISTER_LIST` 有值時只註冊列出的名稱(`registrar.sh:44-45`) | 清空 `REGISTER_LIST`(規則 10);已註冊的 server 不受影響,仍留在 gateway(E-316 mcp-test 實測) | | 改了 description/url 沒生效;改名後舊名還在 | 已註冊的直接略過(`registrar.sh:68-75`);改名只新增、不移除 | `docker compose exec mcpjungle /mcpjungle deregister <名稱>`,再 `docker compose run --rm registrar` | | `gen-book-configs` exit 1,訊息指向 id | id 規則:`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`、最多 30 字元(`gen-book-configs.mjs:10-11`);不可重複(`:40`);`book` 等於書資料夾名,id 等於 `<book>-<language 小寫>` 或 `<book>-bi`(`:52-55`) | 改資料夾名或 `corpus.json` | 9. **MUST** 非書本註冊檔(`mcpjungle/registry/<name>.json`、`registry/optional/<name>.json`)依 MCPJungle `RegisterServerInput`(`mcpjungle/gateway/pkg/types/mcp_server.go:49`)撰寫;書本不手寫,走上面 A。下表只適用非書本設定,欄位: | 欄位 | 何時必填 | 說明 | |---|---|---| | `name` | 一律 | `^[a-zA-Z0-9_-]+$`、不含 `__`、不以 `_` 結尾;團隊規定小寫 kebab-case。全 gateway 唯一 | | `transport` | 一律 | `streamable_http`(自家 server)/ `stdio`(npx / uvx 官方 server)/ `sse` | | `description` | 團隊規定必填 | 列出該 server 有效的**全部**工具名(證據 E10) | | `url` | `streamable_http` | gateway → server 的容器內網址 | | `bearer_token` | server 設了 `MCP_AUTH_TOKEN` 時 | 用 `${ENV_NAME}`,CLI 註冊時展開;**MUST NOT** 寫明文 | | `headers` | 選用 | 自訂 header;`Authorization` 會蓋過 `bearer_token` | | `command` / `args` / `env` | `stdio` | `env` 值也用 `${ENV_NAME}` | | `session_mode` | 選用 | `stateless`(預設)/ `stateful` | 範本:`templates/registry/http-server.json`、`http-server-auth.json`、`stdio-server.json`、`stdio-server-env.json`。 mcp-library 現行對照:stdio 看 `mcpjungle/registry/fetch.json`、`filesystem.json`、`time.json`;streamable_http 看 `registry/optional/docs.json`。 檔名(不含 `.json`)就是 `name`,不可跟語料 id 同名(`registrar.sh:34`)。`bearer_token` 的 `${ENV_NAME}` 能不能在 registrar 映像裡展開還沒驗證(待確認 5)。 10. **MUST** 非書本 server 依需求走下面兩條路之一,並遵守後面的限制: - **不需要 token**:放 `mcpjungle/registry/<name>.json`。`compose.yaml` 預設會註冊(`REGISTER_EXTRAS` 預設 1,`compose.yaml:87`);`compose.attach.yaml` 預設不註冊(預設 0,`compose.attach.yaml:25`)。 - **點名才註冊**:放 `mcpjungle/registry/optional/<name>.json`。`optional/` 的意義是「點名才註冊」,不是「需要 token」(`optional/docs.json` 沒有 token)。預設清單只收產生器輸出(`$GEN`)與 `registry/*.json`(`registrar.sh:47-51`);`optional/` 只在 `config_of` 查名稱時用到(`:37-42`)。 只准用一次性指令註冊:`docker compose run --rm -e REGISTER_LIST=<名稱> registrar`。 - **MUST NOT** 把 `REGISTER_LIST` 寫進 `.env`(或 GitOps 工具的 env)。WHY:有值時只註冊列出的名稱(`registrar.sh:44-45`),之後新加的書都不會被註冊,也不會報錯。 - 需要 token 的 server(如 github):token 怎麼傳進 registrar 容器還沒驗證(待確認 5),維持停止條件:**需要 token 時停下來回報**。 - **MUST NOT** 把 token 寫進 `registry/*.json`(含 `optional/`),也不能烤進映像。WHY:`registry/Dockerfile:6-7` 會把 `registry/*.json` 與 `optional/` 整包 COPY 進 `docs-registrar` 映像,推到 GHCR。 mcp-library `mcpjungle/README.md` 教人把 PAT 明文放進 `optional/` 的寫法已由 E-325 修正(`5240cb1`,release `2f71743`):現在 `:95-107` 改寫 `${變數名}` 佔位、由 registrar 的 CLI 代換。這個代換在 registrar 映像裡能不能用仍是待確認 5,上面的停止條件不變。 - 範本細節見第 3 節;CI 與 Dependabot 見 `elf-mcp-knowledge` 路線 C 與本 skill 規則 23。 11. **MUST** 知識語料採**策略 A**:每本書一個 server,名稱=語料 id,位址由產生器寫成 `${DOCS_MCP_URL}/mcp/<id>`(`gen-book-configs.mjs:63`),工具 `<id>__docs_search`;這是預設,不必設定(`mcpjungle/README.md:68`)。 策略 B(整包一個 `docs`,`registry/optional/docs.json`,工具 `docs__docs_*`,用 `corpus` 參數選書)只當補充入口,依規則 10 點名註冊(`mcpjungle/README.md:69`)。 B 是手寫檔,設了 `DOCS_MCP_AUTH_TOKEN` 也不會自動帶 `bearer_token`(`mcpjungle/README.md:69`);要加就屬於規則 10 的「需要 token」,停下來回報。 WHY:A 才能在 gateway 以 server 為單位做 tool group 與 enterprise ACL(`registry/groups/claude-tools.json` 的 `"@books"` 展開成全部書本 server,`mcpjungle/README.md:73`)。 ### registrar 12. **MUST** registrar 以 `mcpjungle/registry/Dockerfile` 把各書 `books/`、`registry/*.json`、`registry/optional/`、`registry/groups/` 與腳本(`registrar.sh`、`gen-book-configs.mjs`、`gen-group-configs.mjs`)**烤進 image**(`ghcr.io/elf-express/docs-registrar`,`registry/Dockerfile:4-10`),**MUST NOT** 改回 bind mount。 WHY:`54a9c08`——Dockhand / Portainer 把 repo clone 進自己的容器,相對 bind mount 由 host daemon 解析,報 `mkdir /app: read-only file system`;現況的說明在 `registry/Dockerfile:3`。 13. **MUST** 保留 `mcpjungle/registry/registrar.sh` 的三段邏輯:等 gateway(每 2 秒、上限 60 次=120 秒,`registrar.sh:60-65`)→ 每個 server 註冊重試(3 秒、上限 20 次,`:77-84`)→ 冪等(`list servers` 已有則略過,`:68-75`)。 另兩段同樣保留:註冊前先驗證,不合法就整批不註冊(`:24-56`);任一 server 或群組失敗時最後 exit 1(`:105`)。各情況的症狀與處理見〈註冊〉B 表。 WHY:`3eaebfd`——docs-mcp 尚未監聽時註冊會 connection refused;冪等讓每次 push 後 redeploy 安全。 14. 註冊見第 2 節〈註冊〉。**MUST NOT** 為新 server 改 `REGISTER_LIST`。
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub