From 0cfcfa77d38577fa0e52add95c2c45c4f0bca346 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sat, 4 Jul 2026 21:57:21 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(mcp-server):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=AE=B9=E5=99=A8=E5=8C=96=E9=83=A8=E7=BD=B2=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MCP Server 的 Dockerfile、Compose 环境示例与 GHCR 镜像流水线\n- 补充 Podman Quadlet、Kubernetes Kustomize 与 Helm Chart 部署样例\n- 完善 MCP Server 独立 README,补充本地与远端 Agent 接入说明\n\nFixes #618 --- .containerignore | 18 ++ .dockerignore | 18 ++ .github/workflows/docker-images.yml | 118 ++++++++++++ Dockerfile.build-env | 42 ++++ Dockerfile.mcp-server | 42 ++++ cmd/gonavi-mcp-server/README.md | 181 ++++++++++++++++++ deploy/helm/gonavi-mcp-server/Chart.yaml | 7 + deploy/helm/gonavi-mcp-server/README.md | 94 +++++++++ .../gonavi-mcp-server/templates/_helpers.tpl | 49 +++++ .../templates/deployment.yaml | 130 +++++++++++++ .../gonavi-mcp-server/templates/ingress.yaml | 34 ++++ .../gonavi-mcp-server/templates/secret.yaml | 27 +++ .../gonavi-mcp-server/templates/service.yaml | 20 ++ .../values-examples/drivers-pvc.yaml | 4 + .../ingress-with-drivers-pvc.yaml | 19 ++ .../values-examples/ingress.yaml | 14 ++ .../values-examples/inline-secret.yaml | 13 ++ .../values-examples/nas-hostpath.yaml | 10 + deploy/helm/gonavi-mcp-server/values.yaml | 99 ++++++++++ deploy/k8s/gonavi-mcp-server/README.md | 151 +++++++++++++++ .../gonavi-mcp-server/base/deployment.yaml | 89 +++++++++ .../gonavi-mcp-server/base/kustomization.yaml | 6 + .../k8s/gonavi-mcp-server/base/service.yaml | 15 ++ .../k8s/gonavi-mcp-server/kustomization.yaml | 5 + .../overlays/drivers-pvc/kustomization.yaml | 8 + .../drivers-pvc/patch-deployment.yaml | 18 ++ .../ingress-with-drivers-pvc/ingress.yaml | 33 ++++ .../kustomization.yaml | 9 + .../patch-deployment.yaml | 18 ++ .../overlays/ingress/ingress.yaml | 33 ++++ .../overlays/ingress/kustomization.yaml | 6 + .../overlays/nas-hostpath/kustomization.yaml | 8 + .../nas-hostpath/patch-deployment.yaml | 14 ++ deploy/podman/gonavi-mcp-server/README.md | 101 ++++++++++ .../gonavi-mcp-server.container | 22 +++ .../gonavi-mcp-server.env.example | 15 ++ docker-compose.mcp-server.local.yml | 7 + docker-compose.mcp-server.yml | 20 ++ docker.mcp-server.env.example | 20 ++ 39 files changed, 1537 insertions(+) create mode 100644 .containerignore create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-images.yml create mode 100644 Dockerfile.build-env create mode 100644 Dockerfile.mcp-server create mode 100644 deploy/helm/gonavi-mcp-server/Chart.yaml create mode 100644 deploy/helm/gonavi-mcp-server/README.md create mode 100644 deploy/helm/gonavi-mcp-server/templates/_helpers.tpl create mode 100644 deploy/helm/gonavi-mcp-server/templates/deployment.yaml create mode 100644 deploy/helm/gonavi-mcp-server/templates/ingress.yaml create mode 100644 deploy/helm/gonavi-mcp-server/templates/secret.yaml create mode 100644 deploy/helm/gonavi-mcp-server/templates/service.yaml create mode 100644 deploy/helm/gonavi-mcp-server/values-examples/drivers-pvc.yaml create mode 100644 deploy/helm/gonavi-mcp-server/values-examples/ingress-with-drivers-pvc.yaml create mode 100644 deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml create mode 100644 deploy/helm/gonavi-mcp-server/values-examples/inline-secret.yaml create mode 100644 deploy/helm/gonavi-mcp-server/values-examples/nas-hostpath.yaml create mode 100644 deploy/helm/gonavi-mcp-server/values.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/README.md create mode 100644 deploy/k8s/gonavi-mcp-server/base/deployment.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/base/kustomization.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/base/service.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/kustomization.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/kustomization.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/patch-deployment.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/ingress.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/kustomization.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/patch-deployment.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/ingress/ingress.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/ingress/kustomization.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/kustomization.yaml create mode 100644 deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/patch-deployment.yaml create mode 100644 deploy/podman/gonavi-mcp-server/README.md create mode 100644 deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container create mode 100644 deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example create mode 100644 docker-compose.mcp-server.local.yml create mode 100644 docker-compose.mcp-server.yml create mode 100644 docker.mcp-server.env.example diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..44e691d --- /dev/null +++ b/.containerignore @@ -0,0 +1,18 @@ +.git +.github +.idea +.ace-tool +.playwright-mcp +.superpowers +.worktrees +bin +build +dist +frontend/dist +node_modules +frontend/node_modules +tools/__pycache__ +GoNavi-Wails +GoNavi-Wails.exe +optional-driver-agent +optional-driver-agent.exe diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..44e691d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.idea +.ace-tool +.playwright-mcp +.superpowers +.worktrees +bin +build +dist +frontend/dist +node_modules +frontend/node_modules +tools/__pycache__ +GoNavi-Wails +GoNavi-Wails.exe +optional-driver-agent +optional-driver-agent.exe diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml new file mode 100644 index 0000000..89504c1 --- /dev/null +++ b/.github/workflows/docker-images.yml @@ -0,0 +1,118 @@ +name: Docker Images + +on: + push: + branches: + - dev + tags: + - 'v*' + +concurrency: + group: docker-images-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Publish ${{ matrix.image_name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - image_name: gonavi-mcp-server + dockerfile: Dockerfile.mcp-server + description: GoNavi MCP Server container image + - image_name: gonavi-build-env + dockerfile: Dockerfile.build-env + description: GoNavi Linux build environment image + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Normalize image namespace + id: vars + shell: bash + run: | + set -euo pipefail + echo "owner_lc=$(printf '%s' "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ matrix.image_name }} + flavor: | + latest=false + tags: | + type=raw,value=dev-latest,enable=${{ github.ref == 'refs/heads/dev' }} + type=sha,format=short,prefix=dev-,enable=${{ github.ref == 'refs/heads/dev' }} + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + labels: | + org.opencontainers.image.title=${{ matrix.image_name }} + org.opencontainers.image.description=${{ matrix.description }} + + - name: Build smoke image + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/amd64 + load: true + tags: codex-smoke/${{ matrix.image_name }}:local + cache-from: type=gha,scope=${{ matrix.image_name }} + + - name: Smoke test MCP Server image + if: matrix.image_name == 'gonavi-mcp-server' + shell: bash + run: | + set -euo pipefail + cid="$(docker run -d -p 8765:8765 -e GONAVI_MCP_HTTP_TOKEN=smoke-token codex-smoke/${{ matrix.image_name }}:local http)" + trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT + for _ in 1 2 3 4 5 6 7 8 9 10; do + if curl --fail --silent http://127.0.0.1:8765/healthz >/dev/null; then + exit 0 + fi + sleep 1 + done + docker logs "$cid" + echo "MCP Server image smoke test failed" >&2 + exit 1 + + - name: Smoke test build environment image + if: matrix.image_name == 'gonavi-build-env' + shell: bash + run: | + set -euo pipefail + docker run --rm codex-smoke/${{ matrix.image_name }}:local \ + bash -lc 'go version && node -v && npm -v && wails version' + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.image_name }} + cache-to: type=gha,mode=max,scope=${{ matrix.image_name }} diff --git a/Dockerfile.build-env b/Dockerfile.build-env new file mode 100644 index 0000000..5048983 --- /dev/null +++ b/Dockerfile.build-env @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1.7 + +ARG GO_IMAGE=golang:1.24-bookworm +ARG NODE_IMAGE=node:20-bookworm + +FROM ${NODE_IMAGE} AS node + +FROM ${GO_IMAGE} + +ARG WEBKIT_API=4.0 +ARG WAILS_VERSION=v2.11.0 + +ENV DEBIAN_FRONTEND=noninteractive +ENV PATH=/root/go/bin:${PATH} + +COPY --from=node /usr/local/ /usr/local/ + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + build-essential \ + ca-certificates \ + curl \ + file \ + git \ + make \ + pkg-config \ + python3 \ + unzip \ + zip \ + libgtk-3-dev \ + && if [ "${WEBKIT_API}" = "4.1" ]; then \ + apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev libsoup-3.0-dev; \ + else \ + apt-get install -y --no-install-recommends libwebkit2gtk-4.0-dev; \ + fi \ + && go install github.com/wailsapp/wails/v2/cmd/wails@${WAILS_VERSION} \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +CMD ["bash"] diff --git a/Dockerfile.mcp-server b/Dockerfile.mcp-server new file mode 100644 index 0000000..7de2d33 --- /dev/null +++ b/Dockerfile.mcp-server @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1.7 + +FROM --platform=$BUILDPLATFORM golang:1.24-bookworm AS builder + +ARG TARGETOS=linux +ARG TARGETARCH=amd64 + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -trimpath -ldflags="-s -w" -o /out/gonavi-mcp-server ./cmd/gonavi-mcp-server + +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --system --create-home --home-dir /var/lib/gonavi --uid 10001 gonavi \ + && mkdir -p /data /var/lib/gonavi/logs \ + && chown -R gonavi:gonavi /data /var/lib/gonavi + +COPY --from=builder /out/gonavi-mcp-server /usr/local/bin/gonavi-mcp-server + +ENV HOME=/var/lib/gonavi \ + GONAVI_DATA_ROOT=/data \ + GONAVI_LOG_DIR=/var/lib/gonavi/logs \ + GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765 \ + GONAVI_MCP_HTTP_PATH=/mcp \ + GONAVI_MCP_SCHEMA_ONLY=true + +VOLUME ["/data"] +EXPOSE 8765 + +USER gonavi + +ENTRYPOINT ["/usr/local/bin/gonavi-mcp-server"] +CMD ["http"] diff --git a/cmd/gonavi-mcp-server/README.md b/cmd/gonavi-mcp-server/README.md index 69205cc..49399a0 100644 --- a/cmd/gonavi-mcp-server/README.md +++ b/cmd/gonavi-mcp-server/README.md @@ -49,6 +49,187 @@ go run ./cmd/gonavi-mcp-server stdio go build -o .\bin\gonavi-mcp-server.exe .\cmd\gonavi-mcp-server ``` +## Docker / Podman / Compose + +当前容器化支持仅覆盖 `gonavi-mcp-server`,不包含 Wails 桌面 GUI。 + +当前支持矩阵: + +- Docker Desktop / Linux 服务器 / NAS:直接使用 Compose 或 `docker run` +- Podman / Quadlet:使用 `deploy/podman/gonavi-mcp-server` +- Kubernetes:使用 `deploy/k8s/gonavi-mcp-server` +- Helm:使用 `deploy/helm/gonavi-mcp-server` +- 仅构建环境:使用仓库根目录 `Dockerfile.build-env` +- 桌面 GUI 浏览器访问版:当前不提供,此仓库主应用仍是 Wails 桌面程序,不是现成的 Web 服务 + +仓库根目录已提供以下文件: + +- `Dockerfile.mcp-server` +- `docker-compose.mcp-server.yml` +- `docker.mcp-server.env.example` +- `deploy/podman/gonavi-mcp-server/*` + +推荐流程: + +```bash +cp docker.mcp-server.env.example docker.mcp-server.env +docker compose --env-file docker.mcp-server.env -f docker-compose.mcp-server.yml up -d +``` + +默认 Compose 会拉取 GHCR 预构建镜像。如果你要基于当前工作区源码本地构建,再叠加: + +```bash +docker compose --env-file docker.mcp-server.env \ + -f docker-compose.mcp-server.yml \ + -f docker-compose.mcp-server.local.yml \ + up -d --build +``` + +其中 `GONAVI_HOST_DATA_ROOT` 必须指向 GoNavi 当前活动数据目录。该目录内至少应包含: + +- `connections.json` +- `daily_secrets.json` +- `drivers/`(如果目标连接依赖可选 driver agent) + +容器内默认会设置: + +- `GONAVI_DATA_ROOT=/data` +- `GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765` +- `GONAVI_MCP_HTTP_PATH=/mcp` + +`GONAVI_DATA_ROOT` 会覆盖默认活动数据目录解析逻辑,避免宿主机路径与容器内路径不一致时依赖 `storage_root.json` 的绝对路径。 + +如果你只想手动构建镜像: + +```bash +docker build -f Dockerfile.mcp-server -t gonavi-mcp-server:local . +docker run --rm -p 8765:8765 \ + -e GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token \ + -e GONAVI_MCP_SCHEMA_ONLY=true \ + -e GONAVI_DATA_ROOT=/data \ + -v /absolute/path/to/gonavi-data:/data \ + gonavi-mcp-server:local http +``` + +如果你直接使用已发布镜像: + +```bash +docker run --rm -p 8765:8765 \ + -e GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token \ + -e GONAVI_MCP_SCHEMA_ONLY=true \ + -e GONAVI_DATA_ROOT=/data \ + -v /absolute/path/to/gonavi-data:/data \ + ghcr.io/syngnat/gonavi-mcp-server:latest http +``` + +### Podman + +仓库内还提供了 Podman 原生部署样例: + +- `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example` +- `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container` +- `deploy/podman/gonavi-mcp-server/README.md` + +直接运行已发布镜像: + +```bash +cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ./gonavi-mcp-server.env +podman run -d --name gonavi-mcp-server --replace \ + -p 8765:8765 \ + --env-file ./gonavi-mcp-server.env \ + -v /absolute/path/to/gonavi-data:/data:Z \ + ghcr.io/syngnat/gonavi-mcp-server:latest http +``` + +如果你要基于当前源码本地构建: + +```bash +podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local . +podman run -d --name gonavi-mcp-server --replace \ + -p 8765:8765 \ + --env-file ./gonavi-mcp-server.env \ + -v /absolute/path/to/gonavi-data:/data:Z \ + localhost/gonavi-mcp-server:local http +``` + +其中: + +- `gonavi-mcp-server.env` 用 `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example` 初始化 +- `:Z` 适用于开启 SELinux 的宿主机;未启用 SELinux 可去掉 +- 更适合长期运行的方式见 [deploy/podman/gonavi-mcp-server/README.md](../../deploy/podman/gonavi-mcp-server/README.md) 中的 Quadlet 示例 + +`podman compose` 本身依赖外部 compose provider,所以仓库对 Podman 的主支持路径是 `podman run` 与 Quadlet,而不是假设所有环境都能直接复用 Compose。 + +## Kubernetes + +仓库内置了最小 K8s 示例: + +- `deploy/k8s/gonavi-mcp-server/kustomization.yaml` +- `deploy/k8s/gonavi-mcp-server/base/deployment.yaml` +- `deploy/k8s/gonavi-mcp-server/base/service.yaml` +- `deploy/k8s/gonavi-mcp-server/README.md` +- `deploy/k8s/gonavi-mcp-server/overlays/*` + +推荐先从现有 GoNavi 数据目录生成 Secret: + +```bash +kubectl create namespace gonavi +kubectl -n gonavi create secret generic gonavi-mcp-server-data \ + --from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \ + --from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \ + --from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token +kubectl apply -k deploy/k8s/gonavi-mcp-server +``` + +如果需要 NAS hostPath、可选 driver agent PVC、Ingress,或两者组合,可直接使用 `overlays/nas-hostpath`、`overlays/drivers-pvc`、`overlays/ingress`、`overlays/ingress-with-drivers-pvc`。 + +更完整的说明见 [deploy/k8s/gonavi-mcp-server/README.md](../../deploy/k8s/gonavi-mcp-server/README.md)。 + +## Helm + +如果你希望把镜像、Secret、Ingress、hostPath / PVC 挂载做成参数化部署,而不是维护多份 Kustomize overlay,可直接使用: + +- `deploy/helm/gonavi-mcp-server` + +快速安装: + +```bash +helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server -n gonavi --create-namespace +``` + +Chart 详细说明见 [deploy/helm/gonavi-mcp-server/README.md](../../deploy/helm/gonavi-mcp-server/README.md)。 + +## Docker / Podman Build Environment + +如果你的目标不是运行 MCP,而是给 Linux 服务器 / NAS / CI 准备一套可重复的 Wails 构建环境,可直接使用仓库根目录的 `Dockerfile.build-env`: + +```bash +docker build -f Dockerfile.build-env -t gonavi-build-env:local . +docker run --rm -it -v "$PWD:/workspace" -w /workspace gonavi-build-env:local bash +``` + +如果你使用 Podman,也可以直接执行: + +```bash +podman build -f Dockerfile.build-env -t localhost/gonavi-build-env:local . +podman run --rm -it -v "$PWD:/workspace" -w /workspace localhost/gonavi-build-env:local bash +``` + +镜像内已预装 Go、Node、Wails CLI、GTK3 与 WebKitGTK 开发依赖,适合执行: + +```bash +wails build +``` + +这个镜像默认安装 WebKitGTK 4.0 构建依赖,适合作为通用 Linux / NAS 构建环境。镜像基座支持多架构,`amd64` / `arm64` 会跟随容器平台。 + +预构建镜像会发布到 GHCR: + +- `ghcr.io/syngnat/gonavi-mcp-server:latest` +- `ghcr.io/syngnat/gonavi-build-env:latest` + +它只负责构建 Linux 产物,不会把 Wails 主程序变成浏览器版服务。 + 远程 Agent 使用 Streamable HTTP 时必须设置 bearer token: ```powershell diff --git a/deploy/helm/gonavi-mcp-server/Chart.yaml b/deploy/helm/gonavi-mcp-server/Chart.yaml new file mode 100644 index 0000000..a5bd7e0 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: gonavi-mcp-server +description: Helm chart for deploying GoNavi MCP Server +type: application +version: 0.1.0 +appVersion: "latest" +kubeVersion: ">=1.24.0-0" diff --git a/deploy/helm/gonavi-mcp-server/README.md b/deploy/helm/gonavi-mcp-server/README.md new file mode 100644 index 0000000..c2ac834 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/README.md @@ -0,0 +1,94 @@ +# GoNavi MCP Server Helm Chart + +这个 Chart 用来部署 `gonavi-mcp-server`,覆盖当前仓库 K8s 示例里的几类常见场景: + +- 基于现有 Secret 直接部署 +- 通过 Helm values 内联创建测试用 Secret +- 启用 Ingress +- 追加 `drivers/` PVC +- NAS / k3s 单机节点直接挂宿主机数据目录 + +Chart 目录: + +- `Chart.yaml` +- `values.yaml` +- `values-examples/*.yaml` +- `templates/*.yaml` + +## 1. 默认安装 + +默认值假设你已经有一个现成 Secret: + +- 名称:`gonavi-mcp-server-data` +- 至少包含: + - `connections.json` + - `daily_secrets.json` + - `GONAVI_MCP_HTTP_TOKEN` + +安装命令: + +```bash +helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server -n gonavi --create-namespace +``` + +## 2. 直接创建测试用 Secret + +如果只是本地验证模板或快速试跑,可以使用示例 values: + +```bash +helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \ + -n gonavi --create-namespace \ + -f deploy/helm/gonavi-mcp-server/values-examples/inline-secret.yaml +``` + +这个示例只适合最小联调,不适合真实生产连接数据。 + +## 3. 常用示例 + +启用 Ingress: + +```bash +helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \ + -n gonavi --create-namespace \ + -f deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml +``` + +挂载 drivers PVC: + +```bash +helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \ + -n gonavi --create-namespace \ + -f deploy/helm/gonavi-mcp-server/values-examples/drivers-pvc.yaml +``` + +同时启用 Ingress 与 drivers PVC: + +```bash +helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \ + -n gonavi --create-namespace \ + -f deploy/helm/gonavi-mcp-server/values-examples/ingress-with-drivers-pvc.yaml +``` + +NAS / k3s hostPath: + +```bash +helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \ + -n gonavi --create-namespace \ + -f deploy/helm/gonavi-mcp-server/values-examples/nas-hostpath.yaml +``` + +## 4. 关键参数 + +- `image.repository` / `image.tag` +- `secret.create` +- `secret.name` +- `data.mode=secret|hostPath|pvc` +- `drivers.mode=none|pvc|hostPath` +- `ingress.enabled` + +## 5. 约束 + +- `data.mode=secret` 时,Secret 必须能提供 `connections.json` 和 `daily_secrets.json` +- `data.mode=hostPath` 时,宿主机目录需要包含 GoNavi 活动数据目录内容 +- `drivers.mode!=none` 时,会在 `/data/drivers` 追加独立挂载 +- Chart 默认只部署 MCP Server,不部署桌面 GUI diff --git a/deploy/helm/gonavi-mcp-server/templates/_helpers.tpl b/deploy/helm/gonavi-mcp-server/templates/_helpers.tpl new file mode 100644 index 0000000..82eb636 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/templates/_helpers.tpl @@ -0,0 +1,49 @@ +{{- define "gonavi-mcp-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "gonavi-mcp-server.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := include "gonavi-mcp-server.name" . -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "gonavi-mcp-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" -}} +{{- end -}} + +{{- define "gonavi-mcp-server.labels" -}} +helm.sh/chart: {{ include "gonavi-mcp-server.chart" . }} +app.kubernetes.io/name: {{ include "gonavi-mcp-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "gonavi-mcp-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "gonavi-mcp-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "gonavi-mcp-server.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride -}} +{{- end -}} + +{{- define "gonavi-mcp-server.secretName" -}} +{{- if .Values.secret.name -}} +{{- .Values.secret.name -}} +{{- else -}} +{{- include "gonavi-mcp-server.fullname" . -}} +{{- end -}} +{{- end -}} + +{{- define "gonavi-mcp-server.serviceName" -}} +{{- include "gonavi-mcp-server.fullname" . -}} +{{- end -}} diff --git a/deploy/helm/gonavi-mcp-server/templates/deployment.yaml b/deploy/helm/gonavi-mcp-server/templates/deployment.yaml new file mode 100644 index 0000000..f1f7482 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/templates/deployment.yaml @@ -0,0 +1,130 @@ +{{- $dataMode := .Values.data.mode -}} +{{- $driversMode := .Values.drivers.mode -}} +{{- if not (or (eq $dataMode "secret") (eq $dataMode "hostPath") (eq $dataMode "pvc")) -}} +{{- fail "data.mode must be one of: secret, hostPath, pvc" -}} +{{- end -}} +{{- if not (or (eq $driversMode "none") (eq $driversMode "pvc") (eq $driversMode "hostPath")) -}} +{{- fail "drivers.mode must be one of: none, pvc, hostPath" -}} +{{- end -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gonavi-mcp-server.fullname" . }} + namespace: {{ include "gonavi-mcp-server.namespace" . }} + labels: + {{- include "gonavi-mcp-server.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "gonavi-mcp-server.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "gonavi-mcp-server.selectorLabels" . | nindent 8 }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: gonavi-mcp-server + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - http + {{- range .Values.mcp.extraArgs }} + - {{ . | quote }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: GONAVI_DATA_ROOT + value: {{ .Values.mcp.dataRoot | quote }} + - name: GONAVI_LOG_DIR + value: {{ .Values.mcp.logDir | quote }} + - name: GONAVI_MCP_HTTP_ADDR + value: {{ .Values.mcp.httpAddr | quote }} + - name: GONAVI_MCP_HTTP_PATH + value: {{ .Values.mcp.httpPath | quote }} + - name: GONAVI_MCP_SCHEMA_ONLY + value: {{ ternary "true" "false" .Values.mcp.schemaOnly | quote }} + - name: GONAVI_MCP_HTTP_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "gonavi-mcp-server.secretName" . }} + key: {{ .Values.secret.tokenKey }} + {{- with .Values.mcp.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: gonavi-data + mountPath: {{ .Values.data.mountPath | quote }} + readOnly: true + {{- if ne $driversMode "none" }} + - name: gonavi-drivers + mountPath: {{ .Values.drivers.mountPath | quote }} + readOnly: true + {{- end }} + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.containerSecurityContext | nindent 12 }} + volumes: + - name: gonavi-data + {{- if eq $dataMode "secret" }} + projected: + sources: + - secret: + name: {{ include "gonavi-mcp-server.secretName" . }} + items: + - key: {{ .Values.secret.connectionsJsonKey }} + path: connections.json + - key: {{ .Values.secret.dailySecretsJsonKey }} + path: daily_secrets.json + {{- else if eq $dataMode "hostPath" }} + hostPath: + path: {{ required "data.hostPath.path is required when data.mode=hostPath" .Values.data.hostPath.path | quote }} + type: {{ default "Directory" .Values.data.hostPath.type | quote }} + {{- else if eq $dataMode "pvc" }} + persistentVolumeClaim: + claimName: {{ required "data.pvc.claimName is required when data.mode=pvc" .Values.data.pvc.claimName | quote }} + {{- end }} + {{- if eq $driversMode "pvc" }} + - name: gonavi-drivers + persistentVolumeClaim: + claimName: {{ required "drivers.pvc.claimName is required when drivers.mode=pvc" .Values.drivers.pvc.claimName | quote }} + {{- else if eq $driversMode "hostPath" }} + - name: gonavi-drivers + hostPath: + path: {{ required "drivers.hostPath.path is required when drivers.mode=hostPath" .Values.drivers.hostPath.path | quote }} + type: {{ default "Directory" .Values.drivers.hostPath.type | quote }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/gonavi-mcp-server/templates/ingress.yaml b/deploy/helm/gonavi-mcp-server/templates/ingress.yaml new file mode 100644 index 0000000..3424018 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/templates/ingress.yaml @@ -0,0 +1,34 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "gonavi-mcp-server.fullname" . }} + namespace: {{ include "gonavi-mcp-server.namespace" . }} + labels: + {{- include "gonavi-mcp-server.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className | quote }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path | quote }} + pathType: {{ .pathType | quote }} + backend: + service: + name: {{ include "gonavi-mcp-server.serviceName" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/gonavi-mcp-server/templates/secret.yaml b/deploy/helm/gonavi-mcp-server/templates/secret.yaml new file mode 100644 index 0000000..713c13d --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/templates/secret.yaml @@ -0,0 +1,27 @@ +{{- if .Values.secret.create }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gonavi-mcp-server.secretName" . }} + namespace: {{ include "gonavi-mcp-server.namespace" . }} + labels: + {{- include "gonavi-mcp-server.labels" . | nindent 4 }} +type: Opaque +stringData: + {{ .Values.secret.tokenKey }}: {{ required "secret.stringData.token is required when secret.create=true" .Values.secret.stringData.token | quote }} + {{- if eq .Values.data.mode "secret" }} + {{ .Values.secret.connectionsJsonKey }}: | +{{ required "secret.stringData.connectionsJson is required when data.mode=secret and secret.create=true" .Values.secret.stringData.connectionsJson | nindent 4 }} + {{ .Values.secret.dailySecretsJsonKey }}: | +{{ required "secret.stringData.dailySecretsJson is required when data.mode=secret and secret.create=true" .Values.secret.stringData.dailySecretsJson | nindent 4 }} + {{- else }} + {{- if .Values.secret.stringData.connectionsJson }} + {{ .Values.secret.connectionsJsonKey }}: | +{{ .Values.secret.stringData.connectionsJson | nindent 4 }} + {{- end }} + {{- if .Values.secret.stringData.dailySecretsJson }} + {{ .Values.secret.dailySecretsJsonKey }}: | +{{ .Values.secret.stringData.dailySecretsJson | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/gonavi-mcp-server/templates/service.yaml b/deploy/helm/gonavi-mcp-server/templates/service.yaml new file mode 100644 index 0000000..b9ea800 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/templates/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gonavi-mcp-server.serviceName" . }} + namespace: {{ include "gonavi-mcp-server.namespace" . }} + labels: + {{- include "gonavi-mcp-server.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "gonavi-mcp-server.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/deploy/helm/gonavi-mcp-server/values-examples/drivers-pvc.yaml b/deploy/helm/gonavi-mcp-server/values-examples/drivers-pvc.yaml new file mode 100644 index 0000000..5f58158 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/values-examples/drivers-pvc.yaml @@ -0,0 +1,4 @@ +drivers: + mode: pvc + pvc: + claimName: gonavi-drivers diff --git a/deploy/helm/gonavi-mcp-server/values-examples/ingress-with-drivers-pvc.yaml b/deploy/helm/gonavi-mcp-server/values-examples/ingress-with-drivers-pvc.yaml new file mode 100644 index 0000000..b6aec47 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/values-examples/ingress-with-drivers-pvc.yaml @@ -0,0 +1,19 @@ +ingress: + enabled: true + className: nginx + hosts: + - host: gonavi-mcp.example.com + paths: + - path: /mcp + pathType: Prefix + - path: /healthz + pathType: Prefix + tls: + - secretName: gonavi-mcp-server-tls + hosts: + - gonavi-mcp.example.com + +drivers: + mode: pvc + pvc: + claimName: gonavi-drivers diff --git a/deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml b/deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml new file mode 100644 index 0000000..5e142d4 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml @@ -0,0 +1,14 @@ +ingress: + enabled: true + className: nginx + hosts: + - host: gonavi-mcp.example.com + paths: + - path: /mcp + pathType: Prefix + - path: /healthz + pathType: Prefix + tls: + - secretName: gonavi-mcp-server-tls + hosts: + - gonavi-mcp.example.com diff --git a/deploy/helm/gonavi-mcp-server/values-examples/inline-secret.yaml b/deploy/helm/gonavi-mcp-server/values-examples/inline-secret.yaml new file mode 100644 index 0000000..af3da91 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/values-examples/inline-secret.yaml @@ -0,0 +1,13 @@ +secret: + create: true + stringData: + token: replace-with-a-random-token + connectionsJson: | + { + "connections": [] + } + dailySecretsJson: | + { + "schemaVersion": 1, + "connections": {} + } diff --git a/deploy/helm/gonavi-mcp-server/values-examples/nas-hostpath.yaml b/deploy/helm/gonavi-mcp-server/values-examples/nas-hostpath.yaml new file mode 100644 index 0000000..5af106c --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/values-examples/nas-hostpath.yaml @@ -0,0 +1,10 @@ +data: + mode: hostPath + hostPath: + path: /volume1/docker/gonavi/data + type: Directory + +secret: + create: true + stringData: + token: replace-with-a-random-token diff --git a/deploy/helm/gonavi-mcp-server/values.yaml b/deploy/helm/gonavi-mcp-server/values.yaml new file mode 100644 index 0000000..70231d9 --- /dev/null +++ b/deploy/helm/gonavi-mcp-server/values.yaml @@ -0,0 +1,99 @@ +nameOverride: "" +fullnameOverride: "" +namespaceOverride: "" + +replicaCount: 1 + +image: + repository: ghcr.io/syngnat/gonavi-mcp-server + tag: latest + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 8765 + annotations: {} + +mcp: + dataRoot: /data + logDir: /var/lib/gonavi/logs + httpAddr: 0.0.0.0:8765 + httpPath: /mcp + schemaOnly: true + extraArgs: [] + extraEnv: [] + +secret: + create: false + name: gonavi-mcp-server-data + tokenKey: GONAVI_MCP_HTTP_TOKEN + connectionsJsonKey: connections.json + dailySecretsJsonKey: daily_secrets.json + stringData: + token: "" + connectionsJson: "" + dailySecretsJson: "" + +data: + mode: secret + mountPath: /data + hostPath: + path: /volume1/docker/gonavi/data + type: Directory + pvc: + claimName: "" + +drivers: + mode: none + mountPath: /data/drivers + pvc: + claimName: gonavi-drivers + hostPath: + path: /volume1/docker/gonavi/drivers + type: Directory + +ingress: + enabled: false + className: nginx + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + hosts: + - host: gonavi-mcp.example.com + paths: + - path: /mcp + pathType: Prefix + - path: /healthz + pathType: Prefix + tls: + - secretName: gonavi-mcp-server-tls + hosts: + - gonavi-mcp.example.com + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +podAnnotations: {} + +podSecurityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + seccompProfile: + type: RuntimeDefault + +containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/deploy/k8s/gonavi-mcp-server/README.md b/deploy/k8s/gonavi-mcp-server/README.md new file mode 100644 index 0000000..e792e9d --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/README.md @@ -0,0 +1,151 @@ +# GoNavi MCP Server Kubernetes 示例 + +这个目录提供 `gonavi-mcp-server` 的最小 K8s 部署清单,适合: + +- Linux 服务器上的单集群部署 +- NAS 自带 K8s / k3s / k8s-lite 环境 +- 需要把 GoNavi MCP 通过 Ingress / Gateway 暴露给远端 Agent 的场景 + +如果你希望通过 values 参数统一控制镜像、Secret、Ingress、PVC/hostPath,而不是维护多份 overlay,请直接使用 [deploy/helm/gonavi-mcp-server](../../helm/gonavi-mcp-server)。 + +目录结构: + +- `kustomization.yaml`:基础部署入口 +- `base/kustomization.yaml`:基础资源集合 +- `overlays/nas-hostpath`:直接挂 NAS / 单机节点上的 GoNavi 数据目录 +- `base/deployment.yaml` / `base/service.yaml`:基础资源 +- `overlays/drivers-pvc`:为 `/data/drivers` 增加 PVC 挂载 +- `overlays/ingress`:增加 Ingress 暴露 `/mcp` +- `overlays/ingress-with-drivers-pvc`:同时启用 Ingress 和 drivers PVC + +## 前提 + +容器仍然依赖 GoNavi 的活动数据目录。至少要准备: + +- `connections.json` +- `daily_secrets.json` + +如果目标连接依赖可选 driver agent,还需要额外挂载 `/data/drivers`。 + +`base/deployment.yaml` 默认镜像指向: + +```text +ghcr.io/syngnat/gonavi-mcp-server:latest +``` + +如果你要跟随 `dev` 分支,可以改成: + +```text +ghcr.io/syngnat/gonavi-mcp-server:dev-latest +``` + +如果你要使用自建镜像,再按下面方式覆盖 `image`: + +- 在当前节点可访问的 Docker / Podman / containerd 环境中构建并导入这个镜像 +- 把镜像推送到你的私有仓库,并同步修改 `base/deployment.yaml` 里的 `image` + +例如: + +```bash +docker build -f Dockerfile.mcp-server -t gonavi-mcp-server:local . +# or +podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local . +``` + +## 1. 创建 Secret + +推荐直接从现有 GoNavi 数据文件生成: + +```bash +kubectl create secret generic gonavi-mcp-server-data \ + --from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \ + --from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \ + --from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token +``` + +如需单独 namespace,请先执行: + +```bash +kubectl create namespace gonavi +kubectl -n gonavi create secret generic gonavi-mcp-server-data \ + --from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \ + --from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \ + --from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token +``` + +## 2. 应用清单 + +```bash +kubectl apply -k deploy/k8s/gonavi-mcp-server +``` + +如果使用独立 namespace,请先把 YAML 里的 `namespace` 改成你的目标值,或自行用 `kustomize` / Helm 做二次封装。 + +清单内已经带了基础安全上下文和默认资源配额;如果你的连接数量、对象规模或并发访问更高,建议按实际负载调整 `resources`。 + +### 常用 Overlay + +仅基础部署: + +```bash +kubectl apply -k deploy/k8s/gonavi-mcp-server +``` + +需要可选 driver agent: + +```bash +kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc +``` + +NAS / k3s 单机节点直接挂宿主机目录: + +```bash +kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath +``` + +需要对外暴露 `/mcp`: + +```bash +kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/ingress +``` + +同时需要 Ingress 与 drivers PVC: + +```bash +kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc +``` + +应用 overlay 前,记得先改里面的占位值,例如: + +- `overlays/ingress/ingress.yaml` 里的域名、TLS Secret、`ingressClassName` +- `overlays/drivers-pvc/patch-deployment.yaml` 里的 PVC 名称 `gonavi-drivers` +- `overlays/nas-hostpath/patch-deployment.yaml` 里的宿主机目录 `/volume1/docker/gonavi/data` + +`nas-hostpath` overlay 会直接把整个 GoNavi 数据目录挂到 `/data`,因此 `connections.json`、`daily_secrets.json`、`drivers/` 都从宿主机目录读取;此时仍建议保留 `GONAVI_MCP_HTTP_TOKEN` 的 Secret 注入,不要把 token 直接硬编码进 Deployment。 + +## 3. 校验 + +```bash +kubectl get pods -n gonavi +kubectl get svc -n gonavi +kubectl port-forward -n gonavi svc/gonavi-mcp-server 8765:8765 +curl -H "Authorization: Bearer replace-with-a-random-token" http://127.0.0.1:8765/healthz +``` + +`/healthz` 返回 `ok` 说明 Pod 已对外提供 HTTP 服务。 + +## 4. 暴露给远端 Agent + +- 集群内使用:直接访问 `http://gonavi-mcp-server.gonavi.svc.cluster.local:8765/mcp` +- 集群外使用:通过 Ingress / Gateway / 反向代理暴露 `/mcp` +- 远端 Agent 请求头:`Authorization: Bearer <你的 token>` + +## 5. drivers 挂载 + +当前示例默认只挂载 `connections.json` 与 `daily_secrets.json`。如果保存连接里包含 Dameng、ClickHouse、DuckDB 等依赖 driver agent 的数据源,请在 Deployment 中额外补一个卷,把宿主机或 PVC 中的 `drivers/` 目录挂载到: + +```text +/data/drivers +``` + +否则对应连接会因为缺少 driver agent 无法使用。 diff --git a/deploy/k8s/gonavi-mcp-server/base/deployment.yaml b/deploy/k8s/gonavi-mcp-server/base/deployment.yaml new file mode 100644 index 0000000..b4c2766 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/base/deployment.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gonavi-mcp-server + namespace: gonavi + labels: + app.kubernetes.io/name: gonavi-mcp-server +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: gonavi-mcp-server + template: + metadata: + labels: + app.kubernetes.io/name: gonavi-mcp-server + spec: + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + seccompProfile: + type: RuntimeDefault + containers: + - name: gonavi-mcp-server + # 如需跟随 dev 分支,可改成 ghcr.io/syngnat/gonavi-mcp-server:dev-latest + image: ghcr.io/syngnat/gonavi-mcp-server:latest + imagePullPolicy: IfNotPresent + args: + - http + ports: + - name: http + containerPort: 8765 + protocol: TCP + env: + - name: GONAVI_DATA_ROOT + value: /data + - name: GONAVI_LOG_DIR + value: /var/lib/gonavi/logs + - name: GONAVI_MCP_HTTP_ADDR + value: 0.0.0.0:8765 + - name: GONAVI_MCP_HTTP_PATH + value: /mcp + - name: GONAVI_MCP_SCHEMA_ONLY + value: "true" + - name: GONAVI_MCP_HTTP_TOKEN + valueFrom: + secretKeyRef: + name: gonavi-mcp-server-data + key: GONAVI_MCP_HTTP_TOKEN + volumeMounts: + - name: gonavi-data + mountPath: /data + readOnly: true + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumes: + - name: gonavi-data + projected: + sources: + - secret: + name: gonavi-mcp-server-data + items: + - key: connections.json + path: connections.json + - key: daily_secrets.json + path: daily_secrets.json diff --git a/deploy/k8s/gonavi-mcp-server/base/kustomization.yaml b/deploy/k8s/gonavi-mcp-server/base/kustomization.yaml new file mode 100644 index 0000000..5b98e94 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/base/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml diff --git a/deploy/k8s/gonavi-mcp-server/base/service.yaml b/deploy/k8s/gonavi-mcp-server/base/service.yaml new file mode 100644 index 0000000..7521bb6 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/base/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: gonavi-mcp-server + namespace: gonavi + labels: + app.kubernetes.io/name: gonavi-mcp-server +spec: + selector: + app.kubernetes.io/name: gonavi-mcp-server + ports: + - name: http + port: 8765 + targetPort: http + protocol: TCP diff --git a/deploy/k8s/gonavi-mcp-server/kustomization.yaml b/deploy/k8s/gonavi-mcp-server/kustomization.yaml new file mode 100644 index 0000000..271aa1a --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - base diff --git a/deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/kustomization.yaml b/deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/kustomization.yaml new file mode 100644 index 0000000..6458920 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + +patches: + - path: patch-deployment.yaml diff --git a/deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/patch-deployment.yaml b/deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/patch-deployment.yaml new file mode 100644 index 0000000..06a311a --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc/patch-deployment.yaml @@ -0,0 +1,18 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gonavi-mcp-server + namespace: gonavi +spec: + template: + spec: + containers: + - name: gonavi-mcp-server + volumeMounts: + - name: gonavi-drivers + mountPath: /data/drivers + readOnly: true + volumes: + - name: gonavi-drivers + persistentVolumeClaim: + claimName: gonavi-drivers diff --git a/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/ingress.yaml b/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/ingress.yaml new file mode 100644 index 0000000..a87f378 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/ingress.yaml @@ -0,0 +1,33 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gonavi-mcp-server + namespace: gonavi + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" +spec: + ingressClassName: nginx + tls: + - hosts: + - gonavi-mcp.example.com + secretName: gonavi-mcp-server-tls + rules: + - host: gonavi-mcp.example.com + http: + paths: + - path: /mcp + pathType: Prefix + backend: + service: + name: gonavi-mcp-server + port: + number: 8765 + - path: /healthz + pathType: Prefix + backend: + service: + name: gonavi-mcp-server + port: + number: 8765 diff --git a/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/kustomization.yaml b/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/kustomization.yaml new file mode 100644 index 0000000..482a4ba --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + - ingress.yaml + +patches: + - path: patch-deployment.yaml diff --git a/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/patch-deployment.yaml b/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/patch-deployment.yaml new file mode 100644 index 0000000..06a311a --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc/patch-deployment.yaml @@ -0,0 +1,18 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gonavi-mcp-server + namespace: gonavi +spec: + template: + spec: + containers: + - name: gonavi-mcp-server + volumeMounts: + - name: gonavi-drivers + mountPath: /data/drivers + readOnly: true + volumes: + - name: gonavi-drivers + persistentVolumeClaim: + claimName: gonavi-drivers diff --git a/deploy/k8s/gonavi-mcp-server/overlays/ingress/ingress.yaml b/deploy/k8s/gonavi-mcp-server/overlays/ingress/ingress.yaml new file mode 100644 index 0000000..a87f378 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/ingress/ingress.yaml @@ -0,0 +1,33 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gonavi-mcp-server + namespace: gonavi + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" +spec: + ingressClassName: nginx + tls: + - hosts: + - gonavi-mcp.example.com + secretName: gonavi-mcp-server-tls + rules: + - host: gonavi-mcp.example.com + http: + paths: + - path: /mcp + pathType: Prefix + backend: + service: + name: gonavi-mcp-server + port: + number: 8765 + - path: /healthz + pathType: Prefix + backend: + service: + name: gonavi-mcp-server + port: + number: 8765 diff --git a/deploy/k8s/gonavi-mcp-server/overlays/ingress/kustomization.yaml b/deploy/k8s/gonavi-mcp-server/overlays/ingress/kustomization.yaml new file mode 100644 index 0000000..5c258d6 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/ingress/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + - ingress.yaml diff --git a/deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/kustomization.yaml b/deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/kustomization.yaml new file mode 100644 index 0000000..6458920 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + +patches: + - path: patch-deployment.yaml diff --git a/deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/patch-deployment.yaml b/deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/patch-deployment.yaml new file mode 100644 index 0000000..93ecbf9 --- /dev/null +++ b/deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath/patch-deployment.yaml @@ -0,0 +1,14 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gonavi-mcp-server + namespace: gonavi +spec: + template: + spec: + volumes: + - $patch: replace + name: gonavi-data + hostPath: + path: /volume1/docker/gonavi/data + type: Directory diff --git a/deploy/podman/gonavi-mcp-server/README.md b/deploy/podman/gonavi-mcp-server/README.md new file mode 100644 index 0000000..d879ea5 --- /dev/null +++ b/deploy/podman/gonavi-mcp-server/README.md @@ -0,0 +1,101 @@ +# GoNavi MCP Server Podman 示例 + +这个目录提供 `gonavi-mcp-server` 的 Podman 原生部署入口,覆盖两类常见场景: + +- 直接用 `podman run` 在 Linux 服务器 / NAS 上启动 +- 用 rootless Quadlet + systemd 做常驻服务 + +目录内容: + +- `gonavi-mcp-server.env.example`:给 `podman run` / Quadlet 共用的容器环境变量示例 +- `gonavi-mcp-server.container`:Quadlet 示例 + +前提仍然一样:宿主机 GoNavi 活动数据目录内至少应包含: + +- `connections.json` +- `daily_secrets.json` + +如果目标连接依赖可选 driver agent,还要保证同一数据目录下已有 `drivers/`。 + +## 1. 直接运行已发布镜像 + +```bash +cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ./gonavi-mcp-server.env +``` + +把 `GONAVI_MCP_HTTP_TOKEN` 改成随机值后运行: + +```bash +podman run -d --name gonavi-mcp-server --replace \ + -p 8765:8765 \ + --env-file ./gonavi-mcp-server.env \ + -v /absolute/path/to/gonavi-data:/data:Z \ + ghcr.io/syngnat/gonavi-mcp-server:latest http +``` + +查看状态: + +```bash +podman ps +podman logs -f gonavi-mcp-server +``` + +如果你的宿主机没有启用 SELinux,可把挂载参数末尾的 `:Z` 去掉;如果启用了 SELinux,建议保留它。 + +## 2. 本地源码构建镜像 + +仓库根目录已经提供 `.containerignore`,Podman 会优先读取它来裁剪构建上下文。 + +```bash +podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local . +podman run -d --name gonavi-mcp-server --replace \ + -p 8765:8765 \ + --env-file ./gonavi-mcp-server.env \ + -v /absolute/path/to/gonavi-data:/data:Z \ + localhost/gonavi-mcp-server:local http +``` + +## 3. Rootless Quadlet 常驻服务 + +Podman Quadlet 适合 Linux 服务器 / NAS 上做 rootless 常驻服务。 + +把文件放到用户级目录: + +```bash +mkdir -p ~/.config/containers/systemd +cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container ~/.config/containers/systemd/ +cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ~/.config/containers/systemd/gonavi-mcp-server.env +``` + +然后修改两处: + +- `~/.config/containers/systemd/gonavi-mcp-server.container` 里的 `Volume=/absolute/path/to/gonavi-data:/data:Z` +- `~/.config/containers/systemd/gonavi-mcp-server.env` 里的 `GONAVI_MCP_HTTP_TOKEN` + +启动服务: + +```bash +systemctl --user daemon-reload +systemctl --user enable --now gonavi-mcp-server.service +systemctl --user status gonavi-mcp-server.service +journalctl --user -u gonavi-mcp-server.service -f +``` + +如果你希望用户退出登录后服务仍然保持运行,再执行: + +```bash +loginctl enable-linger "$USER" +``` + +Rootless Quadlet 默认搜索路径是 `~/.config/containers/systemd/`;如果你要做系统级 rootful 部署,可改放到 `/etc/containers/systemd/`。 + +## 4. 关于 Compose + +Podman 的 `podman compose` 依赖外部 compose provider。也就是说,是否能直接复用仓库根目录的 `docker-compose.mcp-server.yml`,取决于你的 Podman 环境是否已经安装并配置了对应 provider。 + +因此,这个仓库对 Podman 的主支持路径是: + +- `podman run` +- Quadlet(推荐长期运行) + +如果你的环境已经有可用的 compose provider,再去复用根目录 Compose 文件即可。 diff --git a/deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container b/deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container new file mode 100644 index 0000000..5126b99 --- /dev/null +++ b/deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container @@ -0,0 +1,22 @@ +[Unit] +Description=GoNavi MCP Server (rootless Podman) +Wants=network-online.target +After=network-online.target + +[Container] +ContainerName=gonavi-mcp-server +Image=ghcr.io/syngnat/gonavi-mcp-server:latest +Pull=newer +EnvironmentFile=./gonavi-mcp-server.env +# 修改成你的 GoNavi 活动数据目录;SELinux 主机建议保留 :Z +Volume=/absolute/path/to/gonavi-data:/data:Z +PublishPort=8765:8765 +Exec=http +LogDriver=journald + +[Service] +Restart=always +TimeoutStartSec=900 + +[Install] +WantedBy=default.target diff --git a/deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example b/deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example new file mode 100644 index 0000000..a0d2f8f --- /dev/null +++ b/deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example @@ -0,0 +1,15 @@ +# 容器内 GoNavi 活动数据根目录。通常不需要修改;Volume= 已把宿主机目录挂到 /data。 +GONAVI_DATA_ROOT=/data + +# 日志目录。默认与镜像内目录保持一致。 +GONAVI_LOG_DIR=/var/lib/gonavi/logs + +# MCP HTTP 监听地址与路径。 +GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765 +GONAVI_MCP_HTTP_PATH=/mcp + +# 远程 MCP 客户端访问时必须携带的 Bearer Token +GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token + +# true = 只暴露结构查询工具,不注册 execute_sql +GONAVI_MCP_SCHEMA_ONLY=true diff --git a/docker-compose.mcp-server.local.yml b/docker-compose.mcp-server.local.yml new file mode 100644 index 0000000..77552c7 --- /dev/null +++ b/docker-compose.mcp-server.local.yml @@ -0,0 +1,7 @@ +services: + gonavi-mcp-server: + build: + context: . + dockerfile: Dockerfile.mcp-server + image: ${GONAVI_MCP_IMAGE_LOCAL:-gonavi-mcp-server:local} + pull_policy: never diff --git a/docker-compose.mcp-server.yml b/docker-compose.mcp-server.yml new file mode 100644 index 0000000..b46a77d --- /dev/null +++ b/docker-compose.mcp-server.yml @@ -0,0 +1,20 @@ +services: + gonavi-mcp-server: + image: ${GONAVI_MCP_IMAGE:-ghcr.io/syngnat/gonavi-mcp-server:latest} + pull_policy: ${GONAVI_MCP_PULL_POLICY:-missing} + container_name: gonavi-mcp-server + restart: unless-stopped + ports: + - "${GONAVI_MCP_HTTP_PORT:-8765}:8765" + environment: + GONAVI_DATA_ROOT: /data + GONAVI_LOG_DIR: /var/lib/gonavi/logs + GONAVI_MCP_HTTP_ADDR: 0.0.0.0:8765 + GONAVI_MCP_HTTP_PATH: ${GONAVI_MCP_HTTP_PATH:-/mcp} + GONAVI_MCP_HTTP_TOKEN: ${GONAVI_MCP_HTTP_TOKEN} + GONAVI_MCP_SCHEMA_ONLY: ${GONAVI_MCP_SCHEMA_ONLY:-true} + volumes: + - type: bind + source: ${GONAVI_HOST_DATA_ROOT} + target: /data + command: ["http"] diff --git a/docker.mcp-server.env.example b/docker.mcp-server.env.example new file mode 100644 index 0000000..698ffc0 --- /dev/null +++ b/docker.mcp-server.env.example @@ -0,0 +1,20 @@ +# 宿主机 GoNavi 活动数据目录,目录内至少应包含 connections.json 与 daily_secrets.json +GONAVI_HOST_DATA_ROOT=/absolute/path/to/gonavi-data + +# 预构建镜像地址。稳定版通常使用 latest,跟随 dev 分支可改成 ghcr.io/syngnat/gonavi-mcp-server:dev-latest +GONAVI_MCP_IMAGE=ghcr.io/syngnat/gonavi-mcp-server:latest + +# 预构建镜像拉取策略:always / missing / never +GONAVI_MCP_PULL_POLICY=missing + +# 远程 MCP 客户端访问时必须携带的 Bearer Token +GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token + +# 宿主机暴露端口 +GONAVI_MCP_HTTP_PORT=8765 + +# MCP HTTP 路径 +GONAVI_MCP_HTTP_PATH=/mcp + +# true = 只暴露结构查询工具,不注册 execute_sql +GONAVI_MCP_SCHEMA_ONLY=true