@absolutejs/mcp 0.6.0 → 0.7.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 CHANGED
@@ -262,6 +262,32 @@ elicitation is safe behind a load balancer with **no sticky routing** — there
262
262
  a test for exactly that: instance A asks, the answer lands on B, the bus carries
263
263
  it back, and A's call finishes.
264
264
 
265
+ AbsoluteJS already ships both production transports. PostgreSQL is the default;
266
+ Redis is an optional at-most-once fan-out optimization:
267
+
268
+ ```ts
269
+ import { createPostgresChannelBus } from "@absolutejs/sync-bus-pg";
270
+ import type { McpElicitAnswer } from "@absolutejs/mcp";
271
+
272
+ const bus = createPostgresChannelBus<McpElicitAnswer>({
273
+ sql,
274
+ channel: "absolutejs_mcp_elicitation",
275
+ spill: "always",
276
+ });
277
+
278
+ const config = {
279
+ // ...
280
+ elicitation: {
281
+ enabled: true,
282
+ store: createPostgresMcpSessionStore({ sql }),
283
+ bus,
284
+ },
285
+ };
286
+ ```
287
+
288
+ The channel is only coordination: durable jobs and side effects belong in
289
+ `@absolutejs/queue` / `@absolutejs/execution`, not Redis pub/sub or NOTIFY.
290
+
265
291
  Consuming a server that elicits? Pass `onElicit` to `createMcpClient` — that is
266
292
  what declares the capability, and what the package uses to answer. Omit it and
267
293
  servers are told you cannot ask anyone.
package/dist/index.js CHANGED
@@ -786,14 +786,14 @@ var resourcesRead = async (config, caller, id, params) => {
786
786
  ]
787
787
  });
788
788
  };
