@zackbart/connecta 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +9 -8
  3. package/SECURITY.md +5 -11
  4. package/dist/auth/downstream-oauth.d.ts +15 -6
  5. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  6. package/dist/auth/downstream-oauth.js +60 -11
  7. package/dist/auth/downstream-oauth.js.map +1 -1
  8. package/dist/catalog-service.d.ts +8 -0
  9. package/dist/catalog-service.d.ts.map +1 -1
  10. package/dist/catalog-service.js +24 -2
  11. package/dist/catalog-service.js.map +1 -1
  12. package/dist/catalog.d.ts +34 -1
  13. package/dist/catalog.d.ts.map +1 -1
  14. package/dist/catalog.js +264 -40
  15. package/dist/catalog.js.map +1 -1
  16. package/dist/connectors/remote-mcp.d.ts +1 -1
  17. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  18. package/dist/connectors/remote-mcp.js +65 -50
  19. package/dist/connectors/remote-mcp.js.map +1 -1
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +1 -0
  23. package/dist/errors.js.map +1 -1
  24. package/dist/execute.d.ts +3 -1
  25. package/dist/execute.d.ts.map +1 -1
  26. package/dist/execute.js +50 -13
  27. package/dist/execute.js.map +1 -1
  28. package/dist/meta-tools.d.ts +1 -1
  29. package/dist/meta-tools.d.ts.map +1 -1
  30. package/dist/meta-tools.js +19 -17
  31. package/dist/meta-tools.js.map +1 -1
  32. package/dist/routes/mcp.d.ts.map +1 -1
  33. package/dist/routes/mcp.js +63 -44
  34. package/dist/routes/mcp.js.map +1 -1
  35. package/dist/routes/oauth.js +1 -1
  36. package/dist/routes/oauth.js.map +1 -1
  37. package/dist/routes/shared.d.ts +2 -2
  38. package/dist/routes/shared.d.ts.map +1 -1
  39. package/dist/skills.d.ts +2 -2
  40. package/dist/skills.d.ts.map +1 -1
  41. package/dist/skills.js +7 -7
  42. package/dist/skills.js.map +1 -1
  43. package/dist/types.d.ts +6 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +3 -2
  48. package/src/auth/downstream-oauth.ts +106 -24
  49. package/src/catalog-service.ts +43 -0
  50. package/src/catalog.ts +327 -33
  51. package/src/connectors/remote-mcp.ts +96 -64
  52. package/src/errors.ts +2 -0
  53. package/src/execute.ts +55 -12
  54. package/src/meta-tools.ts +22 -18
  55. package/src/routes/mcp.ts +70 -44
  56. package/src/routes/oauth.ts +1 -1
  57. package/src/routes/shared.ts +2 -2
  58. package/src/skills.ts +7 -7
  59. package/src/types.ts +10 -2
  60. package/src/version.ts +1 -1
@@ -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
 
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  compactSchema,
3
+ lexicalCorpusStatistics,
3
4
  lexicalSearchQuery,
4
5
  rankTools,
6
+ schemaObjectKeys,
5
7
  summarizeDescription,
6
8
  } from "./catalog.js";
7
9
  import {
@@ -103,6 +105,8 @@ export interface CatalogSearchArgs {
103
105
  offset?: number;
104
106
  fullDescriptions?: boolean;
105
107
  includeSchemas?: "compact" | "json";
108
+ /** Code-mode helper metadata; never exposed by the public search_tools schema. */
109
+ includeSchemaKeys?: boolean;
106
110
  }
107
111
 
108
112
  export interface CatalogDescribeArgs {
@@ -120,10 +124,39 @@ interface CatalogSearchEntry {
120
124
  description?: string;
121
125
  inputSchema?: unknown;
122
126
  outputSchema?: unknown;
127
+ inputKeys?: string[];
128
+ requiredInputKeys?: string[];
129
+ outputKeys?: string[];
123
130
  annotations?: ToolDef["annotations"];
124
131
  };
125
132
  }
126
133
 
134
+ /**
135
+ * Code-mode key metadata for one match. Each half is omitted when its schema
136
+ * does not resolve to an object shape, so a program reads "no metadata, use the
137
+ * rendered schema" rather than "this tool has no fields".
138
+ */
139
+ function schemaKeyMetadata(
140
+ input: JsonSchema,
141
+ output: JsonSchema | undefined,
142
+ ): {
143
+ inputKeys?: string[];
144
+ requiredInputKeys?: string[];
145
+ outputKeys?: string[];
146
+ } {
147
+ const inputKeys = schemaObjectKeys(input);
148
+ const outputKeys = schemaObjectKeys(output);
149
+ return {
150
+ ...(inputKeys
151
+ ? {
152
+ inputKeys: inputKeys.properties,
153
+ requiredInputKeys: inputKeys.required,
154
+ }
155
+ : {}),
156
+ ...(outputKeys ? { outputKeys: outputKeys.properties } : {}),
157
+ };
158
+ }
159
+
127
160
  export interface CatalogSearchPage {
128
161
  entries: CatalogSearchEntry[];
129
162
  total: number;
@@ -391,6 +424,12 @@ export class CatalogService {
391
424
  order: number;
392
425
  }> = [];
393
426
  let matchMode: "all" | "partial" = "all";
427
+ const statistics = lexicalCorpusStatistics(
428
+ catalogs.flatMap((catalog) =>
429
+ catalog.status === "fulfilled" ? [catalog.value] : [],
430
+ ),
431
+ retrievalQuery,
432
+ );
394
433
  const collectMatches = (mode: "all" | "partial") => {
395
434
  matches.length = 0;
396
435
  let orderBase = 0;
@@ -404,6 +443,7 @@ export class CatalogService {
404
443
  catalog.value,
405
444
  retrievalQuery,
406
445
  mode,
446
+ statistics,
407
447
  )) {
408
448
  matches.push({
409
449
  connector,
@@ -450,6 +490,9 @@ export class CatalogService {
450
490
  renderSchema(match.tool.outputSchema, args.includeSchemas),
451
491
  }
452
492
  : {}),
493
+ ...(args.includeSchemas && args.includeSchemaKeys
494
+ ? schemaKeyMetadata(input, match.tool.outputSchema)
495
+ : {}),
453
496
  ...(match.tool.annotations
454
497
  ? { annotations: match.tool.annotations }
455
498
  : {}),