# MCP tools for campaigns and page elements

MCP campaign arguments, Smart Match targeting, safe change updates, live performance results, errors, and draft boundaries.

Available on the Grow, Scale, and Pro plans.

Use these tools to inspect page elements, build audience-specific website changes, and read campaign results. Configuration reads can show an unpublished draft. Performance reports measure the live campaign.

## MCP campaign access and draft changes

All tools accept optional `project_id` (string). Omission uses the connection's active project or an accessible project the server can resolve. Pass it explicitly when working across projects.

The tools follow the [shared MCP authorization and project-selection rules](https://rightmessage.com/help/mcp-authorization-reference). Grow includes MCP access. Campaign personalization and goals require Scale or above to publish. Creating a draft through MCP does not override that publishing requirement.

Create, update, duplicate, and delete operate on the draft. They save a configuration snapshot, validate the result, and update the draft. None publishes or changes a live page immediately. Resource reads use the draft first, otherwise the published configuration. Shared failures include missing or inaccessible projects, missing entitlement, and configuration validation errors. See [publication, backups, and undo](https://rightmessage.com/help/mcp-tools-publishing) before making changes live.

Examples are illustrative, not executed. Requests show the tool-call name and arguments. JSON results show the content, not the MCP transport envelope. Excerpts are labeled.

## Campaign write arguments

Creation and update accept the following fields. Creation requires `name`; update requires `campaign_id` and at least one changed field.

| Argument    | Type and behavior                                                                                                                                                                                                                                                                |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | String. Internal campaign name.                                                                                                                                                                                                                                                  |
| `is_active` | Boolean. Creation default: `true`.                                                                                                                                                                                                                                               |
| `base_url`  | String. Campaign base URL. Creation trims it and adds `https://` if no HTTP scheme is present. Creation default: null. Update does not perform that normalization.                                                                                                               |
| `testing`   | Object. `is_enabled` is boolean; creation default `true`. `withhold` is a numeric control-group percentage; use 0–100. Creation defaults it to 10, converts numeric input to an integer, and replaces nonnumeric input with 10. The saved schema does not enforce a 0–100 bound. |
| `goal_ids`  | Array of goal-ID strings, default `[]` on creation. Order matters for `primary_goal`.                                                                                                                                                                                            |
| `outcome`   | `any_goal` or `primary_goal`. Creation default `any_goal`. Legacy campaigns without the field use `primary_goal`.                                                                                                                                                                |
| `changes`   | Array of change objects described below. Omission creates an empty campaign or preserves an existing campaign's variants.                                                                                                                                                        |

`any_goal` counts a visitor once when they convert on any attached goal. `primary_goal` uses the first ID in `goal_ids`. Use [goal tools](https://rightmessage.com/help/mcp-tools-goals) to create or inspect those goals.

On update, supplied top-level fields replace the corresponding stored fields; omitted fields remain. `testing` replaces the object rather than deep-merging it, although an omitted or nonnumeric `withhold` keeps the stored percentage. Save-time repair resets an omitted or nonboolean `is_enabled` to `true`, even if testing was previously off. Send both `is_enabled` and `withhold` when changing testing. Top-level null arguments are ignored, not a way to clear a field.

### Each `changes` item

| Property                        | Type and behavior                                                                                                                                                                                          |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                            | Existing action ID, used on update to merge into that action. Omit for a new action.                                                                                                                       |
| `variant_id`                    | Existing variant ID returned with `id`. Keep it to distinguish actions whose IDs repeat across variants.                                                                                                   |
| `segment_ids`                   | Comma-separated string, **not an array**. Keep IDs in the same order across new changes that should share a variant. Empty string selects the default/fallback variant.                                    |
| `selector`                      | CSS-selector string. Use unless supplying `targeting`.                                                                                                                                                     |
| `targeting`                     | Smart Match object. Prefer the complete object returned by `get-page-elements`.                                                                                                                            |
| `page`                          | Comma-separated `domain/path` patterns, such as `example.com/pricing, example.com/demo`. No URL scheme. Omission on a new action means all pages; omission on an existing action preserves its page rules. |
| `content`                       | Replacement string, or newline-separated `/find/replace/` expressions for partial text changes.                                                                                                            |
| `visibility`                    | `show` or `hide`.                                                                                                                                                                                          |
| `style`                         | Object mapping CSS properties to overrides.                                                                                                                                                                |
| `attr`                          | Object mapping HTML attributes to overrides.                                                                                                                                                               |
| `add_classes`, `remove_classes` | Comma-separated CSS-class strings.                                                                                                                                                                         |

A domain and its `www.` form match each other. Other subdomains must be targeted explicitly. `*/blog*` targets matching paths on any domain. Each page token becomes `{domain, path}`; a token without a slash gets path `*`.

The `changes` array is a replacement list, not an append operation. On update:

- Start with the complete `changes` returned by `get-campaign`.
- Keep every action you want to retain. Unrepresented actions are removed; variants emptied by that removal are removed too. Variants that already had no actions are retained.
- An existing `id` plus `variant_id` merges supplied properties into the stored action. Omitted action properties survive. Explicit null removes targeting or modification values; null `selector` becomes an empty string, and null `page` resets targeting to all domains and paths.
- Matching segment sets retain the existing variant ID and name. An unchanged round-trip retains the stored campaign content.
- A change without `id` creates a new action.

Do not reconstruct the update from the presentation-oriented `variants` field. Its long text can be truncated.

Resolve every segment ID before saving. New-variant construction silently omits unknown segment IDs from its rules; it does not reliably reject them. A variant whose IDs are all unknown can end up unrestricted. New variants group by the supplied ID order, while matching an existing variant compares the segment set without regard to order.

### Smart Match object

`targeting` requires `mode`: `fingerprint` or `selector`. It can contain `fallbackSelector` and `selector` strings. A `fingerprint` object requires `text`; its nested fields are:

| Field       | Schema                                                                                                                                                                                  |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`      | Required `content` string; optional `matchType`: `exact`, `contains`, or `startsWith`, default `exact`.                                                                                 |
| `element`   | `tagName` string and `candidateTags` string array, default `[]`.                                                                                                                        |
| `structure` | Integer `siblingIndex`, `siblingCount`, and `depth`; string `parentChain`.                                                                                                              |
| `semantics` | Nullable strings `id`, `ariaRole`, `ariaLabel`; nullable object `dataAttributes`; string array `meaningfulClasses`, default `[]`.                                                       |
| `content`   | Integer `charCount` and `wordCount`; string `textPrefix`.                                                                                                                               |
| `visual`    | `fontSize: {min, max}`, `fontWeight: {min}`, and `positionY: {min, max}`, with numeric bounds; boolean `isAboveFold`. These signals are not returned by the server-side page extractor. |

Preserve the returned fingerprint instead of guessing it. Identical text on two elements still requires two actions, because their structure signals distinguish the elements.

## Returned campaign object

Create, update, and duplicate return the saved campaign, including:

- `id` (`cpn_…`), `name`, `is_active`, `base_url`, `goal_ids`, `outcome`, and `testing`.
- Configuration fields `recipe_code`, `dimension_id`, and `condition`; new campaigns use null, null, and true respectively.
- `variants`: each has `id`, `name`, `rules`, and `actions`. Segment rules use `rules.all` entries with `dimensionId` and `segment`; a default variant has no segment restrictions.
- Each action includes its `id`, target/selector, page-rule array, and `modifications`. Modifications can contain `text`, `findReplace` entries `{find, replace}`, `visibility`, `style`, `attr`, and `classes` with `add`/`remove` arrays.

Create and update also add `unique_variant_count` when it is greater than zero. This is the number of segment combinations across the dimensions used in variant rules, not simply the length of `variants`.

## `get-page-elements`

Fetch a page's rendered HTML and identify elements available for personalization. Requires `url` (full HTTP or HTTPS URL). Optional `no_cache` (boolean, default `false`) bypasses the 24-hour HTML cache. Optional `project_id` uses the shared contract.

Returns `{url, title, element_count, elements_capped, elements}`. `element_count` is the total found before the 200-element cap; `elements_capped` indicates truncation. Each element contains uppercase `type`, `content`, `selector_stable`, `text_unique`, and `targeting`.

The extractor includes headings, paragraphs, buttons, links, and list items with at least three bytes of text. It skips scripts, styles, noscript, SVG, head, navigation, and footer subtrees. A header can still contain eligible content. Fingerprint parent chains contain at most five ancestors, while `depth` describes the full depth. Semantic hints omit empty values, skip internal `data-rm-*` attributes, and keep at most three meaningful classes.

This is read-only with respect to RightMessage configuration, but it makes an external page-fetch request through Firecrawl and can populate its cache. It does not install a snippet or edit the page.

Invalid URLs return `Invalid URL format. Provide a full URL including https://`. Fetch failures return `Failed to fetch page HTML: …`, or an error saying the page may be unreachable or Firecrawl may not be configured.

Illustrative request and result:

```json
{"name":"get-page-elements","arguments":{"url":"https://example.com/pricing","no_cache":false}}
```

```json
{"url":"https://example.com/pricing","title":"WorkForge pricing","element_count":1,"elements_capped":false,"elements":[{"type":"H1","content":"WorkForge pricing","selector_stable":true,"text_unique":true,"targeting":{"mode":"fingerprint","fingerprint":{"text":{"content":"WorkForge pricing","matchType":"exact"},"element":{"tagName":"h1","candidateTags":["h1","div","span","p"]},"structure":{"siblingIndex":0,"siblingCount":1,"parentChain":"BODY > MAIN","depth":2},"semantics":{"id":"pricing-title"},"content":{"charCount":17,"wordCount":2,"textPrefix":"WorkForge pricing"}},"fallbackSelector":"#pricing-title"}}]}
```

## `list-campaigns`

List compact campaign summaries. The only argument is optional `project_id`. No configuration changes occur.

Returns an array of `{id, name, is_active, variantCount, goalCount, abTestSummary}` and `outcome` when that field exists. `abTestSummary` is a string such as `10% control group`, or null when testing is off. `variantCount` counts saved variants, not all possible combinations. Shared access errors apply; an empty configuration returns `[]`.

Illustrative request and result:

```json
{"name":"list-campaigns","arguments":{}}
```

```json
[{"id":"cpn_workforge","name":"WorkForge pricing","is_active":true,"variantCount":1,"goalCount":1,"outcome":"any_goal","abTestSummary":"10% control group"}]
```

## `get-campaign`

Read a campaign and its lossless editable changes. Requires `campaign_id` (string); accepts optional `project_id`.

Returns the campaign object, plus `changes` in the write format above. Presentation fields add `isDefault` to variants, `pageFormatted` to actions, and resolved names alongside known IDs. Modification strings in `variants` are shortened after 100 bytes and can include phrase annotations. The separate `changes` array is untruncated and preserves empty JSON objects.

Missing ID returns `campaign_id is required.` Unknown ID returns `Campaign '…' not found.` No configuration changes occur.

Illustrative request and result excerpt:

```json
{"name":"get-campaign","arguments":{"campaign_id":"cpn_workforge"}}
```

```json
{"id":"cpn_workforge","name":"WorkForge pricing","changes":[{"id":"act_title001","variant_id":"var_default1","segment_ids":"","selector":"#pricing-title","targeting":null,"page":"example.com/pricing","content":"Choose your WorkForge plan","visibility":null,"style":null,"attr":null,"add_classes":null,"remove_classes":null}]}
```

## `create-campaign`

Create a campaign in the draft. Requires `name` and accepts the campaign write arguments plus optional `project_id`.

Returns the saved campaign object. Without `changes`, it creates no variants. Invalid stored values can reject the save, but unknown segment IDs can be dropped rather than rejected, as described above. Missing name returns `name is required.` Shared save and plan boundaries apply.

Illustrative request and result excerpt:

```json
{"name":"create-campaign","arguments":{"name":"WorkForge pricing","base_url":"https://example.com/pricing","goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10},"changes":[{"selector":"#pricing-title","segment_ids":"","page":"example.com/pricing","content":"Choose your WorkForge plan"}]}}
```

```json
{"id":"cpn_workforge","name":"WorkForge pricing","is_active":true,"base_url":"https://example.com/pricing","goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10}}
```

Use the Smart Match `targeting` from `get-page-elements` when available; the shorter selector example assumes a known stable ID.

## `update-campaign`

Change a draft campaign. Requires `campaign_id` (string) and at least one campaign write argument. Optional `project_id` uses the shared contract.

Returns the saved campaign object. A supplied `changes` list can remove actions and variants, so retain the complete list you intend to keep. To rename a campaign, omit `changes` entirely. Unknown ID returns `Campaign '…' not found.` Missing fields return `At least one field to update must be provided.` Invalid configuration returns an error rather than a saved result.

Illustrative request and result excerpt:

```json
{"name":"update-campaign","arguments":{"campaign_id":"cpn_workforge","name":"WorkForge pricing headline"}}
```

```json
{"id":"cpn_workforge","name":"WorkForge pricing headline","is_active":true,"base_url":"https://example.com/pricing","goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10}}
```

## `duplicate-campaign`

Copy a campaign into the draft. Requires `campaign_id` (string). Optional `new_name` defaults to the original name plus ` (Copy)`; optional `project_id` uses the shared contract.

Returns the copied campaign with a fresh campaign ID and fresh variant/action IDs. Goals and segment references remain references to the existing resources. The copy retains the campaign's configuration; it is not a copy of historical performance. Nothing is published.

Missing ID, an unknown campaign, or invalid copied configuration returns an error.

Illustrative request and result excerpt:

```json
{"name":"duplicate-campaign","arguments":{"campaign_id":"cpn_workforge","new_name":"WorkForge pricing alternative"}}
```

```json
{"id":"cpn_alternate","name":"WorkForge pricing alternative","is_active":true,"goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10}}
```

## `delete-campaign`

Remove a campaign from the draft. Requires `campaign_id` (string); accepts optional `project_id`.

Returns `{deleted: true, campaign_id}`. It removes the campaign configuration, not the goal resources or previously recorded events. The live campaign is unaffected until publication. Missing ID returns `campaign_id is required.` Unknown ID returns `Campaign '…' not found.`

Illustrative request and result:

```json
{"name":"delete-campaign","arguments":{"campaign_id":"cpn_alternate"}}
```

```json
{"deleted":true,"campaign_id":"cpn_alternate"}
```

## `get-campaign-performance`

Read live campaign results for an inclusive local-date range. This tool does not measure draft-only changes and does not alter a test.

| Argument      | Type and behavior                                                                                                      |
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `date_from`   | Required string: `YYYY-MM-DD` or ISO 8601 timestamp. The date is taken in `timezone`.                                  |
| `date_to`     | Required string in the same format. Includes that local date. Must not precede `date_from`; maximum 90 inclusive days. |
| `timezone`    | IANA timezone string, default `UTC`.                                                                                   |
| `campaign_id` | Optional string to filter returned campaign rows.                                                                      |
| `project_id`  | Optional string under the shared access contract.                                                                      |

Range figures count unique visitors. A conversion must follow exposure in the same arm. Visitors exposed to both arms are excluded. The selected range's descriptive lift is not a fresh experiment decision.

### Response states

A cold or stale computation can return:

```json
{"status":"computing","retry_after_seconds":5,"message":"Campaign results are computing. Call get-campaign-performance again with the same arguments after 5 seconds."}
```

Call again with the same arguments after the stated delay. This queues or reads reporting work; it does not publish.

Other responses have `status`, `period` (`{start, end, timezone}` or null), `computedAt` (timestamp or null), and `stale` (boolean). Non-ready states include `capped`, `not_live`, and `unavailable`; they add `reason` and `totals`. Missing data is not zero.

A ready response adds:

| Field              | Contract                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `headline`         | Portfolio comparison: `status`, `basis`, `relativeLift`, `observedLift`, `campaignCount`, `basisCounts`, `visitors`, `reason`, `display`, and `text`. `basis` is `holdback`, `proven`, `baseline`, `mixed`, or null. `basisCounts` maps the first three bases to counts. `display` has `value` and `label` strings. The headline is visitor-weighted across measured campaigns. |
| `experimentCounts` | Counts keyed by experiment state. This is separate from the date-range measurement.                                                                                                                                                                                                                                                                                             |
| `campaigns`        | Array of campaign result objects described below.                                                                                                                                                                                                                                                                                                                               |
| `notLive`          | Array of `{id, name, reason}` for `draft_only` or `inactive` campaigns. A `draft_goals` entry also contains `goalIds`.                                                                                                                                                                                                                                                          |
| `totals`           | `visitorsReached` and `goalConversions` measurement objects, not bare numbers.                                                                                                                                                                                                                                                                                                  |

Filtering by `campaign_id` filters `campaigns`, `notLive`, and the totals; the portfolio headline and experiment counts are assembled before that filter.

Each campaign result has `id`, `name`, `testingEnabled`, `holdbackPercent`, `daysOfData`, `exposedVisitorsPerDay`, `visitors: {personalized, holdback}`, `excludedBothArmVisitors`, `status`, `statusLabel`, `statusReason`, `measuredFrom`, `goals`, `outcome`, `comparison`, and `experiment`.

`goals` is an array; `outcome` is a goal-shaped object or null. Each contains `id`, `name`, `primary`, `personalized`, `holdback`, `lift`, `relativeLift`, `ci95`, `pValue`, `status`, `statusLabel`, and `statusReason`. Both arms contain `visitors`, `converters`, and nullable `conversionRate`. Rates and `relativeLift` are proportions, not percentages. `ci95` is `{lower, upper}` or null. Null lift means it cannot be measured, not no effect.

`comparison` is null or `{basis, label, lift, relativeLift, rate, reference}`:

- `holdback` compares personalized visitors with the control group in the selected range.
- `proven` is a prior A/B result retained after rollout, not a remeasurement of this range. Its `rate` and `reference` are null.
- `baseline` compares same-page visitors who did not see the campaign, weighted to the personalized group's engagement and known-contact mix. It is not a controlled test.

When present, `reference` has `visitors`, `converters`, and `conversionRate`. Per-goal status text still describes holdback diagnostics even when the campaign comparison uses another basis.

`experiment` is null or the separately persisted decision: `state`, `label`, `versionNumber`, `startedAt`, `seriesStartWeek`, `provenance`, `metricId`, `lookNumber`, `cutoffAt`, `decidedAt`, `arms`, `observedLift`, `decidedLift`, `decidedVisitors`, `haltReason`, and `proven`. Timestamps, metrics, arm results, and lift values can be null when there is not enough decision-grade data. Experiment arms use enrollment and converter counts rather than the range's exposed-visitor totals. `proven`, when present, contains `state`, `label`, `versionNumber`, `startedAt`, `endedAt`, `decidedAt`, `metricId`, `observedLift`, `decidedLift`, `decidedVisitors`, and `unchanged`.

Experiment `state` is `collecting`, `winner`, `loser`, `inconclusive`, `halted`, `not_decision_grade`, or `not_measurable`; those are also the `experimentCounts` keys. When available, `arms.personalized` and `arms.holdback` contain `enrolled`, `converters`, and nullable `pageviews`. `decidedVisitors` is the combined enrolled-visitor count at the decision, not a visitor list. `haltReason` is `srm`, `contamination`, or null.

Each `totals` measurement uses one of three shapes:

- Ready: `{status: "ready", value: number, computedAt: string, stale: boolean}`.
- Computing: `{status: "computing", retryAfterSeconds: number}`.
- Unavailable: `{status: "unavailable", reason: string, explanation: string, availableFrom: string|null}`. There is no numeric `value` in this state.

### Errors and example

Invalid timezone returns `timezone must be a valid IANA timezone.` Too many days returns `The date range must be no more than 90 inclusive days.` Invalid or reversed dates return an error describing the accepted date formats. A specific campaign absent from both live and draft/inactive entries returns a not-found error. Other reporting failures return `Failed to fetch campaign performance for project …`.

Illustrative request and result excerpt from a ready response:

```json
{"name":"get-campaign-performance","arguments":{"campaign_id":"cpn_workforge","date_from":"2026-10-01","date_to":"2026-10-07","timezone":"UTC"}}
```

```json
{"status":"ready","period":{"start":"2026-10-01","end":"2026-10-07","timezone":"UTC"},"computedAt":"2026-10-08T09:00:00Z","stale":false,"campaigns":[{"id":"cpn_workforge","name":"WorkForge pricing","testingEnabled":true,"holdbackPercent":10,"visitors":{"personalized":900,"holdback":100},"excludedBothArmVisitors":0,"comparison":{"basis":"holdback","label":"vs holdback","lift":"+20%","relativeLift":0.2,"rate":0.12,"reference":{"visitors":100,"converters":10,"conversionRate":0.1}}}],"notLive":[]}
```
