@zackbart/connecta 0.8.0 → 0.9.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +31 -55
  3. package/SECURITY.md +5 -11
  4. package/dist/activity.d.ts +0 -8
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js +0 -1
  7. package/dist/activity.js.map +1 -1
  8. package/dist/auth/bearer.d.ts +1 -7
  9. package/dist/auth/bearer.d.ts.map +1 -1
  10. package/dist/auth/bearer.js +2 -23
  11. package/dist/auth/bearer.js.map +1 -1
  12. package/dist/auth/clerk.d.ts +2 -11
  13. package/dist/auth/clerk.d.ts.map +1 -1
  14. package/dist/auth/clerk.js +31 -46
  15. package/dist/auth/clerk.js.map +1 -1
  16. package/dist/auth/downstream-oauth.d.ts +15 -6
  17. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  18. package/dist/auth/downstream-oauth.js +60 -11
  19. package/dist/auth/downstream-oauth.js.map +1 -1
  20. package/dist/call-admission.d.ts.map +1 -1
  21. package/dist/call-admission.js +0 -1
  22. package/dist/call-admission.js.map +1 -1
  23. package/dist/catalog-service.d.ts +1 -2
  24. package/dist/catalog-service.d.ts.map +1 -1
  25. package/dist/catalog-service.js +12 -18
  26. package/dist/catalog-service.js.map +1 -1
  27. package/dist/catalog.d.ts +6 -0
  28. package/dist/catalog.d.ts.map +1 -1
  29. package/dist/catalog.js +51 -0
  30. package/dist/catalog.js.map +1 -1
  31. package/dist/connectors/remote-mcp.d.ts +1 -1
  32. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  33. package/dist/connectors/remote-mcp.js +73 -71
  34. package/dist/connectors/remote-mcp.js.map +1 -1
  35. package/dist/errors.d.ts +19 -1
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +1 -0
  38. package/dist/errors.js.map +1 -1
  39. package/dist/execute.d.ts +4 -9
  40. package/dist/execute.d.ts.map +1 -1
  41. package/dist/execute.js +41 -20
  42. package/dist/execute.js.map +1 -1
  43. package/dist/executor-result.js +1 -1
  44. package/dist/executor-result.js.map +1 -1
  45. package/dist/index.d.ts +3 -82
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +17 -90
  48. package/dist/index.js.map +1 -1
  49. package/dist/invocation.d.ts +5 -0
  50. package/dist/invocation.d.ts.map +1 -1
  51. package/dist/invocation.js +46 -10
  52. package/dist/invocation.js.map +1 -1
  53. package/dist/meta-tools.d.ts +4 -12
  54. package/dist/meta-tools.d.ts.map +1 -1
  55. package/dist/meta-tools.js +119 -169
  56. package/dist/meta-tools.js.map +1 -1
  57. package/dist/operator-ui/generated.d.ts +2 -2
  58. package/dist/operator-ui/generated.d.ts.map +1 -1
  59. package/dist/operator-ui/generated.js +2 -2
  60. package/dist/operator-ui/generated.js.map +1 -1
  61. package/dist/operator-ui/model.d.ts +1 -22
  62. package/dist/operator-ui/model.d.ts.map +1 -1
  63. package/dist/operator-ui/model.js.map +1 -1
  64. package/dist/registry.d.ts +11 -178
  65. package/dist/registry.d.ts.map +1 -1
  66. package/dist/registry.js +52 -330
  67. package/dist/registry.js.map +1 -1
  68. package/dist/retired-toolkits.d.ts +3 -0
  69. package/dist/retired-toolkits.d.ts.map +1 -0
  70. package/dist/retired-toolkits.js +9 -0
  71. package/dist/retired-toolkits.js.map +1 -0
  72. package/dist/routes/activity.d.ts.map +1 -1
  73. package/dist/routes/activity.js +2 -5
  74. package/dist/routes/activity.js.map +1 -1
  75. package/dist/routes/credentials.d.ts.map +1 -1
  76. package/dist/routes/credentials.js +1 -12
  77. package/dist/routes/credentials.js.map +1 -1
  78. package/dist/routes/mcp.d.ts.map +1 -1
  79. package/dist/routes/mcp.js +94 -188
  80. package/dist/routes/mcp.js.map +1 -1
  81. package/dist/routes/oauth.d.ts.map +1 -1
  82. package/dist/routes/oauth.js +3 -8
  83. package/dist/routes/oauth.js.map +1 -1
  84. package/dist/routes/shared.d.ts +9 -30
  85. package/dist/routes/shared.d.ts.map +1 -1
  86. package/dist/routes/shared.js +10 -69
  87. package/dist/routes/shared.js.map +1 -1
  88. package/dist/routes/ui.d.ts.map +1 -1
  89. package/dist/routes/ui.js +4 -10
  90. package/dist/routes/ui.js.map +1 -1
  91. package/dist/server.d.ts.map +1 -1
  92. package/dist/server.js +1 -31
  93. package/dist/server.js.map +1 -1
  94. package/dist/skills.d.ts +3 -6
  95. package/dist/skills.d.ts.map +1 -1
  96. package/dist/skills.js +9 -38
  97. package/dist/skills.js.map +1 -1
  98. package/dist/timeout.d.ts.map +1 -1
  99. package/dist/timeout.js +2 -4
  100. package/dist/timeout.js.map +1 -1
  101. package/dist/types.d.ts +8 -61
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/ui.d.ts +1 -2
  104. package/dist/ui.d.ts.map +1 -1
  105. package/dist/ui.js +5 -35
  106. package/dist/ui.js.map +1 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/package.json +3 -2
  110. package/src/activity.ts +0 -9
  111. package/src/auth/bearer.ts +3 -32
  112. package/src/auth/clerk.ts +35 -48
  113. package/src/auth/downstream-oauth.ts +106 -24
  114. package/src/call-admission.ts +0 -1
  115. package/src/catalog-service.ts +23 -23
  116. package/src/catalog.ts +55 -0
  117. package/src/connectors/remote-mcp.ts +106 -86
  118. package/src/errors.ts +22 -0
  119. package/src/execute.ts +43 -24
  120. package/src/executor-result.ts +1 -1
  121. package/src/index.ts +24 -208
  122. package/src/invocation.ts +63 -10
  123. package/src/meta-tools.ts +139 -187
  124. package/src/operator-ui/browser.css +1 -54
  125. package/src/operator-ui/browser.ts +16 -150
  126. package/src/operator-ui/generated.ts +2 -2
  127. package/src/operator-ui/model.ts +1 -23
  128. package/src/registry.ts +59 -429
  129. package/src/retired-toolkits.ts +14 -0
  130. package/src/routes/activity.ts +1 -6
  131. package/src/routes/credentials.ts +0 -12
  132. package/src/routes/mcp.ts +110 -230
  133. package/src/routes/oauth.ts +2 -8
  134. package/src/routes/shared.ts +10 -100
  135. package/src/routes/ui.ts +2 -12
  136. package/src/server.ts +1 -31
  137. package/src/skills.ts +9 -38
  138. package/src/timeout.ts +2 -4
  139. package/src/types.ts +11 -62
  140. package/src/ui.ts +9 -46
  141. package/src/version.ts +1 -1
  142. package/dist/credential-health.d.ts +0 -205
  143. package/dist/credential-health.d.ts.map +0 -1
  144. package/dist/credential-health.js +0 -610
  145. package/dist/credential-health.js.map +0 -1
  146. package/dist/toolkits.d.ts +0 -124
  147. package/dist/toolkits.d.ts.map +0 -1
  148. package/dist/toolkits.js +0 -322
  149. package/dist/toolkits.js.map +0 -1
  150. package/src/credential-health.ts +0 -835
  151. package/src/toolkits.ts +0 -461
