# MCP server

<ExperimentalFeatureAside feature="MCP Server" />

Lookup material for the Source CMS [MCP server](/start-here/glossary/#mcp-server): the connection spec, every resource and resource template, and every tool. For setup instructions, see the [MCP quickstart](/source-cms/ai-agents/quickstart/) (Claude Code, Claude Desktop, Cursor, VS Code, GitHub Copilot, OpenCode, Codex) and the [MCP guide](/source-cms/ai-agents/guide/) (those plus MCP Inspector).

## Connection

| Property | Value |
| --- | --- |
| Server URL | `{DRUPAL_SITE_URL}/mcp`: your [site](/start-here/glossary/#site) URL plus `/mcp`. Also shown under `Configurations > Experimental features > MCP Server > Configure > MCP client configuration` in the site admin UI. |
| Transport | Streamable HTTP; responses arrive as SSE (`text/event-stream`) with a single `data:` event per response (verified) |
| Server identity | Name `Acquia Source MCP`, version `1.0.0` |
| Protocol version | `2025-11-25`, negotiated at `initialize`. |
| Capabilities | `logging`, `completions`, `prompts` (listChanged), `resources` (subscribe + listChanged), `tools` (listChanged) |
| Session | Responses carry an `mcp-session-id` header; every subsequent request must echo it back as `Mcp-Session-Id`. |
| Authentication | OAuth 2.0 bearer token. The site's [API client](/start-here/glossary/#api-client) tokens work: a `client_credentials` grant against `{DRUPAL_SITE_URL}/oauth/token` (no scope parameter needed). Interactive clients instead self-register via dynamic client registration and run the browser flow. See the [authentication guide](/source-cms/authenticate/guide/). |
| Token lifetime | Access tokens expire in 300 seconds; clients refresh silently. |
| Discovery | The site publishes `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server` (authorization endpoint `/oauth/authorize`, token endpoint `/oauth/token`, JWKS at `/.well-known/jwks.json`, dynamic client registration at `/oauth/register`). An unauthenticated or expired request gets `401` whose `WWW-Authenticate` header is only `Bearer realm="mcp_server"` (verified 2026-07-03); read the resource metadata document at `{DRUPAL_SITE_URL}/.well-known/oauth-protected-resource/mcp` directly (16 `scopes_supported`, `bearer_methods_supported: ["header"]`). |
| [Scopes](/start-here/glossary/#scope) | The 16 supported scopes are listed in the resource metadata document (not in the `401` header) and under `OAuth configuration` in the Configure panel. Scope gating was not observable in verification: a `client_credentials` token issued without a `scope` parameter (so carrying every scope selected on its API client) called every tool successfully. |
| Availability | Experimental; off by default, enabled per site under `Configurations > Experimental features`. |
| Debug client | `npx @modelcontextprotocol/inspector --transport http`, connection type `Direct`. |

### Client configuration

The server registration per harness, with the values from the table above. Interactive clients complete the browser OAuth flow on first connection; headless setups pass a `client_credentials` token as an `Authorization` header instead ([MCP guide, step 2](/source-cms/ai-agents/guide/#understand-how-credentials-reach-the-server)).

<Tabs syncKey="agent">
  <TabItem label="Claude Code">
    ```bash
    claude mcp add acquia-source-mcp {DRUPAL_SITE_URL}/mcp \
      --transport http --scope project
    ```

    Authenticate from the `/mcp` panel.
  </TabItem>
  <TabItem label="Claude Desktop">
    Custom connector: `Customize > Connectors`, `+`, `Add custom connector`, the server URL, `Add`, then `Connect`. Custom connectors reach the server from Anthropic's cloud, so the site must be reachable over the public internet.
  </TabItem>
  <TabItem label="Cursor">
    `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):

    ```json
    {
      "mcpServers": {
        "acquia-source-mcp": {
          "url": "{DRUPAL_SITE_URL}/mcp"
        }
      }
    }
    ```
  </TabItem>
  <TabItem label="VS Code">
    `.vscode/mcp.json` in the project, or `MCP: Open User Configuration` (user profile):

    ```json
    {
      "servers": {
        "acquia-source-mcp": {
          "type": "http",
          "url": "{DRUPAL_SITE_URL}/mcp"
        }
      }
    }
    ```
  </TabItem>
  <TabItem label="GitHub Copilot">
    Copilot Chat in VS Code reads the repository's `.vscode/mcp.json` (the VS Code configuration) or your personal `settings.json`; other IDEs carry their own files per [Extending Copilot Chat with MCP](https://docs.github.com/en/copilot/how-tos/provide-context/use-mcp/extend-copilot-chat-with-mcp).
  </TabItem>
  <TabItem label="OpenCode">
    `opencode.json` at the project root:

    ```json
    {
      "$schema": "https://opencode.ai/config.json",
      "mcp": {
        "acquia-source-mcp": {
          "type": "remote",
          "url": "{DRUPAL_SITE_URL}/mcp"
        }
      }
    }
    ```
  </TabItem>
</Tabs>

Tool domains: **Content Management** (read/write content entities), **Content Modeling** (define and evolve content structure), **Taxonomy & Menus**, **Media**, **Site Settings**, **[Drupal Canvas](/start-here/glossary/#drupal-canvas)** pages and components, and **Drafts & Publishing**. Alongside the tools, the server exposes resources (read-only data) and resource templates (parameterized reads).

## Errors

MCP runs over JSON-RPC 2.0, so failures surface at three levels:

- **HTTP.** An unauthenticated or expired request gets `401 Unauthorized`. The `WWW-Authenticate` header is only `Bearer realm="mcp_server"` (verified 2026-07-03: it does not point at the resource metadata or list scopes); the JSON-RPC body carries error `-32001` `Authentication required`. The resource metadata lives at the well-known URL in [Connection](#connection).
- **JSON-RPC (schema-level).** Input that violates a tool's schema is rejected before execution with code `-32602`: e.g. "Invalid parameters for tool '…': Property '/workflow': Invalid type. Expected string, but received null.", plus a `data.validation_errors` array of `{pointer, keyword, message}` objects.
- **Tool-level.** Failures during execution come back as a *successful* JSON-RPC response with `isError: false` (verified 2026-07-03); check `structuredContent.success`, not `isError`. Two failure forms exist: an `error` object of `{code, message, retryable, details, requestId}` (captured codes: `validation` for a nonexistent target, `configuration` for a missing integration), or, when entity validation fails, a populated `violations` array of `{field_name, message, invalid_value, code}` with no `error` object.

Successful tool calls return their JSON payload twice: as text in `result.content[0].text` and mirrored in `result.structuredContent`.

| Code / status | Meaning | Typical cause here |
| --- | --- | --- |
| HTTP `401 Unauthorized` | Request carried no valid token | OAuth flow not completed, token expired (300 s lifetime), or the MCP Server feature was disabled after connect |
| `-32600` | Invalid request | Malformed JSON-RPC envelope (client bug) |
| `-32601` | Method not found | Stale client capability cache or protocol-version mismatch |
| `-32602` | Invalid params | Tool input doesn't match its schema: details in `data.validation_errors` |
| `-32603` | Internal error | Server-side failure while executing the call |
| `-32001` | Server-defined error | Two captured uses: `Authentication required` on an unauthenticated request, and request timeouts on oversized queries or batches (narrow or split them) |

Every entry below shares this error behavior; per-entry "Errors" notes list only what's specific to that entry. Successful tool responses share `success: true`, a human-readable `message`, and a `violations` array (empty on success); per-entry Response lines list the distinctive members. MCP Inspector (`Tools`, `List Resources`, `List Templates`) shows the same shapes interactively.

## Resources

Read-only lists at fixed URIs. Read them with the standard MCP `resources/read` request.

### drupal://content-types

Lists the site's [content types](/start-here/glossary/#content-type-bundle) with their schemas.

- **URI:** `drupal://content-types` (no parameters)
- **Response:** `{usage, content_types: [{entity_type, bundle, label, description, workflow, schema_uri}]}`: one entry per content type, each pointing at its `drupal://content-types/{bundle}` schema.
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### drupal://media-types

Lists the site's [media types](/start-here/glossary/#media-type) with their schemas. DAM-backed types are flagged with `dam_type: true`.

- **URI:** `drupal://media-types` (no parameters)
- **Response:** `{media_types: [{entity_type, bundle, label, description, source, schema_uri, upload_supported, dam_type}]}`. Captured: 12 types; the 8 DAM types are `dam_type: true, upload_supported: false`; `image`, `document`, and `video` are uploadable; `remote_video` is neither (OEmbed).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://vocabularies

Lists the site's taxonomy [vocabularies](/start-here/glossary/#vocabulary).

- **URI:** `drupal://vocabularies` (no parameters)
- **Response:** `{vocabularies: [{entity_type, vid, label, description, schema_uri}]}`, each entry pointing at its `drupal://vocabularies/{vid}` schema (captured: `categories` and `tags`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://filter-formats

Lists the text filter formats available when creating or editing content with formatted text fields.

- **URI:** `drupal://filter-formats` (no parameters)
- **Response:** `{formats: [{id, label, allowedHtml, allowedAttributes}]}`. Captured: `filtered_html` (13 allowed tags) and `plain_text`; `allowedAttributes` maps attribute names to allowed values, with `"on*": false` blocking event handlers.
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://content-types/fields

Lists [field](/start-here/glossary/#field) storage definitions for the node entity type, with usage information: which storages exist and which bundles use them.

- **URI:** `drupal://content-types/fields` (no parameters)
- **Response:** `{fields: [{field_name, type, target_type, cardinality, appears_on, translatable}], description}`; `appears_on` lists the bundles using each storage (captured).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://field-types

Lists the field types available for [content modeling](#content-modeling-tools), organized by category.

- **URI:** `drupal://field-types` (no parameters)
- **Response:** the field-type catalog: plugin IDs usable as `field_type` in the modeling tools, including entries such as `entity_reference` and `field_ui:entity_reference:media`.
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://menus

Lists the site's menus. Each entry includes an `items_uri` pointing at the `drupal://menus/{menu_name}` template for that menu's item tree.

- **URI:** `drupal://menus` (no parameters)
- **Response:** `{menus: [{id, label, description, items_uri}]}` (captured: one menu, `main`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://site-info

Returns the site name and configured front/home page. Use its `homepage` object to identify the home page; never guess by path string-matching. If `homepage` is `null`, the site has no configured front page.

- **URI:** `drupal://site-info` (no parameters)
- **Response:** captured verbatim: `{"site_name": "New Site", "homepage": {"path": "/homepage", "entity_type": "canvas_page", "entity_id": 1}}`.
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### canvas://components

Lists the Drupal Canvas [components](/start-here/glossary/#component) available for composition.

- **URI:** `canvas://components` (no parameters)
- **Response:** array of `{id, name, library, source, version, props, slots, detail_uri}`; each prop is `{required, schema, default_value}` with JSON Schema types (enums for select-style props). Captured: one code component, `js.text`.
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### canvas://pages

Lists the site's editable Drupal Canvas [pages](/start-here/glossary/#pages-canvas) with live and draft metadata.

- **URI:** `canvas://pages` (no parameters)
- **Response:** array of `{id, hasDraftChanges, live: {title, status, path, internalPath, canonicalUrl}, draft}`; `draft` is `null` when the page has no pending draft (captured).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### canvas://page-regions

Lists the global Canvas page regions (header, footer, and so on) shared across all pages for the active theme. Region IDs feed the [page-region tools](#get_page_region_layout).

- **URI:** `canvas://page-regions` (no parameters)
- **Response:** array of `{id, region, theme, label}` with theme-prefixed IDs (captured: `astral.header` and `astral.footer`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### canvas://auto-saves

Lists the current auto-saved drafts, with the identifiers the publish/discard tools need: [`publish_auto_saves`](#publish_auto_saves) takes both `autosave_key` and `data_hash`; [`discard_auto_saves`](#discard_auto_saves) takes the keys.

- **URI:** `canvas://auto-saves` (no parameters)
- **Response:** array of `{autosave_key, entity_type, entity_id, label, data_hash, updated, owner, langcode, draft_published}` (captured).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Resource templates

Parameterized reads. Substitute the `{placeholder}` with a concrete value.

### drupal://entity/\{entity_type_id\}/\{id\}

Loads one entity by [entity type](/start-here/glossary/#entity-type) and ID. Supported entity types: `node`, `taxonomy_term`, `media`, `user`, `canvas_page`.

- **Parameters:** `entity_type_id`: entity type [machine name](/start-here/glossary/#machine-name); `id`: the entity's ID.
- **Example URI:** `drupal://entity/node/17`
- **Response:** the raw Drupal field-array representation, not the JSON:API shape: every field is an array of value objects (`"title": [{"value": "…"}]`), and references carry `{target_id, target_uuid, target_type, url}` (captured for `drupal://entity/node/6`).
- **Errors:** [standard errors](#errors); a nonexistent entity is a failed read.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://content-types/\{bundle\}

Returns the JSON Schema for one content type.

- **Parameters:** `bundle`: content type machine name.
- **Example URI:** `drupal://content-types/article`
- **Response:** the full JSON Schema (draft-04) for the bundle's payload: the fields the create/update tools accept for that content type.
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://content-types/\{bundle\}/fields/\{field_name\}

Returns one field's configuration on a content type: instance settings (label, description, required) and storage settings (type, cardinality, `has_data`, locked). Read it before changing a field with [`update_field_config`](#update_field_config).

- **Parameters:** `bundle`: content type machine name; `field_name`: field machine name.
- **Example URI:** `drupal://content-types/article/fields/summary`
- **Response:** `{field_name, bundle, label, description, required, default_value, field_settings, storage: {type, cardinality, translatable, storage_settings, has_data, locked}}` (captured for `article`/`body`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://media-types/\{bundle\}

Returns the JSON Schema for one media type.

- **Parameters:** `bundle`: media type machine name (e.g. `image`).
- **Example URI:** `drupal://media-types/image`
- **Response:** the full JSON Schema (draft-04) for the media type's payload, same structure as the content-type schemas (captured for `image`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://vocabularies/\{vid\}

Returns the JSON Schema for one taxonomy vocabulary.

- **Parameters:** `vid`: vocabulary machine name.
- **Example URI:** `drupal://vocabularies/tags`
- **Response:** the full JSON Schema (draft-04) for term payloads in that vocabulary (captured for `tags`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### drupal://menus/\{menu_name\}

Returns the item tree for one menu: enabled links in weight order, hierarchy represented by a `depth` field. Each item includes an `id` usable with [`update_menu_item`](#update_menu_item) and [`delete_menu_item`](#delete_menu_item).

- **Parameters:** `menu_name`: menu machine name; discover IDs via `drupal://menus`.
- **Example URI:** `drupal://menus/main`
- **Response:** `{menu_name, label, items}` (captured for `main`; its `items` was `[]`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### canvas://components/\{id\}

Returns one Drupal Canvas component's detailed schema and metadata, including the [props](/start-here/glossary/#prop) and [slots](/start-here/glossary/#slot) it declares.

- **Parameters:** `id`: component ID exactly as listed by `canvas://components`; captured IDs are dotted, `js.<machine_name>` for code components (e.g. `js.text`).
- **Example URI:** `canvas://components/js.text`
- **Response:** everything the `canvas://components` entry lists for the component plus, per prop, `source_type` (e.g. `static:field_item:string`) and `default_values: {source, resolved}` (captured for `js.text`).
- **Errors:** [standard errors](#errors) only.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Content management tools

### create_node

Creates one content entry (node) of a given content type.

- **Input:**
  - `bundle` (string, **required**): content type machine name; discover bundles via `drupal://content-types`.
  - `fields` (object, **required**): field names as keys, values as direct values (`{"title": "My Title", "bio": "Text"}`, not an array of name/value pairs). Must include a `title` key. Field names do **not** use the `field_` prefix. Read `drupal://content-types/{bundle}` to discover the fields.
- **Response:** `{success, message, violations, warnings, entity_id, url, bundle, schema_uri}` (captured). On a workflow-moderated bundle, `warnings` notes that draft content is not accessible or referenceable until published.
- **Errors:** [standard errors](#errors); `-32602` when input doesn't match the bundle's schema (read `drupal://content-types/{bundle}` first).
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### update_node

Updates an existing node. Partial update: only the provided fields change.

- **Input:**
  - `entity_id` (integer, **required**): node ID to update.
  - `fields` (object, **required**): field names as keys; only provided fields are updated. Entity references accept labels or IDs. Use a `moderation_state` key for [workflow](/start-here/glossary/#workflow) transitions (e.g. `"moderation_state": "published"`). Field names do not use the `field_` prefix.
- **Response:** same shape as `create_node` (captured). A value that fails entity validation returns `success: false` with the failure in `violations[]` (e.g. a `moderation_state` that doesn't exist in the bundle's workflow) and no `error` object.
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### batch_create_nodes

Creates multiple nodes of the same content type in one call (the bulk-generation tool for content entries). Media and terms are created one call at a time.

- **Input:**
  - `bundle` (string, **required**): content type machine name; all nodes in the batch share it.
  - `nodes` (array of objects, **required**, min 1): one field object per node, same format as `create_node`'s `fields`; every object must include `title`.
  - `options` (object): `validate_only` (boolean, dry-run validation without saving), `stop_on_error` (boolean, default `false`), `return_details` (boolean, default `true`; set `false` for token-efficient bulk runs).
- **Response:** `{total, successful, failed, results: [{index, success, title}], errors}`; with `options.validate_only: true` the response adds `validate_only: true` and nothing is saved: a true dry run (both captured).
- **Errors:** [standard errors](#errors); `-32001` on oversized batches: split them.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### list_entities

Lists entities with filtering and pagination: the general read used to fetch site content (e.g. recent articles). Supports `node`, `taxonomy_term`, `media`, `user`, and `canvas_page`.

- **Input:**
  - `entity_type` (string, **required**): the entity type to list.
  - `bundle` (string): bundle/content type machine name to filter by.
  - `filters` (object): field names as keys, filter values as values. Operator suffixes: `__contains`, `__starts_with`, `__ends_with`, `__gt`, `__gte`, `__lt`, `__lte`, `__not`, `__in`, `__not_in`, `__between`, `__not_between`, `__is_null`, `__is_not_null`. `label` is an alias for the entity label field (e.g. `{"label__contains": "News"}`, `{"created__gte": 1704067200}`).
  - `limit` (integer, default 50): maximum results.
  - `offset` (integer, default 0): pagination offset.
- **Response:** `{entity_type, bundle, resource_template, total, items, pagination}` where each item is `{id, uuid, label, bundle, status, uri, path, canonicalUrl, revision_id}`, `uri` is a ready-to-read `drupal://entity/{entity_type_id}/{id}` URI, and `pagination` is `{limit, offset, has_more}` (captured).
- **Errors:** [standard errors](#errors); `-32001` on unfiltered sweeps of large sites: filter by content type or page the results.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

## Content modeling tools

### create_content_type

Creates a new content type (node bundle) and returns the schema URI for it.

:::caution
`workflow` is **required** and must name a [workflow](/start-here/glossary/#workflow) that already exists. Empty or null values are rejected at the schema level (`-32602`), and a nonexistent name fails with `Workflow "…" does not exist.` No MCP tool creates workflows, and a fresh Source CMS site has none, so on a fresh site this tool fails until an administrator creates a workflow in the admin UI. Existing workflows appear per content type in `drupal://content-types`.
:::

- **Input:**
  - `machine_name` (string, **required**, max 32, `^[a-z][a-z0-9_]*$`): the content type machine name.
  - `label` (string, **required**, max 255): human-readable label.
  - `workflow` (string, **required**): the workflow to assign to the content type; see the caution above.
  - `description` (string): optional description.
  - `options` (object): `create_body_field` (boolean, default `true`: add the standard body field), `help` (string: help text on the node add form).
- **Response:** `{machine_name, label, schema_uri, message}` (captured).
- **Errors:** [standard errors](#errors); tool-level `validation` error `Workflow "…" does not exist.` when `workflow` names a workflow the site doesn't have.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### add_field_to_content_type

Adds a field to an existing content type. Supports field-storage reuse across bundles. Valid field types are listed by `drupal://field-types`.

- **Input:**
  - `content_type` (string, **required**): the content type machine name.
  - `field_name` (string, **required**, max 32, `^[a-z][a-z0-9_]*$`): unique machine name; do **not** prefix with `field_`.
  - `field_type` (string, **required**): field type plugin ID (e.g. `string`, `integer`, `datetime`) from `drupal://field-types`.
  - `label` (string, **required**, max 255): human-readable label.
  - `description` (string): optional help text.
  - `required` (boolean, default `false`).
  - `cardinality` (integer, default 1): number of values; `-1` for unlimited.
  - `storage_settings` (object): e.g. `max_length` for string fields; for `list_string`/`list_integer`, pass `allowed_values` as `{"gold": "Gold", "silver": "Silver"}`.
  - `field_settings` (object): field instance settings.
  - `default_value`: optional default value.
- **Response:** `{success, field_name, content_type, field_type, storage_created, violations, message}`; `storage_created` reports whether a new field storage was created rather than reused (captured).
- **Errors:** [standard errors](#errors); `-32602` for a field type not present in `drupal://field-types`.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### batch_add_fields_to_content_type

Adds multiple fields to one or more content types in a single call. Items are processed in order, so a field storage created by an earlier item can be reused by a later item on a different bundle.

- **Input:**
  - `fields` (array of objects, **required**, min 1): each object takes the same parameters as `add_field_to_content_type`: `content_type`, `field_name`, `field_type`, `label` required; `description`, `required`, `cardinality`, `storage_settings`, `field_settings`, `default_value` optional.
- **Response:** `{total, successful, failed, results, errors}`; each result is the `add_field_to_content_type` shape plus its `index` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### update_field_config

Updates an existing field's configuration on a content type. Mutable: `label`, `description`, `required`, `default_value`, `field_settings`, `cardinality`, and (for list fields only) `storage_settings.allowed_values`. Other storage-level settings (`target_type`, `max_length`, …) cannot be changed with this tool.

- **Input:**
  - `content_type` (string, **required**): the content type machine name.
  - `field_name` (string, **required**): field machine name; do not prefix with `field_`.
  - `label` (string): new label.
  - `description` (string): new help text.
  - `required` (boolean): set required or optional.
  - `field_settings` (object): instance settings to update.
  - `default_value`: new default value.
  - `cardinality` (integer): `-1` for unlimited or a positive integer. Increasing is always safe; reducing is only allowed when no entities have values at higher deltas.
  - `storage_settings` (object): only `allowed_values` for `list_string`/`list_integer` field types.
- **Response:** `{success, field_name, content_type, violations, message}`; the message names the keys that changed, e.g. `"(label, required)"` (captured).
- **Errors:** [standard errors](#errors). Inspect current settings first via `drupal://content-types/{bundle}/fields/{field_name}`.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Taxonomy and menu tools

### create_vocabulary

Creates a taxonomy vocabulary. Idempotent: if the vocabulary already exists, returns the existing metadata without creating a duplicate.

- **Input:**
  - `vid` (string, **required**, max 32, `^[a-z][a-z0-9_]*$`): vocabulary machine name (e.g. `tags`, `sponsor_tier`).
  - `name` (string, **required**, max 255): human-readable label.
  - `description` (string): optional description.
  - `weight` (integer): optional display weight.
- **Response:** `{success, vid, name, description, schema_uri, existed, message}`; `existed: true` on the idempotent path for the existing `tags`.
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### get_or_create_term

Returns an existing taxonomy term by exact name match (case-insensitive), or creates it if absent (idempotent tagging).

- **Input:**
  - `vid` (string, **required**): vocabulary machine name; discover via `drupal://vocabularies`.
  - `name` (string, **required**): the exact term name to find or create.
  - `additional_fields` (object): optional extra field values (e.g. `description`, `parent`, `weight`).
- **Response:** `{success, entity_id, name, vid, existed, message, violations, warnings}` (captured: an existing term returns `existed: true` with its `entity_id`).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### create_menu

Creates a new custom menu.

- **Input:**
  - `id` (string, **required**, max 32, `^[a-z0-9_-]+$`): menu machine name (e.g. `footer-menu`).
  - `label` (string, **required**, max 255): human-readable label.
  - `description` (string): optional description.
- **Response:** `{success, message, violations, id, label}` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### update_menu

Updates an existing menu's label or description. At least one of `label` or `description` must be provided.

- **Input:**
  - `id` (string, **required**): menu machine name.
  - `label` (string, max 255): new label.
  - `description` (string): new description.
- **Response:** `{success, message, violations, id, label}` carrying the updated label (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### create_menu_item

Creates a menu link. The item is saved as an auto-save draft. Call [`publish_auto_saves`](#publish_auto_saves) with the returned `autosave_key` to make it visible in menus.

- **Input:**
  - `menu_name` (string, **required**): menu machine name (e.g. `main`, `footer`).
  - `title` (string, **required**, max 255): link title.
  - `link` (string, **required**): link URI: `internal:/path`, `https://example.com`, `entity:node/1`, or `route:route.name`.
  - `parent` (string): parent menu link plugin ID (e.g. `menu_link_content:<uuid>`).
  - `weight` (integer): ordering weight; lower first (default 0).
  - `enabled` (boolean, default `true`) · `expanded` (boolean, default `false`) · `description` (string): optional tooltip text.
- **Response:** `{success, uuid, id, title, menu_name, autosave_key, message, violations, warnings}`; `autosave_key` has the form `menu_link_content:<id>:<langcode>` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### update_menu_item

Updates a menu item by entity ID. Partial update: only provided fields change; use `weight` to reorder. Changes are saved as an auto-save draft; publish them with [`publish_auto_saves`](#publish_auto_saves).

- **Input:**
  - `id` (integer, **required**): menu link content entity ID (from `drupal://menus/{menu_name}`).
  - `title` (string) · `link` (string) · `parent` (string; empty string removes the parent) · `weight` (integer) · `enabled` (boolean) · `expanded` (boolean) · `description` (string).
- **Response:** same shape as `create_menu_item`, with a draft-state warning while the item is unpublished (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### delete_menu_item

Deletes a menu item by entity ID. One of the catalog's two delete tools (with [`delete_canvas_page`](#delete_canvas_page)). Nodes, media, and terms have no MCP delete.

- **Input:**
  - `id` (integer, **required**): menu link content entity ID.
  - `delete_children` (boolean, default `false`): when `false`, existing children are re-parented to this item's parent; when `true`, all descendants are also deleted.
- **Response:** `{success, id, title, children_deleted, message}` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Media tools

### create_media

Creates a file-based media entity (any file-based media type: `image`, `document`, `video`, …) and returns a **signed upload URL**. Upload the file afterwards with `curl -X PUT <upload_url> -H "Content-Type: application/octet-stream" --data-binary @/path/to/file`; the upload attaches the file and publishes the entity automatically. Reference the media in Canvas component props as the scalar MID (e.g. `42`), and in Drupal entity reference fields as `{"target_id": <mid>}`. Don't fetch the file URL from the media entity.

- **Input:**
  - `bundle` (string, **required**): media type machine name; discover via `drupal://media-types`.
  - `name` (string, **required**): media name.
  - `filename` (string, **required**): target filename with extension (e.g. `photo.jpg`).
  - `metadata` (object): source-field metadata (e.g. `{"alt": "Image description", "title": "Image title"}` for images).
  - `fields` (object): additional field values keyed by field name; see `drupal://media-types/{bundle}`.
- **Response:** `{mid, upload_url, method: "PUT", expires_in: 300, message}`; the entity stays an unpublished draft until the file is uploaded, and the upload URL expires in 300 seconds (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### create_remote_video

Creates a remote-video media entity from an OEmbed URL (YouTube, Vimeo, …). Created and published immediately.

- **Input:**
  - `bundle` (string, **required**): media type machine name (e.g. `remote_video`); any OEmbed-based bundle.
  - `url` (string, **required**): the OEmbed URL; must be from a supported provider.
  - `name` (string): media name; if omitted, Drupal auto-fetches the title from the OEmbed provider.
  - `fields` (object): additional field values keyed by field name.
- **Response:** `{mid, url, message}`; created and published in one step (from a YouTube URL).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### create_dam_media

Creates a DAM-backed media entity by referencing an existing Acquia DAM asset: only the `asset_id` reference is stored; no file upload. Works only with DAM media types (`dam_type: true` in `drupal://media-types`).

- **Input:**
  - `bundle` (string, **required**): a DAM-backed media type machine name.
  - `asset_id` (string, **required**): the Acquia DAM asset UUID; the asset must already exist in DAM.
  - `name` (string): media name; defaults to the asset filename from DAM.
  - `version_id` (string): optional DAM asset version UUID; empty uses the latest finalized version (the DAM CDN always serves the current published asset regardless).
  - `external_id` (string): optional DAM external ID.
- **Response:** `structuredContent` carries `mid` (integer), `url` (string, the media entity's absolute URL), and `message` (string), all three required. On a fresh import the message reads `DAM media entity {mid} created and published successfully (asset_id: {asset_id}).`; the tool is idempotent, so a repeat call for an already-imported asset returns the existing entity with message `DAM media entity {mid} already exists for asset_id {asset_id}, returning existing entity.` (shape from the tool's `outputSchema` and kernel test against the DAM fixture, not a live DAM tenant). If the referenced asset is missing or not publicly available in DAM, the just-created entity saves unpublished, is deleted, and the tool returns `code: "validation"`. On a site without the DAM integration the call fails with tool-level error `code: "configuration"`, message `Acquia DAM is not configured on this site…`.
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Site settings tools

### update_site_settings

Updates site settings such as the site name. Changes are staged and take effect when auto-saves are published.

- **Input:**
  - `site_name` (string, **required**, min length 1): the new site name.
- **Response:** `{success, message, violations, site_name, already_set, staged}`. When the requested name already matches, the tool is a no-op: `already_set: true`, `staged: false`, and no auto-save is created. A real change instead stages an auto-save (`staged: true`), applied by [`publish_auto_saves`](#publish_auto_saves).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### update_site_logo

Uploads a new site logo and updates the theme configuration. Like [`create_media`](#create_media), it returns a signed upload URL: upload with `curl -X PUT <upload_url> -H "Content-Type: application/octet-stream" --data-binary @/path/to/logo.png`, after which the logo is immediately live.

- **Input:**
  - `filename` (string, **required**): filename with extension. Supported formats: `png`, `jpg`, `jpeg`, `gif`, `svg`, `webp`, `ico`, `apng`.
- **Response:** `{upload_url, method: "PUT", expires_in: 300, message}`: the same signed-upload envelope as [`create_media`](#create_media). Until a file is PUT to `upload_url`, nothing changes; the URL expires in 300 seconds.
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### set_homepage

Sets a Canvas page as the site homepage. If the page is unpublished, a published draft is staged. Takes effect when auto-saves are published.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
- **Response:** `{success, message, violations, page_id, page_title, page_url, auto_published, is_homepage, homepage_staged}`. When the page is already the homepage, the tool is a no-op: `is_homepage: true`, `homepage_staged: false`. Setting a new homepage stages an auto-save (`homepage_staged: true`), and an unpublished target is auto-published.
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Canvas page and component tools

Layout edits on Canvas pages write to the page's **draft** (auto-save) channel. Make a page or its changes live with the [draft and publishing tools](#draft-and-publishing-tools).

### create_canvas_page

Creates an unpublished Canvas page draft with optional metadata.

- **Input:**
  - `title` (string, **required**, min length 1): page title.
  - `path` (string): optional URL alias, e.g. `/acquia-engage`.
  - `description` (string): optional page description.
- **Response:** `{success, message, violations, page_id, revision_reference, page}`; `page` is the `canvas://pages` entry shape with both channels: `live.status: false` and a `draft` carrying `revisionId`, `draftUrl`, and `dataHash` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### update_canvas_page

Updates page metadata (title/path/description/status) on an existing Canvas page draft. This changes the auto-save `data_hash`: if you publish via `publish_auto_saves` afterwards, use the `page.draft.dataHash` from this tool's response, not a cached value.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `title` (string) · `path` (string) · `description` (string): optional updated metadata.
  - `status` (boolean): optional publish status: `true` publishes the page when the draft is published; `false` keeps it unpublished.
- **Response:** the same page envelope; the draft's `dataHash` changes with every metadata update (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### delete_canvas_page

Deletes a Canvas page by ID. One of the catalog's two delete tools (with [`delete_menu_item`](#delete_menu_item)). Nodes, media, and terms have no MCP delete.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `force` (boolean, default `false`): force deletion when `true`.
- **Response:** `{success, message, violations, page_id, deleted: true}` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### get_page_layout

Returns the native Canvas client-side layout/model representation for a page.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
- **Response:** `{success, message, violations, page_id, revision_reference, layout: {layout, model}}`: `layout` is the component tree (nodes of `{uuid, nodeType, type: "js.text@<version>", name, slots}`) and `model` maps instance UUIDs to per-prop sources (`{sourceType, expression}`) (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### set_page_layout

Sets the native Canvas layout/model payload on the page's draft channel, replacing the current draft layout.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `layout` (object, **required**): native Canvas layout/model payload (same format as `get_page_layout` returns).
  - `base_revision_id` (integer or string): optional optimistic-lock token; a stale value returns a conflict.
- **Response:** `{success, message, violations, page_id, layout}` echoing the saved layout; accepted with `base_revision_id` set to the `revision_reference` from `get_page_layout` (captured).
- **Errors:** [standard errors](#errors); conflict when `base_revision_id` is stale.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### add_component_to_page

Adds a component instance at a slot/index target in a page's draft layout. Available components and their IDs come from `canvas://components`.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `component_id` (string, **required**): component ID to insert.
  - `props` (object): component props (static source only for Alpha).
  - `parent_instance_id` (string): instance ID of the parent component to nest inside; omit for top-level placement.
  - `slot` (string): target slot name on the parent; required when `parent_instance_id` is set (discover slot names via `get_page_layout` or `canvas://components/{id}`).
  - `index` (integer): insertion position within the page layout or the slot.
- **Response:** `{success, message, violations, page_id, new_instance_id, layout}`; `layout` echoes the full updated draft layout/model (captured).
- **Errors:** [standard errors](#errors); `-32602` for a component ID not listed by `canvas://components`.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### batch_add_components_to_page

Adds multiple component instances to a page's draft layout in one call. All-or-nothing: if any component fails validation, the whole batch is rejected, no draft is written, and every per-component error is returned in `failed_components` so you can fix them all and retry.

Omit `index` on every item to append in batch order. Parents being created in the same batch get a `temp_id` starting with `@` that children reference via `parent_instance_id` (e.g. `{"temp_id": "@hero", …}` then `{"parent_instance_id": "@hero", "slot": "body", …}`). Each parent must appear before its children.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `components` (array of objects, **required**, min 1): each item: `component_id` (**required**), plus optional `props`, `temp_id` (`^@.+`), `parent_instance_id`, `slot`, `index`.
- **Response:** on success `{success, message, violations, page_id, total, successful, failed, new_instance_ids, component_results, layout}`; each `component_results` entry is `{index, component_id, instance_id, parent_instance_id, slot}` (captured).
- **Errors:** [standard errors](#errors); validation failures list every failing component in `failed_components`.
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### move_component

Moves a component instance to a new slot/index target in the draft layout.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `instance_id` (string, **required**): component instance ID to move.
  - `parent_instance_id` (string): target parent component instance ID.
  - `slot` (string): target slot name under the parent.
  - `index` (integer): target index in the region/slot component list.
- **Response:** `{success, message, violations, page_id, instance_id, layout}` with the full layout echo (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### remove_component

Removes a component instance (including nested descendants) from the draft layout.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `instance_id` (string, **required**): component instance ID to remove.
- **Response:** `{success, message, violations, page_id, removed_instance_id, layout}` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### update_component_props

Partially updates the props of a component instance in a page's draft layout.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
  - `instance_id` (string, **required**): component instance ID to update.
  - `props` (object, **required**): partial props patch (static source only for Alpha). Prop names and types for a given component come from `canvas://components/{id}`.
- **Response:** `{success, message, violations, page_id, instance_id, layout}` (captured).
- **Errors:** [standard errors](#errors); `-32602` for prop values that don't match the component's declared types.
- **Used in:** [MCP quickstart](/source-cms/ai-agents/quickstart/) · [MCP guide](/source-cms/ai-agents/guide/)

### get_page_region_layout

Returns the current layout of a global Canvas page region (e.g. header or footer). Regions are shared across all pages for the active theme.

- **Input:**
  - `region_id` (string, **required**): page region config entity ID (e.g. `astral.header`); list regions via `canvas://page-regions`.
- **Response:** `{success, message, violations, region_id, layout: {layoutVersion, regions: [{id, components}], componentCount}}` (captured for `astral.header`).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### set_page_region_layout

Replaces the full layout of a global Canvas page region (e.g. header or footer).

- **Input:**
  - `region_id` (string, **required**): page region config entity ID; list regions via `canvas://page-regions`.
  - `layout` (object, **required**): layout payload in the same format `get_page_region_layout` returns.
- **Response:** the same shape, echoing the saved region layout (captured). Verified: region layout edits apply directly to the region; they do not appear in `canvas://auto-saves`.
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### add_component_to_page_region

Adds a component instance to a global Canvas page region (e.g. header or footer).

- **Input:**
  - `region_id` (string, **required**): page region config entity ID; list regions via `canvas://page-regions`.
  - `component_id` (string, **required**): component ID to insert.
  - `props` (object): component props (static source only for Alpha).
  - `parent_instance_id` (string) · `slot` (string) · `index` (integer): optional placement, as in `add_component_to_page`.
- **Response:** `{success, message, violations, region_id, new_instance_id, layout}`; region components carry `{instanceId, componentId, name, props, region, parent, slot, index, slots}` with resolved `props` values, unlike the page `model` representation (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Draft and publishing tools

### publish_canvas_page

Sets a Canvas page to published (publicly visible) and commits all pending draft changes. Use it to make a page live; to commit layout or content changes on an already-published page without changing its visibility, use [`publish_auto_saves`](#publish_auto_saves) instead.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
- **Response:** the page envelope after publishing: `live.status: true`, `draft: null`, `hasDraftChanges: false` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### publish_auto_saves

Publishes one or more auto-saved drafts to their canonical entities: Canvas layout edits, menu items, staged site settings.

- **Input:**
  - `autosaves` (array of objects, **required**): each entry **requires** `autosave_key` (string) and `data_hash` (string), both from the `canvas://auto-saves` resource; the hash verifies you are publishing the version you expect.
- **Response:** `{success, message, violations, results: [{autosave_key, success, message}]}` (captured).
- **Errors:** [standard errors](#errors); a stale `data_hash` is rejected: re-read `canvas://auto-saves` (or use the `dataHash` returned by the tool that changed the draft).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### discard_auto_saves

Discards one or more auto-saved drafts without publishing. Accepts individual keys for granular discard.

- **Input:**
  - `autosave_keys` (array of strings, **required**): auto-save keys from the `canvas://auto-saves` resource.
- **Response:** `{success, message, violations, results: [{autosave_key, success, message}]}` (captured). Verified caveat: discarding the auto-save of a newly *created* menu item leaves the disabled entity behind; remove it with [`delete_menu_item`](#delete_menu_item).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

### get_canvas_preview_signed_url

Generates a signed URL for previewing a Canvas page's draft/auto-save without a Drupal login session. The URL is preview-only and expires in 5 minutes.

- **Input:**
  - `page_id` (integer, **required**, ≥ 1): Canvas page entity ID.
- **Response:** `{success, message, violations, preview_url, expires_at, expires_in_seconds: 300}` (captured).
- **Errors:** [standard errors](#errors).
- **Used in:** [MCP guide](/source-cms/ai-agents/guide/)

## Used in

- [MCP quickstart](/source-cms/ai-agents/quickstart/): enable the server and make a first tool call with your coding agent.
- [MCP server integration guide](/source-cms/ai-agents/guide/): client setup patterns, resource browsing, and known limits.
- [MCP guide, govern the agent's access](/source-cms/ai-agents/guide/#govern-the-agents-access): the scope, revocation, and accountability controls around this connection spec.
- [Authentication guide](/source-cms/authenticate/guide/): the OAuth flow behind the connection spec.
