@absolutejs/mcp 0.3.0 → 0.4.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.
- package/README.md +34 -5
- package/dist/index.js +73 -64
- package/dist/src/core.d.ts +6 -1
- package/dist/src/sessions.d.ts +15 -24
- package/dist/src/types.d.ts +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,11 +177,40 @@ spec also forbids eliciting **sensitive information**.
|
|
|
177
177
|
**The trade-off, stated plainly.** Elicitation is the one MCP feature a
|
|
178
178
|
stateless server cannot do: the question goes out on the SSE stream of an
|
|
179
179
|
in-flight `tools/call`, and the client answers on a _separate_ HTTP POST. Two
|
|
180
|
-
requests have to meet, so the
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
requests have to meet, so the endpoint becomes **session-stateful**
|
|
181
|
+
(`Mcp-Session-Id`). Leave `elicitation` off — the default — and nothing changes:
|
|
182
|
+
the server stays stateless, `tools/call` keeps answering with a plain JSON body,
|
|
183
|
+
and only tools marked `mayElicit` ever stream.
|
|
184
|
+
|
|
185
|
+
**Running more than one instance.** Behind one server the defaults handle it.
|
|
186
|
+
Behind several, two different things break, and each has a seam:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
elicitation: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
// (1) The client initializes on A and calls a tool on B, which has never
|
|
192
|
+
// heard of the session. Put session state where every instance sees it.
|
|
193
|
+
// It is an id and a boolean — nothing sensitive, nothing large.
|
|
194
|
+
store: {
|
|
195
|
+
create: ({ canElicit }) => db.insertSession(canElicit), // → id
|
|
196
|
+
get: (id) => db.findSession(id), // → { canElicit } | null
|
|
197
|
+
drop: (id) => db.deleteSession(id),
|
|
198
|
+
},
|
|
199
|
+
// (2) The tool call and its question live on ONE instance, but the user's
|
|
200
|
+
// answer POST can land on any of them. A promise cannot move, so route
|
|
201
|
+
// the answer to the instance that is waiting — over whatever fan-out you
|
|
202
|
+
// already run (Postgres LISTEN/NOTIFY, Redis, …).
|
|
203
|
+
bus: {
|
|
204
|
+
publish: (answer) => notify("mcp_elicit", answer),
|
|
205
|
+
subscribe: (handler) => listen("mcp_elicit", handler),
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Supply neither and run a single instance (or pin sessions). Supply both and
|
|
211
|
+
elicitation is safe behind a load balancer with **no sticky routing** — there is
|
|
212
|
+
a test for exactly that: instance A asks, the answer lands on B, the bus carries
|
|
213
|
+
it back, and A's call finishes.
|
|
185
214
|
|
|
186
215
|
Consuming a server that elicits? Pass `onElicit` to `createMcpClient` — that is
|
|
187
216
|
what declares the capability, and what the package uses to answer. Omit it and
|
package/dist/index.js
CHANGED
|
@@ -345,7 +345,7 @@ var clientCanElicit = (params) => {
|
|
|
345
345
|
return false;
|
|
346
346
|
return isRecord(params.capabilities.elicitation);
|
|
347
347
|
};
|
|
348
|
-
var initialize = (config, id, params, context) => {
|
|
348
|
+
var initialize = async (config, id, params, context) => {
|
|
349
349
|
const supported = config.supportedProtocols ?? DEFAULT_PROTOCOLS;
|
|
350
350
|
const capabilities = {
|
|
351
351
|
tools: { listChanged: false }
|
|
@@ -363,7 +363,7 @@ var initialize = (config, id, params, context) => {
|
|
|
363
363
|
});
|
|
364
364
|
if (!config.elicitation?.enabled || !context.sessions)
|
|
365
365
|
return response;
|
|
366
|
-
const sessionId = context.sessions.create(clientCanElicit(params));
|
|
366
|
+
const sessionId = await context.sessions.create(clientCanElicit(params));
|
|
367
367
|
response.headers.set("Mcp-Session-Id", sessionId);
|
|
368
368
|
return response;
|
|
369
369
|
};
|
|
@@ -418,7 +418,7 @@ var runTool = async (config, caller, id, name, args, meta, tool, context) => {
|
|
|
418
418
|
await config.onCall({ args, caller, meta, name, ok });
|
|
419
419
|
return payload;
|
|
420
420
|
};
|
|
421
|
-
var toolsCallStreaming = (config, caller, id, name, args, meta, tool, sessions,
|
|
421
|
+
var toolsCallStreaming = (config, caller, id, name, args, meta, tool, sessions, canElicit) => {
|
|
422
422
|
const encoder = new TextEncoder;
|
|
423
423
|
const body = new ReadableStream({
|
|
424
424
|
async start(controller) {
|
|
@@ -437,9 +437,7 @@ var toolsCallStreaming = (config, caller, id, name, args, meta, tool, sessions,
|
|
|
437
437
|
elicit: async (request) => {
|
|
438
438
|
if (!canElicit)
|
|
439
439
|
return { action: "unsupported" };
|
|
440
|
-
const pending = sessions.startElicit(
|
|
441
|
-
if (!pending.id)
|
|
442
|
-
return { action: "cancel" };
|
|
440
|
+
const pending = sessions.startElicit(request);
|
|
443
441
|
send({
|
|
444
442
|
id: pending.id,
|
|
445
443
|
jsonrpc: "2.0",
|
|
@@ -479,10 +477,10 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
|
|
|
479
477
|
if (!tool || !scopeAllows(tool, scopes)) {
|
|
480
478
|
return rpcError(id, JSONRPC_INVALID_PARAMS, `Unknown tool: ${name}`);
|
|
481
479
|
}
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
485
|
-
return toolsCallStreaming(config, caller, id, name, args, meta, tool,
|
|
480
|
+
const sessions = context.sessions;
|
|
481
|
+
const session = sessions ? await sessions.get(context.sessionId ?? null) : null;
|
|
482
|
+
if (tool.mayElicit === true && config.elicitation?.enabled === true && sessions && session) {
|
|
483
|
+
return toolsCallStreaming(config, caller, id, name, args, meta, tool, sessions, session.canElicit);
|
|
486
484
|
}
|
|
487
485
|
const payload = await runTool(config, caller, id, name, args, meta, tool, noElicit);
|
|
488
486
|
return new Response(JSON.stringify(payload), {
|
|
@@ -559,7 +557,11 @@ var elicitAnswer = (message, context) => {
|
|
|
559
557
|
const result = isRecord(message.result) ? message.result : null;
|
|
560
558
|
const action = result?.action;
|
|
561
559
|
const answer = action === "accept" && isRecord(result?.content) ? { action: "accept", content: result.content } : action === "decline" ? { action: "decline" } : { action: "cancel" };
|
|
562
|
-
context.sessions.resolveElicit(
|
|
560
|
+
context.sessions.resolveElicit({
|
|
561
|
+
requestId,
|
|
562
|
+
result: answer,
|
|
563
|
+
sessionId: context.sessionId ?? null
|
|
564
|
+
});
|
|
563
565
|
return notificationAck();
|
|
564
566
|
};
|
|
565
567
|
var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
|
|
@@ -574,7 +576,7 @@ var dispatchMcp = async (config, caller, scopes, message, context = {}) => {
|
|
|
574
576
|
const method = typeof message.method === "string" ? message.method : "";
|
|
575
577
|
const { params } = message;
|
|
576
578
|
if (method === "initialize") {
|
|
577
|
-
return initialize(config, id, params, context);
|
|
579
|
+
return await initialize(config, id, params, context);
|
|
578
580
|
}
|
|
579
581
|
if (method === "ping")
|
|
580
582
|
return rpcResult(id, {});
|
|
@@ -694,9 +696,7 @@ var feedbackTools = (config) => ({
|
|
|
694
696
|
var DEFAULT_SESSION_TTL_MS = 3600000;
|
|
695
697
|
var DEFAULT_ELICIT_TIMEOUT_MS = 120000;
|
|
696
698
|
var SWEEP_EVERY = 50;
|
|
697
|
-
var
|
|
698
|
-
const ttlMs = options?.ttlMs ?? DEFAULT_SESSION_TTL_MS;
|
|
699
|
-
const elicitTimeoutMs = options?.elicitTimeoutMs ?? DEFAULT_ELICIT_TIMEOUT_MS;
|
|
699
|
+
var createMemoryStore = (ttlMs) => {
|
|
700
700
|
const sessions = new Map;
|
|
701
701
|
let sinceSweep = 0;
|
|
702
702
|
const sweep = () => {
|
|
@@ -706,66 +706,66 @@ var createSessionRegistry = (options) => {
|
|
|
706
706
|
sinceSweep = 0;
|
|
707
707
|
const cutoff = Date.now() - ttlMs;
|
|
708
708
|
sessions.forEach((session, id) => {
|
|
709
|
-
if (session.lastSeen
|
|
710
|
-
|
|
711
|
-
session.pending.forEach((pending) => {
|
|
712
|
-
clearTimeout(pending.timer);
|
|
713
|
-
pending.resolve({ action: "cancel" });
|
|
714
|
-
});
|
|
715
|
-
sessions.delete(id);
|
|
709
|
+
if (session.lastSeen < cutoff)
|
|
710
|
+
sessions.delete(id);
|
|
716
711
|
});
|
|
717
712
|
};
|
|
718
|
-
const touch = (id) => {
|
|
719
|
-
if (!id)
|
|
720
|
-
return null;
|
|
721
|
-
const session = sessions.get(id);
|
|
722
|
-
if (!session)
|
|
723
|
-
return null;
|
|
724
|
-
session.lastSeen = Date.now();
|
|
725
|
-
return session;
|
|
726
|
-
};
|
|
727
713
|
return {
|
|
728
|
-
create: (
|
|
714
|
+
create: (session) => {
|
|
729
715
|
sweep();
|
|
730
716
|
const id = crypto.randomUUID();
|
|
731
|
-
sessions.set(id, { canElicit, lastSeen: Date.now()
|
|
717
|
+
sessions.set(id, { canElicit: session.canElicit, lastSeen: Date.now() });
|
|
732
718
|
return id;
|
|
733
719
|
},
|
|
734
720
|
drop: (id) => {
|
|
735
|
-
const session = sessions.get(id);
|
|
736
|
-
session?.pending.forEach((pending) => {
|
|
737
|
-
clearTimeout(pending.timer);
|
|
738
|
-
pending.resolve({ action: "cancel" });
|
|
739
|
-
});
|
|
740
721
|
sessions.delete(id);
|
|
741
722
|
},
|
|
742
|
-
get: (id) =>
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
723
|
+
get: (id) => {
|
|
724
|
+
const session = sessions.get(id);
|
|
725
|
+
if (!session)
|
|
726
|
+
return null;
|
|
727
|
+
session.lastSeen = Date.now();
|
|
728
|
+
return { canElicit: session.canElicit };
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
var createSessionRegistry = (options) => {
|
|
733
|
+
const ttlMs = options?.ttlMs ?? DEFAULT_SESSION_TTL_MS;
|
|
734
|
+
const elicitTimeoutMs = options?.elicitTimeoutMs ?? DEFAULT_ELICIT_TIMEOUT_MS;
|
|
735
|
+
const store = options?.store ?? createMemoryStore(ttlMs);
|
|
736
|
+
const pending = new Map;
|
|
737
|
+
const resolveLocal = (answer) => {
|
|
738
|
+
const waiting = pending.get(answer.requestId);
|
|
739
|
+
if (!waiting)
|
|
740
|
+
return false;
|
|
741
|
+
clearTimeout(waiting.timer);
|
|
742
|
+
pending.delete(answer.requestId);
|
|
743
|
+
waiting.resolve(answer.result);
|
|
744
|
+
return true;
|
|
745
|
+
};
|
|
746
|
+
options?.bus?.subscribe((answer) => {
|
|
747
|
+
resolveLocal(answer);
|
|
748
|
+
});
|
|
749
|
+
return {
|
|
750
|
+
create: async (canElicit) => await store.create({ canElicit }),
|
|
751
|
+
drop: async (id) => {
|
|
752
|
+
await store.drop(id);
|
|
753
|
+
},
|
|
754
|
+
get: async (id) => id ? await store.get(id) : null,
|
|
755
|
+
resolveElicit: (answer) => {
|
|
756
|
+
if (resolveLocal(answer))
|
|
757
|
+
return true;
|
|
758
|
+
options?.bus?.publish(answer);
|
|
759
|
+
return false;
|
|
752
760
|
},
|
|
753
|
-
startElicit: (
|
|
754
|
-
const session = sessions.get(sessionId);
|
|
755
|
-
if (!session) {
|
|
756
|
-
return {
|
|
757
|
-
answer: Promise.resolve({ action: "cancel" }),
|
|
758
|
-
id: "",
|
|
759
|
-
request
|
|
760
|
-
};
|
|
761
|
-
}
|
|
761
|
+
startElicit: (request) => {
|
|
762
762
|
const id = `elicit_${crypto.randomUUID()}`;
|
|
763
763
|
const answer = new Promise((resolve) => {
|
|
764
764
|
const timer = setTimeout(() => {
|
|
765
|
-
|
|
765
|
+
pending.delete(id);
|
|
766
766
|
resolve({ action: "cancel" });
|
|
767
767
|
}, elicitTimeoutMs);
|
|
768
|
-
|
|
768
|
+
pending.set(id, { resolve, timer });
|
|
769
769
|
});
|
|
770
770
|
return { answer, id, request };
|
|
771
771
|
}
|
|
@@ -787,6 +787,9 @@ var JSON_HEADERS = {
|
|
|
787
787
|
};
|
|
788
788
|
var HTTP_NOT_FOUND = 404;
|
|
789
789
|
var registries = new WeakMap;
|
|
790
|
+
var primeMcpSessions = (config) => {
|
|
791
|
+
registryFor(config);
|
|
792
|
+
};
|
|
790
793
|
var registryFor = (config) => {
|
|
791
794
|
if (!config.elicitation?.enabled)
|
|
792
795
|
return;
|
|
@@ -794,7 +797,9 @@ var registryFor = (config) => {
|
|
|
794
797
|
if (existing)
|
|
795
798
|
return existing;
|
|
796
799
|
const created = createSessionRegistry({
|
|
797
|
-
|
|
800
|
+
...config.elicitation.bus === undefined ? {} : { bus: config.elicitation.bus },
|
|
801
|
+
...config.elicitation.timeoutMs === undefined ? {} : { elicitTimeoutMs: config.elicitation.timeoutMs },
|
|
802
|
+
...config.elicitation.store === undefined ? {} : { store: config.elicitation.store }
|
|
798
803
|
});
|
|
799
804
|
registries.set(config, created);
|
|
800
805
|
return created;
|
|
@@ -817,7 +822,7 @@ var runMcpPost = async (config, request, body) => {
|
|
|
817
822
|
}
|
|
818
823
|
const sessions = registryFor(config);
|
|
819
824
|
const sessionId = request.headers.get("mcp-session-id");
|
|
820
|
-
if (sessions && sessionId && !sessions.get(sessionId)) {
|
|
825
|
+
if (sessions && sessionId && !await sessions.get(sessionId)) {
|
|
821
826
|
return new Response(null, { status: HTTP_NOT_FOUND });
|
|
822
827
|
}
|
|
823
828
|
return dispatchMcp(config, auth.caller, auth.scopes ?? [], body, {
|
|
@@ -825,13 +830,13 @@ var runMcpPost = async (config, request, body) => {
|
|
|
825
830
|
sessions
|
|
826
831
|
}).catch(() => rpcError(null, JSONRPC_INVALID_REQUEST, "Internal error"));
|
|
827
832
|
};
|
|
828
|
-
var runMcpDelete = (config, request) => {
|
|
833
|
+
var runMcpDelete = async (config, request) => {
|
|
829
834
|
const sessions = registryFor(config);
|
|
830
835
|
const sessionId = request.headers.get("mcp-session-id");
|
|
831
836
|
if (!sessions || !sessionId) {
|
|
832
837
|
return new Response(null, { status: HTTP_METHOD_NOT_ALLOWED });
|
|
833
838
|
}
|
|
834
|
-
sessions.drop(sessionId);
|
|
839
|
+
await sessions.drop(sessionId);
|
|
835
840
|
return new Response(null, { status: HTTP_NO_CONTENT });
|
|
836
841
|
};
|
|
837
842
|
var handleMcpRequest = async (config, request) => {
|
|
@@ -861,11 +866,15 @@ var handleMcpRequest = async (config, request) => {
|
|
|
861
866
|
};
|
|
862
867
|
|
|
863
868
|
// src/handler.ts
|
|
864
|
-
var createMcpHandler = (config) =>
|
|
869
|
+
var createMcpHandler = (config) => {
|
|
870
|
+
primeMcpSessions(config);
|
|
871
|
+
return (request) => handleMcpRequest(config, request);
|
|
872
|
+
};
|
|
865
873
|
// src/server.ts
|
|
866
874
|
import { Elysia } from "elysia";
|
|
867
875
|
var mcpServer = (config) => {
|
|
868
876
|
const metadataPath = metadataPathFor(config.path);
|
|
877
|
+
primeMcpSessions(config);
|
|
869
878
|
const base = new Elysia().get(metadataPath, () => metadataResponse(config)).get(config.path, () => new Response(null, { status: HTTP_METHOD_NOT_ALLOWED })).post(config.path, ({ body, request }) => runMcpPost(config, request, body)).delete(config.path, ({ request }) => runMcpDelete(config, request));
|
|
870
879
|
const app = config.serveRootMetadata ? base.get(ROOT_METADATA_PATH, () => metadataResponse(config)) : base;
|
|
871
880
|
return app;
|
package/dist/src/core.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { McpServerConfig } from "./types";
|
|
2
2
|
export declare const ROOT_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
3
|
+
/** Build the session registry NOW rather than on the first request. The bus
|
|
4
|
+
* subscription has to exist before any answer can arrive, and an instance that
|
|
5
|
+
* has served no traffic yet is exactly the one a load balancer is about to
|
|
6
|
+
* hand an answer to. */
|
|
7
|
+
export declare const primeMcpSessions: <Caller>(config: McpServerConfig<Caller>) => void;
|
|
3
8
|
export declare const metadataResponse: <Caller>(config: McpServerConfig<Caller>) => Response;
|
|
4
9
|
/** Run one POST: authorize → validate the (already-decoded) body → dispatch.
|
|
5
10
|
* The body is passed in because Elysia pre-parses it while a raw handler must
|
|
@@ -8,7 +13,7 @@ export declare const runMcpPost: <Caller>(config: McpServerConfig<Caller>, reque
|
|
|
8
13
|
/** The client is done with its session and says so (spec: Session Management).
|
|
9
14
|
* Only meaningful when elicitation put us in a session at all — otherwise 405,
|
|
10
15
|
* which the spec explicitly allows for servers that don't do sessions. */
|
|
11
|
-
export declare const runMcpDelete: <Caller>(config: McpServerConfig<Caller>, request: Request) => Response
|
|
16
|
+
export declare const runMcpDelete: <Caller>(config: McpServerConfig<Caller>, request: Request) => Promise<Response>;
|
|
12
17
|
/** The full path-aware handler over web-standard Request/Response. Returns a
|
|
13
18
|
* Response for any MCP route (POST endpoint, GET 405, discovery metadata) and
|
|
14
19
|
* `null` for anything else, so a host can compose it with its own routes. */
|
package/dist/src/sessions.d.ts
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
import type { McpElicitResult, McpElicitationRequest } from "./types";
|
|
2
|
-
type Pending = {
|
|
3
|
-
resolve: (result: McpElicitResult) => void;
|
|
4
|
-
timer: ReturnType<typeof setTimeout>;
|
|
5
|
-
};
|
|
6
|
-
type Session = {
|
|
7
|
-
/** The client declared the `elicitation` capability at initialize. */
|
|
8
|
-
canElicit: boolean;
|
|
9
|
-
lastSeen: number;
|
|
10
|
-
/** In-flight elicitations, keyed by the JSON-RPC id we sent. */
|
|
11
|
-
pending: Map<string, Pending>;
|
|
12
|
-
};
|
|
1
|
+
import type { McpElicitAnswer, McpElicitBus, McpElicitResult, McpElicitationRequest, McpSessionStore } from "./types";
|
|
13
2
|
export type SessionRegistry = ReturnType<typeof createSessionRegistry>;
|
|
14
3
|
export declare const createSessionRegistry: (options?: {
|
|
4
|
+
bus?: McpElicitBus;
|
|
15
5
|
elicitTimeoutMs?: number;
|
|
6
|
+
store?: McpSessionStore;
|
|
16
7
|
ttlMs?: number;
|
|
17
8
|
}) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
create: (canElicit: boolean) => Promise<string>;
|
|
10
|
+
drop: (id: string) => Promise<void>;
|
|
11
|
+
get: (id: string | null) => Promise<{
|
|
12
|
+
canElicit: boolean;
|
|
13
|
+
} | null>;
|
|
14
|
+
/** The client answered. If the call that asked is running HERE, resolve it.
|
|
15
|
+
* If not, put the answer on the bus so the instance that is waiting can —
|
|
16
|
+
* the answer must find the promise, and the promise cannot move. */
|
|
17
|
+
resolveElicit: (answer: McpElicitAnswer) => boolean;
|
|
18
|
+
/** Register an outbound question. Returns the id to send it under and the
|
|
19
|
+
* promise that settles when the user answers — or when they never do. */
|
|
20
|
+
startElicit: (request: McpElicitationRequest) => {
|
|
29
21
|
answer: Promise<McpElicitResult>;
|
|
30
22
|
id: string;
|
|
31
23
|
request: McpElicitationRequest;
|
|
32
24
|
};
|
|
33
25
|
};
|
|
34
|
-
export {};
|
package/dist/src/types.d.ts
CHANGED
|
@@ -64,6 +64,36 @@ export type McpElicitResult = {
|
|
|
64
64
|
} | {
|
|
65
65
|
action: "unsupported";
|
|
66
66
|
};
|
|
67
|
+
/** The client's answer, on its way back to whichever instance is waiting. */
|
|
68
|
+
export type McpElicitAnswer = {
|
|
69
|
+
requestId: string;
|
|
70
|
+
result: McpElicitResult;
|
|
71
|
+
sessionId: string | null;
|
|
72
|
+
};
|
|
73
|
+
/** Where session state lives. The default is in-memory (one instance). Put it
|
|
74
|
+
* in your database and any instance can serve any session. Nothing here is
|
|
75
|
+
* sensitive or large — an id and a capability flag. */
|
|
76
|
+
export type McpSessionStore = {
|
|
77
|
+
create: (session: {
|
|
78
|
+
canElicit: boolean;
|
|
79
|
+
}) => Promise<string> | string;
|
|
80
|
+
drop: (id: string) => Promise<void> | void;
|
|
81
|
+
get: (id: string) => Promise<{
|
|
82
|
+
canElicit: boolean;
|
|
83
|
+
} | null> | {
|
|
84
|
+
canElicit: boolean;
|
|
85
|
+
} | null;
|
|
86
|
+
};
|
|
87
|
+
/** How an answer reaches the instance that asked the question. The tool call
|
|
88
|
+
* and its pending promise live on ONE process; the client's answer POST can
|
|
89
|
+
* land on any of them. Wire this to whatever fan-out you already run
|
|
90
|
+
* (Postgres LISTEN/NOTIFY, Redis, …) and elicitation works with no sticky
|
|
91
|
+
* routing. Omit it and you must run a single instance (or pin sessions). */
|
|
92
|
+
export type McpElicitBus = {
|
|
93
|
+
/** An answer nobody here was waiting for — someone else might be. */
|
|
94
|
+
publish: (answer: McpElicitAnswer) => void;
|
|
95
|
+
subscribe: (handler: (answer: McpElicitAnswer) => void) => void;
|
|
96
|
+
};
|
|
67
97
|
/** Passed to a tool handler as its second argument. Ignore it and nothing
|
|
68
98
|
* changes — every existing handler keeps working. */
|
|
69
99
|
export type McpToolCallContext = {
|
|
@@ -181,7 +211,13 @@ export type McpServerConfig<Caller> = {
|
|
|
181
211
|
* remembered in-process. Run one instance, or pin `Mcp-Session-Id`. Tools
|
|
182
212
|
* must also opt in with `mayElicit`. */
|
|
183
213
|
elicitation?: {
|
|
214
|
+
/** Route answers to the instance that asked. Required to run more than one
|
|
215
|
+
* instance without sticky sessions. */
|
|
216
|
+
bus?: McpElicitBus;
|
|
184
217
|
enabled: true;
|
|
218
|
+
/** Shared session state. Required to run more than one instance. */
|
|
219
|
+
store?: McpSessionStore;
|
|
220
|
+
/** How long a question waits for a human before it gives up (default 2m). */
|
|
185
221
|
timeoutMs?: number;
|
|
186
222
|
};
|
|
187
223
|
/** Page size for tools/prompts/resources list pagination (default 50). */
|
package/package.json
CHANGED