# Multiple MCP accounts and resource sets

> Connect the same MCP toolkit more than once, organize exact accounts into reusable resource sets, grant them to agents or agent groups, and store isolated per-customer mappings.

Most personal agents do not need an MCP resource set. Start with the first
useful result, and open the detailed model only when the workflow needs more
than one account, shared access, or an external-customer mapping.

## Start simple - one personal account

A new user begins in one implicit personal space. The first Gmail or other
toolkit connection becomes that toolkit's obvious `default` connection. Give
its numeric configured-tool ID directly to one agent through `available_tools`,
run the agent, freeze an accepted run, and deploy it through the normal agent
lifecycle. The user does not need to create or name an MCP resource set,
group, slot, grant, principal, revision, or mapping.

The MCP path is intentionally short:

```text
search_tools({"query":"read my Gmail inbox"})
# If Gmail is not configured, send the hosted setup link returned by FlyMyAI.
# Programmatic setup may call add_tool({"mcp_tool":"gmail"}) without an alias.
list_configured_tools({"mcp_tool":"gmail","alias":"default","page_size":100})
create_agent({
  "name":"Inbox summary",
  "user_prompt":"Summarize unread messages and return a short action list.",
  "available_tools":[123],
  "mcp_access_mode":"legacy"
})
run_agent({
  "agent_id":"<same agent UUID>",
  "variables":{},
  "operation_key":"inbox-summary-run-<uuid>"
})
```

