Development Guide
This document covers development setup, project structure, and contribution guidelines for Virtual Media Folders.
Requirements
Section titled “Requirements”- Node.js 18+
- PHP 8.3+
- Composer
- WordPress 6.8+
# Clone the repositorygit clone https://github.com/soderlind/virtual-media-folders.gitcd virtual-media-folders
# Install dependenciescomposer installnpm install
# Start development build with watchnpm run start
# Build for productionnpm run buildTesting
Section titled “Testing”# Run PHP tests (PHPUnit)composer test# orvendor/bin/phpunit
# Run JavaScript tests (Vitest)npm test
# Run JS tests in watch modenpm test -- --watchProject Structure
Section titled “Project Structure”virtual-media-folders/├── build/ # Compiled assets (generated)├── docs/ # Documentation│ ├── a11y.md # Accessibility documentation│ ├── design.md # Design decisions│ └── development.md # This file├── languages/ # Translation files├── src/│ ├── Admin.php # Media Library integration│ ├── Editor.php # Gutenberg integration│ ├── RestApi.php # REST API endpoints│ ├── Settings.php # Settings page│ ├── Suggestions.php # Smart folder suggestions│ ├── Taxonomy.php # Custom taxonomy registration│ ├── admin/ # Media Library UI (React)│ │ ├── index.js # Entry point│ │ ├── media-library.js│ │ ├── settings.js # Settings page JS│ │ ├── components/ # React components│ │ └── styles/ # CSS files│ ├── editor/ # Gutenberg integration (React)│ │ ├── index.js│ │ ├── components/│ │ └── styles/│ └── shared/ # Shared components & hooks│ ├── components/ # BaseFolderTree, LiveRegion, etc.│ ├── hooks/ # useFolderData, useMoveMode, etc.│ └── utils/ # folderApi.js├── tests/│ ├── js/ # JavaScript tests (Vitest)│ └── php/ # PHP tests (PHPUnit)├── uninstall.php # Cleanup on uninstall└── virtual-media-folders.php # Main plugin fileBuild System
Section titled “Build System”The plugin uses @wordpress/scripts for building:
- Entry points:
admin,editor,settings(configured inwebpack.config.js) - Output:
build/directory with minified JS/CSS and asset manifests
REST API
Section titled “REST API”The plugin provides REST API endpoints under /wp-json/vmfo/v1:
Endpoints
Section titled “Endpoints”| Method | Endpoint | Description |
|---|---|---|
| GET | /folders | List all folders |
| POST | /folders | Create a folder |
| GET | /folders/{id} | Get a folder |
| PUT | /folders/{id} | Update a folder |
| DELETE | /folders/{id} | Delete a folder |
| POST | /folders/{id}/media | Add media to folder |
| DELETE | /folders/{id}/media | Remove media from folder |
| POST | /folders/reorder | Reorder folders |
| GET | /folders/counts | Get folder counts (with optional media_type filter) |
Authentication
Section titled “Authentication”Use Application Passwords (WordPress 5.6+). Generate one at Users > Profile > Application Passwords.
username: your WordPress usernamexxxx xxxx xxxx xxxx xxxx xxxx: the generated application password
Examples
Section titled “Examples”# Create a new foldercurl -X POST "https://example.com/wp-json/vmfo/v1/folders" \ -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \ -H "Content-Type: application/json" \ -d '{"name": "Photos", "parent": 0}'
# Response: {"id": 5, "name": "Photos", "slug": "photos", "parent": 0, "count": 0}
# Add media (ID 123) to the folder (ID 5)curl -X POST "https://example.com/wp-json/vmfo/v1/folders/5/media" \ -u "username:xxxx xxxx xxxx xxxx xxxx xxxx" \ -H "Content-Type: application/json" \ -d '{"media_id": 123}'
# List all folderscurl "https://example.com/wp-json/vmfo/v1/folders" \ -u "username:xxxx xxxx xxxx xxxx xxxx xxxx"AI Abilities API
Section titled “AI Abilities API”Virtual Media Folders exposes three Abilities API tools for AI/MCP integrations:
vmfo/list-folders(read-only)vmfo/create-folder(write)vmfo/add-to-folder(write)
Recommended flow:
- Call
vmfo/list-foldersto resolve a folder name/path to a stableid. - If no match exists, call
vmfo/create-folder. - Call
vmfo/add-to-folderwith thatfolder_idand one or moreattachment_ids.
Request/Response Examples
Section titled “Request/Response Examples”vmfo/list-folders request input:
{ "search": "travel", "hide_empty": false}vmfo/list-folders response output:
{ "folders": [ { "id": 2285, "name": "Aerial Views", "parent_id": 2284, "path": "Travel / Aerial Views", "count": 2 }, { "id": 2321, "name": "Travel Portraits", "parent_id": 2284, "path": "Travel / Travel Portraits", "count": 6 } ], "total": 2}vmfo/add-to-folder request input:
{ "folder_id": 2285, "attachment_ids": [ 101, 205, 309 ]}vmfo/add-to-folder response output:
{ "success": true, "folder_id": 2285, "attachment_ids": [ 101, 205, 309 ], "processed_count": 3, "message": "Processed 3 media items.", "results": [ { "success": true, "media_id": 101, "folder_id": 2285, "message": "Media added to folder." }, { "success": true, "media_id": 205, "folder_id": 2285, "message": "Media added to folder." }, { "success": true, "media_id": 309, "folder_id": 2285, "message": "Media added to folder." } ]}vmfo/list-foldersandvmfo/add-to-folderrequire theupload_filescapability.vmfo/create-folderrequires themanage_categoriescapability.
For full end-to-end examples (including image upload and editor client setup), see mcp.md.
WordPress MCP Adapter Examples
Section titled “WordPress MCP Adapter Examples”When using the default server from wordpress/mcp-adapter, the MCP HTTP endpoint is:
/wp-json/mcp/mcp-adapter-default-server
List available tools:
curl -X POST "https://example.com/wp-json/mcp/mcp-adapter-default-server" \ -u "username:application-password" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }'Call vmfo/list-folders via the gateway tool (mcp-adapter-execute-ability):
curl -X POST "https://example.com/wp-json/mcp/mcp-adapter-default-server" \ -u "username:application-password" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "mcp-adapter-execute-ability", "arguments": { "ability_name": "vmfo/list-folders", "parameters": { "search": "travel", "hide_empty": false } } } }'Call vmfo/add-to-folder via the gateway tool (mcp-adapter-execute-ability):
curl -X POST "https://example.com/wp-json/mcp/mcp-adapter-default-server" \ -u "username:application-password" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "mcp-adapter-execute-ability", "arguments": { "ability_name": "vmfo/add-to-folder", "parameters": { "folder_id": 2285, "attachment_ids": [101, 205, 309] } } } }'Call vmfo/create-folder via the gateway tool (mcp-adapter-execute-ability):
curl -X POST "https://example.com/wp-json/mcp/mcp-adapter-default-server" \ -u "username:application-password" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "mcp-adapter-execute-ability", "arguments": { "ability_name": "vmfo/create-folder", "parameters": { "name": "Travel", "parent_id": 0 } } } }'In practice, the AI flow is:
tools/call->mcp-adapter-execute-abilitywithability_name = vmfo/list-folderstools/call->mcp-adapter-execute-abilitywithability_name = vmfo/add-to-folder
MCP Adapter Smoke Test
Section titled “MCP Adapter Smoke Test”Use the bundled smoke test to verify MCP adapter gateway behavior end-to-end.
From the plugin root:
MCP_BASE_URL="https://example.com/wp-json/mcp/mcp-adapter-default-server" \MCP_USER="per" \MCP_APP_PASS="xxxx xxxx xxxx xxxx xxxx xxxx" \./scripts/mcp-adapter-smoke-test.shWhat it checks:
initializereturns HTTP 200 andMcp-Session-Idtools/listincludesmcp-adapter-execute-abilityvmfo/list-foldersexecutes via gateway and returns folder datavmfo/add-to-folderexecutes via gateway in safe negative mode (no data mutation)- Optional:
vmfo/create-folderexecutes via gateway in mutating mode
Hooks & Filters
Section titled “Hooks & Filters”See hooks.md for the complete hooks reference, including all core hooks and add-on hooks with examples.
Preconfiguring Folders
Section titled “Preconfiguring Folders”Create folders programmatically using the WordPress taxonomy API:
add_action( 'init', function() { // Only run once if ( get_option( 'my_theme_vmfo_folders_created' ) ) { return; }
if ( ! taxonomy_exists( 'vmfo_folder' ) ) { return; }
$folders = [ 'Photos' => [ 'Events', 'Products', 'Team' ], 'Documents' => [ 'Reports', 'Presentations' ], 'Videos', 'Logos', ];
foreach ( $folders as $parent => $children ) { if ( is_array( $children ) ) { $parent_term = wp_insert_term( $parent, 'vmfo_folder' ); if ( ! is_wp_error( $parent_term ) ) { foreach ( $children as $child ) { wp_insert_term( $child, 'vmfo_folder', [ 'parent' => $parent_term['term_id'], ] ); } } } else { wp_insert_term( $children, 'vmfo_folder' ); } }
update_option( 'my_theme_vmfo_folders_created', true );}, 20 );Set custom folder order:
update_term_meta( $term_id, 'vmfo_order', 0 ); // First positionupdate_term_meta( $term_id, 'vmfo_order', 1 ); // Second positionTranslation
Section titled “Translation”# Generate all translation filesnpm run i18n
# Or individually:npm run i18n:make-pot # Generate POT filenpm run i18n:update-po # Update PO filesnpm run i18n:make-mo # Generate MO filesnpm run i18n:make-json # Generate JSON for JavaScriptnpm run i18n:make-php # Generate PHP for faster loadingThe i18n-map.json file maps source files to their compiled outputs for proper string extraction.
Creating a Distribution
Section titled “Creating a Distribution”# Install WP-CLI dist-archive command (one-time)wp package install wp-cli/dist-archive-command
# Build and create zipnpm run buildcomposer install --no-devwp dist-archive . virtual-media-folders.zip --plugin-dirname=virtual-media-foldersThe .distignore file controls what’s excluded from the distribution.
Contributing
Section titled “Contributing”- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Write tests for new functionality
- Ensure all tests pass (
composer test && npm test) - Commit your changes
- Push to the branch
- Submit a pull request
Code Style
Section titled “Code Style”- PHP: WordPress Coding Standards
- JavaScript: WordPress Scripts ESLint config
- Use strict typing in PHP (
declare(strict_types=1))
📦 Source: soderlind/virtual-media-folders · Edit on GitHub