Skip to content

Commit c81fb0f

Browse files
authored
Merge pull request #308 from wunderio/feature/WNDR-385
WNDR-385: Lando cleanup and add ddev-agents
2 parents 89ee85c + be52945 commit c81fb0f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1547
-740
lines changed

.agents/README.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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+
```

.agents/tools-config/composer.yml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Composer PHP Dependency Manager Configuration
2+
# Defines available composer commands
3+
4+
tools:
5+
- name: composer_install
6+
enabled: true
7+
description: |
8+
Install dependencies from composer.lock file.
9+
10+
Always run in the Drupal project directory (/var/www/html).
11+
Installs packages with versions locked in composer.lock.
12+
13+
type: command
14+
command_template: "composer install {args}"
15+
ssh_target: "web"
16+
ssh_user: "${DDEV_SSH_USER}"
17+
working_dir: "/var/www/html"
18+
default_args:
19+
args: ["--no-interaction"]
20+
21+
input_schema:
22+
type: object
23+
properties:
24+
args:
25+
type: array
26+
items:
27+
type: string
28+
description: >
29+
Each flag or option as a separate array element.
30+
The default ["--no-interaction"] is always included unless overridden.
31+
Examples: ["--no-dev"], ["--no-dev", "--optimize-autoloader"].
32+
33+
- name: composer_update
34+
enabled: true
35+
description: |
36+
Update dependencies to latest versions.
37+
38+
Always run in the Drupal project directory (/var/www/html).
39+
Updates packages according to version constraints in composer.json.
40+
41+
type: command
42+
command_template: "composer update {package} {args}"
43+
ssh_target: "web"
44+
ssh_user: "${DDEV_SSH_USER}"
45+
working_dir: "/var/www/html"
46+
default_args:
47+
package: ""
48+
args: ["--no-interaction"]
49+
50+
input_schema:
51+
type: object
52+
properties:
53+
package:
54+
type: string
55+
description: >
56+
Package name to update (e.g., "drupal/pathauto").
57+
Omit or leave empty to update all packages.
58+
args:
59+
type: array
60+
items:
61+
type: string
62+
description: >
63+
Each flag or option as a separate array element.
64+
Examples: ["--with-dependencies"], ["--no-dev", "--prefer-dist"].
65+
66+
- name: composer_require
67+
enabled: true
68+
description: |
69+
Add one or more dependencies to composer.json.
70+
71+
Always run in the Drupal project directory (/var/www/html).
72+
Adds packages and updates composer.lock.
73+
74+
Supports multiple packages in a single call — prefer this over separate
75+
calls when installing related packages together:
76+
{"packages": ["drupal/pathauto", "drupal/token:^1.0"]}
77+
{"packages": ["drupal/mcp_server", "drupal/tool"], "args": ["--ignore-platform-req=ext-gd"]}
78+
79+
Single package still works:
80+
{"packages": ["drupal/pathauto"]}
81+
82+
Troubleshooting installation failures:
83+
84+
1. **PHP Version Incompatibility** - Package may require different PHP version
85+
- Check PHP version: Use drush_php_eval "echo PHP_VERSION;"
86+
- Check composer.json: Look for "platform" sections with PHP constraints
87+
- Try: args: ["--ignore-platform-req=php-ext-*"]
88+
89+
2. **Stability Constraints** - Package below minimum-stability threshold
90+
- Try: packages: ["drupal/package:@dev"] (or @beta, @alpha)
91+
- Or update composer.json "minimum-stability" to "dev"
92+
93+
3. **Memory Errors** - Large installation can exceed PHP memory limit
94+
- Try: Use COMPOSER_MEMORY_LIMIT=-1 environment variable
95+
96+
4. **Dependency Conflicts** - New package conflicts with existing ones
97+
- Try: args: ["--with-all-dependencies"]
98+
- This updates related packages to compatible versions
99+
100+
type: command
101+
command_template: "composer require {packages} {args}"
102+
ssh_target: "web"
103+
ssh_user: "${DDEV_SSH_USER}"
104+
working_dir: "/var/www/html"
105+
default_args:
106+
args: ["--no-interaction"]
107+
108+
input_schema:
109+
type: object
110+
properties:
111+
packages:
112+
type: array
113+
items:
114+
type: string
115+
description: >
116+
One or more package names to require.
117+
Examples: ["drupal/pathauto"], ["drupal/mcp_server", "drupal/tool:^1.0"].
118+
Each element is shell-escaped and joined with spaces in the command.
119+
args:
120+
type: array
121+
items:
122+
type: string
123+
description: >
124+
Each flag or option as a separate array element.
125+
Examples: ["--with-all-dependencies"], ["--no-dev", "--prefer-dist"].
126+
required:
127+
- packages

.agents/tools-config/drupal.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Drupal MCP Server Proxy Configuration
2+
# Proxies to a Drupal-specific MCP server
3+
#
4+
# == SETUP INSTRUCTIONS (run once per project) ==
5+
#
6+
# 1. Install and enable the module:
7+
# composer_require: drupal/mcp
8+
# drush pm:enable mcp -y
9+
# drush_cr
10+
#
11+
# 2. Enable basic auth:
12+
# drush config:set mcp.settings enable_auth true -y
13+
# drush config:set mcp.settings auth_settings.enable_basic_auth true -y
14+
#
15+
# 3. Verify — expect a JSON-RPC response containing "protocolVersion":
16+
# drush_php_eval:
17+
# $req = \Symfony\Component\HttpFoundation\Request::create('/mcp/post','POST',[],[],[],
18+
# ['HTTP_AUTHORIZATION'=>'Basic '.base64_encode('admin:admin'),'CONTENT_TYPE'=>'application/json'],
19+
# json_encode(['jsonrpc'=>'2.0','method'=>'initialize','params'=>['protocolVersion'=>'2024-11-05','capabilities'=>[],'clientInfo'=>['name'=>'test','version'=>'1.0']],'id'=>1]));
20+
# echo \Drupal::classResolver(\Drupal\mcp\Controller\McpController::class)->post($req)->getContent();
21+
#
22+
# 4. Ask the user for the Drupal admin username and password, then create
23+
# .agents/tools-config/.env:
24+
# DRUPAL_MCP_USER=<username>
25+
# DRUPAL_MCP_PASS=<password>
26+
#
27+
# NOTE: Use http://web/mcp/post (internal container URL — bypasses Varnish).
28+
# The tool config below already references this URL and the env vars above.
29+
30+
tools:
31+
- name: drupal
32+
enabled: true
33+
type: mcp_server
34+
description: |
35+
Drupal MCP server proxy for accessing tools provided by the Drupal site.
36+
37+
This proxies requests to the Drupal site's /mcp/post endpoint to discover
38+
and call available tools. Discovered tools are prefixed with "drupal_"
39+
(e.g., drupal_general_info).
40+
41+
Usage:
42+
- Use discovered tools directly (e.g., drupal_general_info, drupal_entity_info)
43+
- Each discovered tool is automatically prefixed
44+
45+
# Drupal MCP server endpoint - uses internal DDEV network address
46+
server_url: "http://web/mcp/post"
47+
48+
# Basic authentication (if required by Drupal module)
49+
auth_username: "${DRUPAL_MCP_USER}"
50+
auth_password: "${DRUPAL_MCP_PASS}"
51+
52+
# Dynamically expose all tools from the remote MCP server
53+
# Tools are automatically prefixed with the tool name: drupal_<tool_name>
54+
expose_remote_tools: true
55+
56+
# Initialization timeout for fetching remote tools (seconds)
57+
# Drupal MCP server can be slow, so allow enough time during startup
58+
init_timeout: 60
59+
60+
# Request timeout in seconds (for runtime tool execution)
61+
timeout: 60
62+
63+
# Input schema for JSON-RPC method calls
64+
input_schema:
65+
type: object
66+
properties:
67+
method:
68+
type: string
69+
description: "JSON-RPC method name (e.g., 'tools/list', 'tools/call')"
70+
params:
71+
type: object
72+
description: "Method parameters (varies by method)"
73+
required:
74+
- method

0 commit comments

Comments
 (0)