@zackbart/connecta 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +126 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/call-recovery.js +25 -0
  9. package/dist/catalog-cache.js +2 -2
  10. package/dist/catalog-service.js +103 -63
  11. package/dist/config-schema.d.ts +14 -7
  12. package/dist/config-schema.js +29 -10
  13. package/dist/config.d.ts +2 -0
  14. package/dist/config.js +12 -7
  15. package/dist/connectors/guarded-fetch.d.ts +9 -1
  16. package/dist/connectors/guarded-fetch.js +17 -5
  17. package/dist/connectors/negotiation-cache.js +1 -1
  18. package/dist/downstream-input-context.js +9 -0
  19. package/dist/downstream-input.js +13 -11
  20. package/dist/errors.d.ts +38 -12
  21. package/dist/errors.js +19 -9
  22. package/dist/execute.js +375 -43
  23. package/dist/executor-result.js +11 -9
  24. package/dist/executors/quickjs-child.js +12 -5
  25. package/dist/executors/quickjs-protocol.js +5 -6
  26. package/dist/executors/quickjs-runtime.js +6 -1
  27. package/dist/executors/quickjs.js +20 -4
  28. package/dist/guest-runtime.js +52 -2
  29. package/dist/guest-types.d.ts +47 -5
  30. package/dist/index.d.ts +1 -1
  31. package/dist/invocation.js +72 -14
  32. package/dist/meta-output.js +0 -15
  33. package/dist/meta-tools.js +166 -95
  34. package/dist/oauth-handoff.js +88 -11
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +9 -0
  37. package/dist/operator-ui/view.js +11 -0
  38. package/dist/program-result.js +5 -0
  39. package/dist/program-writes.js +7 -48
  40. package/dist/provider.d.ts +8 -6
  41. package/dist/provider.js +3 -4
  42. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  43. package/dist/providers/_shared/request-token.js +83 -0
  44. package/dist/providers/_shared/rest/dispatch.js +41 -0
  45. package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
  46. package/dist/providers/_shared/rest/operation-index.js +527 -0
  47. package/dist/providers/_shared/rest/tools.d.ts +141 -0
  48. package/dist/providers/_shared/rest/tools.js +569 -0
  49. package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
  50. package/dist/providers/_shared/rest/value-safety.js +580 -0
  51. package/dist/providers/cloudflare/graphql.js +385 -0
  52. package/dist/providers/cloudflare/index.d.ts +47 -51
  53. package/dist/providers/cloudflare/index.js +188 -1997
  54. package/dist/providers/cloudflare/openapi.generated.js +3347 -0
  55. package/dist/providers/cloudflare/rest.d.ts +37 -0
  56. package/dist/providers/cloudflare/rest.js +1056 -0
  57. package/dist/providers/cloudflare/skill.generated.js +7 -4
  58. package/dist/providers/cloudflare/value-safety.js +1247 -0
  59. package/dist/providers/infisical/index.d.ts +7 -0
  60. package/dist/providers/infisical/index.js +713 -0
  61. package/dist/providers/infisical/skill.generated.js +8 -0
  62. package/dist/providers/notion/index.d.ts +35 -31
  63. package/dist/providers/notion/index.js +60 -1499
  64. package/dist/providers/notion/openapi.generated.js +75 -0
  65. package/dist/providers/notion/rest.d.ts +24 -0
  66. package/dist/providers/notion/rest.js +1067 -0
  67. package/dist/providers/notion/skill.generated.js +5 -4
  68. package/dist/providers/notion/value-safety.js +31 -0
  69. package/dist/providers/stripe/index.d.ts +26 -20
  70. package/dist/providers/stripe/index.js +103 -130
  71. package/dist/providers/stripe/openapi.generated.js +649 -0
  72. package/dist/providers/stripe/rest.d.ts +14 -0
  73. package/dist/providers/stripe/rest.js +395 -0
  74. package/dist/providers/stripe/skill.generated.js +7 -5
  75. package/dist/providers/stripe/value-safety.js +705 -0
  76. package/dist/providers/vercel/index.d.ts +31 -22
  77. package/dist/providers/vercel/index.js +96 -853
  78. package/dist/providers/vercel/openapi.generated.js +456 -0
  79. package/dist/providers/vercel/rest.d.ts +20 -0
  80. package/dist/providers/vercel/rest.js +1002 -0
  81. package/dist/providers/vercel/skill.generated.js +4 -5
  82. package/dist/providers/vercel/value-safety.js +328 -0
  83. package/dist/registry.d.ts +8 -6
  84. package/dist/registry.js +134 -61
  85. package/dist/resource-read.js +4 -1
  86. package/dist/routes/mcp.js +11 -2
  87. package/dist/sent-secrets.d.ts +54 -13
  88. package/dist/sent-secrets.js +770 -231
  89. package/dist/skills.js +26 -16
  90. package/dist/sqlite.js +21 -6
  91. package/dist/storage/keys.js +20 -5
  92. package/dist/storage/memory.js +9 -4
  93. package/dist/storage/sql.js +18 -9
  94. package/dist/types.d.ts +17 -3
  95. package/dist/usage-guide.js +49 -40
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/worker.js +2 -1
  99. package/dist/write-outcome.js +47 -0
  100. package/documentation/README.md +64 -0
  101. package/documentation/architecture.md +241 -97
  102. package/documentation/auth.md +138 -5
  103. package/documentation/code-mode.md +224 -107
  104. package/documentation/deploying.md +146 -0
  105. package/documentation/integrating.md +170 -0
  106. package/documentation/meta-tools.md +142 -30
  107. package/documentation/operating.md +135 -0
  108. package/documentation/operator-tests.md +4 -0
  109. package/documentation/operator-ui.md +132 -0
  110. package/documentation/provider-auth-migration.md +307 -0
  111. package/documentation/provider-migration-0.29.md +22 -5
  112. package/examples/worker/README.md +9 -2
  113. package/package.json +9 -2
  114. package/templates/node/README.md +9 -2
  115. package/templates/node/package.json +1 -1
  116. package/dist/providers/vercel/mcp-ownership.js +0 -212
