From 64882e221af9ee36eb41a182b041715e8e7c7c6e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 07:21:47 +0000 Subject: [PATCH 1/3] docs(skills): the automation mcp recipe teaches the providerConfig.toolsSnapshot pin One paragraph under recipe 2 (remote MCP): what toolsSnapshot pins and that a pinned name include leaves out is refused at boot; where the digests come from (computeMcpToolsSnapshot, or the served digest the degraded reason prints); what an author sees when a pin no longer matches; the link to the connectors page. Paid for under the skills token ratchet by deleting restatements in the same file that keep a home elsewhere; the file lands at 2487/2488 tokens. Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude --- .changeset/28582-skills-mcp-tools-snapshot.md | 12 +++++ .../references/integration-recipes.md | 52 ++++++++++--------- 2 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 .changeset/28582-skills-mcp-tools-snapshot.md diff --git a/.changeset/28582-skills-mcp-tools-snapshot.md b/.changeset/28582-skills-mcp-tools-snapshot.md new file mode 100644 index 00000000000..2bbdbdfefd0 --- /dev/null +++ b/.changeset/28582-skills-mcp-tools-snapshot.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the automation skill's `mcp` recipe teaches the `providerConfig.toolsSnapshot` pin + +Clause-②: no +Metadata-protocol: none + +- **Before.** `providerConfig.toolsSnapshot` (a sha256 pin per MCP tool, checked on every materialization) shipped in `@objectstack/connector-mcp` and on the connectors page, but no published skill named it. An agent authoring an MCP connector app from the skills declared `include` alone, with no protection against a server silently rewriting a tool's description or `inputSchema`. +- **Now.** `skills/objectstack-automation/references/integration-recipes.md`, recipe 2 (remote MCP), gains one paragraph: what `toolsSnapshot` pins and that a pinned name `include` leaves out is refused at boot; where the digests come from (`computeMcpToolsSnapshot(tools)`, or the served digest the degraded reason prints against a placeholder pin); what an author sees when a pin no longer matches (the instance degraded with no actions, `CONNECTOR_UPSTREAM_CHANGED`, the reason on `GET /api/v1/automation/connectors` naming each tool and the digest it now serves), and the link to the connectors page's pin section. +- The file stays under its token ceiling: the paragraph is paid for by deleting restatements in the same file that keep a home elsewhere (the `connector_action` row of `SKILL.md`, its `api`-trigger *Signature* bullet, the objectstack-platform skill's `plugins:`/`requires` rule, and the connectors page). No ceiling moves, no frontmatter, schema, key or behaviour changes. diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index b7abdacbfbb..e9e73e5f9c1 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -8,23 +8,18 @@ one thing the **customer's administrator** supplies: the value behind value set in the running app at `PUT /api/v1/automation/connectors/credential-refs/:ref`; an unset ref leaves its instance degraded until set. A spec is not a secret and ships in the package; an `api` flow's hook secret goes to the -deployment and to the sender. Scaffolded apps list the three executors in -`plugins:`; keep `automation` in `requires` whenever a connector is declared. Full guide: https://objectstack.ai/docs/automation/connectors. +deployment and to the sender. Full guide: https://objectstack.ai/docs/automation/connectors. ## Holds on every recipe - Actions come from the upstream (an OpenAPI `operationId`, an MCP tool name, the `rest` provider's single `request`) — authored on the entry, they are refused. -- `connectorConfig.input` is a value-slot map like `subflow` `input`: a top-level - CEL envelope is evaluated, any other value kept as written. `registerFlow` and - `os validate` refuse a `{…}` token at `connectorConfig.input.`: a value - from the record is a CEL envelope. Before dispatch the result is judged - against the action's `inputSchema`: `required`, `type` (with OpenAPI - `nullable`), `properties`, `items`; `null` on an optional parameter is - absent. A breach fails the step `VALIDATION_ERROR` (`fault`-routable) naming - each path, nothing sent. Text is never converted: cast a string-held id, - `path: { dialect: 'cel', source: '{"petId": int(record.vendor_id)}' }`. - `os validate` does not judge the `inputSchema` fit yet. +- `connectorConfig.input` is a value-slot map (SKILL.md's `connector_action` + row): a top-level CEL envelope is evaluated, any other value kept as written, + so a value from the record is a CEL envelope. The pre-dispatch `inputSchema` + judgement honours OpenAPI `nullable`, reads `null` on an optional parameter + as absent and names each breached path. Text is never converted: cast a + string-held id, `path: { dialect: 'cel', source: '{"petId": int(record.vendor_id)}' }`. - The answer lands as `.` (`rest` / `openapi`: `{ status, ok, body }`; `mcp`: `{ ok, content, structuredContent? }`), read from a later node's value slot. - `auth`, each variant with a `credentialRef`: `bearer`; `api-key` (`headerName`, @@ -81,13 +76,22 @@ export const GitHubMcp = defineConnector({ headers: { 'X-MCP-Readonly': 'true' } }, // denied unless the host allowlists the command include: ['search_issues'], }, - auth: { type: 'bearer', credentialRef: 'GITHUB_MCP_TOKEN' }, // folded into the request headers + auth: { type: 'bearer', credentialRef: 'GITHUB_MCP_TOKEN' }, }); // Weekly flow: connector_action { connectorId: 'github_mcp', actionId: 'search_issues', // input: { query: 'repo:acme/app is:open label:p0' } } → update_record on `eng_kpi` // fields: { open_p0: { dialect: 'cel', source: 'p0.content' } } (a `json` field). ``` +`toolsSnapshot` pins each tool the app depends on: tool name → sha256 of its +`tools/list` definition; a pinned name `include` leaves out is refused at boot. +Digests: `computeMcpToolsSnapshot(tools)` from `@objectstack/connector-mcp`, or +pin a 64-hex placeholder and copy the served digest from the degraded reason. A +pinned tool served differently degrades the instance with no actions +(`CONNECTOR_UPSTREAM_CHANGED`), one no longer listed too; `GET /api/v1/automation/connectors` +names each tool and any digest it now serves — review, then re-pin. Rest: +https://objectstack.ai/docs/automation/connectors#pinning-tools-toolssnapshot. + ## 3 · Plain REST API — `provider: 'rest'`, the one `request` action ```ts @@ -107,7 +111,7 @@ export const WarehouseApi = defineConnector({ ```ts export const CarrierCallback = defineFlow({ name: 'carrier_callback', label: 'Carrier status callback', type: 'api', - runAs: 'system', // no trigger user + runAs: 'system', nodes: [ { id: 'start', type: 'start', label: 'POST …/hooks/carrier_callback/carrier', config: { hookId: 'carrier', secretRef: 'CARRIER_HOOK_SECRET' } }, @@ -121,10 +125,10 @@ export const CarrierCallback = defineFlow({ ``` `requires: ['automation', 'triggers', 'queue']`. The sender signs each post -`x-objectstack-signature: t=,v1=` — HMAC-SHA256 of `.` -under `CARRIER_HOOK_SECRET`. The door has no unsigned form and verifies no other -scheme, so a vendor whose webhook signs its own header cannot post here today: -poll its status (recipe 1's shape) until vendor schemes verify. +under `CARRIER_HOOK_SECRET` (SKILL.md § Inbound webhook, *Signature*). The door +has no unsigned form and verifies no other scheme, so a vendor whose webhook +signs its own header cannot post here today: poll its status (recipe 1's shape) +until vendor schemes verify. ## 5 · Vendor with no machine-readable spec — write the document by hand @@ -162,15 +166,13 @@ auth: { type: 'app-credential', vendor: 'feishu', appId: 'cli_a1b2', credentialR `client_credentials` and `app-credential` dispatch once the ref is set. `authorization_code` is degraded "not connected" until an administrator holding -`manage_connector_connections` (organization admins by default) connects it, -once per organization, from a signed-in session (an agent or API key is refused): -`POST /api/v1/automation/connectors/:name/connection` answers `{ authorizationUrl }` -for the admin to open; the vendor redirects to `GET …/connectors/oauth/callback`. -`GET` reads the status; `DELETE` revokes. Endpoints are `https://`; an +`manage_connector_connections` connects it, once per organization, from a +signed-in session (an agent or API key is refused): +`POST /api/v1/automation/connectors/:name/connection` answers `{ authorizationUrl }`; +the vendor redirects to `GET …/connectors/oauth/callback`. Endpoints are `https://`; an `authorization_code` instance on `mcp` may omit them and `clientId`, discovered at connect. Still never park a short-lived token behind a `bearer` ref. ## Not yet — mark the integration blocked, write no plugin -- **WeChat Pay**: no `signed` scheme yet (Alipay's is `alipay-v3`, which also - verifies responses). +- **WeChat Pay**: no `signed` scheme yet. From 7a8da47cbc08c2800f6efde526578711445b0cdb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 07:40:50 +0000 Subject: [PATCH 2/3] docs(skills): state both pinned-tool-missing outcomes, restore the requires rule, fix the docs table row Review round on the mcp recipe's toolsSnapshot paragraph: a pinned tool the server no longer lists takes the credential path when the instance's credential reached the server (recipe 2's bearer over http does) and CONNECTOR_UPSTREAM_CHANGED otherwise, read off credentialSent in mcp-provider.ts; the "keep automation in requires whenever a connector is declared" rule is restored; the connectors page's failure-mode row for the pin now says the same two cases as its prose and the code. Paid under the skills token ratchet by same-file cuts with a surviving home; the file lands at 2488/2488 tokens. Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude --- content/docs/automation/connectors.mdx | 2 +- .../references/integration-recipes.md | 49 +++++++++---------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/content/docs/automation/connectors.mdx b/content/docs/automation/connectors.mdx index 459798a7b87..39ea10f873b 100644 --- a/content/docs/automation/connectors.mdx +++ b/content/docs/automation/connectors.mdx @@ -866,7 +866,7 @@ retry): | Instance `name` collides with a plugin-registered connector | **Hard boot error** — there is no silent precedence between the two worlds | | stdio transport without host opt-in | **Hard boot error** with the `declarativeStdio` fix inline — a security rejection is never retried | | MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) | **Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic | -| An `mcp` instance's `toolsSnapshot` does not match what the server serves (a pinned tool's description or `inputSchema` changed, or a pinned tool is no longer listed) | **Not fatal**: the instance registers **degraded** with no actions, its reason naming each tool and the digest it now serves, and the logs say "serves a tool definition that differs from its toolsSnapshot" (`CONNECTOR_UPSTREAM_CHANGED`); it is re-checked on the same backoff and recovers once the server serves the pinned definitions again — or set the pin to the reviewed digest | +| An `mcp` instance's `toolsSnapshot` does not match what the server serves (a pinned tool's description or `inputSchema` changed; or a pinned tool is no longer listed, on an instance whose credential did not reach the server) | **Not fatal**: the instance registers **degraded** with no actions, its reason naming each tool and the digest it now serves, and the logs say "serves a tool definition that differs from its toolsSnapshot" (`CONNECTOR_UPSTREAM_CHANGED`); it is re-checked on the same backoff and recovers once the server serves the pinned definitions again — or set the pin to the reviewed digest. A pinned tool no longer listed on an instance whose credential **did** reach the server (its `auth` over http) takes the credential row above instead: a server may hide tools from a credential, so the reason names both causes (fix the credential and set it again, or update the pin), a stored credential is marked `rejected`, and no retry runs against the same value | | A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer; on an `mcp` instance that sends a static credential, the credential row above applies instead) | **Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it | | Wrong spec URL (other 4xx) or unparseable document | Configuration fault — **hard boot error** | | An `openapi` operation's parameter declares a `style` OpenAPI 3.x does not define for its location (`deepObject` on a path parameter, `form` on a header) | Configuration fault — **hard boot error** naming the operation and the parameter; correct the document or leave the operation out of `include` | diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index e9e73e5f9c1..07ce0c45df5 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -8,7 +8,8 @@ one thing the **customer's administrator** supplies: the value behind value set in the running app at `PUT /api/v1/automation/connectors/credential-refs/:ref`; an unset ref leaves its instance degraded until set. A spec is not a secret and ships in the package; an `api` flow's hook secret goes to the -deployment and to the sender. Full guide: https://objectstack.ai/docs/automation/connectors. +deployment and to the sender. Keep `automation` in `requires` whenever a +connector is declared. Full guide: https://objectstack.ai/docs/automation/connectors. ## Holds on every recipe @@ -23,10 +24,11 @@ deployment and to the sender. Full guide: https://objectstack.ai/docs/automation - The answer lands as `.` (`rest` / `openapi`: `{ status, ok, body }`; `mcp`: `{ ok, content, structuredContent? }`), read from a later node's value slot. - `auth`, each variant with a `credentialRef`: `bearer`; `api-key` (`headerName`, - default `X-API-Key`, or `paramName` for a query-string key); `basic` (`username` + or `paramName` for a query-string key); `basic` (`username` stays in metadata); `signed` on `openapi` / `rest` (`scheme` `aliyun-acs3-hmac-sha256`, `tencentcloud-tc3-hmac-sha256` or `alipay-v3`, - `keyId`) signs each request; `oauth2` / `app-credential`: recipe 6. A secret in a url path has no variant — use a + `keyId`) signs each request — WeChat Pay has no scheme yet: mark it blocked, + write no plugin; `oauth2` / `app-credential`: recipe 6. A secret in a url path has no variant — use a token-authenticated connector. `authentication: { token }` is refused at `os validate`. - `defineConnector`: `@objectstack/spec/integration`; `defineFlow`: `@objectstack/spec`; both register in `defineStack`. @@ -61,8 +63,8 @@ export const StripeBalanceSweep = defineFlow({ `requires: ['automation', 'triggers', 'job']`. Convert Swagger 2.0 or YAML and dereference every `$ref` offline, in-document ones included: the loader skips a -`$ref` parameter, request body or response, and the derived input/output schema -silently loses it. `providerConfig.include` allowlists operation keys as the +`$ref` parameter, request body or response and the derived schema silently +loses it. `providerConfig.include` allowlists operation keys as the `mcp` provider's does (absent = every operation; a non-array or unknown entry is fatal). @@ -72,8 +74,8 @@ fatal). export const GitHubMcp = defineConnector({ name: 'github_mcp', label: 'GitHub (remote MCP)', type: 'api', provider: 'mcp', providerConfig: { - transport: { kind: 'http', url: 'https://api.githubcopilot.com/mcp/', // `stdio` spawns a local process: - headers: { 'X-MCP-Readonly': 'true' } }, // denied unless the host allowlists the command + transport: { kind: 'http', url: 'https://api.githubcopilot.com/mcp/', // `stdio` needs the host's allowlist + headers: { 'X-MCP-Readonly': 'true' } }, include: ['search_issues'], }, auth: { type: 'bearer', credentialRef: 'GITHUB_MCP_TOKEN' }, @@ -85,11 +87,14 @@ export const GitHubMcp = defineConnector({ `toolsSnapshot` pins each tool the app depends on: tool name → sha256 of its `tools/list` definition; a pinned name `include` leaves out is refused at boot. -Digests: `computeMcpToolsSnapshot(tools)` from `@objectstack/connector-mcp`, or -pin a 64-hex placeholder and copy the served digest from the degraded reason. A -pinned tool served differently degrades the instance with no actions -(`CONNECTOR_UPSTREAM_CHANGED`), one no longer listed too; `GET /api/v1/automation/connectors` -names each tool and any digest it now serves — review, then re-pin. Rest: +Digests: `computeMcpToolsSnapshot(tools)` (`@objectstack/connector-mcp`), or a +64-hex placeholder pin, then the served digest from the degraded reason. The +instance degrades with no actions, the reason on `GET /api/v1/automation/connectors` +naming each tool: a pinned tool served differently is `CONNECTOR_UPSTREAM_CHANGED` +with the digest it now serves (review, re-pin); one no longer listed is +the credential path when the instance's credential reached the server, as this +`bearer` does — the stored credential marked `rejected`: fix the credential and +set it again, or update the pin — and `CONNECTOR_UPSTREAM_CHANGED` otherwise. Rest: https://objectstack.ai/docs/automation/connectors#pinning-tools-toolssnapshot. ## 3 · Plain REST API — `provider: 'rest'`, the one `request` action @@ -97,7 +102,7 @@ https://objectstack.ai/docs/automation/connectors#pinning-tools-toolssnapshot. ```ts export const WarehouseApi = defineConnector({ name: 'warehouse_api', label: 'Warehouse API', type: 'api', provider: 'rest', - providerConfig: { baseUrl: 'https://wms.example.com', defaultHeaders: { Accept: 'application/json' } }, + providerConfig: { baseUrl: 'https://wms.example.com' }, auth: { type: 'api-key', credentialRef: 'WMS_API_KEY', headerName: 'X-Api-Key' }, }); // connector_action { connectorId: 'warehouse_api', actionId: 'request', @@ -125,10 +130,9 @@ export const CarrierCallback = defineFlow({ ``` `requires: ['automation', 'triggers', 'queue']`. The sender signs each post -under `CARRIER_HOOK_SECRET` (SKILL.md § Inbound webhook, *Signature*). The door -has no unsigned form and verifies no other scheme, so a vendor whose webhook -signs its own header cannot post here today: poll its status (recipe 1's shape) -until vendor schemes verify. +under `CARRIER_HOOK_SECRET` (SKILL.md *Signature* bullet). The door verifies no +other scheme: a vendor whose webhook signs its own header cannot post here +today — poll its status (recipe 1's shape) until vendor schemes verify. ## 5 · Vendor with no machine-readable spec — write the document by hand @@ -167,12 +171,7 @@ auth: { type: 'app-credential', vendor: 'feishu', appId: 'cli_a1b2', credentialR `client_credentials` and `app-credential` dispatch once the ref is set. `authorization_code` is degraded "not connected" until an administrator holding `manage_connector_connections` connects it, once per organization, from a -signed-in session (an agent or API key is refused): -`POST /api/v1/automation/connectors/:name/connection` answers `{ authorizationUrl }`; -the vendor redirects to `GET …/connectors/oauth/callback`. Endpoints are `https://`; an -`authorization_code` instance on `mcp` may omit them and `clientId`, discovered at connect. +signed-in session: `POST /api/v1/automation/connectors/:name/connection` answers +`{ authorizationUrl }`. An `authorization_code` instance on `mcp` may omit the +`https://` endpoints and `clientId`, discovered at connect. Still never park a short-lived token behind a `bearer` ref. - -## Not yet — mark the integration blocked, write no plugin - -- **WeChat Pay**: no `signed` scheme yet. From 71213196fed0d0a49f13f635d354b9c1bc9309b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 08:00:15 +0000 Subject: [PATCH 3/3] docs(skills): restore the OAuth connect guardrails in recipe 6, pay with skills-homed restatements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 2: "(an agent or API key is refused)" and the vendor redirect to GET .../connectors/oauth/callback return to recipe 6 (the eval expects the latter). Paid under the skills token ratchet only with text restated elsewhere in skills/: the scheduled-run trigger-user comment (examples-flows.md), the organization-pinning comment (SKILL.md's runAs system rule), the WeChat aside's "write no plugin" half (SKILL.md § Integrations), and a wording tightening in the toolsSnapshot paragraph that drops no fact. Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude --- .../references/integration-recipes.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index 07ce0c45df5..9e48f90b02b 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -27,8 +27,8 @@ connector is declared. Full guide: https://objectstack.ai/docs/automation/connec or `paramName` for a query-string key); `basic` (`username` stays in metadata); `signed` on `openapi` / `rest` (`scheme` `aliyun-acs3-hmac-sha256`, `tencentcloud-tc3-hmac-sha256` or `alipay-v3`, - `keyId`) signs each request — WeChat Pay has no scheme yet: mark it blocked, - write no plugin; `oauth2` / `app-credential`: recipe 6. A secret in a url path has no variant — use a + `keyId`) signs each request — WeChat Pay has no scheme yet: blocked; + `oauth2` / `app-credential`: recipe 6. A secret in a url path has no variant — use a token-authenticated connector. `authentication: { token }` is refused at `os validate`. - `defineConnector`: `@objectstack/spec/integration`; `defineFlow`: `@objectstack/spec`; both register in `defineStack`. @@ -44,14 +44,14 @@ export const StripeConnector = defineConnector({ // crm_account gains `stripe_available` (currency) and `stripe_synced_at` (datetime). export const StripeBalanceSweep = defineFlow({ name: 'stripe_balance_sweep', label: 'Stripe balance → house account', - type: 'schedule', runAs: 'system', // a scheduled run has no trigger user + type: 'schedule', runAs: 'system', nodes: [ { id: 'start', type: 'start', label: 'Nightly 03:00', config: { schedule: { type: 'cron', expression: '0 3 * * *' } } }, { id: 'bal', type: 'connector_action', label: 'GET /v1/balance', connectorConfig: { connectorId: 'stripe', actionId: 'GetBalance', input: {} } }, { id: 'write', type: 'update_record', label: 'Write balance', config: { - objectName: 'crm_account', filter: { is_house_account: true }, // multi-tenant: pin the organization too + objectName: 'crm_account', filter: { is_house_account: true }, fields: { stripe_available: { dialect: 'cel', source: 'bal.body.available[0].amount / 100.0' }, stripe_synced_at: { dialect: 'cel', source: 'isoDatetime(now())' } } } }, { id: 'end', type: 'end', label: 'End' }, @@ -89,8 +89,8 @@ export const GitHubMcp = defineConnector({ `tools/list` definition; a pinned name `include` leaves out is refused at boot. Digests: `computeMcpToolsSnapshot(tools)` (`@objectstack/connector-mcp`), or a 64-hex placeholder pin, then the served digest from the degraded reason. The -instance degrades with no actions, the reason on `GET /api/v1/automation/connectors` -naming each tool: a pinned tool served differently is `CONNECTOR_UPSTREAM_CHANGED` +instance degrades with no actions; the `GET /api/v1/automation/connectors` reason +names each tool: a pinned tool served differently is `CONNECTOR_UPSTREAM_CHANGED` with the digest it now serves (review, re-pin); one no longer listed is the credential path when the instance's credential reached the server, as this `bearer` does — the stored credential marked `rejected`: fix the credential and @@ -171,7 +171,8 @@ auth: { type: 'app-credential', vendor: 'feishu', appId: 'cli_a1b2', credentialR `client_credentials` and `app-credential` dispatch once the ref is set. `authorization_code` is degraded "not connected" until an administrator holding `manage_connector_connections` connects it, once per organization, from a -signed-in session: `POST /api/v1/automation/connectors/:name/connection` answers -`{ authorizationUrl }`. An `authorization_code` instance on `mcp` may omit the -`https://` endpoints and `clientId`, discovered at connect. +signed-in session (an agent or API key is refused): +`POST /api/v1/automation/connectors/:name/connection` answers `{ authorizationUrl }`; +the vendor redirects to `GET …/connectors/oauth/callback`. An `authorization_code` +instance on `mcp` may omit the `https://` endpoints and `clientId`, discovered at connect. Still never park a short-lived token behind a `bearer` ref.