@zackbart/connecta 0.18.1 → 0.18.3
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 +80 -0
- package/README.md +4 -0
- package/dist/catalog-service.d.ts +4 -0
- package/dist/catalog-service.js +22 -6
- package/dist/connectors/remote-mcp.d.ts +49 -2
- package/dist/connectors/remote-mcp.js +302 -7
- package/dist/invocation.js +9 -3
- package/dist/providers/linear.d.ts +7 -1
- package/dist/providers/linear.js +12 -2
- package/dist/providers/mixpanel.d.ts +5 -1
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/revenuecat.d.ts +3 -1
- package/dist/providers/revenuecat.js +12 -3
- package/dist/providers/stripe.d.ts +9 -3
- package/dist/providers/stripe.js +24 -5
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +9 -0
- package/dist/result-shapes.d.ts +13 -0
- package/dist/result-shapes.js +331 -0
- package/dist/skills.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +5 -2
- package/documentation/code-mode.md +6 -6
- package/documentation/connectors.md +35 -0
- package/documentation/linear.md +25 -4
- package/documentation/meta-tools.md +22 -3
- package/documentation/mixpanel.md +19 -0
- package/documentation/operations.md +3 -1
- package/documentation/provider-conventions.md +37 -21
- package/documentation/revenuecat.md +23 -1
- package/documentation/storage-and-credentials.md +55 -0
- package/documentation/stripe.md +22 -2
- package/documentation/upgrading.md +32 -4
- package/ethos.md +3 -3
- package/examples/worker/src/index.ts +5 -0
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/dist/registry.d.ts
CHANGED
|
@@ -113,6 +113,10 @@ export interface RegistryView {
|
|
|
113
113
|
observedSuccessAt(id: string): string | undefined;
|
|
114
114
|
/** Local declared-vs-stored credential mismatch, with no downstream I/O. */
|
|
115
115
|
credentialDriftFor(id: string): Promise<string | undefined>;
|
|
116
|
+
/** Value-free shape learned from successful calls, never a provider declaration. */
|
|
117
|
+
observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
|
|
118
|
+
/** Passively learn one successful unwrapped result; failures stay isolated. */
|
|
119
|
+
observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
116
120
|
statusFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ConnectorStatus>;
|
|
117
121
|
invalidateStored(id: string): Promise<void>;
|
|
118
122
|
}
|
|
@@ -141,6 +145,7 @@ export declare class Registry implements RegistryView {
|
|
|
141
145
|
private readonly health;
|
|
142
146
|
/** Last drift counts reported to activity, per connector, in this runtime. */
|
|
143
147
|
private readonly reportedDrift;
|
|
148
|
+
private readonly observedOutputSchemas;
|
|
144
149
|
private readonly ttlMs;
|
|
145
150
|
private readonly staleMs;
|
|
146
151
|
private readonly persistToolCatalog;
|
|
@@ -203,6 +208,8 @@ export declare class Registry implements RegistryView {
|
|
|
203
208
|
* separate from any connector's `conn:<id>:` namespace. Backs get_result.
|
|
204
209
|
*/
|
|
205
210
|
resultsStorage(): KVStorage;
|
|
211
|
+
observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
|
|
212
|
+
observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
206
213
|
/** Resolve "<connectorId>.<toolName>" → connector + tool name. */
|
|
207
214
|
resolveAddress(address: string): {
|
|
208
215
|
connector: Connector;
|
package/dist/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ import { boundedCatalogDrift } from "./catalog-drift.js";
|
|
|
7
7
|
import { fingerprintSerializedCatalog, snapshotCatalog, } from "./catalog-fingerprint.js";
|
|
8
8
|
import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES, } from "./catalog-limits.js";
|
|
9
9
|
import { mapSettledWithConcurrency } from "./concurrency.js";
|
|
10
|
+
import { ObservedOutputSchemas } from "./result-shapes.js";
|
|
10
11
|
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary, } from "./skills.js";
|
|
11
12
|
import { withAbortableTimeout } from "./timeout.js";
|
|
12
13
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
@@ -133,6 +134,7 @@ export class Registry {
|
|
|
133
134
|
health = new HealthLog();
|
|
134
135
|
/** Last drift counts reported to activity, per connector, in this runtime. */
|
|
135
136
|
reportedDrift = new Map();
|
|
137
|
+
observedOutputSchemas;
|
|
136
138
|
ttlMs;
|
|
137
139
|
staleMs;
|
|
138
140
|
persistToolCatalog;
|
|
@@ -140,6 +142,7 @@ export class Registry {
|
|
|
140
142
|
maxResultBytes;
|
|
141
143
|
constructor(connectors, opts) {
|
|
142
144
|
this.opts = opts;
|
|
145
|
+
this.observedOutputSchemas = new ObservedOutputSchemas();
|
|
143
146
|
this.ttlMs =
|
|
144
147
|
(opts.toolCacheTtlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
|
|
145
148
|
this.staleMs =
|
|
@@ -342,6 +345,12 @@ export class Registry {
|
|
|
342
345
|
resultsStorage() {
|
|
343
346
|
return namespaced(this.opts.storage, "results:");
|
|
344
347
|
}
|
|
348
|
+
observedOutputSchema(connectorId, definition) {
|
|
349
|
+
return this.observedOutputSchemas.get(connectorId, definition);
|
|
350
|
+
}
|
|
351
|
+
observeOutputShape(connectorId, definition, value) {
|
|
352
|
+
this.observedOutputSchemas.observe(connectorId, definition, value);
|
|
353
|
+
}
|
|
345
354
|
/** Resolve "<connectorId>.<toolName>" → connector + tool name. */
|
|
346
355
|
resolveAddress(address) {
|
|
347
356
|
const parts = splitAddress(address);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JsonSchema, ToolDef } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Passive, process-local output-shape learning. Provider declarations always
|
|
4
|
+
* win; observations are an open, optional-field routing aid and never a
|
|
5
|
+
* replacement contract.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ObservedOutputSchemas {
|
|
8
|
+
private readonly entries;
|
|
9
|
+
private cacheKey;
|
|
10
|
+
private set;
|
|
11
|
+
get(connectorId: string, definition: ToolDef): JsonSchema | undefined;
|
|
12
|
+
observe(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
2
|
+
const OBSERVATION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
const MAX_CACHED_SHAPES = 256;
|
|
4
|
+
const MAX_DEFINITION_BYTES = 64 * 1024;
|
|
5
|
+
const MAX_SCHEMA_BYTES = 16 * 1024;
|
|
6
|
+
const MAX_SCHEMA_DEPTH = 6;
|
|
7
|
+
const MAX_SCHEMA_NODES = 128;
|
|
8
|
+
const MAX_OBJECT_PROPERTIES = 48;
|
|
9
|
+
const MAX_ARRAY_ITEMS = 32;
|
|
10
|
+
const MAX_PROPERTY_NAME_BYTES = 128;
|
|
11
|
+
const UNSAFE_PROPERTY_NAMES = new Set([
|
|
12
|
+
"__proto__",
|
|
13
|
+
"constructor",
|
|
14
|
+
"prototype",
|
|
15
|
+
]);
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
function broadType(value) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return "null";
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return "array";
|
|
22
|
+
switch (typeof value) {
|
|
23
|
+
case "string":
|
|
24
|
+
case "boolean":
|
|
25
|
+
return typeof value;
|
|
26
|
+
case "number":
|
|
27
|
+
return Number.isFinite(value) ? "number" : undefined;
|
|
28
|
+
case "object":
|
|
29
|
+
return "object";
|
|
30
|
+
default:
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function serializedSchema(schema) {
|
|
35
|
+
try {
|
|
36
|
+
const text = JSON.stringify(schema);
|
|
37
|
+
return encoder.encode(text).byteLength <= MAX_SCHEMA_BYTES
|
|
38
|
+
? text
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function cloneSchema(schema) {
|
|
46
|
+
const serialized = serializedSchema(schema);
|
|
47
|
+
if (!serialized)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(serialized);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function definitionIdentity(definition) {
|
|
57
|
+
try {
|
|
58
|
+
const serialized = JSON.stringify(definition);
|
|
59
|
+
return encoder.encode(serialized).byteLength <= MAX_DEFINITION_BYTES
|
|
60
|
+
? serialized
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function schemaType(schema) {
|
|
68
|
+
return typeof schema.type === "string" ? schema.type : undefined;
|
|
69
|
+
}
|
|
70
|
+
function safePropertyName(name) {
|
|
71
|
+
return (!UNSAFE_PROPERTY_NAMES.has(name) &&
|
|
72
|
+
encoder.encode(name).byteLength <= MAX_PROPERTY_NAME_BYTES);
|
|
73
|
+
}
|
|
74
|
+
function boundedPropertyNames(value) {
|
|
75
|
+
const names = [];
|
|
76
|
+
for (const name in value) {
|
|
77
|
+
if (!Object.hasOwn(value, name) || !safePropertyName(name))
|
|
78
|
+
continue;
|
|
79
|
+
names.push(name);
|
|
80
|
+
if (names.length >= MAX_OBJECT_PROPERTIES)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return names.sort();
|
|
84
|
+
}
|
|
85
|
+
function unionBranches(schema) {
|
|
86
|
+
return Array.isArray(schema.anyOf)
|
|
87
|
+
? schema.anyOf
|
|
88
|
+
: [schema];
|
|
89
|
+
}
|
|
90
|
+
function branchOrder(schema) {
|
|
91
|
+
return ["null", "boolean", "number", "string", "array", "object"].indexOf(schemaType(schema) ?? "");
|
|
92
|
+
}
|
|
93
|
+
function mergeSchemas(left, right) {
|
|
94
|
+
const leftType = schemaType(left);
|
|
95
|
+
const rightType = schemaType(right);
|
|
96
|
+
if (leftType === "object" && rightType === "object") {
|
|
97
|
+
const leftProperties = left.properties && typeof left.properties === "object"
|
|
98
|
+
? left.properties
|
|
99
|
+
: {};
|
|
100
|
+
const rightProperties = right.properties && typeof right.properties === "object"
|
|
101
|
+
? right.properties
|
|
102
|
+
: {};
|
|
103
|
+
const properties = Object.create(null);
|
|
104
|
+
for (const key of [...new Set([
|
|
105
|
+
...Object.keys(leftProperties),
|
|
106
|
+
...Object.keys(rightProperties),
|
|
107
|
+
])].sort()) {
|
|
108
|
+
const leftProperty = leftProperties[key];
|
|
109
|
+
const rightProperty = rightProperties[key];
|
|
110
|
+
properties[key] =
|
|
111
|
+
leftProperty && rightProperty
|
|
112
|
+
? mergeSchemas(leftProperty, rightProperty)
|
|
113
|
+
: (leftProperty ?? rightProperty);
|
|
114
|
+
}
|
|
115
|
+
return { type: "object", properties };
|
|
116
|
+
}
|
|
117
|
+
if (leftType === "array" && rightType === "array") {
|
|
118
|
+
const leftItems = left.items && typeof left.items === "object"
|
|
119
|
+
? left.items
|
|
120
|
+
: undefined;
|
|
121
|
+
const rightItems = right.items && typeof right.items === "object"
|
|
122
|
+
? right.items
|
|
123
|
+
: undefined;
|
|
124
|
+
return {
|
|
125
|
+
type: "array",
|
|
126
|
+
...(leftItems || rightItems
|
|
127
|
+
? {
|
|
128
|
+
items: leftItems && rightItems
|
|
129
|
+
? mergeSchemas(leftItems, rightItems)
|
|
130
|
+
: (leftItems ?? rightItems),
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (leftType && leftType === rightType)
|
|
136
|
+
return left;
|
|
137
|
+
const byType = new Map();
|
|
138
|
+
for (const branch of [...unionBranches(left), ...unionBranches(right)]) {
|
|
139
|
+
const type = schemaType(branch);
|
|
140
|
+
if (!type)
|
|
141
|
+
continue;
|
|
142
|
+
const existing = byType.get(type);
|
|
143
|
+
byType.set(type, existing ? mergeSchemas(existing, branch) : branch);
|
|
144
|
+
}
|
|
145
|
+
const branches = [...byType.values()].sort((a, b) => branchOrder(a) - branchOrder(b));
|
|
146
|
+
return branches.length === 1 ? branches[0] : { anyOf: branches };
|
|
147
|
+
}
|
|
148
|
+
function inferSchema(value, budget, depth = 0) {
|
|
149
|
+
const type = broadType(value);
|
|
150
|
+
if (!type || depth > MAX_SCHEMA_DEPTH || budget.nodes >= MAX_SCHEMA_NODES) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
budget.nodes++;
|
|
154
|
+
if (type !== "object" && type !== "array")
|
|
155
|
+
return { type };
|
|
156
|
+
const object = value;
|
|
157
|
+
if (budget.seen.has(object))
|
|
158
|
+
return undefined;
|
|
159
|
+
budget.seen.add(object);
|
|
160
|
+
try {
|
|
161
|
+
if (type === "array") {
|
|
162
|
+
let items;
|
|
163
|
+
for (const item of value.slice(0, MAX_ARRAY_ITEMS)) {
|
|
164
|
+
const inferred = inferSchema(item, budget, depth + 1);
|
|
165
|
+
if (inferred)
|
|
166
|
+
items = items ? mergeSchemas(items, inferred) : inferred;
|
|
167
|
+
if (budget.nodes >= MAX_SCHEMA_NODES)
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
return { type: "array", ...(items ? { items } : {}) };
|
|
171
|
+
}
|
|
172
|
+
const properties = Object.create(null);
|
|
173
|
+
const record = value;
|
|
174
|
+
for (const key of boundedPropertyNames(record)) {
|
|
175
|
+
const inferred = inferSchema(record[key], budget, depth + 1);
|
|
176
|
+
if (inferred)
|
|
177
|
+
properties[key] = inferred;
|
|
178
|
+
if (budget.nodes >= MAX_SCHEMA_NODES)
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
return { type: "object", properties };
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
budget.seen.delete(object);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function boundSchema(schema, budget, depth = 0) {
|
|
188
|
+
if (depth > MAX_SCHEMA_DEPTH || budget.nodes >= MAX_SCHEMA_NODES) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
budget.nodes++;
|
|
192
|
+
if (Array.isArray(schema.anyOf)) {
|
|
193
|
+
const branches = schema.anyOf
|
|
194
|
+
.slice(0, 6)
|
|
195
|
+
.map((branch) => boundSchema(branch, budget, depth + 1))
|
|
196
|
+
.filter((branch) => Boolean(branch));
|
|
197
|
+
return branches.length >= 2 ? { anyOf: branches } : branches[0];
|
|
198
|
+
}
|
|
199
|
+
const type = schemaType(schema);
|
|
200
|
+
if (!type)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (type === "array") {
|
|
203
|
+
const items = schema.items && typeof schema.items === "object"
|
|
204
|
+
? boundSchema(schema.items, budget, depth + 1)
|
|
205
|
+
: undefined;
|
|
206
|
+
return { type: "array", ...(items ? { items } : {}) };
|
|
207
|
+
}
|
|
208
|
+
if (type === "object") {
|
|
209
|
+
const source = schema.properties && typeof schema.properties === "object"
|
|
210
|
+
? schema.properties
|
|
211
|
+
: {};
|
|
212
|
+
const properties = Object.create(null);
|
|
213
|
+
let included = 0;
|
|
214
|
+
for (const key of Object.keys(source).sort()) {
|
|
215
|
+
if (included >= MAX_OBJECT_PROPERTIES ||
|
|
216
|
+
!safePropertyName(key)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const property = boundSchema(source[key], budget, depth + 1);
|
|
220
|
+
if (property) {
|
|
221
|
+
properties[key] = property;
|
|
222
|
+
included++;
|
|
223
|
+
}
|
|
224
|
+
if (budget.nodes >= MAX_SCHEMA_NODES)
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
return { type: "object", properties };
|
|
228
|
+
}
|
|
229
|
+
return { type };
|
|
230
|
+
}
|
|
231
|
+
function observedSchema(value) {
|
|
232
|
+
try {
|
|
233
|
+
const inferred = inferSchema(value, { nodes: 0, seen: new WeakSet() });
|
|
234
|
+
const schema = inferred
|
|
235
|
+
? boundSchema(inferred, { nodes: 0 })
|
|
236
|
+
: undefined;
|
|
237
|
+
return schema && serializedSchema(schema) ? schema : undefined;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Passive, process-local output-shape learning. Provider declarations always
|
|
245
|
+
* win; observations are an open, optional-field routing aid and never a
|
|
246
|
+
* replacement contract.
|
|
247
|
+
*/
|
|
248
|
+
export class ObservedOutputSchemas {
|
|
249
|
+
entries = new Map();
|
|
250
|
+
cacheKey(connectorId, definition) {
|
|
251
|
+
return JSON.stringify([connectorId, definition.name]);
|
|
252
|
+
}
|
|
253
|
+
set(key, entry) {
|
|
254
|
+
this.entries.delete(key);
|
|
255
|
+
this.entries.set(key, entry);
|
|
256
|
+
while (this.entries.size > MAX_CACHED_SHAPES) {
|
|
257
|
+
const oldest = this.entries.keys().next().value;
|
|
258
|
+
if (oldest === undefined)
|
|
259
|
+
break;
|
|
260
|
+
this.entries.delete(oldest);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
get(connectorId, definition) {
|
|
264
|
+
try {
|
|
265
|
+
const key = this.cacheKey(connectorId, definition);
|
|
266
|
+
if (definition.outputSchema) {
|
|
267
|
+
this.entries.delete(key);
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
if (!isExplicitlyReadOnly(definition)) {
|
|
271
|
+
this.entries.delete(key);
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const identity = definitionIdentity(definition);
|
|
275
|
+
if (!identity) {
|
|
276
|
+
this.entries.delete(key);
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const entry = this.entries.get(key);
|
|
280
|
+
if (!entry)
|
|
281
|
+
return undefined;
|
|
282
|
+
if (entry.definition !== identity || entry.expiresAt <= Date.now()) {
|
|
283
|
+
this.entries.delete(key);
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
this.entries.delete(key);
|
|
287
|
+
this.entries.set(key, entry);
|
|
288
|
+
return cloneSchema(entry.schema);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
observe(connectorId, definition, value) {
|
|
295
|
+
try {
|
|
296
|
+
const key = this.cacheKey(connectorId, definition);
|
|
297
|
+
if (definition.outputSchema) {
|
|
298
|
+
this.entries.delete(key);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!isExplicitlyReadOnly(definition)) {
|
|
302
|
+
this.entries.delete(key);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const identity = definitionIdentity(definition);
|
|
306
|
+
const inferred = observedSchema(value);
|
|
307
|
+
if (!identity) {
|
|
308
|
+
this.entries.delete(key);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!inferred)
|
|
312
|
+
return;
|
|
313
|
+
const current = this.entries.get(key);
|
|
314
|
+
const merged = boundSchema(current &&
|
|
315
|
+
current.definition === identity &&
|
|
316
|
+
current.expiresAt > Date.now()
|
|
317
|
+
? mergeSchemas(current.schema, inferred)
|
|
318
|
+
: inferred, { nodes: 0 });
|
|
319
|
+
if (!merged || !serializedSchema(merged))
|
|
320
|
+
return;
|
|
321
|
+
this.set(key, {
|
|
322
|
+
definition: identity,
|
|
323
|
+
expiresAt: Date.now() + OBSERVATION_TTL_MS,
|
|
324
|
+
schema: merged,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Observation is an optimization. A provider success stays successful.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
package/dist/skills.js
CHANGED
|
@@ -23,13 +23,13 @@ The minimum guest API is:
|
|
|
23
23
|
|
|
24
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
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
|
|
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 available output shapes. An observed shape carries \`outputSchemaSource: "observed"\`; treat it as routing evidence rather than a provider contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
|
|
27
27
|
|
|
28
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
|
|
29
|
+
- Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and available outputs. Never select only because a result ranks first or has fewer required inputs.
|
|
30
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
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
|
|
32
|
+
- Reduce through available output keys. Treat an observed key as a hint, since later results may omit it or add others. 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
33
|
|
|
34
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
35
|
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -14,8 +14,9 @@ stating before anything else.
|
|
|
14
14
|
|
|
15
15
|
**Per isolate, built once.** `createConnecta(config)` returns
|
|
16
16
|
`{ fetch, registry, close }`. The `Registry` owns the connector set, address
|
|
17
|
-
resolution, catalog caches, connector health, and the
|
|
18
|
-
limiters. It is constructed once and lives as long as the
|
|
17
|
+
resolution, catalog caches, observed output schemas, connector health, and the
|
|
18
|
+
per-connector call limiters. It is constructed once and lives as long as the
|
|
19
|
+
isolate or process —
|
|
19
20
|
on Workers that means a lazy module-scope singleton, which is why both
|
|
20
21
|
deployment shapes build it outside the request handler.
|
|
21
22
|
|
|
@@ -94,6 +95,7 @@ owns or hands out, and a change usually belongs in exactly one of them:
|
|
|
94
95
|
| `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. |
|
|
95
96
|
| `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. |
|
|
96
97
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
|
|
98
|
+
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
97
99
|
|
|
98
100
|
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same
|
|
99
101
|
three services. That is the point: a program's `connecta.call` and a top-level
|
|
@@ -142,6 +144,7 @@ src/
|
|
|
142
144
|
registry.ts connector set, addresses, health, call limiters
|
|
143
145
|
catalog-service.ts request-local catalog access, search, and describe
|
|
144
146
|
catalog.ts ranking, summaries, compact schema rendering
|
|
147
|
+
result-shapes.ts passive runtime-only observed output schemas
|
|
145
148
|
invocation.ts one tool call, end to end
|
|
146
149
|
catalog-drift.ts vetted manifests and the counts a refresh produces
|
|
147
150
|
credentials.ts the AES-GCM connector vault over KVStorage
|
|
@@ -149,10 +149,7 @@ a cycle, a `BigInt`, a function, a class instance — never round-trips: it eith
|
|
|
149
149
|
ends the run with an error or is converted lossily, executor's choice (`X9`).
|
|
150
150
|
Return JSON-shaped data and the question does not arise.
|
|
151
151
|
|
|
152
|
-
**P4.** Nothing survives an execution. There is no module scope, cache, or
|
|
153
|
-
scratch storage carried to the next program, and no request-bound object outlives
|
|
154
|
-
the request that created it. Within one execution, host calls share one
|
|
155
|
-
downstream request scope.
|
|
152
|
+
**P4.** Nothing survives an execution. There is no module scope, cache, or scratch storage carried to the next program, and no request-bound object outlives the request that created it. Within one execution, host calls share one downstream request scope. `S9`'s host-owned output observation is catalog metadata, not guest memory: a later program receives no prior value or object, only a labeled field/type schema through discovery.
|
|
156
153
|
|
|
157
154
|
**P5.** Plain JavaScript only. TypeScript syntax is a syntax error. Portable code
|
|
158
155
|
does not import: QuickJS blocks imports, while Dynamic Workers expose the `X5`
|
|
@@ -223,7 +220,7 @@ const page = await connecta.search({
|
|
|
223
220
|
});
|
|
224
221
|
```
|
|
225
222
|
|
|
226
|
-
**S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
|
|
223
|
+
**S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
|
|
227
224
|
|
|
228
225
|
**S1a.** `connector` loads only the named catalog; omit it only when the integration is ambiguous, because an unscoped search fans out across every configured connector. `safety: "readOnly"` returns exactly the tools available through `connecta.call`, connector shortcuts, and `connecta.batch`; `"approvalRequired"` returns the complementary fail-closed class, including false, missing, and contradictory annotations. Omitted or `"all"` preserves the complete catalog. These filters grant no authority and change no admission decision.
|
|
229
226
|
|
|
@@ -265,7 +262,7 @@ carry a route-aware `nextAction`; a close miss may add three canonical `suggesti
|
|
|
265
262
|
Catalog failures add only `retryAfterMs` when known. One bad address never fails the whole call. Each failed entry clamps its
|
|
266
263
|
caller-authored `address` to 512 UTF-8 bytes with an `…` marker. Entry order
|
|
267
264
|
correlates a clipped address with its request; successes keep canonical addresses. More than 100
|
|
268
|
-
addresses is `invalid_args`; the same 256,000-byte ceiling applies.
|
|
265
|
+
addresses is `invalid_args`; the same 256,000-byte ceiling applies. A success whose output shape came from `S9` carries `outputSchemaSource: "observed"` beside the rendered schema.
|
|
269
266
|
|
|
270
267
|
### connecta.call
|
|
271
268
|
|
|
@@ -304,6 +301,8 @@ the batch, and more than ten calls throws.
|
|
|
304
301
|
|
|
305
302
|
**S8.** Batch and thrown failures share one vocabulary (`E1`): an entry's `errorDetails.code` and `retryable` equal the fields on the error the same call would throw. Use batch for independent concurrency, not to recover lost type.
|
|
306
303
|
|
|
304
|
+
**S9.** A successful explicitly read-only call whose provider declared no `outputSchema` passively learns one from the unwrapped result. The observation retains field names and broad JSON types only: no arguments, scalar values, raw results, code, credentials, or errors. Property names may be user-authored. Objects stay open, every field stays optional, and search or describe labels the shape `outputSchemaSource: "observed"` so a model cannot mistake runtime evidence for a provider contract. Later observations merge fields and types in a process-local 256-entry LRU; a provider declaration always wins. Inference stops at depth 6, 128 schema nodes, 48 properties per object, 32 inspected array items, and 128 UTF-8 bytes per property name; `__proto__`, `constructor`, and `prototype` names are discarded. A tool definition over 64 KiB or an observed schema over 16 KiB is ignored. An entry expires after 24 hours and carries the exact serialized tool definition, so a changed catalog entry, process restart, or Worker isolate eviction starts cold. A failed call or failed result-processing step learns nothing, and any observation failure is discarded without changing a successful call. No discovery read, timer, refresh, background job, or storage adapter executes or persists work for this cache: the result-sampling refusal in [#282](https://github.com/zackbart/connecta/issues/282) stands.
|
|
305
|
+
|
|
307
306
|
### connecta.emit
|
|
308
307
|
|
|
309
308
|
```js
|
|
@@ -847,6 +846,7 @@ the upstream `Executor` shape assignable.
|
|
|
847
846
|
| `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
|
|
848
847
|
| `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (batch cap) |
|
|
849
848
|
| `S8`, `E1`, `X11` | both guest-contract executors (caught call, namespace, discovery, utility, batch-validation, budget, and forgery cases; typed batch equivalence) |
|
|
849
|
+
| `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, and failure isolation) |
|
|
850
850
|
| `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, batch, and uncaught validation recovery), `test/meta-tools.test.ts` (direct, destructive, batch, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
|
|
851
851
|
| `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
|
|
852
852
|
| `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (destructive) |
|
|
@@ -268,6 +268,16 @@ Tool calls must use the shared invocation path. That keeps direct calls, batch
|
|
|
268
268
|
children, and code-mode host calls aligned on safety, retries, admission,
|
|
269
269
|
timeouts, validation, result guards, and typed failures.
|
|
270
270
|
|
|
271
|
+
That path also learns an observed output schema after a successful explicitly
|
|
272
|
+
read-only call when the catalog declared none. The observation retains field
|
|
273
|
+
names and broad JSON types rather than a result. Object fields stay optional,
|
|
274
|
+
additional fields stay allowed, discovery labels the source `observed`, and any
|
|
275
|
+
later provider declaration wins. Property names can be user-authored data, so
|
|
276
|
+
the bounded cache stays in this process or Worker isolate and expires entries
|
|
277
|
+
after 24 hours. An exact tool-definition comparison rejects stale shapes. This
|
|
278
|
+
does not weaken the catalog completeness rule or the refusal of result sampling:
|
|
279
|
+
no catalog read executes a tool.
|
|
280
|
+
|
|
271
281
|
Connector usage guides are configuration too. `usageGuide` accepts the
|
|
272
282
|
historical markdown string or `{ content, summary?, required? }`; the latter
|
|
273
283
|
lets discovery explain what the guide covers without loading it. The summary
|
|
@@ -294,3 +304,28 @@ server issuer discovered and validated by the SDK; see
|
|
|
294
304
|
[storage and credentials](./storage-and-credentials.md#downstream-oauth).
|
|
295
305
|
The callback route validates `state` before passing the complete callback query
|
|
296
306
|
to the SDK so RFC 9207 `iss` validation is not lost.
|
|
307
|
+
|
|
308
|
+
A remote MCP connector that authenticates with a static key has two ways to
|
|
309
|
+
receive one. `{ type: "headers", headers }` bakes the literal value into the
|
|
310
|
+
deployment file, which suits a secret the runtime already holds.
|
|
311
|
+
`{ type: "credential" }` declares the slot instead and lets an operator paste
|
|
312
|
+
the key at `/credentials`, where it is encrypted at rest and rotatable without
|
|
313
|
+
a redeploy:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
remoteMcp("revenuecat_bepresent", {
|
|
317
|
+
url: "https://mcp.revenuecat.ai/mcp",
|
|
318
|
+
auth: {
|
|
319
|
+
type: "credential",
|
|
320
|
+
credential: { label: "API v2 secret key" },
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Header name and framing are configurable — `header` defaults to
|
|
326
|
+
`Authorization`, `scheme` to `Bearer`, `scheme: null` sends the value bare, and
|
|
327
|
+
a `Basic` framing base64-encodes a `user:secret` pair. Every maintained hosted
|
|
328
|
+
connection takes the shape through its existing `auth` option and fills in its
|
|
329
|
+
own label and framing. The full behavior, including rotation and the empty-slot
|
|
330
|
+
state, is in
|
|
331
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
package/documentation/linear.md
CHANGED
|
@@ -65,9 +65,12 @@ connection; it now answers 404.
|
|
|
65
65
|
## Authentication
|
|
66
66
|
|
|
67
67
|
OAuth 2.1 with dynamic client registration is the default and keeps each
|
|
68
|
-
connector instance's flow and tokens in its connector-scoped storage. Linear
|
|
69
|
-
also
|
|
70
|
-
`Authorization
|
|
68
|
+
connector instance's flow and tokens in its connector-scoped storage. Linear's
|
|
69
|
+
MCP server also "supports passing OAuth token and API keys directly in the
|
|
70
|
+
`Authorization: Bearer <yourtoken>` header instead of using the interactive
|
|
71
|
+
authentication flow" ([Linear MCP docs](https://linear.app/docs/mcp)), which
|
|
72
|
+
suits a headless deployment. Note the framing: the bare-`Authorization`
|
|
73
|
+
convention is Linear's *GraphQL* API, and this endpoint is not that.
|
|
71
74
|
|
|
72
75
|
```ts
|
|
73
76
|
linear("automation_tracker", {
|
|
@@ -75,7 +78,7 @@ linear("automation_tracker", {
|
|
|
75
78
|
access: "read-only",
|
|
76
79
|
auth: {
|
|
77
80
|
type: "headers",
|
|
78
|
-
headers: { Authorization: env.LINEAR_API_KEY },
|
|
81
|
+
headers: { Authorization: `Bearer ${env.LINEAR_API_KEY}` },
|
|
79
82
|
},
|
|
80
83
|
});
|
|
81
84
|
```
|
|
@@ -85,6 +88,24 @@ configuration. A personal API key carries the acting user's full workspace
|
|
|
85
88
|
permissions, so pair it with `access: "read-only"` unless the deployment
|
|
86
89
|
genuinely writes.
|
|
87
90
|
|
|
91
|
+
The same key can arrive from `/credentials` instead, which is what a deployment
|
|
92
|
+
with no secret store — or an operator who rotates keys without a redeploy —
|
|
93
|
+
wants:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
linear("automation_tracker", {
|
|
97
|
+
purpose: "Headless release reporting",
|
|
98
|
+
access: "read-only",
|
|
99
|
+
auth: { type: "credential" },
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The slot renders as "Personal API key" and Connecta sends the stored value as
|
|
104
|
+
`Authorization: Bearer <key>`, the framing Linear's MCP page documents; pass
|
|
105
|
+
`credential` or `scheme` to override either. Until the operator saves a value
|
|
106
|
+
the connector is present and reports `auth_required`. See
|
|
107
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
|
108
|
+
|
|
88
109
|
## Safety classification
|
|
89
110
|
|
|
90
111
|
The wrapper classifies Linear's documented `list_*`, `get_*`, and
|