Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:36:21 +02:00
commit cb5be898ea
663 files changed
+82674

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
# SAFE TEMPLATE ONLY. Copy outside Git and replace every REPLACE_* value.
# This file contains references and ordinary configuration, never credentials.
# Operator-provided host paths and identity
UNRAID_HOST=REPLACE_WITH_UNRAID_LAN_HOST_OR_IP
APPDATA_ROOT=/srv/example/appdata/itworx-mcp-hub
SECRETS_DIR=/srv/example/appdata/itworx-mcp-hub/secrets
PROJECTS_READONLY_ROOT=/srv/example/appdata/itworx-mcp-hub/sources
KNOWLEDGE_SOURCES_JSON=[{"project":"REPLACE_PROJECT","sourceRef":"repository","path":"REPLACE_RELATIVE_SOURCE_PATH","sourceType":"documentation","revision":"REPLACE_WITH_IMMUTABLE_COMMIT"}]
BACKUP_ROOT=/srv/example/appdata/itworx-mcp-hub/backups
PUID=REPLACE_WITH_NUMERIC_UID
PGID=REPLACE_WITH_NUMERIC_GID
DOCKER_SOCKET_GID=REPLACE_WITH_DOCKER_SOCKET_NUMERIC_GID
# Operator-provided bind addresses and generated Compose ports
HUB_BIND_IP=REPLACE_WITH_UNRAID_LAN_IP
MCP_BIND_IP=REPLACE_WITH_UNRAID_LAN_IP
HUB_PORT=1100
MCP_PORT=18442
INSPECTOR_BIND_IP=REPLACE_ONLY_IF_INSPECTOR_IS_ENABLED
INSPECTOR_PORT=18443
# Registry references. REGISTRY_CREDENTIAL_REF names an external credential
# helper/login context; it is not a password.
REGISTRY_HOST=REPLACE_WITH_REGISTRY_HOST
REGISTRY_NAMESPACE=REPLACE_WITH_REGISTRY_NAMESPACE
REGISTRY_CREDENTIAL_REF=REPLACE_WITH_OPERATOR_CREDENTIAL_REFERENCE
# Immutable owned images: registry/path:tag@sha256:<64 lowercase hex>.
HUB_CONTROL_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
HUB_WORKER_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
HUB_DATAPLANE_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
HUB_WEB_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_KNOWLEDGE_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_GITEA_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_UNRAID_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_PROJECTBRAIN_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_APPOPS_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_STITCH_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
DOCKER_READ_BROKER_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
OBSERVABILITY_COLLECTOR_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
# Authentik/OIDC ordinary configuration. Secret values live only in
# $SECRETS_DIR/authentik_oidc_client_secret and hub_session_signing_key.
HUB_OIDC_ISSUER=https://REPLACE_WITH_AUTHENTIK_HOST/application/o/itworx/
HUB_OIDC_AUDIENCE=itworx-mcp-hub
HUB_OIDC_CLIENT_ID=REPLACE_WITH_AUTHENTIK_CLIENT_ID
HUB_OIDC_REDIRECT_URI=https://REPLACE_WITH_MCP_HOST/auth/callback
HUB_OIDC_ROLE_GROUP_MAP={"REPLACE_WITH_ADMIN_GROUP":"admin","REPLACE_WITH_OPERATOR_GROUP":"operator"}
HUB_MCP_ADDITIONAL_OIDC_ISSUER=https://REPLACE_WITH_AUTHENTIK_HOST/application/o/chatgpt-itworx-gitea/
HUB_MCP_ADDITIONAL_OIDC_AUDIENCE=chatgpt-itworx-gitea
HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID=chatgpt-itworx-gitea
HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI=https://chatgpt.com/connector/oauth/REPLACE_WITH_CONNECTOR_ID
HUB_PUBLIC_MCP_URL=https://REPLACE_WITH_MCP_HOST
HUB_RATE_LIMIT_PER_MINUTE=600
HUB_UPSTREAM_MAX_CONCURRENCY=24
HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS=5
AUTHENTIK_PROVIDER_ID=REPLACE_WITH_PROVIDER_ID
AUTHENTIK_HUB_APPLICATION_ID=REPLACE_WITH_APPLICATION_ID
AUTHENTIK_SCOPES=openid,profile,email,groups,offline_access
AUTHENTIK_ALLOWED_GROUPS=REPLACE_WITH_COMMA_SEPARATED_GROUPS
AUTHENTIK_SERVICE_ACCOUNTS=["REPLACE_WITH_SCOPED_SERVICE_ACCOUNT"]
MCP_CLIENT_BINDINGS_JSON=[{"clientId":"REPLACE_WITH_UUID","subject":"REPLACE_WITH_OIDC_SUBJECT","project":"REPLACE_PROJECT","environment":"prod","enabled":true}]
# ContextForge internal endpoint/configuration. Credentials and key material
# live in the named files under SECRETS_DIR.
HUB_CONTEXTFORGE_URL=http://contextforge-control:4444
CONTEXTFORGE_DB_HOST=postgres
CONTEXTFORGE_DB_NAME=contextforge
CONTEXTFORGE_DB_USER=contextforge
REDIS_AUTH_ENABLED=true
# Existing Gitea; this deploys only the connector, never another Gitea server.
GITEA_ENDPOINT=https://REPLACE_WITH_GITEA_HOST
GITEA_REPOSITORIES_JSON=[{"project":"REPLACE_PROJECT","owner":"REPLACE_OWNER","name":"REPLACE_REPOSITORY"}]
GITEA_ADDITIONAL_PROJECTS=gitea-loop
GITEA_AUTO_DISCOVER_PROJECT=
GITEA_WRITE_OWNER=
GITEA_TOKEN_SCOPE=read:repository
GITEA_WRITE_TOKEN_SCOPE=write:repository,write:issue
GITEA_ENABLE_PR_WRITES=true
# Existing ProjectBrain; the Hub uses a dedicated server-to-server secret file.
PROJECTBRAIN_ENDPOINT=https://projectbrain.example.com
PROJECTBRAIN_PROJECTS=gitea-all
# Existing private AppOps MCP; the Hub reuses its dedicated MCP service token.
APPOPS_ENDPOINT=http://198.18.10.150:1216
APPOPS_PROJECTS=server-ops
# Google Stitch remote MCP. The API key lives only in $SECRETS_DIR/stitch_api_key.
STITCH_HUB_PROJECTS=stitch-design
# Existing NPM and DNS inputs. NETWORK_TARGETS_JSON is checked only with the
# explicit --verify-network-targets flag.
#
# Production uses a single public hostname (see
# infrastructure/nginx-proxy-manager/ROUTING.md): one NPM proxy host and one
# DNS record for REPLACE_WITH_MCP_HOST, routed by path (default location ->
# web:8080, "/mcp" custom location -> dataplane-router:8080). The *_HUB_*
# variables below are the management-surface half of that single-host
# routing config, not a second host -- set them to the exact same hostname
# value as the *_MCP_* variables. Never set a distinct hub.example.com value.
NPM_NETWORK_NAME=REPLACE_WITH_EXISTING_NPM_NETWORK
NPM_HUB_PROXY_HOST=REPLACE_WITH_MCP_HOST
NPM_MCP_PROXY_HOST=REPLACE_WITH_MCP_HOST
NPM_CERTIFICATE_REF=REPLACE_WITH_CERTIFICATE_REFERENCE
DNS_HUB_HOSTNAME=REPLACE_WITH_MCP_HOST
DNS_MCP_HOSTNAME=REPLACE_WITH_MCP_HOST
DNS_EXPECTED_ADDRESSES_JSON={"REPLACE_WITH_MCP_HOST":["REPLACE_WITH_INGRESS_IP"]}
NETWORK_TARGETS_JSON={"registry":"REPLACE_WITH_REGISTRY_HOST:443","gitea":"REPLACE_WITH_GITEA_HOST:443"}
# Generated during deployment; do not hand-edit into credentials.
UNRAID_REGISTRATIONS_FILE=/srv/example/appdata/itworx-mcp-hub/config/registered-targets.json
@@ -0,0 +1,52 @@
# Docker Compose implementation requirements
`docker-compose.blueprint.yml` is an executable architecture blueprint, not a release manifest. `WP-020` must replace every `:?` image variable with an exact tag **and digest**, build local images, and generate a validated production override.
## Compose project
- fixed project name: `itworx-mcp-hub`;
- dedicated appdata root, default proposal: `/srv/example/appdata/itworx-mcp-hub`;
- no modification of unrelated containers, networks, volumes, or proxy hosts;
- no service uses `network_mode: host`;
- only the explicit management, dataplane, and optional Inspector bindings may publish host ports;
- PostgreSQL, Redis, ContextForge, workers, connectors, and broker remain internal;
- all local services run non-root, drop Linux capabilities, use `no-new-privileges`, and use a read-only root filesystem where practical;
- each service has bounded memory/CPU/logging and a real healthcheck;
- startup ordering uses health conditions but applications must still tolerate dependency restarts;
- Redis is non-authoritative and may be emptied without data loss;
- the Docker socket is mounted only into `docker-read-broker`, read-only;
- target-system credentials are Compose secrets or equivalent mounted files, never normal environment values;
- release deployments use immutable image digests and record them in release evidence.
## Required profiles
- default/core: production Hub services;
- `project-connectors`: optional connectors requiring external project credentials;
- `inspector`: MCP Inspector bound to an explicitly configured LAN address only;
- `observability`: optional OTel Collector/Grafana stack if not reusing existing services;
- `fake`: deterministic fake targets for clean-room acceptance.
## Required generated outputs in WP-020
- `docker-compose.yml` or an equivalent production base;
- Unraid-specific override;
- development/fake override;
- exact `.env.example` without secrets;
- all Dockerfiles and healthchecks;
- `docker compose config` evidence;
- image pin/digest inventory;
- network and published-port assertion tests.
## Production build-fallback (WP-200 / DIAG-02)
`docker-compose.prod.yml` must cancel every owned service's inherited `build:`
block using the Compose Specification's `!reset` YAML tag — `build: !reset
null` — never plain `build: null`. Plain `null` is not treated as "delete
this key" by the real Compose merge engine; the base file's `build:` mapping
survives and `docker compose up -d` will attempt a local build (and fail in a
confusing way, or in the worst case succeed and deploy an unscanned image)
if a registry pull ever fails. `tests/test_compose.py`'s
`test_real_compose_config_has_no_build_for_owned_services` enforces this
against the real `docker compose config` CLI output; any new owned service
added to `docker-compose.prod.yml` must be added to that test's service list
and use the same `!reset` pattern.
@@ -0,0 +1,17 @@
name: itworx-mcp-hub
services:
postgres:
volumes: [postgres-data-dev:/var/lib/postgresql/data]
contextforge:
environment:
HOST: 0.0.0.0
PORT: 4444
AUTH_REQUIRED: "true"
MCPGATEWAY_UI_ENABLED: "false"
MCPGATEWAY_ADMIN_API_ENABLED: "false"
inspector:
profiles: [inspector]
volumes:
postgres-data-dev: {name: itworx-mcp-hub-postgres-dev}
+528
View File
@@ -0,0 +1,528 @@
name: itworx-mcp-hub
x-owned-service: &owned-service
restart: unless-stopped
read_only: true
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
tmpfs: ["/tmp:size=64m,mode=1777"]
pids_limit: 128
mem_limit: 512m
cpus: 1.0
logging:
driver: json-file
options: {max-size: "10m", max-file: "3"}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=2)"]
interval: 60s
timeout: 3s
retries: 5
start_period: 10s
x-third-party: &third-party
restart: unless-stopped
security_opt: ["no-new-privileges:true"]
logging:
driver: json-file
options: {max-size: "10m", max-file: "3"}
services:
postgres:
<<: *third-party
image: postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a
entrypoint: ["sh", "/opt/itworx/postgres-entrypoint.sh"]
command: ["postgres"]
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_superuser_password
PGDATA: /var/lib/postgresql/data/pgdata
SECRET_EXPECTED_UID: "${PUID:-1000}"
SECRET_EXPECTED_GID: "${PGID:-1000}"
secrets: [postgres_superuser_password, hub_database_password, contextforge_database_password]
volumes:
- postgres-data:/var/lib/postgresql/data
- ../postgres/init:/docker-entrypoint-initdb.d:ro
- ../images/postgres/entrypoint.sh:/opt/itworx/postgres-entrypoint.sh:ro
tmpfs: ["/run/itworx-secrets:size=1m,mode=0700"]
networks: [hub-data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 15s
timeout: 5s
retries: 5
redis:
<<: *third-party
image: redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236
entrypoint: ["sh", "/opt/itworx/redis-entrypoint.sh"]
environment:
REDIS_AUTH_ENABLED: "${REDIS_AUTH_ENABLED:-false}"
SECRET_EXPECTED_UID: "${PUID:-1000}"
SECRET_EXPECTED_GID: "${PGID:-1000}"
volumes:
- ../images/redis/entrypoint.sh:/opt/itworx/redis-entrypoint.sh:ro
- ../images/redis/healthcheck.sh:/opt/itworx/redis-healthcheck.sh:ro
secrets: [redis_password]
tmpfs: ["/run/itworx:size=1m,mode=0700"]
networks: [hub-data]
healthcheck:
test: ["CMD", "sh", "/opt/itworx/redis-healthcheck.sh"]
interval: 15s
timeout: 3s
retries: 5
contextforge:
<<: *third-party
image: ghcr.io/ibm/mcp-context-forge:v1.0.6@sha256:049b8e61fa1187f72773d94bcf91fc63338f4fea7808949a8a5daacbc89ef92e
user: "${PUID:-1000}:${PGID:-1000}"
entrypoint: ["sh", "/opt/itworx/contextforge-entrypoint.sh"]
environment:
HOST: 0.0.0.0
PORT: 4444
AUTH_REQUIRED: "true"
MCPGATEWAY_UI_ENABLED: "false"
MCPGATEWAY_ADMIN_API_ENABLED: "false"
MCPGATEWAY_MCP_APPS_ENABLED: "true"
ENABLE_HEADER_PASSTHROUGH: "true"
API_ALLOW_BASIC_AUTH: "false"
PLATFORM_ADMIN_EMAIL: "${CONTEXTFORGE_PLATFORM_ADMIN_EMAIL:-contextforge-admin@example.com}"
SSRF_PROTECTION_ENABLED: "true"
SSRF_ALLOW_PRIVATE_NETWORKS: "false"
SSRF_DNS_FAIL_CLOSED: "true"
SSRF_ALLOWED_NETWORKS: '["203.0.113.0/24"]'
UAID_ALLOWED_DOMAINS: '["example.com"]'
GUNICORN_WORKERS: "2"
# ContextForge only receives authenticated internal Hub traffic. Its
# generic limiter groups every caller behind the router's single IP and
# can lock out all ChatGPT sessions at once. Admission control therefore
# belongs to the public dataplane router below.
RATE_LIMITING_ENABLED: "false"
REDIS_AUTH_ENABLED: "${REDIS_AUTH_ENABLED:-false}"
SECRET_EXPECTED_UID: "${PUID:-1000}"
SECRET_EXPECTED_GID: "${PGID:-1000}"
volumes:
- ../images/contextforge/entrypoint.sh:/opt/itworx/contextforge-entrypoint.sh:ro
secrets: [contextforge_database_password, contextforge_auth_encryption_secret, contextforge_jwt_secret, contextforge_admin_credential, redis_password]
depends_on:
postgres: {condition: service_healthy}
redis: {condition: service_healthy}
networks:
hub-control:
aliases: [contextforge-control]
hub-connectors: {}
hub-data: {}
hub-observe: {}
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:4444/health', timeout=3)\""]
interval: 60s
timeout: 10s
retries: 5
start_period: 120s
control-api:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: control-api}}
image: local/itworx-mcp-hub-control:dev
environment:
HUB_OIDC_ISSUER: "${HUB_OIDC_ISSUER:-https://auth.dev.invalid/application/o/itworx/}"
HUB_OIDC_AUDIENCE: "${HUB_OIDC_AUDIENCE:-itworx-mcp-hub}"
HUB_OIDC_CLIENT_ID: "${HUB_OIDC_CLIENT_ID:-itworx-mcp-hub-dev}"
HUB_OIDC_REDIRECT_URI: "${HUB_OIDC_REDIRECT_URI:-https://hub.dev.invalid/auth/callback}"
HUB_OIDC_CLIENT_SECRET_FILE: /run/secrets/authentik_oidc_client_secret
HUB_OIDC_ROLE_GROUP_MAP: '${HUB_OIDC_ROLE_GROUP_MAP:-{"hub-admin":"admin"}}'
HUB_SESSION_SIGNING_KEY_FILE: /run/secrets/hub_session_signing_key
HUB_CONTEXTFORGE_URL: "${HUB_CONTEXTFORGE_URL:-http://contextforge-control:4444}"
HUB_CONTEXTFORGE_TOKEN_FILE: /run/secrets/contextforge_api_token
HUB_CONTEXTFORGE_AUTH_SCHEME: Bearer
HUB_DATABASE_PASSWORD_FILE: /run/secrets/hub_database_password
HUB_DATABASE_HOST: postgres
HUB_DATABASE_NAME: itworx_mcp_hub
HUB_DATABASE_USER: itworx_mcp_hub
HUB_CATALOG_ROOT: /opt/hub/catalog
INGESTION_CONNECTOR_ENABLED: "${INGESTION_CONNECTOR_ENABLED:-false}"
HUB_PUBLIC_MCP_URL: "${HUB_PUBLIC_MCP_URL:-https://mcp.example.com}"
HUB_BACKUP_STATUS_FILE: /var/lib/itworx/backup-status.json
HUB_BACKUP_STALE_AFTER_HOURS: "${HUB_BACKUP_STALE_AFTER_HOURS:-26}"
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/authentik_oidc_client_secret,/run/secrets/hub_session_signing_key,/run/secrets/contextforge_api_token,/run/secrets/hub_database_password,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [authentik_oidc_client_secret, hub_session_signing_key, contextforge_api_token, hub_database_password, connector_identity_signing_key]
volumes:
- "${BACKUP_STATUS_FILE_HOST:-../../examples/backup-status.json}:/var/lib/itworx/backup-status.json:ro"
- "../../catalog:/opt/hub/catalog:ro"
depends_on:
postgres: {condition: service_healthy}
redis: {condition: service_healthy}
contextforge: {condition: service_healthy}
networks: [hub-control, hub-connectors, hub-data, hub-observe, hub-egress]
worker:
<<: *owned-service
user: "${PUID:-10001}:${PGID:-10001}"
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: worker}}
image: local/itworx-mcp-hub-worker:dev
environment:
HUB_OIDC_ISSUER: "${HUB_OIDC_ISSUER:-https://auth.dev.invalid/application/o/itworx/}"
HUB_CONTEXTFORGE_URL: "${HUB_CONTEXTFORGE_URL:-http://contextforge-control:4444}"
HUB_CONTEXTFORGE_TOKEN_FILE: /run/secrets/contextforge_api_token
HUB_CONTEXTFORGE_AUTH_SCHEME: Bearer
HUB_DATABASE_PASSWORD_FILE: /run/secrets/hub_database_password
HUB_DATABASE_HOST: postgres
HUB_DATABASE_NAME: itworx_mcp_hub
HUB_DATABASE_USER: itworx_mcp_hub
HUB_CATALOG_ROOT: /opt/hub/catalog
INGESTION_CONNECTOR_ENABLED: "${INGESTION_CONNECTOR_ENABLED:-false}"
MCP_CLIENT_BINDINGS_JSON: '${MCP_CLIENT_BINDINGS_JSON:-[]}'
REQUIRED_SECRET_FILES: /run/secrets/hub_database_password,/run/secrets/contextforge_api_token
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [hub_database_password, contextforge_api_token]
volumes:
- "../../catalog:/opt/hub/catalog:ro"
- "../../services/runtime/state_sync.py:/opt/hub/services/runtime/state_sync.py:ro"
depends_on:
control-api: {condition: service_healthy}
redis: {condition: service_healthy}
connector-knowledge: {condition: service_healthy}
connector-gitea: {condition: service_healthy}
connector-unraid: {condition: service_healthy}
connector-mobilityops: {condition: service_healthy}
connector-projectbrain: {condition: service_healthy}
connector-appops: {condition: service_healthy}
connector-stitch: {condition: service_healthy}
networks: [hub-control, hub-connectors, hub-data, hub-observe]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=2)"]
interval: 10s
timeout: 3s
retries: 18
start_period: 10s
dataplane-router:
<<: *owned-service
# Docker bind-mounted secret files retain the Unraid PUID/PGID. Run the
# router as that already unprivileged identity so its fail-closed secret
# validation can read files that remain owner-only (0600).
user: "${PUID:-10001}:${PGID:-10001}"
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: dataplane-router}}
image: local/itworx-mcp-hub-dataplane:dev
environment:
HUB_OIDC_ISSUER: "${HUB_OIDC_ISSUER:-https://auth.dev.invalid/application/o/itworx/}"
HUB_OIDC_AUDIENCE: "${HUB_OIDC_AUDIENCE:-itworx-mcp-hub}"
HUB_OIDC_CLIENT_ID: "${HUB_OIDC_CLIENT_ID:-itworx-mcp-hub-dev}"
HUB_OIDC_REDIRECT_URI: "${HUB_OIDC_REDIRECT_URI:-https://hub.dev.invalid/auth/callback}"
HUB_MCP_ADDITIONAL_OIDC_ISSUER: "${HUB_MCP_ADDITIONAL_OIDC_ISSUER:-}"
HUB_MCP_ADDITIONAL_OIDC_AUDIENCE: "${HUB_MCP_ADDITIONAL_OIDC_AUDIENCE:-}"
HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID: "${HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID:-}"
HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI: "${HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI:-}"
HUB_PUBLIC_MCP_URL: "${HUB_PUBLIC_MCP_URL:-https://mcp.example.com}"
HUB_CONTEXTFORGE_URL: "${HUB_CONTEXTFORGE_URL:-http://contextforge-control:4444}"
HUB_CONTEXTFORGE_TOKEN_FILE: /run/secrets/contextforge_api_token
HUB_CONTEXTFORGE_AUTH_SCHEME: Bearer
HUB_DATABASE_PASSWORD_FILE: /run/secrets/hub_database_password
HUB_DATABASE_HOST: postgres
HUB_DATABASE_NAME: itworx_mcp_hub
HUB_DATABASE_USER: itworx_mcp_hub
HUB_CATALOG_ROOT: /opt/hub/catalog
HUB_RATE_LIMIT_PER_MINUTE: "${HUB_RATE_LIMIT_PER_MINUTE:-600}"
HUB_UPSTREAM_MAX_CONCURRENCY: "${HUB_UPSTREAM_MAX_CONCURRENCY:-24}"
HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS: "${HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS:-5}"
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
INGESTION_CONNECTOR_ENABLED: "${INGESTION_CONNECTOR_ENABLED:-false}"
DEMO_TOKEN_KEY_FILE: /run/secrets/demo_token_signing_key
DEMO_TOKEN_REVOCATIONS_FILE: /etc/itworx/demo-token-revocations.json
DEMO_TOKEN_RATE_LIMIT_PER_MINUTE: "${DEMO_TOKEN_RATE_LIMIT_PER_MINUTE:-10}"
REQUIRED_SECRET_FILES: /run/secrets/contextforge_api_token,/run/secrets/hub_database_password,/run/secrets/connector_identity_signing_key,/run/secrets/demo_token_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [contextforge_api_token, hub_database_password, connector_identity_signing_key, demo_token_signing_key]
volumes:
- "../../catalog:/opt/hub/catalog:ro"
- "../../services/runtime/dataplane.py:/opt/hub/services/runtime/dataplane.py:ro"
# Never silently fall back to the repository example: that would make
# the operator's early-revocation file ineffective.
# Mount the containing directory: the operator verifier atomically replaces
# the file and therefore needs a writable parent directory in its profile.
- "${DEMO_TOKEN_REVOCATIONS_DIR_HOST:?Set the host-side demo-token revocations directory}:/etc/itworx:ro"
depends_on: {postgres: {condition: service_healthy}, contextforge: {condition: service_healthy}}
networks: [hub-edge, hub-control, hub-data, hub-observe, hub-egress]
ports: ["${MCP_BIND_IP:-127.0.0.1}:${MCP_PORT:-18442}:8080"]
operator-tools:
# Explicit one-shot profile for operator-only proofs. It receives no Docker
# socket and does not start as part of the normal application topology.
image: local/itworx-mcp-hub-dataplane:dev
profiles: [operator-tools]
user: "${PUID:-10001}:${PGID:-10001}"
working_dir: /opt/hub
entrypoint: ["python", "-m", "scripts.verify_demo_token_flow"]
secrets: [demo_token_signing_key]
volumes:
- "${DEMO_TOKEN_REVOCATIONS_DIR_HOST:?Set the host-side demo-token revocations directory}:/etc/itworx:rw"
networks: [hub-edge]
web:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: web}}
image: local/itworx-mcp-hub-web:dev
environment:
HUB_PUBLIC_MCP_URL: "${HUB_PUBLIC_MCP_URL:-https://mcp.example.com}"
HUB_MCP_ADDITIONAL_OIDC_ISSUER: "${HUB_MCP_ADDITIONAL_OIDC_ISSUER:-https://auth.example-host.com/application/o/chatgpt-itworx-gitea/}"
HUB_WORKER_URL: "http://worker:8080"
volumes:
- "../../services/runtime/web.py:/opt/hub/services/runtime/web.py:ro"
depends_on:
control-api: {condition: service_healthy}
worker: {condition: service_healthy}
networks: [hub-edge, hub-control, hub-observe]
ports: ["${HUB_BIND_IP:-127.0.0.1}:${HUB_PORT:-1100}:8080"]
connector-knowledge:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-knowledge}}
image: local/itworx-mcp-hub-connector-knowledge:dev
volumes:
- "${PROJECTS_READONLY_ROOT:-../../}:/sources:ro"
environment:
KNOWLEDGE_SOURCES_JSON: '${KNOWLEDGE_SOURCES_JSON:-[]}'
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [connector_identity_signing_key]
networks: [hub-connectors, hub-observe]
connector-ingestion:
<<: *owned-service
profiles: [knowledge-intake]
# Keep /tmp writable to the non-root connector user; each intake job creates
# its own 0700 directory and 0600 file inside this shared sticky tmpfs.
tmpfs: ["/tmp:size=128m,mode=1777"]
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-ingestion}}
image: local/itworx-mcp-hub-connector-ingestion:dev
environment:
KNOWLEDGE_HUB_ENDPOINT: "${KNOWLEDGE_HUB_ENDPOINT:-https://knowledge-hub.invalid}"
KNOWLEDGE_HUB_ALLOWED_PROJECT_IDS: "${KNOWLEDGE_HUB_ALLOWED_PROJECT_IDS:-}"
KNOWLEDGE_HUB_WORKSPACE_READ_ENABLED: "${KNOWLEDGE_HUB_WORKSPACE_READ_ENABLED:-false}"
KNOWLEDGE_HUB_INGESTION_TOKEN_FILE: /run/secrets/knowledge_hub_ingestion_token
INGESTION_SCANNER_HOST: ingestion-clamd
INGESTION_SCANNER_PORT: "3310"
KNOWLEDGE_HUB_INGESTION_IDENTITY_KEY_FILE: /run/secrets/knowledge_hub_ingestion_identity_key
REQUIRED_SECRET_FILES: /run/secrets/knowledge_hub_ingestion_token,/run/secrets/knowledge_hub_ingestion_identity_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [knowledge_hub_ingestion_token, knowledge_hub_ingestion_identity_key]
depends_on: {ingestion-clamd: {condition: service_started}}
networks: [hub-connectors, hub-observe, hub-egress]
ingestion-clamd:
<<: *third-party
profiles: [knowledge-intake]
image: clamav/clamav:1.4.6@sha256:a5f03c12a79dbe9f6d8a527b6bb1ea053fa8dd061d3738a26897f055ee2d9303
cap_drop: ["ALL"]
# /init must chown its signature volume, prepare log files, and drop to clamav.
cap_add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID"]
mem_limit: 3g
pids_limit: 128
volumes: ["ingestion-clamav-db:/var/lib/clamav"]
networks: [hub-connectors, hub-egress]
connector-gitea:
<<: *owned-service
tmpfs: ["/tmp:size=512m,mode=1777"]
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-gitea}}
image: local/itworx-mcp-hub-connector-gitea:dev
environment:
GITEA_CONNECTOR_BUILD_SHA: "${GITEA_COMMIT_SHA:-unknown}"
GITEA_ENDPOINT: "${GITEA_ENDPOINT:-https://gitea.dev.invalid}"
GITEA_TOKEN_FILE: /run/secrets/gitea_mcp_token
GITEA_ENABLE_PR_WRITES: "${GITEA_ENABLE_PR_WRITES:-false}"
GITEA_WRITE_TOKEN_FILE: /run/secrets/gitea_mcp_write_token
GITEA_WRITE_OWNER: '${GITEA_WRITE_OWNER:-}'
GITEA_REPOSITORIES_JSON: '${GITEA_REPOSITORIES_JSON:-[{"project":"dev","owner":"dev","name":"dev"}]}'
GITEA_ADDITIONAL_PROJECTS: '${GITEA_ADDITIONAL_PROJECTS:-gitea-loop}'
GITEA_AUTO_DISCOVER_PROJECT: '${GITEA_AUTO_DISCOVER_PROJECT:-}'
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/gitea_mcp_token,/run/secrets/gitea_mcp_write_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [gitea_mcp_token, gitea_mcp_write_token, connector_identity_signing_key]
volumes:
- "../../services/runtime/connector_gitea.py:/opt/hub/services/runtime/connector_gitea.py:ro"
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-mobilityops:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-mobilityops}}
image: local/itworx-mcp-hub-connector-mobilityops:dev
environment:
MOBILITYOPS_ENDPOINT: "${MOBILITYOPS_ENDPOINT:-https://mobilityops.dev.invalid}"
MOBILITYOPS_SERVICE_TOKEN_FILE: /run/secrets/mobilityops_service_token
MOBILITYOPS_PROJECTS_JSON: '${MOBILITYOPS_PROJECTS_JSON:-["mobilityops"]}'
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/mobilityops_service_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [mobilityops_service_token, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-projectbrain:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-projectbrain}}
image: local/itworx-mcp-hub-connector-projectbrain:dev
environment:
PROJECTBRAIN_ENDPOINT: "${PROJECTBRAIN_ENDPOINT:-https://projectbrain.dev.invalid}"
PROJECTBRAIN_PROJECTS: "${PROJECTBRAIN_PROJECTS:-gitea-all},gitea-analysis,gitea-loop"
PROJECTBRAIN_SERVICE_TOKEN_FILE: /run/secrets/projectbrain_hub_service_token
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/projectbrain_hub_service_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [projectbrain_hub_service_token, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-appops:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-appops}}
image: local/itworx-mcp-hub-connector-appops:dev
environment:
APPOPS_ENDPOINT: "${APPOPS_ENDPOINT:-http://198.18.10.150:1216}"
APPOPS_PROJECTS: "${APPOPS_PROJECTS:-server-ops}"
APPOPS_SERVICE_TOKEN_FILE: /run/secrets/appops_hub_service_token
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/appops_hub_service_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [appops_hub_service_token, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-stitch:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-stitch}}
image: local/itworx-mcp-hub-connector-stitch:dev
environment:
STITCH_ENDPOINT: "https://stitch.googleapis.com/mcp"
STITCH_HUB_PROJECTS: "${STITCH_HUB_PROJECTS:-stitch-design}"
STITCH_API_KEY_FILE: /run/secrets/stitch_api_key
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/stitch_api_key,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [stitch_api_key, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
docker-read-broker:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/docker-read-broker/Dockerfile}
image: local/itworx-mcp-hub-docker-read-broker:dev
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://observability-collector:4318
BROKER_TOKEN_FILE: /run/secrets/docker_broker_service_token
BROKER_REGISTRATIONS_FILE: /etc/itworx/registered-targets.json
SECRET_EXPECTED_UID: "${PUID:-10002}"
SECRET_EXPECTED_GID: "${PGID:-10002}"
secrets: [docker_broker_service_token]
group_add: ["${DOCKER_SOCKET_GID:-10002}"]
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "${UNRAID_REGISTRATIONS_FILE:-../unraid/registered-targets.example.json}:/etc/itworx/registered-targets.json:ro"
networks: [hub-broker, hub-observe]
connector-unraid:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-unraid}}
image: local/itworx-mcp-hub-connector-unraid:dev
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://observability-collector:4318
BROKER_URL: http://docker-read-broker:8080
BROKER_TOKEN_FILE: /run/secrets/docker_broker_service_token
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
UNRAID_REGISTRATIONS_FILE: /etc/itworx/registered-targets.json
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/docker_broker_service_token,/run/secrets/connector_identity_signing_key
secrets: [docker_broker_service_token, connector_identity_signing_key]
volumes:
- "${UNRAID_REGISTRATIONS_FILE:-../unraid/registered-targets.example.json}:/etc/itworx/registered-targets.json:ro"
depends_on: {docker-read-broker: {condition: service_healthy}}
networks: [hub-connectors, hub-broker, hub-observe]
observability-collector:
image: local/itworx-mcp-hub-observability:dev
build: {context: ../.., dockerfile: infrastructure/images/observability/Dockerfile}
profiles: [observability]
networks: [hub-observe]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4318/healthz', timeout=2)"]
interval: 60s
timeout: 3s
retries: 5
inspector:
<<: *third-party
image: ghcr.io/modelcontextprotocol/inspector:2.0.0-rc.1@sha256:700500bbce937754d2ac0dd95797c3e057c8ebe2f4faf80f994652b0958a7478
profiles: [inspector]
networks: [hub-edge]
ports: ["${INSPECTOR_BIND_IP:?Set an explicit LAN-only Inspector bind address}:${INSPECTOR_PORT:-18443}:6274"]
networks:
hub-edge:
name: itworx-mcp-hub-edge
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-egress:
name: itworx-mcp-hub-egress
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-control:
name: itworx-mcp-hub-control
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-connectors:
name: itworx-mcp-hub-connectors
internal: true
ipam:
config: [{subnet: 203.0.113.0/24}]
hub-broker:
name: itworx-mcp-hub-broker
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-data:
name: itworx-mcp-hub-data
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-observe:
name: itworx-mcp-hub-observe
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
volumes:
postgres-data: {name: itworx-mcp-hub-postgres}
ingestion-clamav-db: {name: itworx-mcp-hub-ingestion-clamav-db}
secrets:
postgres_superuser_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/postgres_superuser_password"}
hub_database_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/hub_database_password"}
contextforge_database_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_database_password"}
contextforge_auth_encryption_secret: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_auth_encryption_secret"}
contextforge_jwt_secret: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_jwt_secret"}
contextforge_admin_credential: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_admin_credential"}
contextforge_api_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_api_token"}
docker_broker_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/docker_broker_service_token"}
connector_identity_signing_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/connector_identity_signing_key"}
demo_token_signing_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/demo_token_signing_key"}
authentik_oidc_client_secret: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/authentik_oidc_client_secret"}
hub_session_signing_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/hub_session_signing_key"}
gitea_mcp_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/gitea_mcp_token"}
gitea_mcp_write_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/gitea_mcp_write_token"}
mobilityops_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/mobilityops_service_token"}
projectbrain_hub_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/projectbrain_hub_service_token"}
appops_hub_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/appops_hub_service_token"}
stitch_api_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/stitch_api_key"}
knowledge_hub_ingestion_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/knowledge_hub_ingestion_token"}
knowledge_hub_ingestion_identity_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/knowledge_hub_ingestion_identity_key"}
redis_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/redis_password"}