# Migration guide

> Move from today's 13 MCP tools and the RunExtract endpoint to v2. A table maps every old call to its replacement, with worked before and after examples.

Source: https://emailmovers.com/mydatahub/docs/migrate
Updated: 16 October 2026

Every one of today's 13 MCP tools has a replacement among the 22 new ones, and the legacy download endpoint has one too. The plan is a 12 month window from the v2 launch, with 3 emailed notices, in which today's tools, keys and installs keep working while you move. Installs at today's MCP URL, `https://mcp.mydatahub.uk`, are planned to keep working: it passes MCP through to the new server at `https://mydatahub.emailmovers.com/mcp` for at least 12 months, with no redirect, and credit balances and saved lists are planned to carry over.

> **Not yet live.** MyDataHub v2 is built and tested locally and is not yet live. The 12 months run from the launch date, which is to confirm. Until then, today's tools and endpoints are the current product.

> **To confirm.** The old MCP tools are built into the v2 server and answer as the table below says. Three other parts of the plan are not built, so do not rely on them yet.

- The `X-Api-Key` header and `POST /api/RunExtract`. The v2 API does not accept the header, has no such route and sends no `Deprecation` or `Sunset` header.
- Tokens from today's OAuth issuer. The v2 API refuses any bearer token that is not an `mdh_` key.
- The redirect from `agents.mydatahub.uk` to this site, page for page.

Moving traffic to v2 without them would break today's integrations and installs. The launch has to leave today's endpoints where they are until they exist (to confirm).

## What changes

| Today | After migration, as planned |
| --- | --- |
| 13 MCP tools that hold a session on the server | 22 tools where every call carries its own audience |
| Numeric `structureId` values | Field names such as `company.employee_band`, from `list_filters` |
| `incEx` codes 0, 1 and 2 | `in` to include, and `in` under `not` to exclude |
| `create_data_extract` with `confirmed` true | `quote_order`, then `create_order` with a person's confirmation or an agent key |
| `POST /api/RunExtract` with `X-Api-Key` | `GET /v1/orders/{id}/records` with `Authorization: Bearer` |
| Credit balance | An opening ledger entry, one for one, with licence dates unchanged |
| Saved lists | Audiences with `aud_` ids, in the same organisation |
| 29 export bundles, five of them tests | Named field sets, 1 credit a record each. The test bundles are removed |
| `agents.mydatahub.uk` | This site, with a permanent redirect page for page (not built) |

The plan is for the old tools, the `X-Api-Key` header and `POST /api/RunExtract` to stay available for 12 months from the v2 launch date, with 3 emailed notices, and then to return 410, with `Deprecation` and `Sunset` headers on legacy download responses (RFC 9745 and RFC 8594). Only the old MCP tools are built so far. The v2 API has no `X-Api-Key` handling, no `/api/RunExtract` route and no deprecation headers yet, so those parts of the plan are to confirm.

## The 13 tools

The old tools are listed after the 22 new ones, with "Deprecated" in their titles and the replacement named in each description. `preview_records` is the thirteenth: the new tool keeps the name and replaces it in place, so a name appears once in `tools/list`. The last column says what each old tool does on the v2 server now.