@@ -1,10 +1,11 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "\n## Identity before routing\n\nThis explicit `surface: \"api\"` connection acts as an internal integration.\nIts `integration_*` tools are canonical for bot-scoped/headless operations.\nHosted MCP is the default and acts as its OAuth-authorized user. Authorization\nis not equivalent: an integration token cannot authorize hosted MCP, and user\nOAuth cannot reproduce the integration bot's ownership/sharing boundary.\nConfigure each under a separate connector id and authorize it independently.\nDo not switch to a user identity to recover a failed integration operation.\n\nUse the bot tools when the task requires that integration identity. Use hosted\n`notion-search`, `notion-fetch`, `notion-get-users`, `notion-get-comments`,\n`notion-create-pages`, `notion-update-page`, and `notion-create-comment` for\nuser-owned content work. Hosted MCP additionally covers sessions, agents,\nattachments and broader workspace operations. REST tool names are not MCP aliases.\n\n\n\n## Databases contain data sources\n\nA Notion database is a container; the rows and the schema live in a *data\nsource* inside it. The two ids are not interchangeable and Notion rejects the\nwrong one.\n\n- The id in a Notion database URL is a **database id**.\n- `integration_get_database` turns it into the `data_sources` list — usually one entry.\n- `integration_get_data_source_schema` and `integration_query_data_source` take that\n **data_source_id**, and so does `integration_create_page` when adding a row.\n\nSo the sequence for \"find rows in this database\" is `integration_get_database` →\n`integration_get_data_source_schema` → `integration_query_data_source`. `integration_search` returns data\nsources directly and skips the first step.\n\n## Property quirks that break writes\n\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read `integration_get_data_source_schema` before composing one.\n- `select` and `status` writes must use an existing option name; inventing\n one fails. The schema lists the valid options.\n- Writes **replace** a property. Sending one item to a `multi_select` or\n `relation` drops the others, so send the complete intended value.\n- `rollup`, `formula`, `unique_id`, and the created/edited fields are\n computed. They cannot be written.\n- A page's title column is rarely called \"title\" — pass `title_property` from\n the schema when creating or updating a row.\n- Notion truncates `title`, `rich_text`, `relation`, and `people` at 25\n entries. `integration_get_page` reports each one in `truncated_properties` as\n `{ name, id }`; pass that `id` as `property_id` to\n `integration_get_page_property` for the complete value.\n\n## Reading page content\n\n`integration_get_page` returns properties. `integration_get_page_content` returns the body as flat\nblocks. Nested blocks (toggles, list children, table rows) need `depth`, and\neach level multiplies requests — a deep read stops at an internal ceiling and\nreports `truncated: true` rather than spending the whole rate-limit budget.\n\n## Appending is append-only\n\n`integration_append_blocks` adds children and nothing else. It cannot move, reorder, or\nreplace an existing block, and a block appended through the API can never be\nrelocated by it afterwards. Get the position right the first time with\n`position` and `after_block_id`. Notion caps one call at 100 blocks.\n\n## Lean by default, raw on request\n\nEvery read projects Notion's payload down to ids, plain text, and flattened\nproperty values. Where the dropped detail can matter — `integration_search`, `integration_get_page`,\n`integration_get_page_content`, `integration_get_page_property`, `integration_get_data_source_schema`,\n`integration_query_data_source`, `integration_list_comments` — pass `raw: true` to get Notion's\nuntouched response instead. It is much larger; reach for it only when a\nspecific field is missing. Narrow `integration_query_data_source` and `integration_get_page` with\n`properties` instead whenever the goal is fewer fields, not more.\n\n`integration_get_page_content` with `raw: true` returns one level exactly as Notion\nsent it and does not walk nested children, so `depth` is ignored alongside\nit. A block type this projection does not model keeps its payload under\n`raw` on the block itself, so nothing silently flattens to an empty string.\n\n## Failures worth reading carefully\n\n- **404** means \"no such object\" *or* \"not shared with this integration\", and\n Notion will not say which. Never conclude a page was deleted from it; check\n that the page is shared with the integration in Notion.\n- **403** is not an expired token. The integration is missing a capability\n (comment capabilities are off by default) or the object was never shared.\n Re-authorizing cannot fix it; an operator must change it in Notion.\n- **429** carries a retry window. Notion allows roughly three requests per\n second per integration, so wait it out rather than retrying immediately.\n\n## No escape hatch\n\nThis connection has no guarded raw-REST tool, deliberately. Notion's public\nAPI is small and slow-moving enough for the named surface to cover it, so\nthere is no `notion_api_*` to reach for — an operation absent from the tool\nlist is absent from this connection, not hidden behind a generic call.\n\n## Writes and pagination\n\n- Notion has **no idempotency key**. A retried `integration_create_page` or\n `integration_add_comment` creates a duplicate. Confirm with `integration_search` before repeating\n a write that may have partially succeeded.\n- List tools take `page_size` (max 100) and return `has_more` with\n `next_cursor`. Pass a cursor back verbatim — it is opaque and must never be\n parsed or constructed. Follow pages inside `execute_code` and reduce there.\n# Notion MCP usage\n\nOfficial MCP interface: tool names, descriptions, argument schemas, and result\nschemas come from Notion's live server. Connecta preserves that catalog and\napplies release-reviewed classification. Reviewed writes stay writes even when Notion claims they only read.\n\nWorkspace purpose: \n\n- This connection acts as its OAuth-authorized user. The separately configured\n `surface: \"api\"` connection retains required internal-integration operations\n under `integration_*` names, including headless content writes, cursor-complete\n queries, exact JSON blocks and property items. No identity fallback occurs.\n- Discover the live catalog before assuming a tool exists. Notion can gate\n tools by workspace, account, client, and rollout independently of Connecta.\n- Start broad discovery with `notion-search`, then use `notion-fetch` on\n the exact page, database, data source, or object before changing it.\n- Use the live input schema as the contract. Notion owns these MCP schemas;\n the REST schemas in Connecta's API interface do not apply to MCP tools with\n similar names.\n- Session and agent tools can launch asynchronous work. Read session state and\n events before sending another message, waiting, or stopping a session.\n- File uploads and attachment tools create durable workspace state. Keep\n downloads inside the requested task and do not expose signed attachment URLs.\n- An `auth_required` failure means this connector's OAuth grant is missing or\n expired. Run `authorize_connector` for this connector id, then retry.\n",
3
+ "content": "\n## Notion's data model\n\n- A database is a container; its rows and schema live in a *data source*\n inside it. Queries, schema reads, and new rows take the data source, and\n Notion rejects a database id in its place.\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read the schema before composing one.\n- `select` and `status` writes must use an existing option name; inventing\n one fails. The schema lists the valid options.\n- Writes **replace** a property. Sending one item to a `multi_select` or\n `relation` drops the others, so send the complete intended value.\n- `rollup`, `formula`, `unique_id`, and the created and edited fields are\n computed. They cannot be written.\n- A data source's title column is rarely called \"title\". Read its name from\n the schema before creating or renaming a row.\n- Notion has **no idempotency key**. A retried page create or comment makes a\n duplicate. Confirm with a search before repeating a write that may have\n partially succeeded.\n",
4
4
  "instructionsHeading": "Workspace instructions",
