Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: docker-publish

on:
push:
branches:
- main
paths:
- "docs/**"
- "scripts/**"
- "requirements-mcp.txt"
- "retrieval-rules.yml"
- "zensical.toml"
- "docker-compose-mcp-only/**"
- ".github/workflows/docker-publish.yml"
workflow_dispatch:

env:
REGISTRY_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'malikovpro/v8std-mcp' }}

jobs:
publish:
if: ${{ vars.DOCKERHUB_IMAGE != '' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=latest
type=raw,value={{date 'YYYY-MM-DD'}}
type=sha,format=long
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: docker-compose-mcp-only/docker/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
REVISION=${{ github.sha }}
CREATED=${{ github.event.head_commit.timestamp }}
cache-from: type=gha
cache-to: type=gha,mode=max
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ docker compose -f docker-compose/docker-compose.yml up -d v8std-mcp
MCP-сервер будет доступен на `http://127.0.0.1:8765/mcp` и читает локальный индекс
`docs/ai/pages.jsonl` из смонтированного репозитория.

Только MCP, без сервера документации — из готового образа Docker Hub (индекс
запечён на этапе сборки, монтирование репозитория не нужно):

```bash
docker compose -f docker-compose-mcp-only/docker-compose.yml up -d v8std-mcp
```

Запуск без клонирования репозитория:

```bash
docker run -d --name v8std-mcp -p 127.0.0.1:8765:8765 malikovpro/v8std-mcp:latest
```

```bash
codex mcp add v8std-local --url http://127.0.0.1:8765/mcp
```
15 changes: 15 additions & 0 deletions docker-compose-mcp-only/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Скопируйте в .env рядом с docker-compose.yml: cp .env.example .env
# Значения из .env подставляются в docker-compose-mcp-only/docker-compose.yml.

# Адреса, по которым к серверу обращаются из сети (проверяется Host-заголовок).
# Формат: точный адрес или хост с любым портом (хост:*), разделитель — запятая
# или пробел. Вайлдкарды в середине адреса (192.168.1.*) не поддерживаются.
# V8STD_MCP_ALLOWED_HOSTS=192.168.1.55:*, v8std.local:*

# Origin-заголовки браузерных клиентов; редакторам (codex, claude, cursor)
# обычно не требуется — Origin у них отсутствует и разрешен.
# V8STD_MCP_ALLOWED_ORIGINS=http://192.168.1.55:8765

# Интерфейс публикации порта 8765: 127.0.0.1 (по умолчанию — только локально),
# 0.0.0.0 (все интерфейсы) или конкретный IP сервера.
# V8STD_MCP_BIND=0.0.0.0
1 change: 1 addition & 0 deletions docker-compose-mcp-only/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
20 changes: 20 additions & 0 deletions docker-compose-mcp-only/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
v8std-mcp:
container_name: v8std-mcp
image: malikovpro/v8std-mcp:latest
build:
context: ..
dockerfile: docker-compose-mcp-only/docker/Dockerfile.mcp
restart: always
ports:
- "${V8STD_MCP_BIND:-127.0.0.1}:8765:8765"
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment:
V8STD_MCP_GENERATE_INDEX: auto
V8STD_MCP_HOST: 0.0.0.0
V8STD_MCP_PORT: "8765"
V8STD_MCP_ALLOWED_HOSTS: "${V8STD_MCP_ALLOWED_HOSTS:-}"
V8STD_MCP_ALLOWED_ORIGINS: "${V8STD_MCP_ALLOWED_ORIGINS:-}"
45 changes: 45 additions & 0 deletions docker-compose-mcp-only/docker/Dockerfile.mcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
FROM python:3.12-alpine

ARG PIP_INDEX_URL=https://pypi.org/simple
ARG REVISION=unknown
ARG CREATED=unknown

LABEL org.opencontainers.image.title="v8std MCP" \
org.opencontainers.image.description="Локальный MCP-сервер стандартов разработки 1С (v8std.ru)" \
org.opencontainers.image.source="https://github.com/zeegin/v8std" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}" \
org.opencontainers.image.licenses="CC0-1.0"

ENV V8STD_REPO_ROOT=/opt/v8std \
V8STD_MCP_GENERATE_INDEX=auto \
V8STD_MCP_HOST=0.0.0.0 \
V8STD_MCP_PORT=8765 \
PYTHONUNBUFFERED=1

WORKDIR /opt/v8std

RUN apk add --no-cache bash

COPY requirements-mcp.txt /opt/v8std/requirements-mcp.txt
COPY scripts /opt/v8std/scripts
COPY docs /opt/v8std/docs
COPY zensical.toml retrieval-rules.yml /opt/v8std/

RUN chmod +x /opt/v8std/scripts/run_v8std_mcp.sh \
/opt/v8std/scripts/v8std_mcp_server.py \
&& PIP_INDEX_URL="${PIP_INDEX_URL}" python -m pip install --no-cache-dir -r /opt/v8std/requirements-mcp.txt \
&& python scripts/generate_ai_artifacts.py \
&& python scripts/generate_search_vectors.py \
--pages docs/ai/pages.jsonl \
--output docs/ai/search-vectors.jsonl

RUN addgroup -S v8std \
&& adduser -S -D -H -G v8std v8std \
&& chown -R v8std:v8std /opt/v8std

USER v8std

EXPOSE 8765

ENTRYPOINT ["/opt/v8std/scripts/run_v8std_mcp.sh"]
8 changes: 8 additions & 0 deletions docker-compose-mcp-only/docker/Dockerfile.mcp.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
**
!requirements-mcp.txt
!scripts/
!scripts/**
!docs/
!docs/**
!zensical.toml
!retrieval-rules.yml
36 changes: 36 additions & 0 deletions docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,42 @@ cd v8std
docker compose -f docker-compose/docker-compose.yml up -d v8std-mcp
```

Только MCP, без сервера документации — из готового образа Docker Hub:

```bash
docker compose -f docker-compose-mcp-only/docker-compose.yml up -d v8std-mcp
```

Можно обойтись без клонирования репозитория. Готовый образ содержит базу
стандартов и индекс поиска:

```bash
docker run -d --name v8std-mcp -p 127.0.0.1:8765:8765 malikovpro/v8std-mcp:latest
```

Образ обновляется вместе с базой стандартов. Тег `latest` указывает на свежую
сборку, датированные теги вида `2026-09-01` и теги вида `sha-…` позволяют
зафиксировать конкретную версию. Актуальность базы в запущенном контейнере
зависит от даты сборки образа.

По умолчанию сервер принимает запросы только с `localhost` — это защита от
DNS-rebinding: без нее страница злоумышленника в браузере могла бы обращаться
к вашему локальному MCP и читать ответы, включая отправленные фрагменты кода.

Доступ из локальной сети — добавьте в окружение адреса, по которым к серверу
будут обращаться, и опубликуйте порт на нужном интерфейсе:

```bash
V8STD_MCP_ALLOWED_HOSTS="192.168.1.55:*, v8std.local:*" \
V8STD_MCP_BIND=0.0.0.0 \
docker compose -f docker-compose-mcp-only/docker-compose.yml up -d v8std-mcp
```

Паттерны поддерживают точный адрес (`192.168.1.55:*`) и суффикс `:*`
(любой порт). Вайлдкарды в середине адреса (`192.168.1.*`) не поддерживаются —
перечисляйте адреса явно. Подключение: `http://192.168.1.55:8765/mcp` или
`http://v8std.local:8765/mcp`.

Локальный адрес MCP:

```text
Expand Down
3 changes: 2 additions & 1 deletion requirements-mcp.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mcp==1.27.0
PyYAML
PyYAML==6.0.3
Pillow==12.3.0
132 changes: 132 additions & 0 deletions scripts/build_mcp_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"

IMAGE="malikovpro/v8std-mcp"
LOCAL_TAG="local"
PUSH=0
SMOKE=0
PIP_INDEX_URL="${V8STD_PIP_INDEX_URL:-https://pypi.org/simple}"
PLATFORMS=""

usage() {
cat <<EOF
Сборка и публикация self-contained MCP-образа v8std.

Использование:
scripts/build_mcp_image.sh [опции]

Опции:
--image NAME Имя образа (по умолчанию malikovpro/v8std-mcp)
--tag TAG Локальный тег (по умолчанию local)
--pip-index URL Индекс PyPI (по умолчанию https://pypi.org/simple)
--platforms LIST Платформы для локальной сборки (по умолчанию — архитектура хоста)
--push Мультиархитектурная сборка и push в Docker Hub:
NAME:latest, NAME:YYYY-MM-DD, NAME:sha-XXXXXXXXXXXX
--smoke После локальной сборки запустить контейнер и проверить
ответ MCP на http://127.0.0.1:8765/mcp

Примеры:
scripts/build_mcp_image.sh --smoke
scripts/build_mcp_image.sh --push
EOF
}

while [ $# -gt 0 ]; do
case "$1" in
--image) IMAGE="$2"; shift 2 ;;
--tag) LOCAL_TAG="$2"; shift 2 ;;
--pip-index) PIP_INDEX_URL="$2"; shift 2 ;;
--platforms) PLATFORMS="$2"; shift 2 ;;
--push) PUSH=1; shift ;;
--smoke) SMOKE=1; shift ;;
-h|--help) usage; exit 0 ;;
*) printf 'неизвестная опция: %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
esac
done

if [ "${PUSH}" -eq 1 ] && [ "${SMOKE}" -eq 1 ]; then
printf -- '--smoke работает только с локальной сборкой (без --push)\n' >&2
exit 2
fi

REVISION="$(git -C "${REPO_ROOT}" rev-parse HEAD)"
CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
DATE_TAG="$(date -u +%Y-%m-%d)"
SHA_TAG="sha-$(git -C "${REPO_ROOT}" rev-parse --short=12 HEAD)"
DOCKERFILE="docker-compose-mcp-only/docker/Dockerfile.mcp"

BUILDER=""
SMOKE_NAME=""

cleanup() {
if [ -n "${BUILDER}" ]; then
docker buildx rm -f "${BUILDER}" >/dev/null 2>&1 || true
fi
if [ -n "${SMOKE_NAME}" ]; then
docker rm -f "${SMOKE_NAME}" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT

BUILD_ARGS=(
--build-arg "REVISION=${REVISION}"
--build-arg "CREATED=${CREATED}"
--build-arg "PIP_INDEX_URL=${PIP_INDEX_URL}"
)

cd "${REPO_ROOT}"

if [ "${PUSH}" -eq 1 ]; then
BUILDER="v8std-mcp-builder-$$"
docker buildx create --name "${BUILDER}" --driver docker-container --use >/dev/null
docker buildx build \
--file "${DOCKERFILE}" \
--platform "linux/amd64,linux/arm64" \
--tag "${IMAGE}:latest" \
--tag "${IMAGE}:${DATE_TAG}" \
--tag "${IMAGE}:${SHA_TAG}" \
--push \
"${BUILD_ARGS[@]}" \
.
printf 'запушено: %s:latest, %s:%s, %s:%s\n' "${IMAGE}" "${IMAGE}" "${DATE_TAG}" "${IMAGE}" "${SHA_TAG}"
else
PLATFORM_ARGS=()
if [ -n "${PLATFORMS}" ]; then
PLATFORM_ARGS=(--platform "${PLATFORMS}")
fi
docker build \
--file "${DOCKERFILE}" \
"${PLATFORM_ARGS[@]}" \
--tag "${IMAGE}:${LOCAL_TAG}" \
"${BUILD_ARGS[@]}" \
.
printf 'собрано: %s:%s\n' "${IMAGE}" "${LOCAL_TAG}"
fi

if [ "${SMOKE}" -eq 1 ]; then
SMOKE_NAME="v8std-mcp-smoke-$$"
docker run -d --name "${SMOKE_NAME}" \
-p 127.0.0.1:8765:8765 \
"${IMAGE}:${LOCAL_TAG}" >/dev/null
ok=0
for _ in $(seq 1 30); do
code="$(curl --noproxy '*' -s -o /dev/null -w '%{http_code}' --max-time 2 http://127.0.0.1:8765/mcp || true)"
if [ "${code}" != "000" ]; then
ok=1
printf 'smoke: MCP ответил HTTP %s на http://127.0.0.1:8765/mcp\n' "${code}"
break
fi
sleep 1
done
if [ "${ok}" -ne 1 ]; then
printf 'smoke: нет ответа на http://127.0.0.1:8765/mcp, логи контейнера:\n' >&2
docker logs "${SMOKE_NAME}" >&2 || true
exit 1
fi
docker rm -f "${SMOKE_NAME}" >/dev/null
SMOKE_NAME=""
fi
15 changes: 14 additions & 1 deletion scripts/run_v8std_mcp.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ if [ ! -s "${PAGES_PATH}" ]; then
exit 1
fi

extra_transport_args=()
if [ -n "${V8STD_MCP_ALLOWED_HOSTS:-}" ]; then
for pattern in ${V8STD_MCP_ALLOWED_HOSTS//,/ }; do
extra_transport_args+=(--allowed-host "${pattern}")
done
fi
if [ -n "${V8STD_MCP_ALLOWED_ORIGINS:-}" ]; then
for pattern in ${V8STD_MCP_ALLOWED_ORIGINS//,/ }; do
extra_transport_args+=(--allowed-origin "${pattern}")
done
fi

exec python "${SCRIPT_DIR}/v8std_mcp_server.py" \
--pages "${PAGES_PATH}" \
--vectors "${VECTORS_PATH}" \
Expand All @@ -42,4 +54,5 @@ exec python "${SCRIPT_DIR}/v8std_mcp_server.py" \
--allowed-host "localhost:*" \
--allowed-host "v8std-mcp:*" \
--allowed-origin "http://127.0.0.1:*" \
--allowed-origin "http://localhost:*"
--allowed-origin "http://localhost:*" \
"${extra_transport_args[@]}"