@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 +26 -0
- package/dist/index.js +57 -13
- package/dist/src/sessions.d.ts +3 -1
- package/dist/src/types.d.ts +2 -2
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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) =>
|
|
1005
|
-
|
|
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')`, [
|
|
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`, [
|
|
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)`, [
|
|
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`, [
|
|
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`, [
|
|
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
|
}
|
package/dist/src/sessions.d.ts
CHANGED
|
@@ -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) => {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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