@zackbart/connecta 0.18.0 → 0.18.2
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 +108 -0
- package/README.md +79 -118
- package/dist/connectors/remote-mcp.d.ts +49 -2
- package/dist/connectors/remote-mcp.js +302 -7
- package/dist/providers/linear.d.ts +7 -1
- package/dist/providers/linear.js +12 -2
- package/dist/providers/mixpanel.d.ts +5 -1
- package/dist/providers/mixpanel.js +17 -3
- package/dist/providers/revenuecat.d.ts +79 -0
- package/dist/providers/revenuecat.js +323 -0
- package/dist/providers/stripe.d.ts +9 -3
- package/dist/providers/stripe.js +29 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/connectors.md +26 -0
- package/documentation/linear.md +25 -4
- package/documentation/meta-tools.md +5 -0
- package/documentation/mixpanel.md +19 -0
- package/documentation/operations.md +2 -0
- package/documentation/provider-audit.md +32 -2
- package/documentation/provider-conventions.md +61 -36
- package/documentation/revenuecat.md +301 -0
- package/documentation/storage-and-credentials.md +55 -0
- package/documentation/stripe.md +32 -2
- package/documentation/upgrading.md +31 -4
- package/examples/worker/src/index.ts +5 -0
- package/package.json +5 -1
- package/templates/node/package.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
|
+
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
|
+
/** RevenueCat publishes one hosted MCP endpoint, streamable HTTP. */
|
|
4
|
+
export const REVENUECAT_MCP_ENDPOINT = "https://mcp.revenuecat.ai/mcp";
|
|
5
|
+
/**
|
|
6
|
+
* Tools whose official contract is observational rather than mutating.
|
|
7
|
+
*
|
|
8
|
+
* Every name here carries `Read` in RevenueCat's own tool reference
|
|
9
|
+
* (https://www.revenuecat.com/docs/tools/mcp/tools-reference, read
|
|
10
|
+
* 2026-08-18). The list is a superset by design (P5): a name a project never
|
|
11
|
+
* serves costs nothing, while an unclassified new one fails closed onto
|
|
12
|
+
* `call_destructive_tool`.
|
|
13
|
+
*/
|
|
14
|
+
const READ_ONLY_TOOLS = new Set([
|
|
15
|
+
// Projects and apps
|
|
16
|
+
"get-account-billing",
|
|
17
|
+
"get-app",
|
|
18
|
+
"get-project-ui-config",
|
|
19
|
+
"list-account-billing-invoices",
|
|
20
|
+
"list-app-public-api-keys",
|
|
21
|
+
"list-apps",
|
|
22
|
+
"list-audit-logs",
|
|
23
|
+
"list-collaborators",
|
|
24
|
+
"list-projects",
|
|
25
|
+
// Products and prices
|
|
26
|
+
"get-product",
|
|
27
|
+
"get-product-store-state",
|
|
28
|
+
"get-product-store-state-operation",
|
|
29
|
+
"list-products",
|
|
30
|
+
// Entitlements
|
|
31
|
+
"get-entitlement",
|
|
32
|
+
"get-products-from-entitlement",
|
|
33
|
+
"list-entitlements",
|
|
34
|
+
// Offerings and packages
|
|
35
|
+
"get-offering",
|
|
36
|
+
"get-offering-prices",
|
|
37
|
+
"list-offerings",
|
|
38
|
+
"list-packages",
|
|
39
|
+
// Targeting and audiences
|
|
40
|
+
"get-audience",
|
|
41
|
+
"get-audience-filter-options",
|
|
42
|
+
"get-targeting-rule",
|
|
43
|
+
"list-audiences",
|
|
44
|
+
"list-targeting-rules",
|
|
45
|
+
// Paywalls
|
|
46
|
+
"get-paywall",
|
|
47
|
+
"list-paywalls",
|
|
48
|
+
// Customers and subscriptions
|
|
49
|
+
"get-customer",
|
|
50
|
+
"get-customer-center-config",
|
|
51
|
+
"get-subscription",
|
|
52
|
+
"list-customer-events",
|
|
53
|
+
"list-customers",
|
|
54
|
+
"list-purchases",
|
|
55
|
+
"list-subscriptions",
|
|
56
|
+
"list-virtual-currencies-balances",
|
|
57
|
+
// Virtual currencies
|
|
58
|
+
"get-virtual-currency",
|
|
59
|
+
"list-virtual-currencies",
|
|
60
|
+
// Charts, metrics, and experiments
|
|
61
|
+
"get-benchmarks",
|
|
62
|
+
"get-chart-data",
|
|
63
|
+
"get-chart-options-schema",
|
|
64
|
+
"get-experiment",
|
|
65
|
+
"get-experiment-results",
|
|
66
|
+
"get-overview-metrics",
|
|
67
|
+
"get-revenue-metric",
|
|
68
|
+
"list-experiments",
|
|
69
|
+
// Integrations and webhooks
|
|
70
|
+
"get-webhook-integration",
|
|
71
|
+
"list-webhook-integrations",
|
|
72
|
+
// SDK compatibility
|
|
73
|
+
"list-sdk-feature-gates",
|
|
74
|
+
"list-sdk-versions",
|
|
75
|
+
// Paywall editing. Polling an async task is a read; the two tools that
|
|
76
|
+
// *start* one are writes and sit below.
|
|
77
|
+
"get-paywall-ai-task",
|
|
78
|
+
]);
|
|
79
|
+
/**
|
|
80
|
+
* The maintained write catalog. `"destructive"` tools modify or remove state
|
|
81
|
+
* that already exists; `"additive"` ones only bring something new into being.
|
|
82
|
+
* Both leave the read-only path — the distinction only decides whether the
|
|
83
|
+
* connection asserts `destructiveHint`, which shapes the host's approval copy.
|
|
84
|
+
*
|
|
85
|
+
* Every name here carries `Write` in RevenueCat's tool reference. The mass
|
|
86
|
+
* verdicts follow the verb: `archive-*` and `unarchive-*` flip an existing
|
|
87
|
+
* object's active state, `update-*` and `delete-*` and `publish-*` and
|
|
88
|
+
* `unpublish-*` and `detach-*` change or remove something that already exists,
|
|
89
|
+
* and a plain `create-*` brings a new object into being beside the old ones.
|
|
90
|
+
*
|
|
91
|
+
* Nine verdicts are not decided by the verb, and each is argued where it sits:
|
|
92
|
+
* `create-product-prices`, `equalize-subscription-prices`,
|
|
93
|
+
* `validate-app-credentials`, `upload-product-store-state-screenshot`,
|
|
94
|
+
* `attach-products-to-entitlement`, `attach-products-to-package`,
|
|
95
|
+
* `duplicate-paywall`, `create-paywall-ai`, and `edit-paywall-ai`.
|
|
96
|
+
*
|
|
97
|
+
* `render-paywall-screenshot` is deliberately on neither list. RevenueCat's
|
|
98
|
+
* reference gives it no access column at all, and a tool nobody has classified
|
|
99
|
+
* fails closed (P5) rather than being guessed into the read path because its
|
|
100
|
+
* name sounds harmless.
|
|
101
|
+
*/
|
|
102
|
+
const WRITE_TOOLS = new Map([
|
|
103
|
+
// Projects and apps
|
|
104
|
+
["create-app", "additive"],
|
|
105
|
+
["create-project", "additive"],
|
|
106
|
+
["update-app", "destructive"],
|
|
107
|
+
["update-project-ui-config", "destructive"],
|
|
108
|
+
// "Checks one saved App Store or Google Play credential set." RevenueCat
|
|
109
|
+
// files it Write, so it does not reach the read path — but it leaves the
|
|
110
|
+
// credentials themselves alone and only records the outcome of a check.
|
|
111
|
+
// Additive: a verdict comes into being, nothing existing is overwritten.
|
|
112
|
+
["validate-app-credentials", "additive"],
|
|
113
|
+
// Products and prices
|
|
114
|
+
["archive-product", "destructive"],
|
|
115
|
+
["create-product", "additive"],
|
|
116
|
+
// Named `create-`, described "Configure prices for a product". A product's
|
|
117
|
+
// price set already exists, and configuring it replaces what is there
|
|
118
|
+
// rather than adding a second price beside the first. Money-facing and
|
|
119
|
+
// overwriting, so: destructive, whatever the verb says.
|
|
120
|
+
["create-product-prices", "destructive"],
|
|
121
|
+
// "Fills missing App Store subscription territory prices." By RevenueCat's
|
|
122
|
+
// own word it only writes where a price is absent, so nothing already set
|
|
123
|
+
// is changed. Additive.
|
|
124
|
+
["equalize-subscription-prices", "additive"],
|
|
125
|
+
["set-product-store-state", "destructive"],
|
|
126
|
+
["submit-products-to-store", "destructive"],
|
|
127
|
+
["unarchive-product", "destructive"],
|
|
128
|
+
["update-product", "destructive"],
|
|
129
|
+
// "Reserves an App Store Connect review screenshot slot." A new slot comes
|
|
130
|
+
// into being; no existing screenshot is replaced by the reservation.
|
|
131
|
+
["upload-product-store-state-screenshot", "additive"],
|
|
132
|
+
// Entitlements
|
|
133
|
+
["archive-entitlement", "destructive"],
|
|
134
|
+
// Attach adds a product to a membership set and removes nothing;
|
|
135
|
+
// `detach-products-from-entitlement` is its destructive counterpart. Filing
|
|
136
|
+
// both destructive would make the pair read identically in the approval copy
|
|
137
|
+
// a human is shown, which is exactly the inflation P5 warns about.
|
|
138
|
+
["attach-products-to-entitlement", "additive"],
|
|
139
|
+
["create-entitlement", "additive"],
|
|
140
|
+
["detach-products-from-entitlement", "destructive"],
|
|
141
|
+
["unarchive-entitlement", "destructive"],
|
|
142
|
+
["update-entitlement", "destructive"],
|
|
143
|
+
// Offerings and packages
|
|
144
|
+
["archive-offering", "destructive"],
|
|
145
|
+
// The same attach/detach argument one level down.
|
|
146
|
+
["attach-products-to-package", "additive"],
|
|
147
|
+
["create-offering", "additive"],
|
|
148
|
+
["create-packages", "additive"],
|
|
149
|
+
["delete-package-from-offering", "destructive"],
|
|
150
|
+
["detach-products-from-package", "destructive"],
|
|
151
|
+
["unarchive-offering", "destructive"],
|
|
152
|
+
["update-offering", "destructive"],
|
|
153
|
+
// Targeting and audiences
|
|
154
|
+
["create-audience", "additive"],
|
|
155
|
+
["update-audience", "destructive"],
|
|
156
|
+
// Paywalls
|
|
157
|
+
// "Duplicates an existing paywall's current draft." The original is
|
|
158
|
+
// untouched and a new paywall appears beside it. Additive.
|
|
159
|
+
["duplicate-paywall", "additive"],
|
|
160
|
+
["publish-paywall", "destructive"],
|
|
161
|
+
["unpublish-paywall", "destructive"],
|
|
162
|
+
// Customers and subscriptions. Neither verb is in the mass rule and neither
|
|
163
|
+
// removes anything, so the file's own criterion would read them additive.
|
|
164
|
+
// They are destructive on consequence, the way `create_refund` is in
|
|
165
|
+
// `stripe.ts`: `assign-customer-offering` overrides which offering a live
|
|
166
|
+
// customer's app serves, and `grant-customer-entitlement` opens paid access
|
|
167
|
+
// to a real person without a store purchase (the promotional subscription it
|
|
168
|
+
// creates is the mechanism, not the point). Both change what a customer
|
|
169
|
+
// gets today, and both deserve the destructive approval copy.
|
|
170
|
+
["assign-customer-offering", "destructive"],
|
|
171
|
+
["grant-customer-entitlement", "destructive"],
|
|
172
|
+
// Virtual currencies
|
|
173
|
+
["archive-virtual-currency", "destructive"],
|
|
174
|
+
["create-virtual-currency", "additive"],
|
|
175
|
+
["unarchive-virtual-currency", "destructive"],
|
|
176
|
+
["update-virtual-currency", "destructive"],
|
|
177
|
+
// Integrations and webhooks
|
|
178
|
+
// A new integration is a new object, but one that "starts delivering
|
|
179
|
+
// RevenueCat events to the given url" — with filters omitted, every customer
|
|
180
|
+
// event in the project, to a URL the caller typed. That is customer data
|
|
181
|
+
// leaving the account on consequence, which is the `create_refund` argument
|
|
182
|
+
// again: the verb says additive, the effect says destructive, and the
|
|
183
|
+
// approval copy should say the latter.
|
|
184
|
+
["create-webhook-integration", "destructive"],
|
|
185
|
+
["delete-webhook-integration", "destructive"],
|
|
186
|
+
["update-webhook-integration", "destructive"],
|
|
187
|
+
// Paywall editing. Both start an async task; what the task does decides the
|
|
188
|
+
// verdict. Creating a paywall leaves every existing one alone (additive);
|
|
189
|
+
// editing one rewrites a draft that already exists (destructive).
|
|
190
|
+
["create-paywall-ai", "additive"],
|
|
191
|
+
["edit-paywall-ai", "destructive"],
|
|
192
|
+
]);
|
|
193
|
+
/**
|
|
194
|
+
* The manifest this release reviewed: both lists in one place, which is what
|
|
195
|
+
* makes the classification the connector applies and the drift check that runs
|
|
196
|
+
* beside it the same fact (P13). Ninety-four of the ninety-five tools
|
|
197
|
+
* RevenueCat's reference lists on 2026-08-18 are classified; the ninety-fifth,
|
|
198
|
+
* `render-paywall-screenshot`, has no access column to classify from and fails
|
|
199
|
+
* closed.
|
|
200
|
+
*
|
|
201
|
+
* No schema digests. No release has read RevenueCat's live schemas and written
|
|
202
|
+
* them down — that needs a live project and a maintainer's own `sk_` key — and
|
|
203
|
+
* an invented digest would report a change that never happened.
|
|
204
|
+
* `npm run drift:check -- --record` reads them from a live catalog and prints
|
|
205
|
+
* the block to paste in
|
|
206
|
+
* ([#351](https://github.com/zackbart/connecta/issues/351)).
|
|
207
|
+
*
|
|
208
|
+
* Exported because the maintainer-run check compares against this manifest and
|
|
209
|
+
* *names* what moved, which the runtime check deliberately cannot.
|
|
210
|
+
*/
|
|
211
|
+
export const REVENUECAT_VETTED_CATALOG = vettedCatalog({
|
|
212
|
+
reads: READ_ONLY_TOOLS,
|
|
213
|
+
writes: WRITE_TOOLS,
|
|
214
|
+
});
|
|
215
|
+
/** The catalog's summary bound; a longer declared value throws (`src/registry.ts`). */
|
|
216
|
+
const SUMMARY_BUDGET = 120;
|
|
217
|
+
/**
|
|
218
|
+
* Fit a purpose-bearing summary inside the catalog's bound.
|
|
219
|
+
*
|
|
220
|
+
* Stripe and Mixpanel declare static summaries because their routing fact is
|
|
221
|
+
* an enumerable variant. RevenueCat's is not: two `sk_` connectors have the
|
|
222
|
+
* same title, the same endpoint, and the same catalog, and differ only by the
|
|
223
|
+
* project the operator says each key reaches. So the summary carries that, and
|
|
224
|
+
* clipping is this function's job rather than the operator's.
|
|
225
|
+
*/
|
|
226
|
+
function boundedSummary(prefix, purpose) {
|
|
227
|
+
const full = `${prefix}${purpose}`;
|
|
228
|
+
if (full.length <= SUMMARY_BUDGET)
|
|
229
|
+
return full;
|
|
230
|
+
return `${full.slice(0, SUMMARY_BUDGET - 1).trimEnd()}…`;
|
|
231
|
+
}
|
|
232
|
+
function sharedUsageGuide() {
|
|
233
|
+
return `
|
|
234
|
+
- Resolve ids before acting; never guess one. \`list-projects\` yields the \`project_id\` every project-scoped call takes. \`list-apps\`, \`list-products\`, \`list-entitlements\`, \`list-offerings\`, \`list-paywalls\`, \`list-audiences\`, and \`list-customers\` yield the ids their \`get-\`, \`update-\`, \`archive-\`, and \`delete-\` counterparts expect. A plausible-looking id belongs to another project or to nobody.
|
|
235
|
+
- Customers are addressed by the app user id your SDK set, not by an internal key. Find one with \`list-customers\` before \`get-customer\`, and carry the id it returned unchanged.
|
|
236
|
+
- Customer and subscription objects are large, and a customer's history is larger. Page with the cursor the list returned rather than raising the page size, and reduce inside \`execute_code\` — select the fields the question needs and return those, not the whole object.
|
|
237
|
+
- Whether a customer should have access is \`gives_access\` on each subscription from \`list-subscriptions\`, which RevenueCat calls the authoritative flag. \`status\` and \`expires_date\` describe the store-side state and disagree with it during grace periods, billing retries, and promotional grants — answer access questions from \`gives_access\` and say which subscription it came from.
|
|
238
|
+
- \`get-chart-data\` is the metrics path: read \`get-chart-options-schema\` for the chart you want before calling it, rather than guessing an option name. \`get-overview-metrics\` and \`get-revenue-metric\` answer the summary questions in one call.
|
|
239
|
+
- \`create-paywall-ai\`, \`edit-paywall-ai\`, and \`set-product-store-state\` are asynchronous. They return a task or operation id; poll it with \`get-paywall-ai-task\` or \`get-product-store-state-operation\` rather than assuming the work finished when the call returned.
|
|
240
|
+
- This connection's tool list is not a fixed set. RevenueCat gates parts of its MCP catalog by plan, platform, and beta enrollment — paywall AI editing, benchmarks, experiments, virtual currencies, and the account-billing tools are the usual absentees — so search this connector for what it actually exposes rather than assuming a documented tool is here.
|
|
241
|
+
- \`render-paywall-screenshot\` is unclassified on purpose: RevenueCat's reference gives it no access column, so it fails closed onto \`call_destructive_tool\` until a release reviews it.
|
|
242
|
+
- RevenueCat meters API v2 per minute and per domain, and the domains differ: 480 requests per minute for customer information and virtual currencies, 60 for project configuration and audiences, 25 for charts and metrics. It answers a breach with \`429\`, a \`Retry-After\` header, and a \`backoff_ms\` field. Back off on that rather than retrying immediately, and expect chart sweeps to hit the ceiling long before customer reads do.
|
|
243
|
+
- Treat every create, update, archive, unarchive, attach, detach, delete, publish, unpublish, grant, assign, and submit operation as a write. Connecta routes the maintained write catalog through \`call_destructive_tool\`; newly added tools also fail closed until a release classifies them.
|
|
244
|
+
- An \`auth_required\` failure means this connector's RevenueCat authorization is missing or expired: run \`authorize_connector\` for this connector id, then retry the same call unchanged. A rejected argument, a permission gap, or a plan restriction comes back in RevenueCat's own words instead — read it rather than re-authorizing.
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
function oauthUsageGuide(purpose, instructions) {
|
|
248
|
+
const projectInstructions = instructions?.trim();
|
|
249
|
+
return `# RevenueCat usage
|
|
250
|
+
|
|
251
|
+
Account-scoped connection: this OAuth session reaches every RevenueCat project the account can see. Connector purpose: ${purpose}
|
|
252
|
+
|
|
253
|
+
Call \`list-projects\` first and carry the exact \`project_id\` it returned into every project-scoped call. Connecta does not pick a project, and the connector id, title, and purpose are routing hints rather than proof of which project a call will land in. If more than one project fits the request, stop and ask; never guess a \`project_id\`.
|
|
254
|
+
${sharedUsageGuide()}${projectInstructions
|
|
255
|
+
? `\n## Project instructions\n\n${projectInstructions}\n`
|
|
256
|
+
: ""}`;
|
|
257
|
+
}
|
|
258
|
+
function keyUsageGuide(purpose, instructions) {
|
|
259
|
+
const projectInstructions = instructions?.trim();
|
|
260
|
+
return `# RevenueCat usage
|
|
261
|
+
|
|
262
|
+
Single-project connection: ${purpose}. RevenueCat secret API keys are project-wide, so this key reaches exactly one project and nothing outside it. A second project is a second connector with its own key and its own id — never a \`project_id\` argument pointed somewhere else.
|
|
263
|
+
|
|
264
|
+
Confirm the project on first use: \`list-projects\` returns the one project this key can see, and its \`project_id\` is the one every project-scoped call takes. An empty or unexpected result means wrong connector, not missing data.
|
|
265
|
+
|
|
266
|
+
A RevenueCat secret key is issued read-only or write-enabled, and connecta cannot tell which this one is. It does not filter writes for a read-only key: every write is offered, reaches RevenueCat, and fails there in RevenueCat's own words. Read that refusal as "this key cannot write" rather than as a bad argument, and route the write to a connector configured with a write-enabled key.
|
|
267
|
+
${sharedUsageGuide()}${projectInstructions
|
|
268
|
+
? `\n## Project instructions\n\n${projectInstructions}\n`
|
|
269
|
+
: ""}`;
|
|
270
|
+
}
|
|
271
|
+
/** A maintained RevenueCat hosted-MCP connection. */
|
|
272
|
+
export function revenuecat(id, options) {
|
|
273
|
+
const purpose = options.purpose.trim();
|
|
274
|
+
if (!purpose) {
|
|
275
|
+
throw new Error("revenuecat() requires a non-empty project purpose.");
|
|
276
|
+
}
|
|
277
|
+
const auth = withCredentialDefaults(options.auth ?? { type: "oauth" }, {
|
|
278
|
+
credential: {
|
|
279
|
+
label: "API v2 secret key",
|
|
280
|
+
description: "A RevenueCat API v2 secret key. It reaches exactly one project, which is why two projects are two connectors; it is stored encrypted and never displayed.",
|
|
281
|
+
placeholder: "sk_…",
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
// Both static shapes reach one project. Where the key came from — the
|
|
285
|
+
// deployment file or the operator page — changes nothing an agent must know
|
|
286
|
+
// about scope, so the title, description, and guide follow the scope alone.
|
|
287
|
+
const scoped = auth.type !== "oauth";
|
|
288
|
+
const connector = remoteMcp(id, {
|
|
289
|
+
url: REVENUECAT_MCP_ENDPOINT,
|
|
290
|
+
// The scope shape rides the title because browse-time discovery renders
|
|
291
|
+
// the title and the guide summary and nothing else, and reaching one
|
|
292
|
+
// project versus every project the account has is the fact an agent must
|
|
293
|
+
// not get wrong between two RevenueCat connections.
|
|
294
|
+
title: options.title ?? (scoped ? "RevenueCat (single project)" : "RevenueCat"),
|
|
295
|
+
description: scoped
|
|
296
|
+
? `RevenueCat subscriptions and revenue (one project, static key) — ${purpose}`
|
|
297
|
+
: `RevenueCat subscriptions and revenue (every project the account can reach) — ${purpose}`,
|
|
298
|
+
auth,
|
|
299
|
+
requireHttps: true,
|
|
300
|
+
usageGuide: {
|
|
301
|
+
content: scoped
|
|
302
|
+
? keyUsageGuide(purpose, options.instructions)
|
|
303
|
+
: oauthUsageGuide(purpose, options.instructions),
|
|
304
|
+
// Explicit rather than derived, and purpose-bearing rather than static:
|
|
305
|
+
// the derived summary would cut the scoping sentence mid-clause at 120
|
|
306
|
+
// characters, and two static summaries would leave two `sk_` connectors
|
|
307
|
+
// indistinguishable in the one field search returns (P3).
|
|
308
|
+
summary: scoped
|
|
309
|
+
? boundedSummary("One project only: ", purpose)
|
|
310
|
+
: boundedSummary("All account projects; list-projects first: ", purpose),
|
|
311
|
+
// Not `required`. RevenueCat's own schemas describe each call; the guide
|
|
312
|
+
// carries the project-resolution sequence, which is worth reading before
|
|
313
|
+
// a run rather than before every call.
|
|
314
|
+
},
|
|
315
|
+
...(options.callAdmission !== undefined
|
|
316
|
+
? { callAdmission: options.callAdmission }
|
|
317
|
+
: {}),
|
|
318
|
+
...(options.maxResultBytes !== undefined
|
|
319
|
+
? { maxResultBytes: options.maxResultBytes }
|
|
320
|
+
: {}),
|
|
321
|
+
});
|
|
322
|
+
return withVettedCatalog(connector, REVENUECAT_VETTED_CATALOG);
|
|
323
|
+
}
|
|
@@ -22,10 +22,16 @@ export interface StripeOAuthOptions extends StripeCommonOptions {
|
|
|
22
22
|
mode?: never;
|
|
23
23
|
connectedAccount?: never;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Static credentials have one fixed mode, including Stripe Connect calls.
|
|
27
|
+
*
|
|
28
|
+
* Both static shapes belong here: a key the deployment supplies as a literal
|
|
29
|
+
* header, and one the operator pastes at `/credentials`. Neither can discover
|
|
30
|
+
* its own mode — a restricted key answers for exactly one — so both declare it.
|
|
31
|
+
*/
|
|
26
32
|
export interface StripeHeaderOptions extends StripeCommonOptions {
|
|
27
|
-
auth:
|
|
28
|
-
type: "
|
|
33
|
+
auth: Exclude<RemoteMcpAuth, {
|
|
34
|
+
type: "oauth";
|
|
29
35
|
}>;
|
|
30
36
|
mode: StripeMode;
|
|
31
37
|
/** Act as one Connect account by sending Stripe's `Stripe-Account` header. */
|
package/dist/providers/stripe.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { remoteMcp, } from "../connectors/remote-mcp.js";
|
|
1
|
+
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
2
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
3
|
/** Stripe publishes one hosted MCP endpoint for every account and mode. */
|
|
4
4
|
export const STRIPE_MCP_ENDPOINT = "https://mcp.stripe.com/";
|
|
@@ -109,17 +109,31 @@ function assertModeMatchesKey(id, mode, auth) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
function resolveAuth(id, options) {
|
|
112
|
-
const auth = options.auth ?? { type: "oauth" }
|
|
112
|
+
const auth = withCredentialDefaults(options.auth ?? { type: "oauth" }, {
|
|
113
|
+
credential: {
|
|
114
|
+
label: "Secret or restricted API key",
|
|
115
|
+
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.",
|
|
116
|
+
placeholder: "sk_… or rk_…",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
113
119
|
const connectedAccount = options.connectedAccount?.trim();
|
|
114
120
|
if (connectedAccount === undefined || connectedAccount === "")
|
|
115
121
|
return auth;
|
|
116
122
|
if (!connectedAccount.startsWith("acct_")) {
|
|
117
123
|
throw new Error(`stripe("${id}") connectedAccount must be a Stripe account id ("acct_...").`);
|
|
118
124
|
}
|
|
119
|
-
if (auth.type
|
|
125
|
+
if (auth.type === "oauth") {
|
|
120
126
|
throw new Error(`stripe("${id}") cannot reach a connected account over OAuth; Stripe ` +
|
|
121
127
|
`requires a restricted API key for Stripe-Account calls.`);
|
|
122
128
|
}
|
|
129
|
+
if (auth.type === "credential") {
|
|
130
|
+
// `Stripe-Account` is a second header beside the credential's own, and the
|
|
131
|
+
// credential shape assembles exactly one. A Connect connector therefore
|
|
132
|
+
// still takes its restricted key as a literal header.
|
|
133
|
+
throw new Error(`stripe("${id}") cannot reach a connected account with an ` +
|
|
134
|
+
`operator-managed credential; Stripe-Account is a second static ` +
|
|
135
|
+
`header, so declare auth: { type: "headers" } for this connector.`);
|
|
136
|
+
}
|
|
123
137
|
return {
|
|
124
138
|
type: "headers",
|
|
125
139
|
headers: { ...auth.headers, "Stripe-Account": connectedAccount },
|
|
@@ -173,7 +187,11 @@ function sharedUsageGuide(rate) {
|
|
|
173
187
|
- Four generic tools reach any Stripe API method. Find the method with \`stripe_api_search\`, read its parameters with \`stripe_api_details\`, then call \`stripe_api_read\` (GET) or \`stripe_api_write\` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — \`stripe_api_details\` is cheaper than a rejected write.
|
|
174
188
|
- Prefer a dedicated tool when one covers the task: \`get_stripe_account_info\` for account information, \`get_balance_summary\` for balances, \`create_refund\` for refunds, \`stripe_report\` for reports. One call instead of three, and a refund named \`create_refund\` reads far more clearly in the approval a human sees than the same refund buried in \`stripe_api_write\` arguments.
|
|
175
189
|
- \`stripe_api_write\` carries the blast radius of the entire write API — every POST, PATCH, PUT, and DELETE, from a customer edit to a subscription cancellation. State the method and path explicitly; expect approval on every call.
|
|
176
|
-
- Lists are cursor-paginated: \`limit\` defaults to 10 and caps at 100, \`starting_after\` and \`ending_before\` take an object id and are mutually exclusive, and \`has_more\` says whether to continue. Page inside \`execute_code
|
|
190
|
+
- Lists are cursor-paginated: \`limit\` defaults to 10 and caps at 100, \`starting_after\` and \`ending_before\` take an object id and are mutually exclusive, and \`has_more\` says whether to continue. Page inside \`execute_code\`.
|
|
191
|
+
- Any \`stripe_api_read\` list or \`stripe_api_search\` that returns full objects belongs inside \`execute_code\`, projected to the fields the question needs before \`return\`. Neither \`limit\` nor \`expand\` substitutes for that: an unprojected list of customers or invoices truncates long before it answers, and a projected one keeps the customer's name, email, and address out of the transcript.
|
|
192
|
+
- Search filters on a documented per-resource field set, not on arbitrary attributes. Charges search takes \`amount\`, \`created\`, \`currency\`, \`customer\`, \`status\`, \`refunded\`, \`disputed\`, \`metadata\`, \`billing_details.address.postal_code\`, and \`payment_method_details.<source>.*\` card fields — there is no \`payment_intent\` field. When the field you want is not searchable, retrieve the parent object and follow its reference (the PaymentIntent's \`latest_charge\`) instead of retrying the search with another spelling.
|
|
193
|
+
- The account → \`stripe_api_search\` → \`stripe_api_details\` → \`stripe_api_read\` sequence is one program, not four turns: resolve the method once, then call that method as many times as the investigation needs in the same run.
|
|
194
|
+
- Decline outcomes live on the charge — \`outcome\`, \`failure_code\`, \`failure_message\` — reached from the PaymentIntent's \`latest_charge\`, so "why did this payment fail" is a PaymentIntent read followed by one charge read.
|
|
177
195
|
- Resolve ids before acting; never guess one. Stripe ids are typed prefixes — \`cus_\` customer, \`sub_\` subscription, \`ch_\` charge, \`pi_\` payment intent, \`in_\` invoice, \`acct_\` account — and a plausible-looking id belongs to a different object or to nobody. Find the object with \`stripe_api_search\` (or a list endpoint through \`stripe_api_read\`) and carry the \`id\` it returned into the write.
|
|
178
196
|
- This connection's tool list is not a fixed set. Stripe gates parts of its MCP catalog by account, integration, and beta enrollment, so search this connector for what it actually exposes rather than assuming a documented tool is here.
|
|
179
197
|
- Amounts are integers in the currency's minor unit: \`1099\` is 10.99 USD, and zero-decimal currencies like JPY take \`10\` for 10 JPY. Never send a decimal.
|
|
@@ -195,9 +213,14 @@ export function stripe(id, options) {
|
|
|
195
213
|
if (auth.type === "oauth" && mode !== undefined) {
|
|
196
214
|
throw new Error(`stripe("${id}") cannot declare a connector-wide mode for OAuth; Stripe returns mode with each account.`);
|
|
197
215
|
}
|
|
198
|
-
if (auth.type
|
|
199
|
-
throw new Error(`stripe("${id}") with headers auth requires mode
|
|
216
|
+
if (auth.type !== "oauth" && mode !== "production" && mode !== "sandbox") {
|
|
217
|
+
throw new Error(`stripe("${id}") with headers or credential auth requires mode ` +
|
|
218
|
+
`"production" or "sandbox".`);
|
|
200
219
|
}
|
|
220
|
+
// Only a literal header can be inspected. An operator-managed credential is
|
|
221
|
+
// not readable at construction — there is nothing in the deployment file to
|
|
222
|
+
// read — so the declared mode stands alone, and a key pointed at the other
|
|
223
|
+
// one is Stripe's own refusal to report.
|
|
201
224
|
if (auth.type === "headers") {
|
|
202
225
|
assertModeMatchesKey(id, mode, auth);
|
|
203
226
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -122,6 +122,7 @@ Maintained provider guides:
|
|
|
122
122
|
- [Linear](./linear.md)
|
|
123
123
|
- [Mixpanel](./mixpanel.md)
|
|
124
124
|
- [Notion](./notion.md)
|
|
125
|
+
- [RevenueCat](./revenuecat.md)
|
|
125
126
|
- [Stripe](./stripe.md)
|
|
126
127
|
|
|
127
128
|
## The `api()` construction contract
|
|
@@ -293,3 +294,28 @@ server issuer discovered and validated by the SDK; see
|
|
|
293
294
|
[storage and credentials](./storage-and-credentials.md#downstream-oauth).
|
|
294
295
|
The callback route validates `state` before passing the complete callback query
|
|
295
296
|
to the SDK so RFC 9207 `iss` validation is not lost.
|
|
297
|
+
|
|
298
|
+
A remote MCP connector that authenticates with a static key has two ways to
|
|
299
|
+
receive one. `{ type: "headers", headers }` bakes the literal value into the
|
|
300
|
+
deployment file, which suits a secret the runtime already holds.
|
|
301
|
+
`{ type: "credential" }` declares the slot instead and lets an operator paste
|
|
302
|
+
the key at `/credentials`, where it is encrypted at rest and rotatable without
|
|
303
|
+
a redeploy:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
remoteMcp("revenuecat_bepresent", {
|
|
307
|
+
url: "https://mcp.revenuecat.ai/mcp",
|
|
308
|
+
auth: {
|
|
309
|
+
type: "credential",
|
|
310
|
+
credential: { label: "API v2 secret key" },
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Header name and framing are configurable — `header` defaults to
|
|
316
|
+
`Authorization`, `scheme` to `Bearer`, `scheme: null` sends the value bare, and
|
|
317
|
+
a `Basic` framing base64-encodes a `user:secret` pair. Every maintained hosted
|
|
318
|
+
connection takes the shape through its existing `auth` option and fills in its
|
|
319
|
+
own label and framing. The full behavior, including rotation and the empty-slot
|
|
320
|
+
state, is in
|
|
321
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
package/documentation/linear.md
CHANGED
|
@@ -65,9 +65,12 @@ connection; it now answers 404.
|
|
|
65
65
|
## Authentication
|
|
66
66
|
|
|
67
67
|
OAuth 2.1 with dynamic client registration is the default and keeps each
|
|
68
|
-
connector instance's flow and tokens in its connector-scoped storage. Linear
|
|
69
|
-
also
|
|
70
|
-
`Authorization
|
|
68
|
+
connector instance's flow and tokens in its connector-scoped storage. Linear's
|
|
69
|
+
MCP server also "supports passing OAuth token and API keys directly in the
|
|
70
|
+
`Authorization: Bearer <yourtoken>` header instead of using the interactive
|
|
71
|
+
authentication flow" ([Linear MCP docs](https://linear.app/docs/mcp)), which
|
|
72
|
+
suits a headless deployment. Note the framing: the bare-`Authorization`
|
|
73
|
+
convention is Linear's *GraphQL* API, and this endpoint is not that.
|
|
71
74
|
|
|
72
75
|
```ts
|
|
73
76
|
linear("automation_tracker", {
|
|
@@ -75,7 +78,7 @@ linear("automation_tracker", {
|
|
|
75
78
|
access: "read-only",
|
|
76
79
|
auth: {
|
|
77
80
|
type: "headers",
|
|
78
|
-
headers: { Authorization: env.LINEAR_API_KEY },
|
|
81
|
+
headers: { Authorization: `Bearer ${env.LINEAR_API_KEY}` },
|
|
79
82
|
},
|
|
80
83
|
});
|
|
81
84
|
```
|
|
@@ -85,6 +88,24 @@ configuration. A personal API key carries the acting user's full workspace
|
|
|
85
88
|
permissions, so pair it with `access: "read-only"` unless the deployment
|
|
86
89
|
genuinely writes.
|
|
87
90
|
|
|
91
|
+
The same key can arrive from `/credentials` instead, which is what a deployment
|
|
92
|
+
with no secret store — or an operator who rotates keys without a redeploy —
|
|
93
|
+
wants:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
linear("automation_tracker", {
|
|
97
|
+
purpose: "Headless release reporting",
|
|
98
|
+
access: "read-only",
|
|
99
|
+
auth: { type: "credential" },
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The slot renders as "Personal API key" and Connecta sends the stored value as
|
|
104
|
+
`Authorization: Bearer <key>`, the framing Linear's MCP page documents; pass
|
|
105
|
+
`credential` or `scheme` to override either. Until the operator saves a value
|
|
106
|
+
the connector is present and reports `auth_required`. See
|
|
107
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
|
108
|
+
|
|
88
109
|
## Safety classification
|
|
89
110
|
|
|
90
111
|
The wrapper classifies Linear's documented `list_*`, `get_*`, and
|
|
@@ -310,6 +310,11 @@ handoff:
|
|
|
310
310
|
declared credential label and field names/guidance; or
|
|
311
311
|
- `unavailable`: an honest deployment/configuration message.
|
|
312
312
|
|
|
313
|
+
The class follows what the connector declares, not how it was authored: a
|
|
314
|
+
`remoteMcp()` connection using `auth: { type: "credential" }` declares a slot
|
|
315
|
+
and no OAuth flow, so it lands in `operator_config` beside every `api()`
|
|
316
|
+
credential.
|
|
317
|
+
|
|
313
318
|
The tool accepts no secret. `force` applies only to OAuth and may discard its
|
|
314
319
|
stored grant before restarting consent. Static credential values are written
|
|
315
320
|
only through the same-origin, Clerk-operator credential route. After OAuth
|
|
@@ -50,6 +50,25 @@ password, not ordinary configuration. Mixpanel currently labels service-account
|
|
|
50
50
|
MCP authentication beta. Prefer OAuth unless the deployment is intentionally
|
|
51
51
|
headless.
|
|
52
52
|
|
|
53
|
+
The same service account can arrive from `/credentials` instead, and there the
|
|
54
|
+
operator pastes the readable pair rather than an encoded blob:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
mixpanel("automation_analytics", {
|
|
58
|
+
purpose: "Headless release-health reporting",
|
|
59
|
+
auth: { type: "credential" },
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The slot renders as "Service account" and takes `username:secret`. Connecta
|
|
64
|
+
base64-encodes it and sends Mixpanel's documented `Bearer Basic` framing, so the
|
|
65
|
+
operator never has to encode anything by hand. **The two paths take different
|
|
66
|
+
strings:** the `headers` example above wants the already-encoded blob
|
|
67
|
+
(`echo -n "username:secret" | base64`), and this one wants the plaintext pair.
|
|
68
|
+
Migrating from one to the other means decoding, not copying. Until a value is
|
|
69
|
+
saved the connector is present and reports `auth_required`. See
|
|
70
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
|
71
|
+
|
|
53
72
|
## Conditional input contracts
|
|
54
73
|
|
|
55
74
|
Mixpanel's hosted descriptions enforce three cross-field conditions that its
|
|
@@ -247,8 +247,10 @@ in.
|
|
|
247
247
|
| `provider-conventions.test.ts` | the conventions a test can hold: hand-written providers refusing schemas they cannot enforce (H5), their compact discovery schemas staying complete (H7), Cloudflare stating its second pagination convention in the schema (H10), and Notion saying it has no escape hatch (H14) |
|
|
248
248
|
| `registry.test.ts` | construction and id validation, startup warnings, address resolution, catalog TTL/persistence/completeness, agent-only stale-while-revalidate with cross-request single-flight shared with blocking reads in both start orders, owned teardown, invalidation/fingerprint guards, blocking diagnostics, and broken-connector isolation |
|
|
249
249
|
| `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, and downstream session termination |
|
|
250
|
+
| `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from `/credentials`: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, `lastError`, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
|
|
250
251
|
| `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
|
|
251
252
|
| `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
|
|
253
|
+
| `revenuecat-provider.test.ts` / `revenuecat-registry.test.ts` | the RevenueCat proxy's per-project key scoping and account-wide OAuth guides, its purpose-bearing summary, the argued borderline verdicts in its digest-free manifest, and the deliberately unclassified `render-paywall-screenshot`; then two project-scoped keys as two connectors in a real deployment |
|
|
252
254
|
| `server.test.ts` | end-to-end `/mcp` (401 → compact initialize instructions → seven compact definitions with bounded connector inventory → complete usage skill → `call_tool`), conditional guide pointers, open routes, Clerk `.well-known` metadata without network, code mode, and deferred catalog reads through both discovery surfaces |
|
|
253
255
|
| `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: every built-in answered ahead of connector routes inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
|
|
254
256
|
| `startup-warnings.test.ts` | every construction-time `logger.warn` and, as importantly, the conditions that must *not* trigger one: open mode with a credential or OAuth connector, `publicUrl` unset beside OAuth, dropped branding and `uiAuth` URLs, a missing `verifyState`, a credential test-hook mismatch, and an unusable `calls.maxResultBytes` |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Provider audit
|
|
2
2
|
|
|
3
3
|
[`provider-conventions.md`](./provider-conventions.md) wrote the bar down. This
|
|
4
|
-
document runs it against the
|
|
4
|
+
document runs it against the six maintained prebuilt connections and returns a
|
|
5
5
|
verdict for every applicable convention: **meets**, **misses** (with the fix),
|
|
6
6
|
or **n/a** (with the reason). A convention is never quietly skipped, and an
|
|
7
7
|
accepted miss is recorded as a provider-specific exception with its argument
|
|
@@ -147,6 +147,28 @@ Linear's reasoning.
|
|
|
147
147
|
| P12 admission budget | **missed → fixed** | the connection hardcoded a 600-call hourly budget transcribed from a limit Mixpanel meters **per user**. P12 names this case exactly: a per-runtime counter cannot approximate a per-user quota in either direction — one runtime serving several users under-counts, several isolates sharing one credential each admit a full budget. The default is removed; `callAdmission` is now an operator option with a documented example, matching Linear |
|
|
148
148
|
| P13 drift visible | meets | both lists are module-level constants in one file, and are the manifest the refresh-time drift check compares against ([#343](https://github.com/zackbart/connecta/issues/343)) |
|
|
149
149
|
|
|
150
|
+
## RevenueCat — hosted-MCP proxy
|
|
151
|
+
|
|
152
|
+
Written after the conventions existed, so it has no misses to record — only two
|
|
153
|
+
places where the honest answer departs from the obvious one, both argued below.
|
|
154
|
+
Ninety-five documented tools, ninety-four classified, one deliberately not.
|
|
155
|
+
|
|
156
|
+
| Convention | Verdict | Notes |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| P1 add, never rewrite | meets | `listTools` maps annotations and returns every other field untouched |
|
|
159
|
+
| P2 identity | meets | required `purpose` (blank throws), `instructions` appended under `## Project instructions`, and appended text cannot reach the classification |
|
|
160
|
+
| P3 routing fact | meets, with the fact split in two | the routing fact is scope, and it has two halves. The *shape* — one project versus every project the account can reach — is knowable at construction and rides the default title (`RevenueCat (single project)` versus `RevenueCat`). *Which* project a key opens is not knowable without calling something, which P10 forbids, so it rides the guide's first line and the declared summary, built from the operator's `purpose`. That makes this the one maintained proxy with a purpose-bearing summary rather than a static one, and the reason is P3's own cost: two `sk_` connectors share a title, an endpoint, and a catalog, so a static summary would leave them indistinguishable in the only field search returns |
|
|
161
|
+
| P4 endpoint default | n/a — one endpoint, and the scope rides the credential | RevenueCat publishes a single MCP endpoint, so there is nothing to select between. The scope difference comes from the credential shape itself, which the constructor reads rather than asks for: `auth.type === "headers"` *is* the single-project declaration. There is no mode to default and no mode to contradict, so the P4 machinery Stripe needs has nothing to do here |
|
|
162
|
+
| P5 classification | meets | 50 reads, 15 additive writes, 29 destructive writes named; `render-paywall-screenshot` is on neither list because RevenueCat's reference gives it no access column, and it fails closed. Nine borderline verdicts are argued beside the rows they decide, and asserted in the suite so a silent flip fails |
|
|
163
|
+
| P6 catalog varies | meets | the guide names paywall AI editing, benchmarks, experiments, virtual currencies, and account billing as the plan-, platform-, and beta-gated areas where absence is expected, and separately names the unclassified tool so its approval prompt does not read as a bug |
|
|
164
|
+
| P7 reduction advice | meets | structured guide, declared summary, cursor-then-reduce advice aimed at the two objects that are actually large here (customers and their event history). `required` stays unset: the project-resolution sequence is worth reading before a run, not before every call |
|
|
165
|
+
| P8 identity resolution | meets | the guide names the whole chain — `list-projects` for the `project_id` every project-scoped call takes, then `list-apps`, `list-products`, `list-entitlements`, `list-offerings`, `list-paywalls`, `list-audiences`, and `list-customers` for the ids their `get-`, `update-`, `archive-`, and `delete-` counterparts expect — and says a plausible-looking id belongs to another project or to nobody. For OAuth it also says to stop and ask when more than one project fits |
|
|
166
|
+
| P9 authentication | meets | OAuth default, `requireHttps`, the API v2 secret key documented as a secret and paired with the narrowest scope RevenueCat offers (one project). The guide names the `auth_required` → `authorize_connector` route, and separately says that a read-only key's refusal is RevenueCat's own words rather than an authorization gap connecta can repair |
|
|
167
|
+
| P10 no credential test | meets | no `credential`, `testCredential`, or `testCredentials`. This is also where the constructor's most tempting option was refused: a `project?: string` checked against `list-projects` at construction is a credential test wearing a configuration hat, so the operator's stated purpose carries the claim and the agent confirms it on first use. There is no recognizable-credential contradiction to throw on either — an `sk_` key encodes no project — so the construction-time half of P10 has nothing to check here, exactly as it has nothing to check for Mixpanel's region |
|
|
168
|
+
| P11 transport vs tool error | meets | inherited whole from `remoteMcp()`; the wrapper adds no error handling and reads no downstream prose. The guide says a rejected argument, a permission gap, and a plan restriction all arrive in RevenueCat's own words |
|
|
169
|
+
| P12 admission budget | meets, by declining a number that exists | RevenueCat does publish limits, which is why this row needed an argument rather than a shrug. It meters per domain — 480/min for customer information, virtual currencies, and refunds; 60 for project configuration and audiences; 25 for charts and metrics — and a `ConnectorCallAdmissionPolicy` carries exactly one rule. Picking 25 throttles a customer read loop to a nineteenth of its allowance; picking 480 leaves a chart sweep unprotected; neither is the provider's limit. The metering scope repeats the point: developer-level keys are metered per developer, which a per-runtime counter cannot approximate. So the guide states RevenueCat's own numbers and the `429` / `Retry-After` / `backoff_ms` signals, and `callAdmission` stays an operator option with a documented example |
|
|
170
|
+
| P13 drift visible | meets | both lists are module-level constants in one file and *are* the manifest the wrapper classifies from, compared against the live catalog on every refresh ([#343](https://github.com/zackbart/connecta/issues/343)). The maintainer-run check accepts `revenuecat` with `CONNECTA_DRIFT_REVENUECAT_KEY`. No schema digests are recorded, and the manifest says so rather than shipping invented ones |
|
|
171
|
+
|
|
150
172
|
## Scoreboard
|
|
151
173
|
|
|
152
174
|
| Provider | Meets | Missed and fixed | Recorded exception | Open |
|
|
@@ -156,8 +178,9 @@ Linear's reasoning.
|
|
|
156
178
|
| Linear | 11 | 2 | P4 departs from the letter | — |
|
|
157
179
|
| Stripe | 10 | 3 | — | — |
|
|
158
180
|
| Mixpanel | 7 | 5 | P10 half n/a | — |
|
|
181
|
+
| RevenueCat | 12 | 0 | P4 n/a (one endpoint); P3 met with a purpose-bearing summary | — |
|
|
159
182
|
|
|
160
|
-
Nineteen misses, nineteen fixes,
|
|
183
|
+
Nineteen misses, nineteen fixes, six recorded exceptions, one judgment left to
|
|
161
184
|
the issue that owns it. The pattern in the misses is worth naming: sixteen of
|
|
162
185
|
the nineteen are a guide, a title, or a schema description failing to *say*
|
|
163
186
|
something the implementation already did correctly. Only three changed what a
|
|
@@ -166,3 +189,10 @@ access declaration, Mixpanel dropping a budget it could not honestly compute.
|
|
|
166
189
|
The conventions are mostly not asking for different behavior. They are asking
|
|
167
190
|
for the behavior to reach the agent, which is a different problem and, on this
|
|
168
191
|
evidence, the one the providers were losing.
|
|
192
|
+
|
|
193
|
+
RevenueCat is the first connection written *after* the conventions and adds no
|
|
194
|
+
misses to those nineteen, which is the least interesting thing about its row.
|
|
195
|
+
The interesting part is that two conventions came out somewhere other than
|
|
196
|
+
their obvious reading — P4 has no endpoint to select and P12 declines a number
|
|
197
|
+
the provider actually publishes — and both had to be argued rather than
|
|
198
|
+
skipped. A convention that only ever returns "meets" is not being applied.
|