@zackbart/connecta 0.18.0 → 0.18.1
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 +55 -0
- package/README.md +79 -118
- package/dist/providers/mixpanel.js +4 -1
- package/dist/providers/revenuecat.d.ts +77 -0
- package/dist/providers/revenuecat.js +314 -0
- package/dist/providers/stripe.js +5 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/connectors.md +1 -0
- package/documentation/operations.md +1 -0
- package/documentation/provider-audit.md +32 -2
- package/documentation/provider-conventions.md +24 -15
- package/documentation/revenuecat.md +279 -0
- package/documentation/stripe.md +10 -0
- package/documentation/upgrading.md +14 -4
- package/package.json +5 -1
- package/templates/node/package.json +1 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { remoteMcp, } 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 = options.auth ?? { type: "oauth" };
|
|
278
|
+
const scoped = auth.type === "headers";
|
|
279
|
+
const connector = remoteMcp(id, {
|
|
280
|
+
url: REVENUECAT_MCP_ENDPOINT,
|
|
281
|
+
// The scope shape rides the title because browse-time discovery renders
|
|
282
|
+
// the title and the guide summary and nothing else, and reaching one
|
|
283
|
+
// project versus every project the account has is the fact an agent must
|
|
284
|
+
// not get wrong between two RevenueCat connections.
|
|
285
|
+
title: options.title ?? (scoped ? "RevenueCat (single project)" : "RevenueCat"),
|
|
286
|
+
description: scoped
|
|
287
|
+
? `RevenueCat subscriptions and revenue (one project, static key) — ${purpose}`
|
|
288
|
+
: `RevenueCat subscriptions and revenue (every project the account can reach) — ${purpose}`,
|
|
289
|
+
auth,
|
|
290
|
+
requireHttps: true,
|
|
291
|
+
usageGuide: {
|
|
292
|
+
content: scoped
|
|
293
|
+
? keyUsageGuide(purpose, options.instructions)
|
|
294
|
+
: oauthUsageGuide(purpose, options.instructions),
|
|
295
|
+
// Explicit rather than derived, and purpose-bearing rather than static:
|
|
296
|
+
// the derived summary would cut the scoping sentence mid-clause at 120
|
|
297
|
+
// characters, and two static summaries would leave two `sk_` connectors
|
|
298
|
+
// indistinguishable in the one field search returns (P3).
|
|
299
|
+
summary: scoped
|
|
300
|
+
? boundedSummary("One project only: ", purpose)
|
|
301
|
+
: boundedSummary("All account projects; list-projects first: ", purpose),
|
|
302
|
+
// Not `required`. RevenueCat's own schemas describe each call; the guide
|
|
303
|
+
// carries the project-resolution sequence, which is worth reading before
|
|
304
|
+
// a run rather than before every call.
|
|
305
|
+
},
|
|
306
|
+
...(options.callAdmission !== undefined
|
|
307
|
+
? { callAdmission: options.callAdmission }
|
|
308
|
+
: {}),
|
|
309
|
+
...(options.maxResultBytes !== undefined
|
|
310
|
+
? { maxResultBytes: options.maxResultBytes }
|
|
311
|
+
: {}),
|
|
312
|
+
});
|
|
313
|
+
return withVettedCatalog(connector, REVENUECAT_VETTED_CATALOG);
|
|
314
|
+
}
|
package/dist/providers/stripe.js
CHANGED
|
@@ -173,7 +173,11 @@ function sharedUsageGuide(rate) {
|
|
|
173
173
|
- 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
174
|
- 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
175
|
- \`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
|
|
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\`.
|
|
177
|
+
- 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.
|
|
178
|
+
- 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.
|
|
179
|
+
- 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.
|
|
180
|
+
- 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
181
|
- 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
182
|
- 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
183
|
- 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.
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -249,6 +249,7 @@ in.
|
|
|
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
250
|
| `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
251
|
| `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 |
|
|
252
|
+
| `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
253
|
| `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
254
|
| `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
255
|
| `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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Provider conventions
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The six maintained prebuilt connections grew one at a time, and until now
|
|
4
4
|
"excellent provider" meant whatever the last author thought. This document
|
|
5
5
|
writes the judgment down so it can be argued with, audited, and reused.
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ cannot honestly cover both:
|
|
|
11
11
|
tool name, schema, projection, and error. Today: Cloudflare, Notion.
|
|
12
12
|
- **Hosted-MCP proxies** — `remoteMcp()` wrappers around a server somebody else
|
|
13
13
|
operates, where the names, schemas, results, and error prose arrive as they
|
|
14
|
-
are. Today: Linear, Stripe, Mixpanel.
|
|
14
|
+
are. Today: Linear, Stripe, Mixpanel, RevenueCat.
|
|
15
15
|
|
|
16
16
|
The governing principle for every convention below is the same: **keep the
|
|
17
17
|
model that interacts with connecta as efficient as possible.** A convention
|
|
@@ -386,11 +386,18 @@ complete spends calls proving it is not. *Cost:* wrong-tool selection.
|
|
|
386
386
|
|
|
387
387
|
A proxy cannot project a downstream result, so the guide tells the agent to
|
|
388
388
|
page with the cursor rather than raising the page size, and to reduce inside
|
|
389
|
-
`execute_code` before returning anything
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
`execute_code` before returning anything — and, where a value's rendering is
|
|
390
|
+
the provider's rule rather than the schema's, what that value means: Mixpanel
|
|
391
|
+
renders an absent boolean property as `false` in a breakdown, so the guide
|
|
392
|
+
says to confirm presence before reading `false` as a signal
|
|
393
|
+
([#430](https://github.com/zackbart/connecta/issues/430)). Structured form,
|
|
394
|
+
explicit `summary`, `required: true` only for a genuine cross-tool sequence or
|
|
395
|
+
a generic wrapper.
|
|
396
|
+
|
|
397
|
+
*Why:* the only projection available is the one the program writes, and a
|
|
398
|
+
value the schema types correctly can still mislead without the provider's
|
|
399
|
+
rendering rule beside it — the agent then re-queries to explain a signal that
|
|
400
|
+
was never there. *Cost:* result size.
|
|
394
401
|
|
|
395
402
|
### P8 — Identity resolution comes before action
|
|
396
403
|
|
|
@@ -494,8 +501,8 @@ is the shape that does not become it.
|
|
|
494
501
|
**What a manifest holds.** Every tool name a release reviewed, the verdict it
|
|
495
502
|
reviewed it as (`read-only`, `additive`, `destructive`), and — where a release
|
|
496
503
|
actually read them — a digest of that tool's input and output schemas. Today
|
|
497
|
-
the
|
|
498
|
-
has read a live schema and written it down, and an invented digest reports a
|
|
504
|
+
three of the four proxies ship names and verdicts and no digests, because no
|
|
505
|
+
release has read a live schema and written it down, and an invented digest reports a
|
|
499
506
|
change that never happened. `npm run drift:check -- --record` reads them from a
|
|
500
507
|
live catalog and prints the block a release pastes in; until a release does,
|
|
501
508
|
a manifest without digests counts no schema changes, which is the honest answer
|
|
@@ -567,9 +574,10 @@ compares its own totals against `detectCatalogDrift()`: two readings of one
|
|
|
567
574
|
manifest that disagree mean one of them is lying, which is worth failing over.
|
|
568
575
|
One credential per provider comes from the environment —
|
|
569
576
|
`CONNECTA_DRIFT_LINEAR_KEY`, `CONNECTA_DRIFT_STRIPE_KEY`,
|
|
570
|
-
`CONNECTA_DRIFT_MIXPANEL_KEY` — and a missing
|
|
571
|
-
message naming it rather than reporting an
|
|
572
|
-
|
|
577
|
+
`CONNECTA_DRIFT_MIXPANEL_KEY`, `CONNECTA_DRIFT_REVENUECAT_KEY` — and a missing
|
|
578
|
+
or dead one stops the run with a message naming it rather than reporting an
|
|
579
|
+
empty catalog as mass removal. Linear, bare Stripe, and RevenueCat `sk_` values
|
|
580
|
+
use their documented bearer or Basic framing.
|
|
573
581
|
Mixpanel's beta service-account form is provider-specific:
|
|
574
582
|
`user:secret` becomes `Bearer Basic <base64(user:secret)>`, exactly as its MCP
|
|
575
583
|
documentation requires. A value that already includes whitespace is treated
|
|
@@ -623,11 +631,11 @@ evidence and nothing else: no tool is generated from one, which is the
|
|
|
623
631
|
## What the audit checks
|
|
624
632
|
|
|
625
633
|
The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
|
|
626
|
-
runs this document against each of the
|
|
634
|
+
runs this document against each of the six providers and returns a verdict per
|
|
627
635
|
convention: **meets**, **misses** (with the fix), or **not applicable** (with
|
|
628
636
|
the reason). A convention is never quietly skipped, and an accepted miss is
|
|
629
637
|
recorded as a provider-specific exception with its argument, not left blank.
|
|
630
|
-
Its
|
|
638
|
+
Its six reports live in [provider-audit.md](./provider-audit.md), and the
|
|
631
639
|
mechanically checkable half of the hand-written bar runs on every test run in
|
|
632
640
|
[`test/provider-conventions.test.ts`](https://github.com/zackbart/connecta/blob/main/test/provider-conventions.test.ts) —
|
|
633
641
|
so a convention that was met once stays met, or fails loudly.
|
|
@@ -675,6 +683,7 @@ their place, so they are this audit's work, not a second removal argument.
|
|
|
675
683
|
|
|
676
684
|
Each provider's own guide ([Cloudflare](./cloudflare.md),
|
|
677
685
|
[Linear](./linear.md), [Mixpanel](./mixpanel.md), [Notion](./notion.md),
|
|
678
|
-
[Stripe](./stripe.md)) is part of the audited
|
|
686
|
+
[RevenueCat](./revenuecat.md), [Stripe](./stripe.md)) is part of the audited
|
|
687
|
+
surface: documentation moves with
|
|
679
688
|
the work, and a guide describing a surface that shipped differently is itself a
|
|
680
689
|
miss.
|