@zackbart/connecta 0.34.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -2
  3. package/bin/version.mjs +1 -1
  4. package/dist/catalog-drift.d.ts +30 -26
  5. package/dist/catalog-drift.js +169 -84
  6. package/dist/catalog-service.js +1 -1
  7. package/dist/connectors/api-connector.d.ts +3 -0
  8. package/dist/connectors/api-connector.js +5 -1
  9. package/dist/connectors/option-shapes.js +1 -1
  10. package/dist/connectors/remote-mcp.d.ts +2 -0
  11. package/dist/connectors/remote-mcp.js +5 -1
  12. package/dist/describe-config.d.ts +2 -0
  13. package/dist/describe-config.js +1 -0
  14. package/dist/invocation.js +3 -2
  15. package/dist/meta-tools.js +3 -2
  16. package/dist/operator-ui/config-summary.js +3 -0
  17. package/dist/operator-ui/connection-map.js +2 -1
  18. package/dist/operator-ui/endpoints.js +1 -1
  19. package/dist/operator-ui/generated.js +3 -3
  20. package/dist/operator-ui/health.js +52 -0
  21. package/dist/operator-ui/overview.js +6 -17
  22. package/dist/operator-ui/view.d.ts +3 -4
  23. package/dist/operator-ui/view.js +29 -11
  24. package/dist/provider.d.ts +6 -1
  25. package/dist/provider.js +18 -1
  26. package/dist/providers/_shared/google/workspace.d.ts +3 -0
  27. package/dist/providers/_shared/google/workspace.js +1 -1
  28. package/dist/providers/basecamp/index.d.ts +2 -0
  29. package/dist/providers/breeze/index.d.ts +2 -0
  30. package/dist/providers/ccb/index.d.ts +2 -0
  31. package/dist/providers/ccb/index.js +1 -1
  32. package/dist/providers/cloudflare/index.d.ts +2 -0
  33. package/dist/providers/cloudflare/index.js +5 -1
  34. package/dist/providers/cloudflare/openapi.generated.js +3296 -3206
  35. package/dist/providers/cloudflare/rest.js +19 -0
  36. package/dist/providers/cloudflare/skill.generated.js +2 -2
  37. package/dist/providers/cloudflare/value-safety.js +51 -18
  38. package/dist/providers/intercom/index.d.ts +24 -0
  39. package/dist/providers/intercom/index.js +121 -0
  40. package/dist/providers/intercom/skill.generated.js +8 -0
  41. package/dist/providers/linear/index.js +204 -19
  42. package/dist/providers/linear/skill.generated.js +2 -2
  43. package/dist/providers/mixpanel/index.d.ts +2 -0
  44. package/dist/providers/mixpanel/index.js +21 -17
  45. package/dist/providers/mixpanel/skill.generated.js +1 -1
  46. package/dist/providers/notion/index.d.ts +2 -0
  47. package/dist/providers/notion/index.js +1 -1
  48. package/dist/providers/overflow/index.d.ts +2 -0
  49. package/dist/providers/planning-center/index.d.ts +2 -0
  50. package/dist/providers/revenuecat/index.d.ts +5 -3
  51. package/dist/providers/revenuecat/index.js +147 -7
  52. package/dist/providers/revenuecat/skill.generated.js +3 -3
  53. package/dist/providers/stripe/index.d.ts +2 -0
  54. package/dist/providers/stripe/index.js +50 -30
  55. package/dist/providers/stripe/skill.generated.js +2 -1
  56. package/dist/providers/tithely/index.d.ts +2 -0
  57. package/dist/providers/vercel/index.d.ts +2 -0
  58. package/dist/providers/vercel/index.js +1 -1
  59. package/dist/providers/vercel/openapi.generated.js +391 -374
  60. package/dist/providers/vercel/skill.generated.js +1 -1
  61. package/dist/providers/vercel/value-safety.js +14 -4
  62. package/dist/registry.d.ts +10 -14
  63. package/dist/registry.js +62 -62
  64. package/dist/routes/ui-data.js +8 -2
  65. package/dist/skills.js +8 -2
  66. package/dist/types.d.ts +18 -4
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/documentation/architecture.md +48 -5
  70. package/documentation/code-mode.md +7 -5
  71. package/documentation/integrating.md +55 -1
  72. package/documentation/meta-tools.md +2 -2
  73. package/documentation/operating.md +1 -1
  74. package/documentation/operator-tests.md +8 -2
  75. package/documentation/operator-ui.md +58 -11
  76. package/documentation/provider-auth-migration.md +22 -0
  77. package/package.json +5 -1
  78. package/templates/node/package.json +1 -1