5
5
  "fragments": {
6
- "guide_0": "\n## Identity before routing\n\nThis explicit `surface: \"api\"` connection acts as an internal integration.\nIts `integration_*` tools are canonical for bot-scoped/headless operations.\nHosted MCP is the default and acts as its OAuth-authorized user. Authorization\nis not equivalent: an integration token cannot authorize hosted MCP, and user\nOAuth cannot reproduce the integration bot's ownership/sharing boundary.\nConfigure each under a separate connector id and authorize it independently.\nDo not switch to a user identity to recover a failed integration operation.\n\nUse the bot tools when the task requires that integration identity. Use hosted\n`notion-search`, `notion-fetch`, `notion-get-users`, `notion-get-comments`,\n`notion-create-pages`, `notion-update-page`, and `notion-create-comment` for\nuser-owned content work. Hosted MCP additionally covers sessions, agents,\nattachments and broader workspace operations. REST tool names are not MCP aliases.\n\n\n\n## Databases contain data sources\n\nA Notion database is a container; the rows and the schema live in a *data\nsource* inside it. The two ids are not interchangeable and Notion rejects the\nwrong one.\n\n- The id in a Notion database URL is a **database id**.\n- `integration_get_database` turns it into the `data_sources` list — usually one entry.\n- `integration_get_data_source_schema` and `integration_query_data_source` take that\n **data_source_id**, and so does `integration_create_page` when adding a row.\n\nSo the sequence for \"find rows in this database\" is `integration_get_database` →\n`integration_get_data_source_schema` → `integration_query_data_source`. `integration_search` returns data\nsources directly and skips the first step.\n\n## Property quirks that break writes\n\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read `integration_get_data_source_schema` before composing one.\n- `select` and `status` writes must use an existing option name; inventing\n one fails. The schema lists the valid options.\n- Writes **replace** a property. Sending one item to a `multi_select` or\n `relation` drops the others, so send the complete intended value.\n- `rollup`, `formula`, `unique_id`, and the created/edited fields are\n computed. They cannot be written.\n- A page's title column is rarely called \"title\" — pass `title_property` from\n the schema when creating or updating a row.\n- Notion truncates `title`, `rich_text`, `relation`, and `people` at 25\n entries. `integration_get_page` reports each one in `truncated_properties` as\n `{ name, id }`; pass that `id` as `property_id` to\n `integration_get_page_property` for the complete value.\n\n## Reading page content\n\n`integration_get_page` returns properties. `integration_get_page_content` returns the body as flat\nblocks. Nested blocks (toggles, list children, table rows) need `depth`, and\neach level multiplies requests — a deep read stops at an internal ceiling and\nreports `truncated: true` rather than spending the whole rate-limit budget.\n\n## Appending is append-only\n\n`integration_append_blocks` adds children and nothing else. It cannot move, reorder, or\nreplace an existing block, and a block appended through the API can never be\nrelocated by it afterwards. Get the position right the first time with\n`position` and `after_block_id`. Notion caps one call at 100 blocks.\n\n## Lean by default, raw on request\n\nEvery read projects Notion's payload down to ids, plain text, and flattened\nproperty values. Where the dropped detail can matter — `integration_search`, `integration_get_page`,\n`integration_get_page_content`, `integration_get_page_property`, `integration_get_data_source_schema`,\n`integration_query_data_source`, `integration_list_comments` — pass `raw: true` to get Notion's\nuntouched response instead. It is much larger; reach for it only when a\nspecific field is missing. Narrow `integration_query_data_source` and `integration_get_page` with\n`properties` instead whenever the goal is fewer fields, not more.\n\n`integration_get_page_content` with `raw: true` returns one level exactly as Notion\nsent it and does not walk nested children, so `depth` is ignored alongside\nit. A block type this projection does not model keeps its payload under\n`raw` on the block itself, so nothing silently flattens to an empty string.\n\n## Failures worth reading carefully\n\n- **404** means \"no such object\" *or* \"not shared with this integration\", and\n Notion will not say which. Never conclude a page was deleted from it; check\n that the page is shared with the integration in Notion.\n- **403** is not an expired token. The integration is missing a capability\n (comment capabilities are off by default) or the object was never shared.\n Re-authorizing cannot fix it; an operator must change it in Notion.\n- **429** carries a retry window. Notion allows roughly three requests per\n second per integration, so wait it out rather than retrying immediately.\n\n## No escape hatch\n\nThis connection has no guarded raw-REST tool, deliberately. Notion's public\nAPI is small and slow-moving enough for the named surface to cover it, so\nthere is no `notion_api_*` to reach for — an operation absent from the tool\nlist is absent from this connection, not hidden behind a generic call.\n\n## Writes and pagination\n\n- Notion has **no idempotency key**. A retried `integration_create_page` or\n `integration_add_comment` creates a duplicate. Confirm with `integration_search` before repeating\n a write that may have partially succeeded.\n- List tools take `page_size` (max 100) and return `has_more` with\n `next_cursor`. Pass a cursor back verbatim — it is opaque and must never be\n parsed or constructed. Follow pages inside `execute_code` and reduce there.\n",
7
- "guide_1": "# Notion MCP usage\n\nOfficial MCP interface: tool names, descriptions, argument schemas, and result\nschemas come from Notion's live server. Connecta preserves that catalog and\napplies release-reviewed classification. Reviewed writes stay writes even when Notion claims they only read.\n\nWorkspace purpose: ",
8
- "guide_2": "\n\n- This connection acts as its OAuth-authorized user. The separately configured\n `surface: \"api\"` connection retains required internal-integration operations\n under `integration_*` names, including headless content writes, cursor-complete\n queries, exact JSON blocks and property items. No identity fallback occurs.\n- Discover the live catalog before assuming a tool exists. Notion can gate\n tools by workspace, account, client, and rollout independently of Connecta.\n- Start broad discovery with `notion-search`, then use `notion-fetch` on\n the exact page, database, data source, or object before changing it.\n- Use the live input schema as the contract. Notion owns these MCP schemas;\n the REST schemas in Connecta's API interface do not apply to MCP tools with\n similar names.\n- Session and agent tools can launch asynchronous work. Read session state and\n events before sending another message, waiting, or stopping a session.\n- File uploads and attachment tools create durable workspace state. Keep\n downloads inside the requested task and do not expose signed attachment URLs.\n- An `auth_required` failure means this connector's OAuth grant is missing or\n expired. Run `authorize_connector` for this connector id, then retry.\n"
6
+ "oauth_head": "# Notion MCP usage\n\nOfficial MCP interface: tool names, descriptions, argument schemas, and result\nschemas come from Notion's live server. Connecta preserves that catalog and\napplies release-reviewed classification. Reviewed writes stay writes even when Notion claims they only read.\n\nWorkspace purpose: ",
7
+ "oauth": "\n\n- This connection acts as its OAuth-authorized user and reaches what that user\n can. It holds no integration token. An internal integration (bot) is a\n separate connector with `auth: { type: \"token\" }` under another id; never\n switch to it to recover a failed call here.\n- Discover the live catalog before assuming a tool exists. Notion can gate\n tools by workspace, account, client, and rollout independently of Connecta.\n- Start broad discovery with `notion-search`, then use `notion-fetch` on\n the exact page, database, data source, or object before changing it.\n- Use the live input schema as the contract. Notion owns these MCP schemas;\n the REST schemas of Connecta's token connector do not apply to MCP tools with\n similar names.\n- Session and agent tools can launch asynchronous work. Read session state and\n events before sending another message, waiting, or stopping a session.\n- File uploads and attachment tools create durable workspace state. Keep\n downloads inside the requested task and do not expose signed attachment URLs.\n- Connecta's generic REST tools (`notion_api_*`) and the `integration_*`\n helpers exist only on a token connector.\n- An `auth_required` failure means this connector's OAuth grant is missing or\n expired. Run `authorize_connector` for this connector id, then retry.\n",
8
+ "key": "\n## Identity and tools\n\nThis is Connecta's REST connector to `api.notion.com`, acting as one internal\nintegration (a bot) with an operator-managed integration token. It reaches\nexactly the pages and data sources shared with that integration, and the\nintegration's capabilities decide which calls succeed: comment and\nuser-information capabilities are off by default. It never acts as a person.\nHosted Notion MCP is a separate OAuth connector.\n\n- Named tools cover the common path and flatten Notion's payloads:\n `integration_search` (titles), `integration_get_page` (properties),\n `integration_get_page_content` (blocks as text),\n `integration_get_data_source_schema`, and `integration_query_data_source`\n (rows). `integration_create_page` and `integration_append_blocks` author\n Notion's rich-text and block shapes from plain text, Markdown, or checklists.\n- Four generic tools reach every other operation in the pinned API index.\n `notion_api_search` finds operations, not content. `notion_api_details`\n returns one operation's parameters. `notion_api_read` calls GETs plus the\n read-only POST queries: search, data source and meeting note queries, and\n agent, session, and session event queries. `notion_api_write` calls every\n other POST, PATCH, and DELETE with `method` and `path` stated.\n- Every generic call is checked against the index before it is sent. An\n unknown path returns the nearest operations, and an unknown or missing\n parameter returns `validation.issues` with the accepted names. Nothing\n reached Notion, so fix the call from that answer.\n- The generic set replaces the old one-operation tools. Read a database\n container with `GET /v1/databases/{database_id}`, users with `GET /v1/users`\n or `GET /v1/users/me`, comments with `GET /v1/comments`, and a page as\n Markdown with `GET /v1/pages/{page_id}/markdown`. Update properties, trash or\n restore (`in_trash`) with `PATCH /v1/pages/{page_id}`, and comment with\n `POST /v1/comments`.\n- Refused by design: Notion's OAuth token endpoints (`/v1/oauth/*`) and\n multipart file sends. Import a file with `POST /v1/file_uploads` and\n `mode: \"external_url\"` instead.\n- Notion-hosted file URLs in pages and blocks are pre-signed: Connecta\n withholds their signature, credential, and security-token parameters, so a\n returned URL names the file but does not download it. Credential-named\n fields read as `\"[redacted]\"`, a database property named like a credential\n (`API token`) included. Failures of the OAuth, file upload, bot user, and\n agent operations carry Notion's code, not its message.\n- Not here: the hosted MCP's user identity, its search across connected\n apps, page duplication, team and skill search, attachment downloads, and\n page-to-skill conversion. Those need an OAuth connector.\n\n## Databases contain data sources\n\n- The id in a Notion database URL is a **database id**.\n `GET /v1/databases/{database_id}` through `notion_api_read` lists its\n `data_sources`, usually one. `integration_search` returns data sources\n directly and skips that step.\n- `integration_get_data_source_schema` and `integration_query_data_source`\n take the **data_source_id**, and so does `integration_create_page` when\n adding a row.\n\nSo the sequence for \"find rows in this database\" is the database read, then\n`integration_get_data_source_schema`, then `integration_query_data_source`.\n\n## Reading\n\n- `integration_get_page` returns properties. `integration_get_page_content`\n returns the body as flat blocks. Nested blocks (toggles, list children,\n table rows) need `depth`, and each level multiplies requests. A deep read\n stops at an internal ceiling and reports `truncated: true` rather than\n spending the whole rate-limit budget.\n- Notion truncates `title`, `rich_text`, `relation`, and `people` at 25\n entries. `integration_get_page` reports each in `truncated_properties` as\n `{ name, id }`; read the complete value with `notion_api_read`\n `GET /v1/pages/{page_id}/properties/{property_id}` and follow its cursor.\n- Every named read projects Notion's payload to ids, plain text, and\n flattened property values. Pass `raw: true` for Notion's untouched response\n when a specific field is missing. It is much larger. Narrow\n `integration_query_data_source` and `integration_get_page` with `properties`\n when the goal is fewer fields. `integration_get_page_content` with\n `raw: true` returns one level and ignores `depth`.\n\n## Appending is append-only\n\n`integration_append_blocks` adds children and nothing else. It cannot move,\nreorder, or replace an existing block, and a block appended through the API can\nnever be relocated by it afterwards. Get the position right the first time with\n`position` and `after_block_id`. Notion caps one call at 100 blocks.\n\n## Pagination\n\n- Named list tools take `page_size` (max 100) and return `has_more` with\n `next_cursor`; pass it back as `start_cursor`.\n- Generic results arrive as `{ status, data, page? }`. On a list,\n `page.next` is the cursor and `page.param` is `start_cursor`: send it in\n `query` for a GET, and in `body` when `page.in` is `body`, as on the POST\n queries. Pass `select` dot paths relative to `data`, such as `results.id`;\n `page` survives any `select`.\n- A view query is created with `notion_api_write`\n `POST /v1/views/{view_id}/queries`, a write because Notion stores it until\n `expires_at`. Its first page names the continuation in `page.path`: read\n `GET /v1/views/{view_id}/queries/{query_id}` with `start_cursor` in `query`.\n- Meeting-note queries have no cursor. `limit` caps at 50, and\n `page.hasMore` without `page.next` means more notes exist: narrow the\n filter (for example by `created_time`) rather than paging.\n- Cursors are opaque: never parse or construct one. Follow pages inside\n `execute_code` and reduce there.\n\n## Failures worth reading carefully\n\n- **404** means \"no such object\" *or* \"not shared with this integration\", and\n Notion will not say which. Never conclude a page was deleted from it; check\n that the page is shared with the integration in Notion.\n- **403** is not an expired token. The integration is missing a capability\n (comment capabilities are off by default) or the object was never shared.\n Re-authorizing cannot fix it; an operator must change it in Notion.\n- **429** carries a retry window. Notion allows an average of three requests\n per second per integration; Connecta admits 180 calls a minute, three at a\n time, per runtime. Wait out the window rather than retrying immediately.\n",
9
+ "shared": "\n## Notion's data model\n\n- A database is a container; its rows and schema live in a *data source*\n inside it. Queries, schema reads, and new rows take the data source, and\n Notion rejects a database id in its place.\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read the schema before composing one.\n- `select` and `status` writes must use an existing option name; inventing\n one fails. The schema lists the valid options.\n- Writes **replace** a property. Sending one item to a `multi_select` or\n `relation` drops the others, so send the complete intended value.\n- `rollup`, `formula`, `unique_id`, and the created and edited fields are\n computed. They cannot be written.\n- A data source's title column is rarely called \"title\". Read its name from\n the schema before creating or renaming a row.\n- Notion has **no idempotency key**. A retried page create or comment makes a\n duplicate. Confirm with a search before repeating a write that may have\n partially succeeded.\n"
9
10
  }
