@zackbart/connecta 0.34.0 → 0.34.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -2
  3. package/bin/version.mjs +1 -1
  4. package/dist/catalog-drift.d.ts +30 -26
  5. package/dist/catalog-drift.js +169 -84
  6. package/dist/catalog-service.js +1 -1
  7. package/dist/connectors/api-connector.d.ts +3 -0
  8. package/dist/connectors/api-connector.js +5 -1
  9. package/dist/connectors/option-shapes.js +1 -1
  10. package/dist/connectors/remote-mcp.d.ts +2 -0
  11. package/dist/connectors/remote-mcp.js +5 -1
  12. package/dist/describe-config.d.ts +2 -0
  13. package/dist/describe-config.js +1 -0
  14. package/dist/invocation.js +3 -2
  15. package/dist/meta-tools.js +3 -2
  16. package/dist/operator-ui/config-summary.js +3 -0
  17. package/dist/operator-ui/connection-map.js +2 -1
  18. package/dist/operator-ui/endpoints.js +1 -1
  19. package/dist/operator-ui/generated.js +3 -3
  20. package/dist/operator-ui/health.js +52 -0
  21. package/dist/operator-ui/overview.js +6 -17
  22. package/dist/operator-ui/view.d.ts +3 -4
  23. package/dist/operator-ui/view.js +29 -11
  24. package/dist/provider.d.ts +6 -1
  25. package/dist/provider.js +18 -1
  26. package/dist/providers/_shared/google/workspace.d.ts +3 -0
  27. package/dist/providers/_shared/google/workspace.js +1 -1
  28. package/dist/providers/basecamp/index.d.ts +2 -0
  29. package/dist/providers/breeze/index.d.ts +2 -0
  30. package/dist/providers/ccb/index.d.ts +2 -0
  31. package/dist/providers/ccb/index.js +1 -1
  32. package/dist/providers/cloudflare/index.d.ts +2 -0
  33. package/dist/providers/cloudflare/index.js +5 -1
  34. package/dist/providers/cloudflare/openapi.generated.js +3296 -3206
  35. package/dist/providers/cloudflare/rest.js +19 -0
  36. package/dist/providers/cloudflare/skill.generated.js +2 -2
  37. package/dist/providers/cloudflare/value-safety.js +51 -18
  38. package/dist/providers/intercom/index.d.ts +24 -0
  39. package/dist/providers/intercom/index.js +121 -0
  40. package/dist/providers/intercom/skill.generated.js +8 -0
  41. package/dist/providers/linear/index.js +204 -19
  42. package/dist/providers/linear/skill.generated.js +2 -2
  43. package/dist/providers/mixpanel/index.d.ts +2 -0
  44. package/dist/providers/mixpanel/index.js +21 -17
  45. package/dist/providers/mixpanel/skill.generated.js +1 -1
  46. package/dist/providers/notion/index.d.ts +2 -0
  47. package/dist/providers/notion/index.js +1 -1
  48. package/dist/providers/overflow/index.d.ts +2 -0
  49. package/dist/providers/planning-center/index.d.ts +2 -0
  50. package/dist/providers/revenuecat/index.d.ts +5 -3
  51. package/dist/providers/revenuecat/index.js +147 -7
  52. package/dist/providers/revenuecat/skill.generated.js +3 -3
  53. package/dist/providers/stripe/index.d.ts +2 -0
  54. package/dist/providers/stripe/index.js +50 -30
  55. package/dist/providers/stripe/skill.generated.js +2 -1
  56. package/dist/providers/tithely/index.d.ts +2 -0
  57. package/dist/providers/vercel/index.d.ts +2 -0
  58. package/dist/providers/vercel/index.js +1 -1
  59. package/dist/providers/vercel/openapi.generated.js +391 -374
  60. package/dist/providers/vercel/skill.generated.js +1 -1
  61. package/dist/providers/vercel/value-safety.js +14 -4
  62. package/dist/registry.d.ts +10 -14
  63. package/dist/registry.js +62 -62
  64. package/dist/routes/ui-data.js +8 -2
  65. package/dist/skills.js +8 -2
  66. package/dist/types.d.ts +18 -4
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/documentation/architecture.md +48 -5
  70. package/documentation/code-mode.md +7 -5
  71. package/documentation/integrating.md +55 -1
  72. package/documentation/meta-tools.md +2 -2
  73. package/documentation/operating.md +1 -1
  74. package/documentation/operator-tests.md +8 -2
  75. package/documentation/operator-ui.md +58 -11
  76. package/documentation/provider-auth-migration.md +22 -0
  77. package/package.json +5 -1
  78. package/templates/node/package.json +1 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The next steady evidence. A settled connector takes its current row; one