@@ -5,7 +5,7 @@
5
5
  // path instead of silently doing nothing (INV-11). Values stay the factory's
6
6
  // to validate; handlers, schemas, and loggers pass through as `opaque()`.
7
7
  import { array, keys, optionsOf, strings, variants } from "../config-schema.js";
8
- export { CALL_ADMISSION, PROVIDER_COMMON } from "../provider.js";
8
+ export { READ_ONLY, CALL_ADMISSION, PROVIDER_COMMON } from "../provider.js";
9
9
  export const CREDENTIAL = optionsOf()({
10
10
  ...keys("label", "description", "placeholder"),
11
11
  fields: array(optionsOf()(keys("name", "label", "description", "placeholder", "inputType"))),
@@ -82,6 +82,8 @@ export interface RemoteMcpOptions {
82
82
  * construct.
83
83
  */
84
84
  maxResultBytes?: number;
85
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
86
+ readOnly?: boolean;
85
87
  /** Optional per-runtime downstream call-admission policy. */
86
88
  callAdmission?: ConnectorCallAdmissionPolicy;
87
89
  /**
@@ -24,7 +24,7 @@ import { retryAfterMs } from "./guarded-fetch.js";
24
24
  import { resourceUriMatchesTemplates } from "./resource-uri.js";
25
25
  import { callerOf } from "../connector-caller.js";
26
26
  import { readNegotiation, storeNegotiation } from "./negotiation-cache.js";
27
- import { CALL_ADMISSION, REMOTE_MCP_AUTH, USAGE_GUIDE } from "./option-shapes.js";
27
+ import { READ_ONLY, CALL_ADMISSION, REMOTE_MCP_AUTH, USAGE_GUIDE } from "./option-shapes.js";
28
28
  /**
29
29
  * Apply a maintained provider's slot copy and header framing to credential
30
30
  * auth the deployment left bare.
@@ -46,6 +46,7 @@ export function withCredentialDefaults(auth, defaults) {
46
46
  /** The closed options remoteMcp() accepts; see `assertKnownOptions`. */
47
47
  const REMOTE_MCP_OPTIONS = optionsOf()({
48
48
  ...keys("url", "title", "description", "authScope", "maxResultBytes", "versionNegotiation", "redirects", "requireHttps", "logger", "_transportFactory", "classify", "skills"),
49
+ readOnly: READ_ONLY,
49
50
  callAdmission: CALL_ADMISSION,
50
51
  usageGuide: USAGE_GUIDE,
51
52
  auth: REMOTE_MCP_AUTH,
@@ -1024,6 +1025,8 @@ function registrationErrorCode(body) {
1024
1025
  */
1025
1026
  export function remoteMcp(id, opts) {
1026
1027
  opts = assertKnownOptions(opts, `remoteMcp(${JSON.stringify(id)})`, REMOTE_MCP_OPTIONS);
1028
+ if (opts.readOnly !== undefined)
1029
+ READ_ONLY.parse(opts.readOnly, `Connector(${JSON.stringify(id)}).readOnly`);
1027
1030
  if (opts.callAdmission !== undefined)
1028
1031
  CALL_ADMISSION.parse(opts.callAdmission, `remoteMcp(${JSON.stringify(id)}).callAdmission`);
1029
1032
  const skillsEnabled = opts.skills;
@@ -2194,6 +2197,7 @@ export function remoteMcp(id, opts) {
2194
2197
  },
2195
2198
  }),
2196
2199
  ...(opts.description !== undefined ? { description: opts.description } : {}),
2200
+ ...(opts.readOnly !== undefined ? { readOnly: opts.readOnly } : {}),
2197
2201
  ...(opts.authScope !== undefined ? { authScope: opts.authScope } : {}),
2198
2202
  ...(opts.maxResultBytes !== undefined ? { maxResultBytes: opts.maxResultBytes } : {}),
2199
2203
  ...(opts.callAdmission !== undefined ? { callAdmission: opts.callAdmission } : {}),
@@ -20,6 +20,8 @@ export type DescribedMember = {
20
20
  };
21
21
  /** One connector as `describeConfig()` reports it. */
22
22
  export interface DescribedConnector {
23
+ /** Connector policy, independent of endpoint policy; omitted when false. */
24
+ readOnly?: boolean;
23
25
  id: string;
24
26
  title?: string;
25
27
  description?: string;
@@ -133,6 +133,7 @@ function describeConnector(connector, deploymentCap, registry) {
133
133
  const guideSummary = connectorGuideSummary(connector);
134
134
  const guided = connector.usageGuide !== undefined;
135
135
  return {
136
+ ...(connector.readOnly === true ? { readOnly: true } : {}),
136
137
  id: connector.id,
137
138
  ...(str(connector.title) !== undefined ? { title: connector.title } : {}),
138
139
  ...(str(connector.description) !== undefined ? { description: connector.description } : {}),
@@ -464,9 +464,10 @@ export class InvocationService {
464
464
  reconciliationRequired: true,
465
465
  };
466
466
  }
467
- const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly);
467
+ const connectorReadOnly = target.connector.readOnly === true;
468
+ const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly || connectorReadOnly);
468
469
  if (refusal === "read_only_endpoint") {
469
- return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and this endpoint is read-only. Nothing here can send it.`);
470
+ return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and ${connectorReadOnly ? `connector "${target.connector.id}"` : "this endpoint"} is read-only. Nothing here can send it.`);
470
471
  }
471
472
  if (refusal === "destructive_tool_requires_approval") {
472
473
  return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
@@ -796,14 +796,15 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
796
796
  return false;
797
797
  continue;
798
798
  }
799
- if (!registry.getConnector(dependency.connector))
799
+ const connector = registry.getConnector(dependency.connector);
800
+ if (!connector)
800
801
  return false;
801
802
  const tools = await registry.getTools(dependency.connector, baseUrl, requestScope, options.signal ? { signal: options.signal } : {});
802
803
  const tool = tools.find((tool) => tool.name === dependency.tool);
803
804
  if (!tool ||
804
805
  !(dependency.access === "catalog" ||
805
806
  (dependency.classification === "read" && tool.classification === "read") ||
806
- opts.readOnly !== true))
807
+ (opts.readOnly !== true && connector.readOnly !== true)))
807
808
  return false;
808
809
  }
809
810
  return true;
@@ -401,6 +401,7 @@ const AUTH_MODE = {
401
401
  * catalog. Anything else becomes a tile, so a new field is never hidden.
402
402
  */
403
403
  const CONNECTOR_KNOWN = [
404
+ "readOnly",
404
405
  "id",
405
406
  "title",
406
407
  "description",
@@ -488,6 +489,8 @@ export function connectorSummary(connector, sources = {}) {
488
489
  const base = `config.connectors.${connector.id}`;
489
490
  const kind = SOURCE_KIND[connector.source.kind] ?? SOURCE_KIND.custom;
490
491
  const badges = [{ id: "kind", label: kind.label, icon: kind.icon }];
492
+ if (connector.readOnly)
493
+ badges.push({ id: "readOnly", label: "Read-only" });
491
494
  if (connector.source.provider)
492
495
  badges.push({ id: "provider", label: connector.source.provider, icon: "box" });
493
496
  badges.push(connector.authScope === "personal"
@@ -210,7 +210,8 @@ export function connectionMap(contract, options = {}) {
210
210
  const maintainers = new Set((contract.root?.maintainers ?? []).flatMap((member) => member.kind === "human" ? [`person:${member.namespace}:${member.id}`] : []));
211
211
  const connectors = (contract.role === "maintainer" ? (contract.config?.connectors ?? []) : []).flatMap((config) => {
212
212
  const live = contract.live.connectors.find((c) => c.id === config.id);
213
- return live ? [{ config, live, health: connectorVerdict(live, options.signals?.[config.id]) }] : [];
213
+ const signals = options.signals && Object.hasOwn(options.signals, config.id) ? options.signals[config.id] : undefined;
214
+ return live ? [{ config, live, health: connectorVerdict(live, signals) }] : [];
214
215
  });
215
216
  const health = new Map(connectors.map(({ config, health }) => [config.id, health]));
216
217
  const nodes = [];
@@ -63,7 +63,7 @@ function endpointGroups(contract, grants, readOnly, pool) {
63
63
  if (!live || !grant)
64
64
  return [];
65
65
  const tools = live.tools.filter((tool) => {
66
- if (readOnly && tool.classification !== "read")
66
+ if ((readOnly || config.readOnly) && tool.classification !== "read")
67
67
  return false;
68
68
  return (grant.tools === "all" ||
69
69
  grant.tools.some((g) => g.name === tool.name && (!g.requireReadOnly || tool.classification === "read")));