10
11
  };
@@ -0,0 +1,31 @@
1
+ // Notion's reviewed value-safety table (#801), on the shared mechanism in
2
+ // `../_shared/rest/value-safety.ts`: every operation the detector flags in
3
+ // the pinned spec (`value-safety.candidates.json`) has one verdict here, and
4
+ // `value-safety.node.test.ts` runs the shared harness over it.
5
+ //
6
+ // Notion's API returns little that is a credential: an integration token
7
+ // never appears in a response. The OAuth endpoints mint, introspect, and
8
+ // revoke tokens with a public integration's client secret and are refused.
9
+ // A file upload's `upload_url` is Notion's own send endpoint and needs the
10
+ // integration token, so it is kept. Notion-hosted file URLs in pages and
11
+ // blocks are pre-signed: the shared URL rule withholds their signature,
12
+ // credential, and security-token query parameters on every response, so a
13
+ // returned URL names the file without granting access to it.
14
+ import { refuse, safe } from "../_shared/rest/value-safety.js";
15
+ const OAUTH = "Notion's OAuth token endpoints are not reachable through Connecta: they mint and revoke credentials with a public integration's client secret, which this connector never holds.";
16
+ const UPLOAD = "A file upload's upload_url is Notion's own send endpoint, which needs the integration token; it grants nothing alone.";
17
+ const AGENT = "Custom agents name their connected MCP server's host, never its URL or credentials.";
18
+ const OPERATIONS = {
19
+ "POST /v1/oauth/token": refuse(OAUTH),
20
+ "POST /v1/oauth/introspect": refuse(OAUTH),
21
+ "POST /v1/oauth/revoke": refuse(OAUTH),
22
+ "GET /v1/users/me": safe("Answers the integration's bot user; no token."),
23
+ "GET /v1/file_uploads": safe(UPLOAD, ["results.upload_url"]),
24
+ "POST /v1/file_uploads": safe(UPLOAD, ["upload_url"]),
25
+ "GET /v1/file_uploads/{file_upload_id}": safe(UPLOAD, ["upload_url"]),
26
+ "POST /v1/file_uploads/{file_upload_id}/complete": safe(UPLOAD, ["upload_url"]),
27
+ "POST /v1/file_uploads/{file_upload_id}/send": safe(UPLOAD, ["upload_url"]),
28
+ "POST /v1/agents/query": safe(AGENT, ["results.connections.account.server_host"]),
29
+ "GET /v1/agents/{agent_id}": safe(AGENT, ["connections.account.server_host"]),
30
+ };
31
+ export const NOTION_VALUE_SAFETY = { title: "Notion", operations: OPERATIONS };
@@ -1,10 +1,10 @@
1
- import { type RemoteMcpAuth } from "../../connectors/remote-mcp.js";
2
- /** Which Stripe environment a static credential reaches. */
3
- export type StripeMode = "production" | "sandbox";
1
+ import { type StripeMode } from "./rest.js";
2
+ export type { StripeMode } from "./rest.js";
3
+ export { STRIPE_API_BASE_URL } from "./rest.js";
4
4
  /** Stripe publishes one hosted MCP endpoint for every account and mode. */