3
+ * being read again keeps its previous evidence when this identity has any.
4
+ * Returns `previous` itself when nothing changed, so it can live in React
5
+ * state. A new identity starts empty.
6
+ */
7
+ export function steadyHealth(state, previous) {
8
+ const same = previous?.generation === state.generation ? previous : null;
9
+ const contract = state.contract?.role === "maintainer" && state.data ? state.contract : null;
10
+ // No contract on screen: keep what was shown until the next one arrives.
11
+ if (!contract || !state.data)
12
+ return same ?? { generation: state.generation, rows: Object.create(null) };
13
+ const reading = new Set(state.data.connectors.filter((c) => c.status === "loading").map((c) => c.id));
14
+ const held = same?.rows ?? Object.create(null);
15
+ const rows = Object.create(null);
16
+ let changed = !same || Object.keys(held).length !== contract.live.connectors.length;
17
+ for (const live of contract.live.connectors) {
18
+ const before = Object.hasOwn(held, live.id) ? held[live.id] : undefined;
19
+ if (before && reading.has(live.id)) {
20
+ rows[live.id] = before;
21
+ continue;
22
+ }
23
+ const failed = Object.hasOwn(state.connectorFailures, live.id) && Boolean(state.connectorFailures[live.id]);
24
+ if (before?.live === live && before.failed === failed)
25
+ rows[live.id] = before;
26
+ else {
27
+ rows[live.id] = { live, failed };
28
+ changed = true;
29
+ }
30
+ }
31
+ return changed ? { generation: state.generation, rows } : same;
32
+ }
33
+ /** The contract each view renders health from, with every live row replaced by its steady evidence. */
34
+ export function shownHealth(contract, health) {
35
+ const signals = Object.create(null);
36
+ if (!contract)
37
+ return { contract, signals };
38
+ let replaced = false;
39
+ const connectors = contract.live.connectors.map((live) => {
40
+ const row = Object.hasOwn(health.rows, live.id) ? health.rows[live.id] : undefined;
41
+ if (row?.failed)
42
+ signals[live.id] = { failed: true };
43
+ if (!row || row.live === live)
44
+ return live;
45
+ replaced = true;
46
+ return row.live;
47
+ });
48
+ return {
49
+ contract: replaced ? { ...contract, live: { ...contract.live, connectors } } : contract,
50
+ signals,
51
+ };
52
+ }
@@ -13,7 +13,7 @@ function affects(paths) {
13
13
  return paths.length ? `Affects ${joinList(paths)}` : "No endpoint serves it";
14
14
  }
15
15
  /**
16
- * One row per connector that is not healthy, failing first, then one per
16
+ * One row per connector that is not healthy, in config order, then one per
17
17
  * agent that sits in an endpoint without a working token. Each row carries
18
18
  * the fix the rest of the UI already offers for it.
19
19
  */
@@ -26,7 +26,7 @@ export function attentionItems(contract, options = {}) {
26
26
  const live = contract.live.connectors.find((c) => c.id === config.id);
27
27
  if (!live)
28
28
  continue;
29
- const signals = options.signals?.[config.id] ?? {};
29
+ const signals = options.signals && Object.hasOwn(options.signals, config.id) ? options.signals[config.id] : {};
30
30
  const verdict = connectorVerdict(live, signals);
31
31
  if ((verdict.tone !== "warn" && verdict.tone !== "danger") || !verdict.action)
32
32
  continue;
@@ -71,15 +71,6 @@ export function attentionItems(contract, options = {}) {
71
71
  fix: null,
72
72
  });
73
73
  }
74
- else {
75
- // Healthy but drifted: the live tool list differs from the reviewed one.
76
- connectors.push({
77
- ...base,
78
- problem: "has a changed tool list",
79
- body: affects(paths),
80
- fix: { kind: "catalog_drift", connectorId: config.id },
81
- });
82
- }
83
74
  }
84
75
  const agents = contract.agents
85
76
  .filter((agent) => agent.pools.length && agent.token.state !== "issued")
@@ -101,11 +92,9 @@ export function attentionItems(contract, options = {}) {
101
92
  fix: null,
102
93
  };
103
94
  });
104
- return [
105
- ...connectors.filter((item) => item.tone === "danger"),
106
- ...connectors.filter((item) => item.tone === "warn"),
107
- ...agents,
108
- ];
95
+ // Severity changes as individual details settle. Config order keeps surviving
96
+ // rows in place without waiting for the slowest connector's read.
97
+ return [...connectors, ...agents];
109
98
  }
110
99
  /** The numbers above the list, counted from the same map the page draws. */
111
100
  export function overviewHeadline(contract, map, items, signals = {}) {
@@ -113,7 +102,7 @@ export function overviewHeadline(contract, map, items, signals = {}) {
113
102
  const live = contract.live.connectors.find((c) => c.id === config.id);
114
103
  if (!live)
115
104
  return [];
116
- const health = connectorVerdict(live, signals[config.id]);
105
+ const health = connectorVerdict(live, Object.hasOwn(signals, config.id) ? signals[config.id] : undefined);
117
106
  return [{ id: config.id, title: config.title ?? config.id, tone: health.tone, label: health.label }];
118
107
  });
119
108
  const healthy = bar.filter((entry) => entry.tone === "ok").length;
@@ -318,9 +318,8 @@ export declare function problemCopy(problem: UiProblem | undefined): string | nu
318
318
  * error. The two used to share the red box, so the row contradicted itself.
319
319
  */
320
320
  export declare function problemTone(problem: UiProblem): "warn" | "danger";
321
- /** What the page knows about a connector beyond its live status: catalog drift and a failed detail read. */
321
+ /** What the page knows about a connector beyond its live status: a failed detail read. */
322
322
  export interface ConnectorSignals {
323
- drift?: boolean;
324
323
  failed?: boolean;
325
324
  }
326
325
  /** One connector's health everywhere it is shown: badge tone, status word, and the one action beside it. */
@@ -328,12 +327,12 @@ export interface ConnectorVerdict {
328
327
  tone: "ok" | "warn" | "danger" | "neutral";
329
328
  label: string;
330
329
  /** The link to the Connection tab, or null when the connector is fine. */
331
- action: "Connect" | "Replace key" | "Inspect" | "Review" | null;
330
+ action: "Connect" | "Replace key" | "Inspect" | null;
332
331
  }
333
332
  /**
334
333
  * The verdict the Connectors list, the Overview, Endpoints and a connector's
335
334
  * page all show. The server's problem code comes first, then a failed detail
336
- * read on this page, then a non-ok status, then catalog drift. A problem
335
+ * read on this page, then a non-ok status. Catalog drift is informational. A problem
337
336
  * outranks the status, so a connector whose catalog failed is never
338
337
  * "Connected". A failed read marks its connector `connector_unavailable` in
339
338
  * the store, so that one code beside a failed read is the page's own and
@@ -425,7 +425,7 @@ function problemAction(problem) {
425
425
  /**
426
426
  * The verdict the Connectors list, the Overview, Endpoints and a connector's
427
427
  * page all show. The server's problem code comes first, then a failed detail
428
- * read on this page, then a non-ok status, then catalog drift. A problem
428
+ * read on this page, then a non-ok status. Catalog drift is informational. A problem
429
429
  * outranks the status, so a connector whose catalog failed is never
430
430
  * "Connected". A failed read marks its connector `connector_unavailable` in
431
431
  * the store, so that one code beside a failed read is the page's own and
@@ -443,8 +443,6 @@ export function connectorVerdict(live, signals = {}) {
443
443
  const tone = connectorStatusTone(live.status) === "warn" ? "warn" : "danger";
444
444
  return { tone, label: connectorStatusLabel(live.status), action: tone === "warn" ? "Connect" : "Inspect" };
445
445
  }
446
- if (signals.drift)
447
- return { tone: "warn", label: "Catalog drift", action: "Review" };
448
446
  return { tone: "ok", label: "Connected", action: null };
449
447
  }
450
448
  /**
@@ -469,7 +467,15 @@ function driftTotal(drift) {
469
467
  export function driftState(drift) {
470
468
  if (!drift)
471
469
  return "unavailable";
472
- return driftTotal(drift) > 0 ? "warning" : "clean";
470
+ if (driftTotal(drift) > 0)
471
+ return "warning";
472
+ if (drift.reviewedBaseline !== true ||
473
+ drift.verification !== "complete" ||
474
+ !drift.schemaCoverage ||
475
+ drift.schemaCoverage.unpinned > 0 ||
476
+ drift.schemaCoverage.pinned === 0)
477
+ return "unavailable";
478
+ return "clean";
473
479
  }
474
480
  /** Every category with its count, so a clean report still shows its zeros. */
