@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.
- package/README.md +46 -7
- package/dist/auth.d.ts +107 -36
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +252 -37
- package/dist/auth.js.map +1 -1
- package/dist/credential.d.ts +55 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +62 -0
- package/dist/credential.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -9
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +193 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +558 -0
- package/dist/oauth.js.map +1 -0
- package/dist/postgres.d.ts +25 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +97 -0
- package/dist/postgres.js.map +1 -0
- package/dist/routes.d.ts +17 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +228 -0
- package/dist/routes.js.map +1 -0
- package/dist/seal.d.ts +25 -0
- package/dist/seal.d.ts.map +1 -0
- package/dist/seal.js +60 -0
- package/dist/seal.js.map +1 -0
- package/dist/store.d.ts +50 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +83 -0
- package/dist/store.js.map +1 -0
- package/dist/transports.d.ts +5 -1
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +2 -0
- package/dist/transports.js.map +1 -1
- package/package.json +6 -2
- package/src/auth.ts +354 -48
- package/src/credential.ts +78 -0
- package/src/errors.ts +7 -0
- package/src/index.ts +75 -12
- package/src/oauth.ts +659 -0
- package/src/postgres.ts +127 -0
- package/src/routes.ts +243 -0
- package/src/seal.ts +62 -0
- package/src/store.ts +118 -0
- 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 {
|
|
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 {
|
|
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: `
|
|
79
|
-
* Without it, every call shares
|
|
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
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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" };
|