@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.
@@ -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
+ }
@@ -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\` and reduce before returning.
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
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.18.0";
7
+ export declare const CONNECTA_VERSION = "0.18.1";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.18.0";
7
+ export const CONNECTA_VERSION = "0.18.1";
@@ -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
@@ -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 five maintained prebuilt connections and returns a
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, four recorded exceptions, one judgment left to
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 five maintained prebuilt connections grew one at a time, and until now
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. Structured form, explicit `summary`,
390
- `required: true` only for a genuine cross-tool sequence or a generic wrapper.
391
-
392
- *Why:* the only projection available is the one the program writes. *Cost:*
393
- result size.
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 three proxies ship names and verdicts and no digests, because no release
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 or dead one stops the run with a
571
- message naming it rather than reporting an empty catalog as mass removal.
572
- Linear and bare Stripe values use their documented bearer or Basic framing.
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 five providers and returns a verdict per
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 five reports live in [provider-audit.md](./provider-audit.md), and the
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 surface: documentation moves with
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.