name: mike-ai-tools x-tool-common: &tool-common restart: unless-stopped read_only: true tmpfs: - /tmp:rw,noexec,nosuid,nodev,size=64m security_opt: ["no-new-privileges:true"] cap_drop: [ALL] networks: [tools] logging: options: max-size: 10m max-file: "3" services: mcp-web: <<: *tool-common build: context: .. dockerfile: mcp/Dockerfile.web image: mike-ai/mcp-web:local container_name: mike-ai-mcp-web # The relay fetches and validates public result pages itself. It therefore # needs both the private tool network and the explicitly separated egress # network; keeping it on `tools` only makes search discovery work while # every page fetch fails. networks: [tools, egress] dns: ["${AI_DNS:-1.1.1.1}"] environment: TINYSEARCH_MCP_URL: http://tinysearch:8000/mcp SEARXNG_URL: http://searxng:8080 WEB_SEARCH_BUDGET_MAX_RELATED: "6" depends_on: tinysearch: condition: service_started searxng: <<: *tool-common image: searxng/searxng@sha256:e45d5894bfaa0bf8773b9f283795ae57f1c15ddb29c8cecb70b3665b0ce9ec60 container_name: mike-ai-tools-searxng dns: ["${AI_DNS:-1.1.1.1}"] volumes: - ${SEARXNG_SETTINGS_FILE:-../web-search/searxng-settings.example.yml}:/etc/searxng/settings.yml:ro networks: [tools, egress] tinysearch: <<: *tool-common image: marcellm01/tinysearch@sha256:5a03d5a1f1b0fabe48f2a26e05db4a84bcb611106a57ec51e42db4549976aa9c container_name: mike-ai-tools-tinysearch dns: ["${AI_DNS:-1.1.1.1}"] # Crawl4AI keeps transient browser/session state here. The container stays # read-only; only this disposable runtime directory (and /tmp from the # common hardening block) is writable. tmpfs: - /tmp:rw,noexec,nosuid,nodev,size=64m - /home/tinysearch/.crawl4ai:rw,nosuid,nodev,size=256m,mode=1777 shm_size: 1gb volumes: - tinysearch-models:/data/models - ../web-search/tinysearch_config.json:/config/tinysearch_config.json:ro environment: MCP_TRANSPORT: streamable-http MCP_HOST: 0.0.0.0 MCP_PORT: "8000" TINYSEARCH_CONFIG_PATH: /config/tinysearch_config.json TINYSEARCH_SEARCH_BACKEND: searxng SEARXNG_URL: http://searxng:8080/search depends_on: [searxng] cap_add: [SETUID, SETGID, CHOWN] networks: [tools, egress] # The image's built-in `tinysearch doctor` also requires a writable # configuration directory, although normal server operation does not. # Check the service socket instead so read-only hardening remains intact. healthcheck: test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1', 8000), 2); s.close()"] interval: 30s timeout: 5s retries: 5 start_period: 20s mcp-homeassistant: <<: *tool-common build: context: ../.. dockerfile: platform/mcp/Dockerfile.homeassistant-relay image: mike-ai/mcp-homeassistant-relay:local container_name: mike-ai-mcp-homeassistant profiles: [homeassistant] volumes: - ${HA_ENV_FILE:-/etc/mike-ai/homeassistant-admin-mcp.env}:/run/secrets/homeassistant.env:ro cap_add: [CHOWN, SETUID, SETGID] networks: [tools, egress] mcp-arr: <<: *tool-common build: context: . dockerfile: Dockerfile.arr image: mike-ai/mcp-arr:1.0.1-patched container_name: mike-ai-mcp-arr profiles: [arr] env_file: - ${ARR_ENV_FILE:-/etc/mike-ai/arr-mcp.env} volumes: # The local fork adds bounded read-only Sonarr pseudo-actions. Keep the # patch explicit until upstream publishes a self-contained 2.x image. - ${ARR_SONARR_PATCH:-./patches/mcp_sonarr.py}:/usr/local/lib/python3.13/site-packages/arr_mcp/mcp/mcp_sonarr.py:ro # Upstream's generic "Execute any Radarr API action" text gives small # models no routing boundary. This overlay changes guidance only. - ${ARR_RADARR_PATCH:-./patches/mcp_radarr.py}:/usr/local/lib/python3.13/site-packages/arr_mcp/mcp/mcp_radarr.py:ro networks: [tools, egress] mcp-navidrome: <<: *tool-common # Version and amd64 manifest are pinned. The image contains no mpv, so it # cannot play audio on the headless AI host and does not expose playback # controls. It talks to Navidrome only through its authenticated API. build: context: . dockerfile: Dockerfile.navidrome image: mike-ai/mcp-navidrome:2.2.0-schemafix1 container_name: mike-ai-mcp-navidrome profiles: [navidrome] env_file: - ${NAVIDROME_MCP_ENV_FILE:-/etc/mike-ai/navidrome-mcp.env} environment: MCP_TRANSPORT: http MCP_HTTP_EXPOSE: "true" MCP_HTTP_PORT: "3000" # The endpoint is not published on the host. Host filtering still makes # accidental access from any other Docker name fail closed. MCP_HTTP_ALLOWED_HOSTS: "mike-ai-mcp-navidrome:3000,mike-ai-mcp-navidrome" WEBUI_ENABLED: "false" tmpfs: - /tmp:rw,noexec,nosuid,nodev,size=64m - /config:rw,noexec,nosuid,nodev,size=4m,mode=0700 networks: [tools, egress] mcp-platform-context: <<: *tool-common build: context: . dockerfile: Dockerfile.platform-context image: mike-ai/mcp-platform-context:1.0.0 container_name: mike-ai-mcp-platform-context environment: ATHENA_REPO_ROOT: /knowledge/repo ATHENA_DOCS_ROOT: /workspace/docs ATHENA_RUNTIME_FILE: /runtime/runtime.json ATHENA_CONTEXT_STATE: /state # Writes remain confined to docs/*.md and require a prepared proposal # plus its exact confirmation string. The MCP cannot change code, # containers, networking, secrets, Git or recovery bundles. ATHENA_DOC_WRITE_MODE: enabled volumes: - ${PLATFORM_STACK_DIR:-/opt/mike-ai/stack}:/knowledge/repo:ro - ${PLATFORM_DOCS_DIR:-/opt/mike-ai/stack/docs}:/workspace/docs:rw - ${PLATFORM_CONTEXT_RUNTIME_DIR:-/var/lib/mike-ai-platform-context}:/runtime:ro - ${PLATFORM_CONTEXT_STATE_DIR:-/data/mike-ai-platform-context}:/state:rw networks: [tools] healthcheck: test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1',8000),2); s.close()"] interval: 30s timeout: 5s retries: 5 start_period: 10s mcp-github: <<: *tool-common build: context: . dockerfile: Dockerfile.github image: mike-ai/mcp-github:github-v1.10.1-mcp-proxy-v0.12.0 container_name: mike-ai-mcp-github profiles: [github] env_file: - ${GITHUB_MCP_ENV_FILE:-/etc/mike-ai/github-mcp.env} environment: # These server-side limits remain authoritative even if a client asks # for broader toolsets. The token itself must also remain read-only. GITHUB_TOOLS: search_repositories,get_repository_tree,get_file_contents,search_code GITHUB_READ_ONLY: "1" networks: [tools, egress] healthcheck: test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1',8000),2); s.close()"] interval: 30s timeout: 5s retries: 5 start_period: 15s mcp-unraid-official: <<: *tool-common image: debian:13-slim container_name: mike-ai-mcp-unraid-official profiles: [unraid] env_file: - ${RUNRAID_ENV_FILE:-/etc/mike-ai/runraid/.env} environment: UNRAID_RMCP_HOST: 0.0.0.0 UNRAID_RMCP_PORT: "8000" UNRAID_RMCP_DISABLE_HTTP_AUTH: "true" UNRAID_NOAUTH: "true" UNRAID_RMCP_ALLOWED_HOSTS: "mike-ai-mcp-unraid-official:8000,mike-ai-mcp-unraid-official,localhost:8000,127.0.0.1:8000" volumes: - ${RUNRAID_BINARY:-/usr/local/bin/runraid}:/usr/local/bin/unraid:ro entrypoint: ["/usr/local/bin/unraid"] command: ["serve"] networks: [tools, egress] mcp-unraid-ssh: <<: *tool-common profiles: [extended] build: context: . dockerfile: Dockerfile.unraid-ssh image: mike-ai/mcp-unraid-ssh:local container_name: mike-ai-mcp-unraid-ssh environment: UNRAID_MCP_CONFIG: /run/config/unraid-mcp.json volumes: - ${UNRAID_MCP_SOURCE:-/opt/mike-ai/unraid-agent/unraid_mcp.py}:/app/unraid_mcp.py:ro - ${UNRAID_MCP_CONFIG:-/etc/mike-ai/unraid-mcp.json}:/run/config/unraid-mcp.json:ro - ${UNRAID_SSH_KEY:-/etc/mike-ai/keys/unraid_root}:/etc/mike-ai/keys/unraid_root:ro - ${UNRAID_KNOWN_HOSTS:-/etc/mike-ai/ssh/known_hosts_unraid_ai}:/etc/mike-ai/ssh/known_hosts_unraid_ai:ro - unraid-audit:/var/log/mike-ai networks: [tools, egress] networks: tools: name: mike-ai-tools internal: true ipam: config: [{subnet: 172.30.40.0/24}] egress: name: mike-ai-tools-egress ipam: config: [{subnet: 172.30.50.0/24}] volumes: tinysearch-models: name: mike-ai-tools_tinysearch-models external: true unraid-audit: