@zackbart/connecta 0.22.1 → 0.22.3

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 CHANGED
@@ -122,11 +122,12 @@ dead, and neither is a state to leave the repository in.
122
122
  - **CHANGELOG.** Each release opens with a narrative paragraph — what this
123
123
  release is, what breaks, what a deployment can ignore — then
124
124
  `### Added` / `### Changed` / `### Fixed`.
125
- - **Provider drift.** Before tagging a release, run `npm run drift:check` with
126
- local provider credentials exported. It diffs each hosted-MCP catalog against
127
- its vetted manifest and each hand-written provider's touched endpoints against
128
- the provider's published OpenAPI document. Findings are read by a human and
129
- become GitHub issues no credential goes near CI, and nothing files itself.
125
+ - **Provider drift.** Run `npm run providers:check` for the credential-free
126
+ public check across every maintained provider. It covers official MCP
127
+ documentation and OpenAPI contracts and never reads a provider credential.
128
+ Remote MCP schemas remain owned by the live `tools/list` response rather than
129
+ a vendored copy. Findings are read by a human and become GitHub issues;
130
+ nothing files itself.
130
131
  See
131
132
  [`documentation/provider-conventions.md`](./documentation/provider-conventions.md#the-maintainer-run-drift-check).
132
133
  - **Releases.** `npm run release:check`, tag `v<version>` matching
package/CHANGELOG.md CHANGED
@@ -2,6 +2,65 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.22.3 — 2026-09-03
6
+
7
+ This patch carries OAuth discovery across the browser redirect. It matters for
8
+ downstream MCP servers whose authentication challenge names a protected-resource
9
+ metadata URL outside the RFC 9728 default path, including Cloudflare Access
10
+ Managed OAuth. Existing grants and connectors using the default path require no
11
+ configuration or storage migration.
12
+
13
+ ### Fixed
14
+
15
+ - **Non-default OAuth discovery survives callbacks.** The KV OAuth provider now
16
+ persists the SDK's discovery state in the authorization generation, restores
17
+ it in the callback's fresh request scope, and deletes it through discovery,
18
+ all-credential, and generation cleanup. A callback therefore exchanges its
19
+ code against the same validated authorization-server issuer that registered
20
+ the client instead of invalidating that client after rediscovery (#523).
21
+
22
+ ## 0.22.2 — 2026-09-01
23
+
24
+ This patch makes the maintained-provider release check credential-free and
25
+ adds explicit official-MCP alternatives beside the existing Cloudflare,
26
+ Notion, and Vercel API connections. Existing declarations still select the
27
+ hand-written API interface by default. Linear, Mixpanel, RevenueCat, and Stripe
28
+ remain MCP-only, and deployments that do not select a new interface need no
29
+ configuration or credential change.
30
+
31
+ ### Added
32
+
33
+ - **Selectable API and MCP interfaces.** Cloudflare, Notion, and Vercel accept
34
+ `surface: "mcp"` for their official hosted server while preserving the API
35
+ default. Each MCP wrapper supplies release-reviewed safety classifications,
36
+ keeps unknown tools fail-closed, and leaves provider descriptions and input
37
+ and output schemas untouched.
38
+ - **Credential-free provider validation.** `npm run providers:check` compares
39
+ every maintained provider with official public evidence: touched OpenAPI
40
+ operations for the three hand-written interfaces, and endpoint, OAuth, and
41
+ documented tool inventories for the seven hosted interfaces. It reads no
42
+ provider credential and generates no runtime tool.
43
+
44
+ ### Changed
45
+
46
+ - **Public evidence replaces authenticated drift setup.** The lower-level
47
+ `drift:check` command now defaults to the same specification and documentation
48
+ checks; the credential helper and `--hosted` mode are removed. Live
49
+ `tools/list` responses remain authoritative for MCP schemas.
50
+ - **Hosted safety manifests refreshed.** Stripe and RevenueCat classifications
51
+ now cover their current official tool references. Vercel URL fetching is
52
+ approval-gated because an application GET route is not guaranteed to be
53
+ observational.
54
+
55
+ ### Fixed
56
+
57
+ - **Reviewed Vercel event drift.** Build-log projection accepts the expanded
58
+ deployment-event variants, including events without a published timestamp,
59
+ and the touched-endpoint digest records the reviewed contract (#520).
60
+ - **Schema ownership stays intact.** Deterministic coverage now proves the
61
+ safety wrapper preserves each provider description, input schema, and output
62
+ schema by reference instead of substituting a vendored definition (#521).
63
+
5
64
  ## 0.22.1 — 2026-08-31
6
65
 
7
66
  This patch adds a maintained Vercel connection over the public REST API. It is
package/README.md CHANGED
@@ -63,8 +63,10 @@ Fifty issues in, one small object out. Your context window notices.
63
63
  - **Wrap any HTTP API by hand.** A few lines per tool. No OpenAPI conversion —
64
64
  generated tool sprawl is the problem, not the fix.
65
65
  - **Use maintained connections** for Cloudflare, Linear, Mixpanel, Notion,
66
- RevenueCat, Stripe, and Vercel known endpoints, auth defaults, and vetted
67
- read/write classifications, imported one at a time.
66
+ RevenueCat, Stripe, and Vercel: known endpoints, auth defaults, and vetted
67
+ read/write classifications, imported one at a time. Cloudflare, Notion, and
68
+ Vercel each let the deployment choose their hand-written API interface or
69
+ official hosted MCP.
68
70
  - **Let the agent work in code.** Search, chain, filter, join, and reduce
69
71
  inside the sandbox instead of round-tripping every call through the model.
70
72
  - **Teach undeclared result shapes by using them.** Successful read-only calls
@@ -1,4 +1,4 @@
1
- import type { FetchLike, OAuthClientInformationContext, OAuthClientInformationMixed, OAuthClientMetadata, OAuthClientProvider, OAuthTokens } from "@modelcontextprotocol/client";
1
+ import type { FetchLike, OAuthClientInformationContext, OAuthClientInformationMixed, OAuthClientMetadata, OAuthClientProvider, OAuthDiscoveryState, OAuthTokens } from "@modelcontextprotocol/client";
2
2
  import type { KVStorage } from "../types.js";
3
3
  type OAuthRefreshFlightOutcome = {
4
4
  status: "refreshed";
@@ -134,6 +134,8 @@ export declare class KvOAuthProvider implements OAuthClientProvider {
134
134
  get clientMetadata(): OAuthClientMetadata;
135
135
  clientInformation(ctx?: OAuthClientInformationContext): Promise<OAuthClientInformationMixed | undefined>;
136
136
  saveClientInformation(info: OAuthClientInformationMixed, ctx?: OAuthClientInformationContext): Promise<void>;
137
+ discoveryState(): Promise<OAuthDiscoveryState | undefined>;
138
+ saveDiscoveryState(state: OAuthDiscoveryState): Promise<void>;
137
139
  tokens(ctx?: OAuthClientInformationContext): Promise<OAuthTokens | undefined>;
138
140
  saveTokens(tokens: OAuthTokens, ctx?: OAuthClientInformationContext): Promise<void>;
139
141
  /**
@@ -24,6 +24,7 @@ const OAUTH_VALUE_KEYS = [
24
24
  "oauth:pending",
25
25
  "oauth:verifier",
26
26
  "oauth:state",
27
+ "oauth:discovery",
27
28
  ];
28
29
  const MAX_CLEANUP_BACKLOG = 1_000;
29
30
  function isRefreshTokenRequest(init) {
@@ -621,6 +622,12 @@ export class KvOAuthProvider {
621
622
  async saveClientInformation(info, ctx) {
622
623
  await this.writeValue("oauth:client", info, (value) => JSON.stringify(value), ctx?.issuer);
623
624
  }
625
+ async discoveryState() {
626
+ return (await this.readValue("oauth:discovery", (raw) => JSON.parse(raw)))?.value;
627
+ }
628
+ async saveDiscoveryState(state) {
629
+ await this.writeValue("oauth:discovery", state, (value) => JSON.stringify(value));
630
+ }
624
631
  async tokens(ctx) {
625
632
  const refreshGeneration = ctx ? await this.flowGeneration() : undefined;
626
633
  const successIdentity = refreshGeneration !== undefined
@@ -800,6 +807,7 @@ export class KvOAuthProvider {
800
807
  oauthValueStorageKey("oauth:client", generation),
801
808
  oauthValueStorageKey("oauth:tokens", generation),
802
809
  oauthValueStorageKey("oauth:verifier", generation),
810
+ oauthValueStorageKey("oauth:discovery", generation),
803
811
  ]);
804
812
  }
805
813
  else if (scope === "client") {
@@ -811,6 +819,9 @@ export class KvOAuthProvider {
811
819
  else if (scope === "verifier") {
812
820
  await this.storage.delete(oauthValueStorageKey("oauth:verifier", generation));
813
821
  }
822
+ else if (scope === "discovery") {
823
+ await this.storage.delete(oauthValueStorageKey("oauth:discovery", generation));
824
+ }
814
825
  if (endsRefresh) {
815
826
  this.failRefreshFlight(new Error("OAuth refresh invalidated credentials before tokens were saved."));
816
827
  }
@@ -25,9 +25,9 @@ interface VettedToolRecord {
25
25
  * Digest of the input and output schemas that release read, or undefined
26
26
  * when no release has recorded them. Undefined is not "unchanged": a
27
27
  * manifest with no digest cannot report a schema change, and says so by
28
- * counting none. `npm run drift:check -- --record` reads them from a live
29
- * catalog and prints the block a release pastes in
30
- * ([#351](https://github.com/zackbart/connecta/issues/351)).
28
+ * counting none. The credential-free provider check does not create or
29
+ * update schema digests; the live `tools/list` response remains the schema
30
+ * agents receive ([#351](https://github.com/zackbart/connecta/issues/351)).
31
31
  */
32
32
  schemaDigest?: string;
33
33
  }
@@ -1,19 +1,30 @@
1
- import type { Connector, ConnectorCredentialConfig } from "../types.js";
1
+ import { type RemoteMcpAuth } from "../connectors/remote-mcp.js";
2
+ import type { Connector, ConnectorCallAdmissionPolicy, ConnectorCredentialConfig } from "../types.js";
2
3
  /** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
3
4
  export declare const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
5
+ /** Cloudflare's official whole-API hosted MCP endpoint. */
6
+ export declare const CLOUDFLARE_MCP_ENDPOINT = "https://mcp.cloudflare.com/mcp";
4
7
  /** Authentication schemes accepted by Cloudflare's v4 API. */
5
8
  export type CloudflareAuthentication = "apiToken" | "globalApiKey";
6
9
  /** See documentation/cloudflare.md#dns-record-types. */
7
10
  export declare const CLOUDFLARE_DNS_RECORD_TYPES: readonly ["A", "AAAA", "CAA", "CERT", "CNAME", "DNSKEY", "DS", "HTTPS", "LOC", "MX", "NAPTR", "NS", "OPENPGPKEY", "PTR", "SMIMEA", "SRV", "SSHFP", "SVCB", "TLSA", "TXT", "URI"];
8
11
  /** Content-valued types only; see documentation/cloudflare.md#dns-record-types. */
9
12
  export declare const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES: readonly ["A", "AAAA", "CNAME", "MX", "NS", "OPENPGPKEY", "PTR", "TXT"];
10
- export interface CloudflareOptions {
11
- /** Human-readable display name; defaults to "Cloudflare". */
13
+ interface CloudflareCommonOptions {
14
+ /** Human-readable display name; defaults identify the selected interface. */
12
15
  title?: string;
13
16
  /** Downstream auth ownership. Defaults to one shared deployment grant. */
14
17
  authScope?: "shared" | "personal";
15
18
  /** Which account/estate this connection administers, and for whom. */
16
19
  purpose: string;
20
+ /** Account-specific conventions appended to the maintained provider guide. */
21
+ instructions?: string;
22
+ /** Connector-specific inline result limit; omit to inherit the deployment. */
23
+ maxResultBytes?: number;
24
+ }
25
+ export interface CloudflareApiOptions extends CloudflareCommonOptions {
26
+ /** Omit for backward compatibility; the hand-written API interface is default. */
27
+ surface?: "api";
17
28
  /**
18
29
  * Default account id for account-scoped tools. When set, `accountId` becomes
19
30
  * an optional argument; when omitted, agents must pass one and can find it
@@ -32,12 +43,22 @@ export interface CloudflareOptions {
32
43
  authentication?: CloudflareAuthentication;
33
44
  /** Credential presentation override; credentials are always operator-managed. */
34
45
  credential?: ConnectorCredentialConfig;
35
- /** Account-specific conventions appended to the maintained provider guide. */
36
- instructions?: string;
37
- /** Connector-specific inline result limit; omit to inherit the deployment. */
38
- maxResultBytes?: number;
39
46
  /** Simultaneous downstream calls. Defaults to 6. */
40
47
  maxConcurrency?: number;
41
48
  }
42
- /** A maintained Cloudflare REST API connection. */
43
- export declare function cloudflare(id: string, options: CloudflareOptions): Connector;
49
+ export interface CloudflareMcpOptions extends CloudflareCommonOptions {
50
+ surface: "mcp";
51
+ /** OAuth by default, or a scoped API token for a headless deployment. */
52
+ auth?: RemoteMcpAuth;
53
+ /** Optional per-runtime downstream call-admission policy. */
54
+ callAdmission?: ConnectorCallAdmissionPolicy;
55
+ }
56
+ /** Backward-compatible API options; existing consumers may extend this interface. */
57
+ export interface CloudflareOptions extends CloudflareApiOptions {
58
+ }
59
+ /** Select one Cloudflare interface when deployment configuration constructs it. */
60
+ export type CloudflareConnectionOptions = CloudflareOptions | CloudflareMcpOptions;
61
+ export declare const CLOUDFLARE_MCP_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
62
+ /** A maintained Cloudflare connection using the selected provider interface. */
63
+ export declare function cloudflare(id: string, options: CloudflareConnectionOptions): Connector;
64
+ export {};
@@ -1,9 +1,13 @@
1
1
  /** See documentation/cloudflare.md#no-sdk-on-purpose. */
2
- import { api } from "../connectors/api.js";
2
+ import { api, defined } from "../connectors/api.js";
3
+ import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
4
+ import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
3
5
  import { guardedFetch, retryAfterMs, } from "../connectors/guarded-fetch.js";
4
6
  import { ConnectorCallError } from "../errors.js";
5
7
  /** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
6
8
  export const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
9
+ /** Cloudflare's official whole-API hosted MCP endpoint. */
10
+ export const CLOUDFLARE_MCP_ENDPOINT = "https://mcp.cloudflare.com/mcp";
7
11
  /** See documentation/cloudflare.md#dns-record-types. */
8
12
  export const CLOUDFLARE_DNS_RECORD_TYPES = [
9
13
  "A",
@@ -2381,7 +2385,7 @@ function buildTools(scope, authentication) {
2381
2385
  ];
2382
2386
  return tools;
2383
2387
  }
2384
- function usageGuide(purpose, scope, instructions, authentication) {
2388
+ function apiUsageGuide(purpose, scope, instructions, authentication) {
2385
2389
  const accountInstructions = instructions?.trim();
2386
2390
  const zoneLine = scope.zoneId
2387
2391
  ? `This connector defaults to zone \`${scope.zoneId}\`; omit \`zoneId\` unless the request names a different domain.`
@@ -2411,12 +2415,64 @@ ${accountInstructions
2411
2415
  ? `\n## Account instructions\n\n${accountInstructions}\n`
2412
2416
  : ""}`;
2413
2417
  }
2414
- /** A maintained Cloudflare REST API connection. */
2415
- export function cloudflare(id, options) {
2416
- const purpose = options.purpose.trim();
2417
- if (!purpose) {
2418
- throw new Error("cloudflare() requires a non-empty account purpose.");
2419
- }
2418
+ export const CLOUDFLARE_MCP_VETTED_CATALOG = vettedCatalog({
2419
+ reads: new Set(["search"]),
2420
+ // `execute` can send any method to more than 2,500 API endpoints. Its input
2421
+ // schema cannot prove a particular program is observational, so it stays on
2422
+ // the approval path even when that program happens to issue only GETs.
2423
+ writes: new Map([["execute", "destructive"]]),
2424
+ });
2425
+ function mcpUsageGuide(purpose, instructions) {
2426
+ const accountInstructions = instructions?.trim();
2427
+ return `# Cloudflare MCP usage
2428
+
2429
+ Official whole-API MCP interface: ${purpose}
2430
+
2431
+ - The catalog contains \`search\` and \`execute\`. Search runs code against
2432
+ Cloudflare's OpenAPI document. Execute runs code that may call any authorized
2433
+ Cloudflare API endpoint.
2434
+ - Use \`search\` to find the exact method, path, and fields before writing an
2435
+ execute program. Do not guess an endpoint from product naming.
2436
+ - Connecta routes every \`execute\` call through approval because the tool can
2437
+ mix GET, POST, PUT, PATCH, and DELETE requests inside one program. The MCP
2438
+ schema cannot establish that arbitrary code is read-only.
2439
+ - Keep returned values small. Filter and project inside the Cloudflare MCP
2440
+ program, then reduce again inside Connecta's \`execute_code\` when several
2441
+ calls must be joined.
2442
+ - OAuth and API-token permissions remain the provider-side boundary. An
2443
+ \`auth_required\` failure needs authorization or a token with the required
2444
+ Cloudflare permission.
2445
+ ${accountInstructions
2446
+ ? `\n## Account instructions\n\n${accountInstructions}\n`
2447
+ : ""}`;
2448
+ }
2449
+ function cloudflareMcp(id, purpose, options) {
2450
+ const connector = remoteMcp(id, {
2451
+ url: CLOUDFLARE_MCP_ENDPOINT,
2452
+ ...defined({
2453
+ authScope: options.authScope,
2454
+ callAdmission: options.callAdmission,
2455
+ maxResultBytes: options.maxResultBytes,
2456
+ }),
2457
+ title: options.title ?? "Cloudflare (MCP)",
2458
+ description: `Cloudflare's official whole-API MCP interface: ${purpose}`,
2459
+ auth: withCredentialDefaults(options.auth ?? { type: "oauth" }, {
2460
+ credential: {
2461
+ label: "Cloudflare API token",
2462
+ description: "A scoped Cloudflare API token. Connecta sends it as a bearer token to mcp.cloudflare.com and stores it encrypted.",
2463
+ placeholder: "Paste Cloudflare API token",
2464
+ },
2465
+ }),
2466
+ requireHttps: true,
2467
+ usageGuide: {
2468
+ content: mcpUsageGuide(purpose, options.instructions),
2469
+ summary: "Official whole-API MCP. Search the OpenAPI document, then approve each mixed-method execute program.",
2470
+ required: true,
2471
+ },
2472
+ });
2473
+ return withVettedCatalog(connector, CLOUDFLARE_MCP_VETTED_CATALOG);
2474
+ }
2475
+ function cloudflareApi(id, purpose, options) {
2420
2476
  const maxConcurrency = options.maxConcurrency ?? 6;
2421
2477
  if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
2422
2478
  throw new Error("cloudflare() maxConcurrency must be a positive integer.");
@@ -2437,7 +2493,7 @@ export function cloudflare(id, options) {
2437
2493
  credential: credentialConfig(authentication, options.credential),
2438
2494
  callAdmission: admissionPolicy(maxConcurrency),
2439
2495
  usageGuide: {
2440
- content: usageGuide(purpose, scope, options.instructions, authentication),
2496
+ content: apiUsageGuide(purpose, scope, options.instructions, authentication),
2441
2497
  // Explicit rather than derived: the first content line is the zone
2442
2498
  // scoping rule, which varies per deployment and reads as an instruction
2443
2499
  // rather than as the routing fact a browsing agent needs.
@@ -2485,3 +2541,13 @@ export function cloudflare(id, options) {
2485
2541
  }),
2486
2542
  });
2487
2543
  }
2544
+ /** A maintained Cloudflare connection using the selected provider interface. */
2545
+ export function cloudflare(id, options) {
2546
+ const purpose = options.purpose.trim();
2547
+ if (!purpose) {
2548
+ throw new Error("cloudflare() requires a non-empty account purpose.");
2549
+ }
2550
+ return options.surface === "mcp"
2551
+ ? cloudflareMcp(id, purpose, options)
2552
+ : cloudflareApi(id, purpose, options);
2553
+ }
@@ -1,10 +1,12 @@
1
- import type { Connector } from "../types.js";
1
+ import type { Connector, ConnectorCallAdmissionPolicy } 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
+ /** Notion's official hosted MCP endpoint. */
5
+ export declare const NOTION_MCP_ENDPOINT = "https://mcp.notion.com/mcp";
4
6
  /** See documentation/notion.md#the-pinned-api-version. */
5
7
  export declare const NOTION_API_VERSION = "2026-03-11";
6
- export interface NotionOptions {
7
- /** Human-readable display name; defaults to "Notion". */
8
+ interface NotionCommonOptions {
9
+ /** Human-readable display name; defaults identify the selected interface. */
8
10
  title?: string;
9
11
  /** Downstream auth ownership. Defaults to one shared deployment grant. */
10
12
  authScope?: "shared" | "personal";
@@ -12,6 +14,13 @@ export interface NotionOptions {
12
14
  purpose: string;
13
15
  /** Workspace-specific conventions appended to the maintained provider guide. */
14
16
  instructions?: string;
17
+ /** Connector-specific inline result limit; omit to inherit the deployment. */
18
+ maxResultBytes?: number;
19
+ }
20
+ /** Connecta's maintained hand-written Notion REST interface. */
21
+ export interface NotionApiOptions extends NotionCommonOptions {
22
+ /** Omit for backward compatibility; the hand-written API interface is default. */
23
+ surface?: "api";
15
24
  /** Operator-facing label for the integration token. */
16
25
  credentialLabel?: string;
17
26
  /**
@@ -19,8 +28,20 @@ export interface NotionOptions {
19
28
  * Defaults to 25; Notion's maximum is 100.
20
29
  */
21
30
  defaultPageSize?: number;
22
- /** Connector-specific inline result limit; omit to inherit the deployment. */
23
- maxResultBytes?: number;
24
31
  }
25
- /** A maintained Notion connection over the public REST API. */
26
- export declare function notion(id: string, options: NotionOptions): Connector;
32
+ /** Notion's official hosted MCP interface, authenticated through OAuth. */
33
+ export interface NotionMcpOptions extends NotionCommonOptions {
34
+ surface: "mcp";
35
+ /** Optional per-runtime downstream call-admission policy. */
36
+ callAdmission?: ConnectorCallAdmissionPolicy;
37
+ }
38
+ /** Backward-compatible API options; existing consumers may extend this interface. */
39
+ export interface NotionOptions extends NotionApiOptions {
40
+ }
41
+ /** Select one Notion interface when deployment configuration constructs it. */
42
+ export type NotionConnectionOptions = NotionOptions | NotionMcpOptions;
43
+ /** Release-reviewed Notion MCP inventory and safety verdicts. */
44
+ export declare const NOTION_MCP_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
45
+ /** A maintained Notion connection using the selected provider interface. */
46
+ export declare function notion(id: string, options: NotionConnectionOptions): Connector;
47
+ export {};
@@ -1,8 +1,12 @@
1
- import { api } from "../connectors/api.js";
1
+ import { api, defined } from "../connectors/api.js";
2
+ import { remoteMcp } from "../connectors/remote-mcp.js";
3
+ import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
2
4
  import { guardedFetch, retryAfterMs, } from "../connectors/guarded-fetch.js";
3
5
  import { ConnectorCallError } from "../errors.js";
4
6
  /** Notion's REST origin. Every tool below speaks to exactly this host. */
5
7
  export const NOTION_API_BASE_URL = "https://api.notion.com";
8
+ /** Notion's official hosted MCP endpoint. */
9
+ export const NOTION_MCP_ENDPOINT = "https://mcp.notion.com/mcp";
6
10
  /** See documentation/notion.md#the-pinned-api-version. */
7
11
  export const NOTION_API_VERSION = "2026-03-11";
8
12
  /** Notion's hard cap on `page_size` for every paginated endpoint. */
@@ -1393,7 +1397,7 @@ function buildTools(defaultPageSize) {
1393
1397
  // Guide and constructor
1394
1398
  // ---------------------------------------------------------------------------
1395
1399
  /** See documentation/notion.md#databases-contain-data-sources. */
1396
- function usageGuide(purpose, instructions) {
1400
+ function apiUsageGuide(purpose, instructions) {
1397
1401
  const accountInstructions = instructions?.trim();
1398
1402
  return `# Notion usage
1399
1403
 
@@ -1490,12 +1494,95 @@ ${accountInstructions
1490
1494
  ? `\n## Workspace instructions\n\n${accountInstructions}\n`
1491
1495
  : ""}`;
1492
1496
  }
1493
- /** A maintained Notion connection over the public REST API. */
1494
- export function notion(id, options) {
1495
- const purpose = options.purpose.trim();
1496
- if (!purpose) {
1497
- throw new Error("notion() requires a non-empty workspace purpose.");
1498
- }
1497
+ /** Release-reviewed Notion MCP inventory and safety verdicts. */
1498
+ export const NOTION_MCP_VETTED_CATALOG = vettedCatalog({
1499
+ reads: new Set([
1500
+ "notion-search",
1501
+ "notion-search-skills",
1502
+ "notion-fetch",
1503
+ "notion-download-attachment",
1504
+ "notion-query-data-sources",
1505
+ "notion-query-meeting-notes",
1506
+ "notion-search-agents",
1507
+ "notion-list-agents",
1508
+ "notion-query-sessions",
1509
+ "notion-search-sessions",
1510
+ "notion-get-session-status",
1511
+ "notion-wait-session",
1512
+ "notion-list-session-events",
1513
+ "notion-read-session-event",
1514
+ "notion-get-comments",
1515
+ "notion-get-teams",
1516
+ "notion-get-users",
1517
+ "notion-get-async-task",
1518
+ ]),
1519
+ writes: new Map([
1520
+ ["notion-create-file-upload", "additive"],
1521
+ ["notion-create-attachment", "additive"],
1522
+ ["notion-create-pages", "additive"],
1523
+ ["notion-duplicate-page", "additive"],
1524
+ ["notion-create-database", "additive"],
1525
+ ["notion-create-folder", "additive"],
1526
+ ["notion-create-view", "additive"],
1527
+ ["notion-spawn-session", "additive"],
1528
+ ["notion-send-message-to-session", "additive"],
1529
+ ["notion-create-comment", "additive"],
1530
+ ["notion-update-page", "destructive"],
1531
+ ["notion-convert-page-to-skill", "destructive"],
1532
+ ["notion-move-pages", "destructive"],
1533
+ ["notion-update-data-source", "destructive"],
1534
+ ["notion-update-view", "destructive"],
1535
+ ["notion-stop-session", "destructive"],
1536
+ ]),
1537
+ });
1538
+ function mcpUsageGuide(purpose, instructions) {
1539
+ const accountInstructions = instructions?.trim();
1540
+ return `# Notion MCP usage
1541
+
1542
+ Official MCP interface: tool names, descriptions, argument schemas, and result
1543
+ schemas come from Notion's live server. Connecta preserves that catalog and
1544
+ only fills in release-reviewed safety annotations when Notion leaves them out.
1545
+
1546
+ Workspace purpose: ${purpose}
1547
+
1548
+ - Discover the live catalog before assuming a tool exists. Notion can gate
1549
+ tools by workspace, account, client, and rollout independently of Connecta.
1550
+ - Start broad discovery with \`notion-search\`, then use \`notion-fetch\` on
1551
+ the exact page, database, data source, or object before changing it.
1552
+ - Use the live input schema as the contract. Notion owns these MCP schemas;
1553
+ the REST schemas in Connecta's API interface do not apply to MCP tools with
1554
+ similar names.
1555
+ - Session and agent tools can launch asynchronous work. Read session state and
1556
+ events before sending another message, waiting, or stopping a session.
1557
+ - File uploads and attachment tools create durable workspace state. Keep
1558
+ downloads inside the requested task and do not expose signed attachment URLs.
1559
+ - An \`auth_required\` failure means this connector's OAuth grant is missing or
1560
+ expired. Run \`authorize_connector\` for this connector id, then retry.
1561
+ ${accountInstructions
1562
+ ? `\n## Workspace instructions\n\n${accountInstructions}\n`
1563
+ : ""}`;
1564
+ }
1565
+ function notionMcp(id, purpose, options) {
1566
+ const connector = remoteMcp(id, {
1567
+ url: NOTION_MCP_ENDPOINT,
1568
+ ...defined({
1569
+ authScope: options.authScope,
1570
+ callAdmission: options.callAdmission,
1571
+ maxResultBytes: options.maxResultBytes,
1572
+ }),
1573
+ title: options.title ?? "Notion (MCP)",
1574
+ description: `Notion's official hosted MCP interface: ${purpose}`,
1575
+ auth: { type: "oauth" },
1576
+ requireHttps: true,
1577
+ usageGuide: {
1578
+ content: mcpUsageGuide(purpose, options.instructions),
1579
+ summary: "Official MCP. Live Notion schemas, object discovery, sessions, agents, attachments, and OAuth ownership.",
1580
+ required: true,
1581
+ },
1582
+ });
1583
+ return withVettedCatalog(connector, NOTION_MCP_VETTED_CATALOG);
1584
+ }
1585
+ function notionApi(id, purpose, options) {
1499
1586
  const defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_SIZE;
1500
1587
  if (!Number.isInteger(defaultPageSize) ||
1501
1588
  defaultPageSize < 1 ||
@@ -1530,7 +1617,7 @@ export function notion(id, options) {
1530
1617
  },
1531
1618
  callAdmission: NOTION_ADMISSION,
1532
1619
  usageGuide: {
1533
- content: usageGuide(purpose, options.instructions),
1620
+ content: apiUsageGuide(purpose, options.instructions),
1534
1621
  summary: "Database-to-data-source lookup, property write rules, lean-vs-raw results, and Notion's overloaded 403/404.",
1535
1622
  required: true,
1536
1623
  },
@@ -1540,3 +1627,13 @@ export function notion(id, options) {
1540
1627
  : {}),
1541
1628
  });
1542
1629
  }
1630
+ /** A maintained Notion connection using the selected provider interface. */
1631
+ export function notion(id, options) {
1632
+ const purpose = options.purpose.trim();
1633
+ if (!purpose) {
1634
+ throw new Error("notion() requires a non-empty workspace purpose.");
1635
+ }
1636
+ return options.surface === "mcp"
1637
+ ? notionMcp(id, purpose, options)
1638
+ : notionApi(id, purpose, options);
1639
+ }
@@ -108,9 +108,19 @@ const WRITE_TOOLS = new Map([
108
108
  ["create-audience", "additive"],
109
109
  ["update-audience", "destructive"],
110
110
  // Paywalls
111
+ ["attach-offering-to-paywall", "destructive"],
112
+ ["detach-offering-from-paywall", "destructive"],
111
113
  ["duplicate-paywall", "additive"],
112
114
  ["publish-paywall", "destructive"],
113
115
  ["unpublish-paywall", "destructive"],
116
+ // Offerings and experiments added to the published reference after #512.
117
+ ["duplicate-offering", "additive"],
118
+ ["create-experiment", "additive"],
119
+ ["pause-experiment", "destructive"],
120
+ ["resume-experiment", "destructive"],
121
+ ["start-experiment", "destructive"],
122
+ ["stop-experiment", "destructive"],
123
+ ["update-experiment", "destructive"],
114
124
  // Customers and subscriptions
115
125
  ["assign-customer-offering", "destructive"],
116
126
  ["grant-customer-entitlement", "destructive"],
@@ -37,7 +37,12 @@ const READ_ONLY_TOOLS = new Set([
37
37
  "stripe_api_search",
38
38
  "stripe_api_details",
39
39
  "stripe_api_read",
40
+ "get_stripe_account_info",
40
41
  "get_balance_summary",
42
+ "list_metrics",
43
+ "explain_metric",
44
+ "metric_drilldown",
45
+ "show_metric_app",
41
46
  "list_available_accounts_or_orgs",
42
47
  "manage_stripe_accounts",
43
48
  "search_stripe_documentation",
@@ -45,8 +50,10 @@ const READ_ONLY_TOOLS = new Set([
45
50
  /** Reviewed writes, including mixed read/create tools: `documentation/stripe.md`. */
46
51
  const WRITE_TOOLS = new Map([
47
52
  ["stripe_api_write", "destructive"],
53
+ ["create_refund", "destructive"],
48
54
  ["stripe_implementation_planner", "additive"],
49
55
  ["stripe_analytics", "additive"],
56
+ ["stripe_report", "additive"],
50
57
  ["send_stripe_mcp_feedback", "additive"],
51
58
  ]);
52
59
  /** Release-reviewed manifest; see provider conventions P5 and P13. */