Continue by refining and freezing that same agent, then follow
[Create, Freeze, and Embed One Agent](https://docs.flymy.ai/agents/guides/serverless-agents.md) if it
will be deployed. This default path uses the same exact connection IDs,
authority checks, freeze snapshot, and runtime selection model described below.
It can grow into advanced configuration without rebuilding the agent or
reconnecting the first account.

## Expand only when the workflow needs it

Introduce aliases and resource sets when one toolkit name is no longer enough
to identify the account that should act.

For example, one agent can read a support Gmail inbox, compare it with a sales
Gmail inbox, and archive selected messages through a third Gmail account. Each
mailbox is one exact connection instance. A named resource set groups those
instances into one reusable MCP resource set.

The identities stay separate:

| Object | What it identifies | Stable reference |
| --- | --- | --- |
| Toolkit | Which operations exist, such as Gmail actions | `mcp_tool` slug |
| Connection instance | Which authenticated account or remote MCP server acts | `public_id` |
| MCP resource set | A named collection of exact connections | resource-set `public_id` and `revision` |
| Agent grant | Which agent may use the set | `mcp_resource_set_ids` |
| Agent group | A flat group of agents that share set grants | agent-group `public_id` |
| Runtime binding | Which exact connection fills a frozen slot | connection `public_id` |

Aliases such as `support` and `sales` are labels for people. Authorization,
freezing, dispatch, refresh, revocation, and customer isolation use persisted
IDs. Never select an account by email, display name, toolkit slug, resource-set
name, list position, or a database first row.

## Advanced owner workflow

Set the exact REST environment before using the examples. Released production
uses the value shown here; a release candidate must supply its own root and
must not fall back to production:

```bash
export FLYMYAI_AGENTS_API_ROOT=https://backend.flymy.ai/api/v1/agents
: "${FLYMYAI_AGENTS_API_ROOT:?Set the exact Agents API root}"
case "$FLYMYAI_AGENTS_API_ROOT" in */api/v1/agents) ;; *) exit 1 ;; esac
BASE="${FLYMYAI_AGENTS_API_ROOT%/}"
auth=(-H "X-API-KEY: $FLYMYAI_API_KEY")
: "${MCP_RESOURCE_SET_CREATE_KEY:?Persist one key for the exact owner-set create body}"
: "${AGENT_GROUP_CREATE_KEY:?Persist one key for the exact group create body}"
```

The same flow is available through the FlyMyAI Agents MCP gateway. The MCP
tool names are included after the REST example.

### 1. Create exact connection instances

Create each catalog connection with a unique alias:

```bash
support=$(curl -fsS -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data '{"mcp_tool":"gmail","alias":"support"}' \
  "$BASE/tools/")

sales=$(curl -fsS -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data '{"mcp_tool":"gmail","alias":"sales"}' \
  "$BASE/tools/")

archive=$(curl -fsS -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data '{"mcp_tool":"gmail","alias":"archive"}' \
  "$BASE/tools/")

SUPPORT_ID=$(jq -er '.public_id' <<<"$support")
SALES_ID=$(jq -er '.public_id' <<<"$sales")
ARCHIVE_ID=$(jq -er '.public_id' <<<"$archive")
```

Each response includes:

- `id` - the numeric compatibility ID used by legacy direct tool attachment;
- `public_id` - the stable UUID used by resource sets and cross-system code;
- `mcp_tool` - the toolkit definition;
- `alias` - the normalized human label;
- `is_configured`, `next_configuration_step`, `connection_status`,
  `connection_status_reason`, `connection_status_checked_at`,
  `connection_status_valid_until`, `connection_status_reason_code`, and
  `connect_url`, without returning stored secrets to an MCP client.

Follow the returned setup or connect URL for each exact row. Do not ask a user
to paste an API key, OAuth token, password, or refresh token into chat.

Calling `POST /tools/` without `alias` addresses the legacy alias `default`.
That request remains idempotent and preserves old single-connection behavior.
Use an explicit non-default alias whenever another instance is intended.
Aliases and resource-set slots use only ASCII letters, digits, underscores,
and hyphens. The API rejects spaces, punctuation, and arbitrary authority
fields instead of normalizing them into a different identity.

One owner may retain at most 25 connection rows for one toolkit, including
inactive rows. Replaying an existing alias remains idempotent at the limit.
Delete an unused row to free capacity; merely setting `is_active:false` keeps
its exact identity and therefore does not free a slot.

Through MCP, the equivalent calls are:

```text
add_tool({"mcp_tool":"gmail","alias":"support"})
add_tool({"mcp_tool":"gmail","alias":"sales"})
add_tool({"mcp_tool":"gmail","alias":"archive"})
list_configured_tools({"mcp_tool":"gmail","page_size":100})
```

For an unconfigured row, the current gateway returns guidance containing the
hosted `https://app.flymy.ai/mcp-configs#<slug>?connection_id=<public UUID>`
link and the exact alias, public UUID, and numeric compatibility ID. A
slug-only link is used only before an exact row exists. It does not return a field named
`setup_url`. After the user finishes, match that exact alias and `public_id` in
`list_configured_tools`. Require `is_configured:true`; `connected` is ready,
`setup_required` or `reconnect_required` needs the hosted link,
`verification_pending` needs a bounded live read, and
`temporarily_unavailable` must be surfaced rather than bypassed.

Revoke one exact connection with `delete_tool` and that row's numeric `id`
(REST `DELETE /api/v1/agents/tools/{id}/`) after matching its `public_id` and
`alias`. Revocation is per connection: local authority closes atomically with
a durable provider revocation for that row only, sibling aliases keep
working, and a later dispatch on the revoked row fails before any provider
call.

Retain every returned `public_id`. Listing by toolkit slug alone is not enough
when several rows share the same toolkit.

`list_configured_tools` returns exactly one bounded cursor page shaped as
`{next_cursor, previous_cursor, results}`. Filter by exact `mcp_tool` and,
when one named row is wanted, exact `alias`. Pass a non-null `next_cursor`
back unchanged as `cursor`; never assume the first page contains every
connection instance. The endpoint does not expose a fuzzy `query` filter.

### 2. Create a named MCP resource set

An owner resource set can contain the owner's catalog connections and custom
MCP servers:

```bash
resource_set=$(curl -fsS -X POST "${auth[@]}" \
  -H "Idempotency-Key: $MCP_RESOURCE_SET_CREATE_KEY" \
  -H 'Content-Type: application/json' \
  --data '{
    "name":"Inbox operations",
    "description":"Support, sales, and archive mailboxes",
    "management_mode":"flymyai"
  }' \
  "$BASE/mcp-resource-sets/")

RESOURCE_SET_ID=$(jq -er '.public_id' <<<"$resource_set")
RESOURCE_SET_REVISION=$(jq -er '.revision' <<<"$resource_set")
```

For MCP, pass the same durable identity as the required `operation_key` on
`create_mcp_resource_set`: a caller-owned string of 1-255 printable ASCII
characters with no leading or trailing spaces. No client, SDK, or server
generates a fallback key. Persist the exact body and key before the first
dispatch. If the response is lost, replay the identical body with that same
key. A changed body with the old key returns 409; a new key could create a
second resource set and is not a recovery strategy.

Omitting `principal_id` creates an owner-authority set. Do not put a customer
`integration_connection` in an owner set. Customer mappings use a set rooted
at one exact `ExternalPrincipal`, described below.

`management_mode` describes what the saved set represents:

- `flymyai` with no `principal_id` is an owner resource set granted to owner agents;
- `customer` with an exact `principal_id` is a customer-managed named mapping
  whose opaque set ID is stored and supplied by the customer's backend.

A FlyMyAI-managed embedded binding is not a principal resource set. It uses
saved `ConnectionBinding` rows, and the deployment run omits
`resource_set_id` and `connections`. Do not create a principal set with
`management_mode: "flymyai"` and do not send the deprecated
`external_principal_id` field.

### 3. Replace membership atomically

Membership is an atomic full replacement protected by optimistic concurrency.
Send the current `revision` as `expected_revision`:

```bash
members=$(jq -nc \
  --arg support "$SUPPORT_ID" \
  --arg sales "$SALES_ID" \
  --arg archive "$ARCHIVE_ID" \
  --argjson revision "$RESOURCE_SET_REVISION" \
  '{
    expected_revision:$revision,
    members:[
      {
        resource_type:"user_mcp_tool",
        resource_id:$support,
        slot:"support_read",
        allowed_actions:["GMAIL_SEARCH_EMAILS"],
        position:0
      },
      {
        resource_type:"user_mcp_tool",
        resource_id:$support,
        slot:"support_send",
        allowed_actions:["GMAIL_SEND_EMAIL"],
        position:1
      },
      {
        resource_type:"user_mcp_tool",
        resource_id:$sales,
        slot:"sales_mailbox",
        allowed_actions:[],
        position:2
      },
      {
        resource_type:"user_mcp_tool",
        resource_id:$archive,
        slot:"archive_mailbox",
        allowed_actions:[],
        position:3
      }
    ]
  }')

resource_set=$(curl -fsS -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data "$members" \
  "$BASE/mcp-resource-sets/$RESOURCE_SET_ID/replace-members/")

RESOURCE_SET_REVISION=$(jq -er '.revision' <<<"$resource_set")
```

The member input contract is:

| Field | Meaning |
| --- | --- |
| `resource_type` | `user_mcp_tool`, `custom_mcp_server`, or `integration_connection` |
| `resource_id` | Stable public UUID of the exact connection or server |
| `slot` | Logical role in the workflow |
| `allowed_actions` | Optional least-authority action ceiling; omission or an empty list means no additional set-level ceiling, not deny all |
| `position` | Stable display and resolution order, defaulting to request order |

Member identity is the exact `(resource_type, resource_id, slot)` tuple. One
exact resource may appear in several distinct logical slots, as the support
connection does above. Each slot keeps its own `allowed_actions` ceiling, so a
read slot does not inherit the send slot's authority. Repeating the same tuple
is invalid.

A `409` response with `code: "stale_revision"` means another writer changed
the set. Reload the current representation, review the new membership, and
submit a new full replacement with its revision. Do not overwrite it blindly.

Metadata edits use the same CAS boundary. MCP `update_mcp_resource_set`
requires both the stable `resource_set_id` and current `expected_revision`,
even when only name, description, or status changes. A missing revision is a
validation error; HTTP 409 requires reload and intentional review.

A resource set supports at most 100 members and one runtime slot supports at
most 25 connections. Members pooled inside one slot must use one toolkit and
one action ceiling. Different slots remain independent even when they point to
the same exact resource.

One scoped agent execution admits at most 100 MCP resource sets, 500 effective
connector bindings, 100 logical slots, 5,000 allowed-action entries, and 1 MiB
of UTF-8 allowed-action text. An oversized authority graph fails closed before
provider dispatch.

### 4. Grant the resource set to one agent

Pass stable resource-set UUIDs when creating or updating the agent:

```bash
agent=$(curl -fsS -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc --arg set "$RESOURCE_SET_ID" '{
    name:"Cross-mailbox brief",
    user_prompt:(
      "Compare the support and sales mailboxes, then move approved messages " +
      "through the archive mailbox. Return a structured summary."
    ),
    mcp_resource_set_ids:[$set],
    mcp_access_mode:"scoped",
    output_schema:{
      type:"object",
      properties:{summary:{type:"string"}},
      required:["summary"],
      additionalProperties:false
    }
  }')" \
  "$BASE/tasks/")

AGENT_ID=$(jq -er '.uuid' <<<"$agent")
```

Through MCP:

```text
create_agent({
  "name":"Cross-mailbox brief",
  "user_prompt":"Compare support and sales, then archive approved messages.",
  "mcp_resource_set_ids":["<resource-set public UUID>"],
  "mcp_access_mode":"scoped"
})
```

When converting an existing personal agent from its direct
`available_tools` attachment to logical resource slots, discover the live
`update_agent` request schema with MCP `tools/list` first. The current schema
requires `agent_id` and accepts the same optional attachment fields as
`create_agent`. Include the existing default connection in the new set when it
must remain available, then replace the old direct attachment explicitly:

```text
update_agent({
  "agent_id":"<same agent UUID>",
  "available_tools":[],
  "mcp_resource_set_ids":["<resource-set public UUID>"],
  "mcp_access_mode":"scoped"
})
get_agent({"agent_id":"<same agent UUID>"})
```

An omitted attachment field is unchanged; an explicit empty array clears that
relation. Verify that `available_tools` is empty and
`mcp_resource_set_ids` contains exactly the intended set. Direct attachments
and set grants otherwise form a union, which can preserve unintended owner
authority. Clear `available_custom_mcp_servers` only when those legacy server
attachments are also being replaced. Never copy an owner connection ID into a
customer mapping.

The mutable owner runtime receives the union of legacy direct attachments,
direct resource-set grants, and active agent-group grants. Exact connection
members are deduplicated only when the complete
`(resource_type, resource_id, slot)` tuple repeats across grants. Distinct slot
memberships for the same connection remain separate with their own action
ceilings. A foreign-owner set or connection fails closed.

### 5. Share the resource set with a flat agent group

Use an agent group when several agents need the same resource sets:

```bash
curl -fsS -X POST "${auth[@]}" \
  -H "Idempotency-Key: $AGENT_GROUP_CREATE_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc --arg agent "$AGENT_ID" --arg set "$RESOURCE_SET_ID" '{
    name:"Inbox agents",
    description:"Agents allowed to use the inbox operations resource set",
    is_active:true,
    agent_ids:[$agent],
    resource_set_ids:[$set]
  }')" \
  "$BASE/agent-groups/"
```

The MCP equivalents are `create_agent_group`, `update_agent_group`,
`get_agent_group`, and `list_agent_groups`. Supplying `agent_ids` or
`resource_set_ids` on an update replaces that full assignment list atomically.
Omitting a list leaves it unchanged.
`create_agent_group` requires its own caller-owned `operation_key`; an
uncertain response is retried only with the identical body and original key.

`list_configured_tools`, `list_mcp_resource_sets`,
`list_mcp_resource_set_members`, and `list_agent_groups` return one compact
cursor envelope shaped exactly as `{next_cursor, previous_cursor, results}`. Request
`page_size` from 1-100, treat `cursor` as an opaque, nonblank printable string
of at most 1024 Unicode code points, and pass `next_cursor` unchanged to
continue. Resource-set and agent-group list `query` is at most 256 printable
characters; the member list accepts only its set ID, page size, and cursor;
configured-tool list uses only exact `mcp_tool` and `alias` filters. Do not
translate cursors to offsets or stop after the first page when the continuation
is non-null.

Agent groups are flat in this release. They cannot contain groups. A child or
subagent does not inherit a parent's connector authority merely because the
parent invoked it. Add that agent to the group or grant the set directly.

## Exact selection at runtime

FlyMyAI exposes one action schema per toolkit action, not one duplicated schema
per connected account. If exactly one granted connection can execute an action,
legacy implicit selection is preserved. If several can execute it, the action
schema requires `_flymyai_connection`.

Pass an allowed connection `public_id`:

```json
{
  "_flymyai_connection": "22222222-2222-4222-8222-222222222222",
  "query": "after:2026/08/01"
}
```

For a direct MCP `execute_tool` call outside an agent run, pass the exact
configured-tool public UUID as `connection_id`. Omission is supported only
while one eligible connection is unambiguous; it must never select an account
by row order. The direct call still requires its caller-owned stable
`operation_key` and exact action schema.

Read the exact enum and descriptions from the runtime schema. Never substitute
the alias or choose the first row. Missing, foreign, revoked, expired, stale,
or no-longer-granted selectors fail before provider dispatch.

Connected-but-unattached discovery can auto-activate a toolkit only when one
eligible connection exists. An ambiguous toolkit must be attached or grouped
explicitly.

## Freeze and publish without losing account identity

Freeze only an accepted completed execution of the same logical agent. New
frozen versions capture effective resource-set membership, exact owner
connection IDs, logical slots, action ceilings, and source set revisions.

Changing a member, slot, action ceiling, group membership, or set revision can
change the next version fingerprint. An older immutable version does not change.
Do not rely on editing an alias after freeze to retarget an old release.

Adding an account without rebuilding means keeping the same agent UUID:
update that agent, accept a new run, and freeze a new immutable version. Stage
that version on the same stable deployment ID. Neither the old version nor an
incompatible saved customer mapping is mutated in place.

Continue with [Create, Freeze, and Embed One Agent](https://docs.flymy.ai/agents/guides/serverless-agents.md)
for the complete run, freeze, version, preflight, publish, and rollback flow.

## Product-customer mapping modes

`external_user_id` resolves one product customer's `ExternalPrincipal`. It
does not identify a Gmail account, choose a resource set, create a FlyMyAI
user, or switch billing. FlyMyAI still bills the deployment owner.

Choose the mapping mode explicitly:

| Mode | What your backend sends on deployment run | Where the mapping lives |
| --- | --- | --- |
| FlyMyAI-managed binding | Neither `resource_set_id` nor `connections` | Saved `ConnectionBinding` rows |
| Customer-managed named mapping | `resource_set_id` and its saved `resource_set_revision` | Stable opaque mapping ID in your backend, exact members in FlyMyAI |
| One-off explicit mapping | `connections` keyed by slot | Canonical request stored by your backend |

`resource_set_id` and `connections` are mutually exclusive. Supplying both is
a validation error. `resource_set_revision` is valid only with
`resource_set_id`. The backend still accepts an omitted named-mapping revision
for compatibility, but the public safe workflow requires both saved values so
a stale mapping fails closed.

The names describe who stores the run selector. A FlyMyAI-managed run uses
saved bindings and no principal resource-set ID. A customer-managed named run
uses a principal resource set created with `management_mode: "customer"` and
`principal_id`; the customer's backend stores and supplies that set ID.

### Bootstrap the customer principal through `connect-session`

`GET /deployments/{deployment_id}/access/` is read-only. It can show the
deployment requirements and existing customer state, but it never creates an
`ExternalPrincipal`. The first mutating bootstrap is
`POST /deployments/{deployment_id}/connect-session/` with the exact
`external_user_id` and one `connection_required` slot from the published
deployment:

```bash
: "${SLOT:?Use a connection_required slot from deployment access}"
session=$(curl -fsS -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc \
    --arg user "$EXTERNAL_USER_ID" \
    --arg slot "$SLOT" \
    '{external_user_id:$user,slot:$slot}')" \
  "$BASE/deployments/$DEPLOYMENT_ID/connect-session/")

jq '{redirect_url,expires_at}' <<<"$session"
```

That POST resolves or creates the principal and creates a short-lived hosted
authorization session. Treat it as a single-dispatch write: persist the
canonical deployment, customer, and slot before calling, and reconcile an
ambiguous response before creating another session. Redirect the authenticated
customer to `redirect_url`. After authorization, poll the read-only access
route for active, unexpired connections:

```bash
curl -fsS -G "${auth[@]}" \
  --data-urlencode "external_user_id=$EXTERNAL_USER_ID" \
  "$BASE/deployments/$DEPLOYMENT_ID/access/"
```

Only after that mutating bootstrap should the owner query the principal list.
Require exactly one result:

```bash
principals=$(curl -fsS -G "${auth[@]}" \
  --data-urlencode "deployment=$DEPLOYMENT_ID" \
  --data-urlencode "external_user_id=$EXTERNAL_USER_ID" \
  "$BASE/external-principals/")

PRINCIPAL_ID=$(jq -er '
  (.results // .) as $rows |
  if ($rows|length)==1 then $rows[0].public_id else error("expected one principal") end
' <<<"$principals")
```

### Create a customer-managed named mapping

Create a principal-authority set:

```bash
: "${CUSTOMER_RESOURCE_SET_CREATE_KEY:?Persist one key for this exact customer mapping create}"
customer_set=$(curl -fsS -X POST "${auth[@]}" \
  -H "Idempotency-Key: $CUSTOMER_RESOURCE_SET_CREATE_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc --arg p "$PRINCIPAL_ID" '{
    name:"Primary inbox mapping",
    description:"Mapping stored by the customer backend",
    management_mode:"customer",
    principal_id:$p
  }')" \
  "$BASE/mcp-resource-sets/")
```

Replace its members with exact `integration_connection` public UUIDs belonging
to that same principal. Cross-principal IDs are rejected. Store the returned
resource-set `public_id` and `revision` next to your own customer and deployment
record. The ID is opaque and does not expose credentials.

Run the stable deployment with that saved mapping:

```bash
curl -fsS -X POST "${auth[@]}" \
  -H "Idempotency-Key: $PERSISTED_DEPLOYMENT_RUN_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc \
    --arg user "$EXTERNAL_USER_ID" \
    --arg set "$CUSTOMER_RESOURCE_SET_ID" \
    --argjson revision "$CUSTOMER_RESOURCE_SET_REVISION" \
    '{
      external_user_id:$user,
      variables:{},
      resource_set_id:$set,
      resource_set_revision:$revision
    }')" \
  "$BASE/deployments/$DEPLOYMENT_ID/run/"
```

The runtime revalidates set authority and every exact connection at the
immutable snapshot boundary. A stale revision, revoked connection, wrong
principal, or slot mismatch fails closed.

For FlyMyAI-managed bindings, omit both mapping fields. For a one-off mapping,
send `connections` instead:

```json
{
  "external_user_id": "customer_42",
  "variables": {},
  "connections": {
    "support_mailbox": "<integration-connection public UUID>",
    "archive_mailbox": ["<integration-connection public UUID>"]
  }
}
```

Each `connections` value is either one integration-connection public UUID or
an array of UUIDs for a pooled slot. Use only slot names from the frozen access
requirements, honor each slot's cardinality, and include every required slot
when the request is intended to be a complete explicit mapping. An omitted
slot is not an explicit empty value: current runtime behavior continues to its
saved binding for that slot. Every supplied UUID is revalidated against the
resolved external principal; an owner connection, a different customer's
connection, an unknown slot, or invalid cardinality fails before provider
dispatch. The request contains IDs only, never provider credentials.

Derive `external_user_id` from the authenticated product session and keep the
builder key on your server. Persist the mapping ID, revision, canonical run
body, idempotency key, execution ID, and settled price in your own ledger.

See [Per-customer MCP Access](https://docs.flymy.ai/agents/guides/embedded-mcp.md) for hosted connection
sessions, readiness polling, isolation proof, billing, and cleanup.

## Public API shapes

A resource-set list row is a compact summary containing:

```text
public_id, name, description, status, management_mode, authority_type,
principal_id, revision, member_count, created_at, updated_at
```

It does not nest `members`. A resource-set detail/create/update/replace
response contains:

```text
public_id, name, description, status, management_mode, authority_type,
principal_id, revision, member_count, members, created_at, updated_at
```

Every member contains:

```text
public_id, resource_type, resource_id, toolkit_slug, alias, display_name,
slot, allowed_actions, position, created_at, updated_at
```

An agent-group response contains:

```text
public_id, name, description, is_active, agent_ids, resource_set_ids,
created_at, updated_at
```

All new cross-system references are public UUIDs. Numeric IDs remain only on
legacy compatibility routes and fields.

## MCP and SDK paths

Use the implemented lifecycle boundary:

| Stage | Implemented surface |
| --- | --- |
| Author and test | MCP `create_agent`, `run_agent`, and `get_run` |
| Freeze and owner-test | MCP `freeze_agent`, `get_compilation`, and `run_frozen` |
| Exact owner connections | MCP `add_tool`, `update_tool`, and `list_configured_tools` |
| Resource sets and groups | The MCP tools listed below |
| Publish a deployment | Agents REST `POST /deployments/`, `/preflight/`, and `/publish/` |
| Bootstrap and inspect a customer | Agents REST `POST /deployments/{deployment_id}/connect-session/` and read-only `GET /deployments/{deployment_id}/access/` |
| Run for a customer | Agents REST `POST /deployments/{deployment_id}/run/` |

The Agents MCP gateway's resource-set and group surface is:

- exact account creation through `add_tool(mcp_tool, alias)` and bounded
  discovery through `list_configured_tools(mcp_tool, alias, page_size, cursor)`;
- `list_mcp_resource_sets`, `get_mcp_resource_set`,
  `create_mcp_resource_set`, `update_mcp_resource_set`, and
  `delete_mcp_resource_set`;
- paginated `list_mcp_resource_set_members` for large membership reads;
- revision-checked `replace_mcp_resource_set_members`;
- `list_agent_groups`, `get_agent_group`, `create_agent_group`,
  `update_agent_group`, and `delete_agent_group`;
- `mcp_resource_set_ids` on `create_agent` and `update_agent`.

Deployment publication, hosted customer connection setup, principal reads, and
customer runs remain on the Agents REST API. The MCP gateway can author, test,
freeze, and owner-test the same agent before that REST handoff. Always use the
tool list returned by MCP `tools/list` as the source of truth and do not turn a
REST operation into an invented MCP call.

SDK versions that expose resource scopes mirror the same REST objects through
typed `mcp_resource_sets` and `agent_groups` resources. Feature-detect those
namespaces in the installed package. If they are absent, use the canonical REST
calls above instead of inventing SDK methods. The currently released SDK's
deployment limitations are documented in the lifecycle guide.

## Rollout and compatibility

- Additional aliases and resource-scope mutations are feature-gated during the
  staged rollout. A workspace that is not enabled can continue using the
  `default` connection and can receive a clear mutation-disabled error.
- Existing single-connection agents keep their explicit `default`
  compatibility projection. They are not silently reassigned to another row.
- Disabling a resource set stops its grants without deleting connection
  credentials. Deleting a set removes its membership and grants, not the
  underlying connections.
- Sets and agent groups are not nested in this release. Cross-owner sharing and
  organization RBAC are not implied by matching names.
- A rollback disables new creation but must keep reading and executing existing
  exact rows and sets. Never collapse several aliases back into one row.
- Deploy backend expansion and backward-compatible readers before enabling
  multi-alias writers or the new clients. Before disabling those readers or
  rolling database state back across the alias constraint, run the server's
  read-only alias downgrade preflight and require a clean result. If it reports
  non-default aliases or scoped state, keep the compatible backend in service;
  do not drop or rewrite customer mappings to force a rollback.

Before promotion, run the clean-assistant lifecycle gate early. It must create
one logical agent, use exact resource scopes, freeze one accepted run, publish
one deployment, and stop without receiving customer IDs in its prompt. The
trusted harness must then provision two different `external_user_id` bindings
and prove they remain on the same agent, version, deployment, and billed owner
while receiving distinct principals, executions, and connector state.
