@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
|
@@ -3,8 +3,8 @@ export const skill = {
|
|
|
3
3
|
"content": "- Lists are cursor-paginated: `limit` defaults to 10 and caps at 100, `starting_after` and `ending_before` take an object id and are mutually exclusive, and `has_more` says whether to continue. Page inside `execute_code`.\n- Search filters on a documented per-resource field set, not on arbitrary attributes. Charges search takes `amount`, `created`, `currency`, `customer`, `status`, `refunded`, `disputed`, `metadata`, `billing_details.address.postal_code`, and `payment_method_details.<source>.*` card fields — there is no `payment_intent` field. When the field you want is not searchable, retrieve the parent object and follow its reference (the PaymentIntent's `latest_charge`) instead of retrying the search with another spelling.\n- The account → `stripe_api_search` → `stripe_api_details` → `stripe_api_read` sequence is one program, not four turns: resolve the method once, then call that method as many times as the investigation needs in the same run.\n- Decline outcomes live on the charge — `outcome`, `failure_code`, `failure_message` — reached from the PaymentIntent's `latest_charge`, so \"why did this payment fail\" is a PaymentIntent read followed by one charge read.\n- Resolve ids before acting; never guess one. Stripe ids are typed prefixes — `cus_` customer, `sub_` subscription, `ch_` charge, `pi_` payment intent, `in_` invoice, `acct_` account — and a plausible-looking id belongs to a different object or to nobody.\n- Amounts are integers in the currency's minor unit: `1099` is 10.99 USD, and zero-decimal currencies like JPY take `10` for 10 JPY. Never send a decimal.\n- Treat every create, update, delete, refund, and report run as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
|
|
4
4
|
"instructionsHeading": "Account instructions",
|
|
5
5
|
"fragments": {
|
|
6
|
-
"oauth": "\n\nThis OAuth session may expose both live and sandbox Stripe accounts. Discover the live account-selection tools, select the intended account and mode, then carry exact context fields such as `stripe_context` and `livemode` wherever the schema requires them. A live-mode write moves real money; a sandbox write changes test data. Never infer the account or mode from connector metadata.\n\n- Discover account and mode with the live account-selection tools before every account-scoped read or write. The published inventory has changed account-tool names; do not assume `list_available_accounts_or_orgs` is available. Select the intended result and carry the exact context fields required by the live schema unchanged. If the account, mode, or supported selector is ambiguous, stop and ask; never guess.\n- Organization accounts are not Stripe Connect connected accounts. A Connect call requires a separate API-key connector whose `connectedAccount` sends Stripe's documented `Stripe-Account` header; OAuth does not support that path.\n- Four generic tools reach any Stripe API method. Find the method with `stripe_api_search`, read its parameters with `stripe_api_details`, then call `stripe_api_read` (GET) or `stripe_api_write` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — `stripe_api_details` is cheaper than a rejected write.\n- Prefer a dedicated tool when one covers the task: `get_balance_summary` for balances and `stripe_analytics` for Sigma or Metrics reporting. Use `stripe_api_search` for everything else instead of assuming a retired dedicated tool still exists.\n- `stripe_implementation_planner` and the query-execution intents of `stripe_analytics` create provider-side planning or query-run state. Connecta therefore classifies both as writes
|
|
7
|
-
"key": "\n\n- This is Connecta's REST connector to `api.stripe.com`, holding one operator-managed key. It reaches exactly one account in one mode, so there is no account selection and no `stripe_context` or `livemode` argument.\n- `stripe_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `stripe_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `stripe_api_read` calls GET operations, plus `POST /v1/invoices/create_preview`, which only previews. `stripe_api_write` calls every other POST and every DELETE, from a customer edit to a subscription cancellation, with `method` and `path` stated;
|
|
6
|
+
"oauth": "\n\nThis OAuth session may expose both live and sandbox Stripe accounts. Discover the live account-selection tools, select the intended account and mode, then carry exact context fields such as `stripe_context` and `livemode` wherever the schema requires them. A live-mode write moves real money; a sandbox write changes test data. Never infer the account or mode from connector metadata.\n\n- Discover account and mode with the live account-selection tools before every account-scoped read or write. The published inventory has changed account-tool names; do not assume `list_available_accounts_or_orgs` is available. Select the intended result and carry the exact context fields required by the live schema unchanged. If the account, mode, or supported selector is ambiguous, stop and ask; never guess.\n- Organization accounts are not Stripe Connect connected accounts. A Connect call requires a separate API-key connector whose `connectedAccount` sends Stripe's documented `Stripe-Account` header; OAuth does not support that path.\n- Four generic tools reach any Stripe API method. Find the method with `stripe_api_search`, read its parameters with `stripe_api_details`, then call `stripe_api_read` (GET) or `stripe_api_write` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — `stripe_api_details` is cheaper than a rejected write.\n- Prefer a dedicated tool when one covers the task: `get_balance_summary` for balances and `stripe_analytics` for Sigma or Metrics reporting. Use `stripe_api_search` for everything else instead of assuming a retired dedicated tool still exists.\n- `stripe_implementation_planner` and the query-execution intents of `stripe_analytics` create provider-side planning or query-run state. Connecta therefore classifies both as writes, which a read-only endpoint refuses; their retrieval paths stay behind the same tool boundary.\n- `stripe_api_write` carries the blast radius of the entire write API — every POST, PATCH, PUT, and DELETE, from a customer edit to a subscription cancellation. State the method and path explicitly; a read-only endpoint refuses it.\n- Any `stripe_api_read` list or `stripe_api_search` that returns full objects belongs inside `execute_code`, projected to the fields the question needs before `return`. Neither `limit` nor `expand` substitutes for that: an unprojected list of customers or invoices truncates long before it answers, and a projected one keeps the customer's name, email, and address out of the transcript.\n- Find an object with `stripe_api_search` (or a list endpoint through `stripe_api_read`) and carry the `id` it returned into the write.\n- This connection's tool list is not a fixed set. Stripe gates parts of its MCP catalog by account, integration, and beta enrollment, so search this connector for what it actually exposes rather than assuming a documented tool is here.\n- Send an `Idempotency-Key` on every write you might retry, if the tool accepts it, and reuse the same key for the retry. A retry with a fresh key is a second charge, not a second attempt.\n",
|
|
7
|
+
"key": "\n\n- This is Connecta's REST connector to `api.stripe.com`, holding one operator-managed key. It reaches exactly one account in one mode, so there is no account selection and no `stripe_context` or `livemode` argument.\n- `stripe_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `stripe_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `stripe_api_read` calls GET operations, plus `POST /v1/invoices/create_preview`, which only previews. `stripe_api_write` calls every other POST and every DELETE, from a customer edit to a subscription cancellation, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete: substitute ids into the template search returned (`/v1/payment_intents/pi_123`). Pass `query` and `body` as JSON objects named as `stripe_api_details` lists them. Connecta form-encodes v1 bodies and query lists (`expand: [\"latest_charge\"]`), sends v2 bodies as JSON, and pins `Stripe-Version` to the index's API version. Send `null` or `\"\"` to unset a field.\n- Every call is checked against the pinned index before it is sent. An unknown path returns the nearest operations; an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Stripe, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `starting_after`, or `page` for search and v2 lists. Lists belong inside `execute_code`; pass `select` (dot paths such as `data.id` and `data.status`) to keep only the fields the question needs, which also keeps customer names, emails, and addresses out of the transcript.\n- `get_stripe_account_info` confirms the account, mode, and connected account; `get_balance_summary` returns available and pending balances. Use them when they answer the question.\n- Find records with list and search operations through `stripe_api_read` (`GET /v1/customers/search`, `GET /v1/charges` with `customer`) and carry the returned `id` into the write.\n- `stripe_api_write` sends an `Idempotency-Key` on every POST. Pass `idempotencyKey` for anything you might retry, or reuse the generated one the result returns. An ambiguous failure carries it in its message or as `uncertainCall.recovery.idempotencyKey`. Reuse it only with the exact original arguments and within Stripe's retention (v1 keys may be pruned after 24 hours); after that, look the object up before retrying. A retry with a fresh key is a second charge, not a second attempt.\n- Refused by design: operations that hand out a credential (ephemeral keys, Terminal connection tokens, account links (v1 and v2), Terminal onboarding links, Express login links, account sessions, customer sessions, billing portal sessions, file link creation, Financial Connections session creation, and the meter event session), Stripe Apps secret payloads (`expand` of `payload`), Issuing card `number` and `cvc` expansion on any Issuing path, multipart uploads such as `POST /v1/files` (use the Dashboard), and the meter event stream (send single events with `POST /v2/billing/meter_events`). Each refusal names its reason. Quote PDFs arrive from `files.stripe.com` as base64.\n- Secrets read as `\"[redacted]\"` in every result, expansions included: client secrets (PaymentIntents, SetupIntents, Checkout and Identity sessions, Sources, Financial Connections, invoice confirmation secrets), webhook and event destination signing secrets, Apps secret payloads, app-install authorization codes, Terminal Wi-Fi passwords, forwarded request header values, and Issuing card numbers and CVCs. Webhook and event destination URLs, file link URLs (in an expanded File, a list, or an event too), and pre-signed import URLs keep only their scheme and host. Identity verification `url`s come back whole. A Checkout Session's `url` comes back whole only for a guest payment session (`mode: \"payment\"`, no `customer` or `customer_account`, no customer creation, no saved payment method options); any other session returns only its scheme and host, because its page can show, reuse, or remove a customer's saved payment methods. Create those from your own server code, or send a payment link or the invoice's hosted page instead. These rules follow each object wherever it appears, expansions and events included. A `[redacted]` field is never the real value; do not send it back.\n- Failures of credential-bearing operations (webhook endpoints, Apps secrets, Terminal configurations, forwarding) carry Stripe's type, code, decline code, and param, not its message. Payment, billing, and account failures keep Stripe's message.\n- The hosted OAuth connection's natural-language Sigma, metrics, documentation search, implementation planner, and feedback tools do not exist with a key.\n",
|
|
8
8
|
"shared": "- Lists are cursor-paginated: `limit` defaults to 10 and caps at 100, `starting_after` and `ending_before` take an object id and are mutually exclusive, and `has_more` says whether to continue. Page inside `execute_code`.\n- Search filters on a documented per-resource field set, not on arbitrary attributes. Charges search takes `amount`, `created`, `currency`, `customer`, `status`, `refunded`, `disputed`, `metadata`, `billing_details.address.postal_code`, and `payment_method_details.<source>.*` card fields — there is no `payment_intent` field. When the field you want is not searchable, retrieve the parent object and follow its reference (the PaymentIntent's `latest_charge`) instead of retrying the search with another spelling.\n- The account → `stripe_api_search` → `stripe_api_details` → `stripe_api_read` sequence is one program, not four turns: resolve the method once, then call that method as many times as the investigation needs in the same run.\n- Decline outcomes live on the charge — `outcome`, `failure_code`, `failure_message` — reached from the PaymentIntent's `latest_charge`, so \"why did this payment fail\" is a PaymentIntent read followed by one charge read.\n- Resolve ids before acting; never guess one. Stripe ids are typed prefixes — `cus_` customer, `sub_` subscription, `ch_` charge, `pi_` payment intent, `in_` invoice, `acct_` account — and a plausible-looking id belongs to a different object or to nobody.\n- Amounts are integers in the currency's minor unit: `1099` is 10.99 USD, and zero-decimal currencies like JPY take `10` for 10 JPY. Never send a decimal.\n- Treat every create, update, delete, refund, and report run as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
|
|
9
9
|
"oauth_limits": "- Stripe answers a rate limit with `429` and a `Stripe-Rate-Limited-Reason` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of 100 requests per second in live mode and 25 in sandbox mode, and any single endpoint is capped at 25 per second regardless of mode, so paging one list is the real constraint.\n- Use `search_stripe_documentation` when the shape of an object or a flow is unclear; it is a read and costs nothing but a call.\n- An `auth_required` failure means this connector's Stripe 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 Stripe's own words instead — read it rather than re-authorizing.\n",
|
|
10
10
|
"key_limits": "- Stripe answers a rate limit with `429` and a `Stripe-Rate-Limited-Reason` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of ",
|
|
@@ -169,11 +169,11 @@ const VERCEL_MCP_CLASSIFICATION = {
|
|
|
169
169
|
},
|
|
170
170
|
"filter_project_envs": {
|
|
171
171
|
"verdict": "write",
|
|
172
|
-
"reason": "Lists project environment variables and can return decrypted values; reviewed as a write so
|
|
172
|
+
"reason": "Lists project environment variables and can return decrypted values; reviewed as a write so a read-only endpoint refuses the disclosure (#801).",
|
|
173
173
|
},
|
|
174
174
|
"get_project_env": {
|
|
175
175
|
"verdict": "write",
|
|
176
|
-
"reason": "Returns one environment variable's decrypted value; reviewed as a write so
|
|
176
|
+
"reason": "Returns one environment variable's decrypted value; reviewed as a write so a read-only endpoint refuses the disclosure (#801).",
|
|
177
177
|
},
|
|
178
178
|
"create_project_env": {
|
|
179
179
|
"verdict": "destructive",
|
|
@@ -3,8 +3,8 @@ export const skill = {
|
|
|
3
3
|
"content": "- Resolve team, project, and deployment ids with list reads; never guess one. Team ids begin `team_`, project ids `prj_`, and deployment ids `dpl_`.\n- Diagnose a deployment by reading its state first (`readyState`, `errorCode`, `errorMessage`), then its build logs, then runtime logs for a deployment that built. Narrow time windows before raising limits.\n- Environment variable changes apply only to future deployments; a new deployment is a separate write. Promoting an existing deployment to production does not rebuild it.\n- Treat every create, update, delete, promotion, cancellation, and purchase as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
|
|
4
4
|
"instructionsHeading": "Account instructions",
|
|
5
5
|
"fragments": {
|
|
6
|
-
"oauth": "\n\nTool names, descriptions, argument schemas, and result schemas come from Vercel's live server. Connecta applies release-reviewed classification; reviewed writes stay writes even when Vercel claims they only read.\n\n- Discover the live catalog before assuming a tool exists. Vercel can change the surface independently of a Connecta release, and account features may affect what the authorization can reach.\n- Complete authorization and grant access to the owning team before acting. Listing this server does not prove team access.\n- MCP pagination and arguments follow the live MCP schemas; do not apply REST pagination or argument shapes to them.\n- Diagnose deployments with `get_deployment`, then build logs, runtime error clusters, and runtime logs.\n- `get_project_env` and `filter_project_envs` can return decrypted environment values, so Connecta reviews them as writes and
|
|
7
|
-
"key": "\n\n- This is Connecta's REST connector to `api.vercel.com`, holding one operator-managed access token. A default team routes calls; it does not narrow what the token can reach.\n- `vercel_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `vercel_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `vercel_api_read` calls GET operations, plus reviewed POSTs that only read: `POST /v2/observability/query`, registrar availability, price, and search, and `POST /v8/artifacts`. `vercel_api_write` calls every other POST, PUT, PATCH, and DELETE, with `method` and `path` stated;
|
|
6
|
+
"oauth": "\n\nTool names, descriptions, argument schemas, and result schemas come from Vercel's live server. Connecta applies release-reviewed classification; reviewed writes stay writes even when Vercel claims they only read.\n\n- Discover the live catalog before assuming a tool exists. Vercel can change the surface independently of a Connecta release, and account features may affect what the authorization can reach.\n- Complete authorization and grant access to the owning team before acting. Listing this server does not prove team access.\n- MCP pagination and arguments follow the live MCP schemas; do not apply REST pagination or argument shapes to them.\n- Diagnose deployments with `get_deployment`, then build logs, runtime error clusters, and runtime logs.\n- `get_project_env` and `filter_project_envs` can return decrypted environment values, so Connecta reviews them as writes, and a read-only endpoint refuses them. Prefer reading variable keys and targets without values.\n- Purchase tools change billing. Read a quote first and carry its price, idempotency key, and requested term into the confirmed purchase unchanged.\n- `get_access_to_vercel_url` creates a temporary access grant. Treat the URL it returns as a credential and do not expose it outside the requested task.\n- `deploy_to_vercel` and `import-claude-design-from-url` can create or update live projects. Read the target and deployment mode before invoking them.\n- An `auth_required` failure means this connector's OAuth grant is missing or expired. Run `authorize_connector` for this connector id, then retry.\n",
|
|
7
|
+
"key": "\n\n- This is Connecta's REST connector to `api.vercel.com`, holding one operator-managed access token. A default team routes calls; it does not narrow what the token can reach.\n- `vercel_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `vercel_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `vercel_api_read` calls GET operations, plus reviewed POSTs that only read: `POST /v2/observability/query`, registrar availability, price, and search, and `POST /v8/artifacts`. `vercel_api_write` calls every other POST, PUT, PATCH, and DELETE, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete and carry the version search returned: `/v13/deployments/dpl_123`, not `/deployments/dpl_123`. Pass `query` and `body` as JSON objects named as `vercel_api_details` lists them. Every call is checked against the pinned index before it is sent: an unknown path returns the nearest operations, and an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Vercel, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `until` (a millisecond timestamp from `pagination.next`) on most lists, `cursor` on newer ones. Lists belong inside `execute_code`; pass `select` (dot paths such as `deployments.uid` and `deployments.state`) to keep only the fields the question needs.\n- `list_teams` returns team ids and slugs and the connection's default team. `get_deployment_build_logs` reads a deployment's build events without following live output. `get_runtime_logs` collects the runtime log stream for at most `waitMs` or `maxRows`, then stops and says why. Generic reads refuse `follow` and the runtime-log stream, which never end on their own.\n- `vercel_api_upload` sends explicit text or base64 bytes to an octet-stream operation, such as `POST /v2/files` before a deployment (Connecta computes `x-vercel-digest`). `vercel_api_write` sends JSON only.\n- Environment values never come back. `list_project_env_vars` never decrypts. `upsert_project_env_var` (`upsert: false` for create-only), `update_project_env_var`, and `delete_project_env_var` return metadata without values; values are write input only. Generic reads refuse `GET /v1/projects/{idOrName}/env/{id}`, `GET /v10/projects/{idOrName}/env`, `GET /v1/env/{id}`, and any `decrypt`, and Connecta replaces the value of every environment variable in any result with `\"[redacted]\"`, including project reads, shared variables, and audit events.\n- Other secrets read as `[redacted]` in every result: credential-named fields and their whole contents (`token`, `secret`, `password`, `jwt`, `credentials`, `Authorization`, …), protection-bypass secrets (each bypass's metadata stays under a numbered placeholder), deploy hook URLs, drain and webhook headers and signing secrets (including the one a new webhook or drain returns), drain and webhook destination URLs and external route, rewrite, and redirect destinations beyond their scheme and host, the args and values of every header, cookie, and query rule (route transforms and conditions, firewall conditions), team invite codes, synced Global Config items, and credentials inside other URLs. Failures of every reviewed operation, and of any operation whose request takes a credential-named field, say only the status and a reviewed code. Read or rotate withheld secrets in the Vercel dashboard. A `[redacted]` field is never the real value; do not send it back.\n- Refused by design: endpoints that mint credentials (access tokens, AI Gateway API keys, project OIDC and trace tokens, Connect and SSO tokens, Edge Config tokens, KMS-signed tokens, Flags SDK keys, protection-bypass secrets, installation credential rotation), Connect authorization requests, project transfer requests (the code lets another team claim the project), KMS signing, a domain move-out (its answer is a transfer token), Global Config items, backups, and tokens, and domain transfer auth codes. Each refusal names its reason.\n- The hosted OAuth connection's documentation search, runtime error clusters, toolbar threads, agent runs, web analytics summaries, purchase quotes, `deploy_to_vercel`, and access grants for protected URLs do not exist with a token. Configure an OAuth connector for those.\n- A `429` carries Vercel's reset time; Vercel meters endpoints separately, so wait before retrying the same operation. An `auth_required` failure means the token is missing, expired, or outside this team: an operator must replace it in this connection in the operator UI, and `authorize_connector` returns that handoff. A rejected argument comes back in Vercel's own words; read it rather than replacing the token.\n",
|
|
8
8
|
"shared": "- Resolve team, project, and deployment ids with list reads; never guess one. Team ids begin `team_`, project ids `prj_`, and deployment ids `dpl_`.\n- Diagnose a deployment by reading its state first (`readyState`, `errorCode`, `errorMessage`), then its build logs, then runtime logs for a deployment that built. Narrow time windows before raising limits.\n- Environment variable changes apply only to future deployments; a new deployment is a separate write. Promoting an existing deployment to production does not rebuild it.\n- Treat every create, update, delete, promotion, cancellation, and purchase as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n"
|
|
9
9
|
}
|
|
10
10
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RemoteMcpAuth } from "../../connectors/remote-mcp.js";
|
|
2
|
+
import { type ProviderOptions } from "../../provider.js";
|
|
3
|
+
/** Wispr Flow's hosted Streamable HTTP endpoint, without a trailing slash. */
|
|
4
|
+
export declare const WISPR_FLOW_MCP_ENDPOINT = "https://api.wisprflow.ai/connect/mcp";
|
|
5
|
+
type WisprFlowOAuth = Extract<RemoteMcpAuth, {
|
|
6
|
+
type: "oauth";
|
|
7
|
+
}>;
|
|
8
|
+
export interface WisprFlowOptions extends ProviderOptions {
|
|
9
|
+
/** OAuth only; defaults to browser authorization with the advertised scopes. */
|
|
10
|
+
auth?: WisprFlowOAuth;
|
|
11
|
+
}
|
|
12
|
+
/** A maintained Wispr Flow Notetaker hosted-MCP connection. */
|
|
13
|
+
export declare const wisprFlow: import("../../provider.js").ProviderFactory<WisprFlowOptions>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { remoteMcp } from "../../connectors/remote-mcp.js";
|
|
2
|
+
import { keys, optionsOf, variants } from "../../config-schema.js";
|
|
3
|
+
import { defineProvider, PROVIDER_COMMON } from "../../provider.js";
|
|
4
|
+
import { skill } from "./skill.generated.js";
|
|
5
|
+
/** Wispr Flow's hosted Streamable HTTP endpoint, without a trailing slash. */
|
|
6
|
+
export const WISPR_FLOW_MCP_ENDPOINT = "https://api.wisprflow.ai/connect/mcp";
|
|
7
|
+
/**
|
|
8
|
+
* Reviewed public-source superset, 2026-10-13. Scalekit documents 14 reads;
|
|
9
|
+
* Claude's connector directory adds four task tools. Wispr's own help page
|
|
10
|
+
* describes read-only access, so the task writes remain classified even if
|
|
11
|
+
* this account never exposes them. No authenticated catalog or schemas were
|
|
12
|
+
* captured. Vendor names, schemas and results remain live.
|
|
13
|
+
*/
|
|
14
|
+
const CLASSIFICATION = {
|
|
15
|
+
tools: {
|
|
16
|
+
get_account_info: { verdict: "read", reason: "Returns the signed-in identity and calendar display-name aliases." },
|
|
17
|
+
get_calendar_event: { verdict: "read", reason: "Retrieves an existing calendar event and its attendees." },
|
|
18
|
+
get_meeting: {
|
|
19
|
+
verdict: "read",
|
|
20
|
+
reason: "Retrieves recorded notes, the summary, attendees and requested transcript ranges.",
|
|
21
|
+
},
|
|
22
|
+
get_meeting_by_calendar_id: {
|
|
23
|
+
verdict: "read",
|
|
24
|
+
reason: "Finds the existing recording linked to a calendar event and reads its content.",
|
|
25
|
+
},
|
|
26
|
+
get_meeting_participants_enriched: {
|
|
27
|
+
verdict: "read",
|
|
28
|
+
reason: "Reads participant information and identifies the signed-in attendee.",
|
|
29
|
+
},
|
|
30
|
+
get_scratchpad_note: { verdict: "read", reason: "Returns a bounded range of an existing Scratchpad note." },
|
|
31
|
+
get_upcoming_meeting: {
|
|
32
|
+
verdict: "read",
|
|
33
|
+
reason: "Reads an upcoming event and an available pre-read without generating one.",
|
|
34
|
+
},
|
|
35
|
+
list_meeting_series: {
|
|
36
|
+
verdict: "read",
|
|
37
|
+
reason: "Lists recorded occurrences of an existing recurring meeting series.",
|
|
38
|
+
},
|
|
39
|
+
list_upcoming_meetings: {
|
|
40
|
+
verdict: "read",
|
|
41
|
+
reason: "Lists upcoming calendar events and already-available pre-reads.",
|
|
42
|
+
},
|
|
43
|
+
resolve_calendar_link: {
|
|
44
|
+
verdict: "read",
|
|
45
|
+
reason: "Matches a pasted calendar or conference URL to an existing event.",
|
|
46
|
+
},
|
|
47
|
+
resolve_share_link: {
|
|
48
|
+
verdict: "read",
|
|
49
|
+
reason: "Reads an accessible shared note without creating a link or changing sharing permissions.",
|
|
50
|
+
},
|
|
51
|
+
search_calendar_events: { verdict: "read", reason: "Finds existing calendar events by date, attendees or text." },
|
|
52
|
+
search_meetings: {
|
|
53
|
+
verdict: "read",
|
|
54
|
+
reason: "Finds recorded meetings by title, summary, notes, dates or linked attendees.",
|
|
55
|
+
},
|
|
56
|
+
search_scratchpad_notes: { verdict: "read", reason: "Finds existing Scratchpad notes by text." },
|
|
57
|
+
get_task: { verdict: "read", reason: "Retrieves an existing task without changing its state." },
|
|
58
|
+
search_tasks: { verdict: "read", reason: "Finds existing tasks without changing them." },
|
|
59
|
+
add_task: {
|
|
60
|
+
verdict: "write",
|
|
61
|
+
reason: "Creates a new task; the reviewed operation appends rather than replacing an existing task.",
|
|
62
|
+
},
|
|
63
|
+
update_task: { verdict: "destructive", reason: "Changes the state or fields of an existing task." },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const OPTIONS = optionsOf()({
|
|
67
|
+
...PROVIDER_COMMON,
|
|
68
|
+
auth: variants("type", {
|
|
69
|
+
oauth: optionsOf()({
|
|
70
|
+
...keys("type", "clientMetadataUrl", "scope"),
|
|
71
|
+
client: optionsOf()(keys("issuer", "clientId", "clientSecret", "tokenEndpointAuthMethod")),
|
|
72
|
+
}).shape,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
function accountSummary(purpose) {
|
|
76
|
+
const summary = `Account-wide Notetaker access: ${purpose}`;
|
|
77
|
+
const bytes = new TextEncoder().encode(summary);
|
|
78
|
+
if (bytes.byteLength <= 120)
|
|
79
|
+
return summary;
|
|
80
|
+
// Streaming decode drops an incomplete final code point; reserve three bytes for the ellipsis.
|
|
81
|
+
return `${new TextDecoder().decode(bytes.subarray(0, 117), { stream: true }).trimEnd()}…`;
|
|
82
|
+
}
|
|
83
|
+
/** A maintained Wispr Flow Notetaker hosted-MCP connection. */
|
|
84
|
+
export const wisprFlow = defineProvider({
|
|
85
|
+
name: "wispr-flow",
|
|
86
|
+
title: "Wispr Flow",
|
|
87
|
+
kind: "mcp",
|
|
88
|
+
readme: "Wispr Flow",
|
|
89
|
+
skill,
|
|
90
|
+
classify: CLASSIFICATION,
|
|
91
|
+
options: OPTIONS,
|
|
92
|
+
bundle: {
|
|
93
|
+
baselineGzip: 169009,
|
|
94
|
+
maxGzip: 229009,
|
|
95
|
+
note: "./providers/wispr-flow initial measured baseline is 169,009 B gzip for the hosted-MCP wrapper; the cap retains the existing baseline + 60,000 B provider headroom policy.",
|
|
96
|
+
},
|
|
97
|
+
create(id, options, provider) {
|
|
98
|
+
const ownership = options.authScope === "personal"
|
|
99
|
+
? "Each person authorizes separately and may reach a different account."
|
|
100
|
+
: "One shared authorization selects the account for everyone using this connector.";
|
|
101
|
+
return remoteMcp(id, {
|
|
102
|
+
url: WISPR_FLOW_MCP_ENDPOINT,
|
|
103
|
+
...provider.connectorOptions,
|
|
104
|
+
title: options.title ?? "Wispr Flow",
|
|
105
|
+
description: `Wispr Flow Notetaker meetings, Scratchpad notes, calendar events and tasks: ${options.purpose}`,
|
|
106
|
+
auth: { type: "oauth", scope: "openid offline_access", ...options.auth },
|
|
107
|
+
requireHttps: true,
|
|
108
|
+
classify: provider.classify,
|
|
109
|
+
usageGuide: provider.usageGuide({
|
|
110
|
+
context: [
|
|
111
|
+
`Account-wide Notetaker access: ${options.purpose}. Personal and work accounts hold separate content. ${ownership} Confirm the identity with get_account_info. Dictation history and Notetaker chat history are unavailable.`,
|
|
112
|
+
],
|
|
113
|
+
summary: accountSummary(options.purpose),
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
|
+
export const skill = {
|
|
3
|
+
"content": "- This connection reaches Notetaker meetings, summaries, notes, transcripts, participants and their email addresses, and tasks; Scratchpad notes; calendar events and linked recordings; upcoming meetings with available pre-reads; recurring series; accessible shared notes; and account identity. Meeting results name folders you own or that are shared with you; meetings outside folders list none. Another account needs another authorization. Use `get_account_info` to check which identity this grant reaches.\n- OAuth browser authorization is the only supported auth method. There is no API key. Prefer Google, Apple, Microsoft or enterprise SSO and finish sign-in within about five minutes. Email-and-password sign-in may not complete authorization; Wispr's help pages disagree. Consent covers the account's existing content, without per-meeting consent.\n- Search first, then retrieve by the returned id: `search_meetings` → `get_meeting`, `search_scratchpad_notes` → `get_scratchpad_note`, or `search_calendar_events` → `get_calendar_event`. Resolve pasted URLs with `resolve_calendar_link` or `resolve_share_link` instead of guessing ids. Use `get_meeting_by_calendar_id` for the recording linked to an event; an event without a recording is not missing meeting content. `get_upcoming_meeting` is for future events. For `list_meeting_series`, \"no earlier occurrence\" can mean a one-off meeting or a recording without a linked calendar event, not a failure.\n- `search_meetings` matches titles, summaries and notes, not transcripts. Set `field` to `title`, `content` or `both`; use `query`, `since` and `until` to narrow the request. `attendee_emails` with `attendee_match` set to `any` or `all` only finds recordings linked to calendar events. A missing attendee-filter match does not prove a person never attended. Results include an excerpt and up to five attendees; retrieve the meeting for full content and attendees.\n- List and search tools page with `cursor` and `limit`, normally 25 items and at most 200. Pass the returned `next_cursor` verbatim while `has_more` is true. `list_upcoming_meetings` allows at most 50 items and `window_hours` defaults to 24 with a maximum of 168. A `truncated` result needs a narrower query, even after paging. Reduce results inside `execute_code` before returning them.\n- Notes and transcripts are character-bounded. Read the continuation marker and repeat with `view_content.start_char` or `view_transcript.start_char` set to its exact offset. Request `view_transcript: {}` when source detail matters, then continue through the required ranges. Enterprise note and transcript reads are recorded in audit logs; do not read transcripts speculatively. Summaries are complete. Transcript retention deletes transcripts and audio, not summaries; an unavailable transcript may have expired.\n- Shared-note links require your own meeting-access permissions. If a link copied from email is rejected, ask the user to open it in a browser and pass the final Wispr Flow URL. Transcripts require ownership or an explicit email invitation plus the owner's organization allowing transcript sharing. Domain, team or anyone-with-link access yields summaries only. Reauthorization cannot widen those permissions. Return the provided `share_link` when asked for a link; never fabricate one or assume the link changes who can read it.\n- Timestamps are UTC. Present them in the user's time zone. For a whole local day, supply `since` at local midnight with its UTC offset and `until` at the next local midnight; the upper bound is exclusive. Upcoming pre-reads may be absent because the desktop app generates them on demand; these reads do not generate them.\n- Both official help articles describe read-only access and list tasks or action items as readable, while Wispr's own Claude directory listing advertises \"Create, update, and search action items\" and names `add_task` and `update_task`. Discover this account's actual tools before using them. If exposed, `get_task` and `search_tasks` read, `add_task` creates a new task and is a write, and `update_task` changes existing task state and is destructive. Unknown tools without an explicit, uncontradicted read annotation fail closed.\n- Mac and Windows accounts need Notetaker and MCP access. Personal or organization HIPAA BAA coverage removes all MCP tools. Enterprise administrators can disable MCP or require an approved IP network; Scratchpad and cloud-access policies may further restrict results. An empty catalog or policy denial requires checking account access, not repeated authorization.\n- For \"Too many requests\" or repeated searches that temporarily fail without explanation, wait about one minute before retrying and avoid speculative fan-out. There is no maintained per-runtime admission default; the operator may configure `callAdmission` for this deployment.\n- An `auth_required` failure means this connector's Wispr Flow authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. For guest calls, classification and routing, fetch `skills({ name: \"usage\" })`.\n",
|
|
4
|
+
"instructionsHeading": "Account instructions",
|
|
5
|
+
"fragments": {
|
|
6
|
+
"content": "- This connection reaches Notetaker meetings, summaries, notes, transcripts, participants and their email addresses, and tasks; Scratchpad notes; calendar events and linked recordings; upcoming meetings with available pre-reads; recurring series; accessible shared notes; and account identity. Meeting results name folders you own or that are shared with you; meetings outside folders list none. Another account needs another authorization. Use `get_account_info` to check which identity this grant reaches.\n- OAuth browser authorization is the only supported auth method. There is no API key. Prefer Google, Apple, Microsoft or enterprise SSO and finish sign-in within about five minutes. Email-and-password sign-in may not complete authorization; Wispr's help pages disagree. Consent covers the account's existing content, without per-meeting consent.\n- Search first, then retrieve by the returned id: `search_meetings` → `get_meeting`, `search_scratchpad_notes` → `get_scratchpad_note`, or `search_calendar_events` → `get_calendar_event`. Resolve pasted URLs with `resolve_calendar_link` or `resolve_share_link` instead of guessing ids. Use `get_meeting_by_calendar_id` for the recording linked to an event; an event without a recording is not missing meeting content. `get_upcoming_meeting` is for future events. For `list_meeting_series`, \"no earlier occurrence\" can mean a one-off meeting or a recording without a linked calendar event, not a failure.\n- `search_meetings` matches titles, summaries and notes, not transcripts. Set `field` to `title`, `content` or `both`; use `query`, `since` and `until` to narrow the request. `attendee_emails` with `attendee_match` set to `any` or `all` only finds recordings linked to calendar events. A missing attendee-filter match does not prove a person never attended. Results include an excerpt and up to five attendees; retrieve the meeting for full content and attendees.\n- List and search tools page with `cursor` and `limit`, normally 25 items and at most 200. Pass the returned `next_cursor` verbatim while `has_more` is true. `list_upcoming_meetings` allows at most 50 items and `window_hours` defaults to 24 with a maximum of 168. A `truncated` result needs a narrower query, even after paging. Reduce results inside `execute_code` before returning them.\n- Notes and transcripts are character-bounded. Read the continuation marker and repeat with `view_content.start_char` or `view_transcript.start_char` set to its exact offset. Request `view_transcript: {}` when source detail matters, then continue through the required ranges. Enterprise note and transcript reads are recorded in audit logs; do not read transcripts speculatively. Summaries are complete. Transcript retention deletes transcripts and audio, not summaries; an unavailable transcript may have expired.\n- Shared-note links require your own meeting-access permissions. If a link copied from email is rejected, ask the user to open it in a browser and pass the final Wispr Flow URL. Transcripts require ownership or an explicit email invitation plus the owner's organization allowing transcript sharing. Domain, team or anyone-with-link access yields summaries only. Reauthorization cannot widen those permissions. Return the provided `share_link` when asked for a link; never fabricate one or assume the link changes who can read it.\n- Timestamps are UTC. Present them in the user's time zone. For a whole local day, supply `since` at local midnight with its UTC offset and `until` at the next local midnight; the upper bound is exclusive. Upcoming pre-reads may be absent because the desktop app generates them on demand; these reads do not generate them.\n- Both official help articles describe read-only access and list tasks or action items as readable, while Wispr's own Claude directory listing advertises \"Create, update, and search action items\" and names `add_task` and `update_task`. Discover this account's actual tools before using them. If exposed, `get_task` and `search_tasks` read, `add_task` creates a new task and is a write, and `update_task` changes existing task state and is destructive. Unknown tools without an explicit, uncontradicted read annotation fail closed.\n- Mac and Windows accounts need Notetaker and MCP access. Personal or organization HIPAA BAA coverage removes all MCP tools. Enterprise administrators can disable MCP or require an approved IP network; Scratchpad and cloud-access policies may further restrict results. An empty catalog or policy denial requires checking account access, not repeated authorization.\n- For \"Too many requests\" or repeated searches that temporarily fail without explanation, wait about one minute before retrying and avoid speculative fan-out. There is no maintained per-runtime admission default; the operator may configure `callAdmission` for this deployment.\n- An `auth_required` failure means this connector's Wispr Flow authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. For guest calls, classification and routing, fetch `skills({ name: \"usage\" })`.\n"
|
|
7
|
+
}
|
|
8
|
+
};
|
package/dist/routes/mcp.js
CHANGED
|
@@ -294,7 +294,7 @@ function toolkitRetired(logger, sentSecrets) {
|
|
|
294
294
|
},
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
|
-
function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, registry, canManageAuth, principalKey, runtimeContext,
|
|
297
|
+
function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, registry, canManageAuth, principalKey, runtimeContext, readOnly = false, pool, principalActor) {
|
|
298
298
|
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
299
299
|
// Every McpServer the request builds is fresh and closes with its scope.
|
|
300
300
|
// The modern handler tears its own down after the exchange; the legacy
|
|
@@ -361,7 +361,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
361
361
|
client,
|
|
362
362
|
baseUrl,
|
|
363
363
|
requestScope,
|
|
364
|
-
|
|
364
|
+
readOnly,
|
|
365
365
|
canManageAuth,
|
|
366
366
|
oauthConnectUrl: (id, force) => oauthConnectUrl(opts, baseUrl, id, principalKey, force),
|
|
367
367
|
oauthConnectUnavailable: oauthConnectUnavailable(opts),
|
|
@@ -385,7 +385,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
385
385
|
requestSignal,
|
|
386
386
|
...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
|
|
387
387
|
...executeLimits(opts.config),
|
|
388
|
-
|
|
388
|
+
readOnly,
|
|
389
389
|
});
|
|
390
390
|
servers.push(server);
|
|
391
391
|
return server;
|
|
@@ -517,6 +517,8 @@ export function createMcpRoute(opts) {
|
|
|
517
517
|
};
|
|
518
518
|
function routeMcp(context) {
|
|
519
519
|
const { path, request, baseUrl, runtimeContext } = context;
|
|
520
|
+
// Protected-resource metadata stays public, including when a grant is
|
|
521
|
+
// configured, so a fresh OAuth client can discover how to authenticate.
|
|
520
522
|
if (path !== "/mcp" && !path.startsWith("/mcp/"))
|
|
521
523
|
return Effect.succeed(null);
|
|
522
524
|
// Admission, refusal bodies, SDK serialization and connector work share
|
|
@@ -644,7 +646,23 @@ export function createMcpRoute(opts) {
|
|
|
644
646
|
// one identical 404 so a credential never enumerates the other pools;
|
|
645
647
|
// the operator log is where the reason lives.
|
|
646
648
|
let access = authz;
|
|
647
|
-
let
|
|
649
|
+
let readOnly = opts.config.readOnly;
|
|
650
|
+
if (poolName === undefined && opts.config.grant) {
|
|
651
|
+
const denialReason = yield* Effect.promise(async () => {
|
|
652
|
+
try {
|
|
653
|
+
return (await opts.config.grant(authz.identity)) === true
|
|
654
|
+
? undefined
|
|
655
|
+
: "default_grant_denied";
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return "default_grant_threw";
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
if (denialReason !== undefined) {
|
|
662
|
+
logFailure(opts.config.logger, "MCP default endpoint request denied", failureRecord({ reason: denialReason }));
|
|
663
|
+
return cors(mcpRefusal(sentSecrets, 404, -33004, "MCP endpoint not found."));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
648
666
|
if (poolName !== undefined) {
|
|
649
667
|
const pool = opts.pools?.get(poolName);
|
|
650
668
|
const denialReason = !pool
|
|
@@ -662,7 +680,7 @@ export function createMcpRoute(opts) {
|
|
|
662
680
|
return cors(mcpRefusal(sentSecrets, 404, -33004, "MCP endpoint not found."));
|
|
663
681
|
}
|
|
664
682
|
access = intersectAccess(authz, pool.access);
|
|
665
|
-
|
|
683
|
+
readOnly = pool.readOnly;
|
|
666
684
|
}
|
|
667
685
|
let scopedRegistry;
|
|
668
686
|
try {
|
|
@@ -685,21 +703,23 @@ export function createMcpRoute(opts) {
|
|
|
685
703
|
if (current.subjectKey !== authz.subjectKey || current.principalKey !== authz.principalKey)
|
|
686
704
|
return false;
|
|
687
705
|
validateAuthPermissions(current, opts.registry);
|
|
706
|
+
if (poolName === undefined && opts.config.grant && (await opts.config.grant(current.identity)) !== true)
|
|
707
|
+
return false;
|
|
688
708
|
let currentAccess = current;
|
|
689
|
-
let
|
|
709
|
+
let currentReadOnly = opts.config.readOnly;
|
|
690
710
|
if (poolName !== undefined) {
|
|
691
711
|
const pool = opts.pools.get(poolName);
|
|
692
712
|
if (!pool || (await pool.grant(current.identity)) !== true)
|
|
693
713
|
return false;
|
|
694
714
|
currentAccess = intersectAccess(current, pool.access);
|
|
695
|
-
|
|
715
|
+
currentReadOnly = pool.readOnly;
|
|
696
716
|
}
|
|
697
717
|
const view = opts.registry.scoped({
|
|
698
718
|
...currentAccess,
|
|
699
719
|
...(current.subjectKey ? { subjectKey: current.subjectKey } : {}),
|
|
700
720
|
...(current.principalKey ? { principalKey: current.principalKey } : {}),
|
|
701
721
|
});
|
|
702
|
-
if (classification === "write" &&
|
|
722
|
+
if (classification === "write" && currentReadOnly)
|
|
703
723
|
return false;
|
|
704
724
|
if (address === undefined)
|
|
705
725
|
return true;
|
|
@@ -716,7 +736,7 @@ export function createMcpRoute(opts) {
|
|
|
716
736
|
return Boolean(tool &&
|
|
717
737
|
(access === "catalog" ||
|
|
718
738
|
(classification === "read" && tool.classification === "read") ||
|
|
719
|
-
|
|
739
|
+
!currentReadOnly));
|
|
720
740
|
}
|
|
721
741
|
finally {
|
|
722
742
|
await closeConnectorScope(resolved.connector, view.contextFor(resolved.connector.id, baseUrl, requestScope), runtimeContext?.waitUntil?.bind(runtimeContext));
|
|
@@ -740,7 +760,7 @@ export function createMcpRoute(opts) {
|
|
|
740
760
|
return cors(yield* serveMcp(localRequest, localAbort.signal, requestScope, opts, baseUrl, authz.actor, scopedRegistry, (id) => {
|
|
741
761
|
const connector = scopedRegistry.getConnector(id);
|
|
742
762
|
return Boolean(connector && mayManageConnector(authz, connector));
|
|
743
|
-
}, authz.principalKey, runtimeContext,
|
|
763
|
+
}, authz.principalKey, runtimeContext, readOnly, poolName, authz.identity.principal
|
|
744
764
|
? {
|
|
745
765
|
kind: authz.actor.kind,
|
|
746
766
|
id: authz.identity.principal.id,
|
package/dist/routes/operator.js
CHANGED
|
@@ -62,13 +62,20 @@ export function scopeFor(authz, connectorIds = authz.connectorIds) {
|
|
|
62
62
|
* included, rather than quietly granting what remains.
|
|
63
63
|
*/
|
|
64
64
|
export function visibleRegistry({ opts }, authz) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
// Operator catalogs describe the default endpoint's view, never a pool view.
|
|
66
|
+
// Authentication alone cannot admit a caller that deployment code refuses.
|
|
67
|
+
const admission = opts.config.grant
|
|
68
|
+
? Effect.tryPromise(async () => (await opts.config.grant(authz.identity)) === true).pipe(Effect.orElseSucceed(() => false))
|
|
69
|
+
: Effect.succeed(true);
|
|
70
|
+
return Effect.flatMap(admission, (allowed) => allowed
|
|
71
|
+
? Effect.try({
|
|
72
|
+
try: () => {
|
|
73
|
+
validateAuthPermissions(authz, opts.registry);
|
|
74
|
+
return opts.registry.scoped(scopeFor(authz));
|
|
75
|
+
},
|
|
76
|
+
catch: (error) => new Answer(privateJson({ error: msg(error) }, { status: 403 })),
|
|
77
|
+
})
|
|
78
|
+
: refuse("not found", 404));
|
|
72
79
|
}
|
|
73
80
|
/**
|
|
74
81
|
* Run one route program to its Response.
|
package/dist/routes/ui-config.js
CHANGED
|
@@ -154,7 +154,7 @@ function configRead(context) {
|
|
|
154
154
|
pools.push({
|
|
155
155
|
name,
|
|
156
156
|
path: `/mcp/${name}`,
|
|
157
|
-
|
|
157
|
+
readOnly: pool.readOnly,
|
|
158
158
|
grants: grants(intersectAccess(authz, pool.access), ids),
|
|
159
159
|
});
|
|
160
160
|
}
|
|
@@ -199,7 +199,7 @@ function configRead(context) {
|
|
|
199
199
|
you: {
|
|
200
200
|
interactive: authz.identity.interactive,
|
|
201
201
|
grants: grants(authz, ids),
|
|
202
|
-
|
|
202
|
+
readOnly: opts.config.readOnly,
|
|
203
203
|
pools,
|
|
204
204
|
permissions: {
|
|
205
205
|
activity: activity === "available" || activity === "unavailable",
|
package/dist/server.js
CHANGED
|
@@ -88,6 +88,9 @@ export function createFetchHandler(opts) {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
if (path.startsWith("/.well-known/")) {
|
|
91
|
+
const guardedMetadata = yield* routeMcp.handle(context);
|
|
92
|
+
if (guardedMetadata)
|
|
93
|
+
return guardedMetadata;
|
|
91
94
|
const response = yield* metadata(request, baseUrl);
|
|
92
95
|
if (response)
|
|
93
96
|
return response;
|
package/dist/skills.js
CHANGED
|
@@ -7,11 +7,12 @@ import { sentSecretsForRequest } from "./sent-secrets.js";
|
|
|
7
7
|
const ROUTE = "Choose a route before discovery. One known-address read uses call_tool; one known-address write uses call_destructive_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.";
|
|
8
8
|
const RECOVERY = 'When an auth error includes a handoff, give it to the user or operator. Use authorize_connector for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Fetch skills({ name: "usage" }) only for missing guidance or program repair.';
|
|
9
9
|
/**
|
|
10
|
-
* The always-loaded MCP `instructions` string.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The always-loaded MCP `instructions` string. An endpoint is writable or
|
|
11
|
+
* read-only: a writable one lets programs and call_destructive_tool write,
|
|
12
|
+
* and the client owns approval; a read-only one omits write tools from search
|
|
13
|
+
* and describe and refuses every write call. Guides may still name them.
|
|
13
14
|
*/
|
|
14
|
-
export const CONNECTA_INSTRUCTIONS = `${ROUTE}
|
|
15
|
+
export const CONNECTA_INSTRUCTIONS = `${ROUTE} On a writable endpoint, programs and call_destructive_tool may write. On a read-only endpoint, search omits write tools and every write call is refused; guides may still name them. The execute_code description says which this endpoint is. Never repeat a write to recover its output. ${RECOVERY}`;
|
|
15
16
|
export { USAGE_SKILL } from "./usage-guide.js";
|
|
16
17
|
/** True when at least one of `connectors` carries a usage guide. */
|
|
17
18
|
export function hasConnectorGuides(connectors) {
|
package/dist/tool-safety.js
CHANGED
|
@@ -14,9 +14,17 @@ export function classifyTool(definition, override, review) {
|
|
|
14
14
|
? "read"
|
|
15
15
|
: "write";
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The only permission decision for a connector call. Approval is client-owned.
|
|
19
|
+
* A read-only endpoint refuses every write on every surface; a writable one
|
|
20
|
+
* sends writes through call_destructive_tool and programs, never call_tool.
|
|
21
|
+
*/
|
|
22
|
+
export function writeRefusal(classification, surface, readOnly = false) {
|
|
23
|
+
if (classification === "read")
|
|
24
|
+
return undefined;
|
|
25
|
+
if (readOnly)
|
|
26
|
+
return "read_only_endpoint";
|
|
27
|
+
return surface === "call_destructive_tool" || surface === "execute_code"
|
|
28
|
+
? undefined
|
|
29
|
+
: "destructive_tool_requires_approval";
|
|
22
30
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -95,7 +95,7 @@ export interface ToolDef {
|
|
|
95
95
|
* Standard MCP tool behavior hints plus provider-specific extensions.
|
|
96
96
|
* The registry classifies these after deployment overrides and provider
|
|
97
97
|
* review. Only a stored read verdict may use call_tool; program writes
|
|
98
|
-
* require a
|
|
98
|
+
* require a writable endpoint. Approval belongs to the host.
|
|
99
99
|
*/
|
|
100
100
|
annotations?: ToolAnnotations;
|
|
101
101
|
}
|
package/dist/ui.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, i
|
|
|
9
9
|
export type OperatorPage = import("./operator-ui/view.js").OperatorPage;
|
|
10
10
|
export declare function operatorPageForPath(path: string): OperatorPage | undefined;
|
|
11
11
|
export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
|
|
12
|
-
/** Report the verdict the registry stored, independent of
|
|
12
|
+
/** Report the verdict the registry stored, independent of whether an endpoint is read-only. */
|
|
13
13
|
export declare function uiToolSafety(definition: ToolDef): UiToolSafety;
|
|
14
14
|
/**
|
|
15
15
|
* The fix-prompt key for a connector that is not usable. Chosen from the
|
package/dist/ui.js
CHANGED
|
@@ -40,7 +40,7 @@ export function operatorPageForPath(path) {
|
|
|
40
40
|
export function operatorPageTitle(page, configuredTitle) {
|
|
41
41
|
return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
|
|
42
42
|
}
|
|
43
|
-
/** Report the verdict the registry stored, independent of
|
|
43
|
+
/** Report the verdict the registry stored, independent of whether an endpoint is read-only. */
|
|
44
44
|
export function uiToolSafety(definition) {
|
|
45
45
|
if (definition.classification === "read")
|
|
46
46
|
return "runs_in_programs";
|
package/dist/usage-guide.js
CHANGED
|
@@ -16,9 +16,9 @@ description: How to route work between one execute_code program and Connecta's e
|
|
|
16
16
|
|
|
17
17
|
Read one account with call_tool: \`{ "address": "crm.get_account", "args": { "id": "acct_42" } }\`.
|
|
18
18
|
Discover and call together when schemas suffice. search_tools covers catalog
|
|
19
|
-
inspection
|
|
20
|
-
|
|
21
|
-
Unannotated tools fail closed to writes.
|
|
19
|
+
inspection. On a read-only endpoint, as execute_code's description says, search
|
|
20
|
+
omits writes (guides may name them) and each fails read_only_endpoint; tell the
|
|
21
|
+
user. Unannotated tools fail closed to writes.
|
|
22
22
|
|
|
23
23
|
## The global and the envelopes
|
|
24
24
|
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONNECTA_VERSION = "0.
|
|
1
|
+
export declare const CONNECTA_VERSION = "0.32.0";
|
package/dist/version.js
CHANGED