475
481
  export function driftCounts(drift) {
@@ -483,16 +489,28 @@ export function driftCounts(drift) {
483
489
  }
484
490
  /** One line naming the state and when it was observed. Never what drifted. */
485
491
  export function driftSummary(drift) {
486
- const state = driftState(drift);
487
- if (state === "unavailable") {
492
+ if (!drift)
488
493
  return "No catalog refresh yet.";
489
- }
490
- const observed = formatDate(drift?.observedAt);
494
+ const observed = formatDate(drift.observedAt);
491
495
  const when = observed ? ` · observed ${observed}` : "";
492
- if (state === "clean")
493
- return `Matches the reviewed manifest${when}`;
494
496
  const total = driftTotal(drift);
495
- return `${total} difference${total === 1 ? "" : "s"} from the reviewed manifest${when}`;
497
+ const difference = total
498
+ ? `${total} difference${total === 1 ? "" : "s"}${drift.reviewedBaseline === false ? "" : " from the reviewed manifest"} · `
499
+ : "";
500
+ if (drift.reviewedBaseline === false)
501
+ return `${difference}No reviewed baseline${when}`;
502
+ if (drift.verification === "unavailable")
503
+ return `${difference}Schema verification unavailable${when}`;
504
+ const coverage = drift.schemaCoverage;
505
+ if (!coverage || coverage.pinned === 0)
506
+ return `${difference}Schemas not pinned${when}`;
507
+ if (coverage.unpinned > 0)
508
+ return `${difference}Partial schema coverage (${coverage.pinned} pinned, ${coverage.unpinned} not pinned)${when}`;
509
+ if (driftState(drift) === "clean")
510
+ return `Matches the reviewed manifest${when}`;
511
+ if (!total)
512
+ return `Schema verification unavailable${when}`;
513
+ return `${difference.replace(/ · $/, "")}${when}`;
496
514
  }
497
515
  /**
498
516
  * Only http/https may become a clickable href — the browser half of the gate
@@ -19,6 +19,8 @@ export interface ProviderOptions {
19
19
  authScope?: "shared" | "personal";
20
20
  /** Connector-specific inline result limit; omit to inherit the deployment. */
21
21
  maxResultBytes?: number;
22
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
23
+ readOnly?: boolean;
22
24
  /** Optional per-runtime downstream call-admission policy. */
23
25
  callAdmission?: ConnectorCallAdmissionPolicy;
24
26
  }
@@ -82,8 +84,11 @@ export declare const CALL_ADMISSION: Omit<Field<unknown, ConnectorCallAdmissionP
82
84
  parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
83
85
  readonly shape: typeof admissionPolicy.shape;
84
86
  };
87
+ /** Validate the common connector policy before a deployment can serve. */
88
+ export declare const READ_ONLY: Field<boolean, boolean | undefined, true>;
85
89
  /** Options every maintained provider shares. */
