@zackbart/connecta 0.30.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.
- package/CHANGELOG.md +32 -0
- package/README.md +6 -3
- package/bin/version.mjs +1 -1
- package/dist/call-recovery.js +25 -0
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/connectors/guarded-fetch.d.ts +9 -1
- package/dist/connectors/guarded-fetch.js +14 -2
- package/dist/errors.d.ts +2 -0
- package/dist/invocation.js +32 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +11 -0
- package/dist/program-writes.js +2 -47
- package/dist/provider.d.ts +7 -5
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/rest/dispatch.js +41 -0
- package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
- package/dist/providers/_shared/rest/operation-index.js +527 -0
- package/dist/providers/_shared/rest/tools.d.ts +141 -0
- package/dist/providers/_shared/rest/tools.js +569 -0
- package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
- package/dist/providers/_shared/rest/value-safety.js +580 -0
- package/dist/providers/cloudflare/graphql.js +385 -0
- package/dist/providers/cloudflare/index.d.ts +47 -51
- package/dist/providers/cloudflare/index.js +188 -1997
- package/dist/providers/cloudflare/openapi.generated.js +3347 -0
- package/dist/providers/cloudflare/rest.d.ts +37 -0
- package/dist/providers/cloudflare/rest.js +1056 -0
- package/dist/providers/cloudflare/skill.generated.js +7 -4
- package/dist/providers/cloudflare/value-safety.js +1247 -0
- package/dist/providers/notion/index.d.ts +35 -31
- package/dist/providers/notion/index.js +60 -1499
- package/dist/providers/notion/openapi.generated.js +75 -0
- package/dist/providers/notion/rest.d.ts +24 -0
- package/dist/providers/notion/rest.js +1067 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- package/dist/providers/stripe/index.d.ts +26 -20
- package/dist/providers/stripe/index.js +103 -130
- package/dist/providers/stripe/openapi.generated.js +649 -0
- package/dist/providers/stripe/rest.d.ts +14 -0
- package/dist/providers/stripe/rest.js +395 -0
- package/dist/providers/stripe/skill.generated.js +7 -5
- package/dist/providers/stripe/value-safety.js +705 -0
- package/dist/providers/vercel/index.d.ts +31 -22
- package/dist/providers/vercel/index.js +96 -853
- package/dist/providers/vercel/openapi.generated.js +456 -0
- package/dist/providers/vercel/rest.d.ts +20 -0
- package/dist/providers/vercel/rest.js +1002 -0
- package/dist/providers/vercel/skill.generated.js +4 -5
- package/dist/providers/vercel/value-safety.js +328 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +120 -8
- package/documentation/integrating.md +16 -5
- package/documentation/meta-tools.md +9 -2
- package/documentation/provider-auth-migration.md +307 -0
- package/documentation/provider-migration-0.29.md +17 -5
- package/examples/worker/README.md +2 -2
- package/package.json +3 -2
- package/templates/node/README.md +2 -2
- package/templates/node/package.json +1 -1
- 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##
|
|
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
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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
|
|
2
|
-
|
|
3
|
-
export
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
35
|
-
auth:
|
|
36
|
-
type: "
|
|
37
|
-
}
|
|
37
|
+
export interface StripeApiKeyOptions extends StripeCommonOptions {
|
|
38
|
+
auth: {
|
|
39
|
+
type: "apiKey";
|
|
40
|
+
};
|
|
38
41
|
mode: StripeMode;
|
|
39
|
-
/** Act as one Connect account
|
|
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 |
|
|
43
|
-
/**
|
|
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 {
|
|
2
|
+
import { apiConnector } from "../../connectors/api-connector.js";
|
|
3
3
|
import { reviewedCatalog } from "../../catalog-drift.js";
|
|
4
|
-
import { keys, optionsOf,
|
|
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
|
-
*
|
|
12
|
-
* the sandbox rule
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
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
|
|
122
|
+
const { oauth, shared, oauth_limits } = skill.fragments;
|
|
165
123
|
return `# Stripe usage
|
|
166
124
|
|
|
167
|
-
Scope: live and sandbox accounts. Connector purpose: ${purpose}${
|
|
168
|
-
${
|
|
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
|
|
142
|
+
function keyUsageGuide(mode, purpose, connectedAccount, instructions) {
|
|
183
143
|
const copy = MODE_COPY[mode];
|
|
184
|
-
const
|
|
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}${
|
|
190
|
-
${
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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: "
|
|
226
|
+
kind: "dual",
|
|
212
227
|
readme: "Stripe",
|
|
213
|
-
bundle: {
|
|
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:
|
|
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")');
|