@zackbart/connecta 0.18.3 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -4
- package/dist/apps-shell.d.ts +10 -12
- package/dist/apps-shell.js +29 -220
- package/dist/catalog-service.d.ts +16 -13
- package/dist/catalog-service.js +106 -115
- package/dist/catalog.js +29 -46
- package/dist/connector-scope.js +2 -7
- package/dist/connectors/api.d.ts +4 -16
- package/dist/connectors/api.js +19 -46
- package/dist/connectors/guarded-fetch.d.ts +9 -23
- package/dist/connectors/guarded-fetch.js +38 -76
- package/dist/connectors/remote-mcp.js +36 -79
- package/dist/errors.d.ts +6 -27
- package/dist/errors.js +8 -5
- package/dist/execute.d.ts +23 -28
- package/dist/execute.js +101 -257
- package/dist/executor-result.d.ts +1 -0
- package/dist/executor-result.js +4 -11
- package/dist/executors/quickjs-child.js +1 -3
- package/dist/executors/quickjs-runtime.js +1 -3
- package/dist/executors/quickjs.js +1 -3
- package/dist/index.js +134 -123
- package/dist/invocation.d.ts +1 -1
- package/dist/invocation.js +113 -183
- package/dist/meta-tools.d.ts +15 -29
- package/dist/meta-tools.js +41 -582
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/providers/cloudflare.d.ts +2 -18
- package/dist/providers/cloudflare.js +1460 -2451
- package/dist/providers/linear.d.ts +4 -41
- package/dist/providers/linear.js +8 -39
- package/dist/providers/mixpanel.d.ts +3 -25
- package/dist/providers/mixpanel.js +7 -22
- package/dist/providers/notion.d.ts +1 -15
- package/dist/providers/notion.js +44 -173
- package/dist/providers/revenuecat.d.ts +4 -57
- package/dist/providers/revenuecat.js +10 -93
- package/dist/providers/stripe.d.ts +1 -12
- package/dist/providers/stripe.js +7 -45
- package/dist/registry.d.ts +9 -34
- package/dist/registry.js +9 -103
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +3 -3
- package/dist/routes/shared.d.ts +15 -15
- package/dist/routes/shared.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -5
- package/dist/timeout.d.ts +8 -7
- package/dist/timeout.js +47 -38
- package/dist/types.d.ts +3 -3
- package/dist/ui.d.ts +1 -25
- package/dist/ui.js +18 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/call-admission.md +1 -1
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +25 -25
- package/documentation/connectors.md +24 -1
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +4 -30
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +1 -1
- package/documentation/operations.md +29 -25
- package/documentation/provider-conventions.md +4 -5
- package/documentation/revenuecat.md +1 -1
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +43 -8
- package/ethos.md +75 -121
- package/package.json +3 -4
- package/templates/node/package.json +1 -1
- package/documentation/code-first-exploration.md +0 -292
- package/documentation/mcp-2026-07-28.md +0 -46
- package/documentation/mcp-ui-design.md +0 -382
- package/documentation/program-ui-read-calls.md +0 -213
- package/documentation/provider-audit.md +0 -198
- package/documentation/rich-output-design.md +0 -211
|
@@ -17,55 +17,18 @@ export interface LinearOptions {
|
|
|
17
17
|
title?: string;
|
|
18
18
|
/** Which workspace this is and what decisions it answers. */
|
|
19
19
|
purpose: string;
|
|
20
|
-
/**
|
|
21
|
-
* Endpoint selection. Required, and deliberately undefaulted.
|
|
22
|
-
*
|
|
23
|
-
* `"read-only"` binds the connection to Linear's read-only endpoint, whose
|
|
24
|
-
* token is scope-limited downstream — a stronger guarantee than any
|
|
25
|
-
* annotation Connecta applies. `"read-write"` reaches the full API.
|
|
26
|
-
*
|
|
27
|
-
* Neither is a safe default. Defaulting to `"read-write"` hands a deployment
|
|
28
|
-
* write access it never asked for, and defaulting to `"read-only"` turns a
|
|
29
|
-
* deployment that does write into one whose every write fails at Linear —
|
|
30
|
-
* at runtime, where no agent can repair it. So the operator declares it, and
|
|
31
|
-
* a deployment that forgot fails here instead
|
|
32
|
-
* ([#342](https://github.com/zackbart/connecta/issues/342)).
|
|
33
|
-
*/
|
|
20
|
+
/** Required endpoint selection; see `documentation/linear.md`. */
|
|
34
21
|
access: LinearAccess;
|
|
35
|
-
/**
|
|
36
|
-
* OAuth by default. A Linear personal API key works either as a literal
|
|
37
|
-
* header or as an operator-managed credential (`{ type: "credential" }`).
|
|
38
|
-
* Linear's MCP documentation asks for `Authorization: Bearer <yourtoken>`
|
|
39
|
-
* for both API keys and OAuth tokens (https://linear.app/docs/mcp), which is
|
|
40
|
-
* the framing default, so the credential shape needs no `scheme` of its own.
|
|
41
|
-
*/
|
|
22
|
+
/** OAuth or a personal API key; see `documentation/linear.md`. */
|
|
42
23
|
auth?: RemoteMcpAuth;
|
|
43
24
|
/** Workspace-specific conventions appended to the maintained provider guide. */
|
|
44
25
|
instructions?: string;
|
|
45
26
|
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
46
27
|
maxResultBytes?: number;
|
|
47
|
-
/**
|
|
48
|
-
* Optional per-runtime call-admission policy. Deliberately not defaulted:
|
|
49
|
-
* Linear documents no MCP-specific limit, and the underlying API limit is
|
|
50
|
-
* per user per hour, varies by credential type, and is raised dynamically
|
|
51
|
-
* for workspace-level OAuth apps. A hardcoded per-runtime ceiling would
|
|
52
|
-
* either throttle a healthy deployment or fail to protect a busy one, so
|
|
53
|
-
* the number stays with the operator who knows the workspace.
|
|
54
|
-
*/
|
|
28
|
+
/** Optional per-runtime policy; see `documentation/linear.md#rate-limits`. */
|
|
55
29
|
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
56
30
|
}
|
|
57
|
-
/**
|
|
58
|
-
* The manifest this release reviewed: both lists in one place, which is what
|
|
59
|
-
* makes the classification the connector applies and the drift check that runs
|
|
60
|
-
* beside it the same fact (P13). No schema digests yet — no release has read
|
|
61
|
-
* Linear's live schemas and written them down, and an invented digest would
|
|
62
|
-
* report a change that never happened. `npm run drift:check -- --record` reads
|
|
63
|
-
* them from a live workspace and prints the block to paste in
|
|
64
|
-
* ([#351](https://github.com/zackbart/connecta/issues/351)).
|
|
65
|
-
*
|
|
66
|
-
* Exported because the maintainer-run check compares against this manifest and
|
|
67
|
-
* *names* what moved, which the runtime check deliberately cannot.
|
|
68
|
-
*/
|
|
31
|
+
/** Release-reviewed manifest; see provider conventions P5 and P13. */
|
|
69
32
|
export declare const LINEAR_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
|
|
70
33
|
/** A maintained Linear hosted-MCP connection. */
|
|
71
34
|
export declare function linear(id: string, options: LinearOptions): Connector;
|
package/dist/providers/linear.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
2
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
|
+
import { defined } from "../connectors/api.js";
|
|
3
4
|
/**
|
|
4
5
|
* Linear publishes two hosted endpoints. `read-only` is not a client-side
|
|
5
6
|
* filter: it advertises the `read` scope alone, so the token minted for it
|
|
@@ -10,16 +11,7 @@ export const LINEAR_MCP_ENDPOINTS = {
|
|
|
10
11
|
"read-write": "https://mcp.linear.app/mcp",
|
|
11
12
|
"read-only": "https://mcp.linear.app/mcp/readonly",
|
|
12
13
|
};
|
|
13
|
-
/**
|
|
14
|
-
* Tools whose contract is observational rather than mutating.
|
|
15
|
-
*
|
|
16
|
-
* Linear's hosted catalog is not a fixed set — it varies by workspace plan and
|
|
17
|
-
* enabled features (customer requests, releases, and code review are gated),
|
|
18
|
-
* so this list is a superset of what any one workspace lists. A name here that
|
|
19
|
-
* the workspace never returns costs nothing; a real read missing from it merely
|
|
20
|
-
* fails closed. Only a write mistakenly listed here would be a safety bug,
|
|
21
|
-
* which is why ambiguous helpers stay out.
|
|
22
|
-
*/
|
|
14
|
+
/** Reviewed reads; see `documentation/linear.md` and provider convention P5. */
|
|
23
15
|
const READ_ONLY_TOOLS = new Set([
|
|
24
16
|
// Issues
|
|
25
17
|
"list_issues",
|
|
@@ -77,17 +69,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
77
69
|
// than argues.
|
|
78
70
|
"extract_images",
|
|
79
71
|
]);
|
|
80
|
-
/**
|
|
81
|
-
* The maintained write catalog. `"destructive"` tools modify or remove state
|
|
82
|
-
* that already exists; `"additive"` ones only bring something new into being.
|
|
83
|
-
* Both leave the read-only path — the distinction only decides whether the
|
|
84
|
-
* connection asserts `destructiveHint`, which shapes the host's approval copy.
|
|
85
|
-
*
|
|
86
|
-
* Linear's `save_*` tools are upserts: passing an existing record's id updates
|
|
87
|
-
* it in place. An upsert can therefore overwrite, so every `save_*` is
|
|
88
|
-
* destructive even though some calls only create. The `create_*_label` tools
|
|
89
|
-
* are the genuine creates.
|
|
90
|
-
*/
|
|
72
|
+
/** Reviewed writes; `save_*` upsert rationale lives in `documentation/linear.md`. */
|
|
91
73
|
const WRITE_TOOLS = new Map([
|
|
92
74
|
// Issues
|
|
93
75
|
["save_issue", "destructive"],
|
|
@@ -127,18 +109,7 @@ const WRITE_TOOLS = new Map([
|
|
|
127
109
|
["save_customer_need", "destructive"],
|
|
128
110
|
["delete_customer_need", "destructive"],
|
|
129
111
|
]);
|
|
130
|
-
/**
|
|
131
|
-
* The manifest this release reviewed: both lists in one place, which is what
|
|
132
|
-
* makes the classification the connector applies and the drift check that runs
|
|
133
|
-
* beside it the same fact (P13). No schema digests yet — no release has read
|
|
134
|
-
* Linear's live schemas and written them down, and an invented digest would
|
|
135
|
-
* report a change that never happened. `npm run drift:check -- --record` reads
|
|
136
|
-
* them from a live workspace and prints the block to paste in
|
|
137
|
-
* ([#351](https://github.com/zackbart/connecta/issues/351)).
|
|
138
|
-
*
|
|
139
|
-
* Exported because the maintainer-run check compares against this manifest and
|
|
140
|
-
* *names* what moved, which the runtime check deliberately cannot.
|
|
141
|
-
*/
|
|
112
|
+
/** Release-reviewed manifest; see provider conventions P5 and P13. */
|
|
142
113
|
export const LINEAR_VETTED_CATALOG = vettedCatalog({
|
|
143
114
|
reads: READ_ONLY_TOOLS,
|
|
144
115
|
writes: WRITE_TOOLS,
|
|
@@ -214,12 +185,10 @@ export function linear(id, options) {
|
|
|
214
185
|
// guide adds cross-tool sequence advice that is worth reading before a
|
|
215
186
|
// write, not worth loading before every read.
|
|
216
187
|
},
|
|
217
|
-
...(
|
|
218
|
-
|
|
219
|
-
:
|
|
220
|
-
|
|
221
|
-
? { maxResultBytes: options.maxResultBytes }
|
|
222
|
-
: {}),
|
|
188
|
+
...defined({
|
|
189
|
+
callAdmission: options.callAdmission,
|
|
190
|
+
maxResultBytes: options.maxResultBytes,
|
|
191
|
+
}),
|
|
223
192
|
});
|
|
224
193
|
return withVettedCatalog(connector, LINEAR_VETTED_CATALOG);
|
|
225
194
|
}
|
|
@@ -11,12 +11,7 @@ export interface MixpanelOptions {
|
|
|
11
11
|
title?: string;
|
|
12
12
|
/** Who should use this account and for what decisions. */
|
|
13
13
|
purpose: string;
|
|
14
|
-
/**
|
|
15
|
-
* Mixpanel data residency region. Defaults to `"us"`, which is where a
|
|
16
|
-
* project lives unless it was explicitly created in the EU or India
|
|
17
|
-
* residency — the other two are opt-in, so `"us"` is the honest default
|
|
18
|
-
* rather than a convenient one.
|
|
19
|
-
*/
|
|
14
|
+
/** Data residency region; see `documentation/mixpanel.md`. */
|
|
20
15
|
region?: MixpanelRegion;
|
|
21
16
|
/**
|
|
22
17
|
* OAuth by default; static headers support Mixpanel service accounts, and
|
|
@@ -28,27 +23,10 @@ export interface MixpanelOptions {
|
|
|
28
23
|
instructions?: string;
|
|
29
24
|
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
30
25
|
maxResultBytes?: number;
|
|
31
|
-
/**
|
|
32
|
-
* Optional per-runtime call-admission policy. Deliberately not defaulted:
|
|
33
|
-
* Mixpanel meters its MCP server per user per hour, and a per-runtime
|
|
34
|
-
* counter cannot approximate a per-user quota — one runtime serving several
|
|
35
|
-
* users under-counts, and several runtimes sharing one user over-counts.
|
|
36
|
-
* A hardcoded ceiling would therefore either throttle a healthy deployment
|
|
37
|
-
* or fail to protect a busy one, so the number stays with the operator who
|
|
38
|
-
* knows the account.
|
|
39
|
-
*/
|
|
26
|
+
/** Optional per-runtime policy; see `documentation/mixpanel.md#rate-limits`. */
|
|
40
27
|
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
41
28
|
}
|
|
42
|
-
/**
|
|
43
|
-
* The manifest this release reviewed: both lists in one place, which is what
|
|
44
|
-
* makes the classification the connector applies and the drift check that runs
|
|
45
|
-
* beside it the same fact (P13). The 2026-08-13 read-only audit recorded all
|
|
46
|
-
* 63 live US schemas, so a later change is named by the maintainer-run drift
|
|
47
|
-
* check rather than rediscovered in production (#395).
|
|
48
|
-
*
|
|
49
|
-
* Exported because the maintainer-run check compares against this manifest and
|
|
50
|
-
* *names* what moved, which the runtime check deliberately cannot.
|
|
51
|
-
*/
|
|
29
|
+
/** Release-reviewed manifest; see provider conventions P5 and P13. */
|
|
52
30
|
export declare const MIXPANEL_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
|
|
53
31
|
/** A maintained Mixpanel hosted-MCP connection. */
|
|
54
32
|
export declare function mixpanel(id: string, options: MixpanelOptions): Connector;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
2
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
|
+
import { defined } from "../connectors/api.js";
|
|
3
4
|
export const MIXPANEL_MCP_ENDPOINTS = {
|
|
4
5
|
us: "https://mcp.mixpanel.com/mcp",
|
|
5
6
|
eu: "https://mcp-eu.mixpanel.com/mcp",
|
|
@@ -43,12 +44,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
43
44
|
"Get-Feature-Flag-Setup-Guidance",
|
|
44
45
|
"Get-Feature-Flag-Lifecycle-Guidance",
|
|
45
46
|
]);
|
|
46
|
-
/**
|
|
47
|
-
* The maintained write catalog. `"destructive"` tools modify or remove state
|
|
48
|
-
* that already exists; `"additive"` ones only bring something new into being.
|
|
49
|
-
* Both leave the read-only path — the distinction only decides whether the
|
|
50
|
-
* connection asserts `destructiveHint`, which shapes the host's approval copy.
|
|
51
|
-
*/
|
|
47
|
+
/** Reviewed writes; see provider convention P5. */
|
|
52
48
|
const WRITE_TOOLS = new Map([
|
|
53
49
|
["Create-Dashboard", "additive"],
|
|
54
50
|
["Update-Dashboard", "destructive"],
|
|
@@ -145,16 +141,7 @@ const MIXPANEL_SCHEMA_DIGESTS = {
|
|
|
145
141
|
"Update-Lookup-Table": "sha256:ef28f1ec9c9484a7a53b5e2b6659e70f79a8e71e55bda17025aa4ab3c23b6a63",
|
|
146
142
|
"Update-Metric": "sha256:739bb6abdab19282afd4a6644a96183daabe9098a5322c44a77b840608a5ee9d",
|
|
147
143
|
};
|
|
148
|
-
/**
|
|
149
|
-
* The manifest this release reviewed: both lists in one place, which is what
|
|
150
|
-
* makes the classification the connector applies and the drift check that runs
|
|
151
|
-
* beside it the same fact (P13). The 2026-08-13 read-only audit recorded all
|
|
152
|
-
* 63 live US schemas, so a later change is named by the maintainer-run drift
|
|
153
|
-
* check rather than rediscovered in production (#395).
|
|
154
|
-
*
|
|
155
|
-
* Exported because the maintainer-run check compares against this manifest and
|
|
156
|
-
* *names* what moved, which the runtime check deliberately cannot.
|
|
157
|
-
*/
|
|
144
|
+
/** Release-reviewed manifest; see provider conventions P5 and P13. */
|
|
158
145
|
export const MIXPANEL_VETTED_CATALOG = vettedCatalog({
|
|
159
146
|
reads: READ_ONLY_TOOLS,
|
|
160
147
|
writes: WRITE_TOOLS,
|
|
@@ -237,12 +224,10 @@ export function mixpanel(id, options) {
|
|
|
237
224
|
// carries the project-then-context sequence, which is worth reading
|
|
238
225
|
// before an analysis rather than before every call.
|
|
239
226
|
},
|
|
240
|
-
...(
|
|
241
|
-
|
|
242
|
-
:
|
|
243
|
-
|
|
244
|
-
? { maxResultBytes: options.maxResultBytes }
|
|
245
|
-
: {}),
|
|
227
|
+
...defined({
|
|
228
|
+
callAdmission: options.callAdmission,
|
|
229
|
+
maxResultBytes: options.maxResultBytes,
|
|
230
|
+
}),
|
|
246
231
|
});
|
|
247
232
|
return withVettedCatalog(connector, MIXPANEL_VETTED_CATALOG);
|
|
248
233
|
}
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import type { Connector } from "../types.js";
|
|
2
2
|
/** Notion's REST origin. Every tool below speaks to exactly this host. */
|
|
3
3
|
export declare const NOTION_API_BASE_URL = "https://api.notion.com";
|
|
4
|
-
/**
|
|
5
|
-
* The API version this connection is written against, pinned deliberately.
|
|
6
|
-
*
|
|
7
|
-
* Notion's versions are date-named and old ones keep working indefinitely, so
|
|
8
|
-
* a version override would look harmless. It is not: `2026-03-11` is the
|
|
9
|
-
* version in which databases split into data sources, `archived` became
|
|
10
|
-
* `in_trash`, and block append took a `position` object instead of `after`.
|
|
11
|
-
* Every projection and every write body below assumes those shapes. A
|
|
12
|
-
* deployment that pinned an older version would get silently wrong results
|
|
13
|
-
* rather than a loud failure, so the option does not exist.
|
|
14
|
-
*
|
|
15
|
-
* Notion also ships additive changes to *every* version at once, which is why
|
|
16
|
-
* the projections below degrade gracefully on unknown property and block types
|
|
17
|
-
* rather than switching exhaustively.
|
|
18
|
-
*/
|
|
4
|
+
/** See documentation/notion.md#the-pinned-api-version. */
|
|
19
5
|
export declare const NOTION_API_VERSION = "2026-03-11";
|
|
20
6
|
export interface NotionOptions {
|
|
21
7
|
/** Human-readable display name; defaults to "Notion". */
|
package/dist/providers/notion.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
import { api } from "../connectors/api.js";
|
|
2
|
-
import { guardedFetch } from "../connectors/guarded-fetch.js";
|
|
2
|
+
import { guardedFetch, retryAfterMs, } from "../connectors/guarded-fetch.js";
|
|
3
3
|
import { ConnectorCallError } from "../errors.js";
|
|
4
4
|
/** Notion's REST origin. Every tool below speaks to exactly this host. */
|
|
5
5
|
export const NOTION_API_BASE_URL = "https://api.notion.com";
|
|
6
|
-
/**
|
|
7
|
-
* The API version this connection is written against, pinned deliberately.
|
|
8
|
-
*
|
|
9
|
-
* Notion's versions are date-named and old ones keep working indefinitely, so
|
|
10
|
-
* a version override would look harmless. It is not: `2026-03-11` is the
|
|
11
|
-
* version in which databases split into data sources, `archived` became
|
|
12
|
-
* `in_trash`, and block append took a `position` object instead of `after`.
|
|
13
|
-
* Every projection and every write body below assumes those shapes. A
|
|
14
|
-
* deployment that pinned an older version would get silently wrong results
|
|
15
|
-
* rather than a loud failure, so the option does not exist.
|
|
16
|
-
*
|
|
17
|
-
* Notion also ships additive changes to *every* version at once, which is why
|
|
18
|
-
* the projections below degrade gracefully on unknown property and block types
|
|
19
|
-
* rather than switching exhaustively.
|
|
20
|
-
*/
|
|
6
|
+
/** See documentation/notion.md#the-pinned-api-version. */
|
|
21
7
|
export const NOTION_API_VERSION = "2026-03-11";
|
|
22
8
|
/** Notion's hard cap on `page_size` for every paginated endpoint. */
|
|
23
9
|
const MAX_PAGE_SIZE = 100;
|
|
@@ -25,15 +11,7 @@ const MAX_PAGE_SIZE = 100;
|
|
|
25
11
|
const DEFAULT_PAGE_SIZE = 25;
|
|
26
12
|
/** Notion's cap on `children` per append, and on blocks per page create. */
|
|
27
13
|
const MAX_CHILDREN_PER_REQUEST = 100;
|
|
28
|
-
/**
|
|
29
|
-
* Ceiling on downstream requests inside one `get_page_content` call.
|
|
30
|
-
*
|
|
31
|
-
* Recursive block traversal is the one tool here that can turn a single agent
|
|
32
|
-
* call into unbounded provider traffic, and call admission meters *tool calls*,
|
|
33
|
-
* not the fetches inside them. A depth-2 walk of a large page would otherwise
|
|
34
|
-
* spend the whole rate-limit budget without ever being rejected. When the walk
|
|
35
|
-
* hits this ceiling it stops and says so in `truncated`.
|
|
36
|
-
*/
|
|
14
|
+
/** See documentation/notion.md#rate-limiting. */
|
|
37
15
|
const MAX_CONTENT_REQUESTS = 20;
|
|
38
16
|
/**
|
|
39
17
|
* The largest response this connection will read.
|
|
@@ -44,24 +22,7 @@ const MAX_CONTENT_REQUESTS = 20;
|
|
|
44
22
|
* a budget any real read has to think about.
|
|
45
23
|
*/
|
|
46
24
|
const NOTION_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
47
|
-
/**
|
|
48
|
-
* Approximates Notion's documented limit: "an average of three requests per
|
|
49
|
-
* second, with some bursts beyond the average allowed" — expressed as a
|
|
50
|
-
* rolling minute so short bursts pass and a sustained loop does not.
|
|
51
|
-
*
|
|
52
|
-
* `maxConcurrency` is the load-bearing half. A budget alone is an average, and
|
|
53
|
-
* an averaged budget cannot stop a program from firing forty calls in the same
|
|
54
|
-
* tick; the concurrency cap keeps a burst shaped roughly like the one Notion
|
|
55
|
-
* documents. Neither half is a guarantee: admission meters *tool calls*, and
|
|
56
|
-
* one admitted `get_page_content` can spend up to `MAX_CONTENT_REQUESTS`
|
|
57
|
-
* fetches, so this bounds tool calls rather than requests. Declaring the cap is
|
|
58
|
-
* also what makes the queue settings legal — the admission controller refuses
|
|
59
|
-
* queue settings without a queue at construction.
|
|
60
|
-
*
|
|
61
|
-
* Per-runtime, like every connector budget: N Worker isolates or Node
|
|
62
|
-
* processes serving one deployment each keep their own counter. This
|
|
63
|
-
* approximates the provider's limit; it does not enforce it.
|
|
64
|
-
*/
|
|
25
|
+
/** See documentation/notion.md#rate-limiting. */
|
|
65
26
|
const NOTION_ADMISSION = {
|
|
66
27
|
rules: [
|
|
67
28
|
{
|
|
@@ -73,49 +34,24 @@ const NOTION_ADMISSION = {
|
|
|
73
34
|
},
|
|
74
35
|
],
|
|
75
36
|
};
|
|
76
|
-
/**
|
|
77
|
-
function
|
|
78
|
-
if (!header)
|
|
79
|
-
return undefined;
|
|
80
|
-
const seconds = Number(header.trim());
|
|
81
|
-
if (!Number.isFinite(seconds) || seconds < 0)
|
|
82
|
-
return undefined;
|
|
83
|
-
return Math.trunc(seconds * 1000);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Map one Notion error response onto connecta's typed failures.
|
|
87
|
-
*
|
|
88
|
-
* The mapping is deliberately not one-to-one. Notion's `code` values describe
|
|
89
|
-
* what its API thinks happened; connecta's codes describe what the *caller*
|
|
90
|
-
* should do next, and two of Notion's are easy to mistranslate:
|
|
91
|
-
*
|
|
92
|
-
* - `restricted_resource` (403) is not `auth_required`. The token is fine; the
|
|
93
|
-
* integration lacks a capability or was never shared this object. Routing it
|
|
94
|
-
* to `auth_required` would send an agent to `authorize_connector`, which
|
|
95
|
-
* cannot fix it. It is a non-retryable call failure with instructions.
|
|
96
|
-
* - `object_not_found` (404) is overloaded by Notion itself — it means "no
|
|
97
|
-
* such object" *or* "not shared with this integration", and the API will not
|
|
98
|
-
* say which. The message says both, because treating it as definitive
|
|
99
|
-
* absence is how an agent concludes a page was deleted when it simply was
|
|
100
|
-
* never shared.
|
|
101
|
-
*/
|
|
102
|
-
function notionFailure(status, body, retryAfterHeader) {
|
|
37
|
+
/** See documentation/notion.md#typed-failures. */
|
|
38
|
+
function notionFailure(status, body, headers) {
|
|
103
39
|
const code = typeof body?.["code"] === "string" ? body["code"] : undefined;
|
|
104
40
|
const detail = typeof body?.["message"] === "string" && body["message"].trim()
|
|
105
41
|
? body["message"].trim()
|
|
106
42
|
: `Notion returned HTTP ${status}.`;
|
|
107
|
-
const
|
|
43
|
+
const retryAfter = retryAfterMs(headers);
|
|
108
44
|
const labelled = code ? `Notion ${code}: ${detail}` : detail;
|
|
109
45
|
if (status === 429) {
|
|
110
46
|
const additional = body?.["additional_data"];
|
|
111
47
|
const reason = additional && typeof additional === "object"
|
|
112
48
|
? additional["rate_limit_reason"]
|
|
113
49
|
: undefined;
|
|
114
|
-
return new ConnectorCallError("rate_limited", `${labelled}${typeof reason === "string" ? ` (limit: ${reason})` : ""} Notion allows roughly three requests per second per integration.`, { retryAfterMs:
|
|
50
|
+
return new ConnectorCallError("rate_limited", `${labelled}${typeof reason === "string" ? ` (limit: ${reason})` : ""} Notion allows roughly three requests per second per integration.`, { retryAfterMs: retryAfter ?? 1_000 });
|
|
115
51
|
}
|
|
116
52
|
if (status === 529) {
|
|
117
53
|
// Notion documents 529 alongside 429: back off and respect Retry-After.
|
|
118
|
-
return new ConnectorCallError("unavailable", `${labelled} Notion is overloaded; retry after the reported window.`, { retryAfterMs:
|
|
54
|
+
return new ConnectorCallError("unavailable", `${labelled} Notion is overloaded; retry after the reported window.`, { retryAfterMs: retryAfter ?? 5_000 });
|
|
119
55
|
}
|
|
120
56
|
if (status === 401) {
|
|
121
57
|
return new ConnectorCallError("auth_required", `${labelled} The Notion integration token is missing or invalid — an operator must set a valid token on /credentials.`);
|
|
@@ -133,25 +69,16 @@ function notionFailure(status, body, retryAfterHeader) {
|
|
|
133
69
|
return new ConnectorCallError("invalid_args", labelled);
|
|
134
70
|
}
|
|
135
71
|
if (status === 409) {
|
|
136
|
-
return new ConnectorCallError("unavailable", `${labelled} Notion reported a write conflict; this is safe to retry.`, { retryAfterMs:
|
|
72
|
+
return new ConnectorCallError("unavailable", `${labelled} Notion reported a write conflict; this is safe to retry.`, { retryAfterMs: retryAfter ?? 1_000 });
|
|
137
73
|
}
|
|
138
74
|
if (status >= 500) {
|
|
139
|
-
return new ConnectorCallError("unavailable", `${labelled} Notion is failing upstream.`,
|
|
75
|
+
return new ConnectorCallError("unavailable", `${labelled} Notion is failing upstream.`, retryAfter !== undefined ? { retryAfterMs: retryAfter } : {});
|
|
140
76
|
}
|
|
141
77
|
return new ConnectorCallError("connector_call_failed", labelled, {
|
|
142
78
|
retryable: false,
|
|
143
79
|
});
|
|
144
80
|
}
|
|
145
|
-
/**
|
|
146
|
-
* The one transport every Notion tool goes through.
|
|
147
|
-
*
|
|
148
|
-
* URL confinement, `ctx.signal`, redirect refusal, bounded reads, and the
|
|
149
|
-
* "could not reach the provider" normalization live in the shared helper. The
|
|
150
|
-
* two things that cannot be shared stay here: the integration token becomes
|
|
151
|
-
* the headers Notion accepts, and `notionFailure` decides what a status means
|
|
152
|
-
* — which is the whole reason a generic HTTP client is the wrong shape, given
|
|
153
|
-
* that Notion's 403 and 404 both mean something no status table would guess.
|
|
154
|
-
*/
|
|
81
|
+
/** See documentation/connectors.md#the-guarded-fetch-transport. */
|
|
155
82
|
const send = guardedFetch({
|
|
156
83
|
provider: "Notion",
|
|
157
84
|
baseUrl: NOTION_API_BASE_URL,
|
|
@@ -167,36 +94,17 @@ const send = guardedFetch({
|
|
|
167
94
|
});
|
|
168
95
|
async function notionRequest(ctx, request) {
|
|
169
96
|
return await send(request, ctx, async (response) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
payload = (await response.json());
|
|
175
|
-
}
|
|
176
|
-
catch (cause) {
|
|
177
|
-
// A transport failure is not a parse failure. The connector's byte
|
|
178
|
-
// ceiling fires from inside this read, and swallowing it would report an
|
|
179
|
-
// oversized response as an empty success on a 2xx.
|
|
180
|
-
if (cause instanceof ConnectorCallError)
|
|
181
|
-
throw cause;
|
|
182
|
-
payload = undefined;
|
|
183
|
-
}
|
|
97
|
+
const parsed = await response.jsonResult();
|
|
98
|
+
const payload = "value" in parsed
|
|
99
|
+
? parsed.value
|
|
100
|
+
: undefined;
|
|
184
101
|
if (!response.ok) {
|
|
185
|
-
throw notionFailure(response.status, payload, response.headers
|
|
102
|
+
throw notionFailure(response.status, payload, response.headers);
|
|
186
103
|
}
|
|
187
104
|
return payload ?? {};
|
|
188
105
|
});
|
|
189
106
|
}
|
|
190
|
-
//
|
|
191
|
-
// Projections
|
|
192
|
-
//
|
|
193
|
-
// Notion's payloads are the reason this connection is hand-written. A single
|
|
194
|
-
// page carries every property as a discriminated wrapper object, every string
|
|
195
|
-
// as an array of rich-text runs each with its own annotations block, and every
|
|
196
|
-
// user reference as a nested object. The projections below reduce that to what
|
|
197
|
-
// an agent reasons about — ids, plain text, and flattened values — and each
|
|
198
|
-
// tool that can lose information this way takes `raw: true` to opt out.
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
107
|
+
// Projections: documentation/notion.md#lean-projections-and-the-raw-escape-hatch.
|
|
200
108
|
/** Concatenate a rich-text array to its plain text. Safe for every variant. */
|
|
201
109
|
function plainText(value) {
|
|
202
110
|
if (!Array.isArray(value))
|
|
@@ -380,14 +288,7 @@ function projectPage(page, select) {
|
|
|
380
288
|
...(truncated.length ? { truncated_properties: truncated } : {}),
|
|
381
289
|
};
|
|
382
290
|
}
|
|
383
|
-
/**
|
|
384
|
-
* Search returns pages and data sources; both get the identity fields only.
|
|
385
|
-
*
|
|
386
|
-
* Deliberately omits `properties`. A 25-result search over a populated
|
|
387
|
-
* database would otherwise return several hundred flattened property values
|
|
388
|
-
* for results the agent is about to discard — the exact bloat this connection
|
|
389
|
-
* exists to remove. `get_page` fetches properties for the one that matched.
|
|
390
|
-
*/
|
|
291
|
+
/** See documentation/notion.md#lean-projections-and-the-raw-escape-hatch. */
|
|
391
292
|
function projectSearchHit(hit) {
|
|
392
293
|
if (hit?.object === "data_source") {
|
|
393
294
|
return {
|
|
@@ -454,12 +355,7 @@ function projectBlock(block, depth) {
|
|
|
454
355
|
projected["icon"] = iconRef(payload?.icon);
|
|
455
356
|
break;
|
|
456
357
|
default:
|
|
457
|
-
//
|
|
458
|
-
// type is expected rather than exceptional — `meeting_notes` shipped in
|
|
459
|
-
// the very version pinned above. A payload built around `rich_text` is
|
|
460
|
-
// already fully represented by `text`; one that is not would otherwise
|
|
461
|
-
// project to an empty string and lose its entire body, so it keeps the
|
|
462
|
-
// payload verbatim. `color` alone is presentation, not content.
|
|
358
|
+
// Rationale: documentation/notion.md#lean-projections-and-the-raw-escape-hatch.
|
|
463
359
|
if (carriesUnprojectedContent(payload))
|
|
464
360
|
projected["raw"] = payload;
|
|
465
361
|
break;
|
|
@@ -683,6 +579,15 @@ function listEnvelope(payload, results) {
|
|
|
683
579
|
next_cursor: payload?.next_cursor ?? null,
|
|
684
580
|
};
|
|
685
581
|
}
|
|
582
|
+
function mappedListEnvelope(payload, project) {
|
|
583
|
+
return listEnvelope(payload, (payload?.results ?? []).map(project));
|
|
584
|
+
}
|
|
585
|
+
function pagination(args, defaultPageSize) {
|
|
586
|
+
return {
|
|
587
|
+
page_size: resolvePageSize(args.page_size, defaultPageSize),
|
|
588
|
+
...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
686
591
|
/** Exactly-one-of validation, phrased so the agent knows what to send next. */
|
|
687
592
|
function requireExactlyOne(provided, hint) {
|
|
688
593
|
const present = provided.filter(([, value]) => value !== undefined && value !== null && value !== "");
|
|
@@ -741,13 +646,9 @@ function buildTools(defaultPageSize) {
|
|
|
741
646
|
required: ["id", "object", "title"],
|
|
742
647
|
}),
|
|
743
648
|
handler: async (args, ctx) => {
|
|
744
|
-
const body =
|
|
745
|
-
page_size: resolvePageSize(args.page_size, defaultPageSize),
|
|
746
|
-
};
|
|
649
|
+
const body = pagination(args, defaultPageSize);
|
|
747
650
|
if (args.query)
|
|
748
651
|
body["query"] = args.query;
|
|
749
|
-
if (args.start_cursor)
|
|
750
|
-
body["start_cursor"] = args.start_cursor;
|
|
751
652
|
if (args.object_type) {
|
|
752
653
|
body["filter"] = { property: "object", value: args.object_type };
|
|
753
654
|
}
|
|
@@ -767,7 +668,7 @@ function buildTools(defaultPageSize) {
|
|
|
767
668
|
});
|
|
768
669
|
if (args.raw)
|
|
769
670
|
return payload;
|
|
770
|
-
return
|
|
671
|
+
return mappedListEnvelope(payload, projectSearchHit);
|
|
771
672
|
},
|
|
772
673
|
},
|
|
773
674
|
{
|
|
@@ -826,14 +727,9 @@ function buildTools(defaultPageSize) {
|
|
|
826
727
|
additionalProperties: false,
|
|
827
728
|
},
|
|
828
729
|
outputSchema: {
|
|
829
|
-
|
|
730
|
+
...listOutputSchema(BLOCK_OUTPUT_SCHEMA),
|
|
830
731
|
properties: {
|
|
831
|
-
|
|
832
|
-
has_more: {
|
|
833
|
-
type: "boolean",
|
|
834
|
-
description: "True when the top level has another page of blocks.",
|
|
835
|
-
},
|
|
836
|
-
next_cursor: { type: ["string", "null"] },
|
|
732
|
+
...listOutputSchema(BLOCK_OUTPUT_SCHEMA)["properties"],
|
|
837
733
|
truncated: {
|
|
838
734
|
type: "boolean",
|
|
839
735
|
description: "True when the nested walk stopped at its request ceiling. Some descendants are missing; re-read a specific block_id to continue.",
|
|
@@ -846,10 +742,7 @@ function buildTools(defaultPageSize) {
|
|
|
846
742
|
const top = await notionRequest(ctx, {
|
|
847
743
|
method: "GET",
|
|
848
744
|
path: `/v1/blocks/${encodeURIComponent(args.block_id)}/children`,
|
|
849
|
-
query:
|
|
850
|
-
page_size: pageSize,
|
|
851
|
-
...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
|
|
852
|
-
},
|
|
745
|
+
query: pagination(args, pageSize),
|
|
853
746
|
});
|
|
854
747
|
if (args.raw)
|
|
855
748
|
return top;
|
|
@@ -927,10 +820,7 @@ function buildTools(defaultPageSize) {
|
|
|
927
820
|
const payload = await notionRequest(ctx, {
|
|
928
821
|
method: "GET",
|
|
929
822
|
path: `/v1/pages/${encodeURIComponent(args.page_id)}/properties/${encodeURIComponent(args.property_id)}`,
|
|
930
|
-
query:
|
|
931
|
-
page_size: resolvePageSize(args.page_size, defaultPageSize),
|
|
932
|
-
...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
|
|
933
|
-
},
|
|
823
|
+
query: pagination(args, defaultPageSize),
|
|
934
824
|
});
|
|
935
825
|
if (args.raw)
|
|
936
826
|
return payload;
|
|
@@ -939,7 +829,7 @@ function buildTools(defaultPageSize) {
|
|
|
939
829
|
// A list envelope's own `type` is the literal "property_item";
|
|
940
830
|
// the property's real type sits one level down.
|
|
941
831
|
type: payload?.property_item?.type ?? payload?.type ?? null,
|
|
942
|
-
...
|
|
832
|
+
...mappedListEnvelope(payload, projectPropertyItem),
|
|
943
833
|
};
|
|
944
834
|
}
|
|
945
835
|
return {
|
|
@@ -1105,15 +995,11 @@ function buildTools(defaultPageSize) {
|
|
|
1105
995
|
},
|
|
1106
996
|
outputSchema: listOutputSchema(PAGE_OUTPUT_SCHEMA),
|
|
1107
997
|
handler: async (args, ctx) => {
|
|
1108
|
-
const body =
|
|
1109
|
-
page_size: resolvePageSize(args.page_size, defaultPageSize),
|
|
1110
|
-
};
|
|
998
|
+
const body = pagination(args, defaultPageSize);
|
|
1111
999
|
if (args.filter)
|
|
1112
1000
|
body["filter"] = args.filter;
|
|
1113
1001
|
if (args.sorts)
|
|
1114
1002
|
body["sorts"] = args.sorts;
|
|
1115
|
-
if (args.start_cursor)
|
|
1116
|
-
body["start_cursor"] = args.start_cursor;
|
|
1117
1003
|
const payload = await notionRequest(ctx, {
|
|
1118
1004
|
method: "POST",
|
|
1119
1005
|
path: `/v1/data_sources/${encodeURIComponent(args.data_source_id)}/query`,
|
|
@@ -1121,7 +1007,7 @@ function buildTools(defaultPageSize) {
|
|
|
1121
1007
|
});
|
|
1122
1008
|
if (args.raw)
|
|
1123
1009
|
return payload;
|
|
1124
|
-
return
|
|
1010
|
+
return mappedListEnvelope(payload, (row) => projectPage(row, args.properties));
|
|
1125
1011
|
},
|
|
1126
1012
|
},
|
|
1127
1013
|
{
|
|
@@ -1142,12 +1028,9 @@ function buildTools(defaultPageSize) {
|
|
|
1142
1028
|
const payload = await notionRequest(ctx, {
|
|
1143
1029
|
method: "GET",
|
|
1144
1030
|
path: "/v1/users",
|
|
1145
|
-
query:
|
|
1146
|
-
page_size: resolvePageSize(args.page_size, defaultPageSize),
|
|
1147
|
-
...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
|
|
1148
|
-
},
|
|
1031
|
+
query: pagination(args, defaultPageSize),
|
|
1149
1032
|
});
|
|
1150
|
-
return
|
|
1033
|
+
return mappedListEnvelope(payload, projectUser);
|
|
1151
1034
|
},
|
|
1152
1035
|
},
|
|
1153
1036
|
{
|
|
@@ -1206,13 +1089,12 @@ function buildTools(defaultPageSize) {
|
|
|
1206
1089
|
path: "/v1/comments",
|
|
1207
1090
|
query: {
|
|
1208
1091
|
block_id: args.block_id,
|
|
1209
|
-
|
|
1210
|
-
...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
|
|
1092
|
+
...pagination(args, defaultPageSize),
|
|
1211
1093
|
},
|
|
1212
1094
|
});
|
|
1213
1095
|
if (args.raw)
|
|
1214
1096
|
return payload;
|
|
1215
|
-
return
|
|
1097
|
+
return mappedListEnvelope(payload, projectComment);
|
|
1216
1098
|
},
|
|
1217
1099
|
},
|
|
1218
1100
|
// --------------------------------------------------------------- writes
|
|
@@ -1221,12 +1103,7 @@ function buildTools(defaultPageSize) {
|
|
|
1221
1103
|
description: "Create a page, either as a child of another page or as a row in a data source. Notion has no idempotency key: a retried create makes a second page, so confirm with search before repeating one.",
|
|
1222
1104
|
annotations: { readOnlyHint: false },
|
|
1223
1105
|
inputSchema: {
|
|
1224
|
-
//
|
|
1225
|
-
// an exclusive choice a plain-object `required` list cannot express,
|
|
1226
|
-
// and the top-level `anyOf` that could would cost the tool its
|
|
1227
|
-
// `inputKeys` in discovery. The choice is stated in both parent
|
|
1228
|
-
// descriptions and enforced locally as `invalid_args` before any round
|
|
1229
|
-
// trip ([#342](https://github.com/zackbart/connecta/issues/342)).
|
|
1106
|
+
// See documentation/notion.md#what-this-connection-does-not-do.
|
|
1230
1107
|
required: [],
|
|
1231
1108
|
type: "object",
|
|
1232
1109
|
properties: {
|
|
@@ -1515,13 +1392,7 @@ function buildTools(defaultPageSize) {
|
|
|
1515
1392
|
// ---------------------------------------------------------------------------
|
|
1516
1393
|
// Guide and constructor
|
|
1517
1394
|
// ---------------------------------------------------------------------------
|
|
1518
|
-
/**
|
|
1519
|
-
* Only what the schemas cannot carry.
|
|
1520
|
-
*
|
|
1521
|
-
* Marked `required` because the database/data-source split is a mandatory
|
|
1522
|
-
* cross-tool sequence, not advice: a caller who reaches for `query_data_source`
|
|
1523
|
-
* with the id in a Notion URL gets a failure no schema warned it about.
|
|
1524
|
-
*/
|
|
1395
|
+
/** See documentation/notion.md#databases-contain-data-sources. */
|
|
1525
1396
|
function usageGuide(purpose, instructions) {
|
|
1526
1397
|
const accountInstructions = instructions?.trim();
|
|
1527
1398
|
return `# Notion usage
|