@haikit/mcp 0.33.0 → 0.34.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 (51) hide show
  1. package/README.md +46 -7
  2. package/dist/auth.d.ts +107 -36
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +252 -37
  5. package/dist/auth.js.map +1 -1
  6. package/dist/credential.d.ts +55 -0
  7. package/dist/credential.d.ts.map +1 -0
  8. package/dist/credential.js +62 -0
  9. package/dist/credential.js.map +1 -0
  10. package/dist/errors.d.ts +7 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js.map +1 -1
  13. package/dist/index.d.ts +6 -4
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +66 -9
  16. package/dist/index.js.map +1 -1
  17. package/dist/oauth.d.ts +193 -0
  18. package/dist/oauth.d.ts.map +1 -0
  19. package/dist/oauth.js +558 -0
  20. package/dist/oauth.js.map +1 -0
  21. package/dist/postgres.d.ts +25 -0
  22. package/dist/postgres.d.ts.map +1 -0
  23. package/dist/postgres.js +97 -0
  24. package/dist/postgres.js.map +1 -0
  25. package/dist/routes.d.ts +17 -0
  26. package/dist/routes.d.ts.map +1 -0
  27. package/dist/routes.js +228 -0
  28. package/dist/routes.js.map +1 -0
  29. package/dist/seal.d.ts +25 -0
  30. package/dist/seal.d.ts.map +1 -0
  31. package/dist/seal.js +60 -0
  32. package/dist/seal.js.map +1 -0
  33. package/dist/store.d.ts +50 -0
  34. package/dist/store.d.ts.map +1 -0
  35. package/dist/store.js +83 -0
  36. package/dist/store.js.map +1 -0
  37. package/dist/transports.d.ts +5 -1
  38. package/dist/transports.d.ts.map +1 -1
  39. package/dist/transports.js +2 -0
  40. package/dist/transports.js.map +1 -1
  41. package/package.json +6 -2
  42. package/src/auth.ts +354 -48
  43. package/src/credential.ts +78 -0
  44. package/src/errors.ts +7 -0
  45. package/src/index.ts +75 -12
  46. package/src/oauth.ts +659 -0
  47. package/src/postgres.ts +127 -0
  48. package/src/routes.ts +243 -0
  49. package/src/seal.ts +62 -0
  50. package/src/store.ts +118 -0
  51. package/src/transports.ts +7 -0
package/src/index.ts CHANGED
@@ -32,15 +32,25 @@ import {
32
32
  type Transport,
33
33
  type VersionNegotiationMode,
34
34
  } from "@modelcontextprotocol/client";
35
- import { BearerCredential, LookupFailed, NeedsAuth, configOf, type AuthConfig, type Credential, type McpServerAuth } from "./auth.js";
35
+ import { configOf, type AuthConfig, type Binding, type McpServerAuth } from "./auth.js";
36
+ import { LookupFailed, Mismatch, NeedsAuth, type Credential } from "./credential.js";
36
37
  import { McpError, McpToolError, isMcpError, type McpAuthNeed, type McpErrorReason } from "./errors.js";
37
38
  import { madeByHttp } from "./transports.js";
38
39
 
39
- export { createMcpAuth, type McpAuth, type McpAuthOptions, type McpServerAuth } from "./auth.js";
40
+ export {
41
+ createMcpAuth,
42
+ type McpAccount,
43
+ type McpAuth,
44
+ type McpAuthOptions,
45
+ type McpConnected,
46
+ type McpServerAuth,
47
+ type OAuthServerOptions,
48
+ } from "./auth.js";
40
49
  export { McpError, McpToolError, isMcpError, isMcpToolError, type McpAuthNeed, type McpErrorReason } from "./errors.js";
41
50
  export { textOf, type TextOptions } from "./text.js";
51
+ export { memoryCredentialStore, type CredentialStore } from "./store.js";
42
52
  export { http, stdio, type HttpOptions } from "./transports.js";
43
- export type { AuthProvider, ContentBlock, Transport } from "@modelcontextprotocol/client";
53
+ export type { AuthProvider, ContentBlock, OAuthClientProvider, Transport } from "@modelcontextprotocol/client";
44
54
 
45
55
  const { version: VERSION } = createRequire(import.meta.url)("../package.json") as { version: string };
46
56
 