5
5
  export declare const STRIPE_MCP_ENDPOINT = "https://mcp.stripe.com/";
6
6
  interface StripeCommonOptions {
7
- /** Human-readable display name; defaults to "Stripe" for OAuth. */
7
+ /** Human-readable display name; defaults to "Stripe" for OAuth and the mode for a key. */
8
8
  title?: string;
9
9
  /** Downstream auth ownership. Defaults to one shared deployment grant. */
10
10
  authScope?: "shared" | "personal";
@@ -15,33 +15,39 @@ interface StripeCommonOptions {
15
15
  /** Connector-specific inline result limit; omit to inherit the deployment. */
16
16
  maxResultBytes?: number;
17
17
  }
18
- /** OAuth sessions discover account and mode together from Stripe's live tools. */
18
+ /**
19
+ * Stripe's hosted MCP server over OAuth. One session can reach live and
20
+ * sandbox accounts; Stripe returns the mode with each account.
21
+ */
19
22
  export interface StripeOAuthOptions extends StripeCommonOptions {
20
- auth?: {
23
+ auth: {
21
24
  type: "oauth";
22
25
  };
26
+ /** Refused: OAuth reaches live and sandbox accounts, and Stripe returns mode with each. */
23
27
  mode?: never;
28
+ /** Refused: a Connect account needs an API-key connector. */
24
29
  connectedAccount?: never;
25
30
  }
26
31
  /**
27
- * Static credentials have one fixed mode, including Stripe Connect calls.
28
- *
29
- * Both static shapes belong here: a key the deployment supplies as a literal
30
- * header, and one the operator pastes into the connection in the operator UI.
31
- * Neither can discover its own mode, since a restricted key answers for
32
- * exactly one, so both declare it.
32
+ * Connecta's REST connector to `api.stripe.com` with one operator-managed
33
+ * secret or restricted key, pasted into the connection in the operator UI.
34
+ * A key reaches exactly one mode, so the connector declares it, and a key
35
+ * whose prefix says otherwise is refused before any request leaves.
33
36
  */
34
- export interface StripeHeaderOptions extends StripeCommonOptions {
35
- auth: Exclude<RemoteMcpAuth, {
36
- type: "oauth" | "request";
37
- }>;
37
+ export interface StripeApiKeyOptions extends StripeCommonOptions {
38
+ auth: {
39
+ type: "apiKey";
40
+ };
38
41
  mode: StripeMode;
39
- /** Act as one Connect account by sending Stripe's `Stripe-Account` header. */
42
+ /** Act as one Connect account: `Stripe-Account` on v1, `Stripe-Context` on v2. */
40
43
  connectedAccount?: string;
41
44
  }
42
- export type StripeOptions = StripeOAuthOptions | StripeHeaderOptions;
43
- /** A maintained Stripe hosted-MCP connection. */
45
+ export type StripeOptions = StripeOAuthOptions | StripeApiKeyOptions;
46
+ /**
47
+ * A maintained Stripe connection. `auth` selects the implementation: OAuth
48
+ * reaches Stripe's hosted MCP server; an API key reaches Connecta's REST
49
+ * connector to `api.stripe.com`.
50
+ */
44
51
  export declare const stripe: import("../../provider.js").ProviderFactory<StripeOptions>;
45
52
  /** @deprecated Read `stripe.definition.classify` instead. Kept for existing imports. */
46
53
  export declare const STRIPE_VETTED_CATALOG: import("../../catalog-drift.js").VettedCatalog;
47
- export {};
@@ -1,18 +1,21 @@
1
1
  import { skill } from "./skill.generated.js";
2
- import { remoteMcp, withCredentialDefaults } from "../../connectors/remote-mcp.js";
2
+ import { apiConnector } from "../../connectors/api-connector.js";
3
3
  import { reviewedCatalog } from "../../catalog-drift.js";
4
- import { keys, optionsOf, strings, variants } from "../../config-schema.js";
5
- import { CREDENTIAL } from "../../connectors/option-shapes.js";
4
+ import { keys, optionsOf, variants } from "../../config-schema.js";
6
5
  import { defineProvider } from "../../provider.js";
6
+ import { byAuth, hostedOAuth } from "../_shared/rest/dispatch.js";
7
+ import { restTools } from "../_shared/rest/tools.js";
8
+ import { stripeRest } from "./rest.js";
9
+ export { STRIPE_API_BASE_URL } from "./rest.js";
7
10
  /** Stripe publishes one hosted MCP endpoint for every account and mode. */
8
11
  export const STRIPE_MCP_ENDPOINT = "https://mcp.stripe.com/";
9
12
  /**
10
13
  * Stripe documents no MCP-specific limit, so these transcribe the account limit
11
- * MCP traffic spends: 100 requests per second live, 25 in a sandbox. OAuth takes
12
- * the sandbox rule (`mode ?? "sandbox"` below) because one session can reach
13
- * either mode and the policy is fixed before the account-scoped call begins.
14
- * The `maxConcurrency` figures are Connecta's own choice — Stripe says
15
- * per-account and per-endpoint concurrency limits exist but publishes no number.
14
+ * every request spends: 100 requests per second live, 25 in a sandbox. OAuth
15
+ * takes the sandbox rule because one session can reach either mode and the
16
+ * policy is fixed before the account-scoped call begins. The `maxConcurrency`
17
+ * figures are Connecta's own choice — Stripe says per-account and per-endpoint
18
+ * concurrency limits exist but publishes no number.
16
19
  */
17
20
  const STRIPE_ADMISSION = {
18
21
  production: {
@@ -40,7 +43,9 @@ const STRIPE_ADMISSION = {
40
43
  * Reviewed in #705's provider audit against https://docs.stripe.com/mcp.
41
44
  * Retains the release-reviewed inventory, including names absent from today's
42
45
  * public reference. Live annotations were not reverified without credentials.
43
- * No schema digest is asserted without a captured schema to review.
46
+ * No schema digest is asserted without a captured schema to review. This
47
+ * classifies the hosted OAuth catalog only; the REST connector annotates each
48
+ * tool it authors.
44
49
  */
45
50
  const STRIPE_CLASSIFICATION = {
46
51
  tools: {
@@ -109,158 +114,126 @@ const STRIPE_CLASSIFICATION = {
109
114
  },
110
115
  },
111
116
  };
112
- /** Stripe key prefixes carry their own mode; only a clear reading counts. */
113
- const LIVE_KEY = /\b(?:sk|rk|pk)_live_/;
114
- const TEST_KEY = /\b(?:sk|rk|pk)_test_/;
115
- /**
116
- * Refuse a recognizable key/mode mismatch. This is the only mode guard Connecta
117
- * can offer, and it needs the key as a literal header to read the prefix: an
118
- * operator-managed key is not in the deployment file, so its declared mode
119
- * stands alone and a key pointed at the other environment fails at Stripe.
120
- */
121
- function assertModeMatchesKey(id, mode, auth) {
122
- if (auth.type !== "headers")
123
- return;
124
- for (const value of Object.values(auth.headers)) {
125
- const keyMode = LIVE_KEY.test(value) ? "production" : TEST_KEY.test(value) ? "sandbox" : undefined;
126
- if (keyMode !== undefined && keyMode !== mode) {
127
- throw new Error(`stripe("${id}") declares mode "${mode}" but its auth headers carry a ` +
128
- `${keyMode === "production" ? "live" : "test"}-mode Stripe key.`);
129
- }
130
- }
131
- }
132
- function resolveAuth(id, options) {
133
- const auth = withCredentialDefaults(options.auth ?? { type: "oauth" }, {
134
- credential: {
135
- label: "Secret or restricted API key",
136
- description: "A Stripe secret or restricted API key for this connector's declared mode. Stripe sends it as a bearer token; it is stored encrypted and never displayed.",
137
- placeholder: "sk_… or rk_…",
138
- },
139
- });
140
- const connectedAccount = options.connectedAccount?.trim();
141
- if (connectedAccount === undefined || connectedAccount === "")
142
- return auth;
143
- if (!connectedAccount.startsWith("acct_")) {
144
- throw new Error(`stripe("${id}") connectedAccount must be a Stripe account id ("acct_...").`);
145
- }
146
- if (auth.type === "oauth") {
147
- throw new Error(`stripe("${id}") cannot reach a connected account over OAuth; Stripe ` +
148
- `requires a restricted API key for Stripe-Account calls.`);
149
- }
150
- if (auth.type === "credential" || auth.type === "request") {
151
- // `Stripe-Account` is a second header beside the credential's own, and the
152
- // credential shape assembles exactly one. A Connect connector therefore
153
- // still takes its restricted key as a literal header.
154
- throw new Error(`stripe("${id}") cannot reach a connected account with an ` +
155
- `operator-managed credential; Stripe-Account is a second static ` +
156
- `header, so declare auth: { type: "headers" } for this connector.`);
157
- }
158
- return {
159
- type: "headers",
160
- headers: { ...auth.headers, "Stripe-Account": connectedAccount },
161
- };
117
+ function instructionsSection(instructions) {
118
+ const text = instructions?.trim();
119
+ return text ? `\n## ${skill.instructionsHeading}\n\n${text}\n` : "";
162
120
  }
163
121
  function oauthUsageGuide(purpose, instructions) {
164
- const accountInstructions = instructions?.trim();
122
+ const { oauth, shared, oauth_limits } = skill.fragments;
165
123
  return `# Stripe usage
166
124
 
167
- Scope: live and sandbox accounts. Connector purpose: ${purpose}${skill.fragments.guide_0}${sharedUsageGuide("100 requests per second in live mode and 25 in sandbox mode")}
168
- ${accountInstructions ? `\n## ${skill.instructionsHeading}\n\n${accountInstructions}\n` : ""}`;
125
+ Scope: live and sandbox accounts. Connector purpose: ${purpose}${oauth}${shared}${oauth_limits}
126
+ ${instructionsSection(instructions)}`;
169
127
  }
170
128
  const MODE_COPY = {
171
129
  production: {
172
130
  title: "Stripe (production)",
173
131
  blurb: "production — live money and real customers",
174
132
  warning: "This is a PRODUCTION Stripe connection. Every write moves real money against real customers, and a refund cannot be undone. If a request could plausibly be a rehearsal, route it to a sandbox connector instead.",
133
+ rate: "100 requests per second",
175
134
  },
176
135
  sandbox: {
177
136
  title: "Stripe (sandbox)",
178
137
  blurb: "sandbox — test data, no real money",
179
138
  warning: "This is a SANDBOX Stripe connection. Nothing here is real money and none of these objects exist in production, so never answer a question about live revenue, payouts, or a named customer from this connector.",
139
+ rate: "25 requests per second",
180
140
  },
181
141
  };
182
- function fixedModeUsageGuide(mode, purpose, instructions) {
142
+ function keyUsageGuide(mode, purpose, connectedAccount, instructions) {
183
143
  const copy = MODE_COPY[mode];
184
- const accountInstructions = instructions?.trim();
144
+ const { key, shared, key_limits, key_tail } = skill.fragments;
145
+ const connect = connectedAccount
146
+ ? `\n\nConnected account: \`${connectedAccount}\`. Every call acts as this Connect account; the platform account's own objects are out of reach here.`
147
+ : "";
185
148
  return `# Stripe usage
186
149
 
187
150
  Mode: ${mode}. Connector purpose: ${purpose}
188
151
 
189
- ${copy.warning}${skill.fragments.guide_1}${sharedUsageGuide(`${mode === "production" ? "100" : "25"} requests per second`)}
190
- ${accountInstructions ? `\n## ${skill.instructionsHeading}\n\n${accountInstructions}\n` : ""}`;
191
- }
192
- function sharedUsageGuide(rate) {
193
- return `${skill.fragments.guide_2}${rate}${skill.fragments.guide_3}`;
152
+ ${copy.warning}${connect}${key}${shared}${key_limits}${copy.rate}${key_tail}
153
+ ${instructionsSection(instructions)}`;
194
154
  }
195
- /** The closed options stripe() accepts; see `assertKnownOptions`. */
196
- const STRIPE_OPTIONS = optionsOf()({
197
- ...keys("title", "authScope", "purpose", "instructions", "maxResultBytes", "mode", "connectedAccount"),
198
- auth: variants("type", {
199
- oauth: keys("type"),
200
- headers: optionsOf()({ ...keys("type"), headers: strings() }).shape,
201
- credential: optionsOf()({
202
- ...keys("type", "header", "scheme"),
203
- credential: CREDENTIAL,
204
- }).shape,
205
- }),
155
+ const COMMON = keys("title", "authScope", "purpose", "instructions", "maxResultBytes");
156
+ /** The closed options stripe() accepts, selected by `auth.type`; see `assertKnownOptions`. */
157
+ const STRIPE_OPTIONS = variants(["auth", "type"], {
158
+ // The never-typed keys stay out of the shape, so the walk refuses them by name.
159
+ oauth: optionsOf()({
160
+ ...COMMON,
161
+ auth: optionsOf()(keys("type")),
162
+ }).shape,
163
+ apiKey: optionsOf()({
164
+ ...COMMON,
165
+ ...keys("mode", "connectedAccount"),
166
+ auth: optionsOf()(keys("type")),
167
+ }).shape,
206
168
  });
207
- /** A maintained Stripe hosted-MCP connection. */
169
+ function stripeOAuth(id, options, provider) {
170
+ return hostedOAuth(id, provider, {
171
+ url: STRIPE_MCP_ENDPOINT,
172
+ title: options.title ?? "Stripe",
173
+ description: `Stripe payments (live and sandbox accounts) — ${options.purpose}`,
174
+ callAdmission: STRIPE_ADMISSION.sandbox,
175
+ usageGuide: {
176
+ content: oauthUsageGuide(options.purpose, options.instructions),
177
+ // Explicit rather than derived: OAuth must lead with its account-scoped selector pair.
178
+ summary: "Live and sandbox Stripe accounts. List accounts; carry the returned stripe_context and livemode before acting.",
179
+ },
180
+ });
181
+ }
182
+ function stripeApiKey(id, options, provider) {
183
+ const mode = options.mode;
184
+ if (mode !== "production" && mode !== "sandbox") {
185
+ throw new Error(`stripe("${id}") requires mode "production" or "sandbox" with apiKey auth.`);
186
+ }
187
+ let connectedAccount;
188
+ if (options.connectedAccount !== undefined) {
189
+ connectedAccount = typeof options.connectedAccount === "string" ? options.connectedAccount.trim() : "";
190
+ if (!/^acct_[A-Za-z0-9]+$/.test(connectedAccount)) {
191
+ throw new Error(`stripe("${id}") connectedAccount must be a Stripe account id ("acct_...").`);
192
+ }
193
+ }
194
+ const copy = MODE_COPY[mode];
195
+ const live = mode === "production" ? "live" : "test";
196
+ const rest = stripeRest(mode, connectedAccount);
197
+ return apiConnector(id, {
198
+ ...provider.connectorOptions,
199
+ title: options.title ?? copy.title,
200
+ description: `Stripe payments (${copy.blurb}) — ${options.purpose}`,
201
+ credential: {
202
+ label: `Stripe ${live}-mode secret or restricted key`,
203
+ description: `A ${live}-mode secret (sk_${live}_…) or restricted (rk_${live}_…) key; prefer a restricted key with only the permissions this connector needs. It is sent only to api.stripe.com and files.stripe.com, stored encrypted, and never displayed.`,
204
+ placeholder: `rk_${live}_…`,
205
+ },
206
+ testCredential: rest.testCredential,
207
+ callAdmission: STRIPE_ADMISSION[mode],
208
+ usageGuide: {
209
+ content: keyUsageGuide(mode, options.purpose, connectedAccount, options.instructions),
210
+ // Explicit rather than derived: a key connector must lead with its one mode.
211
+ summary: mode === "production"
212
+ ? "PRODUCTION: real money. One live-mode key over REST; search operations, read details, then read or write."
213
+ : "Sandbox: test data only. One test-mode key over REST; search operations, read details, then read or write.",
214
+ },
215
+ tools: [...restTools(rest.vendor), ...rest.tools],
216
+ });
217
+ }
218
+ /**
219
+ * A maintained Stripe connection. `auth` selects the implementation: OAuth
220
+ * reaches Stripe's hosted MCP server; an API key reaches Connecta's REST
221
+ * connector to `api.stripe.com`.
222
+ */
208
223
  export const stripe = defineProvider({
209
224
  name: "stripe",
210
225
  title: "Stripe",
211
- kind: "mcp",
226
+ kind: "dual",
212
227
  readme: "Stripe",
213
- bundle: { "baselineGzip": 129103, "maxGzip": 189103 },
228
+ bundle: {
229
+ "baselineGzip": 232089,
230
+ "maxGzip": 292089,
231
+ "note": "./providers/stripe remeasures at 232,089 B gzip (#801), from 170,771 B on main: auth now selects the implementation, so the entry carries the hosted MCP client and Connecta's REST connector (api(), the schema validator, guarded transport, the shared REST module) plus the pinned Stripe operation index (638 operations; request details to depth 2, without descriptions or enums over 50 values; 37,918 B gzip as source). Details were shrunk before the cap moved: the first full index measured 81 KB gzip. The cap uses the existing baseline + 60,000 B policy.",
232
+ },
214
233
  skill,
215
234
  options: STRIPE_OPTIONS,
216
235
  classify: STRIPE_CLASSIFICATION,
217
- create: stripeConnector,
236
+ create: byAuth({ oauth: stripeOAuth, apiKey: stripeApiKey }),
218
237
  });
219
- function stripeConnector(id, options, provider) {
220
- const purpose = options.purpose.trim();
221
- const auth = resolveAuth(id, options);
222
- const mode = "mode" in options ? options.mode : undefined;
223
- if (auth.type === "oauth" && mode !== undefined) {
224
- throw new Error(`stripe("${id}") cannot declare a connector-wide mode for OAuth; Stripe returns mode with each account.`);
225
- }
226
- if (auth.type !== "oauth" && mode !== "production" && mode !== "sandbox") {
227
- throw new Error(`stripe("${id}") with headers or credential auth requires mode ` + `"production" or "sandbox".`);
228
- }
229
- // Only a literal header can be inspected. An operator-managed credential is
230
- // not readable at construction — there is nothing in the deployment file to
231
- // read — so the declared mode stands alone, and a key pointed at the other
232
- // one is Stripe's own refusal to report.
233
- if (auth.type === "headers") {
234
- assertModeMatchesKey(id, mode, auth);
235
- }
236
- const copy = mode === undefined ? undefined : MODE_COPY[mode];
237
- const connector = remoteMcp(id, {
238
- url: STRIPE_MCP_ENDPOINT,
239
- ...provider.connectorOptions,
240
- title: options.title ?? copy?.title ?? "Stripe",
241
- description: mode === undefined
242
- ? `Stripe payments (live and sandbox accounts) — ${purpose}`
243
- : `Stripe payments (${copy?.blurb}) — ${purpose}`,
244
- auth,
245
- requireHttps: true,
246
- classify: provider.classify,
247
- callAdmission: STRIPE_ADMISSION[mode ?? "sandbox"],
248
- usageGuide: {
249
- content: mode === undefined
250
- ? oauthUsageGuide(purpose, options.instructions)
251
- : fixedModeUsageGuide(mode, purpose, options.instructions),
252
- // Explicit rather than derived: fixed credentials must lead with mode,
253
- // while OAuth must lead with its account-scoped selector pair.
254
- summary: mode === undefined
255
- ? "Live and sandbox Stripe accounts. List accounts; carry the returned stripe_context and livemode before acting."
256
- : mode === "production"
257
- ? "PRODUCTION: real money. This static credential has one fixed live-mode scope."
258
- : "Sandbox: test data only. This static credential has one fixed sandbox scope.",
259
- // Not `required`. The four generic tools are the routing decision; a
260
- // guide forced into every call would pay for the same prose repeatedly.
261
- },
262
- });
263
- return connector;
264
- }
265
238
  /** @deprecated Read `stripe.definition.classify` instead. Kept for existing imports. */
266
239
  export const STRIPE_VETTED_CATALOG = reviewedCatalog(stripe.definition.classify, 'defineProvider("stripe")');