789
- var elicitAnswer = (message, context) => {
789
+ var elicitAnswer = async (message, context) => {
790
790
  const requestId = typeof message.id === "string" ? message.id : null;
791
791
  if (!requestId || !context.sessions)
792
792
  return notificationAck();
793
793
  const result = isRecord(message.result) ? message.result : null;
794
794
  const action = result?.action;
795
795
  const answer = action === "accept" && isRecord(result?.content) ? { action: "accept", content: result.content } : action === "decline" ? { action: "decline" } : { action: "cancel" };
796
- context.sessions.resolveElicit({
796
+ await context.sessions.resolveElicit({
797
797
  requestId,
798
798
  result: answer,
799
799
  sessionId: context.sessionId ?? null
@@ -984,6 +984,7 @@ var createSessionRegistry = (options) => {
984
984
  const elicitTimeoutMs = options?.elicitTimeoutMs ?? DEFAULT_ELICIT_TIMEOUT_MS;
985
985
  const store = options?.store ?? createMemoryStore(ttlMs);
986
986
  const pending = new Map;
987
+ let unsubscribe;
987
988
  const resolveLocal = (answer) => {
988
989
  const waiting = pending.get(answer.requestId);
989
990
  if (!waiting)
@@ -993,19 +994,39 @@ var createSessionRegistry = (options) => {
993
994
  waiting.resolve(answer.result);
994
995
  return true;
995
996
  };
996
- options?.bus?.subscribe((answer) => {
997
+ const ready = options?.bus ? Promise.resolve(options.bus.subscribe((answer) => {
997
998
  resolveLocal(answer);
998
- });
999
+ })).then((stop) => {
1000
+ unsubscribe = stop;
1001
+ }) : Promise.resolve();
999
1002
  return {
1000
- create: async (canElicit) => await store.create({ canElicit }),
1003
+ ready,
1004
+ close: async () => {
1005
+ await ready;
1006
+ await unsubscribe?.();
1007
+ pending.forEach(({ resolve, timer }) => {
1008
+ clearTimeout(timer);
1009
+ resolve({ action: "cancel" });
1010
+ });
1011
+ pending.clear();
1012
+ },
1013
+ create: async (canElicit) => {
1014
+ await ready;
1015
+ return await store.create({ canElicit });
1016
+ },
1001
1017
  drop: async (id) => {
1018
+ await ready;
1002
1019
  await store.drop(id);
1003
1020
  },
1004
- get: async (id) => id ? await store.get(id) : null,
1005
- resolveElicit: (answer) => {
1021
+ get: async (id) => {
1022
+ await ready;
1023
+ return id ? await store.get(id) : null;
1024
+ },
1025
+ resolveElicit: async (answer) => {
1026
+ await ready;
1006
1027
  if (resolveLocal(answer))
1007
1028
  return true;
1008
- options?.bus?.publish(answer);
1029
+ await options?.bus?.publish(answer);
1009
1030
  return false;
1010
1031
  },
1011
1032
  startElicit: (request) => {
@@ -1167,12 +1188,24 @@ var createPostgresMcpTaskStore = ({
1167
1188
  return {
1168
1189
  cancel: async (taskId) => {
1169
1190
  const updatedAt = now().toISOString();
1170
- await client.query(`UPDATE ${ns}.tasks SET status = 'cancelled', updated_at = $2::timestamptz, data = data || $3::jsonb WHERE task_id = $1 AND status NOT IN ('cancelled','completed','failed')`, [taskId, updatedAt, JSON.stringify({ lastUpdatedAt: updatedAt, status: "cancelled" })]);
1191
+ await client.query(`UPDATE ${ns}.tasks SET status = 'cancelled', updated_at = $2::timestamptz, data = data || $3::jsonb WHERE task_id = $1 AND status NOT IN ('cancelled','completed','failed')`, [
1192
+ taskId,
1193
+ updatedAt,
1194
+ JSON.stringify({ lastUpdatedAt: updatedAt, status: "cancelled" })
1195
+ ]);
1171
1196
  },
1172
1197
  get: async (taskId) => (await client.query(`SELECT data FROM ${ns}.tasks WHERE task_id = $1 AND (expires_at IS NULL OR expires_at > $2::timestamptz)`, [taskId, now().toISOString()])).rows[0]?.data ?? null,
1173
1198
  save: async (task) => {
1174
1199
  const expiresAt = task.ttlMs === null ? null : new Date(new Date(task.createdAt).getTime() + task.ttlMs).toISOString();
1175
- await client.query(`INSERT INTO ${ns}.tasks (task_id, authorization_key, status, created_at, updated_at, expires_at, data) VALUES ($1, $2, $3, $4::timestamptz, $5::timestamptz, $6::timestamptz, $7::jsonb) ON CONFLICT (task_id) DO NOTHING`, [task.taskId, task.authorizationKey, task.status, task.createdAt, task.lastUpdatedAt, expiresAt, JSON.stringify(task)]);
1200
+ await client.query(`INSERT INTO ${ns}.tasks (task_id, authorization_key, status, created_at, updated_at, expires_at, data) VALUES ($1, $2, $3, $4::timestamptz, $5::timestamptz, $6::timestamptz, $7::jsonb) ON CONFLICT (task_id) DO NOTHING`, [
1201
+ task.taskId,
1202
+ task.authorizationKey,
1203
+ task.status,
1204
+ task.createdAt,
1205
+ task.lastUpdatedAt,
1206
+ expiresAt,
1207
+ JSON.stringify(task)
1208
+ ]);
1176
1209
  },
1177
1210
  update: async (taskId, update) => {
1178
1211
  const updatedAt = now().toISOString();
@@ -1195,15 +1228,26 @@ var createPostgresMcpSessionStore = ({
1195
1228
  create: async ({ canElicit }) => {
1196
1229
  const id = crypto.randomUUID();
1197
1230
  const current = now();
1198
- await client.query(`INSERT INTO ${ns}.sessions (session_id, can_elicit, created_at, last_seen_at, expires_at) VALUES ($1, $2, $3::timestamptz, $3::timestamptz, $4::timestamptz)`, [id, canElicit, current.toISOString(), new Date(current.getTime() + ttlMs).toISOString()]);
1231
+ await client.query(`INSERT INTO ${ns}.sessions (session_id, can_elicit, created_at, last_seen_at, expires_at) VALUES ($1, $2, $3::timestamptz, $3::timestamptz, $4::timestamptz)`, [
1232
+ id,
1233
+ canElicit,
1234
+ current.toISOString(),
1235
+ new Date(current.getTime() + ttlMs).toISOString()
1236
+ ]);
1199
1237
  return id;
1200
1238
  },
1201
1239
  drop: async (id) => {
1202
- await client.query(`DELETE FROM ${ns}.sessions WHERE session_id = $1`, [id]);
1240
+ await client.query(`DELETE FROM ${ns}.sessions WHERE session_id = $1`, [
1241
+ id
1242
+ ]);
1203
1243
  },
1204
1244
  get: async (id) => {
1205
1245
  const current = now();
1206
- const result = await client.query(`UPDATE ${ns}.sessions SET last_seen_at = $2::timestamptz, expires_at = $3::timestamptz WHERE session_id = $1 AND expires_at > $2::timestamptz RETURNING can_elicit`, [id, current.toISOString(), new Date(current.getTime() + ttlMs).toISOString()]);
1246
+ const result = await client.query(`UPDATE ${ns}.sessions SET last_seen_at = $2::timestamptz, expires_at = $3::timestamptz WHERE session_id = $1 AND expires_at > $2::timestamptz RETURNING can_elicit`, [
1247
+ id,
1248
+ current.toISOString(),
1249
+ new Date(current.getTime() + ttlMs).toISOString()
1250
+ ]);
1207
1251
  const row = result.rows[0];
1208
1252
  return row === undefined ? null : { canElicit: row.can_elicit };
1209
1253
  }
@@ -6,6 +6,8 @@ export declare const createSessionRegistry: (options?: {
6
6
  store?: McpSessionStore;
7
7
  ttlMs?: number;
8
8
  }) => {
9
+ ready: Promise<void>;
10
+ close: () => Promise<void>;
9
11
  create: (canElicit: boolean) => Promise<string>;
10
12
  drop: (id: string) => Promise<void>;
11
13
  get: (id: string | null) => Promise<{
@@ -14,7 +16,7 @@ export declare const createSessionRegistry: (options?: {
14
16
  /** The client answered. If the call that asked is running HERE, resolve it.
15
17
  * If not, put the answer on the bus so the instance that is waiting can —
16
18
  * the answer must find the promise, and the promise cannot move. */
17
- resolveElicit: (answer: McpElicitAnswer) => boolean;
19
+ resolveElicit: (answer: McpElicitAnswer) => Promise<boolean>;
18
20
  /** Register an outbound question. Returns the id to send it under and the
19
21
  * promise that settles when the user answers — or when they never do. */
20
22
  startElicit: (request: McpElicitationRequest) => {
@@ -93,8 +93,8 @@ export type McpSessionStore = {
93
93
  * routing. Omit it and you must run a single instance (or pin sessions). */
94
94
  export type McpElicitBus = {
95
95
  /** An answer nobody here was waiting for — someone else might be. */
96
- publish: (answer: McpElicitAnswer) => void;
97
- subscribe: (handler: (answer: McpElicitAnswer) => void) => void;
96
+ publish: (answer: McpElicitAnswer) => void | Promise<void>;
97
+ subscribe: (handler: (answer: McpElicitAnswer) => void) => void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
98
98
  };
99
99
  /** Passed to a tool handler as its second argument. Ignore it and nothing
100
100
  * changes — every existing handler keeps working. */
package/package.json CHANGED
@@ -58,5 +58,5 @@
58
58
  "typecheck": "tsc --noEmit --project tsconfig.json"
59
59
  },
60
60
  "types": "./dist/src/index.d.ts",
61
- "version": "0.6.0"
61
+ "version": "0.7.0"
62
62
  }