@@ -75,8 +85,9 @@ export interface McpServerOptions {
75
85
  transport: (authProvider?: AuthProvider) => Transport | Promise<Transport>;
76
86
  /**
77
87
  * Makes each call as the user it runs for, with their own credential, over
78
- * a connection of their own: `createMcpAuth({ userOf }).bearer(token)`.
79
- * Without it, every call shares one connection and one identity.
88
+ * a connection of their own: `mcpAuth.oauth()`, `mcpAuth.bearer(token)` or
89
+ * `mcpAuth.sdk(make)`, from `createMcpAuth`. Without it, every call shares
90
+ * one connection and one identity.
80
91
  */
81
92
  auth?: McpServerAuth;
82
93
  /**
@@ -239,8 +250,10 @@ class Server implements McpConnection {
239
250
  readonly options: McpServerOptions;
240
251
  readonly timeoutMs: number;
241
252
  private readonly maxTotalMs: number;
242
- /** With `auth`: whose calls are made as whom, and with what. */
253
+ /** With `auth`: whose calls are made as whom. */
243
254
  readonly auth: AuthConfig | null;
255
+ /** With `auth`: this server's place on its `McpAuth`, which makes each user's credential. */
256
+ private readonly binding: Binding | null;
244
257
  private readonly maxConnections: number;
245
258
  readonly idleMs: number;
246
259
  /** Without `auth`: the one connection every call shares. */
@@ -275,6 +288,7 @@ class Server implements McpConnection {
275
288
  this.maxConnections = 1;
276
289
  this.idleMs = IDLE_MS;
277
290
  this.shared = new Link(this, undefined, null);
291
+ this.binding = null;
278
292
  return;
279
293
  }
280
294
  const auth = configOf(options.auth);
@@ -292,8 +306,7 @@ class Server implements McpConnection {
292
306
  this.idleMs = checkMs("idleMs", options.idleMs ?? IDLE_MS);
293
307
  this.maxConnections = max;
294
308
  // last, so a server refused above leaves its name free
295
- if (auth.names.has(this.name)) throw new TypeError(`another server from the same createMcpAuth is named "${this.name}"`);
296
- auth.names.add(this.name);
309
+ this.binding = auth.attach({ name: this.name, timeoutMs: this.timeoutMs, url: made.url, fetch: made.fetch, forget: (user) => this.forget(user) });
297
310
  this.auth = auth;
298
311
  this.shared = null;
299
312
  }
@@ -332,6 +345,8 @@ class Server implements McpConnection {
332
345
  const link = this.take(userId);
333
346
  try {
334
347
  await link.ready();
348
+ } catch (err) {
349
+ throw this.linked(err, undefined);
335
350
  } finally {
336
351
  link.release();
337
352
  }
@@ -355,7 +370,7 @@ class Server implements McpConnection {
355
370
  private async shut(): Promise<void> {
356
371
  this.closed = true;
357
372
  this.shutdown.abort();
358
- this.auth?.names.delete(this.name);
373
+ this.binding?.detach();
359
374
  const links = this.shared ? [this.shared] : [...this.links.values()];
360
375
  this.links.clear();
361
376
  await Promise.all(links.map((link) => link.close()));
@@ -368,7 +383,7 @@ class Server implements McpConnection {
368
383
  this.links.delete(user);
369
384
  } else {
370
385
  this.trim(this.maxConnections - 1);
371
- link = new Link(this, user, new BearerCredential(this.auth!.token, user, this.timeoutMs));
386
+ link = new Link(this, user, this.binding!.credential(user));
372
387
  }
373
388
  this.links.set(user, link);
374
389
  link.hold();
@@ -474,6 +489,8 @@ class Server implements McpConnection {
474
489
  } catch (err) {
475
490
  throw link.failure(err, tool, client);
476
491
  }
492
+ } catch (err) {
493
+ throw this.linked(err, nonEmpty(ctx?.conversationId));
477
494
  } finally {
478
495
  clearTimeout(timer);
479
496
  if (link !== this.shared) link?.release();
@@ -516,6 +533,19 @@ class Server implements McpConnection {
516
533
  ]);
517
534
  }
518
535
 
536
+ /**
537
+ * An `auth` error of an `oauth()` server, with the connect route's path for
538
+ * the browser: for the call's conversation, and the scope the server asked
539
+ * for. Built from configuration, never from what the server said but that
540
+ * scope. Any other error, as it is.
541
+ */
542
+ private linked(err: unknown, conversationId: string | undefined): unknown {
543
+ const connect = this.binding?.connect;
544
+ if (!connect || !isMcpError(err) || err.reason !== "auth" || !err.auth) return err;
545
+ const auth: McpAuthNeed = { ...err.auth, connect: connect(conversationId, err.auth.kind === "scope" ? err.auth.scope : undefined) };
546
+ return new McpError("auth", err.message, { server: err.server, tool: err.tool, text: err.text, code: err.code, cause: err.cause, auth });
547
+ }
548
+
519
549
  error(reason: McpErrorReason, tool: string | undefined, detail: Detail = {}): McpError {
520
550
  const who = tool === undefined ? this.name : `${this.name}/${tool}`;
521
551
  const { user, ...kept } = detail;
@@ -546,6 +576,8 @@ class Link {
546
576
  private listed: Map<string, ServerTool> | null = null;
547
577
  /** The declared tools checked against this connection's tools. */
548
578
  private readonly checked = new Set<string>();
579
+ /** The credential's check before a call, which calls at once share. */
580
+ private checking: Promise<void> | null = null;
549
581
  /** Calls and `connect()`s using it now: a busy link is never closed for room or for idling. */
550
582
  busy = 0;
551
583
  private idle: ReturnType<typeof setTimeout> | null = null;
@@ -575,8 +607,12 @@ class Link {
575
607
  /** Connected, with a credential if it needs one. */
576
608
  async ready(): Promise<Client> {
577
609
  if (this.credential && !this.closed) {
610
+ const credential = this.credential;
611
+ this.checking ??= this.check(credential).finally(() => {
612
+ this.checking = null;
613
+ });
578
614
  try {
579
- await unless(this.credential.ensure(), [[this.ending.signal, () => this.server.error("closed", undefined)]]);
615
+ await unless(this.checking, [[this.ending.signal, () => this.server.error("closed", undefined)]]);
580
616
  } catch (err) {
581
617
  // closed, whatever the lookup did meanwhile
582
618
  if (this.closed) throw isMcpError(err) ? err : this.server.error("closed", undefined, { cause: err });
@@ -586,6 +622,30 @@ class Link {
586
622
  return this.open();
587
623
  }
588
624
 
625
+ /**
626
+ * The credential, before a call. A connection made with one that's gone,
627
+ * or that the user has since replaced by connecting again, is hung up, so
628
+ * nothing more is sent with it, and the next is made with what's held now.
629
+ */
630
+ private async check(credential: Credential): Promise<void> {
631
+ let changed: boolean;
632
+ try {
633
+ changed = await credential.ensure();
634
+ } catch (err) {
635
+ if (err instanceof NeedsAuth) await this.hangUp();
636
+ throw err;
637
+ }
638
+ if (changed) await this.hangUp();
639
+ }
640
+
641
+ /** Ends the connection, as when it drops, but not the link: the next call connects again. */
642
+ private async hangUp(): Promise<void> {
643
+ await this.dialing?.catch(() => {});
644
+ const client = this.client;
645
+ this.client = null;
646
+ await client?.close().catch(() => {});
647
+ }
648
+
589
649
  /**
590
650
  * A tool declared after this connection's first check is checked on its
591
651
  * first call, and throws as `connect` would. `again` checks it against the
@@ -638,7 +698,9 @@ class Link {
638
698
  private async made(): Promise<Transport> {
639
699
  const { options, timeoutMs } = this.server;
640
700
  const authProvider = this.credential?.provider;
641
- const making = Promise.resolve().then(() => (authProvider ? options.transport(authProvider) : options.transport()));
701
+ // An OAuthClientProvider, from sdk(), goes to http()'s transport as it
702
+ // is, which adapts it: only http() is ever given one.
703
+ const making = Promise.resolve().then(() => (authProvider ? options.transport(authProvider as AuthProvider) : options.transport()));
642
704
  let timer: ReturnType<typeof setTimeout> | undefined;
643
705
  const cut = new Promise<never>((_, reject) => {
644
706
  timer = setTimeout(() => reject(new Error(`no transport within ${timeoutMs} ms`)), timeoutMs);
@@ -760,6 +822,7 @@ class Link {
760
822
  const { server } = this;
761
823
  if (err instanceof NeedsAuth) return server.error("auth", tool, { auth: { kind: err.kind } });
762
824
  if (err instanceof LookupFailed) return server.error("connection", tool, { cause: err.cause });
825
+ if (err instanceof Mismatch) return server.error("protocol", tool, { cause: err });
763
826
  if (!this.credential) return null;
764
827
  if (err instanceof InsufficientScopeError) {
765
828
  const auth: McpAuthNeed = err.requiredScope ? { kind: "scope", scope: err.requiredScope } : { kind: "scope" };