|
| 1 | +# Local Development MCP |
| 2 | + |
| 3 | +An MCP server that gives AI agents tools to run commands in DDEV containers via SSH. Tools are defined in YAML configuration files. |
| 4 | + |
| 5 | +## Architecture |
| 6 | + |
| 7 | +- **Execution Method**: SSH (passwordless key-based authentication) |
| 8 | +- **SSH Keys**: Ephemeral Ed25519 keys, generated fresh each `ddev start` and distributed via `docker exec` |
| 9 | +- **SSH User**: Automatically detected from web container's `/var/www/html` ownership, configured via SSH client `User` directive |
| 10 | +- **No Docker Socket**: Fully isolated from host Docker daemon |
| 11 | +- **No Persistent State**: No `.runtime.env` or stored keys — everything is set up by the `set-up` hook |
| 12 | + |
| 13 | +## How to Use |
| 14 | + |
| 15 | +1. **Installation:** |
| 16 | + ```bash |
| 17 | + cd /path/to/ddev-project |
| 18 | + ddev get wunderio/ddev-agents |
| 19 | + ddev restart # Builds container and sets up SSH automatically |
| 20 | + ``` |
| 21 | + |
| 22 | +2. **First Launch:** |
| 23 | + - Open VS Code in the devcontainer |
| 24 | + - The wdrmcp MCP server config is at `.vscode/mcp.json` |
| 25 | + - Note: Due to VS Code bug, search `@mcp` in extension gallery to enable the MCP registry |
| 26 | + - Open Command Palette: `MCP Servers: List`, find wdrmcp and click Start |
| 27 | + |
| 28 | +3. **Using Tools:** |
| 29 | + - Open VS Code Copilot chat |
| 30 | + - Use tools like `drush`, `composer_install`, `logs_nginx_access`, etc. |
| 31 | + - All tools connect via SSH automatically |
| 32 | + |
| 33 | +## How SSH Works |
| 34 | + |
| 35 | +SSH keys are **ephemeral** — generated fresh on every `ddev start`: |
| 36 | + |
| 37 | +1. The `set-up` hook (runs post-start on host) generates an Ed25519 keypair |
| 38 | +2. Public key is placed in the web container via `docker exec` |
| 39 | +3. Private key + SSH client config are placed in the agents container via `docker exec` |
| 40 | +4. The SSH client config includes a `User` directive with the detected DDEV user |
| 41 | +5. Keys exist only in container memory — never written to disk on the host |
| 42 | + |
| 43 | +Tool configs use `ssh_target: "web"` — the SSH client config handles which user to connect as. |
| 44 | + |
| 45 | +## Files |
| 46 | + |
| 47 | +- `tools-config/` – YAML tool definitions |
| 48 | +- `mcp.json` – MCP server configuration (copied to `.vscode/mcp.json` by set-up hook) |
| 49 | + |
| 50 | +## Adding a Tool |
| 51 | + |
| 52 | +Create a file in `tools-config/my_tool.yml`: |
| 53 | + |
| 54 | +```yaml |
| 55 | +tools: |
| 56 | + - name: my_tool |
| 57 | + type: command |
| 58 | + enabled: true |
| 59 | + description: "What this tool does" |
| 60 | + command_template: "my_command {command} {args}" |
| 61 | + ssh_target: "web" |
| 62 | + working_dir: "/var/www/html" |
| 63 | + default_args: |
| 64 | + args: [] |
| 65 | + input_schema: |
| 66 | + type: object |
| 67 | + properties: |
| 68 | + command: |
| 69 | + type: string |
| 70 | + description: "Command to run" |
| 71 | + args: |
| 72 | + type: array |
| 73 | + items: |
| 74 | + type: string |
| 75 | + description: "Flags and options, each as a separate array element" |
| 76 | + required: |
| 77 | + - command |
| 78 | +``` |
| 79 | +
|
| 80 | +### Parameter types |
| 81 | +
|
| 82 | +- **`string`** — for single values (commands, package names, file paths). |
| 83 | +- **`array`** with `items: { type: string }` — for flags/options. Each element is individually shell-escaped, so multi-flag use is safe: `["--format=json", "--fields=name,status"]`. |
| 84 | +- **`integer`** — for numeric values (line counts, limits). |
| 85 | + |
| 86 | +String parameters are shell-escaped as a single token. Array parameters have each element escaped individually and joined with spaces. Empty strings and empty arrays produce no output in the rendered command. |
| 87 | + |
| 88 | +## Project `.env` Credentials |
| 89 | + |
| 90 | +If any MCP tool config needs credentials (e.g., Drupal MCP proxy), define them in `.env` inside the `tools-config` directory. |
| 91 | + |
| 92 | +Example credentials file (`.agents/tools-config/.env`): |
| 93 | + |
| 94 | +```dotenv |
| 95 | +DRUPAL_MCP_USER=admin |
| 96 | +DRUPAL_MCP_PASS=admin |
| 97 | +``` |
| 98 | + |
| 99 | +This file is preserved across addon reinstalls (non-destructive merge). |
| 100 | + |
| 101 | +## Available Tool Types |
| 102 | + |
| 103 | +- `command` – Run shell commands with parameter substitution |
| 104 | +- `mcp_server` – Proxy to additional MCP servers |
| 105 | + |
| 106 | +### Command Tool Type |
| 107 | + |
| 108 | +Command tools execute shell commands in DDEV containers with parameter substitution. |
| 109 | + |
| 110 | +Example: |
| 111 | + |
| 112 | +```yaml |
| 113 | +tools: |
| 114 | + - name: my_command_tool |
| 115 | + type: command |
| 116 | + enabled: true |
| 117 | + description: "Execute a custom command" |
| 118 | + command_template: "my_command {name} {args}" |
| 119 | + ssh_target: "web" |
| 120 | + working_dir: "/var/www/html" |
| 121 | + default_args: |
| 122 | + args: [] |
| 123 | +
|
| 124 | + input_schema: |
| 125 | + type: object |
| 126 | + properties: |
| 127 | + name: |
| 128 | + type: string |
| 129 | + description: "Name argument" |
| 130 | + args: |
| 131 | + type: array |
| 132 | + items: |
| 133 | + type: string |
| 134 | + description: "Flags and options as separate array elements" |
| 135 | + required: |
| 136 | + - name |
| 137 | +``` |
| 138 | + |
| 139 | +### MCP Server Tool Type |
| 140 | + |
| 141 | +MCP Server tools proxy requests to other MCP servers via HTTP. Both plain JSON-RPC HTTP endpoints and Streamable HTTP MCP endpoints are supported. |
| 142 | + |
| 143 | +For Streamable HTTP MCP endpoints, the proxy uses the MCP SDK Client which handles the protocol handshake, session management, and reconnection automatically. If the remote server does not support the MCP protocol, the proxy falls back to plain JSON-RPC. |
| 144 | + |
| 145 | +**Dynamic Tool Discovery:** When `expose_remote_tools: true` is set, the proxy will query the remote MCP server for all its available tools and expose them as if they were local tools. This allows seamless integration with external MCP servers. |
| 146 | + |
| 147 | +Example (single proxy tool): |
| 148 | + |
| 149 | +```yaml |
| 150 | +tools: |
| 151 | + - name: my_mcp_tool |
| 152 | + type: mcp_server |
| 153 | + enabled: true |
| 154 | + description: "Proxy to another MCP server" |
| 155 | + server_url: "http://localhost:8080/endpoint" |
| 156 | + forward_args: true # Optional: forward arguments (default: true) |
| 157 | + timeout: 30 # Optional: timeout in seconds (default: 30) |
| 158 | + auth_username: "${MCP_USER}" # Optional: basic auth username |
| 159 | + auth_password: "${MCP_PASS}" # Optional: basic auth password |
| 160 | + input_schema: |
| 161 | + type: object |
| 162 | + properties: |
| 163 | + query: |
| 164 | + type: string |
| 165 | + required: |
| 166 | + - query |
| 167 | +``` |
| 168 | + |
| 169 | +Example (dynamic tool exposure): |
| 170 | + |
| 171 | +```yaml |
| 172 | +tools: |
| 173 | + - name: drupal_mcp |
| 174 | + type: mcp_server |
| 175 | + enabled: true |
| 176 | + description: "Proxy to Drupal MCP server" |
| 177 | + server_url: "https://drupal-project.ddev.site/mcp/post" |
| 178 | + auth_username: "${DRUPAL_MCP_USER}" |
| 179 | + auth_password: "${DRUPAL_MCP_PASS}" |
| 180 | + expose_remote_tools: true # Dynamically fetch and expose remote tools |
| 181 | + tool_prefix: "drupal_" # Optional: prefix remote tool names |
| 182 | + timeout: 30 |
| 183 | +``` |
| 184 | + |
| 185 | +## Logging |
| 186 | + |
| 187 | +View MCP server logs: |
| 188 | + |
| 189 | +```bash |
| 190 | +tail -f /tmp/wdrmcp.log |
| 191 | +``` |
0 commit comments