Transport Permission Callbacks
November 10, 2025 · View on GitHub
Transport permission callbacks provide custom authentication for your MCP servers. By default, servers use is_user_logged_in(), but you can implement custom authentication logic.
Basic Usage
Default Behavior (Logged-in Users)
$adapter->create_server(
'my-server',
'my-plugin',
'mcp',
'My MCP Server',
'Server description',
'1.0.0',
[\WP\MCP\Transport\HttpTransport::class],
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
['my-plugin/tool'], // tools
[], // resources
[], // prompts
// No permission callback = uses is_user_logged_in()
);
Custom Permission Callback
Add a permission callback as the last parameter:
// Admin-only access
$adapter->create_server(
'admin-server',
'my-plugin',
'mcp-admin',
'Admin MCP Server',
'Admin-only server',
'1.0.0',
[\WP\MCP\Transport\HttpTransport::class],
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
['my-plugin/admin-tool'], // tools
[], // resources
[], // prompts
function(): bool { // Permission callback
return current_user_can('manage_options');
}
);
Permission Callback Types
Simple Boolean Return
function(): bool {
return current_user_can('edit_posts');
}
Detailed Error Information
function(): WP_Error|bool {
if (!is_user_logged_in()) {
return new WP_Error('not_logged_in', 'Please log in', ['status' => 401]);
}
if (!current_user_can('manage_options')) {
return new WP_Error('insufficient_permissions', 'Admin access required', ['status' => 403]);
}
return true;
}
Error Handling
- Automatic Fallback: Exceptions fall back to
is_user_logged_in() - Error Logging: Callback failures are logged
- Secure Default: Always requires authentication
Common Patterns
Role-Based Access
// Allow editors and administrators
function(): bool {
return current_user_can('edit_posts');
}
// Multiple roles
function(): bool {
return current_user_can('edit_posts') || current_user_can('manage_options');
}
API Key Authentication
function(\WP_REST_Request $request): WP_Error|bool {
$api_key = $request->get_header('X-API-Key');
if (empty($api_key)) {
return new WP_Error('missing_api_key', 'API key required', ['status' => 401]);
}
$valid_keys = get_option('my_plugin_api_keys', []);
if (!in_array($api_key, $valid_keys, true)) {
return new WP_Error('invalid_api_key', 'Invalid API key', ['status' => 403]);
}
return true;
}
Time-Based Access
function(): WP_Error|bool {
if (!is_user_logged_in()) {
return new WP_Error('not_logged_in', 'Authentication required', ['status' => 401]);
}
// Business hours (9 AM - 5 PM)
$current_hour = (int) wp_date('H');
if ($current_hour < 9 || $current_hour > 17) {
if (!current_user_can('manage_options')) {
return new WP_Error(
'outside_business_hours',
'Access only available during business hours (9 AM - 5 PM)',
['status' => 403]
);
}
}
return current_user_can('edit_posts');
}
Two-Layer Security
MCP Adapter uses two security layers:
- Transport Permission (Server-wide gatekeeper)
- Ability Permission (Individual tool access)
Transport permissions act as a gatekeeper - if blocked here, users cannot access ANY abilities on that server.
Example
// Transport: Allow editors and admins
$adapter->create_server(
'content-server',
'my-plugin',
'mcp-content',
'Content Server',
'Content management server',
'1.0.0',
[\WP\MCP\Transport\HttpTransport::class],
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
['my-plugin/edit-post', 'my-plugin/delete-post'],
[], // resources
[], // prompts
function(): bool {
// Transport: Allow editors and admins
return current_user_can('edit_posts');
}
);
// Individual abilities check specific permissions:
wp_register_ability('my-plugin/edit-post', [
'permission_callback' => function($args) {
// Ability: Check if user can edit THIS specific post
return current_user_can('edit_post', $args['post_id']);
},
// ...
]);
Best Practices
Keep Callbacks Fast
// ✅ Good: Simple and direct
function(): bool {
return current_user_can('edit_posts');
}
// ❌ Avoid: Complex operations that slow requests
function(): bool {
return $this->check_remote_api() && $this->complex_calculation();
}
Use Broadest Capability
Set transport permissions to the broadest capability needed by any ability on the server:
// ✅ Good: Transport allows editors, abilities decide specifics
function(): bool {
return current_user_can('edit_posts'); // Broadest capability needed
}
Testing
Test permission callbacks with different user roles:
# Test as admin
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | wp mcp-adapter serve --user=admin --server=admin-server
# Test as editor (should fail for admin-only server)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | wp mcp-adapter serve --user=editor --server=admin-server
Implementation Notes
Callback Parameters
- HttpTransport: Receives
\WP_REST_Request $requestparameter - Legacy transports: May have different signatures
- Return types:
boolorWP_Error
Error Handling
- Exceptions automatically fall back to
is_user_logged_in() - All failures are logged with context
- Secure default behavior
Next Steps
- Custom Transports - For complex authentication needs
- Error Handling - Custom error management
- Creating Abilities - Ability-level permissions