From aac5e321bad907edb1f0e89671cd5d996ff2227e Mon Sep 17 00:00:00 2001 From: Jesper Pedersen Date: Mon, 22 Dec 2025 10:09:42 +0100 Subject: [PATCH 1/4] Attempt on a local mcp for Agents, to help setup projects for Itkdev dev enviroments. --- README.md | 44 ++ docs/itkdev-docker-cli.md | 943 ++++++++++++++++++++++++++ docs/itkdev-docker-compose.md | 819 +++++++++++++++++++++++ docs/itkdev-task-files.md | 608 +++++++++++++++++ docs/rfc-mcp-server.md | 312 +++++++++ mcp/.gitignore | 4 + mcp/README.md | 206 ++++++ mcp/package-lock.json | 1163 +++++++++++++++++++++++++++++++++ mcp/package.json | 31 + mcp/src/index.ts | 696 ++++++++++++++++++++ mcp/tsconfig.json | 19 + 11 files changed, 4845 insertions(+) create mode 100644 docs/itkdev-docker-cli.md create mode 100644 docs/itkdev-docker-compose.md create mode 100644 docs/itkdev-task-files.md create mode 100644 docs/rfc-mcp-server.md create mode 100644 mcp/.gitignore create mode 100644 mcp/README.md create mode 100644 mcp/package-lock.json create mode 100644 mcp/package.json create mode 100644 mcp/src/index.ts create mode 100644 mcp/tsconfig.json diff --git a/README.md b/README.md index a04656c..0bd83c8 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,50 @@ At ITK-dev we have created docker images that matches our development. The fuld list can be found at [https://hub.docker.com/search?q=itkdev&type=image](https://hub.docker.com/search?q=itkdev&type=image). +## MCP Server for AI Assistants + +This repository includes an MCP (Model Context Protocol) server that provides AI coding assistants like Claude Code with access to ITK Dev documentation and project analysis tools. + +### What it provides + +- **Documentation access**: AI assistants can read ITK Dev Docker documentation +- **Project detection**: Analyze projects to detect template, PHP version, framework +- **Template comparison**: Compare projects against templates to find outdated files + +### Installation + +```bash +# Build the MCP server +cd mcp +npm install +npm run build +``` + +Add to `~/.claude/settings.json`: + +```json +{ + "mcpServers": { + "itkdev": { + "command": "node", + "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] + } + } +} +``` + +See [mcp/README.md](mcp/README.md) for detailed documentation and [docs/rfc-mcp-server.md](docs/rfc-mcp-server.md) for the design rationale. + +## Documentation + +Comprehensive documentation is available in the [docs/](docs/) directory: + +- [itkdev-docker-cli.md](docs/itkdev-docker-cli.md) - CLI tool commands and setup procedures +- [itkdev-docker-compose.md](docs/itkdev-docker-compose.md) - Docker Compose patterns +- [itkdev-task-files.md](docs/itkdev-task-files.md) - Taskfile automation patterns +- [github-actions-templates.md](docs/github-actions-templates.md) - GitHub Actions workflows +- [rfc-mcp-server.md](docs/rfc-mcp-server.md) - MCP server design rationale + ## More For more details about usage see diff --git a/docs/itkdev-docker-cli.md b/docs/itkdev-docker-cli.md new file mode 100644 index 0000000..f448c22 --- /dev/null +++ b/docs/itkdev-docker-cli.md @@ -0,0 +1,943 @@ +# ITK Dev Docker CLI Tool + +This document describes the `itkdev-docker-compose` CLI tool, template system, and infrastructure setup for ITK Dev projects. + +## Overview + +The `itkdev-docker-compose` CLI tool provides: + +- Template installation for new projects +- Unified command interface for Docker operations +- Database and file synchronization from remote servers +- Traefik reverse proxy management +- Development utilities (Xdebug, shell access, etc.) + +**Version:** 3.0.0 + +## Installation + +### Prerequisites + +Install required dependencies: + +```bash +# Shell completion support (required for completions) +brew install bash-completion + +# MySQL client for database operations +brew install mysql-client + +# Optional but recommended +brew install jq # Improved Traefik host parsing +brew install pv # Progress display for database sync +``` + +Follow the post-install instructions from brew for each package: + +```bash +brew info bash-completion +brew info mysql-client +``` + +### Adding to PATH + +Clone the repository and add the scripts directory to your PATH. + +**Bash:** + +```bash +git clone https://github.com/itk-dev/devops_itkdev-docker.git ~/itkdev-docker +echo 'export PATH="$HOME/itkdev-docker/scripts:$PATH"' >> ~/.bashrc +source ~/.bashrc +``` + +**Zsh:** + +```bash +git clone https://github.com/itk-dev/devops_itkdev-docker.git ~/itkdev-docker +echo 'export PATH="$HOME/itkdev-docker/scripts:$PATH"' >> ~/.zshrc +source ~/.zshrc +``` + +### Shell Completions + +**Bash:** + +```bash +ln -s ~/itkdev-docker/completion/bash/itkdev-docker-compose-completion.bash \ + $(brew --prefix)/etc/bash_completion.d/itkdev-docker-compose +``` + +**Zsh:** + +```bash +echo 'fpath=(~/itkdev-docker/completion/zsh $fpath)' >> ~/.zshrc +``` + +### Self-Update + +Keep the tool updated: + +```bash +itkdev-docker-compose self:update +``` + +## CLI Commands Reference + +### Template Management + +#### template:install + +Install a template in the current directory: + +```bash +# List available templates +itkdev-docker-compose template:install --list + +# Install a template +itkdev-docker-compose template:install drupal-11 + +# Force overwrite existing files +itkdev-docker-compose template:install drupal-11 --force +``` + +The installer will prompt for: + +- Project name (defaults to directory name) +- Domain (defaults to `.local.itkdev.dk`) + +Creates `.env` file with: + +```env +COMPOSE_PROJECT_NAME= +COMPOSE_DOMAIN=.local.itkdev.dk +ITKDEV_TEMPLATE= +``` + +#### template:update + +Update an existing template installation: + +```bash +# Update template (reads ITKDEV_TEMPLATE from .env) +itkdev-docker-compose template:update + +# Force update without confirmation +itkdev-docker-compose template:update --force +``` + +### Traefik Reverse Proxy + +The Traefik reverse proxy handles domain routing for all local projects. + +#### traefik:start + +Start the reverse proxy (creates frontend network if needed): + +```bash +itkdev-docker-compose traefik:start +``` + +#### traefik:stop + +Stop the reverse proxy: + +```bash +itkdev-docker-compose traefik:stop +``` + +#### traefik:pull + +Pull latest Traefik and socket-proxy images: + +```bash +itkdev-docker-compose traefik:pull +``` + +#### traefik:url + +Get the Traefik dashboard URL: + +```bash +itkdev-docker-compose traefik:url +# Output: http://traefik.local.itkdev.dk:8080 +``` + +#### traefik:open + +Open Traefik dashboard in browser: + +```bash +itkdev-docker-compose traefik:open +``` + +#### traefik:logs + +View Traefik logs: + +```bash +itkdev-docker-compose traefik:logs +``` + +### URL and Browser Access + +#### url + +Get URL for a service: + +```bash +# Get main site URL +itkdev-docker-compose url + +# Get URL for specific service +itkdev-docker-compose url nginx +itkdev-docker-compose url mail 8025 +``` + +#### open + +Open service in default browser: + +```bash +# Open main site +itkdev-docker-compose open + +# Open specific service +itkdev-docker-compose open mail 8025 +``` + +#### mail:url / mail:open + +Access the Mailpit web interface: + +```bash +itkdev-docker-compose mail:url +itkdev-docker-compose mail:open +``` + +### Database Operations + +#### sql:cli + +Open MySQL client connected to the database: + +```bash +# Interactive session +itkdev-docker-compose sql:cli + +# Execute a query +itkdev-docker-compose sql:cli --table <<< 'SHOW TABLES' + +# Run a SQL script +itkdev-docker-compose sql:cli < query.sql +``` + +#### sql:connect + +Print the MySQL connection command: + +```bash +itkdev-docker-compose sql:connect +# Output: docker compose exec mariadb mysql --user=db --password=db db +``` + +#### sql:open + +Open database in GUI application (TablePlus, Sequel Pro, etc.): + +```bash +itkdev-docker-compose sql:open +``` + +#### sql:port + +Display the exposed MariaDB port: + +```bash +itkdev-docker-compose sql:port +# Output: 32768 +``` + +#### sql:log + +Enable and tail SQL query logging: + +```bash +itkdev-docker-compose sql:log +# Press Ctrl-C to stop logging (automatically disables logging) +``` + +### Remote Synchronization + +Requires environment variables in `.env` or `.env.local`: + +```env +REMOTE_HOST=example.com +REMOTE_DB_DUMP_CMD='drush --root=/var/www/html sql-dump' +REMOTE_PATH='/var/www/html/sites/default/files' +REMOTE_EXCLUDE=(styles css js advagg_*) +LOCAL_PATH='web/sites/default/files' +SYNC_DB_POST_SCRIPT='itkdev-docker-compose drush cr' +``` + +#### sync:db + +Synchronize database from remote server: + +```bash +itkdev-docker-compose sync:db +``` + +Executes `REMOTE_DB_DUMP_CMD` on `REMOTE_HOST` and imports to local database. +Runs `SYNC_DB_POST_SCRIPT` after import if defined. + +#### sync:files + +Synchronize files from remote server: + +```bash +itkdev-docker-compose sync:files +``` + +Uses rsync to copy `REMOTE_PATH` to `LOCAL_PATH`, respecting `REMOTE_EXCLUDE` patterns. + +#### sync + +Synchronize both database and files: + +```bash +itkdev-docker-compose sync +``` + +### PHP and Development Commands + +#### drush + +Run Drush commands: + +```bash +itkdev-docker-compose drush cache:rebuild +itkdev-docker-compose drush --yes deploy +itkdev-docker-compose drush user:login +``` + +Automatically detects whether to use `vendor/bin/drush` or the drush container (Drupal 7). + +#### composer + +Run Composer commands: + +```bash +itkdev-docker-compose composer install +itkdev-docker-compose composer require drupal/module +itkdev-docker-compose composer update --with-dependencies +``` + +#### php + +Run PHP commands: + +```bash +itkdev-docker-compose php -v +itkdev-docker-compose php script.php +``` + +#### bin/* and vendor/bin/* + +Run any binary from the project: + +```bash +itkdev-docker-compose bin/console cache:clear +itkdev-docker-compose vendor/bin/phpcs +itkdev-docker-compose vendor/bin/phpstan analyse +``` + +#### shell + +Enter a shell inside a container: + +```bash +# Default: phpfpm +itkdev-docker-compose shell phpfpm + +# Other containers +itkdev-docker-compose shell nginx +itkdev-docker-compose shell mariadb +``` + +### Debugging + +#### xdebug + +Start containers with Xdebug enabled (interactive mode): + +```bash +itkdev-docker-compose xdebug +``` + +Displays IDE configuration instructions for PhpStorm and VS Code. +Press Ctrl-C to restart containers with Xdebug disabled. + +#### xdebug3 + +Start containers with Xdebug enabled (detached mode): + +```bash +itkdev-docker-compose xdebug3 +``` + +### System Commands + +#### hosts:insert + +Add domain to /etc/hosts: + +```bash +itkdev-docker-compose hosts:insert +# Adds: 0.0.0.0 # itkdev-docker-compose +``` + +#### images:pull + +Update all Docker images: + +```bash +itkdev-docker-compose images:pull +``` + +#### down + +Stop and remove containers, networks, and volumes: + +```bash +itkdev-docker-compose down +``` + +#### version + +Display tool version: + +```bash +itkdev-docker-compose version +# Output: Version: 3.0.0 +``` + +### Docker Compose Passthrough + +Any unrecognized command is passed to `docker compose`: + +```bash +itkdev-docker-compose up -d +itkdev-docker-compose ps +itkdev-docker-compose logs -f phpfpm +itkdev-docker-compose exec phpfpm bash +``` + +## Available Templates + +### Template Comparison + +| Template | PHP | Web Root | Database | Memcached | Drush Container | Mail | +|----------|-----|----------|----------|-----------|-----------------|------| +| `drupal-7` | 7.4 | `/app` | Yes | Yes | Yes | Yes | +| `drupal-8` | 7.2-8.3 | `/app/web` | Yes | Yes | No | Yes | +| `drupal-9` | 7.4+ | `/app/web` | Yes | Yes | No | Yes | +| `drupal-10` | 8.3 | `/app/web` | Yes | Yes | No | Yes | +| `drupal-11` | 8.4 | `/app/web` | Yes | Yes | No | Yes | +| `drupal-module` | 8.4 | `/app` | No | No | No | No | +| `symfony-3` | 7.2 | `/app/web` | Yes | Yes | No | Yes | +| `symfony-4` | 7.4 | `/app/web` | Yes | Yes | No | Yes | +| `symfony-6` | 8.4 | `/app/public` | Yes | No | No | Yes | + +### Template Selection Guide + +**Drupal Projects:** + +- New Drupal 11 projects: `drupal-11` +- New Drupal 10 projects: `drupal-10` +- Legacy Drupal 7 maintenance: `drupal-7` +- Drupal module/theme development: `drupal-module` + +**Symfony Projects:** + +- New Symfony 6/7 projects: `symfony-6` +- Legacy Symfony 4 maintenance: `symfony-4` + +### Drupal 7 Specifics + +Drupal 7 template has unique characteristics: + +- Web root is `/app` (not `/app/web`) +- Includes dedicated `drush` container (`itkdev/drush6:latest`) +- Uses shared `drush-cache` volume +- HTTPS redirect disabled by default + +### Drupal 11 Specifics + +Drupal 11 template (newest): + +- PHP 8.4 +- HTTPS redirect enabled by default +- Uses `vendor/bin/drush` (no drush container) +- Depends on mariadb and memcached health checks + +### Symfony 6 Specifics + +Symfony 6 template: + +- Web root is `/app/public` (Symfony standard) +- No memcached dependency (phpfpm only depends on mariadb) +- PHP 8.4 + +### Drupal Module Template + +Minimal template for module development: + +- Only phpfpm and nginx services +- No database, cache, or mail services +- Suitable for isolated module testing + +## Template File Structure + +When you run `template:install`, these files are created: + +``` +project/ +├── .env # Created interactively (if not exists) +├── docker-compose.yml # Base service configuration +├── docker-compose.dev.yml # Server dev/staging overrides +├── docker-compose.redirect.yml # WWW to non-WWW redirect config +├── docker-compose.server.yml # Production server configuration +└── .docker/ + ├── data/ # Persistent data directory + │ └── .gitignore # Ignores data files + ├── nginx.conf # Main nginx config (for production) + └── templates/ + └── default.conf.template # Nginx vhost template +``` + +### File Purposes + +| File | Purpose | Used By | +|------|---------|---------| +| `docker-compose.yml` | Base services for local development | `docker compose up` | +| `docker-compose.dev.yml` | Staging server with basic auth, mail | `itkdev-docker-compose-server` | +| `docker-compose.redirect.yml` | WWW redirect middleware | `itkdev-docker-compose-server` | +| `docker-compose.server.yml` | Production with HTTPS, no DB | `itkdev-docker-compose-server` | +| `.docker/nginx.conf` | Production nginx main config | Server deployments | +| `.docker/templates/default.conf.template` | Nginx vhost with env substitution | All environments | + +## Traefik Infrastructure + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Host Machine │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ Docker Network: frontend ││ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ +│ │ │ Project A │ │ Project B │ │ Project C │ ││ +│ │ │ nginx │ │ nginx │ │ nginx │ ││ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ ││ +│ │ │ │ │ ││ +│ │ └────────────────┼────────────────┘ ││ +│ │ │ ││ +│ │ ┌──────────────┐ ││ +│ │ │ Traefik │ :80, :443, :8080 ││ +│ │ └──────────────┘ ││ +│ │ │ ││ +│ │ ┌──────────────┐ ││ +│ │ │ Socket Proxy │ (Docker socket) ││ +│ │ └──────────────┘ ││ +│ └─────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────┘ +``` + +### Traefik Configuration + +**Entry Points:** + +| Name | Port | Purpose | +|------|------|---------| +| `web` | 80 | HTTP traffic | +| `websecure` | 443 | HTTPS traffic (TLS enabled) | +| Dashboard | 8080 | Traefik admin UI | + +**SSL Certificates:** + +Self-signed wildcard certificate for `*.local.itkdev.dk` located at: + +- Certificate: `traefik/ssl/docker.crt` +- Key: `traefik/ssl/docker.key` + +**Trust Certificate on macOS:** + +1. Open `traefik/ssl/docker.crt` in Keychain Access +2. Right-click the certificate and select "Get Info" +3. Expand "Trust" section +4. Set "When using this certificate" to "Always Trust" + +**Generate Custom Certificate:** + +```bash +openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:2048 \ + -keyout docker.key -out docker.crt \ + -subj "/CN=*.example.local" \ + -reqexts SAN -extensions SAN \ + -config <(cat /usr/local/etc/openssl/openssl.cnf \ + <(printf '[SAN]\nsubjectAltName=DNS:*.example.local')) +``` + +### Socket Proxy Security + +Traefik connects to Docker via a socket proxy (`itkdev/docker-socket-proxy`) instead of direct socket access: + +- Limits Docker API exposure +- Only `CONTAINERS=1` permission enabled +- Runs on internal `proxy` network + +## GitHub Actions Workflows + +Templates include GitHub Actions workflow files for CI/CD. + +### Available Workflows + +**Universal (all project types):** + +| Workflow | Purpose | +|----------|---------| +| `changelog.yaml` | Verify CHANGELOG.md is updated | +| `composer.yaml` | Validate composer.json/lock | +| `markdown.yaml` | Lint markdown files | +| `yaml.yaml` | Lint YAML files | +| `twig.yaml` | Lint Twig templates | + +**Drupal Projects:** + +| Workflow | Purpose | +|----------|---------| +| `drupal/php.yaml` | PHP coding standards (drupal/coder) | +| `drupal/javascript.yaml` | JavaScript linting (Prettier) | +| `drupal/styles.yaml` | CSS/SCSS linting | +| `drupal/site.yaml` | Site install/update tests | + +**Drupal Modules:** + +| Workflow | Purpose | +|----------|---------| +| `drupal-module/php.yaml` | PHP coding standards | +| `drupal-module/javascript.yaml` | JavaScript linting | +| `drupal-module/styles.yaml` | CSS/SCSS linting | + +**Symfony Projects:** + +| Workflow | Purpose | +|----------|---------| +| `symfony/php.yaml` | PHP coding standards (PHP-CS-Fixer) | +| `symfony/javascript.yaml` | JavaScript linting | +| `symfony/styles.yaml` | CSS/SCSS linting | + +### Workflow Requirements + +**Drupal PHP workflow requires:** + +```bash +# Add drupal/coder as dev dependency +docker compose run --rm phpfpm composer require --dev drupal/coder +``` + +**Symfony PHP workflow requires:** + +```bash +# Add friendsofphp/php-cs-fixer as dev dependency +docker compose run --rm phpfpm composer require --dev friendsofphp/php-cs-fixer +``` + +### Workflow Assumptions + +All workflows assume: + +1. A `phpfpm` service exists in docker-compose.yml +2. `COMPOSE_USER: runner` works in CI environment +3. `docker network create frontend` can be run + +The `drupal/site.yaml` workflow additionally assumes: + +1. Database and dependent services are defined +2. Site can be installed with `drush site:install --existing-config` +3. Site can be updated with `drush deploy` + +## Configuration Files + +### PHP CodeSniffer (Drupal) + +Template location: `config/drupal/php/.phpcs.xml.dist` + +```xml + + + The coding standard. + + web/modules/custom/ + web/themes/custom/ + + node_modules + vendor + web/*/custom/*/build/ + + + + + + + + + + +``` + +### PHP-CS-Fixer (Symfony) + +Template location: `config/symfony/php/.php-cs-fixer.dist.php` + +Standard Symfony PHP-CS-Fixer configuration. + +### Twig CS Fixer + +Template location: `config/drupal/twig/.twig-cs-fixer.dist.php` + +Twig template linting configuration. + +### Markdown Lint + +Template location: `config/markdown/.markdownlint.jsonc` + +Markdown linting rules configuration. + +## Environment Variables Reference + +### Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `COMPOSE_PROJECT_NAME` | Docker project namespace | `myproject` | + +### Optional Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `COMPOSE_DOMAIN` | Local development domain | `.docker.localhost` | +| `COMPOSE_USER` | User inside containers | `deploy` | +| `COMPOSE_SERVER_DOMAIN` | Server deployment domain | - | +| `PHP_XDEBUG_MODE` | Xdebug mode | `off` | + +### Sync Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `REMOTE_HOST` | SSH host for sync | `example.com` | +| `REMOTE_DB_DUMP_CMD` | Database dump command | `drush sql-dump` | +| `REMOTE_PATH` | Remote files path | `/var/www/files` | +| `REMOTE_EXCLUDE` | Rsync exclude patterns | `(styles css js)` | +| `LOCAL_PATH` | Local files path | `web/sites/default/files` | +| `SYNC_DB_POST_SCRIPT` | Post-sync script | `drush cr` | + +### Local Overrides + +Create `.env.local` for machine-specific settings (not committed to git): + +```env +# Override sync host for different environment +REMOTE_HOST=staging.example.com + +# Enable Xdebug by default +PHP_XDEBUG_MODE=debug + +# Different domain +COMPOSE_DOMAIN=myproject.local +``` + +## Complete Setup Workflows + +### New Drupal 11 Project + +```bash +# 1. Start Traefik (once per machine) +itkdev-docker-compose traefik:start + +# 2. Create project directory +mkdir myproject && cd myproject + +# 3. Install Drupal via Composer +composer create-project drupal/recommended-project . + +# 4. Install Docker template +itkdev-docker-compose template:install drupal-11 + +# 5. Start containers +docker compose up -d + +# 6. Install Drupal +itkdev-docker-compose drush site:install --yes + +# 7. Open site +itkdev-docker-compose open +``` + +### New Symfony 6 Project + +```bash +# 1. Start Traefik (once per machine) +itkdev-docker-compose traefik:start + +# 2. Create project +composer create-project symfony/skeleton myproject +cd myproject + +# 3. Install Docker template +itkdev-docker-compose template:install symfony-6 + +# 4. Start containers +docker compose up -d + +# 5. Open site +itkdev-docker-compose open +``` + +### Existing Project Setup + +```bash +# 1. Clone repository +git clone git@github.com:org/project.git +cd project + +# 2. Start Traefik if not running +itkdev-docker-compose traefik:start + +# 3. Copy environment template +cp .env.example .env.local + +# 4. Pull images and start +docker compose pull +docker compose up -d + +# 5. Install dependencies +itkdev-docker-compose composer install + +# 6. Sync database from remote (if configured) +itkdev-docker-compose sync:db + +# 7. Run site update +itkdev-docker-compose drush deploy + +# 8. Open site +itkdev-docker-compose open +``` + +### Daily Development Workflow + +```bash +# Start containers +docker compose up -d + +# Check status +docker compose ps + +# View logs +docker compose logs -f phpfpm + +# Run drush commands +itkdev-docker-compose drush cr +itkdev-docker-compose drush cex + +# Enable Xdebug for debugging session +itkdev-docker-compose xdebug + +# Open database GUI +itkdev-docker-compose sql:open + +# Access mail interface +itkdev-docker-compose mail:open +``` + +## Troubleshooting + +### Traefik Not Running + +```bash +# Check if traefik container exists +docker ps -a | grep traefik + +# Start traefik +itkdev-docker-compose traefik:start + +# Check traefik logs +itkdev-docker-compose traefik:logs +``` + +### Port Conflicts + +If port 80, 443, or 8080 are in use: + +```bash +# Find process using port +lsof -i :80 + +# Stop conflicting service (e.g., Apache) +sudo apachectl stop +``` + +### Database Connection Issues + +```bash +# Check if mariadb is healthy +docker compose ps mariadb + +# View mariadb logs +docker compose logs mariadb + +# Test connection +itkdev-docker-compose sql:cli <<< 'SELECT 1' +``` + +### Permission Issues + +```bash +# Fix file permissions +sudo chown -R $USER:$USER . + +# Make Drupal settings writable +chmod +w web/sites/default +chmod +w web/sites/default/settings.php +``` + +### Container User Mismatch + +If files created in container have wrong ownership: + +```bash +# Set COMPOSE_USER in .env.local to match your UID +echo "COMPOSE_USER=$(id -u):$(id -g)" >> .env.local +docker compose up -d --force-recreate +``` + +### SSL Certificate Not Trusted + +1. Open `~/itkdev-docker/traefik/ssl/docker.crt` +2. Add to Keychain Access +3. Set trust to "Always Trust" +4. Restart browser + +### Sync Failing + +```bash +# Test SSH connection +ssh $REMOTE_HOST "echo 'Connected'" + +# Test dump command +ssh $REMOTE_HOST "$REMOTE_DB_DUMP_CMD" | head -20 + +# Check LOCAL_PATH exists +ls -la $LOCAL_PATH +``` diff --git a/docs/itkdev-docker-compose.md b/docs/itkdev-docker-compose.md new file mode 100644 index 0000000..fb1bc98 --- /dev/null +++ b/docs/itkdev-docker-compose.md @@ -0,0 +1,819 @@ +# ITK Dev Docker Compose Setup + +This document describes the Docker Compose patterns used in ITK Dev projects. It covers both local development and server deployment configurations. + +## Overview + +ITK Dev projects use a layered Docker Compose approach: + +- **Local development**: Standard `docker compose` with multiple compose files +- **Server deployments**: `itkdev-docker-compose-server` CLI wrapper for production environments + +## Version Tracking + +ITK Dev compose files include a version comment at the top for tracking template versions: + +```yaml +# itk-version: 3.2.1 +networks: + frontend: + external: true + # ... +``` + +This helps identify which template version a project is based on and when updates are needed. + +## Environment Configuration + +### Required Environment Variables + +Create a `.env` file in the project root: + +```env +COMPOSE_PROJECT_NAME= +COMPOSE_DOMAIN=.local.itkdev.dk +ITKDEV_TEMPLATE= +``` + +| Variable | Description | Example | +|----------|-------------|---------| +| `COMPOSE_PROJECT_NAME` | Docker project namespace | `myproject` | +| `COMPOSE_DOMAIN` | Local development domain | `myproject.local.itkdev.dk` | +| `ITKDEV_TEMPLATE` | Template type identifier | `drupal-10`, `symfony` | + +### Local Overrides + +Create `.env.local` for environment-specific settings (not committed to git): + +```env +COMPOSE_SERVER_DOMAIN=staging.example.com +PHP_XDEBUG_MODE=debug +``` + +## Docker Compose File Structure + +### Base Configuration (docker-compose.yml) + +The base file defines core services. Standard structure: + +```yaml +networks: + frontend: + external: true + app: + driver: bridge + internal: false + +services: + mariadb: + image: itkdev/mariadb:latest + networks: + - app + ports: + - "3306" + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 10s + interval: 10s + timeout: 5s + retries: 3 + environment: + - MYSQL_ROOT_PASSWORD=password + - MYSQL_USER=db + - MYSQL_PASSWORD=db + - MYSQL_DATABASE=db + labels: + com.symfony.server.service-prefix: "DATABASE" + + phpfpm: + image: itkdev/php8.3-fpm:latest + user: ${COMPOSE_USER:-deploy} + networks: + - app + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - PHP_XDEBUG_MODE=${PHP_XDEBUG_MODE:-off} + - PHP_MAX_EXECUTION_TIME=30 + - PHP_MEMORY_LIMIT=256M + - PHP_SENDMAIL_PATH=/usr/bin/msmtp --host=mail --port=1025 --read-recipients --read-envelope-from + - DOCKER_HOST_DOMAIN=${COMPOSE_DOMAIN} + - PHP_IDE_CONFIG=serverName=localhost + - DRUSH_OPTIONS_URI=http://${COMPOSE_DOMAIN} + depends_on: + mariadb: + condition: service_healthy + memcached: + condition: service_healthy + volumes: + - .:/app + + nginx: + image: nginxinc/nginx-unprivileged:alpine + networks: + - app + - frontend + depends_on: + - phpfpm + ports: + - "8080" + volumes: + - ./.docker/templates:/etc/nginx/templates:ro + - .:/app + environment: + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1:9000 + NGINX_WEB_ROOT: /app/web + NGINX_PORT: 8080 + NGINX_MAX_BODY_SIZE: 5M + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${COMPOSE_DOMAIN}`)" + + memcached: + image: memcached:alpine + networks: + - app + ports: + - "11211" + healthcheck: + test: echo "version" | nc -vn -w 1 127.0.0.1 11211 + interval: 10s + retries: 60 + environment: + - MEMCACHED_CACHE_SIZE=64 + + mail: + image: axllent/mailpit + networks: + - app + - frontend + ports: + - "1025" + - "8025" + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}mail.rule=Host(`mail-${COMPOSE_DOMAIN}`)" + - "traefik.http.services.${COMPOSE_PROJECT_NAME}mail.loadbalancer.server.port=8025" +``` + +### Development Override (docker-compose.override.yml) + +Extends base config for local development. Can include additional compose files: + +```yaml +include: + - docker-compose.oidc.yml + - docker-compose.playwright.yml + +services: + node: + image: node:22 + profiles: + - dev + working_dir: /app + volumes: + - .:/app + + phpfpm: + image: itkdev/php8.4-fpm:latest + environment: + - PHP_POST_MAX_SIZE=90M + - PHP_UPLOAD_MAX_FILESIZE=80M + - PHP_MEMORY_LIMIT=512M +``` + +### Profile-Based Services + +Use Docker Compose profiles for optional services: + +```yaml +services: + markdownlint: + image: itkdev/markdownlint + profiles: + - dev + volumes: + - ./:/md + + prettier: + image: jauderho/prettier + profiles: + - dev + volumes: + - ./:/work + + clamav: + image: clamav/clamav:1.4 + profiles: + - clamav + ports: + - "3310" + - "7357" +``` + +Enable profiles via environment variable: + +```bash +COMPOSE_PROFILES=dev,clamav docker compose up -d +``` + +### OIDC Server Mock (docker-compose.oidc.yml) + +For local authentication testing, use the OIDC Server Mock: + +```yaml +services: + idp-citizen: + image: ghcr.io/soluto/oidc-server-mock:0.8.6 + profiles: + - oidc + - test + # Let this container be accessible both internally and externally on the same domain + container_name: idp-citizen.${COMPOSE_DOMAIN} + networks: + - app + - frontend + ports: + - '443' + volumes: + - .:/tmp/config:ro + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}_idp-citizen.rule=Host(`idp-citizen.${COMPOSE_DOMAIN}`)" + - "traefik.http.services.${COMPOSE_PROJECT_NAME}_idp-citizen.loadbalancer.server.port=443" + - "traefik.http.services.${COMPOSE_PROJECT_NAME}_idp-citizen.loadbalancer.server.scheme=https" + environment: + # HTTPS configuration + ASPNETCORE_URLS: https://+:443;http://+:80 + ASPNETCORE_Kestrel__Certificates__Default__Password: mock + ASPNETCORE_Kestrel__Certificates__Default__Path: /tmp/config/.docker/oidc-server-mock/cert/docker.pfx + ASPNETCORE_ENVIRONMENT: Development + + SERVER_OPTIONS_INLINE: | + AccessTokenJwtType: JWT + Discovery: + ShowKeySet: true + Authentication: + CookieSameSiteMode: Lax + CheckSessionCookieSameSiteMode: Lax + + LOGIN_OPTIONS_INLINE: | + { + "AllowRememberLogin": false + } + + LOGOUT_OPTIONS_INLINE: | + { + "AutomaticRedirectAfterSignOut": true + } + + CLIENTS_CONFIGURATION_INLINE: | + - ClientId: client-id + ClientSecrets: [client-secret] + Description: Mock IdP + AllowedGrantTypes: + - authorization_code + RequireClientSecret: false + AllowAccessTokensViaBrowser: true + AlwaysIncludeUserClaimsInIdToken: true + AllowedScopes: + - openid + - profile + - email + ClientClaimsPrefix: '' + RedirectUris: + - '*' + PostLogoutRedirectUris: + - '*' + RequirePkce: false + + # Custom claim types + OVERRIDE_STANDARD_IDENTITY_RESOURCES: 'true' + IDENTITY_RESOURCES_INLINE: | + - Name: openid + ClaimTypes: + - sub + - Name: email + ClaimTypes: + - email + - Name: profile + ClaimTypes: + - dk_ssn + - name + - zip + - uuid + + USERS_CONFIGURATION_INLINE: | + - SubjectId: 1 + Username: citizen1 + Password: citizen1 + Claims: + - Type: dk_ssn + Value: '1111111111' + ValueType: string + - Type: name + Value: 'Anders And' + ValueType: string +``` + +Certificate setup for OIDC mock requires a `.docker/oidc-server-mock/cert/docker.pfx` file. + +For server OIDC (`docker-compose.server.oidc.yml`), use `COMPOSE_SERVER_DOMAIN` instead of `COMPOSE_DOMAIN` in the container name and labels. + +## Server Configuration (docker-compose.server.yml) + +Optimized for production deployments: + +```yaml +networks: + frontend: + external: true + app: + driver: bridge + internal: false + +services: + phpfpm: + image: itkdev/php8.3-fpm:alpine + restart: unless-stopped + networks: + - app + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - PHP_MAX_EXECUTION_TIME=30 + - PHP_MEMORY_LIMIT=128M + - DRUSH_OPTIONS_URI=https://${COMPOSE_SERVER_DOMAIN} + depends_on: + - memcached + volumes: + - .:/app + + nginx: + image: nginxinc/nginx-unprivileged:alpine + restart: unless-stopped + networks: + - app + - frontend + depends_on: + - phpfpm + volumes: + - ./.docker/templates:/etc/nginx/templates:ro + - ./.docker/nginx.conf:/etc/nginx/nginx.conf:ro + - .:/app + environment: + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1:9000 + NGINX_WEB_ROOT: /app/web + NGINX_PORT: 8080 + NGINX_MAX_BODY_SIZE: 5M + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-http.rule=Host(`${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-http.entrypoints=web" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-http.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.entrypoints=websecure" + + memcached: + image: "memcached:latest" + restart: unless-stopped + networks: + - app + environment: + - MEMCACHED_CACHE_SIZE=64 +``` + +Key differences from development: + +- Uses Alpine-based images (smaller footprint) +- Lower memory limits (128M vs 256M+) +- `restart: unless-stopped` policy +- HTTPS redirect middleware +- No database container (external database) + +### Development/Staging Server (docker-compose.dev.yml) + +Used for development and staging servers with additional services like mail capture and basic auth protection: + +```yaml +# itk-version: 3.2.1 +services: + phpfpm: + environment: + - PHP_SENDMAIL_PATH=/usr/sbin/sendmail -S mail:1025 + + nginx: + labels: + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.middlewares=ITKBasicAuth@file" + + mail: + image: axllent/mailpit + restart: unless-stopped + networks: + - app + - frontend + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME}-http.rule=Host(`mail.${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME}-http.entrypoints=web" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME}-http.middlewares=redirect-to-https" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME}.rule=Host(`mail.${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME}.entrypoints=websecure" + - "traefik.http.services.mail_${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=8025" + - "traefik.http.routers.mail_${COMPOSE_PROJECT_NAME}.middlewares=ITKMailhogAuth@file" +``` + +Key features: +- Basic Auth middleware (`ITKBasicAuth@file`) for protected staging environments +- Mailpit with authentication middleware for mail capture on server + +### WWW Redirect (docker-compose.redirect.yml) + +Handles www to non-www redirects: + +```yaml +# itk-version: 3.2.1 +services: + nginx: + labels: + # Add www before domain and set redirect to non-www + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME}-http.rule=Host(`www.${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME}-http.entrypoints=web" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME}-http.middlewares=redirect-to-https,non_www" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME}.rule=Host(`www.${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME}.entrypoints=websecure" + - "traefik.http.routers.www_${COMPOSE_PROJECT_NAME}.middlewares=non_www" + + - traefik.http.middlewares.non_www.redirectregex.regex=^(http|https)?://(?:www\.)?(.+) + - traefik.http.middlewares.non_www.redirectregex.replacement=https://$${2} + - traefik.http.middlewares.non_www.redirectregex.permanent=true +``` + +### Server Override (docker-compose.server.override.yml) + +Environment-specific overrides for server deployments: + +```yaml +services: + phpfpm: + environment: + - PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 + - PHP_POST_MAX_SIZE=18M + - PHP_UPLOAD_MAX_FILESIZE=15M + + nginx: + environment: + - NGINX_MAX_BODY_SIZE=20M +``` + +Key settings: +- `PHP_OPCACHE_VALIDATE_TIMESTAMPS=0` - Disables OPcache timestamp validation for performance +- Customized upload size limits for the specific environment + +### Production Shared Volumes (docker-compose.server.prod.yml) + +Production-specific volume mounts for shared files between deployments: + +```yaml +services: + phpfpm: + volumes: + - ../../shared/settings.local.php:/app/web/sites/default/settings.local.php + - ../../shared/files:/app/web/sites/default/files + - ../../shared/private:/app/private + + nginx: + volumes: + - ../../shared/files:/app/web/sites/default/files +``` + +This pattern allows: +- Persistent settings across deployments +- Shared file storage between releases +- Private files directory for sensitive uploads + +## itkdev-docker-compose-server CLI + +### Purpose + +`itkdev-docker-compose-server` is a wrapper CLI used in production/staging deployments. It: + +- Automatically uses `docker-compose.server.yml` configuration +- Integrates with deployment pipelines (Woodpecker, Ansible) +- Provides consistent command interface across environments + +### Common Commands + +```bash +# Pull latest images +itkdev-docker-compose-server pull + +# Start services +itkdev-docker-compose-server up --detach --force-recreate --remove-orphans + +# Execute commands in running containers +itkdev-docker-compose-server exec phpfpm composer install --no-dev --optimize-autoloader +itkdev-docker-compose-server exec phpfpm vendor/bin/drush --yes cache:rebuild +itkdev-docker-compose-server exec phpfpm vendor/bin/drush --yes deploy + +# Run one-off commands (creates new container) +itkdev-docker-compose-server run --rm phpfpm vendor/bin/drush --yes cache:rebuild +``` + +### CI/CD Integration (Woodpecker) + +Example staging deployment (`.woodpecker/stg.yml`): + +```yaml +when: + - branch: release/* + event: push + +skip_clone: true + +labels: + zone: CLOUD + +steps: + - name: Run test site update + image: itkdev/ansible-plugin:1 + pull: true + settings: + id: + from_secret: id + secret: + from_secret: secret + host: + from_secret: stg_host + path: + from_secret: stg_path + user: + from_secret: user + actions: + # Make Drupal settings writable before git operations + - chmod +w web/sites/default + - chmod +w web/sites/default/settings.php + - git reset --hard + - git fetch origin ${CI_COMMIT_BRANCH} + - git checkout ${CI_COMMIT_BRANCH} + - git pull + - itkdev-docker-compose-server up -d --force-recreate + - itkdev-docker-compose-server exec phpfpm composer install --no-dev -o --classmap-authoritative + # Workaround for autoloader issues + - itkdev-docker-compose-server run --rm phpfpm composer dump-autoload + - itkdev-docker-compose-server exec phpfpm vendor/bin/drush --yes cache:rebuild + - itkdev-docker-compose-server exec phpfpm vendor/bin/drush --yes deploy +``` + +Example production deployment (`.woodpecker/prod.yml`): + +```yaml +when: + - event: release + +skip_clone: true + +labels: + zone: CLOUD + +steps: + - name: Ansible playbook + image: itkdev/ansible-plugin:1 + pull: true + settings: + id: + from_secret: id + secret: + from_secret: secret + host: + from_secret: prod_host + path: + from_secret: prod_path + user: + from_secret: user + playbook: 'release' + pre_up: + # Workaround for 'Drupal\mysql\Driver\Database\mysql\Connection' not found + - itkdev-docker-compose-server run --rm phpfpm composer dump-autoload + - itkdev-docker-compose-server run --rm phpfpm vendor/bin/drush --yes cache:rebuild + - itkdev-docker-compose-server run --rm phpfpm vendor/bin/drush --yes deploy + cron: + cron: + minute: '*/5' + hour: '*' + day: '*' + month: '*' + weekday: '*' + job: 'itkdev-docker-compose-server exec phpfpm vendor/bin/drush --yes core:cron' +``` + +Key Woodpecker patterns: + +| Setting | Purpose | +|---------|---------| +| `skip_clone: true` | Skip automatic git clone (code already on server) | +| `labels: zone: CLOUD` | Route to specific runner pool | +| `pull: true` | Always pull latest plugin image | +| `chmod +w` | Make Drupal settings writable before git operations | +| `composer dump-autoload` | Fix autoloader issues after composer install | + +## Networks + +### Frontend Network + +External network shared with Traefik reverse proxy. Must be created before starting containers: + +```bash +docker network create frontend +``` + +### App Network + +Internal bridge network for service communication. Created automatically. + +## Standard ITK Dev Docker Images + +| Image | Purpose | +|-------|---------| +| `itkdev/mariadb:latest` | MySQL-compatible database | +| `itkdev/php8.3-fpm:latest` | PHP-FPM for development | +| `itkdev/php8.3-fpm:alpine` | PHP-FPM for production | +| `itkdev/php8.4-fpm:latest` | PHP 8.4 variant | +| `nginxinc/nginx-unprivileged:alpine` | Web server | +| `memcached:alpine` | Cache server | +| `axllent/mailpit` | Development mail capture | +| `itkdev/markdownlint` | Markdown linting | +| `jauderho/prettier` | Code formatting | + +## Directory Structure + +``` +.docker/ +├── templates/ +│ └── default.conf.template # Nginx vhost template +├── nginx.conf # Nginx main config (for production) +├── data/ # Local data storage (gitignored) +├── oidc-server-mock/ +│ └── cert/ +│ └── docker.pfx # OIDC mock certificate +└── drupal/ + └── dumps/ + └── drupal.sql.gz # Database dump +``` + +## Nginx Configuration + +### Main Configuration (nginx.conf) + +Used in production for proper client IP logging behind Traefik: + +```nginx +worker_processes auto; + +error_log /dev/stderr notice; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + proxy_temp_path /tmp/proxy_temp; + client_body_temp_path /tmp/client_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Real IP configuration for Traefik proxy + set_real_ip_from 172.16.0.0/16; + real_ip_recursive on; + real_ip_header X-Forwarded-For; + + log_format main '$http_x_real_ip - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/stdout main; + + sendfile on; + keepalive_timeout 65; + gzip on; + + include /etc/nginx/conf.d/*.conf; +} +``` + +### Vhost Template (default.conf.template) + +Drupal-optimized nginx configuration with security rules: + +```nginx +server { + listen ${NGINX_PORT}; + server_name localhost; + + root ${NGINX_WEB_ROOT}; + client_max_body_size ${NGINX_MAX_BODY_SIZE}; + + # Real IP for proper logging behind proxy + set_real_ip_from 172.16.0.0/16; + real_ip_recursive on; + real_ip_header X-Forwarded-For; + + # Static file handling + location = /favicon.ico { log_not_found off; access_log off; } + location = /robots.txt { allow all; log_not_found off; access_log off; } + + # Security rules + location ~* \.(txt|log)$ { deny all; } + location ~ \..*/.*\.php$ { return 403; } + location ~ ^/sites/.*/private/ { return 403; } + location ~ ^/sites/[^/]+/files/.*\.php$ { deny all; } + location ~ (^|/)\. { return 403; } + location ~ /vendor/.*\.php$ { deny all; return 404; } + + # Protect sensitive files + location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|.tar|.gz|.bz2|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { + deny all; + return 404; + } + + # Main location + location / { + try_files $uri /index.php?$query_string; + } + + # PHP handling + location ~ '\.php$|^/update.php' { + fastcgi_buffers 16 32k; + fastcgi_buffer_size 64k; + fastcgi_busy_buffers_size 64k; + fastcgi_split_path_info ^(.+?\.php)(|/.*)$; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param QUERY_STRING $query_string; + fastcgi_intercept_errors on; + fastcgi_pass ${NGINX_FPM_SERVICE}; + } + + # Drupal image styles + location ~ ^/sites/.*/files/styles/ { try_files $uri @rewrite; } + + # Private files + location ~ ^(/[a-z\-]+)?/system/files/ { + try_files $uri /index.php?$query_string; + } + + # Static assets with caching + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + try_files $uri @rewrite; + expires max; + log_not_found off; + } + + location @rewrite { rewrite ^/(.*)$ /index.php?q=$1; } + + # Clean URLs + absolute_redirect off; + if ($request_uri ~* "^(.*/)index\.php/(.*)") { + return 301 /$2; + } + + error_log /dev/stderr; + access_log /dev/stdout main; +} +``` + +## Traefik Labels + +Standard Traefik labels for routing: + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${COMPOSE_DOMAIN}`)" +``` + +For HTTPS with redirect: + +```yaml +labels: + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-http.rule=Host(`${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-http.entrypoints=web" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-http.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${COMPOSE_SERVER_DOMAIN}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.entrypoints=websecure" +``` diff --git a/docs/itkdev-task-files.md b/docs/itkdev-task-files.md new file mode 100644 index 0000000..555add1 --- /dev/null +++ b/docs/itkdev-task-files.md @@ -0,0 +1,608 @@ +# ITK Dev Taskfile Patterns + +This document describes the Taskfile patterns used in ITK Dev projects. Tasks are managed using [Task](https://taskfile.dev/) (go-task), a task runner similar to Make but with YAML configuration. + +## Overview + +The Taskfile provides: + +- Consistent command interface across projects +- Integration with Docker Compose +- Automation of development workflows +- Coding standards enforcement + +## Configuration + +### Taskfile.yml Structure + +```yaml +version: "3" + +# Load environment variables from files +dotenv: [".env.local", ".env"] + +vars: + BASE_URL: '{{.TASK_BASE_URL | default .COMPOSE_SERVER_DOMAIN | default .COMPOSE_DOMAIN | default "https://example.local.itkdev.dk"}}' + DOCKER_COMPOSE: '{{.TASK_DOCKER_COMPOSE | default "docker compose"}}' + DOCKER_COMPOSE_PROFILES: '{{.TASK_DOCKER_COMPOSE_PROFILES | default ""}}' + COMPOSER_INSTALL_ARGUMENTS: '{{.TASK_COMPOSER_INSTALL_ARGUMENTS | default ""}}' + +tasks: + default: + cmds: + - task --list-all + silent: true +``` + +### Variable Hierarchy + +Variables can be overridden via environment: + +| Variable | Environment Override | Purpose | +|----------|---------------------|---------| +| `BASE_URL` | `TASK_BASE_URL` | Site URL for Drush commands | +| `DOCKER_COMPOSE` | `TASK_DOCKER_COMPOSE` | Docker compose command | +| `DOCKER_COMPOSE_PROFILES` | `TASK_DOCKER_COMPOSE_PROFILES` | Docker profiles to enable | +| `COMPOSER_INSTALL_ARGUMENTS` | `TASK_COMPOSER_INSTALL_ARGUMENTS` | Extra composer install flags | + +## Core Tasks + +### Docker Compose Wrapper + +```yaml +compose: + cmds: + - '{{.DOCKER_COMPOSE}} {{if .DOCKER_COMPOSE_PROFILES}}--profile {{.DOCKER_COMPOSE_PROFILES | splitList "," |join " --profile " }}{{end}} {{.CLI_ARGS}}' + vars: + DOCKER_COMPOSE_PROFILES: "{{.PROFILES | default .DOCKER_COMPOSE_PROFILES}}" + +compose-up: + cmds: + - task compose -- up --detach --remove-orphans {{if .COMPOSE_UP_WAIT}}--wait{{end}} + silent: true +``` + +Usage: + +```bash +# Basic compose commands +task compose -- ps +task compose -- logs -f + +# With profiles +PROFILES=dev,oidc task compose-up + +# Or via environment variable +TASK_DOCKER_COMPOSE_PROFILES=dev task compose-up +``` + +### Site Management + +```yaml +site-install: + prompt: "This will reset your setup. Continue?" + cmds: + - task compose -- down + - task compose -- pull + - task compose-up + - task composer-install + - task drush -- --yes site:install --existing-config + - task: translations:import + - task drush -- --yes cache:rebuild + - task assets-build + - task site-open + - task site-open-admin + silent: true + +site-update: + cmds: + - task compose -- pull + - task compose-up + - task composer-install + - task assets-build + - task drush -- deploy + - task: translations:import + - task drush -- --yes cache:rebuild + silent: true + +site-url: + cmds: + - task drush -- browse --no-browser + silent: true + +site-open: + cmds: + - if command -v open 2>&1 >/dev/null; then open "$(task site-url)"; else echo "$(task site-url)"; fi + silent: true + +site-open-admin: + cmds: + - if command -v open 2>&1 >/dev/null; then open "{{.URL}}"; else echo "{{.URL}}"; fi + vars: + URL: + sh: task drush -- user:login --no-browser + silent: true +``` + +Usage: + +```bash +# Fresh installation (will prompt for confirmation) +task site-install + +# Update existing site +task site-update + +# Get site URL +task site-url + +# Open site in browser +task site-open + +# Get admin login link +task site-open-admin +``` + +### Composer + +```yaml +composer: + cmds: + - task compose -- exec phpfpm composer {{.CLI_ARGS}} + silent: true + +composer-install: + cmds: + - task composer -- install {{.COMPOSER_INSTALL_ARGUMENTS}} {{.CLI_ARGS}} + silent: true +``` + +Usage: + +```bash +# Run any composer command +task composer -- require drupal/module + +# Install dependencies +task composer-install + +# Install with extra arguments +TASK_COMPOSER_INSTALL_ARGUMENTS="--no-dev" task composer-install +``` + +### Drush (Drupal CLI) + +```yaml +drush: + cmds: + - >- + if [[ ! -t 0 ]]; then + task compose -- exec --no-TTY phpfpm {{.DRUSH_SCRIPT}} --uri={{.BASE_URL}} {{.CLI_ARGS}}; + else + task compose -- exec phpfpm {{.DRUSH_SCRIPT}} --uri={{.BASE_URL}} {{.CLI_ARGS}}; + fi + vars: + DRUSH_SCRIPT: vendor/bin/drush.php + silent: true +``` + +Usage: + +```bash +# Run drush commands +task drush -- cache:rebuild +task drush -- --yes deploy +task drush -- user:login + +# Pipe SQL queries +echo "SELECT * FROM users" | task drush -- sql:query +``` + +### Asset Building + +```yaml +assets-build: + cmds: + - | + if [[ -z "{{.SKIP_BUILD}}" ]]; then + {{.DOCKER_COMPOSE}} run --rm node npm install --prefix web/themes/custom/ + {{.DOCKER_COMPOSE}} run --rm node npm run build --prefix web/themes/custom/ + fi + vars: + SKIP_BUILD: "{{.ASSETS_SKIP_BUILD | default .TASK_ASSETS_SKIP_BUILD}}" + +assets-watch: + cmds: + - "{{.DOCKER_COMPOSE}} run --rm node npm run watch --prefix web/themes/custom/" +``` + +Usage: + +```bash +# Build assets +task assets-build + +# Skip build (useful in CI) +ASSETS_SKIP_BUILD=1 task site-update + +# Watch for changes +task assets-watch +``` + +## Coding Standards + +### Unified Check/Apply + +```yaml +coding-standards:apply: + desc: "Apply coding standards" + cmds: + - task: coding-standards:javascript:apply + - task: coding-standards:markdown:apply + - task: coding-standards:php:apply + - task: coding-standards:styles:apply + - task: coding-standards:twig:apply + - task: coding-standards:yaml:apply + silent: true + +coding-standards:check: + desc: "Check coding standards" + cmds: + - task: coding-standards:javascript:check + - task: coding-standards:markdown:check + - task: coding-standards:php:check + - task: coding-standards:styles:check + - task: coding-standards:twig:check + - task: coding-standards:yaml:check + silent: true +``` + +### Language-Specific Tasks + +```yaml +# PHP (phpcs/phpcbf) +coding-standards:php:apply: + desc: "Apply coding standards for PHP" + cmds: + - docker compose run --rm phpfpm vendor/bin/phpcbf + silent: true + +coding-standards:php:check: + desc: "Check coding standards for PHP" + cmds: + - task: coding-standards:php:apply + - docker compose run --rm phpfpm vendor/bin/phpcs + silent: true + +# JavaScript (prettier) +coding-standards:javascript:apply: + desc: "Apply coding standards for JavaScript" + cmds: + - docker compose run --rm prettier 'web/themes/custom/**/js/**/*.js' --write + +coding-standards:javascript:check: + desc: "Check coding standards for JavaScript" + cmds: + - task: coding-standards:javascript:apply + - docker compose run --rm prettier 'web/themes/custom/**/js/**/*.js' --check + +# Markdown (markdownlint) +coding-standards:markdown:apply: + desc: "Apply coding standards for Markdown" + cmds: + - docker compose run --rm markdownlint markdownlint '**/*.md' --fix + +coding-standards:markdown:check: + desc: "Check coding standards for Markdown" + cmds: + - task: coding-standards:markdown:apply + - docker compose run --rm markdownlint markdownlint '**/*.md' + +# Styles (prettier for CSS/SCSS) +coding-standards:styles:apply: + desc: "Apply coding standards for styles" + cmds: + - docker compose run --rm prettier 'web/themes/custom/**/css/**/*.{css,scss}' --write + +coding-standards:styles:check: + desc: "Check coding standards for styles" + cmds: + - task: coding-standards:styles:apply + - docker compose run --rm prettier 'web/themes/custom/**/css/**/*.{css,scss}' --check + +# Twig (twig-cs-fixer) +coding-standards:twig:apply: + desc: "Apply coding standards for Twig" + cmds: + - docker compose run --rm phpfpm vendor/bin/twig-cs-fixer fix + silent: true + +coding-standards:twig:check: + desc: "Check coding standards for Twig" + cmds: + - task: coding-standards:twig:apply + - docker compose run --rm phpfpm vendor/bin/twig-cs-fixer lint + silent: true + +# YAML (prettier) +coding-standards:yaml:apply: + desc: "Apply coding standards for YAML" + cmds: + - docker compose run --rm prettier '**/*.{yml,yaml}' --write + +coding-standards:yaml:check: + desc: "Check coding standards for YAML" + cmds: + - task: coding-standards:yaml:apply + - docker compose run --rm prettier '**/*.{yml,yaml}' --check +``` + +Usage: + +```bash +# Apply all standards +task coding-standards:apply + +# Check all standards +task coding-standards:check + +# Language-specific +task coding-standards:php:check +task coding-standards:javascript:apply +``` + +## Code Analysis + +```yaml +code-analysis: + cmds: + - docker compose run --rm phpfpm vendor/bin/phpstan analyse --configuration=phpstan.neon + # Check specific modules with higher level + - docker compose run --rm phpfpm vendor/bin/phpstan analyse --configuration=phpstan.neon --level=9 web/modules/custom// +``` + +Usage: + +```bash +task code-analysis +``` + +## Database Operations + +```yaml +database-dump: + cmds: + - task site-update + - task drush -- sql:dump --extra-dump='--skip-column-statistics' --structure-tables-list="cache,cache_*,advancedqueue,history,search_*,sessions,watchdog" --gzip --result-file=/app/.docker/drupal/dumps/drupal.sql +``` + +Usage: + +```bash +task database-dump +``` + +## Development Settings + +```yaml +development-settings:do-not-cache-markup-enable: + desc: "Disable markup cache" + cmds: + - task drush -- php:eval "Drupal::keyValue('development_settings')->setMultiple(['disable_rendered_output_cache_bins' => TRUE]);" + - task drush -- cache:rebuild + +development-settings:do-not-cache-markup-disable: + desc: "Enable markup cache (for production)" + cmds: + - task drush -- php:eval "Drupal::keyValue('development_settings')->setMultiple(['disable_rendered_output_cache_bins' => FALSE]);" + - task drush -- cache:rebuild + +development-settings:twig-develoment-mode-enable: + desc: "Enable Twig development mode" + cmds: + - task drush -- php:eval "Drupal::keyValue('development_settings')->setMultiple(['twig_debug' => TRUE, 'twig_cache_disable' => TRUE]);" + - task drush -- cache:rebuild + +development-settings:twig-develoment-mode-disable: + desc: "Disable Twig development mode" + cmds: + - task drush -- php:eval "Drupal::keyValue('development_settings')->setMultiple(['twig_debug' => FALSE, 'twig_cache_disable' => FALSE]);" + - task drush -- cache:rebuild + +development-settings:development: + desc: "Set cache settings for development" + cmds: + - task development-settings:do-not-cache-markup-enable + - task development-settings:twig-develoment-mode-enable + - task drush -- cache:rebuild + silent: true + +development-settings:production: + desc: "Set cache settings for production" + cmds: + - task development-settings:do-not-cache-markup-disable + - task development-settings:twig-develoment-mode-disable + - task drush -- cache:rebuild + silent: true +``` + +Usage: + +```bash +# Enable development-friendly settings +task development-settings:development + +# Enable production settings locally +task development-settings:production +``` + +## Translations + +```yaml +translations:import: + cmds: + - task compose -- exec phpfpm bash -c '(cd web && ../vendor/bin/drush locale:import --type=customized --override=all da ../translations/custom-translations.da.po)' + silent: true + +translations:export: + cmds: + - task compose -- exec phpfpm bash -c '(cd web && ../vendor/bin/drush locale:export da --types=customized > ../translations/custom-translations.da.po)' + silent: true +``` + +Usage: + +```bash +# Import translations +task translations:import + +# Export translations +task translations:export +``` + +## Docker Image Management + +```yaml +docker-pull: + desc: "Pull all development docker images" + cmds: + - docker pull jauderho/prettier + - docker pull peterdavehello/markdownlint + - task compose -- pull +``` + +Usage: + +```bash +task docker-pull +``` + +## Fixtures and Test Data + +```yaml +fixtures:load: + prompt: "This will reset your content. Continue?" + cmds: + - COMPOSE_PROFILES= COMPOSE_UP_WAIT=true task compose-up + - task drush -- --yes pm:enable + - task drush -- --yes content-fixtures:load + - task drush -- --yes pm:uninstall content_fixtures + - task compose-up + silent: true +``` + +Usage: + +```bash +task fixtures:load +``` + +## Task Patterns + +### Prompts for Destructive Operations + +```yaml +dangerous-task: + prompt: "This will reset your setup. Continue?" + cmds: + - # destructive commands +``` + +### Silent Output + +Use `silent: true` to suppress command echoing: + +```yaml +my-task: + cmds: + - echo "hello" + silent: true +``` + +### Dynamic Variables + +Compute variables from shell commands: + +```yaml +my-task: + vars: + VALUE: + sh: some-command-that-outputs-value + cmds: + - echo "{{.VALUE}}" +``` + +### Calling Other Tasks + +```yaml +parent-task: + cmds: + - task: child-task-1 + - task: child-task-2 + - task drush -- some-command +``` + +### CLI Arguments + +Pass arguments through `{{.CLI_ARGS}}`: + +```yaml +my-task: + cmds: + - some-command {{.CLI_ARGS}} +``` + +Usage: + +```bash +task my-task -- --flag value +``` + +## Common Workflows + +### New Developer Setup + +```bash +# Clone repository +git clone +cd + +# Copy environment template +cp .env.example .env.local + +# Pull images and start services +task docker-pull +task site-install + +# Enable development settings +task development-settings:development +``` + +### Daily Development + +```bash +# Start services +task compose-up + +# Update after pulling changes +task site-update + +# Watch assets +task assets-watch +``` + +### Before Committing + +```bash +# Apply and check all standards +task coding-standards:apply +task coding-standards:check +task code-analysis +``` + +### Creating Database Dump + +```bash +task database-dump +# Creates .docker/drupal/dumps/drupal.sql.gz +``` diff --git a/docs/rfc-mcp-server.md b/docs/rfc-mcp-server.md new file mode 100644 index 0000000..84228a8 --- /dev/null +++ b/docs/rfc-mcp-server.md @@ -0,0 +1,312 @@ +# RFC: MCP Server for ITK Dev Docker + +**Status:** Proposal +**Author:** AI-assisted development +**Date:** 2025-12-20 +**Branch:** feature/mcp + +## Summary + +This RFC proposes adding a Model Context Protocol (MCP) server to the itkdev-docker repository. The MCP server will provide AI coding assistants (like Claude Code) with structured access to ITK Dev documentation, project detection capabilities, and template management tools. + +## Motivation + +### Problem Statement + +When developers use AI coding assistants on ITK Dev projects, the assistants lack context about: + +1. ITK Dev Docker patterns and conventions +2. Available templates and their differences +3. The `itkdev-docker-compose` CLI tool and its commands +4. Taskfile patterns used across projects +5. How to detect and compare project configurations against templates + +This leads to: +- Inconsistent AI-generated configurations +- Manual explanation of ITK Dev patterns in every session +- Inability to leverage AI for project setup/maintenance tasks + +### Proposed Solution + +Create an MCP server that provides: + +1. **Resources**: Documentation accessible to AI assistants +2. **Tools**: Capabilities to detect, analyze, and compare ITK Dev projects + +## Background Research + +### What is MCP? + +Model Context Protocol (MCP) is an open protocol developed by Anthropic that enables AI applications to connect with external data sources and tools. It provides a standardized way to: + +- Expose resources (documents, data) to AI models +- Provide tools that AI can invoke +- Maintain context across interactions + +### Hosting Options Considered + +| Option | Description | Pros | Cons | +|--------|-------------|------|------| +| **Local stdio** | Process spawned by Claude Code, communicates via stdin/stdout | Simple, secure, no infrastructure | Per-machine setup | +| **npx package** | Published to npm, run via npx | Easy distribution | Requires npm maintenance | +| **HTTP/SSE server** | Persistent network service | Centralized, team-wide | Infrastructure overhead, security concerns | +| **Docker container** | Packaged in Docker image | Portable | Additional complexity | + +### Chosen Approach: Local Stdio + +We chose the **local stdio** approach because: + +1. **Simplicity**: No network infrastructure required +2. **Security**: No exposed ports or authentication needed +3. **Reliability**: No dependency on external services +4. **Integration**: Natural fit with existing itkdev-docker repository +5. **Offline support**: Works without network access + +The MCP server will be added to the existing itkdev-docker repository, allowing developers to configure it once after cloning/updating the repo. + +## Documentation Created + +As part of this research, we created comprehensive documentation: + +### 1. itkdev-docker-cli.md + +Documents the `itkdev-docker-compose` CLI tool: + +- Installation and prerequisites +- All 25+ CLI commands with examples +- Available templates and selection guide +- Template file structure +- Traefik infrastructure setup +- Environment variables reference +- Complete setup workflows +- Troubleshooting guide + +### 2. itkdev-docker-compose.md + +Documents Docker Compose patterns: + +- Service configurations (mariadb, phpfpm, nginx, memcached, mail) +- Environment configuration +- Profile-based services +- OIDC mock setup +- Server configurations +- Traefik labels and routing +- Nginx configuration patterns + +### 3. itkdev-task-files.md + +Documents Taskfile automation: + +- Taskfile.yml structure +- Docker Compose wrapper tasks +- Site management tasks +- Coding standards tasks +- Asset building patterns +- Database operations +- Development workflows + +## Technical Design + +### Repository Structure + +``` +itkdev-docker/ +├── mcp/ # NEW: MCP server +│ ├── package.json +│ ├── tsconfig.json +│ ├── src/ +│ │ └── index.ts # MCP server implementation +│ └── dist/ # Compiled JavaScript (gitignored) +├── docs/ +│ ├── rfc-mcp-server.md # This document +│ └── github-actions-templates.md +├── scripts/ +│ └── itkdev-docker-compose +├── templates/ +│ └── [template directories] +├── itkdev-docker-cli.md # NEW: CLI documentation +├── itkdev-docker-compose.md # NEW: Compose documentation +└── itkdev-task-files.md # NEW: Taskfile documentation +``` + +### MCP Server Capabilities + +#### Resources + +| URI | Description | +|-----|-------------| +| `itkdev://docs/cli` | CLI tool documentation | +| `itkdev://docs/compose` | Docker Compose patterns | +| `itkdev://docs/taskfile` | Taskfile automation patterns | + +#### Tools + +| Tool | Description | Parameters | +|------|-------------|------------| +| `itkdev_list_templates` | List available templates | None | +| `itkdev_detect_project` | Detect project type and configuration | `path` | +| `itkdev_get_template_files` | List files in a template | `template` | +| `itkdev_compare_project` | Compare project against template | `path`, `template` (optional) | + +### User Configuration + +First, build the MCP server: + +```bash +cd /path/to/itkdev-docker/mcp +npm install +npm run build +``` + +Then configure Claude Code using one of these methods: + +#### Option 1: User-wide configuration (all projects) + +Create or edit `~/.claude.json`: + +```json +{ + "mcpServers": { + "itkdev": { + "command": "node", + "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] + } + } +} +``` + +#### Option 2: Project-specific configuration + +Create `.mcp.json` in your project root: + +```json +{ + "mcpServers": { + "itkdev": { + "command": "node", + "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] + } + } +} +``` + +#### Option 3: CLI command + +```bash +claude mcp add itkdev --scope user -- node /path/to/itkdev-docker/mcp/dist/index.js +``` + +After configuration, restart Claude Code and run `/mcp` to verify the server is connected. + +## Use Cases + +### 1. New Project Setup + +Developer: "Set up a new Drupal 11 project called 'citizen-portal'" + +AI assistant can: +1. Read `itkdev://docs/cli` for setup instructions +2. Use `itkdev_list_templates` to confirm template exists +3. Provide exact commands based on documentation + +### 2. Project Analysis + +Developer: "Is this project using the latest template version?" + +AI assistant can: +1. Use `itkdev_detect_project` to analyze current configuration +2. Use `itkdev_compare_project` to find differences +3. Suggest specific updates needed + +### 3. Troubleshooting + +Developer: "Docker containers won't start" + +AI assistant can: +1. Read documentation for troubleshooting steps +2. Use `itkdev_detect_project` to understand configuration +3. Provide targeted solutions + +### 4. Template Migration + +Developer: "Upgrade this project from drupal-10 to drupal-11" + +AI assistant can: +1. Use `itkdev_compare_project` with both templates +2. Identify specific changes needed +3. Guide through migration steps + +## Implementation Plan + +### Phase 1: Core Implementation + +1. Create MCP server with documentation resources +2. Implement basic tools (list templates, detect project) +3. Add installation documentation to README + +### Phase 2: Enhanced Tools + +1. Add `itkdev_compare_project` tool +2. Add `itkdev_get_template_files` tool +3. Consider additional tools based on usage + +### Phase 3: Distribution (Future) + +1. Consider npm package publication +2. Evaluate team adoption and feedback +3. Iterate on tool capabilities + +## Alternatives Considered + +### 1. CLAUDE.md Files Only + +Add CLAUDE.md to each project template pointing to documentation. + +**Rejected because:** +- No tool capabilities +- Requires path assumptions +- Less discoverable + +### 2. Central HTTP Server + +Run MCP server as shared team service. + +**Rejected because:** +- Infrastructure overhead +- Single point of failure +- Overkill for current needs + +### 3. Separate Repository + +Create standalone mcp-itkdev repository. + +**Rejected because:** +- Fragmented maintenance +- Documentation sync issues +- Additional repo to manage + +## Security Considerations + +- MCP server runs locally with user permissions +- No network exposure +- Read-only access to template files +- No credentials or secrets handled + +## Open Questions + +1. Should we publish to npm for easier distribution? +2. What additional tools would be valuable? +3. Should CLAUDE.md templates also be added to project templates? + +## Success Metrics + +- Developers can configure MCP server in < 5 minutes +- AI assistants correctly reference ITK Dev patterns +- Reduced time explaining conventions in AI sessions +- Successful project setup/analysis via AI tools + +## References + +- [Model Context Protocol Documentation](https://modelcontextprotocol.io/) +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) +- [Claude Code MCP Integration](https://docs.anthropic.com/en/docs/claude-code) diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 0000000..dd6e803 --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..f3d9a8e --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,206 @@ +# ITK Dev Docker MCP Server + +Model Context Protocol (MCP) server that provides AI coding assistants with access to ITK Dev Docker documentation and project analysis tools. + +## What is MCP? + +MCP (Model Context Protocol) is an open protocol that enables AI assistants like Claude to access external resources and tools. This server provides: + +- **Documentation Resources**: Access to ITK Dev Docker documentation +- **Project Tools**: Analyze, detect, and compare ITK Dev projects + +## Installation + +### Prerequisites + +- Node.js 18 or later +- npm + +### Build + +```bash +cd mcp +npm install +npm run build +``` + +### Configure Claude Code + +Add to `~/.claude/settings.json`: + +```json +{ + "mcpServers": { + "itkdev": { + "command": "node", + "args": ["/absolute/path/to/itkdev-docker/mcp/dist/index.js"] + } + } +} +``` + +Replace `/absolute/path/to/itkdev-docker` with the actual path to your itkdev-docker clone. + +**Example:** + +```json +{ + "mcpServers": { + "itkdev": { + "command": "node", + "args": ["/Users/developer/itkdev-docker/mcp/dist/index.js"] + } + } +} +``` + +### Verify Installation + +Restart Claude Code and check that the MCP server is connected. You can ask Claude: + +> "What ITK Dev templates are available?" + +Claude should be able to list templates using the `itkdev_list_templates` tool. + +## Available Resources + +| URI | Description | +|-----|-------------| +| `itkdev://docs/cli` | CLI tool commands, templates, and setup procedures | +| `itkdev://docs/compose` | Docker Compose patterns and configurations | +| `itkdev://docs/taskfile` | Taskfile automation patterns | + +## Available Tools + +### itkdev_list_templates + +List all available ITK Dev Docker templates with their characteristics. + +**Example prompt:** +> "What templates are available for Drupal projects?" + +### itkdev_detect_project + +Analyze a directory to detect ITK Dev project configuration. + +**Parameters:** +- `path` (required): Absolute path to the project directory + +**Example prompt:** +> "Analyze the project at /Users/me/projects/mysite" + +**Returns:** +- Project type (Drupal/Symfony) +- Template in use +- PHP version +- Web root +- Services defined +- ITK version + +### itkdev_get_template_files + +List all files that would be installed by a template. + +**Parameters:** +- `template` (required): Template name (e.g., `drupal-11`) + +**Example prompt:** +> "What files does the drupal-11 template include?" + +### itkdev_compare_project + +Compare a project against its template to find differences. + +**Parameters:** +- `path` (required): Absolute path to the project +- `template` (optional): Template to compare against (auto-detected from .env) + +**Example prompt:** +> "Is my project at /Users/me/projects/mysite up to date with its template?" + +**Returns:** +- Missing files +- Outdated files (with version comparison) +- Matching files +- Update recommendations + +### itkdev_get_template_content + +Get the content of a specific file from a template. + +**Parameters:** +- `template` (required): Template name +- `file` (required): Relative file path + +**Example prompt:** +> "Show me the docker-compose.yml from the drupal-11 template" + +## Use Cases + +### Setting Up a New Project + +> "I need to set up a new Drupal 11 project. What template should I use and what are the steps?" + +Claude will read the documentation and provide step-by-step instructions. + +### Checking Project Status + +> "Analyze my project at /path/to/project and tell me if it needs updates" + +Claude will detect the project configuration and compare against the template. + +### Understanding Configurations + +> "What services are included in the drupal-11 template and what do they do?" + +Claude will read the documentation and explain each service. + +### Troubleshooting + +> "My Docker containers won't start. Can you help debug?" + +Claude will read troubleshooting documentation and analyze your project configuration. + +## Development + +### Watch Mode + +```bash +npm run dev +``` + +Rebuilds on file changes. + +### Project Structure + +``` +mcp/ +├── src/ +│ └── index.ts # MCP server implementation +├── dist/ # Compiled output (gitignored) +├── package.json +├── tsconfig.json +└── README.md +``` + +## Troubleshooting + +### Server Not Connecting + +1. Check that the path in `settings.json` is absolute and correct +2. Ensure `npm run build` completed successfully +3. Check Node.js version (`node --version` should be 18+) +4. Restart Claude Code after configuration changes + +### Tools Not Working + +1. Verify the itkdev-docker repository is complete (has templates/, documentation files) +2. Check file permissions on the mcp/dist directory +3. Look for error messages in Claude Code's output + +### Documentation Not Found + +Ensure these files exist in `itkdev-docker/docs/`: +- `docs/itkdev-docker-cli.md` +- `docs/itkdev-docker-compose.md` +- `docs/itkdev-task-files.md` diff --git a/mcp/package-lock.json b/mcp/package-lock.json new file mode 100644 index 0000000..553bcf3 --- /dev/null +++ b/mcp/package-lock.json @@ -0,0 +1,1163 @@ +{ + "name": "@itkdev/mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@itkdev/mcp-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", + "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz", + "integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 0000000..90684d1 --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,31 @@ +{ + "name": "@itkdev/mcp-server", + "version": "1.0.0", + "description": "MCP server for ITK Dev Docker projects - provides documentation and tools for AI assistants", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsc --watch" + }, + "keywords": [ + "mcp", + "itkdev", + "docker", + "claude", + "ai" + ], + "author": "ITK Dev", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/mcp/src/index.ts b/mcp/src/index.ts new file mode 100644 index 0000000..cc2143e --- /dev/null +++ b/mcp/src/index.ts @@ -0,0 +1,696 @@ +#!/usr/bin/env node + +/** + * ITK Dev Docker MCP Server + * + * Provides AI assistants with access to ITK Dev Docker documentation + * and tools for project detection and analysis. + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { readFileSync, existsSync, readdirSync, statSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { execSync } from "child_process"; + +// Get the directory where this script is located +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Root of the itkdev-docker repository (parent of mcp/) +const REPO_ROOT = join(__dirname, "../.."); + +// Documentation files +const DOCS = { + cli: { + path: join(REPO_ROOT, "docs/itkdev-docker-cli.md"), + name: "ITK Dev Docker CLI", + description: + "CLI tool commands, templates, Traefik setup, and complete setup procedures", + }, + compose: { + path: join(REPO_ROOT, "docs/itkdev-docker-compose.md"), + name: "ITK Dev Docker Compose", + description: + "Docker Compose patterns, service configurations, and server deployments", + }, + taskfile: { + path: join(REPO_ROOT, "docs/itkdev-task-files.md"), + name: "ITK Dev Taskfile", + description: "Taskfile automation patterns for development workflows", + }, +}; + +// Templates directory +const TEMPLATES_DIR = join(REPO_ROOT, "templates"); + +/** + * Create and configure the MCP server + */ +function createServer(): Server { + const server = new Server( + { + name: "itkdev-docker", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + resources: {}, + }, + } + ); + + // Register resource handlers + registerResourceHandlers(server); + + // Register tool handlers + registerToolHandlers(server); + + return server; +} + +/** + * Register handlers for documentation resources + */ +function registerResourceHandlers(server: Server): void { + // List available resources + server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: Object.entries(DOCS).map(([key, doc]) => ({ + uri: `itkdev://docs/${key}`, + name: doc.name, + description: doc.description, + mimeType: "text/markdown", + })), + })); + + // Read a specific resource + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const uri = request.params.uri; + const match = uri.match(/^itkdev:\/\/docs\/(\w+)$/); + + if (!match) { + throw new Error(`Invalid resource URI: ${uri}`); + } + + const docKey = match[1] as keyof typeof DOCS; + const doc = DOCS[docKey]; + + if (!doc) { + throw new Error(`Unknown resource: ${uri}`); + } + + if (!existsSync(doc.path)) { + throw new Error(`Documentation file not found: ${doc.path}`); + } + + const content = readFileSync(doc.path, "utf-8"); + + return { + contents: [ + { + uri, + mimeType: "text/markdown", + text: content, + }, + ], + }; + }); +} + +/** + * Register handlers for tools + */ +function registerToolHandlers(server: Server): void { + // List available tools + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "itkdev_list_templates", + description: + "List all available ITK Dev Docker templates with their PHP versions and characteristics", + inputSchema: { + type: "object" as const, + properties: {}, + required: [], + }, + }, + { + name: "itkdev_detect_project", + description: + "Analyze a directory to detect ITK Dev project configuration, template type, PHP version, and framework", + inputSchema: { + type: "object" as const, + properties: { + path: { + type: "string", + description: + "Absolute path to the project directory to analyze", + }, + }, + required: ["path"], + }, + }, + { + name: "itkdev_get_template_files", + description: + "List all files that would be installed by a specific template", + inputSchema: { + type: "object" as const, + properties: { + template: { + type: "string", + description: + "Template name (e.g., drupal-11, symfony-6, drupal-module)", + }, + }, + required: ["template"], + }, + }, + { + name: "itkdev_compare_project", + description: + "Compare a project's Docker configuration against its template to find missing, outdated, or extra files", + inputSchema: { + type: "object" as const, + properties: { + path: { + type: "string", + description: "Absolute path to the project directory", + }, + template: { + type: "string", + description: + "Template to compare against (auto-detected from .env if not provided)", + }, + }, + required: ["path"], + }, + }, + { + name: "itkdev_get_template_content", + description: + "Get the content of a specific file from a template", + inputSchema: { + type: "object" as const, + properties: { + template: { + type: "string", + description: "Template name (e.g., drupal-11)", + }, + file: { + type: "string", + description: + "Relative file path within the template (e.g., docker-compose.yml)", + }, + }, + required: ["template", "file"], + }, + }, + ], + })); + + // Handle tool calls + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + switch (name) { + case "itkdev_list_templates": + return { content: [{ type: "text", text: listTemplates() }] }; + + case "itkdev_detect_project": + return { + content: [ + { + type: "text", + text: JSON.stringify( + detectProject(args?.path as string), + null, + 2 + ), + }, + ], + }; + + case "itkdev_get_template_files": + return { + content: [ + { + type: "text", + text: JSON.stringify( + getTemplateFiles(args?.template as string), + null, + 2 + ), + }, + ], + }; + + case "itkdev_compare_project": + return { + content: [ + { + type: "text", + text: JSON.stringify( + compareProject(args?.path as string, args?.template as string), + null, + 2 + ), + }, + ], + }; + + case "itkdev_get_template_content": + return { + content: [ + { + type: "text", + text: getTemplateContent( + args?.template as string, + args?.file as string + ), + }, + ], + }; + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + content: [{ type: "text", text: `Error: ${message}` }], + isError: true, + }; + } + }); +} + +/** + * List all available templates with details + */ +function listTemplates(): string { + if (!existsSync(TEMPLATES_DIR)) { + return "Templates directory not found"; + } + + const templates: Array<{ + name: string; + phpVersion: string | null; + webRoot: string | null; + hasMemcached: boolean; + hasDrush: boolean; + itkVersion: string | null; + }> = []; + + const entries = readdirSync(TEMPLATES_DIR, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const templateName = entry.name; + const composePath = join(TEMPLATES_DIR, templateName, "docker-compose.yml"); + + if (!existsSync(composePath)) continue; + + const content = readFileSync(composePath, "utf-8"); + + // Extract details from docker-compose.yml + const phpMatch = content.match(/itkdev\/php([\d.]+)-fpm/); + const webRootMatch = content.match(/NGINX_WEB_ROOT:\s*(\/app\S*)/); + const versionMatch = content.match(/# itk-version:\s*([\d.]+)/); + const hasMemcached = content.includes("memcached:"); + const hasDrush = content.includes("drush:"); + + templates.push({ + name: templateName, + phpVersion: phpMatch ? phpMatch[1] : null, + webRoot: webRootMatch ? webRootMatch[1] : null, + hasMemcached, + hasDrush, + itkVersion: versionMatch ? versionMatch[1] : null, + }); + } + + // Sort by name with version sorting + templates.sort((a, b) => { + const aName = a.name.replace(/(\d+)/g, (m) => m.padStart(10, "0")); + const bName = b.name.replace(/(\d+)/g, (m) => m.padStart(10, "0")); + return aName.localeCompare(bName); + }); + + // Format output + let output = "Available ITK Dev Docker Templates:\n\n"; + output += + "| Template | PHP | Web Root | Memcached | Drush | ITK Version |\n"; + output += + "|----------|-----|----------|-----------|-------|-------------|\n"; + + for (const t of templates) { + output += `| ${t.name} | ${t.phpVersion || "-"} | ${t.webRoot || "-"} | ${t.hasMemcached ? "Yes" : "No"} | ${t.hasDrush ? "Yes" : "No"} | ${t.itkVersion || "-"} |\n`; + } + + output += `\nTotal: ${templates.length} templates\n`; + output += `\nUse 'itkdev-docker-compose template:install ' to install a template.`; + + return output; +} + +/** + * Detect project configuration from a directory + */ +function detectProject(projectPath: string): object { + if (!projectPath) { + throw new Error("Project path is required"); + } + + if (!existsSync(projectPath)) { + throw new Error(`Path does not exist: ${projectPath}`); + } + + const result: Record = { + path: projectPath, + isItkDevProject: false, + hasDockerCompose: false, + hasEnv: false, + template: null, + itkVersion: null, + projectName: null, + domain: null, + framework: null, + phpVersion: null, + webRoot: null, + services: [] as string[], + }; + + // Check for docker-compose.yml + const composePath = join(projectPath, "docker-compose.yml"); + if (existsSync(composePath)) { + result.hasDockerCompose = true; + const content = readFileSync(composePath, "utf-8"); + + // Check if it's an ITK Dev compose file + const versionMatch = content.match(/# itk-version:\s*([\d.]+)/); + if (versionMatch) { + result.isItkDevProject = true; + result.itkVersion = versionMatch[1]; + } + + // Extract PHP version + const phpMatch = content.match(/itkdev\/php([\d.]+)-fpm/); + if (phpMatch) result.phpVersion = phpMatch[1]; + + // Extract web root + const webRootMatch = content.match(/NGINX_WEB_ROOT:\s*(\/app\S*)/); + if (webRootMatch) result.webRoot = webRootMatch[1]; + + // Detect services + const services: string[] = []; + const serviceMatches = content.matchAll(/^\s{2}(\w+):\s*$/gm); + for (const match of serviceMatches) { + if (match[1] !== "networks" && match[1] !== "volumes") { + services.push(match[1]); + } + } + result.services = services; + } + + // Check for .env + const envPath = join(projectPath, ".env"); + if (existsSync(envPath)) { + result.hasEnv = true; + const content = readFileSync(envPath, "utf-8"); + + const templateMatch = content.match(/ITKDEV_TEMPLATE=(\S+)/); + if (templateMatch) result.template = templateMatch[1]; + + const projectMatch = content.match(/COMPOSE_PROJECT_NAME=(\S+)/); + if (projectMatch) result.projectName = projectMatch[1]; + + const domainMatch = content.match(/COMPOSE_DOMAIN=(\S+)/); + if (domainMatch) result.domain = domainMatch[1]; + } + + // Detect framework + if (existsSync(join(projectPath, "web/core/lib/Drupal.php"))) { + result.framework = "drupal"; + // Try to detect Drupal version + const drupalPath = join(projectPath, "web/core/lib/Drupal.php"); + const drupalContent = readFileSync(drupalPath, "utf-8"); + const versionMatch = drupalContent.match(/VERSION\s*=\s*'([\d.]+)'/); + if (versionMatch) { + result.frameworkVersion = versionMatch[1]; + } + } else if (existsSync(join(projectPath, "core/lib/Drupal.php"))) { + // Drupal 7 style (no web/ prefix) + result.framework = "drupal"; + result.frameworkVersion = "7.x"; + } else if (existsSync(join(projectPath, "bin/console"))) { + result.framework = "symfony"; + // Try to detect Symfony version from composer.json + const composerPath = join(projectPath, "composer.json"); + if (existsSync(composerPath)) { + try { + const composer = JSON.parse(readFileSync(composerPath, "utf-8")); + const symfonyVersion = + composer.require?.["symfony/framework-bundle"] || + composer.require?.["symfony/symfony"]; + if (symfonyVersion) { + result.frameworkVersion = symfonyVersion; + } + } catch { + // Ignore JSON parse errors + } + } + } + + // Check for Taskfile + result.hasTaskfile = existsSync(join(projectPath, "Taskfile.yml")); + + // Check for GitHub Actions + result.hasGitHubActions = existsSync( + join(projectPath, ".github/workflows") + ); + + return result; +} + +/** + * Get list of files in a template + */ +function getTemplateFiles(template: string): object { + if (!template) { + throw new Error("Template name is required"); + } + + const templateDir = join(TEMPLATES_DIR, template); + + if (!existsSync(templateDir)) { + // List available templates in error message + const available = readdirSync(TEMPLATES_DIR, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .join(", "); + throw new Error( + `Template '${template}' not found. Available: ${available}` + ); + } + + const files: string[] = []; + + function walk(dir: string, prefix = ""): void { + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dir, entry.name); + const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; + + if (entry.isDirectory()) { + walk(fullPath, relativePath); + } else { + files.push(relativePath); + } + } + } + + walk(templateDir); + + return { + template, + path: templateDir, + files: files.sort(), + count: files.length, + }; +} + +/** + * Compare project against template + */ +function compareProject( + projectPath: string, + template?: string +): object { + if (!projectPath) { + throw new Error("Project path is required"); + } + + if (!existsSync(projectPath)) { + throw new Error(`Path does not exist: ${projectPath}`); + } + + // Auto-detect template if not provided + if (!template) { + const envPath = join(projectPath, ".env"); + if (existsSync(envPath)) { + const content = readFileSync(envPath, "utf-8"); + const match = content.match(/ITKDEV_TEMPLATE=(\S+)/); + if (match) template = match[1]; + } + } + + if (!template) { + throw new Error( + "Could not detect template. Please specify the template parameter." + ); + } + + const templateDir = join(TEMPLATES_DIR, template); + if (!existsSync(templateDir)) { + throw new Error(`Template '${template}' not found`); + } + + const result: Record = { + projectPath, + template, + missing: [] as string[], + outdated: [] as object[], + matching: [] as string[], + }; + + // Key files to compare + const keyFiles = [ + "docker-compose.yml", + "docker-compose.server.yml", + "docker-compose.dev.yml", + "docker-compose.redirect.yml", + ".docker/nginx.conf", + ".docker/templates/default.conf.template", + ]; + + for (const file of keyFiles) { + const templateFile = join(templateDir, file); + const projectFile = join(projectPath, file); + + if (!existsSync(templateFile)) continue; + + if (!existsSync(projectFile)) { + (result.missing as string[]).push(file); + continue; + } + + // Compare itk-version in compose files + if (file.endsWith(".yml")) { + const templateContent = readFileSync(templateFile, "utf-8"); + const projectContent = readFileSync(projectFile, "utf-8"); + + const templateVersion = templateContent.match( + /# itk-version:\s*([\d.]+)/ + ); + const projectVersion = projectContent.match( + /# itk-version:\s*([\d.]+)/ + ); + + if (templateVersion && projectVersion) { + if (templateVersion[1] !== projectVersion[1]) { + (result.outdated as object[]).push({ + file, + projectVersion: projectVersion[1], + templateVersion: templateVersion[1], + }); + } else { + (result.matching as string[]).push(file); + } + } else if (templateVersion && !projectVersion) { + (result.outdated as object[]).push({ + file, + projectVersion: "unknown", + templateVersion: templateVersion[1], + note: "Project file missing itk-version comment", + }); + } + } + } + + // Summary + result.summary = { + total: keyFiles.length, + missing: (result.missing as string[]).length, + outdated: (result.outdated as object[]).length, + matching: (result.matching as string[]).length, + upToDate: + (result.missing as string[]).length === 0 && + (result.outdated as object[]).length === 0, + }; + + if (!(result.summary as Record).upToDate) { + result.recommendation = + "Run 'itkdev-docker-compose template:update' to update template files"; + } + + return result; +} + +/** + * Get content of a specific template file + */ +function getTemplateContent(template: string, file: string): string { + if (!template) { + throw new Error("Template name is required"); + } + if (!file) { + throw new Error("File path is required"); + } + + const filePath = join(TEMPLATES_DIR, template, file); + + if (!existsSync(filePath)) { + throw new Error( + `File '${file}' not found in template '${template}'` + ); + } + + return readFileSync(filePath, "utf-8"); +} + +/** + * Main entry point + */ +async function main(): Promise { + const server = createServer(); + const transport = new StdioServerTransport(); + + await server.connect(transport); + + // Handle graceful shutdown + process.on("SIGINT", async () => { + await server.close(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + await server.close(); + process.exit(0); + }); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json new file mode 100644 index 0000000..f44baa9 --- /dev/null +++ b/mcp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 3cc4386a3574ffe579d9cc12b4efb2affd17f0a0 Mon Sep 17 00:00:00 2001 From: Jesper Pedersen Date: Mon, 22 Dec 2025 10:23:20 +0100 Subject: [PATCH 2/4] consolidated the configuration documentation --- README.md | 24 +-------------------- docs/rfc-mcp-server.md | 48 +----------------------------------------- mcp/README.md | 22 ++++++++++++++----- 3 files changed, 19 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 0bd83c8..6fdac32 100644 --- a/README.md +++ b/README.md @@ -168,29 +168,7 @@ This repository includes an MCP (Model Context Protocol) server that provides AI - **Project detection**: Analyze projects to detect template, PHP version, framework - **Template comparison**: Compare projects against templates to find outdated files -### Installation - -```bash -# Build the MCP server -cd mcp -npm install -npm run build -``` - -Add to `~/.claude/settings.json`: - -```json -{ - "mcpServers": { - "itkdev": { - "command": "node", - "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] - } - } -} -``` - -See [mcp/README.md](mcp/README.md) for detailed documentation and [docs/rfc-mcp-server.md](docs/rfc-mcp-server.md) for the design rationale. +See [mcp/README.md](mcp/README.md) for installation and configuration instructions, and [docs/rfc-mcp-server.md](docs/rfc-mcp-server.md) for the design rationale. ## Documentation diff --git a/docs/rfc-mcp-server.md b/docs/rfc-mcp-server.md index 84228a8..a7a0738 100644 --- a/docs/rfc-mcp-server.md +++ b/docs/rfc-mcp-server.md @@ -150,53 +150,7 @@ itkdev-docker/ ### User Configuration -First, build the MCP server: - -```bash -cd /path/to/itkdev-docker/mcp -npm install -npm run build -``` - -Then configure Claude Code using one of these methods: - -#### Option 1: User-wide configuration (all projects) - -Create or edit `~/.claude.json`: - -```json -{ - "mcpServers": { - "itkdev": { - "command": "node", - "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] - } - } -} -``` - -#### Option 2: Project-specific configuration - -Create `.mcp.json` in your project root: - -```json -{ - "mcpServers": { - "itkdev": { - "command": "node", - "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] - } - } -} -``` - -#### Option 3: CLI command - -```bash -claude mcp add itkdev --scope user -- node /path/to/itkdev-docker/mcp/dist/index.js -``` - -After configuration, restart Claude Code and run `/mcp` to verify the server is connected. +See [mcp/README.md](../mcp/README.md) for build and configuration instructions. ## Use Cases diff --git a/mcp/README.md b/mcp/README.md index f3d9a8e..7d8e203 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -26,34 +26,46 @@ npm run build ### Configure Claude Code -Add to `~/.claude/settings.json`: +Configure Claude Code using one of these methods: + +#### Option 1: User-wide configuration (all projects) + +Create or edit `~/.claude.json`: ```json { "mcpServers": { "itkdev": { "command": "node", - "args": ["/absolute/path/to/itkdev-docker/mcp/dist/index.js"] + "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] } } } ``` -Replace `/absolute/path/to/itkdev-docker` with the actual path to your itkdev-docker clone. +Replace `/path/to/itkdev-docker` with the actual path to your itkdev-docker clone. -**Example:** +#### Option 2: Project-specific configuration + +Create `.mcp.json` in your project root: ```json { "mcpServers": { "itkdev": { "command": "node", - "args": ["/Users/developer/itkdev-docker/mcp/dist/index.js"] + "args": ["/path/to/itkdev-docker/mcp/dist/index.js"] } } } ``` +#### Option 3: CLI command + +```bash +claude mcp add itkdev --scope user -- node /path/to/itkdev-docker/mcp/dist/index.js +``` + ### Verify Installation Restart Claude Code and check that the MCP server is connected. You can ask Claude: From eb3b4e94ab4dfa17b9a23ea3c93937032cc8560c Mon Sep 17 00:00:00 2001 From: Jesper Pedersen Date: Mon, 22 Dec 2025 10:28:47 +0100 Subject: [PATCH 3/4] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e66ed4..e270fec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [PR-125](https://github.com/itk-dev/devops_itkdev-docker/pull/125) + - Added MCP server for AI coding assistants (Claude Code integration) - [PR-121](https://github.com/itk-dev/devops_itkdev-docker/pull/121)) - Changed COMPOSE_USER to runner. - [PR-108](https://github.com/itk-dev/devops_itkdev-docker/pull/108) From 738bf6f5fd226dc3fc74a8e162330bc76699d9d4 Mon Sep 17 00:00:00 2001 From: Jesper Pedersen Date: Mon, 22 Dec 2025 10:29:42 +0100 Subject: [PATCH 4/4] Apply markdown coding styles --- README.md | 12 +++++++----- docs/itkdev-docker-cli.md | 6 +++--- docs/itkdev-docker-compose.md | 11 ++++++++--- docs/itkdev-task-files.md | 4 +++- docs/rfc-mcp-server.md | 20 ++++++++++++++++---- mcp/README.md | 15 ++++++++++++--- 6 files changed, 49 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6fdac32..9f2c16f 100644 --- a/README.md +++ b/README.md @@ -160,15 +160,17 @@ The fuld list can be found at ## MCP Server for AI Assistants -This repository includes an MCP (Model Context Protocol) server that provides AI coding assistants like Claude Code with access to ITK Dev documentation and project analysis tools. +This repository includes an MCP (Model Context Protocol) server that provides AI coding +assistants like Claude Code with access to ITK Dev documentation and project analysis tools. ### What it provides -- **Documentation access**: AI assistants can read ITK Dev Docker documentation -- **Project detection**: Analyze projects to detect template, PHP version, framework -- **Template comparison**: Compare projects against templates to find outdated files +- __Documentation access__: AI assistants can read ITK Dev Docker documentation +- __Project detection__: Analyze projects to detect template, PHP version, framework +- __Template comparison__: Compare projects against templates to find outdated files -See [mcp/README.md](mcp/README.md) for installation and configuration instructions, and [docs/rfc-mcp-server.md](docs/rfc-mcp-server.md) for the design rationale. +See [mcp/README.md](mcp/README.md) for installation and configuration instructions, +and [docs/rfc-mcp-server.md](docs/rfc-mcp-server.md) for the design rationale. ## Documentation diff --git a/docs/itkdev-docker-cli.md b/docs/itkdev-docker-cli.md index f448c22..2cf11c3 100644 --- a/docs/itkdev-docker-cli.md +++ b/docs/itkdev-docker-cli.md @@ -342,7 +342,7 @@ itkdev-docker-compose php -v itkdev-docker-compose php script.php ``` -#### bin/* and vendor/bin/* +#### bin/*and vendor/bin/* Run any binary from the project: @@ -501,7 +501,7 @@ Minimal template for module development: When you run `template:install`, these files are created: -``` +```text project/ ├── .env # Created interactively (if not exists) ├── docker-compose.yml # Base service configuration @@ -531,7 +531,7 @@ project/ ### Architecture -``` +```text ┌─────────────────────────────────────────────────────────────┐ │ Host Machine │ │ ┌─────────────────────────────────────────────────────────┐│ diff --git a/docs/itkdev-docker-compose.md b/docs/itkdev-docker-compose.md index fb1bc98..06db1c6 100644 --- a/docs/itkdev-docker-compose.md +++ b/docs/itkdev-docker-compose.md @@ -1,6 +1,7 @@ # ITK Dev Docker Compose Setup -This document describes the Docker Compose patterns used in ITK Dev projects. It covers both local development and server deployment configurations. +This document describes the Docker Compose patterns used in ITK Dev projects. +It covers both local development and server deployment configurations. ## Overview @@ -321,7 +322,8 @@ services: Certificate setup for OIDC mock requires a `.docker/oidc-server-mock/cert/docker.pfx` file. -For server OIDC (`docker-compose.server.oidc.yml`), use `COMPOSE_SERVER_DOMAIN` instead of `COMPOSE_DOMAIN` in the container name and labels. +For server OIDC (`docker-compose.server.oidc.yml`), use `COMPOSE_SERVER_DOMAIN` instead of +`COMPOSE_DOMAIN` in the container name and labels. ## Server Configuration (docker-compose.server.yml) @@ -430,6 +432,7 @@ services: ``` Key features: + - Basic Auth middleware (`ITKBasicAuth@file`) for protected staging environments - Mailpit with authentication middleware for mail capture on server @@ -473,6 +476,7 @@ services: ``` Key settings: + - `PHP_OPCACHE_VALIDATE_TIMESTAMPS=0` - Disables OPcache timestamp validation for performance - Customized upload size limits for the specific environment @@ -494,6 +498,7 @@ services: ``` This pattern allows: + - Persistent settings across deployments - Shared file storage between releases - Private files directory for sensitive uploads @@ -653,7 +658,7 @@ Internal bridge network for service communication. Created automatically. ## Directory Structure -``` +```text .docker/ ├── templates/ │ └── default.conf.template # Nginx vhost template diff --git a/docs/itkdev-task-files.md b/docs/itkdev-task-files.md index 555add1..6374476 100644 --- a/docs/itkdev-task-files.md +++ b/docs/itkdev-task-files.md @@ -1,6 +1,8 @@ # ITK Dev Taskfile Patterns -This document describes the Taskfile patterns used in ITK Dev projects. Tasks are managed using [Task](https://taskfile.dev/) (go-task), a task runner similar to Make but with YAML configuration. +This document describes the Taskfile patterns used in ITK Dev projects. +Tasks are managed using [Task](https://taskfile.dev/) (go-task), +a task runner similar to Make but with YAML configuration. ## Overview diff --git a/docs/rfc-mcp-server.md b/docs/rfc-mcp-server.md index a7a0738..ce6798b 100644 --- a/docs/rfc-mcp-server.md +++ b/docs/rfc-mcp-server.md @@ -7,7 +7,9 @@ ## Summary -This RFC proposes adding a Model Context Protocol (MCP) server to the itkdev-docker repository. The MCP server will provide AI coding assistants (like Claude Code) with structured access to ITK Dev documentation, project detection capabilities, and template management tools. +This RFC proposes adding a Model Context Protocol (MCP) server to the itkdev-docker repository. +The MCP server will provide AI coding assistants (like Claude Code) with structured access to +ITK Dev documentation, project detection capabilities, and template management tools. ## Motivation @@ -22,6 +24,7 @@ When developers use AI coding assistants on ITK Dev projects, the assistants lac 5. How to detect and compare project configurations against templates This leads to: + - Inconsistent AI-generated configurations - Manual explanation of ITK Dev patterns in every session - Inability to leverage AI for project setup/maintenance tasks @@ -37,7 +40,8 @@ Create an MCP server that provides: ### What is MCP? -Model Context Protocol (MCP) is an open protocol developed by Anthropic that enables AI applications to connect with external data sources and tools. It provides a standardized way to: +Model Context Protocol (MCP) is an open protocol developed by Anthropic that enables AI +applications to connect with external data sources and tools. It provides a standardized way to: - Expose resources (documents, data) to AI models - Provide tools that AI can invoke @@ -62,7 +66,8 @@ We chose the **local stdio** approach because: 4. **Integration**: Natural fit with existing itkdev-docker repository 5. **Offline support**: Works without network access -The MCP server will be added to the existing itkdev-docker repository, allowing developers to configure it once after cloning/updating the repo. +The MCP server will be added to the existing itkdev-docker repository, allowing developers to +configure it once after cloning/updating the repo. ## Documentation Created @@ -109,7 +114,7 @@ Documents Taskfile automation: ### Repository Structure -``` +```text itkdev-docker/ ├── mcp/ # NEW: MCP server │ ├── package.json @@ -159,6 +164,7 @@ See [mcp/README.md](../mcp/README.md) for build and configuration instructions. Developer: "Set up a new Drupal 11 project called 'citizen-portal'" AI assistant can: + 1. Read `itkdev://docs/cli` for setup instructions 2. Use `itkdev_list_templates` to confirm template exists 3. Provide exact commands based on documentation @@ -168,6 +174,7 @@ AI assistant can: Developer: "Is this project using the latest template version?" AI assistant can: + 1. Use `itkdev_detect_project` to analyze current configuration 2. Use `itkdev_compare_project` to find differences 3. Suggest specific updates needed @@ -177,6 +184,7 @@ AI assistant can: Developer: "Docker containers won't start" AI assistant can: + 1. Read documentation for troubleshooting steps 2. Use `itkdev_detect_project` to understand configuration 3. Provide targeted solutions @@ -186,6 +194,7 @@ AI assistant can: Developer: "Upgrade this project from drupal-10 to drupal-11" AI assistant can: + 1. Use `itkdev_compare_project` with both templates 2. Identify specific changes needed 3. Guide through migration steps @@ -217,6 +226,7 @@ AI assistant can: Add CLAUDE.md to each project template pointing to documentation. **Rejected because:** + - No tool capabilities - Requires path assumptions - Less discoverable @@ -226,6 +236,7 @@ Add CLAUDE.md to each project template pointing to documentation. Run MCP server as shared team service. **Rejected because:** + - Infrastructure overhead - Single point of failure - Overkill for current needs @@ -235,6 +246,7 @@ Run MCP server as shared team service. Create standalone mcp-itkdev repository. **Rejected because:** + - Fragmented maintenance - Documentation sync issues - Additional repo to manage diff --git a/mcp/README.md b/mcp/README.md index 7d8e203..291bc91 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -1,10 +1,12 @@ # ITK Dev Docker MCP Server -Model Context Protocol (MCP) server that provides AI coding assistants with access to ITK Dev Docker documentation and project analysis tools. +Model Context Protocol (MCP) server that provides AI coding assistants with access to +ITK Dev Docker documentation and project analysis tools. ## What is MCP? -MCP (Model Context Protocol) is an open protocol that enables AI assistants like Claude to access external resources and tools. This server provides: +MCP (Model Context Protocol) is an open protocol that enables AI assistants like Claude to +access external resources and tools. This server provides: - **Documentation Resources**: Access to ITK Dev Docker documentation - **Project Tools**: Analyze, detect, and compare ITK Dev projects @@ -96,12 +98,14 @@ List all available ITK Dev Docker templates with their characteristics. Analyze a directory to detect ITK Dev project configuration. **Parameters:** + - `path` (required): Absolute path to the project directory **Example prompt:** > "Analyze the project at /Users/me/projects/mysite" **Returns:** + - Project type (Drupal/Symfony) - Template in use - PHP version @@ -114,6 +118,7 @@ Analyze a directory to detect ITK Dev project configuration. List all files that would be installed by a template. **Parameters:** + - `template` (required): Template name (e.g., `drupal-11`) **Example prompt:** @@ -124,6 +129,7 @@ List all files that would be installed by a template. Compare a project against its template to find differences. **Parameters:** + - `path` (required): Absolute path to the project - `template` (optional): Template to compare against (auto-detected from .env) @@ -131,6 +137,7 @@ Compare a project against its template to find differences. > "Is my project at /Users/me/projects/mysite up to date with its template?" **Returns:** + - Missing files - Outdated files (with version comparison) - Matching files @@ -141,6 +148,7 @@ Compare a project against its template to find differences. Get the content of a specific file from a template. **Parameters:** + - `template` (required): Template name - `file` (required): Relative file path @@ -185,7 +193,7 @@ Rebuilds on file changes. ### Project Structure -``` +```text mcp/ ├── src/ │ └── index.ts # MCP server implementation @@ -213,6 +221,7 @@ mcp/ ### Documentation Not Found Ensure these files exist in `itkdev-docker/docs/`: + - `docs/itkdev-docker-cli.md` - `docs/itkdev-docker-compose.md` - `docs/itkdev-task-files.md`