Resources and Resource Templates
Version: mcp_server 2.0.0-beta5; mcp_server_examples 1.0.0-beta1 (released 2026-07-29).
When to Use
Use resources to give an agent read-only data. Resources are read-only by protocol, so a site that serves only resources and read tools is read-only.
Decision
| If you need... | Plugin type | Directory | Listed via |
|---|---|---|---|
| Fixed URIs, enumerable up front | mcp_server.resource_provider, #[ResourceProvider], ResourceProviderBase |
src/Plugin/mcp_server/ResourceProvider/ |
resources/list |
Parameterized URIs (drupal://entity/{entity_type}/{entity_id}) |
mcp_server.resource_template_provider, #[ResourceTemplateProvider], ResourceTemplateProviderBase |
src/Plugin/mcp_server/ResourceTemplateProvider/ |
resources/templates/list |
Pattern
A plugin is not enough. It must be listed and enabled in config. McpServerFactory reads mcp_server.resource_providers and mcp_server.resource_template_providers and registers only entries with top-level enabled true. mcp_server ships no install config for either; the UI to toggle them is in mcp_server_ui. Shape from the test fixture mcp_server.resource_template_providers.yml:
plugins:
-
id: test_completion_template
enabled: true
configuration:
enabled: true
Return content as CacheableResourceContent. Adapted from the test provider TestResourceProvider.php:
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Session\AccountInterface;
use Drupal\mcp_server\Resource\CacheableResourceContent;
public const URI = 'test://hello';
public function getResourceContent(string $uri): ?CacheableResourceContent {
$content = ['uri' => $uri, 'mimeType' => 'application/json', 'text' => '{"hello":"world"}'];
return CacheableResourceContent::fromArray($content, new CacheableMetadata());
}
public function checkAccess(string $uri, AccountInterface $account): AccessResultInterface {
return $uri === self::URI ? AccessResult::allowed() : AccessResult::forbidden();
}
The class itself uses Drupal\mcp_server\Attribute\ResourceProvider and extends Drupal\mcp_server\Plugin\ResourceProviderBase.
Access and caching
checkAccess()runs on every read, before any cache lookup. A denial reaches the client as JSON-RPC internal error -32603 "Error while reading resource".- Content is stored with the DTO's cache metadata. The default max-age is permanent. A max-age of 0 skips storage.
- For content that differs per user, set max-age 0. Enforcement at this version was not verified on a running site.
getResourceContent()returningNULLthrows "Resource not found".
Content entities
mcp_server_examples holds ContentEntityResourceTemplate: URI template drupal://entity/{entity_type}/{entity_id}, JSON:API serialization, module_dependencies: ['jsonapi'], and $entity->access('view', $account, TRUE) on every read. Options: require_canonical_url (default TRUE) and denied_entity_types. Copy it into your module. In the examples project its namespace is Drupal\mcp_server_examples\Plugin\ResourceTemplateProvider. mcp_server 2.0.0-beta5 discovers Plugin/mcp_server/ResourceTemplateProvider, so move it under Plugin\mcp_server\ when you copy it. The examples' tool plugins (src/Plugin/Tool/) sit outside Plugin/mcp_server/Tool for the same reason. Read from code, not run.
Common Mistakes
- Adding a resource plugin and expecting it in
resources/list. Add an enabled entry to the config object. - Caching per-user content. Set max-age 0.
- Checking access only in
getResources(). Access is enforced bycheckAccess(); enumerate cheaply and check per URI. - Depending on
mcp_server_examples. It is an examples project; copy the pattern.
See Also
- Prompts → for argument completion providers, which templates share
- Reference:
modules/contrib/mcp_server/src/Plugin/ResourceTemplateProviderInterface.php,src/Plugin/ResourceProviderInterface.php,src/Resource/CacheableResourceContent.php,config/schema/mcp_server.schema.yml;modules/contrib/mcp_server_examples/src/Plugin/ResourceTemplateProvider/ContentEntityResourceTemplate.php