feat(mcp-docugen): scaffold service + Docker stack con gateway Caddy

Task 0 del piano (adattato a workspace uv):
- services/mcp-docugen/: pyproject.toml, src/mcp_docugen/, tests/unit+integration/,
  README, .env.example. Package rinominato da docugen_mcp -> mcp_docugen.
- Root pyproject.toml: aggiunto services/mcp-docugen a workspace members.
- .python-version: 3.11
- uv.lock committato.

Docker stack stile CerberoSuite/Cerbero con prefisso "arca-":
- docker/base.Dockerfile -> arca-base:latest
- docker/mcp-docugen.Dockerfile -> arca-mcp-docugen:dev (porta interna 9100,
  label arca.service, runtime multi-stage, user non-root, healthcheck)
- docker-compose.yml root: gateway Caddy unica porta host (8080) + mcp-docugen
  su rete interna. Security defaults cap_drop ALL, no-new-privileges, read_only
  ove applicabile, restart unless-stopped.
- gateway/Caddyfile: reverse proxy /mcp-docugen/* -> mcp-docugen:9100 + landing.
- gateway/public/index.html: landing page minimale.

.env.example root aggiornato con DOCUGEN_API_KEY + OPENROUTER_API_KEY condivisa.

Task 1-12 (implementazione TDD effettiva) ancora da fare.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 12:16:22 +02:00
parent 7b169fb8db
commit c5e84a578b
16 changed files with 1692 additions and 8 deletions
+9 -7
View File
@@ -1,10 +1,12 @@
# Root stack config (gateway/compose). Ogni servizio aggiunge il proprio .env.example.
# Porta host del gateway (reverse proxy Caddy/nginx) — da definire
# Porta host del gateway Caddy (unico accesso esterno allo stack)
GATEWAY_PORT=8080
# Token OpenRouter condiviso fra MCP che usano LLM
# OPENROUTER_API_KEY=
# URL pubblico del gateway (usato dai servizi per costruire link asset assoluti)
PUBLIC_BASE_URL=http://localhost:8080/mcp-docugen
# Dominio pubblico (usato da servizi che generano URL assoluti)
# PUBLIC_BASE_DOMAIN=arca.tielogic.xyz
# ===== mcp-docugen =====
# Bearer API key che i client MCP (Claude Code, ecc.) useranno
DOCUGEN_API_KEY=
# ===== OpenRouter (condiviso fra MCP che usano LLM) =====
OPENROUTER_API_KEY=
+1
View File
@@ -0,0 +1 @@
3.11
+58
View File
@@ -0,0 +1,58 @@
networks:
internal:
driver: bridge
volumes:
docugen-data:
caddy-data:
caddy-config:
x-common-security: &common-security
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
restart: unless-stopped
networks: [internal]
services:
# ========================================================
# GATEWAY — unica porta host, reverse proxy + landing page
# ========================================================
gateway:
image: caddy:2-alpine
restart: unless-stopped
networks: [internal]
security_opt:
- no-new-privileges:true
ports: ["${GATEWAY_PORT:-8080}:8080"]
volumes:
- ./gateway/Caddyfile:/etc/caddy/Caddyfile:ro
- ./gateway/public:/srv:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
mcp-docugen: { condition: service_healthy }
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/"]
interval: 30s
timeout: 5s
retries: 3
# ========================================================
# MCP — accessibili solo via gateway (nessuna porta host)
# ========================================================
mcp-docugen:
image: arca-mcp-docugen:dev
build:
context: .
dockerfile: docker/mcp-docugen.Dockerfile
<<: *common-security
user: "1000:1000"
environment:
API_KEY: ${DOCUGEN_API_KEY}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:8080/mcp-docugen}
DATA_DIR: /data
volumes:
- docugen-data:/data
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.11-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential curl \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir "uv>=0.5,<0.7"
WORKDIR /app
COPY pyproject.toml uv.lock* ./
ENV PATH="/app/.venv/bin:$PATH"
+26
View File
@@ -0,0 +1,26 @@
ARG BASE_TAG=latest
FROM arca-base:${BASE_TAG} AS builder
COPY services/mcp-docugen ./services/mcp-docugen
RUN uv sync --frozen --no-dev --package mcp-docugen
FROM python:3.11-slim AS runtime
LABEL org.opencontainers.image.source="https://git.tielogic.xyz/Adriano/ArcaSuite" \
arca.service="mcp-docugen"
WORKDIR /app
COPY --from=builder /app /app
ENV PATH="/app/.venv/bin:$PATH"
RUN useradd -m -u 1000 app && \
mkdir -p /data && chown app:app /data
USER app
VOLUME ["/data"]
ENV HOST=0.0.0.0 PORT=9100 DATA_DIR=/data
EXPOSE 9100
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=15s \
CMD python -c "import httpx, os; httpx.get(f'http://localhost:{os.environ.get(\"PORT\",\"9100\")}/health').raise_for_status()"
CMD ["mcp-docugen"]
+21
View File
@@ -0,0 +1,21 @@
{
admin off
auto_https off
}
:8080 {
log {
output stdout
format console
}
handle_path /mcp-docugen/* {
reverse_proxy mcp-docugen:9100
}
# Landing page statica
handle {
root * /srv
file_server
}
}
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>ArcaSuite</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 640px;
margin: 3em auto;
padding: 0 1em;
color: #222;
background: #fafafa;
}
h1 { font-weight: 500; }
ul { padding-left: 1.2em; }
code { background: #eee; padding: 0.1em 0.3em; border-radius: 3px; }
a { color: #0366d6; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>ArcaSuite</h1>
<p>MCP server stack — gateway Caddy. Servizi attivi esposti come path:</p>
<ul>
<li><a href="/mcp-docugen/health">/mcp-docugen/</a> — generazione Markdown da template + LLM</li>
</ul>
<p><small>Accesso via MCP richiede header <code>Authorization: Bearer &lt;API_KEY&gt;</code>.</small></p>
</body>
</html>
+3 -1
View File
@@ -1,5 +1,7 @@
[tool.uv.workspace]
members = []
members = [
"services/mcp-docugen",
]
[tool.ruff]
line-length = 100
+18
View File
@@ -0,0 +1,18 @@
# Authentication (bearer token che i client MCP passano)
API_KEY=
# OpenRouter
OPENROUTER_API_KEY=
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
LLM_MODEL_DEFAULT=anthropic/claude-sonnet-4
# URL pubblico usato per costruire link asset nel Markdown generato
PUBLIC_BASE_URL=https://mcp-docugen.<dominio>
# Storage persistente (mappato su volume in Docker)
DATA_DIR=/data
# Limiti
ASSET_TTL_DAYS=30
MAX_IMAGE_SIZE_MB=10
LLM_TIMEOUT_SECONDS=60
+37
View File
@@ -0,0 +1,37 @@
# mcp-docugen
MCP server per generazione documenti Markdown da template + LLM (OpenRouter).
Design: [`../../docs/mcp-docugen-design.md`](../../docs/mcp-docugen-design.md)
Plan: [`../../docs/mcp-docugen-implementation.md`](../../docs/mcp-docugen-implementation.md)
## Dev
Dalla root del repo:
```bash
uv sync --all-groups
uv run --package mcp-docugen mcp-docugen
```
## Test
```bash
uv run --package mcp-docugen pytest services/mcp-docugen
```
## Docker
```bash
# build (dalla root)
docker build -f docker/base.Dockerfile -t arca-base:latest .
docker build -f docker/mcp-docugen.Dockerfile -t arca-mcp-docugen:dev .
# o via compose
docker compose build mcp-docugen
docker compose up mcp-docugen
```
## Env
Vedi `.env.example`. Variabili obbligatorie: `API_KEY`, `OPENROUTER_API_KEY`, `PUBLIC_BASE_URL`.
+40
View File
@@ -0,0 +1,40 @@
[project]
name = "mcp-docugen"
version = "0.1.0"
description = "MCP server for document generation from Markdown templates via OpenRouter"
requires-python = ">=3.11"
dependencies = [
"mcp>=1.2",
"fastapi>=0.115",
"uvicorn[standard]>=0.34",
"pydantic>=2.0",
"pydantic-settings>=2.0",
"httpx>=0.27",
"aiosqlite>=0.20",
"aiofiles>=24.0",
"pyyaml>=6.0",
"pillow>=11.0",
"python-multipart>=0.0.9",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"respx>=0.21",
"pytest-cov>=5.0",
]
[project.scripts]
mcp-docugen = "mcp_docugen.main:run"
[tool.coverage.run]
source = ["src/mcp_docugen"]
omit = ["src/mcp_docugen/main.py"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_docugen"]
@@ -0,0 +1 @@
__version__ = "0.1.0"
Generated
+1435
View File
File diff suppressed because it is too large Load Diff