@zackbart/connecta 0.16.1 → 0.17.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 +102 -0
- package/dist/catalog-service.d.ts +4 -0
- package/dist/catalog-service.js +8 -1
- package/dist/catalog.js +114 -12
- package/dist/errors.d.ts +4 -6
- package/dist/execute.d.ts +5 -0
- package/dist/execute.js +229 -161
- package/dist/invocation.js +3 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.js +46 -14
- package/dist/operator-ui/generated.d.ts +1 -1
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -1
- package/dist/providers/mixpanel.d.ts +3 -5
- package/dist/providers/mixpanel.js +73 -5
- package/dist/providers/stripe.d.ts +2 -2
- package/dist/providers/stripe.js +13 -11
- package/dist/registry.d.ts +32 -9
- package/dist/registry.js +217 -33
- package/dist/routes/mcp.js +6 -0
- package/dist/skills.d.ts +4 -0
- package/dist/skills.js +157 -18
- package/dist/types.d.ts +14 -2
- package/dist/ui.js +4 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +7 -4
- package/documentation/code-mode.md +45 -53
- package/documentation/connector-guides.md +24 -19
- package/documentation/connectors.md +13 -1
- package/documentation/meta-tools.md +26 -17
- package/documentation/mixpanel.md +20 -0
- package/documentation/operations.md +21 -18
- package/documentation/operator-ui.md +12 -2
- package/documentation/provider-audit.md +3 -3
- package/documentation/provider-conventions.md +26 -13
- package/documentation/stripe.md +45 -14
- package/documentation/upgrading.md +28 -4
- package/ethos.md +3 -3
- package/examples/worker/README.md +4 -3
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
|
@@ -79,14 +79,78 @@ const WRITE_TOOLS = new Map([
|
|
|
79
79
|
["Create-Feature-Flag", "additive"],
|
|
80
80
|
["Update-Feature-Flag", "destructive"],
|
|
81
81
|
]);
|
|
82
|
+
/** Live US hosted-MCP schemas reviewed read-only on 2026-08-13 (#395). */
|
|
83
|
+
const MIXPANEL_SCHEMA_DIGESTS = {
|
|
84
|
+
"Bulk-Edit-Events": "sha256:94512138df153de95436371f5a313e32b97faffebda3be6c0c69d09d1ff2b340",
|
|
85
|
+
"Bulk-Edit-Properties": "sha256:93481eb896ded45b623c678612e2156439efbaad5b4d660da1f2b087e7409ee3",
|
|
86
|
+
"Create-Cohort": "sha256:ea687f4bc607f8bfb5b1949e060239d6f7d629d3ec80fb0b72eae6cc6156ed03",
|
|
87
|
+
"Create-Custom-Property": "sha256:d22818b812f3fd0e785a5bbbee26cf099eaa911d5b6f383f94cf458a2ed26294",
|
|
88
|
+
"Create-Dashboard": "sha256:bd3b6e5134d27dafb04ccdf04d19e54dd23e6e2dc16989e418006e484268037b",
|
|
89
|
+
"Create-Experiment": "sha256:3307bd7202f5483adbc28a9ef1d562095d6942eb2c3a15308f458072c93e6888",
|
|
90
|
+
"Create-Feature-Flag": "sha256:3460d11d726727349cac62db5c19915ab589b7f1d8387042f0a3783356b252c4",
|
|
91
|
+
"Create-Lookup-Table": "sha256:c2fefa33a4f19fc65f394fecdc7126cab0ae874f7c97018cf003d8462a049e4c",
|
|
92
|
+
"Create-Metric": "sha256:315766d0c197d87bb44aedd16732f6e69fbfc93661d1fcfc177cadc1751f26ce",
|
|
93
|
+
"Create-Tag": "sha256:f99b6faad422aa6142d20d0aa4bdec1074e96a8ec45e35573e86d84b01df1d3b",
|
|
94
|
+
"Delete-Cohort": "sha256:e18c500459edd9c2b6a614a3f6c97b668786be9abf72929a0a50086bdadb1dae",
|
|
95
|
+
"Delete-Dashboard": "sha256:3a04594218b491743240e6970f677ed8ed132237d933efe30e97bdc41eb1ab6f",
|
|
96
|
+
"Delete-Tag": "sha256:5f6cc58ada5d2796f124e15883fe05061d03f9129414f4c8e10a0ec463f17807",
|
|
97
|
+
"Describe-Cohort-Schema": "sha256:775dbb8bb71ad3fa39ba35ca3a2683dfd88120f7af84a968531609c5a12de537",
|
|
98
|
+
"Dismiss-Duplicate-Group": "sha256:4e39c6f9bb83ed54020097f6cf34507a5288de2c1ce333456c84682bb5986bc4",
|
|
99
|
+
"Dismiss-Issues": "sha256:9c1f4901eca53f58d16feee126868d23c6ed1f5fad72344f86ea9965385b8644",
|
|
100
|
+
"Display-Query": "sha256:d0776167b8a20898c1ff580ca9517da8dea6b827c64e10b9fa01ae1a3c2db95f",
|
|
101
|
+
"Duplicate-Dashboard": "sha256:9738db7495d29034df966423df3c9d4b16ac3a8254c8473bf1e85a467fe9cf4e",
|
|
102
|
+
"Edit-Event": "sha256:28c47da0b56f57da0f11eb11435441a39ecd5175600f854442744c66a379ef93",
|
|
103
|
+
"Edit-Property": "sha256:11975587d2a566edc468db7cb24df70167d12068f30c1fe52c84f543cf4e2399",
|
|
104
|
+
"Explain-Experiment-Health-Check": "sha256:a50b1263247012dc78521bf6c1dd18584a7b08d46221a444b1ca49c81d3286e5",
|
|
105
|
+
"Find-Duplicate-Groups": "sha256:8f944cef9a147eab3c51bfcf1b873cef1409e5a6c21565d34e7ed20d23dcebb7",
|
|
106
|
+
"Get-Business-Context": "sha256:309f5ba864c90132061096b4f0ebc1e78544a6486bac3a45bd661d16bcab2b6c",
|
|
107
|
+
"Get-Cohort": "sha256:e18c500459edd9c2b6a614a3f6c97b668786be9abf72929a0a50086bdadb1dae",
|
|
108
|
+
"Get-Custom-Property": "sha256:d1cbce906cee66d422846c8ef1a745bbeac2c058ecad6b1ee8dcdd2120fa86be",
|
|
109
|
+
"Get-Dashboard": "sha256:56659fa3277723b75e24f3b4ca142ac9cd62120fb97e6c9252c221d00cdf8509",
|
|
110
|
+
"Get-Events": "sha256:d206fe30ab47d64bf6090bffdb4d119a80cfc2a35963bbe7fd52d56060090ded",
|
|
111
|
+
"Get-Experiment": "sha256:f9f04a2bb136809c754ea612963e1cdfb57c2c9ecc34a53ac6e1730f8758798f",
|
|
112
|
+
"Get-Experiment-Results-Interpretation-Guidance": "sha256:e517d9759f66f4a7ad15b0b67ffaabfb605e80949afcf41d007dfafd8f3e9574",
|
|
113
|
+
"Get-Experiment-Setup-Guidance": "sha256:e517d9759f66f4a7ad15b0b67ffaabfb605e80949afcf41d007dfafd8f3e9574",
|
|
114
|
+
"Get-Feature-Flag": "sha256:129de58b379ffba272bdbcee2b5759262e586dd415edc2067a69e9844d4d0877",
|
|
115
|
+
"Get-Feature-Flag-Lifecycle-Guidance": "sha256:e517d9759f66f4a7ad15b0b67ffaabfb605e80949afcf41d007dfafd8f3e9574",
|
|
116
|
+
"Get-Feature-Flag-Setup-Guidance": "sha256:e517d9759f66f4a7ad15b0b67ffaabfb605e80949afcf41d007dfafd8f3e9574",
|
|
117
|
+
"Get-Issues": "sha256:cf172ef18f37504a758c825c7bd4bc23fd98c3421655754dccd8e5ee2d700a10",
|
|
118
|
+
"Get-Lexicon-URL": "sha256:c6456d5c4a38ba069a2caa6cdac651be9d86d47bc6efb3316a893c7336d0e2bc",
|
|
119
|
+
"Get-Lookup-Table": "sha256:061602b283461f083bbd74348c264f802cb21c4fe6d961241c7090895a8334ac",
|
|
120
|
+
"Get-Metric": "sha256:eef10dd9ca25a63a63775565cbc4f2261b289dec526aa33df0f318eefbc96c46",
|
|
121
|
+
"Get-Projects": "sha256:d23d07a777441f04b128f08a2c30a6e8a1573d203f8a1ad7788bc1fd57a9cbbf",
|
|
122
|
+
"Get-Property-Values": "sha256:0cd487c9b92a187b21f456de3c785bb631aa30cba702785c9ff158608883a6bb",
|
|
123
|
+
"Get-Query-Schema": "sha256:8fea929d2ad4f8fed6c47116ff0eaa6f964b3ddd4685e482b68a8dac0231af73",
|
|
124
|
+
"Get-Report": "sha256:a99ba2af1ab57c1d5a606dbd7701968aa430ab23f5969f356a4c41f345dba041",
|
|
125
|
+
"Get-User-Replays-Data": "sha256:5df287735478a67489075b0c7c9d8c07097abe67408d06743c7fb9be43c6e024",
|
|
126
|
+
"List-Cohorts": "sha256:fd04778d60a027cfd777b40f36fbb037ba070661e149b2fce35a14865cada2db",
|
|
127
|
+
"List-Dashboards": "sha256:994c1c9fc87a4f1f00a677b856a44bad028252d283bd8e647ae157683fc89ea2",
|
|
128
|
+
"List-Experiments": "sha256:26a3f3e0b4e52135d74335dd769103303071fc48c15b1df40efd2dfa5a353848",
|
|
129
|
+
"List-Feature-Flags": "sha256:75fc1ff14f3d3faedb8e00c6563d151de397d584429764fd0c332f6a84f6993c",
|
|
130
|
+
"List-Metrics": "sha256:341710bebabb82112e80eb521d9d5cdb3bca024699b3aa795874d3549a315042",
|
|
131
|
+
"List-Organizations": "sha256:e4c23f123dde073e00038d92ebd53f45157c83a4d3a2ff686ab1b66c859f4a11",
|
|
132
|
+
"List-Properties": "sha256:52b38861df076119dfefe0128cf9819868336bf60e090f63c80da734877a69fe",
|
|
133
|
+
"Merge-Group": "sha256:a3b215a5be70305c2f02439513bee76ae3947da14e2a9e5b6ff0afb9ad3d4dcd",
|
|
134
|
+
"Rename-Tag": "sha256:a7e20e79ecd6ca7edca104fa1030e059ffae8962bf084d4a384c012ac9bbf087",
|
|
135
|
+
"Run-Experiment-Pre-Launch-Checks": "sha256:f6b5d85f75450b76fda9c7ef441f190fd45fcaeaff4cc3c30a6d1bee76e4c706",
|
|
136
|
+
"Run-Query": "sha256:0eb7d52343f6137275a02c2d7bb02dafdbfd25bd6f9067395eb689492d835466",
|
|
137
|
+
"Search-Entities": "sha256:86cfa1d3bf7f17a70ad07264d7b1d16c83480e40996e8d847911fe8bde13e35e",
|
|
138
|
+
"Search-Prior-Experiments": "sha256:847d1dcefaf6dc5e7ea967476f464a4d678a06b5e7d437dfb11ad4af2e56620f",
|
|
139
|
+
"Update-Business-Context": "sha256:0d21290053b6d532386c3c83d60cd78013a7109492c02ba2e7822716f2cde027",
|
|
140
|
+
"Update-Cohort": "sha256:ebe0d5afcc00e23eef55f079cd1816c851c2c647dff9565b7045f1f1615663f2",
|
|
141
|
+
"Update-Custom-Property": "sha256:781aaaab41c1ab5958062dc393d02b1df2ef2a09a28d3c4f21ae3177f4f53c86",
|
|
142
|
+
"Update-Dashboard": "sha256:2cc179ba75eda476ba7488f01503d947f023a4460aa98c4806ef0ab10c3b3ce8",
|
|
143
|
+
"Update-Experiment": "sha256:52ebf7595e48a40e218ff463ed72942a4818abc3ce93d8da6fafa8d358326437",
|
|
144
|
+
"Update-Feature-Flag": "sha256:9b34b19164168938d08a75d268c9becf8d189c85a1789a45061ae304041faf7c",
|
|
145
|
+
"Update-Lookup-Table": "sha256:ef28f1ec9c9484a7a53b5e2b6659e70f79a8e71e55bda17025aa4ab3c23b6a63",
|
|
146
|
+
"Update-Metric": "sha256:739bb6abdab19282afd4a6644a96183daabe9098a5322c44a77b840608a5ee9d",
|
|
147
|
+
};
|
|
82
148
|
/**
|
|
83
149
|
* The manifest this release reviewed: both lists in one place, which is what
|
|
84
150
|
* makes the classification the connector applies and the drift check that runs
|
|
85
|
-
* beside it the same fact (P13).
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* them from a live project and prints the block to paste in
|
|
89
|
-
* ([#351](https://github.com/zackbart/connecta/issues/351)).
|
|
151
|
+
* beside it the same fact (P13). The 2026-08-13 read-only audit recorded all
|
|
152
|
+
* 63 live US schemas, so a later change is named by the maintainer-run drift
|
|
153
|
+
* check rather than rediscovered in production (#395).
|
|
90
154
|
*
|
|
91
155
|
* Exported because the maintainer-run check compares against this manifest and
|
|
92
156
|
* *names* what moved, which the runtime check deliberately cannot.
|
|
@@ -94,6 +158,7 @@ const WRITE_TOOLS = new Map([
|
|
|
94
158
|
export const MIXPANEL_VETTED_CATALOG = vettedCatalog({
|
|
95
159
|
reads: READ_ONLY_TOOLS,
|
|
96
160
|
writes: WRITE_TOOLS,
|
|
161
|
+
schemaDigests: MIXPANEL_SCHEMA_DIGESTS,
|
|
97
162
|
});
|
|
98
163
|
const REGION_COPY = {
|
|
99
164
|
us: "US",
|
|
@@ -116,6 +181,9 @@ Account purpose: ${purpose}
|
|
|
116
181
|
- Start with \`Get-Projects\`, then use \`Get-Business-Context\` for the selected project before interpreting its events or metrics.
|
|
117
182
|
- 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.
|
|
118
183
|
- Discover names with \`Get-Events\`, \`List-Properties\`, and \`Get-Property-Values\`; do not guess event or property spelling.
|
|
184
|
+
- \`Get-Business-Context\` requires either \`project_id\` or \`organization_id\`. Its schema marks both optional, but the hosted tool rejects a call with neither.
|
|
185
|
+
- \`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.
|
|
186
|
+
- \`List-Properties\` accepts \`names\` or \`query\`, never both. Use exact \`names\` for known properties and \`query\` for substring discovery.
|
|
119
187
|
- For a new analysis, fetch \`Get-Query-Schema\` before \`Run-Query\`. Reduce query results inside \`execute_code\` before returning them.
|
|
120
188
|
- Use \`Get-Report\` when the request names an existing saved report. Use \`Run-Query\` for a new question.
|
|
121
189
|
- 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.
|
|
@@ -17,7 +17,7 @@ export interface StripeOptions {
|
|
|
17
17
|
mode: StripeMode;
|
|
18
18
|
/** Human-readable display name; defaults to "Stripe (<mode>)". */
|
|
19
19
|
title?: string;
|
|
20
|
-
/** Which business
|
|
20
|
+
/** Which business purpose and Stripe context this connector is for. */
|
|
21
21
|
purpose: string;
|
|
22
22
|
/** OAuth by default; static headers support restricted API keys. */
|
|
23
23
|
auth?: RemoteMcpAuth;
|
|
@@ -27,7 +27,7 @@ export interface StripeOptions {
|
|
|
27
27
|
* connected-account calls, so this requires `headers` auth.
|
|
28
28
|
*/
|
|
29
29
|
connectedAccount?: string;
|
|
30
|
-
/**
|
|
30
|
+
/** Connector-specific conventions appended to the maintained provider guide. */
|
|
31
31
|
instructions?: string;
|
|
32
32
|
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
33
33
|
maxResultBytes?: number;
|
package/dist/providers/stripe.js
CHANGED
|
@@ -129,12 +129,12 @@ const MODE_COPY = {
|
|
|
129
129
|
production: {
|
|
130
130
|
title: "Stripe (production)",
|
|
131
131
|
blurb: "production — live money and real customers",
|
|
132
|
-
warning: "This is a PRODUCTION
|
|
132
|
+
warning: "This is a PRODUCTION Stripe connection. Every write moves real money against real customers, and a refund cannot be undone. If a request could plausibly be a rehearsal, route it to a sandbox connector instead.",
|
|
133
133
|
},
|
|
134
134
|
sandbox: {
|
|
135
135
|
title: "Stripe (sandbox)",
|
|
136
136
|
blurb: "sandbox — test data, no real money",
|
|
137
|
-
warning: "This is a SANDBOX
|
|
137
|
+
warning: "This is a SANDBOX Stripe connection. Nothing here is real money and none of these objects exist in production, so never answer a question about live revenue, payouts, or a named customer from this connector.",
|
|
138
138
|
},
|
|
139
139
|
};
|
|
140
140
|
function usageGuide(mode, purpose, instructions) {
|
|
@@ -143,19 +143,22 @@ function usageGuide(mode, purpose, instructions) {
|
|
|
143
143
|
const rate = mode === "production" ? "100" : "25";
|
|
144
144
|
return `# Stripe usage
|
|
145
145
|
|
|
146
|
-
Mode: ${mode}.
|
|
146
|
+
Mode: ${mode}. Connector purpose: ${purpose}
|
|
147
147
|
|
|
148
148
|
${copy.warning}
|
|
149
149
|
|
|
150
|
+
- One OAuth session may cover more than one account in the same Stripe organization. The connector id, title, and purpose state routing intent; they do not prove which account a call will use.
|
|
151
|
+
- Before every account-scoped read or write, resolve the intended organization account. Inspect the chosen tool's live input schema and carry the exact account or context field it exposes. If the account or its supported selection mechanism is ambiguous, stop and ask; never guess from the connector metadata, invent an MCP argument, or add a request header the live contract does not expose.
|
|
152
|
+
- Organization accounts are not Stripe Connect connected accounts. A Connect call requires a deployment-configured restricted key plus Stripe's documented \`Stripe-Account\` header; OAuth does not support that path. Do not try to turn an organization-account call into a Connect call inside tool arguments.
|
|
150
153
|
- 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.
|
|
151
|
-
- Prefer a dedicated tool when one covers the task: \`get_stripe_account_info\` for
|
|
154
|
+
- Prefer a dedicated tool when one covers the task: \`get_stripe_account_info\` for account information, \`get_balance_summary\` for balances, \`create_refund\` for refunds, \`stripe_report\` for reports. One call instead of three, and a refund named \`create_refund\` reads far more clearly in the approval a human sees than the same refund buried in \`stripe_api_write\` arguments.
|
|
152
155
|
- \`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; expect approval on every call.
|
|
153
156
|
- 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\` and reduce before returning.
|
|
154
157
|
- 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. Find the object with \`stripe_api_search\` (or a list endpoint through \`stripe_api_read\`) and carry the \`id\` it returned into the write.
|
|
155
|
-
- This
|
|
158
|
+
- 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.
|
|
156
159
|
- 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.
|
|
157
160
|
- 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.
|
|
158
|
-
- Stripe answers a rate limit with \`429\` and a \`Stripe-Rate-Limited-Reason\` header; back off on that rather than retrying immediately.
|
|
161
|
+
- 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 ${rate} requests per second, and any single endpoint is capped at 25 per second regardless of mode, so paging one list is the real constraint.
|
|
159
162
|
- 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.
|
|
160
163
|
- Treat every create, update, delete, refund, and report run as a write. Connecta routes the maintained write catalog through \`call_destructive_tool\`; newly added tools also fail closed until classified.
|
|
161
164
|
- 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.
|
|
@@ -186,12 +189,11 @@ export function stripe(id, options) {
|
|
|
186
189
|
usageGuide: {
|
|
187
190
|
content: usageGuide(mode, purpose, options.instructions),
|
|
188
191
|
// Explicit rather than derived: production versus sandbox is the fact an
|
|
189
|
-
// agent must not get wrong
|
|
190
|
-
//
|
|
191
|
-
// budget ([#342](https://github.com/zackbart/connecta/issues/342)).
|
|
192
|
+
// agent must not get wrong. Multi-account OAuth also has to reach browse
|
|
193
|
+
// before connector metadata can be mistaken for authenticated identity.
|
|
192
194
|
summary: mode === "production"
|
|
193
|
-
? "PRODUCTION:
|
|
194
|
-
: "Sandbox: test data only
|
|
195
|
+
? "PRODUCTION: real money. OAuth may span organization accounts; resolve the account from live schemas before acting."
|
|
196
|
+
: "Sandbox: test data only. OAuth may span organization accounts; resolve the account from live schemas before acting.",
|
|
195
197
|
// Not `required`. The four generic tools are the routing decision, and
|
|
196
198
|
// the mode warning already rides the title and description; a guide
|
|
197
199
|
// forced into every call would pay for the same paragraph repeatedly.
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CatalogDriftReport, Connector, ConnectorContext, ConnectorStatus, KVStorage, Logger, ToolDef } from "./types.js";
|
|
2
|
+
import { type DeferredWork } from "./connector-scope.js";
|
|
2
3
|
import { type CatalogDriftActivityContext } from "./activity.js";
|
|
3
4
|
import { type CredentialVault } from "./credentials.js";
|
|
4
5
|
import { type CallAdmissionPermit, type ConnectorCallAdmissionSnapshot } from "./call-admission.js";
|
|
@@ -70,6 +71,12 @@ export interface RegistryOptions {
|
|
|
70
71
|
catalogDriftActivity?: Omit<CatalogDriftActivityContext, "logger">;
|
|
71
72
|
}
|
|
72
73
|
export type ConnectorOperationOptions = Pick<ConnectorContext, "signal" | "timeoutMs">;
|
|
74
|
+
/** Agent-only catalog behavior. This never enters a ConnectorContext. */
|
|
75
|
+
export interface CatalogReadOptions {
|
|
76
|
+
defer?: DeferredWork;
|
|
77
|
+
/** Fresh deadline for a deferred refresh; never an inbound signal. */
|
|
78
|
+
refreshTimeoutMs: number;
|
|
79
|
+
}
|
|
73
80
|
/**
|
|
74
81
|
* The registry surface a per-connection MCP server consumes: every meta-tool
|
|
75
82
|
* (`src/meta-tools.ts`) and the `execute_code` sandbox bridge (`src/execute.ts`)
|
|
@@ -88,7 +95,7 @@ export interface RegistryView {
|
|
|
88
95
|
connector: Connector;
|
|
89
96
|
toolName: string;
|
|
90
97
|
} | null;
|
|
91
|
-
getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
|
|
98
|
+
getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions, readOptions?: CatalogReadOptions): Promise<ToolDef[]>;
|
|
92
99
|
refreshTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
|
|
93
100
|
peekTools(id: string): ToolDef[] | undefined;
|
|
94
101
|
contextFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): ConnectorContext;
|
|
@@ -126,6 +133,10 @@ export declare class Registry implements RegistryView {
|
|
|
126
133
|
private readonly catalogMutations;
|
|
127
134
|
/** Same-request cold loads share one promise without retaining the request. */
|
|
128
135
|
private readonly requestCatalogLoads;
|
|
136
|
+
/** One live refresh per connector across agent and operator requests. */
|
|
137
|
+
private readonly catalogRefreshes;
|
|
138
|
+
/** Last payload-free agent catalog access in this runtime. */
|
|
139
|
+
private readonly catalogAccess;
|
|
129
140
|
/** Deployment-wide observations — every call, whatever view made it. */
|
|
130
141
|
private readonly health;
|
|
131
142
|
/** Last drift counts reported to activity, per connector, in this runtime. */
|
|
@@ -215,21 +226,33 @@ export declare class Registry implements RegistryView {
|
|
|
215
226
|
* the catalog and resurrect the pre-change listing.
|
|
216
227
|
*/
|
|
217
228
|
private enqueueCatalogMutation;
|
|
229
|
+
private refreshToolsWithContext;
|
|
230
|
+
/**
|
|
231
|
+
* Publish one shared refresh promise before starting its connector work.
|
|
232
|
+
* The first caller owns the scope and deadline; every later caller joins the
|
|
233
|
+
* result without gaining access to that context.
|
|
234
|
+
*/
|
|
235
|
+
private startCatalogRefresh;
|
|
218
236
|
/** Force a live listTools refresh and replace both catalog cache layers. */
|
|
219
237
|
refreshTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
|
|
238
|
+
private observeCatalogAccess;
|
|
239
|
+
/**
|
|
240
|
+
* Start or join one shared refresh. A newly deferred task owns its scope,
|
|
241
|
+
* deadline, and teardown; no inbound signal or request scope crosses into it.
|
|
242
|
+
*/
|
|
243
|
+
private deferCatalogRefresh;
|
|
220
244
|
/** Cached listTools with in-memory + persisted serializable catalog layers. */
|
|
221
245
|
private loadTools;
|
|
222
246
|
/**
|
|
223
|
-
* Coalesce one connector's
|
|
224
|
-
* neither roots the request scope nor lets its connector context
|
|
225
|
-
* another request; settled entries are also removed eagerly.
|
|
247
|
+
* Coalesce one connector's catalog traversal inside one inbound request. The
|
|
248
|
+
* WeakMap neither roots the request scope nor lets its connector context
|
|
249
|
+
* escape into another request; settled entries are also removed eagerly.
|
|
226
250
|
*
|
|
227
|
-
* The
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* the deal being made — one fetch, one deadline.
|
|
251
|
+
* The deployment-wide flight below this layer coalesces the actual live
|
|
252
|
+
* refresh across requests. Its first caller's context and deadline govern;
|
|
253
|
+
* later callers join only its result, never its request scope.
|
|
231
254
|
*/
|
|
232
|
-
getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
|
|
255
|
+
getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions, readOptions?: CatalogReadOptions): Promise<ToolDef[]>;
|
|
233
256
|
/** Return a cached catalog without performing storage or network I/O. */
|
|
234
257
|
peekTools(id: string): ToolDef[] | undefined;
|
|
235
258
|
recordSuccess(id: string, latencyMs: number): void;
|
package/dist/registry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeConnectorScope, } from "./connector-scope.js";
|
|
1
2
|
import { recordCatalogDriftActivity, } from "./activity.js";
|
|
2
3
|
import { storedCredentialShape, } from "./credentials.js";
|
|
3
4
|
import { ConnectorCallError } from "./errors.js";
|
|
@@ -6,6 +7,8 @@ import { boundedCatalogDrift } from "./catalog-drift.js";
|
|
|
6
7
|
import { fingerprintSerializedCatalog, snapshotCatalog, } from "./catalog-fingerprint.js";
|
|
7
8
|
import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES, } from "./catalog-limits.js";
|
|
8
9
|
import { mapSettledWithConcurrency } from "./concurrency.js";
|
|
10
|
+
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary, } from "./skills.js";
|
|
11
|
+
import { withAbortableTimeout } from "./timeout.js";
|
|
9
12
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
10
13
|
const DEFAULT_TTL_SECONDS = 300;
|
|
11
14
|
const DEFAULT_STALE_SECONDS = 3600;
|
|
@@ -122,6 +125,10 @@ export class Registry {
|
|
|
122
125
|
catalogMutations = new Map();
|
|
123
126
|
/** Same-request cold loads share one promise without retaining the request. */
|
|
124
127
|
requestCatalogLoads = new WeakMap();
|
|
128
|
+
/** One live refresh per connector across agent and operator requests. */
|
|
129
|
+
catalogRefreshes = new Map();
|
|
130
|
+
/** Last payload-free agent catalog access in this runtime. */
|
|
131
|
+
catalogAccess = new Map();
|
|
125
132
|
/** Deployment-wide observations — every call, whatever view made it. */
|
|
126
133
|
health = new HealthLog();
|
|
127
134
|
/** Last drift counts reported to activity, per connector, in this runtime. */
|
|
@@ -146,6 +153,16 @@ export class Registry {
|
|
|
146
153
|
if (this.connectors.has(c.id)) {
|
|
147
154
|
throw new Error(`Duplicate connector id "${c.id}"`);
|
|
148
155
|
}
|
|
156
|
+
const configuredGuideSummary = typeof c.usageGuide === "object"
|
|
157
|
+
? normalizeGuideSummary(c.usageGuide.summary ?? "")
|
|
158
|
+
: undefined;
|
|
159
|
+
if (configuredGuideSummary !== undefined &&
|
|
160
|
+
configuredGuideSummary.length > GUIDE_SUMMARY_LENGTH) {
|
|
161
|
+
throw new Error(`Connector "${c.id}" usageGuide.summary is ` +
|
|
162
|
+
`${configuredGuideSummary.length} characters after whitespace ` +
|
|
163
|
+
`normalization; the discovery bound is ${GUIDE_SUMMARY_LENGTH}. ` +
|
|
164
|
+
"Shorten it or omit it to derive one.");
|
|
165
|
+
}
|
|
149
166
|
this.connectors.set(c.id, c);
|
|
150
167
|
if (c.callAdmission) {
|
|
151
168
|
this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
|
|
@@ -592,19 +609,20 @@ export class Registry {
|
|
|
592
609
|
.catch(() => { });
|
|
593
610
|
return next;
|
|
594
611
|
}
|
|
595
|
-
|
|
596
|
-
async refreshTools(id, baseUrl, requestScope, callOptions = {}) {
|
|
597
|
-
const connector = this.connectors.get(id);
|
|
598
|
-
if (!connector)
|
|
599
|
-
throw new Error(`Unknown connector "${id}"`);
|
|
600
|
-
if (connector.staticTools)
|
|
601
|
-
return connector.staticTools;
|
|
612
|
+
async refreshToolsWithContext(id, connector, ctx, skipPublicationWhenAborted = false) {
|
|
602
613
|
const generation = this.catalogGeneration(id);
|
|
603
|
-
const tools = await connector.listTools(
|
|
614
|
+
const tools = await connector.listTools(ctx);
|
|
604
615
|
// The listing a maintained proxy just served is also the only catalog
|
|
605
616
|
// comparison connecta ever makes. It rides this refresh whether or not the
|
|
606
617
|
// result reaches a cache, because what drifted drifted.
|
|
607
618
|
this.observeCatalogDrift(connector);
|
|
619
|
+
// A deferred deadline may close the owned scope while a connector that
|
|
620
|
+
// ignores abort is still listing. The completed list remains a valid drift
|
|
621
|
+
// observation, but must not overwrite a newer same-generation refresh.
|
|
622
|
+
// Blocking callers do not set this flag and retain their prior publication
|
|
623
|
+
// behavior when an inbound abort races a completed listing.
|
|
624
|
+
if (skipPublicationWhenAborted && ctx.signal?.aborted)
|
|
625
|
+
return tools;
|
|
608
626
|
// The caller that began this refresh may still use its result, but a
|
|
609
627
|
// credential/OAuth change that landed while listTools was in flight means
|
|
610
628
|
// the listing must not enter either shared cache layer.
|
|
@@ -618,8 +636,10 @@ export class Registry {
|
|
|
618
636
|
}
|
|
619
637
|
const previous = this.cache.get(id);
|
|
620
638
|
const snapshot = await snapshotCatalog(tools);
|
|
621
|
-
if (generation !== this.catalogGeneration(id)
|
|
639
|
+
if (generation !== this.catalogGeneration(id) ||
|
|
640
|
+
(skipPublicationWhenAborted && ctx.signal?.aborted)) {
|
|
622
641
|
return tools;
|
|
642
|
+
}
|
|
623
643
|
if (snapshot.serializedBytes.byteLength > MAX_SERIALIZED_CATALOG_BYTES) {
|
|
624
644
|
const message = `Connector "${id}" returned a ${snapshot.serializedBytes.byteLength}-byte ` +
|
|
625
645
|
`serialized catalog, over the ${MAX_SERIALIZED_CATALOG_BYTES}-byte ceiling; ` +
|
|
@@ -653,8 +673,106 @@ export class Registry {
|
|
|
653
673
|
}
|
|
654
674
|
return tools;
|
|
655
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Publish one shared refresh promise before starting its connector work.
|
|
678
|
+
* The first caller owns the scope and deadline; every later caller joins the
|
|
679
|
+
* result without gaining access to that context.
|
|
680
|
+
*/
|
|
681
|
+
startCatalogRefresh(id, generation, start, finish) {
|
|
682
|
+
const existing = this.catalogRefreshes.get(id);
|
|
683
|
+
if (existing?.generation === generation)
|
|
684
|
+
return existing;
|
|
685
|
+
let resolveWork;
|
|
686
|
+
let rejectWork;
|
|
687
|
+
const work = new Promise((resolve, reject) => {
|
|
688
|
+
resolveWork = resolve;
|
|
689
|
+
rejectWork = reject;
|
|
690
|
+
});
|
|
691
|
+
let flight;
|
|
692
|
+
const promise = work
|
|
693
|
+
.finally(async () => {
|
|
694
|
+
if (finish)
|
|
695
|
+
await finish();
|
|
696
|
+
})
|
|
697
|
+
.finally(() => {
|
|
698
|
+
if (this.catalogRefreshes.get(id) === flight) {
|
|
699
|
+
this.catalogRefreshes.delete(id);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
flight = { generation, promise };
|
|
703
|
+
this.catalogRefreshes.set(id, flight);
|
|
704
|
+
try {
|
|
705
|
+
Promise.resolve(start()).then(resolveWork, rejectWork);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
rejectWork(err);
|
|
709
|
+
}
|
|
710
|
+
return flight;
|
|
711
|
+
}
|
|
712
|
+
/** Force a live listTools refresh and replace both catalog cache layers. */
|
|
713
|
+
async refreshTools(id, baseUrl, requestScope, callOptions = {}) {
|
|
714
|
+
const connector = this.connectors.get(id);
|
|
715
|
+
if (!connector)
|
|
716
|
+
throw new Error(`Unknown connector "${id}"`);
|
|
717
|
+
if (connector.staticTools)
|
|
718
|
+
return connector.staticTools;
|
|
719
|
+
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions);
|
|
720
|
+
return this.startCatalogRefresh(id, this.catalogGeneration(id), () => this.refreshToolsWithContext(id, connector, ctx)).promise;
|
|
721
|
+
}
|
|
722
|
+
observeCatalogAccess(id, state) {
|
|
723
|
+
this.catalogAccess.set(id, {
|
|
724
|
+
state,
|
|
725
|
+
observedAt: new Date().toISOString(),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Start or join one shared refresh. A newly deferred task owns its scope,
|
|
730
|
+
* deadline, and teardown; no inbound signal or request scope crosses into it.
|
|
731
|
+
*/
|
|
732
|
+
deferCatalogRefresh(id, baseUrl, expectedGeneration, options) {
|
|
733
|
+
const connector = this.connectors.get(id);
|
|
734
|
+
if (!connector || connector.staticTools || !options.defer)
|
|
735
|
+
return;
|
|
736
|
+
let flight = this.catalogRefreshes.get(id);
|
|
737
|
+
if (flight?.generation !== expectedGeneration)
|
|
738
|
+
flight = undefined;
|
|
739
|
+
if (!flight) {
|
|
740
|
+
const requestScope = {};
|
|
741
|
+
let ctx;
|
|
742
|
+
flight = this.startCatalogRefresh(id, expectedGeneration, () => withAbortableTimeout((signal) => {
|
|
743
|
+
const current = this.cache.get(id);
|
|
744
|
+
if (current && current.exp > Date.now()) {
|
|
745
|
+
return Promise.resolve(current.tools);
|
|
746
|
+
}
|
|
747
|
+
if (expectedGeneration !== this.catalogGeneration(id) ||
|
|
748
|
+
this.invalidated.has(id)) {
|
|
749
|
+
return Promise.reject(new Error(`Deferred catalog refresh of "${id}" was invalidated before it started.`));
|
|
750
|
+
}
|
|
751
|
+
ctx = this.contextFor(id, baseUrl, requestScope, {
|
|
752
|
+
signal,
|
|
753
|
+
timeoutMs: options.refreshTimeoutMs,
|
|
754
|
+
});
|
|
755
|
+
return this.refreshToolsWithContext(id, connector, ctx, true);
|
|
756
|
+
}, options.refreshTimeoutMs, `deferred catalog refresh of "${id}"`), async () => {
|
|
757
|
+
if (ctx)
|
|
758
|
+
await closeConnectorScope(connector, ctx, options.defer);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (!flight.deferredTail) {
|
|
762
|
+
// Attach the rejection handler before handing the tail to waitUntil.
|
|
763
|
+
flight.deferredTail = flight.promise.then(() => { }, (err) => {
|
|
764
|
+
this.opts.logger.warn(`[connecta] connector "${id}" deferred catalog refresh failed: ${msg(err)}`);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
options.defer(flight.deferredTail);
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
this.opts.logger.warn(`[connecta] connector "${id}" deferred catalog refresh could not attach to the runtime: ${msg(err)}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
656
774
|
/** Cached listTools with in-memory + persisted serializable catalog layers. */
|
|
657
|
-
async loadTools(id, baseUrl, requestScope, callOptions = {}) {
|
|
775
|
+
async loadTools(id, baseUrl, requestScope, callOptions = {}, readOptions) {
|
|
658
776
|
const connector = this.connectors.get(id);
|
|
659
777
|
if (!connector)
|
|
660
778
|
throw new Error(`Unknown connector "${id}"`);
|
|
@@ -663,9 +781,14 @@ export class Registry {
|
|
|
663
781
|
const now = Date.now();
|
|
664
782
|
const requestGeneration = this.catalogGeneration(id);
|
|
665
783
|
const hit = this.cache.get(id);
|
|
666
|
-
if (hit && hit.exp > now)
|
|
784
|
+
if (hit && hit.exp > now) {
|
|
785
|
+
if (readOptions?.defer)
|
|
786
|
+
this.observeCatalogAccess(id, "fresh");
|
|
667
787
|
return hit.tools;
|
|
668
|
-
|
|
788
|
+
}
|
|
789
|
+
let stale = hit && hit.staleUntil > now
|
|
790
|
+
? { tools: hit.tools, staleUntil: hit.staleUntil }
|
|
791
|
+
: undefined;
|
|
669
792
|
if (this.persistToolCatalog && !this.invalidated.has(id)) {
|
|
670
793
|
const generation = this.catalogGeneration(id);
|
|
671
794
|
let persisted = null;
|
|
@@ -679,49 +802,95 @@ export class Registry {
|
|
|
679
802
|
persisted = null;
|
|
680
803
|
stale = undefined;
|
|
681
804
|
}
|
|
682
|
-
|
|
805
|
+
// Storage can yield while another request finishes a live refresh. Read
|
|
806
|
+
// the shared cache again before an older manifest gets any authority.
|
|
807
|
+
// The candidate with the later fresh deadline wins; both candidates have
|
|
808
|
+
// already passed their own fingerprint and completeness checks.
|
|
809
|
+
const reconciledAt = Date.now();
|
|
810
|
+
const current = this.cache.get(id);
|
|
811
|
+
const usableCurrent = current && current.staleUntil > reconciledAt ? current : undefined;
|
|
812
|
+
if (usableCurrent) {
|
|
813
|
+
stale = {
|
|
814
|
+
tools: usableCurrent.tools,
|
|
815
|
+
staleUntil: usableCurrent.staleUntil,
|
|
816
|
+
};
|
|
817
|
+
if (usableCurrent.exp > reconciledAt) {
|
|
818
|
+
if (readOptions?.defer)
|
|
819
|
+
this.observeCatalogAccess(id, "fresh");
|
|
820
|
+
return usableCurrent.tools;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (persisted &&
|
|
824
|
+
persisted.staleUntil > reconciledAt &&
|
|
825
|
+
(!usableCurrent || persisted.expiresAt > usableCurrent.exp)) {
|
|
683
826
|
this.cache.set(id, {
|
|
684
827
|
tools: persisted.tools,
|
|
685
828
|
fingerprint: persisted.fingerprint,
|
|
686
829
|
exp: persisted.expiresAt,
|
|
687
830
|
staleUntil: persisted.staleUntil,
|
|
688
831
|
});
|
|
689
|
-
if (persisted.expiresAt >
|
|
832
|
+
if (persisted.expiresAt > reconciledAt) {
|
|
833
|
+
if (readOptions?.defer)
|
|
834
|
+
this.observeCatalogAccess(id, "fresh");
|
|
690
835
|
return persisted.tools;
|
|
691
|
-
|
|
836
|
+
}
|
|
837
|
+
stale = {
|
|
838
|
+
tools: persisted.tools,
|
|
839
|
+
staleUntil: persisted.staleUntil,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (stale &&
|
|
844
|
+
stale.staleUntil > Date.now() &&
|
|
845
|
+
readOptions?.defer &&
|
|
846
|
+
requestGeneration === this.catalogGeneration(id) &&
|
|
847
|
+
!this.invalidated.has(id)) {
|
|
848
|
+
this.deferCatalogRefresh(id, baseUrl, requestGeneration, readOptions);
|
|
849
|
+
// Invalidation can land synchronously while the refresh is attached.
|
|
850
|
+
// Repeat the authority check at the exact stale publication point.
|
|
851
|
+
if (stale.staleUntil > Date.now() &&
|
|
852
|
+
requestGeneration === this.catalogGeneration(id) &&
|
|
853
|
+
!this.invalidated.has(id)) {
|
|
854
|
+
this.observeCatalogAccess(id, "stale");
|
|
855
|
+
return stale.tools;
|
|
692
856
|
}
|
|
693
857
|
}
|
|
694
858
|
try {
|
|
695
|
-
|
|
859
|
+
const tools = await this.refreshTools(id, baseUrl, requestScope, callOptions);
|
|
860
|
+
if (readOptions?.defer)
|
|
861
|
+
this.observeCatalogAccess(id, "fresh");
|
|
862
|
+
return tools;
|
|
696
863
|
}
|
|
697
864
|
catch (err) {
|
|
698
865
|
if (stale &&
|
|
866
|
+
stale.staleUntil > Date.now() &&
|
|
699
867
|
requestGeneration === this.catalogGeneration(id) &&
|
|
700
868
|
!this.invalidated.has(id)) {
|
|
869
|
+
if (readOptions?.defer)
|
|
870
|
+
this.observeCatalogAccess(id, "stale");
|
|
701
871
|
this.opts.logger.warn(`[connecta] connector "${id}" catalog refresh failed; serving stale catalog: ${msg(err)}`);
|
|
702
|
-
return stale;
|
|
872
|
+
return stale.tools;
|
|
703
873
|
}
|
|
704
874
|
throw err;
|
|
705
875
|
}
|
|
706
876
|
}
|
|
707
877
|
/**
|
|
708
|
-
* Coalesce one connector's
|
|
709
|
-
* neither roots the request scope nor lets its connector context
|
|
710
|
-
* another request; settled entries are also removed eagerly.
|
|
878
|
+
* Coalesce one connector's catalog traversal inside one inbound request. The
|
|
879
|
+
* WeakMap neither roots the request scope nor lets its connector context
|
|
880
|
+
* escape into another request; settled entries are also removed eagerly.
|
|
711
881
|
*
|
|
712
|
-
* The
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
* the deal being made — one fetch, one deadline.
|
|
882
|
+
* The deployment-wide flight below this layer coalesces the actual live
|
|
883
|
+
* refresh across requests. Its first caller's context and deadline govern;
|
|
884
|
+
* later callers join only its result, never its request scope.
|
|
716
885
|
*/
|
|
717
|
-
async getTools(id, baseUrl, requestScope, callOptions = {}) {
|
|
886
|
+
async getTools(id, baseUrl, requestScope, callOptions = {}, readOptions) {
|
|
718
887
|
const connector = this.connectors.get(id);
|
|
719
888
|
if (!connector)
|
|
720
889
|
throw new Error(`Unknown connector "${id}"`);
|
|
721
890
|
if (connector.staticTools)
|
|
722
891
|
return connector.staticTools;
|
|
723
892
|
if (!requestScope) {
|
|
724
|
-
return this.loadTools(id, baseUrl, requestScope, callOptions);
|
|
893
|
+
return this.loadTools(id, baseUrl, requestScope, callOptions, readOptions);
|
|
725
894
|
}
|
|
726
895
|
let loads = this.requestCatalogLoads.get(requestScope);
|
|
727
896
|
if (!loads) {
|
|
@@ -731,7 +900,7 @@ export class Registry {
|
|
|
731
900
|
const existing = loads.get(id);
|
|
732
901
|
if (existing)
|
|
733
902
|
return existing;
|
|
734
|
-
const loading = this.loadTools(id, baseUrl, requestScope, callOptions);
|
|
903
|
+
const loading = this.loadTools(id, baseUrl, requestScope, callOptions, readOptions);
|
|
735
904
|
loads.set(id, loading);
|
|
736
905
|
try {
|
|
737
906
|
return await loading;
|
|
@@ -796,24 +965,39 @@ export class Registry {
|
|
|
796
965
|
// The report is rebuilt rather than spread through: what the seam returned
|
|
797
966
|
// is third-party output, and status is read by the operator UI and copied
|
|
798
967
|
// into responses.
|
|
799
|
-
const
|
|
968
|
+
const withObservations = (status) => {
|
|
800
969
|
const report = boundedCatalogDrift(connector.catalogDrift?.());
|
|
801
|
-
|
|
970
|
+
const access = this.catalogAccess.get(id);
|
|
971
|
+
// Connector.status is an open plugin seam. Rebuild its public fields so
|
|
972
|
+
// a connector cannot smuggle payload through either registry-owned
|
|
973
|
+
// observation when this runtime has not made one.
|
|
974
|
+
const boundedStatus = {
|
|
975
|
+
state: status.state,
|
|
976
|
+
...(status.authorizationUrl !== undefined
|
|
977
|
+
? { authorizationUrl: status.authorizationUrl }
|
|
978
|
+
: {}),
|
|
979
|
+
...(status.message !== undefined ? { message: status.message } : {}),
|
|
980
|
+
};
|
|
981
|
+
return {
|
|
982
|
+
...boundedStatus,
|
|
983
|
+
...(report ? { catalogDrift: report } : {}),
|
|
984
|
+
...(access ? { catalogAccess: { ...access } } : {}),
|
|
985
|
+
};
|
|
802
986
|
};
|
|
803
987
|
if (connector.status) {
|
|
804
988
|
try {
|
|
805
|
-
return
|
|
989
|
+
return withObservations(await connector.status(ctx));
|
|
806
990
|
}
|
|
807
991
|
catch (err) {
|
|
808
|
-
return
|
|
992
|
+
return withObservations({ state: "error", message: msg(err) });
|
|
809
993
|
}
|
|
810
994
|
}
|
|
811
995
|
try {
|
|
812
996
|
await this.getTools(id, baseUrl, requestScope, callOptions);
|
|
813
|
-
return
|
|
997
|
+
return withObservations({ state: "ok" });
|
|
814
998
|
}
|
|
815
999
|
catch (err) {
|
|
816
|
-
return
|
|
1000
|
+
return withObservations({ state: "error", message: msg(err) });
|
|
817
1001
|
}
|
|
818
1002
|
}
|
|
819
1003
|
markCatalogInvalid(id) {
|