@@ -1,9 +1,10 @@
1
- import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2
1
  import type {
2
+ OAuthClientInformationContext,
3
3
  OAuthClientInformationMixed,
4
4
  OAuthClientMetadata,
5
+ OAuthClientProvider,
5
6
  OAuthTokens,
6
- } from "@modelcontextprotocol/sdk/shared/auth.js";
7
+ } from "@modelcontextprotocol/client";
7
8
  import type { KVStorage } from "../types.js";
8
9
 
9
10
  /** A 256-bit random opaque value, hex-encoded — used for the OAuth `state`. */
@@ -25,7 +26,7 @@ const LEGACY_GENERATION = "legacy";
25
26
  const ACTIVE_GENERATION_PREFIX = "v2:";
26
27
  const RESETTING_GENERATION_PREFIX = "reset:";
27
28
  const DISCONNECTED_GENERATION_PREFIX = "disconnected:";
28
- const STORED_VALUE_VERSION = 1;
29
+ const STORED_VALUE_VERSION = 2;
29
30
  const OAUTH_VALUE_KEYS = [
30
31
  "oauth:client",
31
32
  "oauth:tokens",
@@ -38,6 +39,13 @@ const MAX_CLEANUP_BACKLOG = 1_000;
38
39
  interface StoredOAuthValue<T> {
39
40
  connectaOAuthVersion: typeof STORED_VALUE_VERSION;
40
41
  generation: string;
42
+ issuer?: string;
43
+ value: T;
44
+ }
45
+
46
+ interface LegacyStoredOAuthValue<T> {
47
+ connectaOAuthVersion: 1;
48
+ generation: string;
41
49
  value: T;
42
50
  }
43
51
 
@@ -49,6 +57,19 @@ function storedOAuthValue<T>(
49
57
  return (
50
58
  candidate.connectaOAuthVersion === STORED_VALUE_VERSION &&
51
59
  typeof candidate.generation === "string" &&
60
+ (candidate.issuer === undefined || typeof candidate.issuer === "string") &&
61
+ "value" in candidate
62
+ );
63
+ }
64
+
65
+ function legacyStoredOAuthValue<T>(
66
+ value: unknown,
67
+ ): value is LegacyStoredOAuthValue<T> {
68
+ if (!value || typeof value !== "object") return false;
69
+ const candidate = value as Partial<LegacyStoredOAuthValue<T>>;
70
+ return (
71
+ candidate.connectaOAuthVersion === 1 &&
72
+ typeof candidate.generation === "string" &&
52
73
  "value" in candidate
53
74
  );
54
75
  }
@@ -132,6 +153,7 @@ export class KvOAuthProvider implements OAuthClientProvider {
132
153
  key: string,
133
154
  value: T,
134
155
  serializeLegacy: (value: T) => string,
156
+ issuer?: string,
135
157
  ): Promise<void> {
136
158
  const generation = await this.writeGeneration();
137
159
  if (
@@ -144,12 +166,13 @@ export class KvOAuthProvider implements OAuthClientProvider {
144
166
  const stored: StoredOAuthValue<T> = {
145
167
  connectaOAuthVersion: STORED_VALUE_VERSION,
146
168
  generation,
169
+ ...(issuer !== undefined ? { issuer } : {}),
147
170
  value,
148
171
  };
149
172
  const physicalKey = oauthValueStorageKey(key, generation);
150
173
  await this.storage.set(
151
174
  physicalKey,
152
- isModernGeneration(generation)
175
+ isModernGeneration(generation) || issuer !== undefined
153
176
  ? JSON.stringify(stored)
154
177
  : serializeLegacy(value),
155
178
  );
@@ -182,7 +205,9 @@ export class KvOAuthProvider implements OAuthClientProvider {
182
205
  private async readValue<T>(
183
206
  key: string,
184
207
  parseLegacy: (raw: string) => T,
185
- ): Promise<{ value: T; generation: string } | undefined> {
208
+ ): Promise<
209
+ { value: T; generation: string; issuer?: string } | undefined
210
+ > {
186
211
  const generation = await this.generation();
187
212
  const raw = await this.storage.get(
188
213
  oauthValueStorageKey(key, generation),
@@ -202,6 +227,15 @@ export class KvOAuthProvider implements OAuthClientProvider {
202
227
  // Raw string state from a pre-envelope deployment is handled below.
203
228
  }
204
229
  if (storedOAuthValue<T>(parsed)) {
230
+ return parsed.generation === generation
231
+ ? {
232
+ value: parsed.value,
233
+ generation,
234
+ ...(parsed.issuer !== undefined ? { issuer: parsed.issuer } : {}),
235
+ }
236
+ : undefined;
237
+ }
238
+ if (legacyStoredOAuthValue<T>(parsed)) {
205
239
  return parsed.generation === generation
206
240
  ? { value: parsed.value, generation }
207
241
  : undefined;
@@ -210,6 +244,40 @@ export class KvOAuthProvider implements OAuthClientProvider {
210
244
  return { value: parseLegacy(raw), generation };
211
245
  }
212
246
 
247
+ /**
248
+ * Read credentials only for the authorization server that issued them.
249
+ *
250
+ * Values written before issuer binding are bound on their first read with a
251
+ * validated SDK issuer, preserving an existing grant across this upgrade.
252
+ * A later issuer change is different: publish a new generation before
253
+ * returning no credentials, so every isolate drops the old registration and
254
+ * token set and the SDK starts authorization from scratch.
255
+ */
256
+ private async readIssuerBoundValue<T>(
257
+ key: "oauth:client" | "oauth:tokens",
258
+ parseLegacy: (raw: string) => T,
259
+ serializeLegacy: (value: T) => string,
260
+ ctx?: OAuthClientInformationContext,
261
+ ): Promise<T | undefined> {
262
+ const stored = await this.readValue(key, parseLegacy);
263
+ if (!stored || !ctx) return stored?.value;
264
+ if (stored.issuer === undefined) {
265
+ await this.writeValue(key, stored.value, serializeLegacy, ctx.issuer);
266
+ return stored.value;
267
+ }
268
+ if (stored.issuer === ctx.issuer) return stored.value;
269
+
270
+ try {
271
+ await this.resetAuthorization();
272
+ } finally {
273
+ // The issuer-aware flow may continue after the reset. Stamp any later
274
+ // writes into the replacement epoch; the connector's generation check
275
+ // still discards this connection attempt before caching it.
276
+ this.captureGeneration(await this.generation());
277
+ }
278
+ return undefined;
279
+ }
280
+
213
281
  /** Attempt every key deletion, then report the first backend failure. */
214
282
  private async deleteAll(keys: readonly string[]): Promise<void> {
215
283
  let firstError: unknown;
@@ -276,35 +344,49 @@ export class KvOAuthProvider implements OAuthClientProvider {
276
344
  };
277
345
  }
278
346
 
279
- async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
280
- return (
281
- await this.readValue(
282
- "oauth:client",
283
- (raw) => JSON.parse(raw) as OAuthClientInformationMixed,
284
- )
285
- )?.value;
347
+ async clientInformation(
348
+ ctx?: OAuthClientInformationContext,
349
+ ): Promise<OAuthClientInformationMixed | undefined> {
350
+ return this.readIssuerBoundValue(
351
+ "oauth:client",
352
+ (raw) => JSON.parse(raw) as OAuthClientInformationMixed,
353
+ (value) => JSON.stringify(value),
354
+ ctx,
355
+ );
286
356
  }
287
357
 
288
358
  async saveClientInformation(
289
359
  info: OAuthClientInformationMixed,
360
+ ctx?: OAuthClientInformationContext,
290
361
  ): Promise<void> {
291
- await this.writeValue("oauth:client", info, (value) =>
292
- JSON.stringify(value),
362
+ await this.writeValue(
363
+ "oauth:client",
364
+ info,
365
+ (value) => JSON.stringify(value),
366
+ ctx?.issuer,
293
367
  );
294
368
  }
295
369
 
296
- async tokens(): Promise<OAuthTokens | undefined> {
297
- return (
298
- await this.readValue(
299
- "oauth:tokens",
300
- (raw) => JSON.parse(raw) as OAuthTokens,
301
- )
302
- )?.value;
370
+ async tokens(
371
+ ctx?: OAuthClientInformationContext,
372
+ ): Promise<OAuthTokens | undefined> {
373
+ return this.readIssuerBoundValue(
374
+ "oauth:tokens",
375
+ (raw) => JSON.parse(raw) as OAuthTokens,
376
+ (value) => JSON.stringify(value),
377
+ ctx,
378
+ );
303
379
  }
304
380
 
305
- async saveTokens(tokens: OAuthTokens): Promise<void> {
306
- await this.writeValue("oauth:tokens", tokens, (value) =>
307
- JSON.stringify(value),
381
+ async saveTokens(
382
+ tokens: OAuthTokens,
383
+ ctx?: OAuthClientInformationContext,
384
+ ): Promise<void> {
385
+ await this.writeValue(
386
+ "oauth:tokens",
387
+ tokens,
388
+ (value) => JSON.stringify(value),
389
+ ctx?.issuer,
308
390
  );
309
391
  }
310
392
 
@@ -459,7 +459,6 @@ export class ConnectorCallAdmissionController {
459
459
 
460
460
  private evictIdlePartitions(now: number): void {
461
461
  for (const [key, state] of this.partitions) {
462
- this.pruneBudget(state, now);
463
462
  this.maybeDeletePartition(key, state, now);
464
463
  }
465
464
  }
@@ -1,4 +1,9 @@
1
- import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
1
+ import {
2
+ compactSchema,
3
+ lexicalSearchQuery,
4
+ rankTools,
5
+ summarizeDescription,
6
+ } from "./catalog.js";
2
7
  import {
3
8
  mapSettledWithConcurrency,
4
9
  resolveDiscoveryConcurrency,
@@ -20,10 +25,11 @@ import {
20
25
  } from "./timeout.js";
21
26
  import type {
22
27
  Connector,
28
+ JsonSchema,
23
29
  ToolDef,
24
30
  } from "./types.js";
25
31
 
26
- const DEFAULT_SEARCH_LIMIT = 25;
32
+ export const DEFAULT_SEARCH_LIMIT = 8;
27
33
  export const MAX_SEARCH_LIMIT = 100;
28
34
  export const MAX_DESCRIBE_ADDRESSES = 100;
29
35
  export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
@@ -76,7 +82,7 @@ function discoveryAddresses(value: unknown): unknown[] {
76
82
 
77
83
  /** Serialize once and count the exact bytes the MCP adapter would emit. */
78
84
  export function boundedDiscoveryText(value: unknown, hint: string): string {
79
- const text = JSON.stringify(value, null, 2);
85
+ const text = JSON.stringify(value);
80
86
  if (text === undefined) {
81
87
  throw new TypeError("Discovery result is not JSON-serializable.");
82
88
  }
@@ -90,14 +96,6 @@ export function boundedDiscoveryText(value: unknown, hint: string): string {
90
96
  return text;
91
97
  }
92
98
 
93
- /** Apply the same final UTF-8 boundary where only the check is needed. */
94
- export function assertDiscoveryResultSize(
95
- value: unknown,
96
- hint: string,
97
- ): void {
98
- boundedDiscoveryText(value, hint);
99
- }
100
-
101
99
  export interface CatalogSearchArgs {
102
100
  query?: string;
103
101
  connector?: string;
@@ -172,6 +170,10 @@ function framingError(code: string, message: string): CallErrorDetails {
172
170
  return { code, message, retryable: messageLooksRetryable(message) };
173
171
  }
174
172
 
173
+ function renderSchema(schema: JsonSchema, format: "compact" | "json"): unknown {
174
+ return format === "json" ? schema : compactSchema(schema);
175
+ }
176
+
175
177
  /**
176
178
  * Request-local catalog operations shared by MCP meta-tools and code mode.
177
179
  * Successful catalogs may be reused inside this request; failures are not
@@ -365,6 +367,7 @@ export class CatalogService {
365
367
 
366
368
  async search(args: CatalogSearchArgs): Promise<CatalogSearchPage> {
367
369
  const query = args.query ?? "";
370
+ const retrievalQuery = lexicalSearchQuery(query);
368
371
  const limit = discoverySearchLimit(args.limit);
369
372
  const offset = Math.max(0, Math.trunc(args.offset ?? 0));
370
373
  const connectors = args.connector
@@ -397,7 +400,11 @@ export class CatalogService {
397
400
  throw new Error("Catalog result has no corresponding connector");
398
401
  }
399
402
  if (catalog.status === "fulfilled") {
400
- for (const ranked of rankTools(catalog.value, query, mode)) {
403
+ for (const ranked of rankTools(
404
+ catalog.value,
405
+ retrievalQuery,
406
+ mode,
407
+ )) {
401
408
  matches.push({
402
409
  connector,
403
410
  tool: ranked.tool,
@@ -434,17 +441,13 @@ export class CatalogService {
434
441
  ...(args.includeSchemas
435
442
  ? {
436
443
  inputSchema:
437
- args.includeSchemas === "json"
438
- ? input
439
- : compactSchema(input),
444
+ renderSchema(input, args.includeSchemas),
440
445
  }
441
446
  : {}),
442
447
  ...(args.includeSchemas && match.tool.outputSchema
443
448
  ? {
444
449
  outputSchema:
445
- args.includeSchemas === "json"
446
- ? match.tool.outputSchema
447
- : compactSchema(match.tool.outputSchema),
450
+ renderSchema(match.tool.outputSchema, args.includeSchemas),
448
451
  }
449
452
  : {}),
450
453
  ...(match.tool.annotations
@@ -534,13 +537,10 @@ export class CatalogService {
534
537
  ...(connectorGuide(addressResolution.connector)
535
538
  ? { guide: connectorSkillName(addressResolution.connector.id) }
536
539
  : {}),
537
- inputSchema: format === "json" ? input : compactSchema(input),
540
+ inputSchema: renderSchema(input, format),
538
541
  ...(tool.outputSchema
539
542
  ? {
540
- outputSchema:
541
- format === "json"
542
- ? tool.outputSchema
543
- : compactSchema(tool.outputSchema),
543
+ outputSchema: renderSchema(tool.outputSchema, format),
544
544
  }
545
545
  : {}),
546
546
  ...(tool.annotations ? { annotations: tool.annotations } : {}),
package/src/catalog.ts CHANGED
@@ -17,6 +17,61 @@ function normalized(text: string): string {
17
17
  return text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
18
18
  }
19
19
 
20
+ /**
21
+ * Conversational framing selected by the #188 research run before the #189
22
+ * holdout existed. Action-bearing terms such as get/list/search/find/create
23
+ * deliberately remain: arbitrary connector catalogs need those distinctions.
24
+ */
25
+ const CONVERSATIONAL_QUERY_WORDS = new Set([
26
+ "a",
27
+ "all",
28
+ "an",
29
+ "and",
30
+ "are",
31
+ "can",
32
+ "could",
33
+ "current",
34
+ "for",
35
+ "from",
36
+ "i",
37
+ "in",
38
+ "into",
39
+ "it",
40
+ "latest",
41
+ "let",
42
+ "me",
43
+ "most",
44
+ "of",
45
+ "on",
46
+ "our",
47
+ "please",
48
+ "right",
49
+ "show",
50
+ "that",
51
+ "the",
52
+ "this",
53
+ "to",
54
+ "up",
55
+ "want",
56
+ "when",
57
+ "which",
58
+ "with",
59
+ "you",
60
+ ]);
61
+
62
+ /**
63
+ * Remove conversational framing before the all-term/partial decision. If
64
+ * every term is framing, retain the original query rather than turning a
65
+ * search into an unfiltered catalog browse.
66
+ */
67
+ export function lexicalSearchQuery(query: string): string {
68
+ const terms = normalized(query).split(/\s+/).filter(Boolean);
69
+ const contentTerms = terms.filter(
70
+ (term) => !CONVERSATIONAL_QUERY_WORDS.has(term),
71
+ );
72
+ return contentTerms.length > 0 ? contentTerms.join(" ") : query;
73
+ }
74
+
20
75
  interface SearchDocument {
21
76
  tool: ToolDef;
22
77
  name: string;