@zackbart/connecta 0.16.1 → 0.18.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 +180 -0
- package/README.md +4 -0
- package/dist/catalog-service.d.ts +10 -0
- package/dist/catalog-service.js +77 -5
- package/dist/catalog.js +114 -12
- package/dist/errors.d.ts +4 -6
- package/dist/execute.d.ts +7 -0
- package/dist/execute.js +262 -168
- package/dist/invocation.js +3 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.js +55 -23
- 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 +25 -24
- package/dist/providers/stripe.js +64 -35
- package/dist/registry.d.ts +32 -9
- package/dist/registry.js +217 -33
- package/dist/routes/mcp.js +6 -0
- package/dist/routes/ui.js +1 -1
- package/dist/skills.d.ts +5 -1
- package/dist/skills.js +206 -30
- 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 +8 -5
- package/documentation/code-mode.md +68 -68
- package/documentation/connector-guides.md +29 -27
- package/documentation/connectors.md +13 -1
- package/documentation/meta-tools.md +53 -19
- package/documentation/mixpanel.md +20 -0
- package/documentation/notion.md +17 -0
- package/documentation/operations.md +24 -21
- package/documentation/operator-ui.md +12 -2
- package/documentation/provider-audit.md +15 -7
- package/documentation/provider-conventions.md +26 -13
- package/documentation/stripe.md +66 -59
- package/documentation/upgrading.md +46 -4
- package/ethos.md +7 -7
- package/examples/worker/README.md +4 -3
- package/package.json +2 -2
- package/templates/node/README.md +7 -0
- package/templates/node/package.json +5 -2
package/dist/skills.js
CHANGED
|
@@ -1,40 +1,77 @@
|
|
|
1
|
-
export const CONNECTA_INSTRUCTIONS = '
|
|
1
|
+
export const CONNECTA_INSTRUCTIONS = 'Choose a route before discovery. For one read at an unknown address, use search_tools then call_tool; a known address needs only call_tool. For read-only reduction, multiple or dependent calls, loops, joins, or branches, use one execute_code program that discovers, calls, and returns the reduced answer. Only readOnlyHint: true tools run there. Keep unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool. After auth_required use authorize_connector. After a truncated direct result use fields or get_result. connecta.ui(html) exists only inside execute_code, not in connector search; return the same summary data the HTML renders. Fetch skills({ name: "usage" }) once for program syntax, selection, repair, examples, and runtime details.';
|
|
2
2
|
const USAGE_SKILL_BASE = `# Connecta usage
|
|
3
3
|
|
|
4
4
|
## The surface
|
|
5
5
|
|
|
6
6
|
Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Broad discovery and multi-call work live in a program, not in top-level tools.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The always-loaded MCP instructions are authoritative for choosing the top-level route. Read this skill at most once per task for the program workflow and recovery details below.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Inside a program
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- Anything wider — two or more calls, dependent steps, loops, joins, branching, a whole-catalog browse, or a result to reduce: one \`execute_code\` run.
|
|
14
|
-
- Any unannotated, write-capable, or destructive call: \`call_destructive_tool\`, one at a time, after reviewing its schema and consequences.
|
|
15
|
-
- Truncated result: retry with \`fields\`, else page it with \`get_result\`.
|
|
16
|
-
- \`auth_required\`: \`authorize_connector\`, hand its recovery text to the operator, retry the call.
|
|
12
|
+
Write one plain-JavaScript async arrow function. TypeScript syntax and portable imports do not work. Return JSON-shaped data and reduce large results before returning.
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
The minimum guest API is:
|
|
15
|
+
|
|
16
|
+
- \`<connectorId>.<toolName>(args)\` calls a sanitized shortcut. Non-identifier characters become \`_\`; leading digits gain \`_\`; reserved words gain a trailing \`_\`.
|
|
17
|
+
- \`connecta.call("connector.tool", args)\` uses the canonical address and returns the unwrapped value.
|
|
18
|
+
- \`connecta.search(args)\` returns \`{ tools, total, offset, limit, hasMore }\`; \`connecta.describe(args)\` returns \`{ tools }\`.
|
|
19
|
+
- \`connecta.batch(calls)\` runs 2–10 independent calls. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails }\`.
|
|
20
|
+
- \`console.log(...)\` is captured. \`connecta.emit(block)\` and \`connecta.ui(html, options?)\` produce rich output.
|
|
21
|
+
|
|
22
|
+
## Discover and select
|
|
23
|
+
|
|
24
|
+
Search inside the run and finish the task there. A discovery-only program wastes a round trip. Use 2–4 distinctive action/object terms, not the full request. Use separate short searches for distinct operations.
|
|
25
|
+
|
|
26
|
+
For top-level \`search_tools\`, omit \`limit\` initially (the default is 10), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and declared output shapes. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
|
|
27
|
+
|
|
28
|
+
- \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for program calls. These inputs filter discovery; they grant no authority.
|
|
29
|
+
- Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and declared outputs. Never select only because a result ranks first or has fewer required inputs.
|
|
30
|
+
- Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
|
|
31
|
+
- Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
|
|
32
|
+
- Reduce through declared output keys. Do not guess collection roots such as \`items\` or \`results\`. If a match or result key is missing, inspect, re-search, or describe inside the same run instead of returning discovery for another call.
|
|
33
|
+
|
|
34
|
+
Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and read-only gates run below the sandbox; code cannot widen its authority.
|
|
35
|
+
|
|
36
|
+
## Errors and repair
|
|
37
|
+
|
|
38
|
+
Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Batch failures expose the same classification in \`errorDetails\`. Branch on fields, never prose. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
|
|
39
|
+
|
|
40
|
+
- \`destructive_tool_requires_approval\`: stop the program and use the returned canonical address with top-level \`call_destructive_tool\`.
|
|
41
|
+
- \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
|
|
42
|
+
- A truncated direct-call result: retry \`call_tool\` with \`fields\`, or follow its \`get_result\` action. A truncated program result has no page handle; filter, map, or slice inside a new program.
|
|
43
|
+
- Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
|
|
44
|
+
|
|
45
|
+
For a direct call, \`fields\` selects JSON dot-paths and \`[]\` traverses arrays, for example \`results[].id\`. Projection misses return \`data\` plus \`$connecta\` feedback. \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline. \`maxRetries\` is honored only for safely annotated tools. \`diagnostics: true\` adds timing.
|
|
46
|
+
|
|
47
|
+
\`get_result({ id, offset?, maxBytes? })\` returns \`{ text, offset, nextOffset?, totalBytes }\` for a direct-call result. Both sizes are byte counts: \`maxBytes\` must be a whole number at least 1 and defaults to the deployment cap; \`offset\` must be a whole number at least 0 and defaults to 0. An offset inside a multi-byte character moves back to its first byte, and the response reports the served offset. Follow \`nextOffset\` to reassemble pages. An unknown or expired id is an error.
|
|
48
|
+
|
|
49
|
+
Limits: 20 host calls per run, 10 calls per batch, and a 15-second deadline per host call.
|
|
50
|
+
|
|
51
|
+
## Runtime portability
|
|
52
|
+
|
|
53
|
+
Portable code uses only connector globals, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
One read-only call at a known address:
|
|
58
|
+
|
|
59
|
+
\`async () => await connecta.call("crm.get_account", { id: "acct_42" })\`
|
|
19
60
|
|
|
20
|
-
|
|
61
|
+
Dependent calls, only when the second needs a value from the first:
|
|
21
62
|
|
|
22
|
-
|
|
23
|
-
- Exact schemas: \`connecta.describe({ address: "connector.tool" })\` for one, \`{ addresses: [...] }\` for many; \`format: "json"\` only for exact constraints.
|
|
24
|
-
- Two to ten independent calls: \`connecta.batch([...])\`. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails: { code, retryable } }\` — how a program tells a policy refusal from a transient failure.
|
|
25
|
-
- Search inside the run, not before it; return only the reduction the answer needs, never raw payloads.
|
|
26
|
-
- Only tools annotated \`readOnlyHint: true\` are reachable; the gate, credentials, and admission are enforced below the sandbox — nothing a program does widens its reach.
|
|
63
|
+
\`async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const address = (suffix) => { const tool = tools.find((entry) => entry.address.endsWith(suffix)); if (!tool) throw new Error("missing " + suffix); return tool.address; }; const run = await connecta.call(address(".get_run"), { runId: 42 }); const logs = await connecta.call(address(".get_job_logs"), { jobId: run.failedJobId }); return logs.map(({ timestamp, message }) => ({ timestamp, message })); }\`
|
|
27
64
|
|
|
28
65
|
## Rendering a view
|
|
29
66
|
|
|
30
|
-
\`connecta.ui(html)\` renders
|
|
67
|
+
\`connecta.emit\` accepts text, image, or audio blocks and delivers them only on success. \`connecta.ui(html)\` renders one success-only display view outside model context. One argument is display-only. Bind read-only refresh or drill-down calls with \`{ reads: { name: { address, fixedArgs?, viewArgs? } } }\`; page markup calls \`connecta.read(name, args)\`. Admission and one shared budget apply to the UI and emitted content, not separate budgets. Fetch and check the data shape first. On empty or missing data, return a trimmed first record instead of rendering. Otherwise render returned variables and return the same initial summary because the model reads the return value, not the view. A second, invalid, or over-budget UI call throws catchably.
|
|
31
68
|
|
|
32
69
|
`;
|
|
33
70
|
/** Deployment-scoped guide routing appended to the shared usage guide. */
|
|
34
71
|
const CONNECTOR_GUIDES_SECTION = `
|
|
35
72
|
## Per-connector guides
|
|
36
73
|
|
|
37
|
-
|
|
74
|
+
Connector guides appear in \`skills({})\` and discovery with an exact \`guide\` name and bounded \`guideSummary\`. Fetch only a listed or carried name with \`skills({ name: <guide> })\`; never infer one from a connector id. \`guideRequired: true\` is a hard stop. \`guideRequiredReasons\` says why: \`connector_required\` and \`approval_required\` stand after schema expansion; \`schema_truncated\` clears after exact describe. Otherwise fetch for a relevant sequence, unit, pagination rule, alias, or API convention. A read-only call with a complete compact schema may skip an irrelevant guide. Connector guides never apply to another deployment.
|
|
38
75
|
`;
|
|
39
76
|
/** Shared Connecta routing guidance, byte-identical across deployments. */
|
|
40
77
|
export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
|
|
@@ -80,17 +117,63 @@ export function connectorGuide(connector) {
|
|
|
80
117
|
const content = typeof guide === "string" ? guide : guide?.content;
|
|
81
118
|
return content && content.trim() !== "" ? content : undefined;
|
|
82
119
|
}
|
|
83
|
-
|
|
120
|
+
/** Discovery budget for one connector-guide summary, including an ellipsis. */
|
|
121
|
+
export const GUIDE_SUMMARY_LENGTH = 120;
|
|
84
122
|
/** A `---`/`***`/`___` rule, which also opens and closes YAML frontmatter. */
|
|
85
123
|
const RULE_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
|
|
86
124
|
/** A fenced code block's delimiter. */
|
|
87
125
|
const FENCE_RE = /^\s*(?:```|~~~)/;
|
|
126
|
+
/** Markdown blocks that end a paragraph without a blank physical line. */
|
|
127
|
+
const HEADING_RE = /^\s*#{1,6}/;
|
|
128
|
+
const LIST_ITEM_RE = /^\s*(?:[-*+]\s+|\d+[.)]\s+)/;
|
|
129
|
+
const SETEXT_UNDERLINE_RE = /^\s*=+\s*$/;
|
|
130
|
+
const TABLE_DELIMITER_RE = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
|
|
131
|
+
const OPENS_CLAUSE_RE = /^(?:The|A|An)\b/u;
|
|
132
|
+
const ARTICLE_RE = /^(?:the|a|an)\b/u;
|
|
88
133
|
/**
|
|
89
134
|
* Markup that carries no summary text of its own: horizontal rules, HTML
|
|
90
135
|
* comments, and table rows. Skipped so a guide that opens with one is
|
|
91
136
|
* summarized by its first real line instead of by punctuation.
|
|
92
137
|
*/
|
|
93
138
|
const NOT_SUMMARY_RE = /^\s*(?:<!--|\|)|^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
|
|
139
|
+
/** A standard Markdown table starts with a pipe-bearing row and delimiter. */
|
|
140
|
+
function startsTable(lines, index) {
|
|
141
|
+
const header = lines[index] ?? "";
|
|
142
|
+
const delimiter = lines[index + 1] ?? "";
|
|
143
|
+
return header.includes("|") && TABLE_DELIMITER_RE.test(delimiter);
|
|
144
|
+
}
|
|
145
|
+
/** True when a sentence-looking period belongs to an abbreviation. */
|
|
146
|
+
function isAbbreviation(text, end) {
|
|
147
|
+
const token = text.slice(0, end).match(/\S+$/u)?.[0] ?? "";
|
|
148
|
+
// These introduce an example or restatement even before a capitalized word.
|
|
149
|
+
if (/^(?:e\.g|i\.e)\.$/iu.test(token))
|
|
150
|
+
return true;
|
|
151
|
+
// An initial or title belongs to the proper name that follows it.
|
|
152
|
+
if (/^[A-Z]\.$/u.test(token))
|
|
153
|
+
return true;
|
|
154
|
+
if (/^(?:Mr|Mrs|Ms|Dr|Prof|Sr|Jr|St)\.$/iu.test(token))
|
|
155
|
+
return true;
|
|
156
|
+
if (!/^(?:[A-Za-z]\.){2,}$/u.test(token) &&
|
|
157
|
+
!/^(?:vs|etc|approx|dept|fig|no)\.$/iu.test(token)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// Initialisms can end a sentence or extend a name ("U.S. East region").
|
|
161
|
+
// The mistakes are asymmetric: a false ending presents a fragment as a
|
|
162
|
+
// complete thought, while a missed ending gets an honest ellipsis. Count
|
|
163
|
+
// the period only with narrow evidence of a new clause: an article in one
|
|
164
|
+
// of its first two words. This is grammar evidence, not a starter-word list.
|
|
165
|
+
const following = text
|
|
166
|
+
.slice(end)
|
|
167
|
+
.match(/^[)\]}'"”’]*\s+(\S+)(?:\s+(\S+))?/u);
|
|
168
|
+
if (!following)
|
|
169
|
+
return false;
|
|
170
|
+
const [, nextWord, afterNext] = following;
|
|
171
|
+
const startsClause = nextWord !== undefined &&
|
|
172
|
+
/^\p{Lu}/u.test(nextWord) &&
|
|
173
|
+
(OPENS_CLAUSE_RE.test(nextWord) ||
|
|
174
|
+
(afterNext !== undefined && ARTICLE_RE.test(afterNext)));
|
|
175
|
+
return !startsClause;
|
|
176
|
+
}
|
|
94
177
|
/** Drop a leading YAML frontmatter block — metadata, not summary text. */
|
|
95
178
|
function withoutFrontmatter(lines) {
|
|
96
179
|
let start = 0;
|
|
@@ -102,33 +185,107 @@ function withoutFrontmatter(lines) {
|
|
|
102
185
|
const close = lines.findIndex((line, i) => i > start && RULE_RE.test(line));
|
|
103
186
|
return close === -1 ? lines : lines.slice(close + 1);
|
|
104
187
|
}
|
|
188
|
+
/** Normalize authored and derived summaries under one construction contract. */
|
|
189
|
+
export function normalizeGuideSummary(summary) {
|
|
190
|
+
const normalized = summary.replace(/\s+/g, " ").trim();
|
|
191
|
+
return normalized === "" ? undefined : normalized;
|
|
192
|
+
}
|
|
105
193
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* markup.
|
|
194
|
+
* Shorten a normalized summary at the strongest readable boundary available.
|
|
195
|
+
* A complete sentence needs no ellipsis; clause and word cuts do, so discovery
|
|
196
|
+
* never presents an unfinished fragment as the guide's complete thought.
|
|
110
197
|
*/
|
|
111
198
|
function boundedSummary(summary) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
199
|
+
const normalized = normalizeGuideSummary(summary);
|
|
200
|
+
if (!normalized)
|
|
114
201
|
return undefined;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
202
|
+
if (normalized.length <= GUIDE_SUMMARY_LENGTH)
|
|
203
|
+
return normalized;
|
|
204
|
+
const contentBudget = GUIDE_SUMMARY_LENGTH - 1;
|
|
205
|
+
let sentenceEnd = 0;
|
|
206
|
+
const sentenceBoundary = /[.!?…。!?](?:[)\]}'"”’]+)?(?=\s|$)/gu;
|
|
207
|
+
for (const match of normalized.matchAll(sentenceBoundary)) {
|
|
208
|
+
const end = (match.index ?? 0) + match[0].length;
|
|
209
|
+
if (end > GUIDE_SUMMARY_LENGTH)
|
|
210
|
+
break;
|
|
211
|
+
const punctuationEnd = (match.index ?? 0) + 1;
|
|
212
|
+
if (match[0].startsWith(".") &&
|
|
213
|
+
isAbbreviation(normalized, punctuationEnd)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// Do not mistake another short fragment for a useful complete thought.
|
|
217
|
+
if (end >= 24)
|
|
218
|
+
sentenceEnd = end;
|
|
219
|
+
}
|
|
220
|
+
if (sentenceEnd > 0)
|
|
221
|
+
return normalized.slice(0, sentenceEnd);
|
|
222
|
+
const available = normalized.slice(0, contentBudget);
|
|
223
|
+
let clauseEnd = 0;
|
|
224
|
+
const clauseBoundary = /[,;:](?=\s)|\s[—–-](?=\s)/g;
|
|
225
|
+
for (const match of available.matchAll(clauseBoundary)) {
|
|
226
|
+
const end = match.index ?? 0;
|
|
227
|
+
// Prefer a clause only when it retains most of the discovery budget.
|
|
228
|
+
if (end >= 80)
|
|
229
|
+
clauseEnd = end;
|
|
230
|
+
}
|
|
231
|
+
if (clauseEnd > 0) {
|
|
232
|
+
return `${available.slice(0, clauseEnd).trimEnd()}…`;
|
|
233
|
+
}
|
|
234
|
+
const wordEnd = available.search(/\s+\S*$/);
|
|
235
|
+
if (wordEnd > 0) {
|
|
236
|
+
const prefix = available
|
|
237
|
+
.slice(0, wordEnd)
|
|
238
|
+
.trimEnd()
|
|
239
|
+
.replace(/[,;:([{—–-]+$/u, "")
|
|
240
|
+
.trimEnd();
|
|
241
|
+
if (prefix !== "")
|
|
242
|
+
return `${prefix}…`;
|
|
243
|
+
}
|
|
244
|
+
let hardEnd = contentBudget;
|
|
245
|
+
const code = normalized.charCodeAt(hardEnd - 1);
|
|
246
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
247
|
+
hardEnd--;
|
|
248
|
+
return `${normalized.slice(0, hardEnd)}…`;
|
|
118
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* One thought describing a guide for the cheap list view: the first meaningful
|
|
252
|
+
* paragraph, joined across physical lines, with headings and the connector
|
|
253
|
+
* description as fallbacks when the guide opens with markup alone.
|
|
254
|
+
*/
|
|
119
255
|
function summarizeGuide(connector, guide) {
|
|
256
|
+
const lines = withoutFrontmatter(guide.split("\n"));
|
|
120
257
|
let inFence = false;
|
|
258
|
+
let inComment = false;
|
|
121
259
|
let headingFallback;
|
|
122
|
-
for (
|
|
260
|
+
for (let index = 0; index < lines.length; index++) {
|
|
261
|
+
const raw = lines[index] ?? "";
|
|
262
|
+
if (inComment) {
|
|
263
|
+
if (raw.includes("-->"))
|
|
264
|
+
inComment = false;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
123
267
|
if (FENCE_RE.test(raw)) {
|
|
124
268
|
inFence = !inFence;
|
|
125
269
|
continue;
|
|
126
270
|
}
|
|
127
271
|
if (inFence)
|
|
128
272
|
continue;
|
|
273
|
+
if (raw.trimStart().startsWith("<!--")) {
|
|
274
|
+
if (!raw.includes("-->"))
|
|
275
|
+
inComment = true;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (startsTable(lines, index)) {
|
|
279
|
+
index++;
|
|
280
|
+
while (index + 1 < lines.length &&
|
|
281
|
+
(lines[index + 1] ?? "").includes("|")) {
|
|
282
|
+
index++;
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
129
286
|
if (raw.trim() === "" || NOT_SUMMARY_RE.test(raw))
|
|
130
287
|
continue;
|
|
131
|
-
const heading =
|
|
288
|
+
const heading = HEADING_RE.test(raw);
|
|
132
289
|
const line = raw
|
|
133
290
|
// `\s*` (not `\s+`) so a bare `#` strips to nothing and is skipped, and
|
|
134
291
|
// an unspaced `#Heading` is still read as a heading.
|
|
@@ -142,7 +299,24 @@ function summarizeGuide(connector, guide) {
|
|
|
142
299
|
headingFallback ??= boundedSummary(line);
|
|
143
300
|
continue;
|
|
144
301
|
}
|
|
145
|
-
|
|
302
|
+
const paragraph = [line];
|
|
303
|
+
while (index + 1 < lines.length) {
|
|
304
|
+
const next = lines[index + 1] ?? "";
|
|
305
|
+
if (next.trim() === "" ||
|
|
306
|
+
startsTable(lines, index + 1) ||
|
|
307
|
+
FENCE_RE.test(next) ||
|
|
308
|
+
HEADING_RE.test(next) ||
|
|
309
|
+
LIST_ITEM_RE.test(next) ||
|
|
310
|
+
SETEXT_UNDERLINE_RE.test(next) ||
|
|
311
|
+
NOT_SUMMARY_RE.test(next)) {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
paragraph.push(next.trim());
|
|
315
|
+
index++;
|
|
316
|
+
if (paragraph.join(" ").length > GUIDE_SUMMARY_LENGTH)
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
return boundedSummary(paragraph.join(" ")) ?? line;
|
|
146
320
|
}
|
|
147
321
|
if (headingFallback)
|
|
148
322
|
return headingFallback;
|
|
@@ -155,6 +329,8 @@ export function connectorGuideSummary(connector) {
|
|
|
155
329
|
if (!guide)
|
|
156
330
|
return undefined;
|
|
157
331
|
const configured = typeof connector.usageGuide === "object"
|
|
332
|
+
// Registry construction rejects over-budget configured summaries. Keep
|
|
333
|
+
// normalization here so direct Connector callers see the same text.
|
|
158
334
|
? boundedSummary(connector.usageGuide.summary ?? "")
|
|
159
335
|
: undefined;
|
|
160
336
|
return configured ?? summarizeGuide(connector, guide);
|
package/dist/types.d.ts
CHANGED
|
@@ -180,6 +180,12 @@ export interface CatalogDriftReport extends CatalogDriftCounts {
|
|
|
180
180
|
/** When the observation was taken; never when a probe was scheduled. */
|
|
181
181
|
observedAt: string;
|
|
182
182
|
}
|
|
183
|
+
/** The last agent-facing catalog read this runtime served for one connector. */
|
|
184
|
+
export interface CatalogAccessObservation {
|
|
185
|
+
/** Fresh means the read used a live or unexpired catalog; stale means SWR. */
|
|
186
|
+
state: "fresh" | "stale";
|
|
187
|
+
observedAt: string;
|
|
188
|
+
}
|
|
183
189
|
export interface ConnectorStatus {
|
|
184
190
|
state: ConnectorStatusState;
|
|
185
191
|
/** When state === "auth_required", the URL the operator should open. */
|
|
@@ -193,6 +199,11 @@ export interface ConnectorStatus {
|
|
|
193
199
|
* isolate or process has seen nothing, not that nothing drifted.
|
|
194
200
|
*/
|
|
195
201
|
catalogDrift?: CatalogDriftReport;
|
|
202
|
+
/**
|
|
203
|
+
* The last agent-facing catalog read in this runtime. This payload-free
|
|
204
|
+
* observation is not persisted and operator reads do not replace it.
|
|
205
|
+
*/
|
|
206
|
+
catalogAccess?: CatalogAccessObservation;
|
|
196
207
|
}
|
|
197
208
|
/** The whole plugin contract — the one open seam. */
|
|
198
209
|
export interface Connector {
|
|
@@ -313,8 +324,9 @@ export interface ConnectorUsageGuide {
|
|
|
313
324
|
/** Markdown returned verbatim by `skills({ name: "connector:<id>" })`. */
|
|
314
325
|
content: string;
|
|
315
326
|
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
327
|
+
* Discovery hint describing the conventions the guide covers. Whitespace is
|
|
328
|
+
* normalized, and a value over 120 characters throws at construction. When
|
|
329
|
+
* omitted, Connecta derives a bounded summary from the first body paragraph.
|
|
318
330
|
*/
|
|
319
331
|
summary?: string;
|
|
320
332
|
/**
|
package/dist/ui.js
CHANGED
|
@@ -374,6 +374,9 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
374
374
|
// `boundedCatalogDrift`, so what lands here cannot carry a name or a
|
|
375
375
|
// schema even if the plugin seam returned one.
|
|
376
376
|
...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
|
|
377
|
+
...(status.catalogAccess
|
|
378
|
+
? { catalogAccess: status.catalogAccess }
|
|
379
|
+
: {}),
|
|
377
380
|
...(c.disconnectAuth && c.startAuth ? { oauth: true } : {}),
|
|
378
381
|
...(credential ? { credential } : {}),
|
|
379
382
|
};
|
|
@@ -461,7 +464,7 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
|
|
|
461
464
|
: `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
|
|
462
465
|
: "";
|
|
463
466
|
const clerkScript = clerk && clerkScriptOrigin
|
|
464
|
-
? `<script${nonceAttr}
|
|
467
|
+
? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
465
468
|
: "";
|
|
466
469
|
return `<!doctype html>
|
|
467
470
|
<html lang="en">
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -26,7 +26,10 @@ ethos invariant, not a style preference: a client retained across requests on
|
|
|
26
26
|
Workers is a cross-request capability leak, and a promise awaited after the
|
|
27
27
|
response is work the runtime may have already torn down. Deferred work has one
|
|
28
28
|
sanctioned channel — `ctx.waitUntil`, threaded through `fetch(request, env,
|
|
29
|
-
ctx)
|
|
29
|
+
ctx)`. Best-effort activity writes use it. An agent read that already demanded
|
|
30
|
+
an expired catalog refresh may also use it while serving a complete catalog
|
|
31
|
+
inside its stale window. That refresh owns a new scope and deadline; it never
|
|
32
|
+
carries the inbound scope or signal past the request.
|
|
30
33
|
|
|
31
34
|
The registry is deliberately on the long side of that line and the MCP server
|
|
32
35
|
deliberately on the short side. A fresh `McpServer` per request is what makes
|
|
@@ -87,8 +90,8 @@ owns or hands out, and a change usually belongs in exactly one of them:
|
|
|
87
90
|
|
|
88
91
|
| Module | Owns |
|
|
89
92
|
| --- | --- |
|
|
90
|
-
| `src/registry.ts` | The connector set,
|
|
91
|
-
| `src/catalog-service.ts` |
|
|
93
|
+
| `src/registry.ts` | The connector set, address resolution, catalog TTL/persistence/completeness, shared refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
|
|
94
|
+
| `src/catalog-service.ts` | Request-local tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
|
|
92
95
|
| `src/invocation.ts` | One tool call: argument validation, call admission, per-attempt timeout, retry with the connector's own `Retry-After` honoured exactly or declined, result unwrapping, size capping, and the activity record. |
|
|
93
96
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
|
|
94
97
|
|
|
@@ -137,7 +140,7 @@ src/
|
|
|
137
140
|
apps-shell.ts the one build-time MCP Apps template
|
|
138
141
|
skills.ts MCP instructions, the usage skill, connector guides
|
|
139
142
|
registry.ts connector set, addresses, health, call limiters
|
|
140
|
-
catalog-service.ts catalog
|
|
143
|
+
catalog-service.ts request-local catalog access, search, and describe
|
|
141
144
|
catalog.ts ranking, summaries, compact schema rendering
|
|
142
145
|
invocation.ts one tool call, end to end
|
|
143
146
|
catalog-drift.ts vetted manifests and the counts a refresh produces
|
|
@@ -182,7 +185,7 @@ src/
|
|
|
182
185
|
| The core imports no `node:` builtin and reaches no Node-only module | `test/purity.test.ts` |
|
|
183
186
|
| The published surface matches the same boundary | `test/package-surface.test.ts`, `scripts/check-package.mjs` |
|
|
184
187
|
| Route order, per-route auth, and byte-exact refusals | `test/server-route-contracts.test.ts` |
|
|
185
|
-
| `/mcp` end to end, the open routes, exactly seven tools | `test/server.test.ts`, `test/code-first-surface.test.ts` |
|
|
188
|
+
| `/mcp` end to end, the open routes, exactly seven tools, bounded connector orientation | `test/server.test.ts`, `test/code-first-surface.test.ts` |
|
|
186
189
|
| Construction-time refusals and the grouped config boundary | `test/config.test.ts`, `test/registry.test.ts` |
|
|
187
190
|
| Program and top-level calls take the same enforced path | `test/execute.test.ts` |
|
|
188
191
|
| Both deployment shapes still compile and configure the real thing | `test/deployment-shapes.test.ts`, `npm run check:examples` |
|