Native Tool Plugins
Version: mcp_server 2.0.0-beta5; SDK NameValidator from mcp/sdk v0.8.1.
When to Use
Use a native
#[Tool]plugin when the operation is MCP-only and you do not want a Tool API dependency. If the operation should also run from Drush, ECA or a controller, write a Tool API tool and bridge it instead.
Decision
| If the operation... | Write... | Page |
|---|---|---|
| Is MCP-only | A native #[Tool] plugin (this page) |
— |
| Should also run from Drush, ECA, PHP or the AI module | A Tool API tool, exposed through the bridge | Defining a tool, Tool API Bridge |
Pattern
The README example omits defaultConfiguration(). By the code, that example registers nothing. ToolPluginBase::defaultConfiguration() returns ['enabled' => FALSE], and McpServerFactory::registerTools() creates each plugin with no configuration and skips it when !$plugin->isEnabled(). Every test tool overrides it. The README class with the two missing pieces, taken from the test tool PermissionGatedTool.php:
namespace Drupal\my_module\Plugin\mcp_server\Tool;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\mcp_server\Attribute\Tool;
use Drupal\mcp_server\Plugin\ToolPluginBase;
use Mcp\Server\ClientGateway;
#[Tool(
id: 'send_email',
label: new TranslatableMarkup('Send Email'),
description: new TranslatableMarkup('Sends an email to a recipient.'),
inputSchema: [
'type' => 'object',
'properties' => ['to' => ['type' => 'string'], 'subject' => ['type' => 'string']],
'required' => ['to', 'subject'],
],
)]
final class SendEmail extends ToolPluginBase {
protected function defaultConfiguration(): array {
return ['enabled' => TRUE];
}
public function checkAccess(AccountInterface $account): AccessResultInterface {
return AccessResult::allowedIfHasPermission($account, 'send mcp email');
}
public function execute(array $arguments, ClientGateway $gateway): mixed {
return ['success' => TRUE, 'message' => 'Email sent'];
}
}
Place it in src/Plugin/mcp_server/Tool/. Then run drush cache:rebuild. send mcp email is an example permission your module would declare.
Attribute defaults
From mcp_server 2.0.0-beta5 src/Attribute/Tool.php:
| Argument | Default | Sent to clients as |
|---|---|---|
inputSchema |
[] |
inputSchema; type forced to object, empty sub-schemas sent as {} |
outputSchema |
NULL |
omitted |
readOnly |
FALSE |
readOnlyHint |
destructive |
TRUE |
destructiveHint |
idempotent |
FALSE |
idempotentHint |
openWorld |
TRUE |
openWorldHint |
Registration rules
checkAccess()runs when the server is built: once per HTTP request, once per STDIO run. A denied tool is absent fromtools/list, and a call returns the SDK's "tool not found".- Names must pass the SDK
NameValidator(^[a-zA-Z0-9._/-]{1,64}$); invalid names are skipped with a warning. A duplicate name is skipped; the first one wins. - Derivative IDs use
__, because stricter clients accept only^[a-zA-Z0-9_]{1,64}$(WireSafeDerivativeDiscoveryDecoratordocblock). - Progress and elicitation use the
$gatewayargument ofexecute().ClientGatewayAwareInterfaceis called only by the bridge, on wrapped Tool API tools.
Common Mistakes
- Copying the README example as-is. Add
defaultConfiguration()returningenabled => TRUE. - Importing Tool API's attribute. Both modules define a
#[Tool]: useDrupal\mcp_server\Attribute\Toolhere.Drupal\tool\Attribute\Toolbelongs to Tool API tools, which another manager discovers. - Leaving
checkAccess()at its default. It returnsAccessResult::allowed(), so everyone who reaches the server can call the tool. - Leaving
destructiveat its default on a read tool. SetreadOnly: TRUE, destructive: FALSEso clients can skip confirmation. - Expecting hints to be enforced.
destructiveHintand the others are hints to the client only; nothing in the module asks for confirmation. - Reading
references/tools/index.mdfrom the README. It does not exist in the tree.
See Also
- Tool API Bridge → for the Tool API path
- Defining a tool (Tool API)
- Server Configuration and Extension Points → for
hook_mcp_server_tool_alter() - Reference:
modules/contrib/mcp_server/src/Plugin/ToolPluginBase.php,src/McpServerFactory.php(registerTools()),src/Attribute/Tool.php,tests/modules/mcp_server_test/src/Plugin/mcp_server/Tool/PermissionGatedTool.php;vendor/mcp/sdk/src/Capability/Tool/NameValidator.php