86
90
  export declare const PROVIDER_COMMON: {
91
+ readOnly: Field<boolean, boolean | undefined, true>;
87
92
  callAdmission: Omit<Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
88
93
  parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
89
94
  readonly shape: typeof admissionPolicy.shape;
@@ -129,7 +134,7 @@ export interface ProviderContext {
129
134
  * Common connector options the deployment set, and only those, ready to
130
135
  * spread into `remoteMcp()` or `api()` options.
131
136
  */
132
- readonly connectorOptions: Readonly<Pick<ProviderOptions, "authScope" | "maxResultBytes" | "callAdmission">>;
137
+ readonly connectorOptions: Readonly<Pick<ProviderOptions, "authScope" | "maxResultBytes" | "callAdmission" | "readOnly">>;
133
138
  /**
134
139
  * Render the maintained skill with this connection's context and the
135
140
  * deployment's instructions. Instructions are appended; they never replace
package/dist/provider.js CHANGED
@@ -50,9 +50,17 @@ export const CALL_ADMISSION = {
50
50
  return policy;
51
51
  },
52
52
  };
53
+ /** Validate the common connector policy before a deployment can serve. */
54
+ export const READ_ONLY = opaque({
55
+ check(value, path) {
56
+ if (typeof value !== "boolean")
57
+ throw new ConfigError(`${path} must be a boolean.`);
58
+ },
59
+ });
53
60
  /** Options every maintained provider shares. */
54
61
  export const PROVIDER_COMMON = {
55
62
  ...keys("title", "authScope", "purpose", "instructions", "maxResultBytes"),
63
+ readOnly: READ_ONLY,
56
64
  callAdmission: CALL_ADMISSION,
57
65
  };
58
66
  const PROVIDER_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
@@ -127,6 +135,7 @@ export function defineProvider(definition) {
127
135
  const connector = frozen.create(id, resolved, {
128
136
  ...(frozen.classify !== undefined ? { classify: frozen.classify } : {}),
129
137
  connectorOptions: {
138
+ ...(options.readOnly !== undefined ? { readOnly: options.readOnly } : {}),
130
139
  ...(options.authScope !== undefined ? { authScope: options.authScope } : {}),
131
140
  ...(options.maxResultBytes !== undefined ? { maxResultBytes: options.maxResultBytes } : {}),
132
141
  ...(options.callAdmission !== undefined ? { callAdmission: options.callAdmission } : {}),
@@ -178,6 +187,8 @@ function asProvider(provider, shape, id, options, build) {
178
187
  let connector;
179
188
  try {
180
189
  options = assertKnownOptions(options, `${factory}(${JSON.stringify(id)})`, shape);
190
+ if (options?.readOnly !== undefined)
191
+ READ_ONLY.parse(options.readOnly, `${factory}(${JSON.stringify(id)}).readOnly`);
181
192
  const admission = options == null ? undefined : options.callAdmission;
182
193
  if (admission !== undefined)
183
194
  CALL_ADMISSION.parse(admission, `${factory}(${JSON.stringify(id)}).callAdmission`);
@@ -188,12 +199,18 @@ function asProvider(provider, shape, id, options, build) {
188
199
  }
189
200
  const describe = connector.describe?.bind(connector);
190
201
  const ownDescribe = Object.getOwnPropertyDescriptor(connector, "describe");
202
+ const ownReadOnly = Object.getOwnPropertyDescriptor(connector, "readOnly");
191
203
  // Keep the receiver of prototype methods (including private fields). When
192
204
  // the description cannot be replaced, inherit the whole connector instead
193
205
  // of copying it: decorators may inherit their id, methods, and review.
194
- const stamped = Object.isExtensible(connector) && (!ownDescribe || ownDescribe.configurable)
206
+ const stamped = Object.isExtensible(connector) &&
207
+ (!ownDescribe || ownDescribe.configurable) &&
208
+ (options.readOnly === undefined || !ownReadOnly || ownReadOnly.configurable)
195
209
  ? connector
196
210
  : Object.create(connector);
211
+ if (options.readOnly !== undefined) {
212
+ Object.defineProperty(stamped, "readOnly", { value: options.readOnly, enumerable: true, configurable: true });
213
+ }
197
214
  Object.defineProperty(stamped, "describe", {
198
215
  configurable: true,
199
216
  enumerable: false,
@@ -100,6 +100,8 @@ export interface GoogleWorkspaceOptions {
100
100
  callAdmission?: ConnectorCallAdmissionPolicy;
101
101
  /** Connector-specific inline result limit; omit to inherit the deployment. */
102
102
  maxResultBytes?: number;
103
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
104
+ readOnly?: boolean;
103
105
  /** API base override for a proxy or test double. */
104
106
  baseUrl?: string;
105
107
  }
@@ -144,6 +146,7 @@ export declare const GOOGLE_WORKSPACE_OPTIONS: import("../../../config-schema.js
144
146
  };
145
147
  instructions: import("../../../config-schema.js").Field<unknown, unknown, true>;
146
148
  title: import("../../../config-schema.js").Field<unknown, unknown, true>;
149
+ readOnly: import("../../../config-schema.js").Field<unknown, unknown, true>;
147
150
  maxResultBytes: import("../../../config-schema.js").Field<unknown, unknown, true>;
148
151
  purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
149
152
  subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
@@ -61,7 +61,7 @@ import { tagWorkspaceSubject } from "../../../workspace-subject.js";
61
61
  import { delegatedToken, forgetDelegatedToken, parseServiceAccount, } from "./service-account.js";
62
62
  /** The closed options every Workspace provider accepts; see `assertKnownOptions`. */
63
63
  export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
64
- ...keys("purpose", "subject", "title", "instructions", "maxResultBytes", "baseUrl"),
64
+ ...keys("purpose", "subject", "title", "instructions", "maxResultBytes", "baseUrl", "readOnly"),
65
65
  // A string is the key's JSON; only the object form has keys to check.
66
66
  serviceAccount: optionsOf()(keys("clientEmail", "privateKey", "clientId")),
67
67
  callAdmission: CALL_ADMISSION,
@@ -27,6 +27,8 @@ export interface BasecampOptions {
27
27
  instructions?: string;
28
28
  /** Connector-specific inline result limit; omit to inherit the deployment. */
29
29
  maxResultBytes?: number;
30
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
31
+ readOnly?: boolean;
30
32
  /**
31
33
  * Optional per-runtime policy. There is no default: Basecamp's API documents
32
34
  * several dynamically adjusted limits (GET versus POST, per second, hour,
@@ -27,6 +27,8 @@ export interface BreezeOptions {
27
27
  callAdmission?: ConnectorCallAdmissionPolicy;
28
28
  /** Connector-specific inline result limit; omit to inherit the deployment. */
29
29
  maxResultBytes?: number;
30
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
31
+ readOnly?: boolean;
30
32
  /** Default `list_people` page size. Defaults to 100; connecta's cap is 1,000. */
31
33
  defaultPageSize?: number;
32
34
  }
@@ -98,6 +98,8 @@ export interface CcbOptions {
98
98
  callAdmission?: ConnectorCallAdmissionPolicy;
99
99
  /** Connector-specific inline result limit; omit to inherit the deployment. */
100
100
  maxResultBytes?: number;
101
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
102
+ readOnly?: boolean;
101
103
  }
102
104
  /** A maintained Church Community Builder (Pushpay ChMS) v2 connection. */
103
105
  export declare const ccb: import("../../provider.js").ProviderFactory<CcbOptions>;
@@ -1651,7 +1651,7 @@ function defaultTitle(environment, mode, writable) {
1651
1651
  }
1652
1652
  /** The closed options ccb() accepts; see `assertKnownOptions`. */
1653
1653
  const CCB_OPTIONS = optionsOf()({
1654
- ...keys("purpose", "title", "instructions", "environment", "mode", "clientId", "clientSecret", "access", "scopes", "subdomain", "defaultPerPage", "maxResultBytes"),
1654
+ ...keys("purpose", "title", "instructions", "environment", "mode", "clientId", "clientSecret", "access", "scopes", "subdomain", "defaultPerPage", "maxResultBytes", "readOnly"),
1655
1655
  callAdmission: CALL_ADMISSION,
1656
1656
  });
1657
1657
  /** A maintained Church Community Builder (Pushpay ChMS) v2 connection. */
@@ -15,6 +15,8 @@ interface CloudflareCommonOptions {
15
15
  instructions?: string;
16
16
  /** Connector-specific inline result limit; omit to inherit the deployment. */
17
17
  maxResultBytes?: number;
18
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
19
+ readOnly?: boolean;
18
20
  }
19
21
  /** Cloudflare's hosted MCP server (`search` and `execute`) over OAuth. */
20
22
  export interface CloudflareOAuthOptions extends CloudflareCommonOptions {
@@ -68,6 +68,10 @@ const GLOBAL_API_KEY_CREDENTIAL = {
68
68
  */
69
69
  const CLOUDFLARE_MCP_CLASSIFICATION = {
70
70
  tools: {
71
+ docs: {
72
+ verdict: "read",
73
+ reason: "Searches Cloudflare documentation without executing API methods.",
74
+ },
71
75
  search: {
72
76
  verdict: "read",
73
77
  reason: "Searches the Cloudflare OpenAPI contract without executing API methods.",
@@ -116,7 +120,7 @@ function keyUsageGuide(auth, options) {
116
120
 
117
121
  REST with ${scheme}. Connector purpose: ${options.purpose}${auth === "globalApiKey" ? global_warning.trimEnd() : ""}${pin}${defaultLine}${key}${shared}${key_tail}${instructionsSection(options.instructions)}`;
118
122
  }
119
- const COMMON = keys("title", "authScope", "purpose", "instructions", "maxResultBytes");
123
+ const COMMON = keys("title", "authScope", "purpose", "instructions", "maxResultBytes", "readOnly");
120
124
  const KEY_COMMON = keys("accountId", "zoneId", "baseUrl", "maxConcurrency");
121
125
  const PIN = optionsOf()(keys("accountIds", "zoneIds"));
122
126
  /** The closed options cloudflare() accepts, selected by `auth.type`; see `assertKnownOptions`. */