| Today's tool | Replaced by | What the replacement does | During the window |
| --- | --- | --- | --- |
| `available_selection_categories` | [`list_filters`](https://emailmovers.com/mydatahub/docs/reference/mcp/list_filters.md) | It returns every field by name, such as company.employee_band, with its type, operators and value count. | Runs list_filters and answers with its result. |
| `available_selection_values` | [`search_filter_values`](https://emailmovers.com/mydatahub/docs/reference/mcp/search_filter_values.md) | It takes the field name and search text and returns codes with contact and company counts. | Runs search_filter_values. A structureId with no field, or an offset above 0, is refused with a next step. |
| `search_discrete_values` | [`search_filter_values`](https://emailmovers.com/mydatahub/docs/reference/mcp/search_filter_values.md) | It returns codes with counts and did_you_mean for near misses. | Runs search_filter_values with the first value as the search text. A structureId with no field is refused with a next step. |
| `create_search` | [`compile_audience`](https://emailmovers.com/mydatahub/docs/reference/mcp/compile_audience.md), then [`count_audience`](https://emailmovers.com/mydatahub/docs/reference/mcp/count_audience.md) | count_audience takes the whole audience definition in one call, so there is no session state to build up, and compile_audience turns a brief into that definition. | Counts the filters you send with count_audience. A range, or a structureId with no field, is refused with a next step. Nothing is kept for later calls. |
| `clear_search` | [`count_audience`](https://emailmovers.com/mydatahub/docs/reference/mcp/count_audience.md) | There is nothing to clear: every count_audience call carries its own audience. | Succeeds and does nothing, because there is no session to clear. |
| `get_count` | [`count_audience`](https://emailmovers.com/mydatahub/docs/reference/mcp/count_audience.md) | It returns Total, New to you and Emailable with breakdowns, quality, the compliance summary, a price estimate and a count id. | Answers an error that names count_audience, because the filters it read were held in a session that no longer exists. |
| `preview_records` | [`preview_records`](https://emailmovers.com/mydatahub/docs/reference/mcp/preview_records.md) | The new preview_records keeps the name and takes an audience or a count_id instead; a call in the old form fails validation with a next_step naming count_audience. | Not listed under its old form, because the new tool has the same name. A call in the old form fails validation with a next step that names count_audience. |
| `save_selections` | [`save_audience`](https://emailmovers.com/mydatahub/docs/reference/mcp/save_audience.md) | It saves an audience definition shared across the organisation; saved lists carry over as audiences (aud_). | Answers an error that names save_audience, because the filters it saved were held in a session that no longer exists. |
| `get_saved_selections` | [`list_audiences`](https://emailmovers.com/mydatahub/docs/reference/mcp/list_audiences.md) | Saved lists carry over as audiences (aud_) in the same organisation, with their current counts. | Runs list_audiences. An offset above 0 is refused: pass next_cursor instead. |
| `create_data_extract` | [`quote_order`](https://emailmovers.com/mydatahub/docs/reference/mcp/quote_order.md), then [`create_order`](https://emailmovers.com/mydatahub/docs/reference/mcp/create_order.md) | The flag is ignored, since a flag set by a model is never consent: the call makes a machine quote for the list, and the user confirms the spend in a form or on mydatahub.emailmovers.com, or an agent key covers it within its limits; otherwise nothing is charged and it returns the approval_url. | Ignores confirmed. It needs the aud_ id of a saved audience, counts it, makes a machine quote and goes through the same confirmation as create_order. With neither a person's approval nor an agent key, nothing is charged and it returns the approval_url. |
| `get_credits` | [`get_account`](https://emailmovers.com/mydatahub/docs/reference/mcp/get_account.md) | It returns the balance by source, the plan, per-action prices, the rate limit and any agent key budget; balances carry over one for one. | Runs get_account and answers with its result. |
| `get_packs` | [`list_field_sets`](https://emailmovers.com/mydatahub/docs/reference/mcp/list_field_sets.md) | The old bundles become named field sets with their exact columns; the five test bundles are removed. | Runs list_field_sets and answers with its result. |
| `get_pack_details` | [`list_field_sets`](https://emailmovers.com/mydatahub/docs/reference/mcp/list_field_sets.md) | list_field_sets returns every field set with its exact columns and credits per record. | Runs list_field_sets and adds that the old bundle ids became field set names. |

## What the old tools do meanwhile

During the window each deprecated tool answers as the last column above says. Nine run the tool that replaces them, three return an error that names the replacement, and `clear_search` succeeds and does nothing.

- The answer is the new tool's result, with a "Deprecated:" line first and the replacement in `_meta["uk.mydatahub/deprecated"]`.
- A numeric `structureId` is mapped to a field name (the tables below), `search` becomes `q`, and `limit` is capped at 100. An `offset` above 0 has no equivalent: pass `next_cursor` instead.
- `create_search` turns its filters into an audience and counts it. Includes become `in`, and excludes go under `not`. A range filter has no equivalent.
- `get_count` and `save_selections` read and wrote filters held in a server session, and sessions no longer exist. They return an error that names `count_audience` and `save_audience`.
- `preview_records` in the old form fails validation with a `next_step` that names `count_audience`.
- `clear_search` succeeds and does nothing, because there is nothing to clear.
- `create_data_extract` ignores `confirmed`. It makes a machine quote for the saved list and goes through the same confirmation as `create_order`. With neither a person's approval nor an agent key, nothing is charged and it returns the `approval_url`.

A retried `create_data_extract` for the same list, field set and volume reuses the live quote and the same idempotency key, so it finds the order it made and cannot buy a second batch. For another batch, call `quote_order` and then `create_order` with a new `idempotency_key`. A saved list's old numeric id has no mapping: call `list_audiences` to find its `aud_` id.

## Old ids to field names

These old `structureId` values each map to a field. Several old ids can stand for one field.

| Old `structureId` | New field |
| --- | --- |
| 1 | `site.region` |
| 2 | `site.town` |
| 3 | `site.postcode` |
| 5 | `company.turnover_band` |
| 6 | `company.employee_band` |
| 8 | `email.type` |
| 9, 20 | `company.business_class` |
| 10, 11, 12 | `company.sic_2007` |
| 14, 18, 31 | `contact.job_function` |
| 17 | `company.fleet_band` |
| 21 | `site.county` |
| 27 | `company.type` |
| 28 | `company.status` |
| 30 | `contact.job_level_rank` |

These old ids have no field. A call that names one is refused with a next step.

| Old `structureId` | What the old catalogue called it | What to use now |
| --- | --- | --- |
| 15 | `Company name` | lookup_company finds a company by its name. |
| 34 | `Email` | enrich_contact looks a person up by email address. |
| 23 | `Email universe` | The defaults, which exclude generic addresses and dissolved companies. email.exists and email.type are the filters. |
| 22 | `Phone universe` | phone.exists. |
| 24 | `Mail universe` | No equivalent field. |
| 19 | `TPS` | phone.tps_ctps_status. Whenever a phone is requested, numbers a screen found TPS or CTPS registered are left out by default; numbers not screened are kept and say so. |
| 13 | `Tag` | No equivalent field: tags were for internal use. |
| 25, 26 | `Matched jobs and Match prospects` | No equivalent field: they were internal matching references. |
| 29 | `Research` | No equivalent field: an internal research universe. |
| 36 | `Internal quality flag` | No equivalent field. |

## Old bundles to field sets

A telephone bundle becomes `standard_telephone`, the Full bundle becomes `full`, and every other bundle is delivered as `standard`. Check the columns with `list_field_sets` before you rely on one.

| Old `packId` | What the old catalogue called it | Field set |
| ---: | --- | --- |
| 545 | `Test1` | `standard`, because the test bundles are removed |
| 548 | `Standard` | `standard` |
| 549 | `Postal` | `standard` |
| 550 | `Telephone` | `standard_telephone` |
| 551 | `Postal and Telephone` | `standard_telephone` |
| 552 | `Business Class` | `standard` |
| 553 | `Job Level` | `standard` |
| 554 | `Job Function` | `standard` |
| 555 | `Full` | `full` |
| 591 | `Linked In Pack` | `standard` |
| 599 | `Job_Level_NEW` | `standard` |
| 607 | `TEST` | `standard`, because the test bundles are removed |
| 613 | `Eventbrite Pack` | `standard`, because the test bundles are removed |
| 615 | `Testtag Pack` | `standard`, because the test bundles are removed |
| 618 | `Sample` | `standard` |
| 622 | `Boost + LinkedIn` | `standard` |
| 762 | `Phone Pack` | `standard_telephone` |
| 783 | `Website Only` | `standard` |
| 822 | `Data with Tel` | `standard_telephone` |
| 824 | `Telephone Pack` | `standard_telephone` |
| 826 | `Email Pack` | `standard` |
| 837 | `STANDARD PLUS LINKEDIN` | `standard` |
| 843 | `STANDARD PLUS WEBISTE` | `standard` |
| 848 | `Skeleton` | `standard` |
| 887 | `Company Number` | `standard` |
| 906 | `Agent Pack` | `standard` |
| 936 | `84013` | `standard`, because the test bundles are removed |
| 968 | `TEL_JOB_LEVEL` | `standard_telephone` |
| 1036 | `Linkedin PLUS TEL` | `standard_telephone` |

## Before and after

The values in the old calls are illustrative. Use `list_filters` and `search_filter_values` for the codes your account sees.

### 1. Counting without a session

Before, an agent built the count up on the server, one call at a time.

1. `available_selection_categories`, to learn the numeric ids.
2. `available_selection_values` with `{ "structureId": 6 }`, to learn the codes.
3. `create_search` with the filters to include or exclude.
4. `get_count`, to read the number held in the session.

```json
{
  "selections": [
    { "structure": 6, "fieldValue": "E", "incEx": 1 },
    { "structure": 28, "fieldValue": "dissolved", "incEx": 2 }
  ]
}
```

After, one call carries everything and returns the count, the breakdowns and an id.

```json
{
  "name": "count_audience",
  "arguments": {
    "audience": {
      "schema": "mydatahub.audience/1",
      "dataset": "uk_contacts",
      "where": {
        "all": [
          { "field": "company.employee_band", "op": "gte", "value": "50-99" },
          { "not": { "field": "company.status", "op": "in", "values": ["dissolved"] } }
        ]
      }
    }
  }
}
```

There is nothing to clear and nothing to remember between calls. Dissolved companies are already excluded by default, and the exclusion above is only to show the shape. See [The audience definition](https://emailmovers.com/mydatahub/docs/audience.md).

### 2. Finding a value

Before, `available_selection_values` took a numeric id and paged with `offset`.

```json
{ "structureId": 10, "search": "manufact", "limit": 50 }
```

After, `search_filter_values` takes the field name and pages with a cursor. It returns codes with contact and company counts, and `did_you_mean` for near misses.

```json
{ "dimension": "company.sic_2007", "q": "manufact", "limit": 50 }
```

### 3. Saving and listing

Before, `save_selections` saved whatever the session held, and `get_saved_selections` listed the saved lists. After, you save a definition by name and list audiences with their current counts.

```json
{
  "name": "Manufacturers, 50 or more staff",
  "audience": {
    "schema": "mydatahub.audience/1",
    "where": {
      "all": [
        { "field": "company.sic_2007", "op": "in", "values": ["C"] },
        { "field": "company.employee_band", "op": "gte", "value": "50-99" }
      ]
    }
  }
}
```

That is the input of `save_audience`. Audiences are shared across your organisation, so the old `forUserGroup` and `days` arguments are not needed.

### 4. Ordering

Before, the model's own flag spent the credits.

```json
{ "savedSelectionId": 1234, "packId": 548, "volumeRequired": 500, "confirmed": true }
```

That is `create_data_extract`. The old `packId` 548 is the Standard bundle. After, a quote comes first and a person confirms.

1. `quote_order` with `{ "count_id": "cnt_7K2PQ4MAXN3R6TBY5WDZ", "field_set": "standard", "limit": 500 }` returns a machine quote.
2. `create_order` with the quote and an idempotency key. The host asks the person to approve, or an agent key within its limits places the order.

```json
{
  "name": "create_order",
  "arguments": {
    "quote_id": "qte_01JB4M2R6T1Y5W8ZAK8Q2V7XN3",
    "idempotency_key": "6f1c3b2a-9d4e-4f8a-b1c2-7e5d9a0f3c21",
    "name": "Leeds marketing Q4"
  }
}
```

Every old `packId` and its field set is in [Old bundles to field sets](#old-bundles-to-field-sets). See [Spending safely](https://emailmovers.com/mydatahub/docs/spending-safely.md).

### 5. Getting the records

Before, the legacy download endpoint paged records by number. It took `OrderId`, `pageNumber` and `pageSize`, and the key travelled in the `X-Api-Key` header.

```http
POST https://app.mydatahub.uk/api/RunExtract
X-Api-Key: <your key>
```

After, `GET /v1/orders/{id}/records` pages by cursor with a bearer token. The default page is 50 records and the maximum 500, and a page can hold fewer when it reaches about 8,000 tokens.

```bash
curl "https://mydatahub.emailmovers.com/v1/orders/ord_01JB4N9S3W2X6Y7Z8A1B2C3D4E/records?limit=500" \
  -H "Authorization: Bearer $MYDATAHUB_API_KEY"
```

Follow `next_cursor` until `has_more` is false. Over MCP the tool is `fetch_order_records`, with at most 100 rows a page.

The plan is for legacy download responses to carry `Deprecation` and `Sunset` headers until the endpoint returns 410. The v2 API does not serve that endpoint yet.

### 6. Checking the balance

Before, `get_credits` returned the balance. After, `get_account` takes no arguments and returns the balance by source, the plan, per-action prices, the rate limit and any agent key budget.

```json
{ "name": "get_account", "arguments": {} }
```

## Migration checklist

1. Make an `mdh_test_` key in the app, and point your integration at it. Test keys return synthetic data and never charge.
2. Replace `X-Api-Key` with `Authorization: Bearer`.
3. Replace the session calls with one `count_audience` call per audience.
4. Replace `create_data_extract` and the legacy download endpoint with `quote_order`, `create_order` and `fetch_order_records`.
5. Decide how spends are confirmed: a person in the app, or an agent key with a budget and a per-order cap. See [Authentication](https://emailmovers.com/mydatahub/docs/authentication.md).
6. Subscribe to `order.ready` instead of polling. See [Webhooks](https://emailmovers.com/mydatahub/docs/webhooks.md).
7. Switch to an `mdh_live_` key when the test run passes.

## Questions

**Do I need to reinstall the connector?** No. `https://mcp.mydatahub.uk` keeps serving today's product after launch, so an install made there keeps working as it is.

Once the new server supports OAuth sign-in from the client, that address passes MCP through to `https://mydatahub.emailmovers.com/mcp` with no redirect. To use the 22 new tools before then, add the new server with a MyDataHub API key, as the [Quickstart for agents](https://emailmovers.com/mydatahub/docs/quickstart/agents.md) shows.

**Will my saved lists and credits survive?** That is the plan. Saved lists become audiences in the same organisation, and the credit balance carries over one for one with licence dates unchanged (to confirm).

**Can an old agent still spend?** Only through a person's confirmation or an agent key. A flag set by a model is never consent, in the old tool as in the new ones.

**What happens when the window closes?** The plan is that the old tools stop being listed, and that the old header and endpoint return 410. See [Changelog and deprecation policy](https://emailmovers.com/mydatahub/docs/changelog.md).

## Next steps

- Start with the [Quickstart for agents](https://emailmovers.com/mydatahub/docs/quickstart/agents.md) or the [Quickstart for developers](https://emailmovers.com/mydatahub/docs/quickstart/developers.md).
- Browse the replacements in the [MCP tool reference](https://emailmovers.com/mydatahub/docs/reference/mcp.md).
