@zackbart/connecta 0.31.0 → 0.32.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 +34 -0
- package/README.md +15 -13
- package/bin/connecta.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/auth-elicitation.js +1 -1
- package/dist/catalog-service.js +7 -2
- package/dist/config-value-sources.js +6 -3
- package/dist/config.d.ts +26 -8
- package/dist/config.js +38 -16
- package/dist/connector-access.d.ts +2 -1
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/describe-config.d.ts +6 -3
- package/dist/describe-config.js +3 -2
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.js +4 -3
- package/dist/invocation.js +7 -4
- package/dist/meta-tools.js +17 -19
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -0
- package/dist/operator-ui/contract.d.ts +5 -4
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -1
- package/dist/operator-ui/view.js +1 -0
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +2 -3
- package/dist/providers/_shared/google/workspace.js +1 -1
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +274 -23
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.js +16 -0
- package/dist/providers/notion/rest.js +138 -19
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +286 -24
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +1 -1
- package/dist/providers/sheets/index.js +3 -3
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/routes/mcp.js +30 -10
- package/dist/routes/operator.js +14 -7
- package/dist/routes/ui-config.js +2 -2
- package/dist/server.js +3 -0
- package/dist/skills.js +5 -4
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +38 -23
- package/documentation/auth.md +86 -27
- package/documentation/code-mode.md +81 -72
- package/documentation/deploying.md +10 -5
- package/documentation/integrating.md +56 -4
- package/documentation/meta-tools.md +44 -37
- package/documentation/operating.md +11 -8
- package/documentation/operator-tests.md +2 -3
- package/documentation/operator-ui.md +9 -3
- package/documentation/provider-auth-migration.md +5 -4
- package/documentation/provider-migration-0.29.md +1 -1
- package/examples/worker/AGENTS.md +5 -0
- package/examples/worker/src/connecta.config.ts +5 -0
- package/package.json +5 -1
- package/templates/node/AGENTS.md +6 -4
- package/templates/node/README.md +4 -2
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +5 -0
- package/dist/tool-safety.d.ts +0 -9
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "
|
|
3
|
+
"content": "\n\n- This connection uses Connecta's REST tools with a service account. Hosted MCP is the default `mixpanel()` mode; `via: \"api\"` explicitly selects REST. Declare two connection ids when both are useful. The operator pastes `username:secret` into the API connection, which sends ordinary HTTP Basic, unlike hosted MCP's `Bearer Basic` scheme. OAuth and static headers are refused in API mode.\n- One connection stays in one residency region, across all projects the account can reach. US uses `mixpanel.com` for query/app, `data.mixpanel.com` for export and `api.mixpanel.com` for ingestion. EU uses `eu.mixpanel.com`, `data-eu.mixpanel.com` and `api-eu.mixpanel.com`. India uses `in.mixpanel.com`, `data-in.mixpanel.com` and `api-in.mixpanel.com`. Wrong residency can look like missing data; do not interpret an empty response without checking the project region.\n- Start with `list_projects`, which returns redacted account metadata from `/api/app/me`. Its public reference does not specify the response schema. Resolve ids from that metadata or project settings, never guess one. Every project-scoped tool accepts `project_id`; an omitted id uses `defaultProjectId` when configured. An explicit id overrides that default and still requires service-account membership. Projects with Data Views may also require `workspace_id`.\n- Use REST for raw event export, profile queries and individual event sequences. Use hosted MCP for schema discovery, business context, aggregate analytics and dashboard work. Discover exact event and property names there before filtering raw data.\n- Export has no documented cursor. Begin with a short date range and an event or `where` filter. Every export returns `truncated` and a notice. A true flag means events may be missing at the byte or event cap; narrow ranges or filters, rather than inferring \"no later event B\" from a sample. Dates are inclusive. Mixpanel documents UTC for newer projects and the project timezone for older projects; check project settings around the January 2023 transition. Event timestamps default to seconds, or milliseconds with `time_in_ms`.\n- `get_profile_activity` sorts a user's returned events by timestamp, but `/stream/query` is in maintenance mode and has no documented pagination/completeness guarantee. For absence or sequence claims use complete, filtered raw export and sort/reduce inside `execute_code`.\n- `query_profiles` is a read-only POST to the Query API's `/engage`, distinct from ingestion `/engage` profile mutations. Pagination is vendor-owned: there is no documented caller `page_size`. Keep the same selector and output properties, pass the returned `session_id`, and increment `page` while `page.hasMore` is true. Do not treat `total` as the page length. `raw: true` preserves the redacted vendor envelope when needed.\n- Saved Insights uses `bookmark_id` from a report URL. Saved Funnels uses `funnel_id`, discoverable with `mixpanel_api_read` on `/api/query/funnels/list`. The documented Retention endpoint takes events and date parameters, not a saved-report id; default/birth retention also requires `born_event`. Use `query_retention` or hosted MCP for an existing saved Retention report. Reduce nested report results before returning them.\n- Retention `interval` is capped at 90 only for `unit: \"day\"`, including the default when unit is omitted. Week and month intervals have no documented 90-unit cap. Both named and generic retention reads enforce the day limit.\n- `mixpanel_api_search` and `mixpanel_api_details` inspect the reviewed operation subset locally. `mixpanel_api_read` permits GET only on those operations, with fixed residency hosts and no caller URL or headers. It cannot reach service-account administration, arbitrary app APIs, profile mutation, JQL or ingestion. Generic export retains the named export's byte/event bounds. Use named tools for Engage and import.\n- `import_events` is an additive write and requires an Owner or Admin service account on the target project. Connecta enforces 2000 events, 10MB per batch, less than 1MB per event and Mixpanel's documented property limits. Supply stable `$insert_id` values and use strict validation. A 400 can still ingest the valid events; never retry a whole batch automatically. Inspect the target and failed records through a trusted operator workflow before deciding what to send again.\n- Profile writes are deferred. The official ingestion `/engage` contract requires `$token` in each update; service-account Basic alone does not satisfy it, and the service-account reference documents no project-token retrieval operation. Profile delete and other irreversible mutations are absent.\n- Query permits 60 queries per hour and 5 concurrent queries per project. Export permits 60 per hour, 3 per second and 100 concurrent queries. Ingestion permits 2GB uncompressed JSON per minute or about 30000 events per second. The API connection conservatively shares a 60-call/hour, 5-concurrent default across all tools and projects; it is a per-runtime approximation, not a distributed quota or a per-second export limiter. Operators may override `callAdmission`, especially for import-heavy deployments or multiple runtimes sharing an account. A vendor 429 still wins; respect `Retry-After`.\n- Successful responses recursively redact credential-named properties and bearer URLs. Arbitrary analytics properties remain user data; do not put secrets into free-text event names or property values. HTTP failures expose status and recovery guidance rather than vendor text that could echo a credential.\n",
|
|
4
4
|
"instructionsHeading": "Account instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"guide_0": " endpoint. A project created in another residency is not reachable from here at all, so an empty result may mean wrong connector rather than no data.",
|
|
7
|
-
"guide_1": "\n\n- Start with `List-Organizations`, then supply the returned organization id where the live `Get-Projects` schema requires it. Resolve the project and workspace, then read `Get-Business-Context` before interpreting events or metrics.\n- Resolve ids before acting; never guess one. `Get-Projects` yields the project id every other call is scoped by, and `List-Dashboards`, `List-Cohorts`, `List-Metrics`, `List-Experiments`, and `List-Feature-Flags` yield the ids their `Get-`, `Update-`, and `Delete-` counterparts expect.\n- Discover names with `Get-Events`, `List-Properties`, and `Get-Property-Values`; do not guess event or property spelling.\n- `Get-Business-Context` requires either `project_id` or `organization_id`. Its schema marks both optional, but the hosted tool rejects a call with neither.\n- `Get-Property-Values` requires `properties` or the deprecated `property` alias. Event property values also require `event`; prefer `properties` and never send both property forms with conflicting values.\n- `List-Properties` accepts `names` or `query`, never both. Use exact `names` for known properties and `query` for substring discovery.\n- One analysis is one `execute_code` program: fetch `Get-Query-Schema` once, run every `Run-Query` of the analysis in that program, and return the reduced table. The schema is tens of kilobytes and the same for every report type, so re-fetching it per query buys nothing. Never return raw `Run-Query` output.\n- Insights, funnels, and retention answer aggregate questions. A per-user ordered event timeline, or a sequence question such as \"event A with no later event B\", is not answerable with `Run-Query` in a reasonable number of calls, and this hosted catalog has no per-`distinct_id` event timeline — `Get-User-Replays-Data` covers one user's replays with their events only where session replay is enabled and present. If the deployment exposes a Mixpanel export or activity-feed connector, use that; if it does not, tell the user the question is out of reach here rather than approximating it with hourly buckets and hundreds of empty rows.\n- `false` on a boolean property may be an absent property: Mixpanel renders a missing value as `false` in boolean breakdowns, and server-imported events often lack client-side properties entirely. Confirm the property is present with `List-Properties` or `Get-Property-Values` before treating `false` as a signal, and say when a conclusion rests on that ambiguity.\n- Breakdown responses nest `$overall` and per-segment series objects. Flatten to one row per complete breakdown combination inside `execute_code` before returning, and drop `$overall` unless the question asks for the total.\n- Use `Get-Report` when the request names an existing saved report. Use `Run-Query` for a new question.\n- Dashboard row and cell ids are opaque. Read the full layout before editing and preserve grouping and order. Report cells need a project-local `query_id`; duplicate within the same project when possible. Cross-project reconstruction needs new queries in the target project.\n- Query ids can expire. Build the dashboard promptly and rerun a query if its id is rejected as expired; no fixed TTL is guaranteed.\n- This account's tool list is not a fixed set. Mixpanel gates parts of its MCP catalog by plan and beta enrollment — experiments, feature flags, session replay, and issue triage are the usual absentees — so search this connector for what it actually exposes rather than assuming a documented tool is here.\n- Mixpanel meters MCP traffic per user per hour, shared with everything else that credential does. Reuse discovery results within a run and avoid speculative fan-out.\n- An `auth_required` failure means this connector's Mixpanel authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. A rejected argument or a plan restriction comes back in Mixpanel's own words instead — read it rather than re-authorizing.\n- Treat every create, update, edit, merge, dismiss, duplicate, or delete operation as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n"
|
|
7
|
+
"guide_1": "\n\n- Start with `List-Organizations`, then supply the returned organization id where the live `Get-Projects` schema requires it. Resolve the project and workspace, then read `Get-Business-Context` before interpreting events or metrics.\n- Resolve ids before acting; never guess one. `Get-Projects` yields the project id every other call is scoped by, and `List-Dashboards`, `List-Cohorts`, `List-Metrics`, `List-Experiments`, and `List-Feature-Flags` yield the ids their `Get-`, `Update-`, and `Delete-` counterparts expect.\n- Discover names with `Get-Events`, `List-Properties`, and `Get-Property-Values`; do not guess event or property spelling.\n- `Get-Business-Context` requires either `project_id` or `organization_id`. Its schema marks both optional, but the hosted tool rejects a call with neither.\n- `Get-Property-Values` requires `properties` or the deprecated `property` alias. Event property values also require `event`; prefer `properties` and never send both property forms with conflicting values.\n- `List-Properties` accepts `names` or `query`, never both. Use exact `names` for known properties and `query` for substring discovery.\n- One analysis is one `execute_code` program: fetch `Get-Query-Schema` once, run every `Run-Query` of the analysis in that program, and return the reduced table. The schema is tens of kilobytes and the same for every report type, so re-fetching it per query buys nothing. Never return raw `Run-Query` output.\n- Insights, funnels, and retention answer aggregate questions. A per-user ordered event timeline, or a sequence question such as \"event A with no later event B\", is not answerable with `Run-Query` in a reasonable number of calls, and this hosted catalog has no per-`distinct_id` event timeline — `Get-User-Replays-Data` covers one user's replays with their events only where session replay is enabled and present. If the deployment exposes a Mixpanel export or activity-feed connector, use that; if it does not, tell the user the question is out of reach here rather than approximating it with hourly buckets and hundreds of empty rows.\n- `false` on a boolean property may be an absent property: Mixpanel renders a missing value as `false` in boolean breakdowns, and server-imported events often lack client-side properties entirely. Confirm the property is present with `List-Properties` or `Get-Property-Values` before treating `false` as a signal, and say when a conclusion rests on that ambiguity.\n- Breakdown responses nest `$overall` and per-segment series objects. Flatten to one row per complete breakdown combination inside `execute_code` before returning, and drop `$overall` unless the question asks for the total.\n- Use `Get-Report` when the request names an existing saved report. Use `Run-Query` for a new question.\n- Dashboard row and cell ids are opaque. Read the full layout before editing and preserve grouping and order. Report cells need a project-local `query_id`; duplicate within the same project when possible. Cross-project reconstruction needs new queries in the target project.\n- Query ids can expire. Build the dashboard promptly and rerun a query if its id is rejected as expired; no fixed TTL is guaranteed.\n- This account's tool list is not a fixed set. Mixpanel gates parts of its MCP catalog by plan and beta enrollment — experiments, feature flags, session replay, and issue triage are the usual absentees — so search this connector for what it actually exposes rather than assuming a documented tool is here.\n- Mixpanel meters MCP traffic per user per hour, shared with everything else that credential does. Reuse discovery results within a run and avoid speculative fan-out.\n- An `auth_required` failure means this connector's Mixpanel authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. A rejected argument or a plan restriction comes back in Mixpanel's own words instead — read it rather than re-authorizing.\n- Treat every create, update, edit, merge, dismiss, duplicate, or delete operation as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
|
|
8
|
+
"api": "\n\n- This connection uses Connecta's REST tools with a service account. Hosted MCP is the default `mixpanel()` mode; `via: \"api\"` explicitly selects REST. Declare two connection ids when both are useful. The operator pastes `username:secret` into the API connection, which sends ordinary HTTP Basic, unlike hosted MCP's `Bearer Basic` scheme. OAuth and static headers are refused in API mode.\n- One connection stays in one residency region, across all projects the account can reach. US uses `mixpanel.com` for query/app, `data.mixpanel.com` for export and `api.mixpanel.com` for ingestion. EU uses `eu.mixpanel.com`, `data-eu.mixpanel.com` and `api-eu.mixpanel.com`. India uses `in.mixpanel.com`, `data-in.mixpanel.com` and `api-in.mixpanel.com`. Wrong residency can look like missing data; do not interpret an empty response without checking the project region.\n- Start with `list_projects`, which returns redacted account metadata from `/api/app/me`. Its public reference does not specify the response schema. Resolve ids from that metadata or project settings, never guess one. Every project-scoped tool accepts `project_id`; an omitted id uses `defaultProjectId` when configured. An explicit id overrides that default and still requires service-account membership. Projects with Data Views may also require `workspace_id`.\n- Use REST for raw event export, profile queries and individual event sequences. Use hosted MCP for schema discovery, business context, aggregate analytics and dashboard work. Discover exact event and property names there before filtering raw data.\n- Export has no documented cursor. Begin with a short date range and an event or `where` filter. Every export returns `truncated` and a notice. A true flag means events may be missing at the byte or event cap; narrow ranges or filters, rather than inferring \"no later event B\" from a sample. Dates are inclusive. Mixpanel documents UTC for newer projects and the project timezone for older projects; check project settings around the January 2023 transition. Event timestamps default to seconds, or milliseconds with `time_in_ms`.\n- `get_profile_activity` sorts a user's returned events by timestamp, but `/stream/query` is in maintenance mode and has no documented pagination/completeness guarantee. For absence or sequence claims use complete, filtered raw export and sort/reduce inside `execute_code`.\n- `query_profiles` is a read-only POST to the Query API's `/engage`, distinct from ingestion `/engage` profile mutations. Pagination is vendor-owned: there is no documented caller `page_size`. Keep the same selector and output properties, pass the returned `session_id`, and increment `page` while `page.hasMore` is true. Do not treat `total` as the page length. `raw: true` preserves the redacted vendor envelope when needed.\n- Saved Insights uses `bookmark_id` from a report URL. Saved Funnels uses `funnel_id`, discoverable with `mixpanel_api_read` on `/api/query/funnels/list`. The documented Retention endpoint takes events and date parameters, not a saved-report id; default/birth retention also requires `born_event`. Use `query_retention` or hosted MCP for an existing saved Retention report. Reduce nested report results before returning them.\n- Retention `interval` is capped at 90 only for `unit: \"day\"`, including the default when unit is omitted. Week and month intervals have no documented 90-unit cap. Both named and generic retention reads enforce the day limit.\n- `mixpanel_api_search` and `mixpanel_api_details` inspect the reviewed operation subset locally. `mixpanel_api_read` permits GET only on those operations, with fixed residency hosts and no caller URL or headers. It cannot reach service-account administration, arbitrary app APIs, profile mutation, JQL or ingestion. Generic export retains the named export's byte/event bounds. Use named tools for Engage and import.\n- `import_events` is an additive write and requires an Owner or Admin service account on the target project. Connecta enforces 2000 events, 10MB per batch, less than 1MB per event and Mixpanel's documented property limits. Supply stable `$insert_id` values and use strict validation. A 400 can still ingest the valid events; never retry a whole batch automatically. Inspect the target and failed records through a trusted operator workflow before deciding what to send again.\n- Profile writes are deferred. The official ingestion `/engage` contract requires `$token` in each update; service-account Basic alone does not satisfy it, and the service-account reference documents no project-token retrieval operation. Profile delete and other irreversible mutations are absent.\n- Query permits 60 queries per hour and 5 concurrent queries per project. Export permits 60 per hour, 3 per second and 100 concurrent queries. Ingestion permits 2GB uncompressed JSON per minute or about 30000 events per second. The API connection conservatively shares a 60-call/hour, 5-concurrent default across all tools and projects; it is a per-runtime approximation, not a distributed quota or a per-second export limiter. Operators may override `callAdmission`, especially for import-heavy deployments or multiple runtimes sharing an account. A vendor 429 still wins; respect `Retry-After`.\n- Successful responses recursively redact credential-named properties and bearer URLs. Arbitrary analytics properties remain user data; do not put secrets into free-text event names or property values. HTTP failures expose status and recovery guidance rather than vendor text that could echo a credential.\n"
|
|
8
9
|
}
|
|
9
10
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { safe } from "../_shared/rest/value-safety.js";
|
|
2
|
+
/** Reviewed 2026-10-14 against the official schemas and service-account guide. */
|
|
3
|
+
export const MIXPANEL_VALUE_SAFETY = {
|
|
4
|
+
title: "Mixpanel",
|
|
5
|
+
operations: {
|
|
6
|
+
"GET /api/app/me": safe("Account metadata has no documented response schema; redact credential-named fields recursively, including project tokens. No service-account administration endpoints are admitted."),
|
|
7
|
+
"GET /api/2.0/export": safe("JSONL event names and properties; arbitrary property bags pass through recursive credential and bearer-URL redaction."),
|
|
8
|
+
"GET /api/query/stream/query": safe("Activity event names and properties; arbitrary property bags pass through recursive credential and bearer-URL redaction."),
|
|
9
|
+
"POST /api/query/engage": safe("Existing profile ids and properties only; arbitrary property bags pass through recursive credential and bearer-URL redaction."),
|
|
10
|
+
"GET /api/query/insights": safe("Report headers name breakdown dimensions, not HTTP headers; numeric time series contain no authentication values.", ["headers"]),
|
|
11
|
+
"GET /api/query/funnels": safe("Funnel steps and conversion counts, no authentication values."),
|
|
12
|
+
"GET /api/query/funnels/list": safe("Saved funnel names and ids, no authentication values."),
|
|
13
|
+
"GET /api/query/retention": safe("Cohort dates and retention counts, no authentication values."),
|
|
14
|
+
"POST /import": safe("Import status and counts only. Vendor error text is withheld because it may quote submitted properties."),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -59,6 +59,22 @@ function oauthUsageGuide(purpose, instructions) {
|
|
|
59
59
|
const NOTION_MCP_CLASSIFICATION = {
|
|
60
60
|
tools: {
|
|
61
61
|
"notion-search": { "verdict": "read", "reason": "Retrieves Notion information without changing vendor state." },
|
|
62
|
+
"notion-ai-search": {
|
|
63
|
+
"verdict": "read",
|
|
64
|
+
"reason": "Legacy AI search, still callable by name; retrieves matching content without changing vendor state.",
|
|
65
|
+
},
|
|
66
|
+
"notion-get-self": {
|
|
67
|
+
"verdict": "read",
|
|
68
|
+
"reason": "Returns the connected workspace and user without changing vendor state.",
|
|
69
|
+
},
|
|
70
|
+
"notion-get-tool-access": {
|
|
71
|
+
"verdict": "read",
|
|
72
|
+
"reason": "Reads which tools the workspace plan allows; it grants nothing and changes nothing.",
|
|
73
|
+
},
|
|
74
|
+
"notion-download-skill": {
|
|
75
|
+
"verdict": "read",
|
|
76
|
+
"reason": "Returns an existing Skill as a signed, expiring archive URL; it changes no workspace state.",
|
|
77
|
+
},
|
|
62
78
|
"notion-search-skills": {
|
|
63
79
|
"verdict": "read",
|
|
64
80
|
"reason": "Retrieves Notion skills information without changing vendor state.",
|
|
@@ -23,6 +23,9 @@ export const NOTION_API_VERSION = "2026-03-11";
|
|
|
23
23
|
export const MAX_PAGE_SIZE = 100;
|
|
24
24
|
/** Notion's cap on `children` per append, and on blocks per page create. */
|
|
25
25
|
const MAX_CHILDREN_PER_REQUEST = 100;
|
|
26
|
+
/** Notion's cap on one rich-text run's `text.content`, and on runs in one rich-text array. */
|
|
27
|
+
const MAX_TEXT_CONTENT = 2_000;
|
|
28
|
+
const MAX_RICH_TEXT_RUNS = 100;
|
|
26
29
|
/**
|
|
27
30
|
* Ceiling on the fetches one `integration_get_page_content` walk may spend. Call admission
|
|
28
31
|
* meters tool calls, not the requests inside them, so a deep `depth` would
|
|
@@ -61,21 +64,75 @@ function isRecord(value) {
|
|
|
61
64
|
function invalid(message) {
|
|
62
65
|
throw new ConnectorCallError("invalid_args", message);
|
|
63
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Notion repeats `Retry-After` in the body as `additional_data.retry_after`,
|
|
69
|
+
* a string of whole seconds, so a proxy that drops the header still leaves a
|
|
70
|
+
* window to wait out.
|
|
71
|
+
*/
|
|
72
|
+
function bodyRetryAfterMs(additional) {
|
|
73
|
+
const raw = isRecord(additional) ? additional["retry_after"] : undefined;
|
|
74
|
+
const seconds = typeof raw === "string" || typeof raw === "number" ? Number(raw) : Number.NaN;
|
|
75
|
+
return Number.isFinite(seconds) && seconds >= 0 ? Math.trunc(seconds * 1000) : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** A Notion object id, dashed or not: the only committed-id shape shown when error text is withheld. */
|
|
78
|
+
const NOTION_ID = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
|
|
79
|
+
/**
|
|
80
|
+
* The ids a 503 reports as already committed, so a caller reads them instead
|
|
81
|
+
* of repeating the write. For an operation whose error text is withheld, only
|
|
82
|
+
* ids shaped as Notion ids are named, since anything else could echo what the
|
|
83
|
+
* request sent; the write is still reported as committed.
|
|
84
|
+
*/
|
|
85
|
+
function committedIds(additional, withheld) {
|
|
86
|
+
if (!isRecord(additional))
|
|
87
|
+
return "";
|
|
88
|
+
const resource = additional["committed_resource_id"];
|
|
89
|
+
const children = Array.isArray(additional["committed_child_ids"])
|
|
90
|
+
? additional["committed_child_ids"].filter((child) => typeof child === "string")
|
|
91
|
+
: [];
|
|
92
|
+
if (typeof resource !== "string" || resource === "")
|
|
93
|
+
return "";
|
|
94
|
+
if (withheld && ![resource, ...children].every((committed) => NOTION_ID.test(committed))) {
|
|
95
|
+
return " Notion committed the write before failing; what it committed is withheld because this operation handles credentials. Read the object instead of repeating the write.";
|
|
96
|
+
}
|
|
97
|
+
return children.length
|
|
98
|
+
? ` Notion committed the write before failing: it appended ${children.length} block(s) (${children.join(", ")}) under ${resource}. Read that block instead of repeating the write.`
|
|
99
|
+
: ` Notion committed the write before failing: the object it created or changed is ${resource}. Read it instead of repeating the write.`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Notion's own next step for a failed write, `additional_data.retry_guidance`.
|
|
103
|
+
* An update to an existing object that was saved anyway names no committed
|
|
104
|
+
* id, only this, so it is relayed as Notion wrote it.
|
|
105
|
+
*/
|
|
106
|
+
function retryGuidance(additional) {
|
|
107
|
+
const guidance = isRecord(additional) ? additional["retry_guidance"] : undefined;
|
|
108
|
+
const lines = (Array.isArray(guidance) ? guidance : [guidance]).filter((line) => typeof line === "string" && line.trim() !== "");
|
|
109
|
+
return lines.map((line) => line.trim()).join(" ");
|
|
110
|
+
}
|
|
64
111
|
/**
|
|
65
112
|
* Map to what the caller should do next, not to what Notion's `code` says
|
|
66
113
|
* happened. The two that are easy to mistranslate:
|
|
67
114
|
*
|
|
68
115
|
* - **403 is not an authentication failure.** The token is fine; the integration
|
|
69
|
-
* lacks a capability
|
|
70
|
-
* `authorize_connector` nor a new token can fix
|
|
116
|
+
* lacks a capability, was never shared the object, or the workspace is past
|
|
117
|
+
* its block limit, and neither `authorize_connector` nor a new token can fix
|
|
118
|
+
* it. Notion's message says which. Non-retryable call failure.
|
|
71
119
|
* - **404 does not prove absence.** Notion returns `object_not_found` both for an
|
|
72
120
|
* object that does not exist and for one never shared with the integration, and
|
|
73
121
|
* will not say which — so not `not_found`, which exists to assert absence
|
|
74
122
|
* (H11). The message names both possibilities.
|
|
123
|
+
*
|
|
124
|
+
* A 503 on a write may still have been saved. When Notion names what it
|
|
125
|
+
* committed, or gives its own retry guidance, the failure says so and stops
|
|
126
|
+
* being retryable; otherwise the shared path reports a write's outcome as
|
|
127
|
+
* unknown (INV-9).
|
|
128
|
+
*
|
|
129
|
+
* `withheld` replaces every free-text field (message, retry guidance,
|
|
130
|
+
* committed ids) for an operation whose error text the review withholds.
|
|
75
131
|
*/
|
|
76
132
|
function notionFailure(status, headers, payload, withheld = false) {
|
|
77
133
|
const body = isRecord(payload) ? payload : undefined;
|
|
78
134
|
const code = typeof body?.["code"] === "string" ? body["code"] : undefined;
|
|
135
|
+
const additional = body?.["additional_data"];
|
|
79
136
|
// For an operation the value-safety review withholds, Notion's message is
|
|
80
137
|
// replaced; its code still routes.
|
|
81
138
|
const detail = withheld
|
|
@@ -83,12 +140,17 @@ function notionFailure(status, headers, payload, withheld = false) {
|
|
|
83
140
|
: typeof body?.["message"] === "string" && body["message"].trim()
|
|
84
141
|
? body["message"].trim()
|
|
85
142
|
: `Notion returned HTTP ${status}.`;
|
|
86
|
-
const retryAfter = retryAfterMs(headers);
|
|
143
|
+
const retryAfter = retryAfterMs(headers) ?? bodyRetryAfterMs(additional);
|
|
87
144
|
const labelled = code ? `Notion ${code}: ${detail}` : detail;
|
|
88
145
|
if (status === 429) {
|
|
89
|
-
const additional = body?.["additional_data"];
|
|
90
146
|
const reason = isRecord(additional) ? additional["rate_limit_reason"] : undefined;
|
|
91
|
-
|
|
147
|
+
if (reason === "public_api_request_blocked") {
|
|
148
|
+
return new ConnectorCallError("connector_call_failed", `${labelled} (limit: ${reason}) Notion has restricted this integration's API access. Retrying will not help; an operator must contact Notion support.`, { retryable: false });
|
|
149
|
+
}
|
|
150
|
+
const scope = reason === "public_api_space_request_rate_limit"
|
|
151
|
+
? " The workspace's shared budget across all its connections is spent, and its window can exceed a minute."
|
|
152
|
+
: " Notion allows each integration 180 requests a minute (600 on Business and Enterprise), and a separate budget shared by the whole workspace.";
|
|
153
|
+
return new ConnectorCallError("rate_limited", `${labelled}${typeof reason === "string" ? ` (limit: ${reason})` : ""}${scope} Wait out the retry window.`, { retryAfterMs: retryAfter ?? 1_000 });
|
|
92
154
|
}
|
|
93
155
|
if (status === 529) {
|
|
94
156
|
// Notion documents 529 alongside 429: back off and respect Retry-After.
|
|
@@ -100,7 +162,7 @@ function notionFailure(status, headers, payload, withheld = false) {
|
|
|
100
162
|
return new ConnectorCallError("auth_required", `${labelled} The Notion integration token is missing or invalid. Call authorize_connector for recovery options. When available, an operator can set a valid token in this connection in the operator UI.`);
|
|
101
163
|
}
|
|
102
164
|
if (status === 403) {
|
|
103
|
-
return new ConnectorCallError("provider_permission_denied", `${labelled} The token is valid but
|
|
165
|
+
return new ConnectorCallError("provider_permission_denied", `${labelled} The token is valid but Notion refused this operation, and Notion's message says why. Usually an operator must enable the matching capability on the Notion integration (comment capabilities are off by default) or share the object with it; a workspace past its plan's block limit also answers 403. Re-authorizing will not help.`, { retryable: false });
|
|
104
166
|
}
|
|
105
167
|
if (status === 404) {
|
|
106
168
|
return new ConnectorCallError("connector_call_failed", `${labelled} Notion returns this both for an object that does not exist and for one that exists but has not been shared with this integration — do not treat it as proof of deletion. Confirm the id, then confirm the page or database is shared with the integration in Notion.`, { retryable: false });
|
|
@@ -117,6 +179,16 @@ function notionFailure(status, headers, payload, withheld = false) {
|
|
|
117
179
|
});
|
|
118
180
|
}
|
|
119
181
|
if (status >= 500) {
|
|
182
|
+
const committed = committedIds(additional, withheld);
|
|
183
|
+
if (committed) {
|
|
184
|
+
return new ConnectorCallError("connector_call_failed", `${labelled}${committed}`, { retryable: false });
|
|
185
|
+
}
|
|
186
|
+
const guidance = retryGuidance(additional);
|
|
187
|
+
if (guidance) {
|
|
188
|
+
return new ConnectorCallError("connector_call_failed", withheld
|
|
189
|
+
? `${labelled} Notion sent retry guidance, withheld because this operation handles credentials. Read the object before repeating the write.`
|
|
190
|
+
: `${labelled} Notion's guidance: ${guidance}`, { retryable: false });
|
|
191
|
+
}
|
|
120
192
|
return new ConnectorCallError("unavailable", `${labelled} Notion is failing upstream.`, retryAfter !== undefined ? { retryAfterMs: retryAfter } : {});
|
|
121
193
|
}
|
|
122
194
|
return new ConnectorCallError("connector_call_failed", labelled, { retryable: false });
|
|
@@ -131,6 +203,9 @@ function notionFailure(status, headers, payload, withheld = false) {
|
|
|
131
203
|
* (until `expires_at`), so the page names that path.
|
|
132
204
|
* - `POST /v1/blocks/meeting_notes/query` has no cursor at all: `has_more`
|
|
133
205
|
* past its `limit` (50 at most) can only be answered by a narrower filter.
|
|
206
|
+
* - A data source or view query stops at 10,000 results with `has_more: false`
|
|
207
|
+
* and `request_status.type: "incomplete"`. That is a cut-off with no cursor,
|
|
208
|
+
* reported like the meeting-note one: `hasMore` without `next`.
|
|
134
209
|
*
|
|
135
210
|
* Every shape keeps `hasMore`, so a `select` that drops the body's own
|
|
136
211
|
* completeness fields cannot hide that a list was cut short.
|
|
@@ -138,6 +213,8 @@ function notionFailure(status, headers, payload, withheld = false) {
|
|
|
138
213
|
function page(data, call) {
|
|
139
214
|
if (!isRecord(data) || typeof data["has_more"] !== "boolean" || !Array.isArray(data["results"]))
|
|
140
215
|
return undefined;
|
|
216
|
+
if (!data["has_more"] && incomplete(data))
|
|
217
|
+
return { hasMore: true };
|
|
141
218
|
const hasMore = data["has_more"];
|
|
142
219
|
const next = typeof data["next_cursor"] === "string" && data["next_cursor"] !== "" ? data["next_cursor"] : undefined;
|
|
143
220
|
if (!hasMore || !next)
|
|
@@ -153,6 +230,11 @@ function page(data, call) {
|
|
|
153
230
|
}
|
|
154
231
|
return { hasMore, next, param: "start_cursor", ...(call.method === "GET" ? {} : { in: "body" }) };
|
|
155
232
|
}
|
|
233
|
+
/** Whether a query stopped at Notion's per-query result cap rather than at its last row. */
|
|
234
|
+
function incomplete(data) {
|
|
235
|
+
const status = data["request_status"];
|
|
236
|
+
return isRecord(status) && status["type"] === "incomplete";
|
|
237
|
+
}
|
|
156
238
|
/** The pinned index, shared by every Notion REST connector in the deployment. */
|
|
157
239
|
let index;
|
|
158
240
|
let transport;
|
|
@@ -226,9 +308,26 @@ function plainText(value) {
|
|
|
226
308
|
return "";
|
|
227
309
|
return value.map((run) => (typeof run?.plain_text === "string" ? run.plain_text : "")).join("");
|
|
228
310
|
}
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Wrap a plain string as the rich-text array Notion expects. Notion rejects a
|
|
313
|
+
* run over 2,000 characters, so a longer string becomes consecutive runs, cut
|
|
314
|
+
* between UTF-16 surrogate pairs, never inside one. Past 100 runs Notion
|
|
315
|
+
* rejects the array itself, so that refuses here with the way forward.
|
|
316
|
+
*/
|
|
317
|
+
function richText(value, field) {
|
|
318
|
+
const runs = [];
|
|
319
|
+
let rest = value;
|
|
320
|
+
while (rest.length > MAX_TEXT_CONTENT) {
|
|
321
|
+
const high = rest.charCodeAt(MAX_TEXT_CONTENT - 1);
|
|
322
|
+
const cut = high >= 0xd800 && high <= 0xdbff ? MAX_TEXT_CONTENT - 1 : MAX_TEXT_CONTENT;
|
|
323
|
+
runs.push(rest.slice(0, cut));
|
|
324
|
+
rest = rest.slice(cut);
|
|
325
|
+
}
|
|
326
|
+
runs.push(rest);
|
|
327
|
+
if (runs.length > MAX_RICH_TEXT_RUNS) {
|
|
328
|
+
invalid(`${field} is ${value.length} characters; Notion accepts at most ${MAX_RICH_TEXT_RUNS} runs of ${MAX_TEXT_CONTENT} characters in one rich-text value. Split it across several blocks.`);
|
|
329
|
+
}
|
|
330
|
+
return runs.map((content) => ({ type: "text", text: { content } }));
|
|
232
331
|
}
|
|
233
332
|
function userRef(value) {
|
|
234
333
|
if (!value || typeof value !== "object")
|
|
@@ -290,10 +389,11 @@ function projectPropertyValue(value) {
|
|
|
290
389
|
.map((related) => related?.id ?? null)
|
|
291
390
|
.filter((related) => typeof related === "string");
|
|
292
391
|
case "formula":
|
|
293
|
-
|
|
392
|
+
// Notion answers `unsupported` for a value it cannot compute through the API.
|
|
393
|
+
return value.formula?.type === "unsupported" ? null : (value.formula?.[value.formula?.type] ?? null);
|
|
294
394
|
case "rollup": {
|
|
295
395
|
const rollup = value.rollup;
|
|
296
|
-
if (!rollup)
|
|
396
|
+
if (!rollup || rollup.type === "unsupported")
|
|
297
397
|
return null;
|
|
298
398
|
if (rollup.type === "array") {
|
|
299
399
|
return (rollup.array ?? []).map(projectPropertyValue);
|
|
@@ -343,11 +443,20 @@ function parentRef(parent) {
|
|
|
343
443
|
return null;
|
|
344
444
|
return { type, id: typeof parent[type] === "string" ? parent[type] : null };
|
|
345
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* An icon as one string: the emoji itself, a native Notion icon's name, a
|
|
448
|
+
* workspace custom emoji as `:name:`, or an image URL.
|
|
449
|
+
*/
|
|
346
450
|
function iconRef(icon) {
|
|
347
451
|
if (!icon || typeof icon !== "object")
|
|
348
452
|
return null;
|
|
349
453
|
if (typeof icon.emoji === "string")
|
|
350
454
|
return icon.emoji;
|
|
455
|
+
if (icon.type === "icon" && typeof icon.icon?.name === "string")
|
|
456
|
+
return icon.icon.name;
|
|
457
|
+
if (icon.type === "custom_emoji" && typeof icon.custom_emoji?.name === "string") {
|
|
458
|
+
return `:${icon.custom_emoji.name}:`;
|
|
459
|
+
}
|
|
351
460
|
return icon.external?.url ?? icon.file?.url ?? null;
|
|
352
461
|
}
|
|
353
462
|
function projectPage(page, select) {
|
|
@@ -537,7 +646,7 @@ const PAGE_OUTPUT_SCHEMA = {
|
|
|
537
646
|
properties: { type: { type: "string" }, id: { type: ["string", "null"] } },
|
|
538
647
|
required: ["type", "id"],
|
|
539
648
|
},
|
|
540
|
-
icon: { type: ["string", "null"], description: "Emoji or icon URL." },
|
|
649
|
+
icon: { type: ["string", "null"], description: "Emoji, native icon name, :custom_emoji:, or icon URL." },
|
|
541
650
|
created_time: { type: ["string", "null"] },
|
|
542
651
|
last_edited_time: { type: ["string", "null"] },
|
|
543
652
|
created_by: { type: ["object", "null"] },
|
|
@@ -875,7 +984,16 @@ function namedTools(defaultPageSize) {
|
|
|
875
984
|
required: ["data_source_id"],
|
|
876
985
|
additionalProperties: false,
|
|
877
986
|
},
|
|
878
|
-
outputSchema:
|
|
987
|
+
outputSchema: {
|
|
988
|
+
...listOutputSchema(PAGE_OUTPUT_SCHEMA),
|
|
989
|
+
properties: {
|
|
990
|
+
...listOutputSchema(PAGE_OUTPUT_SCHEMA)["properties"],
|
|
991
|
+
incomplete: {
|
|
992
|
+
type: "boolean",
|
|
993
|
+
description: "Present when Notion stopped this query at its 10,000-result cap: has_more is false but rows are missing. Narrow the filter.",
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
879
997
|
handler: async (args, ctx) => {
|
|
880
998
|
const body = pagination(args, defaultPageSize);
|
|
881
999
|
if (args.filter)
|
|
@@ -885,7 +1003,8 @@ function namedTools(defaultPageSize) {
|
|
|
885
1003
|
const payload = await notion(ctx, "POST", `/v1/data_sources/${id(args.data_source_id)}/query`, { body });
|
|
886
1004
|
if (args.raw)
|
|
887
1005
|
return payload;
|
|
888
|
-
|
|
1006
|
+
const rows = mappedListEnvelope(payload, (row) => projectPage(row, args.properties));
|
|
1007
|
+
return incomplete(payload) ? { ...rows, incomplete: true } : rows;
|
|
889
1008
|
},
|
|
890
1009
|
},
|
|
891
1010
|
{
|
|
@@ -944,7 +1063,7 @@ function namedTools(defaultPageSize) {
|
|
|
944
1063
|
}
|
|
945
1064
|
const properties = { ...args.properties };
|
|
946
1065
|
if (args.title !== undefined)
|
|
947
|
-
properties[args.title_property ?? "title"] = { title: richText(args.title) };
|
|
1066
|
+
properties[args.title_property ?? "title"] = { title: richText(args.title, "title") };
|
|
948
1067
|
const body = {
|
|
949
1068
|
parent: parentKey === "parent_page_id"
|
|
950
1069
|
? { type: "page_id", page_id: parentValue }
|
|
@@ -1014,16 +1133,16 @@ function namedTools(defaultPageSize) {
|
|
|
1014
1133
|
["children", args.children],
|
|
1015
1134
|
], "Use text for plain paragraphs, checklist for to-do items, or children for raw Notion blocks.");
|
|
1016
1135
|
const children = kind === "text"
|
|
1017
|
-
? value.map((line) => ({
|
|
1136
|
+
? value.map((line, at) => ({
|
|
1018
1137
|
object: "block",
|
|
1019
1138
|
type: "paragraph",
|
|
1020
|
-
paragraph: { rich_text: richText(line) },
|
|
1139
|
+
paragraph: { rich_text: richText(line, `text[${at}]`) },
|
|
1021
1140
|
}))
|
|
1022
1141
|
: kind === "checklist"
|
|
1023
|
-
? value.map((line) => ({
|
|
1142
|
+
? value.map((line, at) => ({
|
|
1024
1143
|
object: "block",
|
|
1025
1144
|
type: "to_do",
|
|
1026
|
-
to_do: { rich_text: richText(line), checked: false },
|
|
1145
|
+
to_do: { rich_text: richText(line, `checklist[${at}]`), checked: false },
|
|
1027
1146
|
}))
|
|
1028
1147
|
: value;
|
|
1029
1148
|
if (children.length === 0)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "\n## Notion's data model\n\n- A database is a container; its rows and schema live in a *data source*\n inside it. Queries, schema reads, and new rows take the data source, and\n Notion rejects a database id in its place.\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read the schema before composing one.\n-
|
|
3
|
+
"content": "\n## Notion's data model\n\n- A database is a container; its rows and schema live in a *data source*\n inside it. Queries, schema reads, and new rows take the data source, and\n Notion rejects a database id in its place.\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read the schema before composing one.\n- A `status` write must use an existing option name; inventing one fails.\n A new `select` or `multi_select` name does not fail: when the connection\n can write to the data source, Notion adds it to the schema as a new\n option. Use the options the schema lists unless a schema change is meant.\n- Writes **replace** a property. Sending one item to a `multi_select` or\n `relation` drops the others, so send the complete intended value.\n- `rollup`, `formula`, `unique_id`, and the created and edited fields are\n computed. They cannot be written.\n- A data source's title column is rarely called \"title\". Read its name from\n the schema before creating or renaming a row.\n- Notion has **no idempotency key**. A retried page create or comment makes a\n duplicate. Confirm with a search before repeating a write that may have\n partially succeeded.\n",
|
|
4
4
|
"instructionsHeading": "Workspace instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"oauth_head": "# Notion MCP usage\n\nOfficial MCP interface: tool names, descriptions, argument schemas, and result\nschemas come from Notion's live server. Connecta preserves that catalog and\napplies release-reviewed classification. Reviewed writes stay writes even when Notion claims they only read.\n\nWorkspace purpose: ",
|
|
7
|
-
"oauth": "\n\n- This connection acts as its OAuth-authorized user and reaches what that user\n can. It holds no integration token. An internal integration (bot) is a\n separate connector with `auth: { type: \"token\" }` under another id; never\n switch to it to recover a failed call here.\n- Discover the live catalog before assuming a tool exists. Notion can gate\n tools by workspace, account, client, and rollout independently of Connecta.\n- Start broad discovery with `notion-search`, then use `notion-fetch` on\n the exact page, database, data source, or object before changing it.\n- Use the live input schema as the contract. Notion owns these MCP schemas;\n the REST schemas of Connecta's token connector do not apply to MCP tools with\n similar names.\n- Session and agent tools can launch asynchronous work. Read session state and\n events before sending another message, waiting, or stopping a session.\n- File uploads and attachment tools create durable workspace state. Keep\n downloads inside the requested task and do not expose signed attachment URLs.\n- Connecta's generic REST tools (`notion_api_*`) and the `integration_*`\n helpers exist only on a token connector.\n- An `auth_required` failure means this connector's OAuth grant is missing or\n expired. Run `authorize_connector` for this connector id, then retry.\n",
|
|
8
|
-
"key": "\n## Identity and tools\n\nThis is Connecta's REST connector to `api.notion.com`, acting as one internal\nintegration (a bot) with an operator-managed integration token. It reaches\nexactly the pages and data sources shared with that integration, and the\nintegration's capabilities decide which calls succeed: comment and\nuser-information capabilities are off by default. It never acts as a person.\nHosted Notion MCP is a separate OAuth connector.\n\n- Named tools cover the common path and flatten Notion's payloads:\n `integration_search` (titles), `integration_get_page` (properties),\n `integration_get_page_content` (blocks as text),\n `integration_get_data_source_schema`, and `integration_query_data_source`\n (rows). `integration_create_page` and `integration_append_blocks` author\n Notion's rich-text and block shapes from plain text, Markdown, or checklists.\n- Four generic tools reach every other operation in the pinned API index.\n `notion_api_search` finds operations, not content. `notion_api_details`\n returns one operation's parameters. `notion_api_read` calls GETs plus the\n read-only POST queries: search, data source and meeting note queries, and\n agent, session, and session event queries. `notion_api_write` calls every\n other POST, PATCH, and DELETE with `method` and `path` stated.\n- Every generic call is checked against the index before it is sent. An\n unknown path returns the nearest operations, and an unknown or missing\n parameter returns `validation.issues` with the accepted names. Nothing\n reached Notion, so fix the call from that answer.\n- The generic set replaces the old one-operation tools. Read a database\n container with `GET /v1/databases/{database_id}`, users with `GET /v1/users`\n or `GET /v1/users/me`, comments with `GET /v1/comments
|
|
9
|
-
"shared": "\n## Notion's data model\n\n- A database is a container; its rows and schema live in a *data source*\n inside it. Queries, schema reads, and new rows take the data source, and\n Notion rejects a database id in its place.\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read the schema before composing one.\n-
|
|
7
|
+
"oauth": "\n\n- This connection acts as its OAuth-authorized user and reaches what that user\n can. It holds no integration token. An internal integration (bot) is a\n separate connector with `auth: { type: \"token\" }` under another id; never\n switch to it to recover a failed call here.\n- Discover the live catalog before assuming a tool exists. Notion can gate\n tools by workspace, account, client, and rollout independently of Connecta.\n `notion-get-tool-access` reports what the workspace's plan allows.\n- Start broad discovery with `notion-search`, then use `notion-fetch` on\n the exact page, database, data source, or object before changing it.\n- Use the live input schema as the contract. Notion owns these MCP schemas;\n the REST schemas of Connecta's token connector do not apply to MCP tools with\n similar names.\n- Session and agent tools can launch asynchronous work. Read session state and\n events before sending another message, waiting, or stopping a session.\n- File uploads and attachment tools create durable workspace state. Keep\n downloads inside the requested task and do not expose signed attachment URLs.\n `notion-download-skill` returns a signed, expiring archive URL by design;\n the token connector's REST redaction does not apply to hosted tools, so\n use that URL for the download and do not repeat it in replies.\n- Connecta's generic REST tools (`notion_api_*`) and the `integration_*`\n helpers exist only on a token connector.\n- An `auth_required` failure means this connector's OAuth grant is missing or\n expired. Run `authorize_connector` for this connector id, then retry.\n",
|
|
8
|
+
"key": "\n## Identity and tools\n\nThis is Connecta's REST connector to `api.notion.com`, acting as one internal\nintegration (a bot) with an operator-managed integration token. It reaches\nexactly the pages and data sources shared with that integration, and the\nintegration's capabilities decide which calls succeed: comment and\nuser-information capabilities are off by default. It never acts as a person.\nHosted Notion MCP is a separate OAuth connector.\n\n- Named tools cover the common path and flatten Notion's payloads:\n `integration_search` (titles), `integration_get_page` (properties),\n `integration_get_page_content` (blocks as text),\n `integration_get_data_source_schema`, and `integration_query_data_source`\n (rows). `integration_create_page` and `integration_append_blocks` author\n Notion's rich-text and block shapes from plain text, Markdown, or checklists.\n- Four generic tools reach every other operation in the pinned API index.\n `notion_api_search` finds operations, not content. `notion_api_details`\n returns one operation's parameters. `notion_api_read` calls GETs plus the\n read-only POST queries: search, data source and meeting note queries, and\n agent, session, and session event queries. `notion_api_write` calls every\n other POST, PATCH, and DELETE with `method` and `path` stated.\n- Every generic call is checked against the index before it is sent. An\n unknown path returns the nearest operations, and an unknown or missing\n parameter returns `validation.issues` with the accepted names. Nothing\n reached Notion, so fix the call from that answer.\n- The generic set replaces the old one-operation tools. Read a database\n container with `GET /v1/databases/{database_id}`, users with `GET /v1/users`\n or `GET /v1/users/me`, and comments with `GET /v1/comments`. Update\n properties, trash or restore (`in_trash`) with `PATCH /v1/pages/{page_id}`,\n and comment with `POST /v1/comments` (`rich_text` or `markdown`).\n- Refused by design: Notion's OAuth token endpoints (`/v1/oauth/*`) and\n multipart file sends. Import a file with `POST /v1/file_uploads` and\n `mode: \"external_url\"` instead.\n- Notion-hosted file URLs in pages and blocks are pre-signed: Connecta\n withholds their signature, credential, and security-token parameters, so a\n returned URL names the file but does not download it. File links inside\n page Markdown come back as Notion sent them: use one only for its download\n and do not repeat it in replies. Credential-named\n fields read as `\"[redacted]\"`, a database property named like a credential\n (`API token`) included. Failures of the OAuth, file upload, bot user, and\n agent operations carry Notion's code, not its message.\n- Not here: the hosted MCP's user identity, its search across connected\n apps, page duplication, team and skill search, attachment downloads, and\n page-to-skill conversion. Those need an OAuth connector.\n\n## Databases contain data sources\n\n- The id in a Notion database URL is a **database id**.\n `GET /v1/databases/{database_id}` through `notion_api_read` lists its\n `data_sources`, usually one. `integration_search` returns data sources\n directly and skips that step.\n- `integration_get_data_source_schema` and `integration_query_data_source`\n take the **data_source_id**, and so does `integration_create_page` when\n adding a row.\n\nSo the sequence for \"find rows in this database\" is the database read, then\n`integration_get_data_source_schema`, then `integration_query_data_source`.\n\n## Reading\n\n- `integration_get_page` returns properties. To read or summarize a whole\n body, prefer `notion_api_read` `GET /v1/pages/{page_id}/markdown`: one\n request returns every nested level as Markdown (headings, lists, to-dos,\n toggles, tables, code, links, mentions, equations). A huge page comes back\n with `truncated: true` and the unread blocks in `unknown_block_ids`; read\n each id as a `page_id` on the same path.\n- `integration_get_page_content` returns the body as flat blocks with ids,\n for quoting a block or appending after one. Nested blocks (toggles, list\n children, table rows) need `depth`, and each level multiplies requests. A\n deep read stops at an internal ceiling and reports `truncated: true` rather\n than spending the whole rate-limit budget.\n- Notion truncates `title`, `rich_text`, `relation`, and `people` at 25\n entries. `integration_get_page` reports each in `truncated_properties` as\n `{ name, id }`; read the complete value with `notion_api_read`\n `GET /v1/pages/{page_id}/properties/{property_id}` and follow its cursor.\n- Every named read projects Notion's payload to ids, plain text, and\n flattened property values. Pass `raw: true` for Notion's untouched response\n when a specific field is missing. It is much larger. Narrow\n `integration_query_data_source` and `integration_get_page` with `properties`\n when the goal is fewer fields. `integration_get_page_content` with\n `raw: true` returns one level and ignores `depth`.\n\n## Writing content\n\n`integration_append_blocks` adds children and nothing else. It cannot move,\nreorder, or replace an existing block, and a block appended through the API can\nnever be relocated by it afterwards. Get the position right the first time with\n`position` and `after_block_id`. Notion caps one call at 100 blocks. Text\nlonger than Notion's 2,000-character run limit is split into runs for you.\n\nFor formatted content, write Markdown to a page with `notion_api_write`\n`PATCH /v1/pages/{page_id}/markdown`. Append with\n`{\"type\":\"insert_content\",\"insert_content\":{\"content\":\"## Notes\\n- one\",\"position\":{\"type\":\"end\"}}}`\n(`start` prepends). Edit text in place with `update_content`, whose\n`content_updates` are `{ old_str, new_str }` pairs copied exactly from the\npage's Markdown, and `replace_content` swaps the whole page for `new_str`.\nNotion refuses an edit that would delete a child page or database with a\n`validation_error` naming them. Removing ordinary text needs no flag; to delete\nthose children, set `allow_deleting_content: true` inside the command body\n(`{\"type\":\"update_content\",\"update_content\":{\"content_updates\":[…],\"allow_deleting_content\":true}}`),\nand leave it out unless the task is to delete them. `integration_create_page`\ntakes the same Markdown for a new page's body.\n\n## Pagination\n\n- Named list tools take `page_size` (max 100) and return `has_more` with\n `next_cursor`; pass it back as `start_cursor`.\n- Generic results arrive as `{ status, data, page? }`. On a list,\n `page.next` is the cursor and `page.param` is `start_cursor`: send it in\n `query` for a GET, and in `body` when `page.in` is `body`, as on the POST\n queries. Pass `select` dot paths relative to `data`, such as `results.id`;\n `page` survives any `select`.\n- A view query is created with `notion_api_write`\n `POST /v1/views/{view_id}/queries`, a write because Notion stores it until\n `expires_at`. Its first page names the continuation in `page.path`: read\n `GET /v1/views/{view_id}/queries/{query_id}` with `start_cursor` in `query`.\n- Meeting-note queries have no cursor. `limit` caps at 50, and\n `page.hasMore` without `page.next` means more notes exist: narrow the\n filter (for example by `created_time`) rather than paging.\n- A data source query stops at 10,000 rows. `integration_query_data_source`\n then returns `incomplete: true`, and a generic query `page.hasMore` without\n `page.next`: narrow the filter rather than trusting the count.\n- Cursors are opaque: never parse or construct one. Follow pages inside\n `execute_code` and reduce there.\n\n## Failures worth reading carefully\n\n- **404** means \"no such object\" *or* \"not shared with this integration\", and\n Notion will not say which. Never conclude a page was deleted from it; check\n that the page is shared with the integration in Notion.\n- **403** is not an expired token. The integration is missing a capability\n (comment capabilities are off by default), the object was never shared, or\n the workspace is past its plan's block limit; Notion's message says which.\n Re-authorizing cannot fix it; an operator must change it in Notion.\n- **429** carries a retry window. Notion allows each integration 180 requests\n a minute (600 on Business and Enterprise) plus a budget shared by the whole\n workspace; Connecta admits 180 calls a minute, three at a time, per runtime.\n Wait out the window rather than retrying immediately. A 429 that says the\n integration's API access is blocked is not retryable.\n- **5xx on a write** may still have been saved. When Notion names the\n object it committed, the failure says so: read that object instead of\n repeating the write. Otherwise check the target before trying again.\n",
|
|
9
|
+
"shared": "\n## Notion's data model\n\n- A database is a container; its rows and schema live in a *data source*\n inside it. Queries, schema reads, and new rows take the data source, and\n Notion rejects a database id in its place.\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read the schema before composing one.\n- A `status` write must use an existing option name; inventing one fails.\n A new `select` or `multi_select` name does not fail: when the connection\n can write to the data source, Notion adds it to the schema as a new\n option. Use the options the schema lists unless a schema change is meant.\n- Writes **replace** a property. Sending one item to a `multi_select` or\n `relation` drops the others, so send the complete intended value.\n- `rollup`, `formula`, `unique_id`, and the created and edited fields are\n computed. They cannot be written.\n- A data source's title column is rarely called \"title\". Read its name from\n the schema before creating or renaming a row.\n- Notion has **no idempotency key**. A retried page create or comment makes a\n duplicate. Confirm with a search before repeating a write that may have\n partially succeeded.\n"
|
|
10
10
|
}
|
|
11
11
|
};
|
|
@@ -4,6 +4,12 @@ export declare const PLANNING_CENTER_API_BASE_URL = "https://api.planningcentero
|
|
|
4
4
|
/** The products behind `/<app>/v2`, in the order their guides list them. */
|
|
5
5
|
export declare const PLANNING_CENTER_APPS: readonly ["people", "services", "groups", "check-ins", "calendar", "registrations", "giving", "publishing", "webhooks", "api"];
|
|
6
6
|
export type PlanningCenterApp = (typeof PLANNING_CENTER_APPS)[number];
|
|
7
|
+
/**
|
|
8
|
+
* A product a deployment may remove from this connection. People is the one
|
|
9
|
+
* it may not: every product joins through its person ids, and the credential
|
|
10
|
+
* test reads `/people/v2/me`.
|
|
11
|
+
*/
|
|
12
|
+
export type PlanningCenterExcludableApp = Exclude<PlanningCenterApp, "people">;
|
|
7
13
|
/**
|
|
8
14
|
* One reviewed `X-PCO-API-Version` per product, sent on every request.
|
|
9
15
|
*
|
|
@@ -46,6 +52,27 @@ export interface PlanningCenterOptions {
|
|
|
46
52
|
userAgent?: string;
|
|
47
53
|
/** Default `per_page` for list tools. Defaults to 25; Planning Center's maximum is 100. */
|
|
48
54
|
defaultPageSize?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Products this connection never reaches, e.g. `["giving"]` for a
|
|
57
|
+
* deployment that keeps agents away from donor and financial data. Their
|
|
58
|
+
* named tools leave the catalog, the guide and description stop naming
|
|
59
|
+
* them, and the hatches refuse their paths before any request is sent.
|
|
60
|
+
* While any product is excluded, hatch paths must be literal, People lists
|
|
61
|
+
* are unreachable, and existing webhook subscriptions and their payloads
|
|
62
|
+
* are refused, because each could carry an excluded product's data.
|
|
63
|
+
* Defaults to none.
|
|
64
|
+
*
|
|
65
|
+
* Exclusion guarantees agents reach no excluded-product endpoint, no live
|
|
66
|
+
* cross-product query (People lists), and no webhook payload or
|
|
67
|
+
* subscription toggle. It does not hide records the church's automations
|
|
68
|
+
* or staff have copied into a reachable product: a Giving automation that
|
|
69
|
+
* adds first-time donors to a People workflow leaves readable workflow
|
|
70
|
+
* cards, as do notes, custom fields, and form answers. For a hard
|
|
71
|
+
* boundary, use a personal access token from a user with No Access in
|
|
72
|
+
* the excluded product; Planning Center sets permissions per product, per
|
|
73
|
+
* user, and a token carries its user's. Exclusion is defense in depth.
|
|
74
|
+
*/
|
|
75
|
+
excludeApps?: readonly PlanningCenterExcludableApp[];
|
|
49
76
|
/**
|
|
50
77
|
* Replaces the default per-runtime budget (100 calls per 20 seconds, five at
|
|
51
78
|
* a time). Split it when two connections share one token's user, because
|