@zackbart/connecta 0.24.3 → 0.24.4
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/AGENTS.md +18 -20
- package/CHANGELOG.md +64 -1
- package/README.md +5 -6
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +10 -2
- package/dist/connectors/guarded-fetch.d.ts +5 -1
- package/dist/connectors/guarded-fetch.js +34 -4
- package/dist/connectors/remote-mcp.js +8 -4
- package/dist/errors.d.ts +11 -3
- package/dist/errors.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -1
- package/dist/meta-tools.js +105 -29
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +12 -4
- package/dist/registry.js +22 -8
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -181
- package/documentation/auth.md +197 -176
- package/documentation/code-mode.md +426 -321
- package/documentation/meta-tools.md +356 -416
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/documentation/call-admission.md +0 -158
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -431
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -729
- package/documentation/request-admission.md +0 -204
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -254
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -768
- package/documentation/vercel.md +0 -241
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
# Request admission
|
|
2
|
-
|
|
3
|
-
Connecta bounds work at the Web-standard request boundary — before inbound
|
|
4
|
-
auth, before the MCP server exists, before any catalog is touched. A burst
|
|
5
|
-
therefore meets an explicit active count and an explicit queue instead of
|
|
6
|
-
asking traffic shape and the runtime allocator to pick the process high-water
|
|
7
|
-
mark.
|
|
8
|
-
|
|
9
|
-
There are two pools here and a third elsewhere. This guide covers the first
|
|
10
|
-
two: the deployment-wide `/mcp` pool and the fallback code pool. Per-connector
|
|
11
|
-
downstream bounds are [call admission](./call-admission.md), which is a
|
|
12
|
-
different question — request admission bounds the whole MCP envelope, call
|
|
13
|
-
admission bounds the individual `Connector.callTool` attempts fanned out inside
|
|
14
|
-
it.
|
|
15
|
-
|
|
16
|
-
## The pools
|
|
17
|
-
|
|
18
|
-
Every non-preflight `/mcp` or `/mcp/<pool>` request with an admitted Origin takes one permit from a deployment-wide FIFO
|
|
19
|
-
pool. Initialization, discovery, ordinary calls, and `execute_code` all pay it.
|
|
20
|
-
A program then takes a *second* permit from the deliberately smaller code pool,
|
|
21
|
-
so one request cannot trade ordinary capacity for an unbounded number of
|
|
22
|
-
sandboxes.
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
const connecta = createConnecta({
|
|
26
|
-
admission: {
|
|
27
|
-
requests: { concurrency: 16, maxQueueSize: 32, queueTimeoutMs: 5_000, retryAfterMs: 1_000 },
|
|
28
|
-
code: { concurrency: 2, maxQueueSize: 8, queueTimeoutMs: 5_000, retryAfterMs: 1_000 },
|
|
29
|
-
},
|
|
30
|
-
// …
|
|
31
|
-
});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Those values are the defaults, and both pools are the same
|
|
35
|
-
`AdmissionController` (`src/executor-admission.ts`) with different numbers.
|
|
36
|
-
`maxQueueSize: 0` is the fail-fast shape. Every value is a finite whole number;
|
|
37
|
-
concurrency and the queue timeout must be positive, while queue size and the
|
|
38
|
-
retry hint may be zero. Invalid bounds throw at construction rather than
|
|
39
|
-
silently removing the deployment's memory boundary — a pool that quietly became
|
|
40
|
-
unbounded is worse than a deployment that refuses to boot.
|
|
41
|
-
|
|
42
|
-
`admission.code` is a *fallback*. An executor that implements `acquire()` is an
|
|
43
|
-
`AdmittingExecutor` and already owns a bounded pool, so its own settings win and
|
|
44
|
-
connecta warns that `admission.code` was ignored. `quickJsExecutor()` is one of
|
|
45
|
-
those: it defaults to one active execution and 32 queued callers, configured on
|
|
46
|
-
the executor rather than here. Cloudflare's `DynamicWorkerExecutor` is not, so
|
|
47
|
-
a Worker deployment gets the fallback pool wrapped around it at construction —
|
|
48
|
-
which is also why `/health` always has a code-admission shape to report.
|
|
49
|
-
|
|
50
|
-
The request pool is global FIFO across identities. It is a capacity boundary,
|
|
51
|
-
not tenant fairness: one busy caller can occupy it. Per-tenant fairness needs a
|
|
52
|
-
policy above connecta, and one deployment still serves one tenant even when
|
|
53
|
-
identity rules give its principals different connector views
|
|
54
|
-
([`ethos.md`](../ethos.md)), so a global queue is not pretending to supply
|
|
55
|
-
something it does not.
|
|
56
|
-
|
|
57
|
-
## Origin before admission
|
|
58
|
-
|
|
59
|
-
MCP checks `Origin` before redirects, request admission, auth, and preflight.
|
|
60
|
-
A present, disallowed origin gets HTTP 403 with the exact body
|
|
61
|
-
`{"error":"origin not allowed"}` and `Cache-Control: no-store`. This local
|
|
62
|
-
check consumes no permit or auth lookup, even when the queue is full or closed.
|
|
63
|
-
Requests without Origin, including ordinary non-browser MCP clients, pass.
|
|
64
|
-
`/health`, OAuth callbacks, and auth metadata retain their existing behavior.
|
|
65
|
-
|
|
66
|
-
`allowedOrigins` accepts a list of exact HTTP(S) origins or `"*"`. An explicit
|
|
67
|
-
list replaces the defaults, including loopback; an empty list admits only
|
|
68
|
-
originless clients. By default the configured `publicUrl` origin and HTTP(S)
|
|
69
|
-
loopback origins at any port are admitted. Loopback means `localhost`,
|
|
70
|
-
`127.0.0.0/8`, or `[::1]`. Without `publicUrl`, only loopback is admitted.
|
|
71
|
-
The inbound Host header never chooses a trusted browser origin. Invalid list
|
|
72
|
-
entries, including paths, credentials, and opaque origins, refuse construction.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
createConnecta({
|
|
76
|
-
publicUrl: "https://connecta.example",
|
|
77
|
-
allowedOrigins: ["https://connecta.example", "https://client.example"],
|
|
78
|
-
// …
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
For unrestricted browser access, set `allowedOrigins: "*"` explicitly. Otherwise
|
|
83
|
-
MCP responses reflect an admitted Origin and carry `Vary: Origin`; originless
|
|
84
|
-
and refused requests have no `Access-Control-Allow-Origin`. Allowed preflight
|
|
85
|
-
returns 204 without auth or admission. The SDK validates SEP-2243 parameter
|
|
86
|
-
headers, so preflight echoes valid requested `mcp-param-*` names alongside the
|
|
87
|
-
fixed MCP header list and varies by `Access-Control-Request-Headers` too.
|
|
88
|
-
|
|
89
|
-
## Admission before auth
|
|
90
|
-
|
|
91
|
-
`/mcp` acquires its permit *before* running the auth gate. This looks backwards
|
|
92
|
-
until you price it: authenticating first means an unauthenticated flood buys a
|
|
93
|
-
Clerk network lookup per request, so the cheapest possible attack becomes the
|
|
94
|
-
most expensive request the server can serve. Admitting first means it buys a
|
|
95
|
-
queue slot and a 503.
|
|
96
|
-
|
|
97
|
-
The permit is released with the response *body*, not when the handler returns.
|
|
98
|
-
A slow client draining a large result still counts as active work, because its
|
|
99
|
-
bytes and its socket still exist. `releaseAdmissionWithResponse` re-wraps the
|
|
100
|
-
response stream to do this, absorbs a rejecting `cancel()` rather than leaking
|
|
101
|
-
an unhandled rejection, and releases exactly once — release is idempotent, and
|
|
102
|
-
`test/request-admission.test.ts` pins both the stream-cancel path and the
|
|
103
|
-
double-release case.
|
|
104
|
-
|
|
105
|
-
Every other route bypasses the pool entirely. `/health` and the operator
|
|
106
|
-
surface stay responsive while MCP is saturated, which is the whole point: an
|
|
107
|
-
operator diagnosing an overload must not have to queue behind it. `/health`
|
|
108
|
-
names the exempt routes in `admission.reservedRoutes` so the claim is checkable
|
|
109
|
-
from outside.
|
|
110
|
-
|
|
111
|
-
## Overload, cancellation, shutdown
|
|
112
|
-
|
|
113
|
-
A full queue or an expired queue deadline answers HTTP 503 with `Retry-After`,
|
|
114
|
-
CORS headers, and a stable JSON-RPC error:
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
{
|
|
118
|
-
"jsonrpc": "2.0",
|
|
119
|
-
"id": null,
|
|
120
|
-
"error": {
|
|
121
|
-
"code": -31001,
|
|
122
|
-
"message": "Server capacity is exhausted. Retry later.",
|
|
123
|
-
"data": { "code": "server_overloaded", "retryable": true, "retryAfterMs": 1000 }
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
`Retry-After` is that hint rounded up to at least one whole second. It is
|
|
129
|
-
advice, not a reservation. Shutdown uses `-31002` / `server_shutting_down` and
|
|
130
|
-
is not retryable. These application codes sit outside JSON-RPC's reserved
|
|
131
|
-
range: MCP forbids new allocations in the legacy `-32000..-32019` range.
|
|
132
|
-
Code-pool overload never reaches this layer: it surfaces as
|
|
133
|
-
an ordinary MCP tool error with `executor_overloaded`, `retryable: true`, and
|
|
134
|
-
the executor's own `retryAfterMs`.
|
|
135
|
-
|
|
136
|
-
A cancelled queued request is removed immediately and never receives a later
|
|
137
|
-
permit — cancelling and then being admitted would hold capacity for a caller
|
|
138
|
-
that is gone. An admitted request keeps its permit until its body completes,
|
|
139
|
-
errors, or is cancelled.
|
|
140
|
-
|
|
141
|
-
`connecta.close()` closes both queues before releasing executor resources:
|
|
142
|
-
queued and future MCP work is rejected with `server_shutting_down` while
|
|
143
|
-
admitted work drains. Node's `listen()` calls it on SIGTERM or SIGINT, stops
|
|
144
|
-
accepting connections, drains, and enforces `shutdownTimeoutMs` (10 s default)
|
|
145
|
-
— SIGTERM arrives on every `docker compose up` recreate, and Node's default
|
|
146
|
-
response to it is to die mid-request.
|
|
147
|
-
|
|
148
|
-
## What admission is not
|
|
149
|
-
|
|
150
|
-
The Node adapter's `maxBodyBytes` (10 MiB default) is a separate ingress guard.
|
|
151
|
-
It caps the HTTP body while constructing the Web `Request`, which happens
|
|
152
|
-
*before* the portable `/mcp` boundary can run. Admission bounds MCP, auth,
|
|
153
|
-
catalog, and response work; it is not a byte budget for many simultaneous slow
|
|
154
|
-
or near-limit uploads. Hostile public traffic wants an ingress proxy with a
|
|
155
|
-
body-rate limit in front, and `maxBodyBytes` set to the smallest value the
|
|
156
|
-
deployment actually needs.
|
|
157
|
-
|
|
158
|
-
The rejection warning is rate-limited to one per second, and each line reports
|
|
159
|
-
how many were suppressed since the last one. The `/health` totals count every
|
|
160
|
-
rejection, so the log is a sample and the counters are the record. Queue waits
|
|
161
|
-
log at debug level. Nothing on this path records or exposes request bodies,
|
|
162
|
-
tool arguments, identities, or results.
|
|
163
|
-
|
|
164
|
-
## Observations
|
|
165
|
-
|
|
166
|
-
`/health` exposes payload-free snapshots under `admission.requests`,
|
|
167
|
-
`admission.code`, and `admission.downstreamCalls`. The first two carry
|
|
168
|
-
configured bounds, current active and queued counts, cumulative
|
|
169
|
-
admitted/queued/rejected/cancelled/closed totals, and queue-wait count, total,
|
|
170
|
-
and maximum. The request policy is labelled `global-fifo`; downstream policy is
|
|
171
|
-
labelled `connector-partitioned-per-runtime`. An executor that owns its own
|
|
172
|
-
pool and exposes no snapshot reports `{ managedByExecutor: true }`.
|
|
173
|
-
|
|
174
|
-
With `execute_code({ diagnostics: true })`, a caller sees the same split from
|
|
175
|
-
the inside: `admissionMs` is time spent waiting for a permit and `connectorMs`
|
|
176
|
-
is the admitted attempt.
|
|
177
|
-
|
|
178
|
-
## Measuring capacity
|
|
179
|
-
|
|
180
|
-
`npm run load:admission` builds the package, then starts server and generator
|
|
181
|
-
in separate processes over real loopback TCP, warms a 10,000-tool catalog,
|
|
182
|
-
verifies every returned value, and records throughput, p50/p95/p99, server-only
|
|
183
|
-
peak RSS, RSS after forced GC, and live heap after GC. Each matrix cell gets a
|
|
184
|
-
fresh server so an earlier allocator high-water mark cannot contaminate the
|
|
185
|
-
next baseline; the three-round soak deliberately reuses one, because allocator
|
|
186
|
-
high-water retention and a live-object climb look identical in a single run and
|
|
187
|
-
different across three.
|
|
188
|
-
|
|
189
|
-
`CONNECTA_LOAD_CATALOG_SIZE`, `CONNECTA_LOAD_CONCURRENCY`, and
|
|
190
|
-
`CONNECTA_LOAD_MAX_QUEUE_SIZE` change catalog size, server concurrency, and
|
|
191
|
-
queue depth. The script prints its own numbers; no baseline is checked in,
|
|
192
|
-
deliberately. A laptop matrix is an example capacity profile, not a portable
|
|
193
|
-
SLO, and downstream payload size moves it more than any setting here does — pin
|
|
194
|
-
a runner before enforcing a regression ratio, and measure the connector mix the
|
|
195
|
-
deployment actually runs.
|
|
196
|
-
|
|
197
|
-
## Tests that enforce this
|
|
198
|
-
|
|
199
|
-
| Invariant | Suite |
|
|
200
|
-
| --- | --- |
|
|
201
|
-
| FIFO bounds, active and queue ceilings, stable retryable overload, queue timeout, cancellation removal, idempotent release, shutdown | `test/executor-admission.test.ts` (Node + Workers) |
|
|
202
|
-
| `/mcp` bounded before auth, stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, the separate fallback code pool | `test/request-admission.test.ts` |
|
|
203
|
-
| A client disconnect propagating through the Web `Request` into a program's connector call, releasing both permits | `test/node.test.ts` |
|
|
204
|
-
| The `/health` admission payload alongside the drift counts | `test/catalog-drift.test.ts`, `test/server.test.ts` |
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
# RevenueCat prebuilt connection
|
|
2
|
-
|
|
3
|
-
Import `revenuecat()` independently from
|
|
4
|
-
`@zackbart/connecta/providers/revenuecat`. It wraps
|
|
5
|
-
[RevenueCat's hosted MCP server](https://www.revenuecat.com/docs/tools/mcp/setup)
|
|
6
|
-
with OAuth by default, project-scoping guidance that differs by credential
|
|
7
|
-
shape, a task-oriented usage guide, and a vetted safety classification. It adds
|
|
8
|
-
no provider dependency and is not reachable from Connecta's root entry.
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import { revenuecat } from "@zackbart/connecta/providers/revenuecat";
|
|
12
|
-
|
|
13
|
-
const subscriptions = revenuecat("revenuecat", {
|
|
14
|
-
purpose: "Subscription state, entitlements, and revenue across our projects",
|
|
15
|
-
instructions: "Never grant a promotional entitlement without a support ticket.",
|
|
16
|
-
});
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
The endpoint is `https://mcp.revenuecat.ai/mcp` over streamable HTTP.
|
|
20
|
-
|
|
21
|
-
`purpose` is required, and it does more work here than in any other maintained
|
|
22
|
-
connection. RevenueCat's own tools do not report which project a static key
|
|
23
|
-
reaches until you call one, and Connecta runs no credential test at construction
|
|
24
|
-
(P10), so `purpose` is the only place the deployment's intent is written down.
|
|
25
|
-
It opens the guide and it *is* the guide summary, which is the field search
|
|
26
|
-
returns. Project `instructions` are appended to the maintained guide and cannot
|
|
27
|
-
change the connector's safety classification.
|
|
28
|
-
|
|
29
|
-
## The scoping fact this connection exists to get right
|
|
30
|
-
|
|
31
|
-
RevenueCat has two credential shapes with two different scopes, and the guide
|
|
32
|
-
you get depends on which one you configured.
|
|
33
|
-
|
|
34
|
-
**A secret API key is project-wide.** RevenueCat's own words:
|
|
35
|
-
"Secret API keys are project-wide and can be created and revoked by project
|
|
36
|
-
Admins" ([authentication](https://www.revenuecat.com/docs/projects/authentication)).
|
|
37
|
-
`list-projects` "lists all RevenueCat projects accessible with the provided API
|
|
38
|
-
key" — with an `sk_` key that is exactly one project. So a `headers`-auth
|
|
39
|
-
connector reaches one project and nothing outside it. Its title is
|
|
40
|
-
`RevenueCat (single project)` and its guide opens by naming the project the
|
|
41
|
-
operator said the key is for.
|
|
42
|
-
|
|
43
|
-
**OAuth is account-scoped.** One session reaches every project the account can
|
|
44
|
-
see, and each project-scoped tool takes a `project_id`. Its title is
|
|
45
|
-
`RevenueCat` and its guide opens with the resolution discipline: call
|
|
46
|
-
`list-projects` first, carry the exact `project_id` it returned into every
|
|
47
|
-
project-scoped call, and stop and ask when more than one project fits.
|
|
48
|
-
Connecta does not pick a project, and the connector id, title, and purpose are
|
|
49
|
-
routing hints rather than proof of where a call will land.
|
|
50
|
-
|
|
51
|
-
The constructor deliberately has no `project` option. Declaring a project that
|
|
52
|
-
Connecta then checked against `list-projects` at construction would be a
|
|
53
|
-
credential test, which P10 forbids — a proxy makes no unasked-for downstream
|
|
54
|
-
call. The operator's stated purpose carries the claim; the agent confirms it
|
|
55
|
-
with `list-projects` on first use.
|
|
56
|
-
|
|
57
|
-
## Several projects
|
|
58
|
-
|
|
59
|
-
One key, one project, one connector. A deployment that needs two projects
|
|
60
|
-
declares two connectors, each with its own key and its own id:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
import { revenuecat } from "@zackbart/connecta/providers/revenuecat";
|
|
64
|
-
|
|
65
|
-
connectors: [
|
|
66
|
-
revenuecat("bepresent_ios", {
|
|
67
|
-
purpose: "Subscription state for the BePresent iOS project",
|
|
68
|
-
auth: {
|
|
69
|
-
type: "headers",
|
|
70
|
-
headers: { Authorization: `Bearer ${env.REVENUECAT_BEPRESENT_KEY}` },
|
|
71
|
-
},
|
|
72
|
-
}),
|
|
73
|
-
revenuecat("biblescroll", {
|
|
74
|
-
purpose: "Subscription state for the BibleScroll project",
|
|
75
|
-
auth: {
|
|
76
|
-
type: "headers",
|
|
77
|
-
headers: { Authorization: `Bearer ${env.REVENUECAT_BIBLESCROLL_KEY}` },
|
|
78
|
-
},
|
|
79
|
-
}),
|
|
80
|
-
]
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Neither key has to be a runtime secret. Declare the slot instead and each
|
|
84
|
-
connector's key is pasted, tested, and rotated on the connection UI at `/`:
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
connectors: [
|
|
88
|
-
revenuecat("bepresent_ios", {
|
|
89
|
-
purpose: "Subscription state for the BePresent iOS project",
|
|
90
|
-
auth: { type: "credential", credential: { label: "API v2 secret key" } },
|
|
91
|
-
}),
|
|
92
|
-
revenuecat("biblescroll", {
|
|
93
|
-
purpose: "Subscription state for the BibleScroll project",
|
|
94
|
-
auth: { type: "credential", credential: { label: "API v2 secret key" } },
|
|
95
|
-
}),
|
|
96
|
-
]
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Two ids, two slots, two single-project catalogs — the `credential` option is
|
|
100
|
-
optional, and omitting it gives the same "API v2 secret key" label. See
|
|
101
|
-
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
|
102
|
-
|
|
103
|
-
That is config-as-code doing what an account model would otherwise do: one
|
|
104
|
-
credential per connector, each with its own catalog, storage namespace, health,
|
|
105
|
-
and admission counters. The two share a title, because Connecta cannot know
|
|
106
|
-
which project a key opens — so the guide summary is what tells them apart, and
|
|
107
|
-
it is built from `purpose`. Write a purpose that names the project, not one
|
|
108
|
-
that names RevenueCat.
|
|
109
|
-
|
|
110
|
-
If the deployment genuinely needs to move between projects in one session, use
|
|
111
|
-
OAuth instead and let the agent resolve `project_id`. Do not point a
|
|
112
|
-
project-scoped key's `project_id` argument at a project it cannot reach; the
|
|
113
|
-
call fails at RevenueCat, which is the correct outcome but a wasted round trip.
|
|
114
|
-
|
|
115
|
-
## Authentication
|
|
116
|
-
|
|
117
|
-
OAuth is the default and the option RevenueCat recommends: "OAuth provides a
|
|
118
|
-
seamless authentication experience: log in to your RevenueCat account and grant
|
|
119
|
-
access to the MCP server, with no API keys to manage." Each connector instance
|
|
120
|
-
keeps its own flow and tokens in connector-scoped storage.
|
|
121
|
-
|
|
122
|
-
RevenueCat also accepts an API v2 secret key as a bearer token for headless
|
|
123
|
-
agents:
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
revenuecat("bepresent_ios", {
|
|
127
|
-
purpose: "Subscription state for the BePresent iOS project",
|
|
128
|
-
auth: {
|
|
129
|
-
type: "headers",
|
|
130
|
-
headers: { Authorization: `Bearer ${env.REVENUECAT_KEY}` },
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Keys are prefixed `sk_`, are issued read-only or write-enabled, and can be
|
|
136
|
-
revoked at any time by a project Admin. RevenueCat's setup guidance is to "use
|
|
137
|
-
a write-enabled key if you plan to create/modify resources"; "a read-only key
|
|
138
|
-
works if you only need to view data". Keep the key in the runtime's secret
|
|
139
|
-
store, never in the deployment file — or declare
|
|
140
|
-
`auth: { type: "credential" }` and let the operator hold it in the vault
|
|
141
|
-
instead, which is the shape the two-project example above uses.
|
|
142
|
-
|
|
143
|
-
**Connecta does not filter writes for a read-only key.** It has no way to tell
|
|
144
|
-
which kind a key is without spending a call, so every write in the catalog is
|
|
145
|
-
offered, reaches RevenueCat, and fails there in RevenueCat's own words. The
|
|
146
|
-
guide says so, so an agent reads that refusal as "this key cannot write" rather
|
|
147
|
-
than as a bad argument and repairs it by routing to a write-enabled connector
|
|
148
|
-
instead of retrying.
|
|
149
|
-
|
|
150
|
-
An expired or revoked credential surfaces as `auth_required`, and the guide
|
|
151
|
-
names the `authorize_connector` recovery. A permission gap, a plan restriction,
|
|
152
|
-
or a rejected argument arrives as RevenueCat wrote it and is not an
|
|
153
|
-
authorization problem.
|
|
154
|
-
|
|
155
|
-
## The ninety-six tools, and what they are classified as
|
|
156
|
-
|
|
157
|
-
RevenueCat's
|
|
158
|
-
[tool reference](https://www.revenuecat.com/docs/tools/mcp/tools-reference),
|
|
159
|
-
read on **2026-08-30**, documents ninety-six tools. Ninety-five carry an access
|
|
160
|
-
column and are classified here: **51 read-only, 15 additive writes, 29
|
|
161
|
-
destructive writes.**
|
|
162
|
-
|
|
163
|
-
Reads are every `Read` row, verbatim — the nine project and app reads, the four
|
|
164
|
-
product reads, the entitlement, offering, targeting, paywall, customer, virtual
|
|
165
|
-
currency, chart, webhook, and SDK reads, `get-paywall-ai-task`, and
|
|
166
|
-
`get-refund-request-preferences`.
|
|
167
|
-
|
|
168
|
-
Writes follow the verb where the verb is honest: `archive-*` and `unarchive-*`
|
|
169
|
-
flip an existing object's active state, `update-*`, `delete-*`, `publish-*`,
|
|
170
|
-
`unpublish-*`, and `detach-*` change or remove something that already exists,
|
|
171
|
-
and a plain `create-*` brings a new object into being beside the old ones.
|
|
172
|
-
`set-product-store-state` is an upsert and `submit-products-to-store` sends
|
|
173
|
-
products to Apple for review, so both are destructive.
|
|
174
|
-
`assign-customer-offering` and `grant-customer-entitlement` change a real
|
|
175
|
-
customer's access, so both are destructive too.
|
|
176
|
-
|
|
177
|
-
Nine verdicts are not decided by the verb, and each is argued in the source
|
|
178
|
-
beside the row:
|
|
179
|
-
|
|
180
|
-
| Tool | Verdict | Why |
|
|
181
|
-
| --- | --- | --- |
|
|
182
|
-
| `create-product-prices` | destructive | named `create-`, described "Configure prices for a product". The price set already exists and configuring it replaces what is there. Money-facing and overwriting |
|
|
183
|
-
| `equalize-subscription-prices` | additive | "Fills **missing** App Store subscription territory prices" — by RevenueCat's own word it writes only where nothing is set |
|
|
184
|
-
| `validate-app-credentials` | additive | RevenueCat files it `Write`, so it does not reach the read path, but it leaves the saved credentials alone and only records the outcome of a check |
|
|
185
|
-
| `upload-product-store-state-screenshot` | additive | "Reserves an App Store Connect review screenshot slot" — a new slot appears; nothing existing is replaced |
|
|
186
|
-
| `attach-products-to-entitlement` | additive | attach adds membership and removes nothing; `detach-products-from-entitlement` is the destructive half. Filing both destructive would make the pair read identically in the approval copy a human sees |
|
|
187
|
-
| `attach-products-to-package` | additive | the same argument one level down |
|
|
188
|
-
| `duplicate-paywall` | additive | "Duplicates an existing paywall's current draft" — the original is untouched |
|
|
189
|
-
| `create-paywall-ai` | additive | starts an async task that creates a paywall; every existing one is left alone |
|
|
190
|
-
| `edit-paywall-ai` | destructive | starts an async task that rewrites a draft that already exists |
|
|
191
|
-
|
|
192
|
-
`create-webhook-integration` deserves a sentence too. No existing integration
|
|
193
|
-
changes, so the verb reads additive — but with filters omitted the new one
|
|
194
|
-
"starts delivering" every customer event in the project to a URL the caller
|
|
195
|
-
typed. Customer data leaving the account makes it destructive on consequence,
|
|
196
|
-
so the approval copy says what is at stake.
|
|
197
|
-
|
|
198
|
-
**`render-paywall-screenshot` is deliberately unclassified.** RevenueCat's
|
|
199
|
-
reference gives it no access column. The current live server explicitly marks
|
|
200
|
-
it read-only, so that catalog keeps it callable from `execute_code`; if a later
|
|
201
|
-
catalog omits the annotation, it fails closed onto `call_destructive_tool`.
|
|
202
|
-
Connecta preserves the provider's current annotation without inventing a
|
|
203
|
-
release classification from the tool's harmless-sounding name (P5).
|
|
204
|
-
|
|
205
|
-
That classification fills in downstream silence and otherwise preserves explicit
|
|
206
|
-
annotations. A tool on the read allowlist arriving with `destructiveHint: true`
|
|
207
|
-
or `readOnlyHint: false` keeps exactly what the downstream said and stays behind
|
|
208
|
-
`call_destructive_tool`. A tool on neither maintained list arriving with
|
|
209
|
-
`readOnlyHint: true` keeps that too. Both are the downstream telling you this
|
|
210
|
-
release's allowlist is stale. The one fail-closed exception applies to a name
|
|
211
|
-
this release reviewed and filed destructive: a `grant-customer-entitlement`
|
|
212
|
-
claiming `readOnlyHint: true` is a downstream bug rather than news, and stays on
|
|
213
|
-
the approval path.
|
|
214
|
-
|
|
215
|
-
The tool list is not a fixed set, and the guide says so. RevenueCat gates parts
|
|
216
|
-
of its catalog by plan, platform, and beta enrollment — paywall AI editing,
|
|
217
|
-
benchmarks, experiments, virtual currencies, and the account-billing tools are
|
|
218
|
-
the usual absentees — so search this connector for what it actually exposes
|
|
219
|
-
rather than assuming a documented tool is here.
|
|
220
|
-
|
|
221
|
-
**No schemas are vendored.** The manifest ships names and safety verdicts only.
|
|
222
|
-
The live `tools/list` response remains the schema agents receive; Connecta does
|
|
223
|
-
not replace it with a snapshot or require a maintainer credential to validate
|
|
224
|
-
one.
|
|
225
|
-
|
|
226
|
-
## Rate limits
|
|
227
|
-
|
|
228
|
-
RevenueCat documents numbers, and this connection still declares no budget.
|
|
229
|
-
|
|
230
|
-
API v2 meters per minute and **per domain**
|
|
231
|
-
([rate limits](https://www.revenuecat.com/docs/api-v2#tag/Rate-Limit), read
|
|
232
|
-
2026-08-18):
|
|
233
|
-
|
|
234
|
-
| Domain | Requests per minute |
|
|
235
|
-
| --- | --- |
|
|
236
|
-
| Customer Information | 480 |
|
|
237
|
-
| Virtual Currencies | 480 |
|
|
238
|
-
| Subscription Transactions Refunds | 480 |
|
|
239
|
-
| Audiences | 60 |
|
|
240
|
-
| Project Configuration | 60 |
|
|
241
|
-
| Charts & Metrics | 25 |
|
|
242
|
-
|
|
243
|
-
A `ConnectorCallAdmissionPolicy` carries exactly one rule, so a connector-wide
|
|
244
|
-
budget has to pick one of those six numbers for all ninety-six tools.
|
|
245
|
-
Transcribing 25 would throttle a customer read loop to a nineteenth of its
|
|
246
|
-
documented allowance; transcribing 480 would leave a chart sweep unprotected.
|
|
247
|
-
Neither is the provider's limit, and both would look like RevenueCat being
|
|
248
|
-
flaky. The metering scope says the same thing again: the limit applies per API
|
|
249
|
-
key for app-level keys and **per developer** for developer-level keys, so an
|
|
250
|
-
OAuth session shares one budget with everything else that developer does, which
|
|
251
|
-
a per-runtime counter cannot approximate in either direction.
|
|
252
|
-
|
|
253
|
-
So the number stays with the operator who knows the account (P12), and the
|
|
254
|
-
guide states RevenueCat's own limits instead, along with the `429`,
|
|
255
|
-
`Retry-After`, and `backoff_ms` signals to back off on. Supply one like this:
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
revenuecat("revenuecat", {
|
|
259
|
-
purpose: "Revenue charts and cohort reporting",
|
|
260
|
-
callAdmission: {
|
|
261
|
-
rules: [
|
|
262
|
-
{
|
|
263
|
-
maxConcurrency: 4,
|
|
264
|
-
queueTimeoutMs: 5_000,
|
|
265
|
-
retryAfterMs: 2_000,
|
|
266
|
-
// The Charts & Metrics ceiling, because this connector is used for
|
|
267
|
-
// charts. A customer-lookup connector would declare 480.
|
|
268
|
-
budget: { kind: "rolling-window", maxCalls: 25, windowMs: 60_000 },
|
|
269
|
-
},
|
|
270
|
-
],
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
As with every connector policy this is a **best-effort approximation** of the
|
|
276
|
-
provider's limit, not an enforcement of it. Each runtime keeps its own counter,
|
|
277
|
-
so N Worker isolates or Node processes serving one deployment can each admit up
|
|
278
|
-
to the stated rate. Discovery traffic is outside connector call admission and
|
|
279
|
-
still needs restrained use.
|
|
280
|
-
|
|
281
|
-
## What is not verified
|
|
282
|
-
|
|
283
|
-
- **The 2026-08-30 live review used a project-scoped catalog.** It proves the
|
|
284
|
-
additions that catalog serves, including `get-refund-request-preferences`,
|
|
285
|
-
but cannot prove a globally documented tool was removed. The manifest stays
|
|
286
|
-
a superset because plan, platform, and credential scope hide tools.
|
|
287
|
-
- **No complete schema set is vendored.** The live review read the new schemas,
|
|
288
|
-
but its scoped catalog omitted many classified writes. Runtime schemas still
|
|
289
|
-
come from the server.
|
|
290
|
-
- **Whether `render-paywall-screenshot` mutates anything.** It has no access
|
|
291
|
-
column, and guessing is exactly what P5 exists to prevent.
|
|
292
|
-
|
|
293
|
-
`npm run drift:check -- --docs --provider revenuecat` checks the official setup
|
|
294
|
-
page and its 105-row tool reference without a credential. It compares the
|
|
295
|
-
documented names with the release-reviewed classifications. The screenshot
|
|
296
|
-
tool's blank Access column is a manually reviewed exception: the checker
|
|
297
|
-
reports it separately and Connecta keeps it fail-closed. The check reads names,
|
|
298
|
-
not live schemas or machine-interpreted access verdicts.
|
|
299
|
-
|
|
300
|
-
## Conventions
|
|
301
|
-
|
|
302
|
-
This connection is audited against
|
|
303
|
-
[the provider conventions](./provider-conventions.md). Its verdict per
|
|
304
|
-
convention is the RevenueCat section of
|
|
305
|
-
[the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).
|