@frontmcp/plugin-remember 1.10.9-rc.1 → 1.10.9-rc.3
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/esm/index.mjs +2 -1742
- package/esm/package.json +5 -5
- package/index.js +2 -1771
- package/package.json +5 -5
package/esm/index.mjs
CHANGED
|
@@ -1,1745 +1,5 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
5
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
6
|
-
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
7
|
-
if (decorator = decorators[i])
|
|
8
|
-
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
9
|
-
if (kind && result) __defProp(target, key, result);
|
|
10
|
-
return result;
|
|
11
|
-
};
|
|
12
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
13
|
-
|
|
14
|
-
// plugins/plugin-remember/src/remember.plugin.ts
|
|
15
|
-
import {
|
|
16
|
-
DynamicPlugin,
|
|
17
|
-
FRONTMCP_CONTEXT,
|
|
18
|
-
FrontMcpConfig,
|
|
19
|
-
FrontMcpLogger,
|
|
20
|
-
getGlobalStoreConfig,
|
|
21
|
-
isVercelKvProvider,
|
|
22
|
-
Plugin,
|
|
23
|
-
ProviderScope as ProviderScope5
|
|
24
|
-
} from "@frontmcp/sdk";
|
|
25
|
-
|
|
26
|
-
// plugins/plugin-remember/src/providers/remember-accessor.provider.ts
|
|
27
|
-
import {
|
|
28
|
-
getRunningTool,
|
|
29
|
-
isAnonymousSubject,
|
|
30
|
-
Provider,
|
|
31
|
-
ProviderScope,
|
|
32
|
-
STATELESS_SESSION_ID
|
|
33
|
-
} from "@frontmcp/sdk";
|
|
34
|
-
|
|
35
|
-
// plugins/plugin-remember/src/remember.crypto.ts
|
|
36
|
-
import {
|
|
37
|
-
base64urlDecode,
|
|
38
|
-
base64urlEncode as base64urlEncode2,
|
|
39
|
-
decryptAesGcm,
|
|
40
|
-
encryptAesGcm,
|
|
41
|
-
hkdfSha256,
|
|
42
|
-
randomBytes as randomBytes2
|
|
43
|
-
} from "@frontmcp/utils";
|
|
44
|
-
|
|
45
|
-
// plugins/plugin-remember/src/remember.secret-persistence.ts
|
|
46
|
-
import * as path from "path";
|
|
47
|
-
import { z } from "@frontmcp/lazy-zod";
|
|
48
|
-
import { base64urlEncode, mkdir, randomBytes, readFile, rename, unlink, writeFile } from "@frontmcp/utils";
|
|
49
|
-
var DEFAULT_SECRET_FILE_PATH = ".frontmcp/remember-secret.json";
|
|
50
|
-
var SECRET_BYTES = 32;
|
|
51
|
-
var rememberSecretDataSchema = z.object({
|
|
52
|
-
secret: z.string().min(32),
|
|
53
|
-
// base64url of 32 bytes is ~43 chars
|
|
54
|
-
createdAt: z.number().positive().int(),
|
|
55
|
-
version: z.literal(1)
|
|
56
|
-
});
|
|
57
|
-
function validateSecretData(data) {
|
|
58
|
-
const result = rememberSecretDataSchema.safeParse(data);
|
|
59
|
-
if (!result.success) {
|
|
60
|
-
return { valid: false, error: result.error.issues[0]?.message ?? "Invalid secret structure" };
|
|
61
|
-
}
|
|
62
|
-
const parsed = result.data;
|
|
63
|
-
const now = Date.now();
|
|
64
|
-
if (parsed.createdAt > now + 6e4) {
|
|
65
|
-
return { valid: false, error: "createdAt is in the future" };
|
|
66
|
-
}
|
|
67
|
-
const hundredYearsMs = 100 * 365 * 24 * 60 * 60 * 1e3;
|
|
68
|
-
if (parsed.createdAt < now - hundredYearsMs) {
|
|
69
|
-
return { valid: false, error: "createdAt is too old" };
|
|
70
|
-
}
|
|
71
|
-
return { valid: true };
|
|
72
|
-
}
|
|
73
|
-
function isSecretPersistenceEnabled(options) {
|
|
74
|
-
const isProduction = process.env["NODE_ENV"] === "production";
|
|
75
|
-
if (isProduction) {
|
|
76
|
-
return options?.forceEnable === true;
|
|
77
|
-
}
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
function resolveSecretPath(options) {
|
|
81
|
-
const secretPath = options?.secretPath ?? DEFAULT_SECRET_FILE_PATH;
|
|
82
|
-
if (path.isAbsolute(secretPath)) {
|
|
83
|
-
return secretPath;
|
|
84
|
-
}
|
|
85
|
-
return path.resolve(process.cwd(), secretPath);
|
|
86
|
-
}
|
|
87
|
-
async function loadRememberSecret(options) {
|
|
88
|
-
if (!isSecretPersistenceEnabled(options)) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const secretPath = resolveSecretPath(options);
|
|
92
|
-
try {
|
|
93
|
-
const content = await readFile(secretPath, "utf8");
|
|
94
|
-
const data = JSON.parse(content);
|
|
95
|
-
const validation = validateSecretData(data);
|
|
96
|
-
if (!validation.valid) {
|
|
97
|
-
console.warn(
|
|
98
|
-
`[RememberSecretPersistence] Invalid secret file format at ${secretPath}: ${validation.error}, will regenerate`
|
|
99
|
-
);
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
return data;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if (error.code === "ENOENT") {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
console.warn(`[RememberSecretPersistence] Failed to load secret from ${secretPath}: ${error.message}`);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
async function saveRememberSecret(secretData, options) {
|
|
112
|
-
if (!isSecretPersistenceEnabled(options)) {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
const secretPath = resolveSecretPath(options);
|
|
116
|
-
const dir = path.dirname(secretPath);
|
|
117
|
-
const tempPath = `${secretPath}.tmp.${Date.now()}.${base64urlEncode(randomBytes(8))}`;
|
|
118
|
-
try {
|
|
119
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
120
|
-
const content = JSON.stringify(secretData, null, 2);
|
|
121
|
-
await writeFile(tempPath, content, { mode: 384 });
|
|
122
|
-
await rename(tempPath, secretPath);
|
|
123
|
-
return true;
|
|
124
|
-
} catch (error) {
|
|
125
|
-
console.error(`[RememberSecretPersistence] Failed to save secret to ${secretPath}: ${error.message}`);
|
|
126
|
-
try {
|
|
127
|
-
await unlink(tempPath);
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
var cachedSecret = null;
|
|
134
|
-
var secretGenerationPromise = null;
|
|
135
|
-
function generateSecret() {
|
|
136
|
-
return base64urlEncode(randomBytes(SECRET_BYTES));
|
|
137
|
-
}
|
|
138
|
-
async function getOrCreatePersistedSecret(options) {
|
|
139
|
-
if (cachedSecret) {
|
|
140
|
-
return cachedSecret;
|
|
141
|
-
}
|
|
142
|
-
if (secretGenerationPromise) {
|
|
143
|
-
return secretGenerationPromise;
|
|
144
|
-
}
|
|
145
|
-
secretGenerationPromise = (async () => {
|
|
146
|
-
try {
|
|
147
|
-
const loaded = await loadRememberSecret(options);
|
|
148
|
-
if (loaded) {
|
|
149
|
-
cachedSecret = loaded.secret;
|
|
150
|
-
return cachedSecret;
|
|
151
|
-
}
|
|
152
|
-
const newSecret = generateSecret();
|
|
153
|
-
const secretData = {
|
|
154
|
-
secret: newSecret,
|
|
155
|
-
createdAt: Date.now(),
|
|
156
|
-
version: 1
|
|
157
|
-
};
|
|
158
|
-
if (isSecretPersistenceEnabled(options)) {
|
|
159
|
-
await saveRememberSecret(secretData, options);
|
|
160
|
-
} else {
|
|
161
|
-
console.warn(
|
|
162
|
-
"[RememberSecretPersistence] No REMEMBER_SECRET (or MCP_MEMORY_SECRET / MCP_SESSION_SECRET) is set in production, so a random in-memory secret is used. Encrypted memory will be unreadable after a restart and by other instances sharing the store. Set REMEMBER_SECRET to the same value on every instance."
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
cachedSecret = newSecret;
|
|
166
|
-
return cachedSecret;
|
|
167
|
-
} finally {
|
|
168
|
-
secretGenerationPromise = null;
|
|
169
|
-
}
|
|
170
|
-
})();
|
|
171
|
-
return secretGenerationPromise;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// plugins/plugin-remember/src/remember.crypto.ts
|
|
175
|
-
async function getBaseSecret() {
|
|
176
|
-
const envSecret = process.env["REMEMBER_SECRET"] || process.env["MCP_MEMORY_SECRET"] || process.env["MCP_SESSION_SECRET"];
|
|
177
|
-
if (envSecret) {
|
|
178
|
-
return envSecret;
|
|
179
|
-
}
|
|
180
|
-
return getOrCreatePersistedSecret();
|
|
181
|
-
}
|
|
182
|
-
async function secretFor(source) {
|
|
183
|
-
return source.secret ? source.secret : getBaseSecret();
|
|
184
|
-
}
|
|
185
|
-
var textEncoder = new TextEncoder();
|
|
186
|
-
async function deriveEncryptionKey(source) {
|
|
187
|
-
const salt = textEncoder.encode("remember-plugin-v1");
|
|
188
|
-
let ikm;
|
|
189
|
-
let context;
|
|
190
|
-
switch (source.type) {
|
|
191
|
-
case "session":
|
|
192
|
-
ikm = await secretFor(source) + source.sessionId;
|
|
193
|
-
context = `remember:session:${source.sessionId}`;
|
|
194
|
-
break;
|
|
195
|
-
case "tool":
|
|
196
|
-
ikm = await secretFor(source) + source.sessionId;
|
|
197
|
-
context = `remember:tool:${source.toolName}:${source.sessionId}`;
|
|
198
|
-
break;
|
|
199
|
-
case "user":
|
|
200
|
-
ikm = await secretFor(source) + source.userId;
|
|
201
|
-
context = `remember:user:${source.userId}`;
|
|
202
|
-
break;
|
|
203
|
-
case "global":
|
|
204
|
-
ikm = await secretFor(source);
|
|
205
|
-
context = "remember:global";
|
|
206
|
-
break;
|
|
207
|
-
case "custom":
|
|
208
|
-
ikm = source.key;
|
|
209
|
-
context = `remember:custom:${source.key}`;
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
const ikmBytes = textEncoder.encode(ikm);
|
|
213
|
-
const info = textEncoder.encode(context);
|
|
214
|
-
return hkdfSha256(ikmBytes, salt, info, 32);
|
|
215
|
-
}
|
|
216
|
-
function getKeySourceForScope(scope, context, secret) {
|
|
217
|
-
switch (scope) {
|
|
218
|
-
case "session":
|
|
219
|
-
return { type: "session", sessionId: context.sessionId, secret };
|
|
220
|
-
case "user":
|
|
221
|
-
return { type: "user", userId: context.userId ?? "anonymous", secret };
|
|
222
|
-
case "tool":
|
|
223
|
-
return {
|
|
224
|
-
type: "tool",
|
|
225
|
-
toolName: context.toolName ?? "unknown",
|
|
226
|
-
sessionId: context.sessionId,
|
|
227
|
-
secret
|
|
228
|
-
};
|
|
229
|
-
case "global":
|
|
230
|
-
return { type: "global", secret };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
var textDecoder = new TextDecoder();
|
|
234
|
-
async function encryptValue(data, keySource) {
|
|
235
|
-
const key = await deriveEncryptionKey(keySource);
|
|
236
|
-
const plaintext = textEncoder.encode(JSON.stringify(data));
|
|
237
|
-
const iv = randomBytes2(12);
|
|
238
|
-
const { ciphertext, tag } = encryptAesGcm(key, plaintext, iv);
|
|
239
|
-
return {
|
|
240
|
-
alg: "A256GCM",
|
|
241
|
-
iv: base64urlEncode2(iv),
|
|
242
|
-
tag: base64urlEncode2(tag),
|
|
243
|
-
data: base64urlEncode2(ciphertext)
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
async function decryptValue(blob, keySource) {
|
|
247
|
-
try {
|
|
248
|
-
const key = await deriveEncryptionKey(keySource);
|
|
249
|
-
const iv = base64urlDecode(blob.iv);
|
|
250
|
-
const tag = base64urlDecode(blob.tag);
|
|
251
|
-
const ciphertext = base64urlDecode(blob.data);
|
|
252
|
-
const decrypted = decryptAesGcm(key, ciphertext, iv, tag);
|
|
253
|
-
return JSON.parse(textDecoder.decode(decrypted));
|
|
254
|
-
} catch {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function serializeBlob(blob) {
|
|
259
|
-
return JSON.stringify(blob);
|
|
260
|
-
}
|
|
261
|
-
function deserializeBlob(str) {
|
|
262
|
-
try {
|
|
263
|
-
const parsed = JSON.parse(str);
|
|
264
|
-
if (parsed && typeof parsed === "object" && parsed.alg === "A256GCM" && typeof parsed.iv === "string" && typeof parsed.tag === "string" && typeof parsed.data === "string") {
|
|
265
|
-
return parsed;
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
268
|
-
} catch {
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
async function encryptAndSerialize(data, keySource) {
|
|
273
|
-
const blob = await encryptValue(data, keySource);
|
|
274
|
-
return serializeBlob(blob);
|
|
275
|
-
}
|
|
276
|
-
async function deserializeAndDecrypt(str, keySource) {
|
|
277
|
-
const blob = deserializeBlob(str);
|
|
278
|
-
if (!blob) return null;
|
|
279
|
-
return decryptValue(blob, keySource);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// plugins/plugin-remember/src/remember.errors.ts
|
|
283
|
-
import { InternalMcpError, PublicMcpError } from "@frontmcp/sdk";
|
|
284
|
-
var RememberIdentityError = class extends PublicMcpError {
|
|
285
|
-
name = "RememberIdentityError";
|
|
286
|
-
constructor(message) {
|
|
287
|
-
super(message, "REMEMBER_IDENTITY_REQUIRED", 403);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
var RememberScopeNotAllowedError = class extends PublicMcpError {
|
|
291
|
-
name = "RememberScopeNotAllowedError";
|
|
292
|
-
constructor(scope, allowedScopes) {
|
|
293
|
-
super(
|
|
294
|
-
`Scope '${scope}' is not allowed. Allowed scopes: ${allowedScopes.join(", ")}`,
|
|
295
|
-
"REMEMBER_SCOPE_NOT_ALLOWED",
|
|
296
|
-
400
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
var RememberConfigurationError = class extends InternalMcpError {
|
|
301
|
-
name = "RememberConfigurationError";
|
|
302
|
-
constructor(message) {
|
|
303
|
-
super(message, "REMEMBER_CONFIGURATION");
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// plugins/plugin-remember/src/remember.legacy-purge.ts
|
|
308
|
-
var LEGACY_SCOPES = ["session", "tool", "user"];
|
|
309
|
-
var DEFAULT_LEGACY_PURGE_DELAY_MS = 864e5;
|
|
310
|
-
var LAYOUT_MARKER_KEY = "__layout__";
|
|
311
|
-
var LAYOUT_VERSION = 2;
|
|
312
|
-
var MAX_TIMER_MS = 2147483647;
|
|
313
|
-
var scheduled = /* @__PURE__ */ new WeakSet();
|
|
314
|
-
function parseFirstSeenAt(raw) {
|
|
315
|
-
if (!raw) return void 0;
|
|
316
|
-
try {
|
|
317
|
-
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
318
|
-
const firstSeenAt = parsed?.firstSeenAt;
|
|
319
|
-
return typeof firstSeenAt === "number" && Number.isFinite(firstSeenAt) ? firstSeenAt : void 0;
|
|
320
|
-
} catch {
|
|
321
|
-
return void 0;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
async function readLayoutFirstSeenAt(store, keyPrefix, logger, markerTtlSeconds) {
|
|
325
|
-
const key = `${keyPrefix}${LAYOUT_MARKER_KEY}`;
|
|
326
|
-
const standDown = (reason, extra = {}) => {
|
|
327
|
-
logger?.debug?.(`remember: ${reason}, legacy purge stood down`, { key, ...extra });
|
|
328
|
-
return void 0;
|
|
329
|
-
};
|
|
330
|
-
try {
|
|
331
|
-
const existing = await store.getValue(key);
|
|
332
|
-
if (existing) {
|
|
333
|
-
return parseFirstSeenAt(existing) ?? standDown("layout marker is unreadable");
|
|
334
|
-
}
|
|
335
|
-
const marker = { version: LAYOUT_VERSION, firstSeenAt: Date.now() };
|
|
336
|
-
const serialized = JSON.stringify(marker);
|
|
337
|
-
if (!store.setIfAbsent) {
|
|
338
|
-
await store.setValue(key, serialized, markerTtlSeconds);
|
|
339
|
-
return marker.firstSeenAt;
|
|
340
|
-
}
|
|
341
|
-
if (await store.setIfAbsent(key, serialized, markerTtlSeconds)) {
|
|
342
|
-
return marker.firstSeenAt;
|
|
343
|
-
}
|
|
344
|
-
const winner = await store.getValue(key);
|
|
345
|
-
return parseFirstSeenAt(winner) ?? standDown("layout marker vanished after a lost race");
|
|
346
|
-
} catch (error) {
|
|
347
|
-
return standDown("could not establish the layout marker", {
|
|
348
|
-
error: error instanceof Error ? error.message : String(error)
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
async function purgeLegacyRememberEntries(store, keyPrefix, logger) {
|
|
353
|
-
let deleted = 0;
|
|
354
|
-
for (const scope of LEGACY_SCOPES) {
|
|
355
|
-
const pattern = `${keyPrefix}${scope}:*`;
|
|
356
|
-
let keys;
|
|
357
|
-
try {
|
|
358
|
-
keys = await store.keys(pattern);
|
|
359
|
-
} catch (error) {
|
|
360
|
-
logger?.debug?.("remember: legacy purge skipped, store could not list keys", {
|
|
361
|
-
pattern,
|
|
362
|
-
error: error instanceof Error ? error.message : String(error)
|
|
363
|
-
});
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
for (const key of keys) {
|
|
367
|
-
try {
|
|
368
|
-
await store.delete(key);
|
|
369
|
-
deleted += 1;
|
|
370
|
-
} catch {
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (deleted > 0) {
|
|
375
|
-
logger?.warn?.(
|
|
376
|
-
`remember: purged ${deleted} entr${deleted === 1 ? "y" : "ies"} left unreadable by the session/tool key-derivation change and the namespace encoding. This is a one-time upgrade step; set skipLegacyPurge to handle the migration yourself.`
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
return deleted;
|
|
380
|
-
}
|
|
381
|
-
function scheduleLegacyRememberPurge(store, keyPrefix, options = {}) {
|
|
382
|
-
if (scheduled.has(store)) return;
|
|
383
|
-
scheduled.add(store);
|
|
384
|
-
const delayMs = options.delayMs ?? DEFAULT_LEGACY_PURGE_DELAY_MS;
|
|
385
|
-
const markerTtlSeconds = options.defaultTTL ? Math.max(options.defaultTTL, Math.ceil(2 * delayMs / 1e3)) : void 0;
|
|
386
|
-
const arm = (waitMs) => {
|
|
387
|
-
const timer = setTimeout(
|
|
388
|
-
() => {
|
|
389
|
-
void attempt().catch(() => void 0);
|
|
390
|
-
},
|
|
391
|
-
Math.min(waitMs, MAX_TIMER_MS)
|
|
392
|
-
);
|
|
393
|
-
timer.unref?.();
|
|
394
|
-
};
|
|
395
|
-
const attempt = async () => {
|
|
396
|
-
const firstSeenAt = await readLayoutFirstSeenAt(store, keyPrefix, options.logger, markerTtlSeconds);
|
|
397
|
-
if (firstSeenAt === void 0) return;
|
|
398
|
-
const remainingMs = firstSeenAt + delayMs - Date.now();
|
|
399
|
-
if (remainingMs > 0) {
|
|
400
|
-
arm(remainingMs);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
await purgeLegacyRememberEntries(store, keyPrefix, options.logger);
|
|
404
|
-
};
|
|
405
|
-
void attempt().catch(() => void 0);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// plugins/plugin-remember/src/providers/remember-accessor.provider.ts
|
|
409
|
-
var STORAGE_LAYOUT_VERSION = "v2";
|
|
410
|
-
function encodeKeyPart(value) {
|
|
411
|
-
return encodeURIComponent(value);
|
|
412
|
-
}
|
|
413
|
-
function remainingSeconds(expiresAt, now) {
|
|
414
|
-
return Math.max(1, Math.ceil((expiresAt - now) / 1e3));
|
|
415
|
-
}
|
|
416
|
-
function expiryAfter(ttlSeconds, now) {
|
|
417
|
-
return ttlSeconds ? now + ttlSeconds * 1e3 : void 0;
|
|
418
|
-
}
|
|
419
|
-
var RememberAccessor = class {
|
|
420
|
-
store;
|
|
421
|
-
ctx;
|
|
422
|
-
config;
|
|
423
|
-
keyPrefix;
|
|
424
|
-
encryptionEnabled;
|
|
425
|
-
constructor(store, ctx, config, logger) {
|
|
426
|
-
this.store = store;
|
|
427
|
-
this.ctx = ctx;
|
|
428
|
-
this.config = config;
|
|
429
|
-
this.keyPrefix = config.keyPrefix ?? "remember:";
|
|
430
|
-
this.encryptionEnabled = config.encryption?.enabled !== false;
|
|
431
|
-
if (!config.skipLegacyPurge) {
|
|
432
|
-
scheduleLegacyRememberPurge(store, this.keyPrefix, {
|
|
433
|
-
delayMs: config.legacyPurgeDelayMs,
|
|
434
|
-
defaultTTL: config.defaultTTL,
|
|
435
|
-
logger
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
440
|
-
// Public API
|
|
441
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
442
|
-
/**
|
|
443
|
-
* Store a value in memory.
|
|
444
|
-
*
|
|
445
|
-
* Without a `ttl`, the plugin's `defaultTTL` applies, and the entry records the deadline either one sets.
|
|
446
|
-
*
|
|
447
|
-
* @param key - The key to store under
|
|
448
|
-
* @param value - The value to store (any JSON-serializable data)
|
|
449
|
-
* @param options - Storage options (scope, ttl, brand, metadata)
|
|
450
|
-
* @returns The stored entry, with its `expiresAt` deadline
|
|
451
|
-
*/
|
|
452
|
-
async set(key, value, options = {}) {
|
|
453
|
-
const scope = options.scope ?? "session";
|
|
454
|
-
const storageKey = this.buildStorageKey(key, scope);
|
|
455
|
-
const now = Date.now();
|
|
456
|
-
const ttlSeconds = options.ttl ?? this.config.defaultTTL;
|
|
457
|
-
const entry = {
|
|
458
|
-
value,
|
|
459
|
-
brand: options.brand,
|
|
460
|
-
createdAt: now,
|
|
461
|
-
updatedAt: now,
|
|
462
|
-
expiresAt: expiryAfter(ttlSeconds, now),
|
|
463
|
-
metadata: options.metadata
|
|
464
|
-
};
|
|
465
|
-
const serialized = this.encryptionEnabled ? await encryptAndSerialize(entry, this.getKeySource(scope)) : JSON.stringify(entry);
|
|
466
|
-
await this.store.setValue(storageKey, serialized, ttlSeconds);
|
|
467
|
-
return entry;
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Retrieve a value from memory.
|
|
471
|
-
*
|
|
472
|
-
* @param key - The key to retrieve
|
|
473
|
-
* @param options - Retrieval options (scope, defaultValue)
|
|
474
|
-
* @returns The stored value or defaultValue if not found
|
|
475
|
-
*/
|
|
476
|
-
async get(key, options = {}) {
|
|
477
|
-
const scope = options.scope ?? "session";
|
|
478
|
-
const entry = await this.readEntry(this.buildStorageKey(key, scope), scope);
|
|
479
|
-
return entry ? entry.value : options.defaultValue;
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Get the full entry including metadata.
|
|
483
|
-
*
|
|
484
|
-
* @param key - The key to retrieve
|
|
485
|
-
* @param options - Retrieval options (scope)
|
|
486
|
-
* @returns The full entry or undefined if not found
|
|
487
|
-
*/
|
|
488
|
-
async getEntry(key, options = {}) {
|
|
489
|
-
const scope = options.scope ?? "session";
|
|
490
|
-
return this.readEntry(this.buildStorageKey(key, scope), scope);
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Forget (delete) a value from memory.
|
|
494
|
-
*
|
|
495
|
-
* @param key - The key to forget
|
|
496
|
-
* @param options - Options (scope)
|
|
497
|
-
*/
|
|
498
|
-
async forget(key, options = {}) {
|
|
499
|
-
const scope = options.scope ?? "session";
|
|
500
|
-
const storageKey = this.buildStorageKey(key, scope);
|
|
501
|
-
await this.store.delete(storageKey);
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Check if a key is remembered: true exactly when `get()` would return its value.
|
|
505
|
-
*
|
|
506
|
-
* @param key - The key to check
|
|
507
|
-
* @param options - Options (scope)
|
|
508
|
-
* @returns true if the key exists and has not expired
|
|
509
|
-
*/
|
|
510
|
-
async knows(key, options = {}) {
|
|
511
|
-
const scope = options.scope ?? "session";
|
|
512
|
-
return await this.readEntry(this.buildStorageKey(key, scope), scope) !== void 0;
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* List all remembered keys for a scope: the keys `get()` would return a value for.
|
|
516
|
-
*
|
|
517
|
-
* @param options - Options (scope, pattern)
|
|
518
|
-
* @returns Array of keys (without the scope prefix)
|
|
519
|
-
*/
|
|
520
|
-
async list(options = {}) {
|
|
521
|
-
const scope = options.scope ?? "session";
|
|
522
|
-
const scopePrefix = this.buildScopePrefix(scope);
|
|
523
|
-
const storageKeys = await this.store.keys(scopePrefix + (options.pattern ?? "*"));
|
|
524
|
-
const entries = await Promise.all(storageKeys.map((storageKey) => this.readEntry(storageKey, scope)));
|
|
525
|
-
return storageKeys.filter((_storageKey, index) => entries[index] !== void 0).map((storageKey) => storageKey.slice(scopePrefix.length));
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Update an existing entry's value while preserving metadata.
|
|
529
|
-
*
|
|
530
|
-
* With a `ttl` the entry expires that many seconds from now; without one it keeps its current
|
|
531
|
-
* expiry, in the store as well, so the store does not keep it past that (rounded up to a second).
|
|
532
|
-
* An entry with no recorded expiry gets the plugin's `defaultTTL` from now, as the store would give it.
|
|
533
|
-
*
|
|
534
|
-
* @param key - The key to update
|
|
535
|
-
* @param value - The new value
|
|
536
|
-
* @param options - Options (scope, ttl)
|
|
537
|
-
*/
|
|
538
|
-
async update(key, value, options = {}) {
|
|
539
|
-
const scope = options.scope ?? "session";
|
|
540
|
-
const existing = await this.getEntry(key, { scope });
|
|
541
|
-
if (!existing) return false;
|
|
542
|
-
const now = Date.now();
|
|
543
|
-
const expiresAt = expiryAfter(options.ttl, now) ?? existing.expiresAt ?? expiryAfter(this.config.defaultTTL, now);
|
|
544
|
-
const entry = {
|
|
545
|
-
...existing,
|
|
546
|
-
value,
|
|
547
|
-
updatedAt: now,
|
|
548
|
-
expiresAt
|
|
549
|
-
};
|
|
550
|
-
const storageKey = this.buildStorageKey(key, scope);
|
|
551
|
-
const serialized = this.encryptionEnabled ? await encryptAndSerialize(entry, this.getKeySource(scope)) : JSON.stringify(entry);
|
|
552
|
-
const writtenAt = Date.now();
|
|
553
|
-
if (expiresAt !== void 0 && expiresAt <= writtenAt) return false;
|
|
554
|
-
const storageTtl = expiresAt !== void 0 ? remainingSeconds(expiresAt, writtenAt) : void 0;
|
|
555
|
-
await this.store.setValue(storageKey, serialized, storageTtl);
|
|
556
|
-
return true;
|
|
557
|
-
}
|
|
558
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
559
|
-
// Convenience Methods
|
|
560
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
561
|
-
/**
|
|
562
|
-
* Get the session ID from context.
|
|
563
|
-
*
|
|
564
|
-
* This is the id the request carries, which under MCP 2026-07-28 the client chooses; it is not
|
|
565
|
-
* proof of identity. Session and tool memory are namespaced by the verified session instead.
|
|
566
|
-
*/
|
|
567
|
-
get sessionId() {
|
|
568
|
-
return this.ctx.sessionId;
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Get the user ID from context (if available).
|
|
572
|
-
*/
|
|
573
|
-
get userId() {
|
|
574
|
-
const authInfo = this.ctx.authInfo;
|
|
575
|
-
return authInfo?.extra?.["userId"] ?? authInfo?.extra?.["sub"] ?? authInfo?.clientId ?? void 0;
|
|
576
|
-
}
|
|
577
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
578
|
-
// Private Methods
|
|
579
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
580
|
-
/**
|
|
581
|
-
* Build the full storage key including scope prefix.
|
|
582
|
-
*/
|
|
583
|
-
buildStorageKey(key, scope) {
|
|
584
|
-
return this.buildScopePrefix(scope) + key;
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* The session id the server verified for this request, or undefined when there is none.
|
|
588
|
-
*
|
|
589
|
-
* Read from the verified auth info (`authInfo.sessionId` from the transport, `extra.sessionId`
|
|
590
|
-
* from session verification, which records only a session the request presented), the same rule
|
|
591
|
-
* CONTEXT providers are cached by. Never `FrontMcpContext.sessionId` alone: under MCP 2026-07-28
|
|
592
|
-
* that is whatever `mcp-session-id` the caller sent, including another caller's. The stateless
|
|
593
|
-
* transport's shared `__stateless__` id identifies no one.
|
|
594
|
-
*/
|
|
595
|
-
get verifiedSessionId() {
|
|
596
|
-
const authInfo = this.ctx.authInfo;
|
|
597
|
-
return [authInfo?.sessionId, authInfo?.extra?.["sessionId"]].find(
|
|
598
|
-
(candidate) => typeof candidate === "string" && candidate.length > 0 && candidate !== STATELESS_SESSION_ID
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* The identity that namespaces session- and tool-scoped storage.
|
|
603
|
-
*
|
|
604
|
-
* Only a session the server verified identifies one client across requests. Keying on the
|
|
605
|
-
* session id a request merely carries let a caller read or overwrite another caller's memory by
|
|
606
|
-
* sending that caller's `mcp-session-id` (MCP 2026-07-28), and put every stateless client under
|
|
607
|
-
* the shared `__stateless__` id (GHSA-225p-f8jh-f3rh). A request without a verified session
|
|
608
|
-
* (stateless transport, MCP 2026-07-28, or a session id the server did not verify) falls back to
|
|
609
|
-
* the authenticated principal, the only per-client identity it has; with neither, it has no
|
|
610
|
-
* business reading or writing per-client memory at all. An anonymous subject (`anon:…`) is no
|
|
611
|
-
* principal: without a session, the server makes a new one up for each request.
|
|
612
|
-
*/
|
|
613
|
-
resolveSessionIdentity() {
|
|
614
|
-
const sessionId = this.verifiedSessionId;
|
|
615
|
-
if (sessionId) {
|
|
616
|
-
return sessionId;
|
|
617
|
-
}
|
|
618
|
-
const userId = this.userId;
|
|
619
|
-
if (userId && !isAnonymousSubject(userId)) {
|
|
620
|
-
return `stateless-user:${userId}`;
|
|
621
|
-
}
|
|
622
|
-
throw new RememberIdentityError(
|
|
623
|
-
`Remember cannot use session or tool scope for an unauthenticated request without a verified session: a stateless request shares the session id "${STATELESS_SESSION_ID}", and an mcp-session-id the server did not verify could belong to any caller, so the data would be shared across clients. Authenticate the request, use a stateful transport, or choose the 'global' scope if the data really is shared.`
|
|
624
|
-
);
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* The tool that tool-scoped memory belongs to: the tool whose `execute()` is running (its
|
|
628
|
-
* owner-qualified name). Read when the memory is used, not when this accessor was built (it can
|
|
629
|
-
* serve several calls), so a tool that calls other tools still gets its own. Outside a tool,
|
|
630
|
-
* `'unknown'`.
|
|
631
|
-
*/
|
|
632
|
-
get toolName() {
|
|
633
|
-
return getRunningTool()?.fullName ?? "unknown";
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Build the scope-specific prefix.
|
|
637
|
-
*
|
|
638
|
-
* Every variable component is encoded, because the separator is also a legal character in
|
|
639
|
-
* a user id, a tool name and a memory key. Left raw, identity `a` with key `b:c` produces
|
|
640
|
-
* the same storage key as identity `a:b` with key `c` — one client reading or overwriting
|
|
641
|
-
* another's value through nothing more exotic than a colon in a name.
|
|
642
|
-
*/
|
|
643
|
-
buildScopePrefix(scope) {
|
|
644
|
-
switch (scope) {
|
|
645
|
-
case "session":
|
|
646
|
-
return `${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:session:${encodeKeyPart(this.resolveSessionIdentity())}:`;
|
|
647
|
-
case "user": {
|
|
648
|
-
const userId = this.userId;
|
|
649
|
-
if (!userId || isAnonymousSubject(userId)) {
|
|
650
|
-
throw new RememberIdentityError(
|
|
651
|
-
"Remember cannot use user scope without an authenticated user: all unauthenticated callers would share one namespace. Authenticate the request or use the 'global' scope."
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
return `${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:user:${encodeKeyPart(userId)}:`;
|
|
655
|
-
}
|
|
656
|
-
case "tool":
|
|
657
|
-
return `${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:tool:${encodeKeyPart(this.toolName)}:${encodeKeyPart(this.resolveSessionIdentity())}:`;
|
|
658
|
-
case "global":
|
|
659
|
-
return `${this.keyPrefix}global:`;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Get the encryption key source for a scope.
|
|
664
|
-
*/
|
|
665
|
-
getKeySource(scope) {
|
|
666
|
-
const needsSessionIdentity = scope === "session" || scope === "tool";
|
|
667
|
-
return getKeySourceForScope(
|
|
668
|
-
scope,
|
|
669
|
-
{
|
|
670
|
-
sessionId: needsSessionIdentity ? this.resolveSessionIdentity() : this.ctx.sessionId,
|
|
671
|
-
userId: this.userId,
|
|
672
|
-
toolName: scope === "tool" ? this.toolName : void 0
|
|
673
|
-
},
|
|
674
|
-
this.config.encryption?.customKey
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
/** The entry stored under `storageKey`, if readable and unexpired; an expired one is deleted. */
|
|
678
|
-
async readEntry(storageKey, scope) {
|
|
679
|
-
const stored = await this.store.getValue(storageKey);
|
|
680
|
-
if (!stored) return void 0;
|
|
681
|
-
const raw = typeof stored === "string" ? stored : JSON.stringify(stored);
|
|
682
|
-
const entry = this.encryptionEnabled ? await deserializeAndDecrypt(raw, this.getKeySource(scope)) : this.parseEntry(raw);
|
|
683
|
-
if (!entry) return void 0;
|
|
684
|
-
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
685
|
-
await this.store.delete(storageKey);
|
|
686
|
-
return void 0;
|
|
687
|
-
}
|
|
688
|
-
return entry;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Parse a JSON entry (when encryption is disabled).
|
|
692
|
-
*/
|
|
693
|
-
parseEntry(raw) {
|
|
694
|
-
try {
|
|
695
|
-
return JSON.parse(raw);
|
|
696
|
-
} catch {
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
RememberAccessor = __decorateClass([
|
|
702
|
-
Provider({
|
|
703
|
-
name: "provider:remember:accessor",
|
|
704
|
-
description: "Context-scoped accessor for RememberPlugin storage",
|
|
705
|
-
scope: ProviderScope.CONTEXT
|
|
706
|
-
})
|
|
707
|
-
], RememberAccessor);
|
|
708
|
-
function createRememberAccessor(store, ctx, config, logger) {
|
|
709
|
-
return new RememberAccessor(store, ctx, config, logger);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// plugins/plugin-remember/src/providers/remember-memory.provider.ts
|
|
713
|
-
import { Provider as Provider2, ProviderScope as ProviderScope2 } from "@frontmcp/sdk";
|
|
714
|
-
var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
715
|
-
var RememberMemoryProvider = class {
|
|
716
|
-
constructor(sweepIntervalSeconds = 60, defaultTTL) {
|
|
717
|
-
this.defaultTTL = defaultTTL;
|
|
718
|
-
this.sweeper = setInterval(() => this.sweep(), sweepIntervalSeconds * 1e3);
|
|
719
|
-
this.sweeper.unref?.();
|
|
720
|
-
}
|
|
721
|
-
defaultTTL;
|
|
722
|
-
memory = /* @__PURE__ */ new Map();
|
|
723
|
-
sweeper;
|
|
724
|
-
/**
|
|
725
|
-
* Store a value with optional TTL.
|
|
726
|
-
* Always JSON-serializes the value for consistent storage/retrieval.
|
|
727
|
-
*/
|
|
728
|
-
async setValue(key, value, ttlSeconds) {
|
|
729
|
-
const strValue = JSON.stringify(value);
|
|
730
|
-
const existing = this.memory.get(key);
|
|
731
|
-
if (existing?.timeout) clearTimeout(existing.timeout);
|
|
732
|
-
const entry = { value: strValue };
|
|
733
|
-
const ttl = ttlSeconds ?? this.defaultTTL;
|
|
734
|
-
if (ttl && ttl > 0) {
|
|
735
|
-
const ttlMs = ttl * 1e3;
|
|
736
|
-
entry.expiresAt = Date.now() + ttlMs;
|
|
737
|
-
if (ttlMs <= MAX_TIMEOUT_MS) {
|
|
738
|
-
entry.timeout = setTimeout(() => {
|
|
739
|
-
const e = this.memory.get(key);
|
|
740
|
-
if (e && e.expiresAt && e.expiresAt <= Date.now()) {
|
|
741
|
-
this.memory.delete(key);
|
|
742
|
-
}
|
|
743
|
-
}, ttlMs);
|
|
744
|
-
entry.timeout.unref?.();
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
this.memory.set(key, entry);
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Store a value only if the key is absent.
|
|
751
|
-
*
|
|
752
|
-
* A single-threaded runtime makes the check-then-set indivisible; an expired entry counts as
|
|
753
|
-
* absent, matching what `getValue` would report.
|
|
754
|
-
*/
|
|
755
|
-
async setIfAbsent(key, value, ttlSeconds) {
|
|
756
|
-
const existing = this.memory.get(key);
|
|
757
|
-
if (existing && !this.isExpired(existing)) return false;
|
|
758
|
-
await this.setValue(key, value, ttlSeconds);
|
|
759
|
-
return true;
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Retrieve a value by key.
|
|
763
|
-
* JSON-parses the stored value to match the setValue serialization.
|
|
764
|
-
*/
|
|
765
|
-
async getValue(key, defaultValue) {
|
|
766
|
-
const entry = this.memory.get(key);
|
|
767
|
-
if (!entry) return defaultValue;
|
|
768
|
-
if (this.isExpired(entry)) {
|
|
769
|
-
await this.delete(key);
|
|
770
|
-
return defaultValue;
|
|
771
|
-
}
|
|
772
|
-
try {
|
|
773
|
-
return JSON.parse(entry.value);
|
|
774
|
-
} catch {
|
|
775
|
-
return entry.value;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
/**
|
|
779
|
-
* Delete a key.
|
|
780
|
-
*/
|
|
781
|
-
async delete(key) {
|
|
782
|
-
const entry = this.memory.get(key);
|
|
783
|
-
if (entry?.timeout) clearTimeout(entry.timeout);
|
|
784
|
-
this.memory.delete(key);
|
|
785
|
-
}
|
|
786
|
-
/**
|
|
787
|
-
* Check if a key exists (and is not expired).
|
|
788
|
-
*/
|
|
789
|
-
async exists(key) {
|
|
790
|
-
const entry = this.memory.get(key);
|
|
791
|
-
if (!entry) return false;
|
|
792
|
-
if (this.isExpired(entry)) {
|
|
793
|
-
await this.delete(key);
|
|
794
|
-
return false;
|
|
795
|
-
}
|
|
796
|
-
return true;
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* List keys matching a glob-style pattern.
|
|
800
|
-
* Supports * and ? wildcards.
|
|
801
|
-
*/
|
|
802
|
-
async keys(pattern) {
|
|
803
|
-
const result = [];
|
|
804
|
-
const regex = pattern ? this.patternToRegex(pattern) : null;
|
|
805
|
-
const expiredKeys = [];
|
|
806
|
-
for (const [key, entry] of this.memory) {
|
|
807
|
-
if (this.isExpired(entry)) {
|
|
808
|
-
expiredKeys.push(key);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
for (const key of expiredKeys) {
|
|
812
|
-
await this.delete(key);
|
|
813
|
-
}
|
|
814
|
-
for (const [key, entry] of this.memory) {
|
|
815
|
-
if (this.isExpired(entry)) {
|
|
816
|
-
continue;
|
|
817
|
-
}
|
|
818
|
-
if (regex && !regex.test(key)) {
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
result.push(key);
|
|
822
|
-
}
|
|
823
|
-
return result;
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Gracefully close the provider.
|
|
827
|
-
*/
|
|
828
|
-
async close() {
|
|
829
|
-
if (this.sweeper) clearInterval(this.sweeper);
|
|
830
|
-
for (const [, entry] of this.memory) {
|
|
831
|
-
if (entry.timeout) clearTimeout(entry.timeout);
|
|
832
|
-
}
|
|
833
|
-
this.memory.clear();
|
|
834
|
-
}
|
|
835
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
836
|
-
// Private Methods
|
|
837
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
838
|
-
isExpired(entry) {
|
|
839
|
-
return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Periodically remove expired keys to keep memory tidy.
|
|
843
|
-
*/
|
|
844
|
-
sweep() {
|
|
845
|
-
const now = Date.now();
|
|
846
|
-
for (const [key, entry] of this.memory) {
|
|
847
|
-
if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
|
|
848
|
-
if (entry.timeout) clearTimeout(entry.timeout);
|
|
849
|
-
this.memory.delete(key);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Convert a glob-style pattern to a RegExp.
|
|
855
|
-
* * matches any sequence of characters
|
|
856
|
-
* ? matches any single character
|
|
857
|
-
*
|
|
858
|
-
* Includes ReDoS protection: validates pattern length and wildcard count.
|
|
859
|
-
*/
|
|
860
|
-
patternToRegex(pattern) {
|
|
861
|
-
if (pattern.length > RememberMemoryProvider.MAX_PATTERN_LENGTH) {
|
|
862
|
-
throw new Error(`Pattern too long: ${pattern.length} chars (max ${RememberMemoryProvider.MAX_PATTERN_LENGTH})`);
|
|
863
|
-
}
|
|
864
|
-
const wildcardCount = (pattern.match(/[*?]/g) || []).length;
|
|
865
|
-
if (wildcardCount > RememberMemoryProvider.MAX_WILDCARDS) {
|
|
866
|
-
throw new Error(`Too many wildcards in pattern: ${wildcardCount} (max ${RememberMemoryProvider.MAX_WILDCARDS})`);
|
|
867
|
-
}
|
|
868
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
869
|
-
return new RegExp(`^${escaped}$`);
|
|
870
|
-
}
|
|
871
|
-
};
|
|
872
|
-
// ReDoS protection constants
|
|
873
|
-
// Pattern length allows for long prefixes (e.g., remember:session:{uuid}:user:*)
|
|
874
|
-
__publicField(RememberMemoryProvider, "MAX_PATTERN_LENGTH", 500);
|
|
875
|
-
__publicField(RememberMemoryProvider, "MAX_WILDCARDS", 20);
|
|
876
|
-
RememberMemoryProvider = __decorateClass([
|
|
877
|
-
Provider2({
|
|
878
|
-
name: "provider:remember:memory",
|
|
879
|
-
description: "In-memory storage provider for RememberPlugin",
|
|
880
|
-
scope: ProviderScope2.GLOBAL
|
|
881
|
-
})
|
|
882
|
-
], RememberMemoryProvider);
|
|
883
|
-
|
|
884
|
-
// plugins/plugin-remember/src/providers/remember-redis.provider.ts
|
|
885
|
-
import Redis from "ioredis";
|
|
886
|
-
import { Provider as Provider3, ProviderScope as ProviderScope3 } from "@frontmcp/sdk";
|
|
887
|
-
|
|
888
|
-
// plugins/plugin-remember/src/providers/remember-key-prefix.ts
|
|
889
|
-
function prefixedStoreKey(keyPrefix, key) {
|
|
890
|
-
return keyPrefix && key.startsWith(keyPrefix) ? key : keyPrefix + key;
|
|
891
|
-
}
|
|
892
|
-
function doubledPrefixKey(keyPrefix, key) {
|
|
893
|
-
return keyPrefix && key.startsWith(keyPrefix) ? keyPrefix + key : void 0;
|
|
894
|
-
}
|
|
895
|
-
function callerKeyOf(keyPrefix, pattern, backendKey) {
|
|
896
|
-
return keyPrefix && pattern.startsWith(keyPrefix) ? backendKey : backendKey.slice(keyPrefix.length);
|
|
897
|
-
}
|
|
898
|
-
var SET_IF_NEITHER_KEY_EXISTS_SCRIPT = `
|
|
1
|
+
var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __defNormalProp=(obj,key,value)=>key in obj?__defProp(obj,key,{enumerable:true,configurable:true,writable:true,value}):obj[key]=value;var __decorateClass=(decorators,target,key,kind)=>{var result=kind>1?void 0:kind?__getOwnPropDesc(target,key):target;for(var i=decorators.length-1,decorator;i>=0;i--)if(decorator=decorators[i])result=(kind?decorator(target,key,result):decorator(result))||result;if(kind&&result)__defProp(target,key,result);return result};var __publicField=(obj,key,value)=>__defNormalProp(obj,typeof key!=="symbol"?key+"":key,value);import{DynamicPlugin,FRONTMCP_CONTEXT,FrontMcpConfig,FrontMcpLogger,getGlobalStoreConfig,isVercelKvProvider,Plugin,ProviderScope as ProviderScope5}from"@frontmcp/sdk";import{getRunningTool,isAnonymousSubject,Provider,ProviderScope,STATELESS_SESSION_ID}from"@frontmcp/sdk";import{base64urlDecode,base64urlEncode as base64urlEncode2,decryptAesGcm,encryptAesGcm,hkdfSha256,randomBytes as randomBytes2}from"@frontmcp/utils";import*as path from"path";import{z}from"@frontmcp/lazy-zod";import{base64urlEncode,mkdir,randomBytes,readFile,rename,unlink,writeFile}from"@frontmcp/utils";var DEFAULT_SECRET_FILE_PATH=".frontmcp/remember-secret.json";var SECRET_BYTES=32;var rememberSecretDataSchema=z.object({secret:z.string().min(32),createdAt:z.number().positive().int(),version:z.literal(1)});function validateSecretData(data){const result=rememberSecretDataSchema.safeParse(data);if(!result.success){return{valid:false,error:result.error.issues[0]?.message??"Invalid secret structure"}}const parsed=result.data;const now=Date.now();if(parsed.createdAt>now+6e4){return{valid:false,error:"createdAt is in the future"}}const hundredYearsMs=100*365*24*60*60*1e3;if(parsed.createdAt<now-hundredYearsMs){return{valid:false,error:"createdAt is too old"}}return{valid:true}}function isSecretPersistenceEnabled(options){const isProduction=process.env["NODE_ENV"]==="production";if(isProduction){return options?.forceEnable===true}return true}function resolveSecretPath(options){const secretPath=options?.secretPath??DEFAULT_SECRET_FILE_PATH;if(path.isAbsolute(secretPath)){return secretPath}return path.resolve(process.cwd(),secretPath)}async function loadRememberSecret(options){if(!isSecretPersistenceEnabled(options)){return null}const secretPath=resolveSecretPath(options);try{const content=await readFile(secretPath,"utf8");const data=JSON.parse(content);const validation=validateSecretData(data);if(!validation.valid){console.warn(`[RememberSecretPersistence] Invalid secret file format at ${secretPath}: ${validation.error}, will regenerate`);return null}return data}catch(error){if(error.code==="ENOENT"){return null}console.warn(`[RememberSecretPersistence] Failed to load secret from ${secretPath}: ${error.message}`);return null}}async function saveRememberSecret(secretData,options){if(!isSecretPersistenceEnabled(options)){return true}const secretPath=resolveSecretPath(options);const dir=path.dirname(secretPath);const tempPath=`${secretPath}.tmp.${Date.now()}.${base64urlEncode(randomBytes(8))}`;try{await mkdir(dir,{recursive:true,mode:448});const content=JSON.stringify(secretData,null,2);await writeFile(tempPath,content,{mode:384});await rename(tempPath,secretPath);return true}catch(error){console.error(`[RememberSecretPersistence] Failed to save secret to ${secretPath}: ${error.message}`);try{await unlink(tempPath)}catch{}return false}}var cachedSecret=null;var secretGenerationPromise=null;function generateSecret(){return base64urlEncode(randomBytes(SECRET_BYTES))}async function getOrCreatePersistedSecret(options){if(cachedSecret){return cachedSecret}if(secretGenerationPromise){return secretGenerationPromise}secretGenerationPromise=(async()=>{try{const loaded=await loadRememberSecret(options);if(loaded){cachedSecret=loaded.secret;return cachedSecret}const newSecret=generateSecret();const secretData={secret:newSecret,createdAt:Date.now(),version:1};if(isSecretPersistenceEnabled(options)){await saveRememberSecret(secretData,options)}else{console.warn("[RememberSecretPersistence] No REMEMBER_SECRET (or MCP_MEMORY_SECRET / MCP_SESSION_SECRET) is set in production, so a random in-memory secret is used. Encrypted memory will be unreadable after a restart and by other instances sharing the store. Set REMEMBER_SECRET to the same value on every instance.")}cachedSecret=newSecret;return cachedSecret}finally{secretGenerationPromise=null}})();return secretGenerationPromise}async function getBaseSecret(){const envSecret=process.env["REMEMBER_SECRET"]||process.env["MCP_MEMORY_SECRET"]||process.env["MCP_SESSION_SECRET"];if(envSecret){return envSecret}return getOrCreatePersistedSecret()}async function secretFor(source){return source.secret?source.secret:getBaseSecret()}var textEncoder=new TextEncoder;async function deriveEncryptionKey(source){const salt=textEncoder.encode("remember-plugin-v1");let ikm;let context;switch(source.type){case"session":ikm=await secretFor(source)+source.sessionId;context=`remember:session:${source.sessionId}`;break;case"tool":ikm=await secretFor(source)+source.sessionId;context=`remember:tool:${source.toolName}:${source.sessionId}`;break;case"user":ikm=await secretFor(source)+source.userId;context=`remember:user:${source.userId}`;break;case"global":ikm=await secretFor(source);context="remember:global";break;case"custom":ikm=source.key;context=`remember:custom:${source.key}`;break}const ikmBytes=textEncoder.encode(ikm);const info=textEncoder.encode(context);return hkdfSha256(ikmBytes,salt,info,32)}function getKeySourceForScope(scope,context,secret){switch(scope){case"session":return{type:"session",sessionId:context.sessionId,secret};case"user":return{type:"user",userId:context.userId??"anonymous",secret};case"tool":return{type:"tool",toolName:context.toolName??"unknown",sessionId:context.sessionId,secret};case"global":return{type:"global",secret}}}var textDecoder=new TextDecoder;async function encryptValue(data,keySource){const key=await deriveEncryptionKey(keySource);const plaintext=textEncoder.encode(JSON.stringify(data));const iv=randomBytes2(12);const{ciphertext,tag}=encryptAesGcm(key,plaintext,iv);return{alg:"A256GCM",iv:base64urlEncode2(iv),tag:base64urlEncode2(tag),data:base64urlEncode2(ciphertext)}}async function decryptValue(blob,keySource){try{const key=await deriveEncryptionKey(keySource);const iv=base64urlDecode(blob.iv);const tag=base64urlDecode(blob.tag);const ciphertext=base64urlDecode(blob.data);const decrypted=decryptAesGcm(key,ciphertext,iv,tag);return JSON.parse(textDecoder.decode(decrypted))}catch{return null}}function serializeBlob(blob){return JSON.stringify(blob)}function deserializeBlob(str){try{const parsed=JSON.parse(str);if(parsed&&typeof parsed==="object"&&parsed.alg==="A256GCM"&&typeof parsed.iv==="string"&&typeof parsed.tag==="string"&&typeof parsed.data==="string"){return parsed}return null}catch{return null}}async function encryptAndSerialize(data,keySource){const blob=await encryptValue(data,keySource);return serializeBlob(blob)}async function deserializeAndDecrypt(str,keySource){const blob=deserializeBlob(str);if(!blob)return null;return decryptValue(blob,keySource)}import{InternalMcpError,PublicMcpError}from"@frontmcp/sdk";var RememberIdentityError=class extends PublicMcpError{name="RememberIdentityError";constructor(message){super(message,"REMEMBER_IDENTITY_REQUIRED",403)}};var RememberScopeNotAllowedError=class extends PublicMcpError{name="RememberScopeNotAllowedError";constructor(scope,allowedScopes){super(`Scope '${scope}' is not allowed. Allowed scopes: ${allowedScopes.join(", ")}`,"REMEMBER_SCOPE_NOT_ALLOWED",400)}};var RememberConfigurationError=class extends InternalMcpError{name="RememberConfigurationError";constructor(message){super(message,"REMEMBER_CONFIGURATION")}};var LEGACY_SCOPES=["session","tool","user"];var DEFAULT_LEGACY_PURGE_DELAY_MS=864e5;var LAYOUT_MARKER_KEY="__layout__";var LAYOUT_VERSION=2;var MAX_TIMER_MS=2147483647;var scheduled=new WeakSet;function parseFirstSeenAt(raw){if(!raw)return void 0;try{const parsed=typeof raw==="string"?JSON.parse(raw):raw;const firstSeenAt=parsed?.firstSeenAt;return typeof firstSeenAt==="number"&&Number.isFinite(firstSeenAt)?firstSeenAt:void 0}catch{return void 0}}async function readLayoutFirstSeenAt(store,keyPrefix,logger,markerTtlSeconds){const key=`${keyPrefix}${LAYOUT_MARKER_KEY}`;const standDown=(reason,extra={})=>{logger?.debug?.(`remember: ${reason}, legacy purge stood down`,{key,...extra});return void 0};try{const existing=await store.getValue(key);if(existing){return parseFirstSeenAt(existing)??standDown("layout marker is unreadable")}const marker={version:LAYOUT_VERSION,firstSeenAt:Date.now()};const serialized=JSON.stringify(marker);if(!store.setIfAbsent){await store.setValue(key,serialized,markerTtlSeconds);return marker.firstSeenAt}if(await store.setIfAbsent(key,serialized,markerTtlSeconds)){return marker.firstSeenAt}const winner=await store.getValue(key);return parseFirstSeenAt(winner)??standDown("layout marker vanished after a lost race")}catch(error){return standDown("could not establish the layout marker",{error:error instanceof Error?error.message:String(error)})}}async function purgeLegacyRememberEntries(store,keyPrefix,logger){let deleted=0;for(const scope of LEGACY_SCOPES){const pattern=`${keyPrefix}${scope}:*`;let keys;try{keys=await store.keys(pattern)}catch(error){logger?.debug?.("remember: legacy purge skipped, store could not list keys",{pattern,error:error instanceof Error?error.message:String(error)});continue}for(const key of keys){try{await store.delete(key);deleted+=1}catch{}}}if(deleted>0){logger?.warn?.(`remember: purged ${deleted} entr${deleted===1?"y":"ies"} left unreadable by the session/tool key-derivation change and the namespace encoding. This is a one-time upgrade step; set skipLegacyPurge to handle the migration yourself.`)}return deleted}function scheduleLegacyRememberPurge(store,keyPrefix,options={}){if(scheduled.has(store))return;scheduled.add(store);const delayMs=options.delayMs??DEFAULT_LEGACY_PURGE_DELAY_MS;const markerTtlSeconds=options.defaultTTL?Math.max(options.defaultTTL,Math.ceil(2*delayMs/1e3)):void 0;const arm=waitMs=>{const timer=setTimeout(()=>{void attempt().catch(()=>void 0)},Math.min(waitMs,MAX_TIMER_MS));timer.unref?.()};const attempt=async()=>{const firstSeenAt=await readLayoutFirstSeenAt(store,keyPrefix,options.logger,markerTtlSeconds);if(firstSeenAt===void 0)return;const remainingMs=firstSeenAt+delayMs-Date.now();if(remainingMs>0){arm(remainingMs);return}await purgeLegacyRememberEntries(store,keyPrefix,options.logger)};void attempt().catch(()=>void 0)}var STORAGE_LAYOUT_VERSION="v2";function encodeKeyPart(value){return encodeURIComponent(value)}function remainingSeconds(expiresAt,now){return Math.max(1,Math.ceil((expiresAt-now)/1e3))}function expiryAfter(ttlSeconds,now){return ttlSeconds?now+ttlSeconds*1e3:void 0}var RememberAccessor=class{store;ctx;config;keyPrefix;encryptionEnabled;constructor(store,ctx,config,logger){this.store=store;this.ctx=ctx;this.config=config;this.keyPrefix=config.keyPrefix??"remember:";this.encryptionEnabled=config.encryption?.enabled!==false;if(!config.skipLegacyPurge){scheduleLegacyRememberPurge(store,this.keyPrefix,{delayMs:config.legacyPurgeDelayMs,defaultTTL:config.defaultTTL,logger})}}async set(key,value,options={}){const scope=options.scope??"session";const storageKey=this.buildStorageKey(key,scope);const now=Date.now();const ttlSeconds=options.ttl??this.config.defaultTTL;const entry={value,brand:options.brand,createdAt:now,updatedAt:now,expiresAt:expiryAfter(ttlSeconds,now),metadata:options.metadata};const serialized=this.encryptionEnabled?await encryptAndSerialize(entry,this.getKeySource(scope)):JSON.stringify(entry);await this.store.setValue(storageKey,serialized,ttlSeconds);return entry}async get(key,options={}){const scope=options.scope??"session";const entry=await this.readEntry(this.buildStorageKey(key,scope),scope);return entry?entry.value:options.defaultValue}async getEntry(key,options={}){const scope=options.scope??"session";return this.readEntry(this.buildStorageKey(key,scope),scope)}async forget(key,options={}){const scope=options.scope??"session";const storageKey=this.buildStorageKey(key,scope);await this.store.delete(storageKey)}async knows(key,options={}){const scope=options.scope??"session";return await this.readEntry(this.buildStorageKey(key,scope),scope)!==void 0}async list(options={}){const scope=options.scope??"session";const scopePrefix=this.buildScopePrefix(scope);const storageKeys=await this.store.keys(scopePrefix+(options.pattern??"*"));const entries=await Promise.all(storageKeys.map(storageKey=>this.readEntry(storageKey,scope)));return storageKeys.filter((_storageKey,index)=>entries[index]!==void 0).map(storageKey=>storageKey.slice(scopePrefix.length))}async update(key,value,options={}){const scope=options.scope??"session";const existing=await this.getEntry(key,{scope});if(!existing)return false;const now=Date.now();const expiresAt=expiryAfter(options.ttl,now)??existing.expiresAt??expiryAfter(this.config.defaultTTL,now);const entry={...existing,value,updatedAt:now,expiresAt};const storageKey=this.buildStorageKey(key,scope);const serialized=this.encryptionEnabled?await encryptAndSerialize(entry,this.getKeySource(scope)):JSON.stringify(entry);const writtenAt=Date.now();if(expiresAt!==void 0&&expiresAt<=writtenAt)return false;const storageTtl=expiresAt!==void 0?remainingSeconds(expiresAt,writtenAt):void 0;await this.store.setValue(storageKey,serialized,storageTtl);return true}get sessionId(){return this.ctx.sessionId}get userId(){const authInfo=this.ctx.authInfo;return authInfo?.extra?.["userId"]??authInfo?.extra?.["sub"]??authInfo?.clientId??void 0}buildStorageKey(key,scope){return this.buildScopePrefix(scope)+key}get verifiedSessionId(){const authInfo=this.ctx.authInfo;return[authInfo?.sessionId,authInfo?.extra?.["sessionId"]].find(candidate=>typeof candidate==="string"&&candidate.length>0&&candidate!==STATELESS_SESSION_ID)}resolveSessionIdentity(){const sessionId=this.verifiedSessionId;if(sessionId){return sessionId}const userId=this.userId;if(userId&&!isAnonymousSubject(userId)){return`stateless-user:${userId}`}throw new RememberIdentityError(`Remember cannot use session or tool scope for an unauthenticated request without a verified session: a stateless request shares the session id "${STATELESS_SESSION_ID}", and an mcp-session-id the server did not verify could belong to any caller, so the data would be shared across clients. Authenticate the request, use a stateful transport, or choose the 'global' scope if the data really is shared.`)}get toolName(){return getRunningTool()?.fullName??"unknown"}buildScopePrefix(scope){switch(scope){case"session":return`${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:session:${encodeKeyPart(this.resolveSessionIdentity())}:`;case"user":{const userId=this.userId;if(!userId||isAnonymousSubject(userId)){throw new RememberIdentityError("Remember cannot use user scope without an authenticated user: all unauthenticated callers would share one namespace. Authenticate the request or use the 'global' scope.")}return`${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:user:${encodeKeyPart(userId)}:`}case"tool":return`${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:tool:${encodeKeyPart(this.toolName)}:${encodeKeyPart(this.resolveSessionIdentity())}:`;case"global":return`${this.keyPrefix}global:`}}getKeySource(scope){const needsSessionIdentity=scope==="session"||scope==="tool";return getKeySourceForScope(scope,{sessionId:needsSessionIdentity?this.resolveSessionIdentity():this.ctx.sessionId,userId:this.userId,toolName:scope==="tool"?this.toolName:void 0},this.config.encryption?.customKey)}async readEntry(storageKey,scope){const stored=await this.store.getValue(storageKey);if(!stored)return void 0;const raw=typeof stored==="string"?stored:JSON.stringify(stored);const entry=this.encryptionEnabled?await deserializeAndDecrypt(raw,this.getKeySource(scope)):this.parseEntry(raw);if(!entry)return void 0;if(entry.expiresAt&&Date.now()>entry.expiresAt){await this.store.delete(storageKey);return void 0}return entry}parseEntry(raw){try{return JSON.parse(raw)}catch{return null}}};RememberAccessor=__decorateClass([Provider({name:"provider:remember:accessor",description:"Context-scoped accessor for RememberPlugin storage",scope:ProviderScope.CONTEXT})],RememberAccessor);function createRememberAccessor(store,ctx,config,logger){return new RememberAccessor(store,ctx,config,logger)}import{Provider as Provider2,ProviderScope as ProviderScope2}from"@frontmcp/sdk";var MAX_TIMEOUT_MS=2**31-1;var RememberMemoryProvider=class{constructor(sweepIntervalSeconds=60,defaultTTL){this.defaultTTL=defaultTTL;this.sweeper=setInterval(()=>this.sweep(),sweepIntervalSeconds*1e3);this.sweeper.unref?.()}defaultTTL;memory=new Map;sweeper;async setValue(key,value,ttlSeconds){const strValue=JSON.stringify(value);const existing=this.memory.get(key);if(existing?.timeout)clearTimeout(existing.timeout);const entry={value:strValue};const ttl=ttlSeconds??this.defaultTTL;if(ttl&&ttl>0){const ttlMs=ttl*1e3;entry.expiresAt=Date.now()+ttlMs;if(ttlMs<=MAX_TIMEOUT_MS){entry.timeout=setTimeout(()=>{const e=this.memory.get(key);if(e&&e.expiresAt&&e.expiresAt<=Date.now()){this.memory.delete(key)}},ttlMs);entry.timeout.unref?.()}}this.memory.set(key,entry)}async setIfAbsent(key,value,ttlSeconds){const existing=this.memory.get(key);if(existing&&!this.isExpired(existing))return false;await this.setValue(key,value,ttlSeconds);return true}async getValue(key,defaultValue){const entry=this.memory.get(key);if(!entry)return defaultValue;if(this.isExpired(entry)){await this.delete(key);return defaultValue}try{return JSON.parse(entry.value)}catch{return entry.value}}async delete(key){const entry=this.memory.get(key);if(entry?.timeout)clearTimeout(entry.timeout);this.memory.delete(key)}async exists(key){const entry=this.memory.get(key);if(!entry)return false;if(this.isExpired(entry)){await this.delete(key);return false}return true}async keys(pattern){const result=[];const regex=pattern?this.patternToRegex(pattern):null;const expiredKeys=[];for(const[key,entry]of this.memory){if(this.isExpired(entry)){expiredKeys.push(key)}}for(const key of expiredKeys){await this.delete(key)}for(const[key,entry]of this.memory){if(this.isExpired(entry)){continue}if(regex&&!regex.test(key)){continue}result.push(key)}return result}async close(){if(this.sweeper)clearInterval(this.sweeper);for(const[,entry]of this.memory){if(entry.timeout)clearTimeout(entry.timeout)}this.memory.clear()}isExpired(entry){return entry.expiresAt!==void 0&&entry.expiresAt<=Date.now()}sweep(){const now=Date.now();for(const[key,entry]of this.memory){if(entry.expiresAt!==void 0&&entry.expiresAt<=now){if(entry.timeout)clearTimeout(entry.timeout);this.memory.delete(key)}}}patternToRegex(pattern){if(pattern.length>RememberMemoryProvider.MAX_PATTERN_LENGTH){throw new Error(`Pattern too long: ${pattern.length} chars (max ${RememberMemoryProvider.MAX_PATTERN_LENGTH})`)}const wildcardCount=(pattern.match(/[*?]/g)||[]).length;if(wildcardCount>RememberMemoryProvider.MAX_WILDCARDS){throw new Error(`Too many wildcards in pattern: ${wildcardCount} (max ${RememberMemoryProvider.MAX_WILDCARDS})`)}const escaped=pattern.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".");return new RegExp(`^${escaped}$`)}};__publicField(RememberMemoryProvider,"MAX_PATTERN_LENGTH",500);__publicField(RememberMemoryProvider,"MAX_WILDCARDS",20);RememberMemoryProvider=__decorateClass([Provider2({name:"provider:remember:memory",description:"In-memory storage provider for RememberPlugin",scope:ProviderScope2.GLOBAL})],RememberMemoryProvider);import Redis from"ioredis";import{Provider as Provider3,ProviderScope as ProviderScope3}from"@frontmcp/sdk";function prefixedStoreKey(keyPrefix,key){return keyPrefix&&key.startsWith(keyPrefix)?key:keyPrefix+key}function doubledPrefixKey(keyPrefix,key){return keyPrefix&&key.startsWith(keyPrefix)?keyPrefix+key:void 0}function callerKeyOf(keyPrefix,pattern,backendKey){return keyPrefix&&pattern.startsWith(keyPrefix)?backendKey:backendKey.slice(keyPrefix.length)}var SET_IF_NEITHER_KEY_EXISTS_SCRIPT=`
|
|
899
2
|
if redis.call('EXISTS', KEYS[1], KEYS[2]) > 0 then return 0 end
|
|
900
3
|
if ARGV[2] ~= '' then redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]) else redis.call('SET', KEYS[1], ARGV[1]) end
|
|
901
4
|
return 1
|
|
902
|
-
`;
|
|
903
|
-
|
|
904
|
-
// plugins/plugin-remember/src/providers/remember-redis.provider.ts
|
|
905
|
-
var RememberRedisProvider = class {
|
|
906
|
-
client;
|
|
907
|
-
/**
|
|
908
|
-
* Prefix prepended to Redis keys that don't already start with it.
|
|
909
|
-
* Include any separator (e.g., "myapp:" or "user:123:") as part of the prefix.
|
|
910
|
-
*/
|
|
911
|
-
keyPrefix;
|
|
912
|
-
/** TTL in seconds for values stored without one. */
|
|
913
|
-
defaultTTL;
|
|
914
|
-
/** True if this provider created the client (and should close it), false if externally provided */
|
|
915
|
-
ownsClient;
|
|
916
|
-
constructor(options) {
|
|
917
|
-
this.keyPrefix = options.keyPrefix ?? "";
|
|
918
|
-
this.defaultTTL = options.defaultTTL;
|
|
919
|
-
if (options.type === "redis-client") {
|
|
920
|
-
this.client = options.client;
|
|
921
|
-
this.ownsClient = false;
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
this.ownsClient = true;
|
|
925
|
-
this.client = new Redis({
|
|
926
|
-
lazyConnect: false,
|
|
927
|
-
maxRetriesPerRequest: 3,
|
|
928
|
-
...options.config
|
|
929
|
-
});
|
|
930
|
-
this.client.on("connect", () => {
|
|
931
|
-
if (process.env["DEBUG"] === "true") {
|
|
932
|
-
console.log("[RememberPlugin:Redis] Connected");
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
this.client.on("error", (err) => {
|
|
936
|
-
console.error("[RememberPlugin:Redis] Error:", err.message);
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
/**
|
|
940
|
-
* Store a value with optional TTL.
|
|
941
|
-
*
|
|
942
|
-
* @param key - The key to store under
|
|
943
|
-
* @param value - The value to store (must not be undefined)
|
|
944
|
-
* @param ttlSeconds - Optional TTL in seconds (must be positive integer if provided)
|
|
945
|
-
* @throws Error if value is undefined or ttlSeconds is invalid
|
|
946
|
-
*/
|
|
947
|
-
async setValue(key, value, ttlSeconds) {
|
|
948
|
-
if (value === void 0) {
|
|
949
|
-
throw new Error("Cannot store undefined value. Use null or delete the key instead.");
|
|
950
|
-
}
|
|
951
|
-
if (ttlSeconds !== void 0) {
|
|
952
|
-
if (typeof ttlSeconds !== "number" || !Number.isFinite(ttlSeconds)) {
|
|
953
|
-
throw new Error(`Invalid TTL: expected a number, got ${typeof ttlSeconds}`);
|
|
954
|
-
}
|
|
955
|
-
if (ttlSeconds <= 0) {
|
|
956
|
-
throw new Error(`Invalid TTL: must be positive, got ${ttlSeconds}`);
|
|
957
|
-
}
|
|
958
|
-
if (!Number.isInteger(ttlSeconds)) {
|
|
959
|
-
throw new Error(`Invalid TTL: must be an integer, got ${ttlSeconds}`);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
const fullKey = prefixedStoreKey(this.keyPrefix, key);
|
|
963
|
-
const strValue = JSON.stringify(value);
|
|
964
|
-
const ttl = ttlSeconds ?? this.defaultTTL;
|
|
965
|
-
if (ttl !== void 0 && ttl > 0) {
|
|
966
|
-
await this.client.set(fullKey, strValue, "EX", ttl);
|
|
967
|
-
} else {
|
|
968
|
-
await this.client.set(fullKey, strValue);
|
|
969
|
-
}
|
|
970
|
-
await this.deleteDoubledPrefixKey(key);
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* Store a value only if the key is absent, via Redis `SET ... NX`.
|
|
974
|
-
*
|
|
975
|
-
* `set` resolves to `'OK'` when it created the key and `null` when the key already existed,
|
|
976
|
-
* so the whole check-and-write is one round trip and two callers racing cannot both win.
|
|
977
|
-
* A key that releases up to 1.9.1 may hold under the doubled prefix counts as present too: one
|
|
978
|
-
* script checks both keys and writes, so an entry still there is never shadowed.
|
|
979
|
-
*/
|
|
980
|
-
async setIfAbsent(key, value, ttlSeconds) {
|
|
981
|
-
const fullKey = prefixedStoreKey(this.keyPrefix, key);
|
|
982
|
-
const strValue = JSON.stringify(value);
|
|
983
|
-
const ttl = ttlSeconds ?? this.defaultTTL;
|
|
984
|
-
const hasTtl = ttl !== void 0 && ttl > 0;
|
|
985
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
986
|
-
if (doubledKey) {
|
|
987
|
-
const ttlArgument = hasTtl ? String(ttl) : "";
|
|
988
|
-
const created = await this.client.eval(
|
|
989
|
-
SET_IF_NEITHER_KEY_EXISTS_SCRIPT,
|
|
990
|
-
2,
|
|
991
|
-
fullKey,
|
|
992
|
-
doubledKey,
|
|
993
|
-
strValue,
|
|
994
|
-
ttlArgument
|
|
995
|
-
);
|
|
996
|
-
return created === 1;
|
|
997
|
-
}
|
|
998
|
-
const result = hasTtl ? await this.client.set(fullKey, strValue, "EX", ttl, "NX") : await this.client.set(fullKey, strValue, "NX");
|
|
999
|
-
return result === "OK";
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Retrieve a value by key.
|
|
1003
|
-
*
|
|
1004
|
-
* Returns the parsed JSON value if successful, or `defaultValue` if:
|
|
1005
|
-
* - The key does not exist
|
|
1006
|
-
* - The stored value is not valid JSON (malformed or legacy data)
|
|
1007
|
-
*
|
|
1008
|
-
* @param key - The key to retrieve
|
|
1009
|
-
* @param defaultValue - Value to return if key doesn't exist or parsing fails
|
|
1010
|
-
* @returns The parsed value as T, or defaultValue/undefined
|
|
1011
|
-
*/
|
|
1012
|
-
async getValue(key, defaultValue) {
|
|
1013
|
-
const raw = await this.client.get(prefixedStoreKey(this.keyPrefix, key)) ?? await this.readDoubledPrefixValue(key);
|
|
1014
|
-
if (raw === null) return defaultValue;
|
|
1015
|
-
try {
|
|
1016
|
-
return JSON.parse(raw);
|
|
1017
|
-
} catch {
|
|
1018
|
-
return defaultValue;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Delete a key.
|
|
1023
|
-
*/
|
|
1024
|
-
async delete(key) {
|
|
1025
|
-
await this.client.del(prefixedStoreKey(this.keyPrefix, key));
|
|
1026
|
-
await this.deleteDoubledPrefixKey(key);
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Check if a key exists.
|
|
1030
|
-
*/
|
|
1031
|
-
async exists(key) {
|
|
1032
|
-
if (await this.client.exists(prefixedStoreKey(this.keyPrefix, key)) === 1) return true;
|
|
1033
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
1034
|
-
return doubledKey !== void 0 && await this.client.exists(doubledKey) === 1;
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* List keys matching a pattern.
|
|
1038
|
-
* Uses Redis SCAN for efficient iteration.
|
|
1039
|
-
*/
|
|
1040
|
-
async keys(pattern = "*") {
|
|
1041
|
-
const result = /* @__PURE__ */ new Set();
|
|
1042
|
-
for (const key of await this.scan(prefixedStoreKey(this.keyPrefix, pattern))) {
|
|
1043
|
-
result.add(callerKeyOf(this.keyPrefix, pattern, key));
|
|
1044
|
-
}
|
|
1045
|
-
const doubledPattern = doubledPrefixKey(this.keyPrefix, pattern);
|
|
1046
|
-
if (doubledPattern) {
|
|
1047
|
-
for (const key of await this.scan(doubledPattern)) result.add(key.slice(this.keyPrefix.length));
|
|
1048
|
-
}
|
|
1049
|
-
return [...result];
|
|
1050
|
-
}
|
|
1051
|
-
async scan(match) {
|
|
1052
|
-
const found = [];
|
|
1053
|
-
let cursor = "0";
|
|
1054
|
-
do {
|
|
1055
|
-
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", match, "COUNT", 100);
|
|
1056
|
-
cursor = nextCursor;
|
|
1057
|
-
found.push(...keys);
|
|
1058
|
-
} while (cursor !== "0");
|
|
1059
|
-
return found;
|
|
1060
|
-
}
|
|
1061
|
-
/**
|
|
1062
|
-
* The stored text of an entry a release up to 1.9.1 wrote under the doubled key, or null. It is read
|
|
1063
|
-
* in place, not copied: the entry keeps its TTL, a rolled-back release still finds it, and a
|
|
1064
|
-
* concurrent `delete()` cannot be undone by a copy. The next write or delete of the key removes it.
|
|
1065
|
-
*/
|
|
1066
|
-
async readDoubledPrefixValue(key) {
|
|
1067
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
1068
|
-
return doubledKey ? this.client.get(doubledKey) : null;
|
|
1069
|
-
}
|
|
1070
|
-
/** Drops what releases up to 1.9.1 left under the doubled key, so it cannot come back once this key is gone. */
|
|
1071
|
-
async deleteDoubledPrefixKey(key) {
|
|
1072
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
1073
|
-
if (doubledKey) await this.client.del(doubledKey);
|
|
1074
|
-
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Gracefully close the Redis connection.
|
|
1077
|
-
* Only closes if this provider owns the client (created it internally).
|
|
1078
|
-
* Externally-provided clients are left open for the caller to manage.
|
|
1079
|
-
*/
|
|
1080
|
-
async close() {
|
|
1081
|
-
if (this.ownsClient) {
|
|
1082
|
-
await this.client.quit();
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
|
-
RememberRedisProvider = __decorateClass([
|
|
1087
|
-
Provider3({
|
|
1088
|
-
name: "provider:remember:redis",
|
|
1089
|
-
description: "Redis-based storage provider for RememberPlugin",
|
|
1090
|
-
scope: ProviderScope3.GLOBAL
|
|
1091
|
-
})
|
|
1092
|
-
], RememberRedisProvider);
|
|
1093
|
-
|
|
1094
|
-
// plugins/plugin-remember/src/providers/remember-vercel-kv.provider.ts
|
|
1095
|
-
import { Provider as Provider4, ProviderScope as ProviderScope4 } from "@frontmcp/sdk";
|
|
1096
|
-
import { createVercelKvClient } from "@frontmcp/utils";
|
|
1097
|
-
var RememberVercelKvProvider = class {
|
|
1098
|
-
connection;
|
|
1099
|
-
client;
|
|
1100
|
-
keyPrefix;
|
|
1101
|
-
defaultTTL;
|
|
1102
|
-
constructor(options = {}) {
|
|
1103
|
-
const hasUrl = options.url !== void 0;
|
|
1104
|
-
const hasToken = options.token !== void 0;
|
|
1105
|
-
if (hasUrl !== hasToken) {
|
|
1106
|
-
throw new Error(
|
|
1107
|
-
`RememberVercelKvProvider: Both 'url' and 'token' must be provided together, or neither. Received: url=${hasUrl ? "provided" : "missing"}, token=${hasToken ? "provided" : "missing"}`
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
this.connection = { url: options.url, token: options.token };
|
|
1111
|
-
this.keyPrefix = options.keyPrefix ?? "remember:";
|
|
1112
|
-
this.defaultTTL = options.defaultTTL;
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* The client, built on first use from the url and token (or KV_REST_API_URL / KV_REST_API_TOKEN)
|
|
1116
|
-
* by the shared loader, which a Cloudflare Worker can bundle (#711). Values are read back as
|
|
1117
|
-
* stored. A failed attempt is retried.
|
|
1118
|
-
*/
|
|
1119
|
-
kv() {
|
|
1120
|
-
this.client ??= createVercelKvClient(this.connection).catch((error) => {
|
|
1121
|
-
this.client = void 0;
|
|
1122
|
-
throw error;
|
|
1123
|
-
});
|
|
1124
|
-
return this.client;
|
|
1125
|
-
}
|
|
1126
|
-
prefixKey(key) {
|
|
1127
|
-
return prefixedStoreKey(this.keyPrefix, key);
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Store a value with optional TTL.
|
|
1131
|
-
*/
|
|
1132
|
-
async setValue(key, value, ttlSeconds) {
|
|
1133
|
-
const fullKey = this.prefixKey(key);
|
|
1134
|
-
const strValue = JSON.stringify(value);
|
|
1135
|
-
const ttl = ttlSeconds ?? this.defaultTTL;
|
|
1136
|
-
const kv = await this.kv();
|
|
1137
|
-
if (ttl && ttl > 0) {
|
|
1138
|
-
await kv.set(fullKey, strValue, { ex: ttl });
|
|
1139
|
-
} else {
|
|
1140
|
-
await kv.set(fullKey, strValue);
|
|
1141
|
-
}
|
|
1142
|
-
await this.deleteDoubledPrefixKey(key);
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Store a value only if the key is absent, via the `nx` option.
|
|
1146
|
-
*
|
|
1147
|
-
* Resolves to `'OK'` when it created the key and `null` when the key already existed, so two
|
|
1148
|
-
* callers racing cannot both win. A key that releases up to 1.9.1 may hold under the doubled
|
|
1149
|
-
* prefix counts as present too: one script checks both keys and writes, so an entry still there
|
|
1150
|
-
* is never shadowed.
|
|
1151
|
-
*/
|
|
1152
|
-
async setIfAbsent(key, value, ttlSeconds) {
|
|
1153
|
-
const fullKey = this.prefixKey(key);
|
|
1154
|
-
const strValue = JSON.stringify(value);
|
|
1155
|
-
const ttl = ttlSeconds ?? this.defaultTTL;
|
|
1156
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
1157
|
-
const kv = await this.kv();
|
|
1158
|
-
if (doubledKey) {
|
|
1159
|
-
const ttlArgument = ttl && ttl > 0 ? String(ttl) : "";
|
|
1160
|
-
const created = await kv.eval(SET_IF_NEITHER_KEY_EXISTS_SCRIPT, [fullKey, doubledKey], [strValue, ttlArgument]);
|
|
1161
|
-
return Number(created) === 1;
|
|
1162
|
-
}
|
|
1163
|
-
const result = ttl && ttl > 0 ? await kv.set(fullKey, strValue, { nx: true, ex: ttl }) : await kv.set(fullKey, strValue, { nx: true });
|
|
1164
|
-
return result === "OK";
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Retrieve a value by key.
|
|
1168
|
-
*/
|
|
1169
|
-
async getValue(key, defaultValue) {
|
|
1170
|
-
const kv = await this.kv();
|
|
1171
|
-
const raw = await kv.get(this.prefixKey(key)) ?? await this.readDoubledPrefixValue(key);
|
|
1172
|
-
if (raw === null) return defaultValue;
|
|
1173
|
-
try {
|
|
1174
|
-
return JSON.parse(raw);
|
|
1175
|
-
} catch {
|
|
1176
|
-
return raw;
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
/**
|
|
1180
|
-
* Delete a key.
|
|
1181
|
-
*/
|
|
1182
|
-
async delete(key) {
|
|
1183
|
-
const kv = await this.kv();
|
|
1184
|
-
await kv.del(this.prefixKey(key));
|
|
1185
|
-
await this.deleteDoubledPrefixKey(key);
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* Check if a key exists.
|
|
1189
|
-
*/
|
|
1190
|
-
async exists(key) {
|
|
1191
|
-
const kv = await this.kv();
|
|
1192
|
-
if (await kv.exists(this.prefixKey(key)) === 1) return true;
|
|
1193
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
1194
|
-
return doubledKey !== void 0 && await kv.exists(doubledKey) === 1;
|
|
1195
|
-
}
|
|
1196
|
-
/**
|
|
1197
|
-
* List keys matching a pattern.
|
|
1198
|
-
* Uses SCAN for efficient iteration.
|
|
1199
|
-
*/
|
|
1200
|
-
async keys(pattern = "*") {
|
|
1201
|
-
const result = /* @__PURE__ */ new Set();
|
|
1202
|
-
for (const key of await this.scan(this.prefixKey(pattern))) {
|
|
1203
|
-
result.add(callerKeyOf(this.keyPrefix, pattern, key));
|
|
1204
|
-
}
|
|
1205
|
-
const doubledPattern = doubledPrefixKey(this.keyPrefix, pattern);
|
|
1206
|
-
if (doubledPattern) {
|
|
1207
|
-
for (const key of await this.scan(doubledPattern)) result.add(key.slice(this.keyPrefix.length));
|
|
1208
|
-
}
|
|
1209
|
-
return [...result];
|
|
1210
|
-
}
|
|
1211
|
-
async scan(match) {
|
|
1212
|
-
const found = [];
|
|
1213
|
-
const kv = await this.kv();
|
|
1214
|
-
try {
|
|
1215
|
-
let cursor = 0;
|
|
1216
|
-
do {
|
|
1217
|
-
const [nextCursor, keys] = await kv.scan(cursor, { match, count: 100 });
|
|
1218
|
-
cursor = nextCursor;
|
|
1219
|
-
found.push(...keys);
|
|
1220
|
-
} while (String(cursor) !== "0");
|
|
1221
|
-
} catch {
|
|
1222
|
-
try {
|
|
1223
|
-
found.push(...await kv.keys(match));
|
|
1224
|
-
} catch {
|
|
1225
|
-
console.warn("[RememberPlugin:VercelKV] keys() operation not supported");
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
return found;
|
|
1229
|
-
}
|
|
1230
|
-
/**
|
|
1231
|
-
* The stored text of an entry a release up to 1.9.1 wrote under the doubled key, or null. It is read
|
|
1232
|
-
* in place, not copied: the entry keeps its TTL, a rolled-back release still finds it, and a
|
|
1233
|
-
* concurrent `delete()` cannot be undone by a copy. The next write or delete of the key removes it.
|
|
1234
|
-
*/
|
|
1235
|
-
async readDoubledPrefixValue(key) {
|
|
1236
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
1237
|
-
if (!doubledKey) return null;
|
|
1238
|
-
const kv = await this.kv();
|
|
1239
|
-
return kv.get(doubledKey);
|
|
1240
|
-
}
|
|
1241
|
-
/** Drops what releases up to 1.9.1 left under the doubled key, so it cannot come back once this key is gone. */
|
|
1242
|
-
async deleteDoubledPrefixKey(key) {
|
|
1243
|
-
const doubledKey = doubledPrefixKey(this.keyPrefix, key);
|
|
1244
|
-
if (!doubledKey) return;
|
|
1245
|
-
const kv = await this.kv();
|
|
1246
|
-
await kv.del(doubledKey);
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* Gracefully close the provider.
|
|
1250
|
-
* No-op for Vercel KV as it uses stateless REST API.
|
|
1251
|
-
*/
|
|
1252
|
-
async close() {
|
|
1253
|
-
}
|
|
1254
|
-
};
|
|
1255
|
-
RememberVercelKvProvider = __decorateClass([
|
|
1256
|
-
Provider4({
|
|
1257
|
-
name: "provider:remember:vercel-kv",
|
|
1258
|
-
description: "Vercel KV-based storage provider for RememberPlugin",
|
|
1259
|
-
scope: ProviderScope4.GLOBAL
|
|
1260
|
-
})
|
|
1261
|
-
], RememberVercelKvProvider);
|
|
1262
|
-
|
|
1263
|
-
// plugins/plugin-remember/src/remember.symbols.ts
|
|
1264
|
-
var RememberStoreToken = /* @__PURE__ */ Symbol(
|
|
1265
|
-
"plugin:remember:store"
|
|
1266
|
-
);
|
|
1267
|
-
var RememberConfigToken = /* @__PURE__ */ Symbol(
|
|
1268
|
-
"plugin:remember:config"
|
|
1269
|
-
);
|
|
1270
|
-
var RememberAccessorToken = /* @__PURE__ */ Symbol(
|
|
1271
|
-
"plugin:remember:accessor"
|
|
1272
|
-
);
|
|
1273
|
-
|
|
1274
|
-
// plugins/plugin-remember/src/tools/remember-tools.factory.ts
|
|
1275
|
-
import { Tool as Tool5 } from "@frontmcp/sdk";
|
|
1276
|
-
|
|
1277
|
-
// plugins/plugin-remember/src/tools/forget.tool.ts
|
|
1278
|
-
import { z as z2 } from "@frontmcp/lazy-zod";
|
|
1279
|
-
import { Tool, ToolContext } from "@frontmcp/sdk";
|
|
1280
|
-
|
|
1281
|
-
// plugins/plugin-remember/src/tools/remember-scope.description.ts
|
|
1282
|
-
var REMEMBER_SCOPE_DESCRIPTION = "Whose memory to use (default: session). session: this session; without one (stateless HTTP, MCP 2026-07-28), the signed-in caller across its requests. user: the signed-in caller, across all of its sessions. tool: the tool running the call, for the same caller as session; each memory tool has its own, so the other memory tools do not see what one of them stores in tool scope. global: shared by every caller. An anonymous caller cannot use user scope, nor session or tool scope without a session.";
|
|
1283
|
-
|
|
1284
|
-
// plugins/plugin-remember/src/tools/remember-tool-names.ts
|
|
1285
|
-
function rememberToolNames(prefix = "") {
|
|
1286
|
-
return {
|
|
1287
|
-
rememberThis: `${prefix}remember_this`,
|
|
1288
|
-
recall: `${prefix}recall`,
|
|
1289
|
-
forget: `${prefix}forget`,
|
|
1290
|
-
listMemories: `${prefix}list_memories`
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// plugins/plugin-remember/src/tools/forget.tool.ts
|
|
1295
|
-
var forgetInputSchema = {
|
|
1296
|
-
key: z2.string().min(1).describe("What memory to forget"),
|
|
1297
|
-
scope: z2.enum(["session", "user", "tool", "global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION)
|
|
1298
|
-
};
|
|
1299
|
-
var forgetOutputSchema = z2.object({
|
|
1300
|
-
success: z2.boolean(),
|
|
1301
|
-
key: z2.string(),
|
|
1302
|
-
scope: z2.string(),
|
|
1303
|
-
existed: z2.boolean()
|
|
1304
|
-
});
|
|
1305
|
-
function forgetDescription() {
|
|
1306
|
-
return "Forget a previously remembered value. Use this when the user wants to delete stored preferences or information.";
|
|
1307
|
-
}
|
|
1308
|
-
function forgetToolMetadata(names = rememberToolNames()) {
|
|
1309
|
-
return {
|
|
1310
|
-
name: names.forget,
|
|
1311
|
-
description: forgetDescription(),
|
|
1312
|
-
inputSchema: forgetInputSchema,
|
|
1313
|
-
outputSchema: forgetOutputSchema,
|
|
1314
|
-
annotations: { readOnlyHint: false }
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
var ForgetTool = class extends ToolContext {
|
|
1318
|
-
async execute(input) {
|
|
1319
|
-
const remember = this.get(RememberAccessorToken);
|
|
1320
|
-
const config = this.get(RememberConfigToken);
|
|
1321
|
-
const scope = input.scope ?? "session";
|
|
1322
|
-
const allowedScopes = config.tools?.allowedScopes ?? ["session", "user", "tool", "global"];
|
|
1323
|
-
if (!allowedScopes.includes(scope)) {
|
|
1324
|
-
throw this.fail(new RememberScopeNotAllowedError(scope, allowedScopes));
|
|
1325
|
-
}
|
|
1326
|
-
const existed = await remember.knows(input.key, { scope });
|
|
1327
|
-
await remember.forget(input.key, { scope });
|
|
1328
|
-
return {
|
|
1329
|
-
success: true,
|
|
1330
|
-
key: input.key,
|
|
1331
|
-
scope,
|
|
1332
|
-
existed
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
|
-
};
|
|
1336
|
-
ForgetTool = __decorateClass([
|
|
1337
|
-
Tool(forgetToolMetadata())
|
|
1338
|
-
], ForgetTool);
|
|
1339
|
-
|
|
1340
|
-
// plugins/plugin-remember/src/tools/list-memories.tool.ts
|
|
1341
|
-
import { z as z3 } from "@frontmcp/lazy-zod";
|
|
1342
|
-
import { Tool as Tool2, ToolContext as ToolContext2 } from "@frontmcp/sdk";
|
|
1343
|
-
var listMemoriesInputSchema = {
|
|
1344
|
-
scope: z3.enum(["session", "user", "tool", "global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION),
|
|
1345
|
-
pattern: z3.string().optional().describe('Pattern to filter keys (e.g., "user_*")'),
|
|
1346
|
-
limit: z3.number().positive().max(100).optional().describe("Maximum number of keys to return (default: 50)")
|
|
1347
|
-
};
|
|
1348
|
-
var listMemoriesOutputSchema = z3.object({
|
|
1349
|
-
keys: z3.array(z3.string()),
|
|
1350
|
-
scope: z3.string(),
|
|
1351
|
-
count: z3.number(),
|
|
1352
|
-
truncated: z3.boolean()
|
|
1353
|
-
});
|
|
1354
|
-
function listMemoriesDescription(names = rememberToolNames()) {
|
|
1355
|
-
return `List the remembered keys in one scope (default: session). Use this to see what memories are stored before you ${names.recall} or ${names.forget} one.`;
|
|
1356
|
-
}
|
|
1357
|
-
function listMemoriesToolMetadata(names = rememberToolNames()) {
|
|
1358
|
-
return {
|
|
1359
|
-
name: names.listMemories,
|
|
1360
|
-
description: listMemoriesDescription(names),
|
|
1361
|
-
inputSchema: listMemoriesInputSchema,
|
|
1362
|
-
outputSchema: listMemoriesOutputSchema,
|
|
1363
|
-
annotations: { readOnlyHint: true }
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
var ListMemoriesTool = class extends ToolContext2 {
|
|
1367
|
-
async execute(input) {
|
|
1368
|
-
const remember = this.get(RememberAccessorToken);
|
|
1369
|
-
const config = this.get(RememberConfigToken);
|
|
1370
|
-
const scope = input.scope ?? "session";
|
|
1371
|
-
const allowedScopes = config.tools?.allowedScopes ?? ["session", "user", "tool", "global"];
|
|
1372
|
-
if (!allowedScopes.includes(scope)) {
|
|
1373
|
-
throw this.fail(new RememberScopeNotAllowedError(scope, allowedScopes));
|
|
1374
|
-
}
|
|
1375
|
-
const limit = input.limit ?? 50;
|
|
1376
|
-
let keys = await remember.list({ scope, pattern: input.pattern });
|
|
1377
|
-
const truncated = keys.length > limit;
|
|
1378
|
-
if (truncated) {
|
|
1379
|
-
keys = keys.slice(0, limit);
|
|
1380
|
-
}
|
|
1381
|
-
return {
|
|
1382
|
-
keys,
|
|
1383
|
-
scope,
|
|
1384
|
-
count: keys.length,
|
|
1385
|
-
truncated
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
1388
|
-
};
|
|
1389
|
-
ListMemoriesTool = __decorateClass([
|
|
1390
|
-
Tool2(listMemoriesToolMetadata())
|
|
1391
|
-
], ListMemoriesTool);
|
|
1392
|
-
|
|
1393
|
-
// plugins/plugin-remember/src/tools/recall.tool.ts
|
|
1394
|
-
import { z as z4 } from "@frontmcp/lazy-zod";
|
|
1395
|
-
import { Tool as Tool3, ToolContext as ToolContext3 } from "@frontmcp/sdk";
|
|
1396
|
-
var recallInputSchema = {
|
|
1397
|
-
key: z4.string().min(1).describe("What memory to recall"),
|
|
1398
|
-
scope: z4.enum(["session", "user", "tool", "global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION)
|
|
1399
|
-
};
|
|
1400
|
-
var recallOutputSchema = z4.object({
|
|
1401
|
-
found: z4.boolean(),
|
|
1402
|
-
key: z4.string(),
|
|
1403
|
-
value: z4.unknown().optional(),
|
|
1404
|
-
scope: z4.string(),
|
|
1405
|
-
createdAt: z4.number().optional(),
|
|
1406
|
-
expiresAt: z4.number().optional()
|
|
1407
|
-
});
|
|
1408
|
-
function recallDescription(names = rememberToolNames()) {
|
|
1409
|
-
return `Recall something that was previously remembered. Use this to retrieve stored preferences, settings, or any information that was saved with ${names.rememberThis}.`;
|
|
1410
|
-
}
|
|
1411
|
-
function recallToolMetadata(names = rememberToolNames()) {
|
|
1412
|
-
return {
|
|
1413
|
-
name: names.recall,
|
|
1414
|
-
description: recallDescription(names),
|
|
1415
|
-
inputSchema: recallInputSchema,
|
|
1416
|
-
outputSchema: recallOutputSchema,
|
|
1417
|
-
annotations: { readOnlyHint: true }
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
var RecallTool = class extends ToolContext3 {
|
|
1421
|
-
async execute(input) {
|
|
1422
|
-
const remember = this.get(RememberAccessorToken);
|
|
1423
|
-
const config = this.get(RememberConfigToken);
|
|
1424
|
-
const scope = input.scope ?? "session";
|
|
1425
|
-
const allowedScopes = config.tools?.allowedScopes ?? ["session", "user", "tool", "global"];
|
|
1426
|
-
if (!allowedScopes.includes(scope)) {
|
|
1427
|
-
throw this.fail(new RememberScopeNotAllowedError(scope, allowedScopes));
|
|
1428
|
-
}
|
|
1429
|
-
const entry = await remember.getEntry(input.key, { scope });
|
|
1430
|
-
if (!entry) {
|
|
1431
|
-
return {
|
|
1432
|
-
found: false,
|
|
1433
|
-
key: input.key,
|
|
1434
|
-
scope
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
return {
|
|
1438
|
-
found: true,
|
|
1439
|
-
key: input.key,
|
|
1440
|
-
value: entry.value,
|
|
1441
|
-
scope,
|
|
1442
|
-
createdAt: entry.createdAt,
|
|
1443
|
-
expiresAt: entry.expiresAt
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
};
|
|
1447
|
-
RecallTool = __decorateClass([
|
|
1448
|
-
Tool3(recallToolMetadata())
|
|
1449
|
-
], RecallTool);
|
|
1450
|
-
|
|
1451
|
-
// plugins/plugin-remember/src/tools/remember-this.tool.ts
|
|
1452
|
-
import { z as z5 } from "@frontmcp/lazy-zod";
|
|
1453
|
-
import { Tool as Tool4, ToolContext as ToolContext4 } from "@frontmcp/sdk";
|
|
1454
|
-
var rememberThisInputSchema = {
|
|
1455
|
-
key: z5.string().min(1).describe('What to call this memory (e.g., "user_preference", "last_action")'),
|
|
1456
|
-
value: z5.unknown().describe("The value to remember (any JSON-serializable data)"),
|
|
1457
|
-
scope: z5.enum(["session", "user", "tool", "global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION),
|
|
1458
|
-
ttl: z5.number().int().positive().optional().describe("Forget after this many whole seconds"),
|
|
1459
|
-
brand: z5.enum(["preference", "cache", "state", "conversation", "custom"]).optional().describe("Type hint for the stored value")
|
|
1460
|
-
};
|
|
1461
|
-
var rememberThisOutputSchema = z5.object({
|
|
1462
|
-
success: z5.boolean(),
|
|
1463
|
-
key: z5.string(),
|
|
1464
|
-
scope: z5.string(),
|
|
1465
|
-
expiresAt: z5.number().optional()
|
|
1466
|
-
});
|
|
1467
|
-
function rememberThisDescription(names = rememberToolNames()) {
|
|
1468
|
-
return `Remember something for later: store a value under a key that ${names.recall} can read back, in the scope you choose (this session by default; user scope for the signed-in user across sessions). It lasts until it is forgotten or its ttl runs out. Use this when the user asks you to remember preferences, settings, or any information they want to persist.`;
|
|
1469
|
-
}
|
|
1470
|
-
function rememberThisToolMetadata(names = rememberToolNames()) {
|
|
1471
|
-
return {
|
|
1472
|
-
name: names.rememberThis,
|
|
1473
|
-
description: rememberThisDescription(names),
|
|
1474
|
-
inputSchema: rememberThisInputSchema,
|
|
1475
|
-
outputSchema: rememberThisOutputSchema,
|
|
1476
|
-
annotations: { readOnlyHint: false }
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
var RememberThisTool = class extends ToolContext4 {
|
|
1480
|
-
async execute(input) {
|
|
1481
|
-
const remember = this.get(RememberAccessorToken);
|
|
1482
|
-
const config = this.get(RememberConfigToken);
|
|
1483
|
-
const scope = input.scope ?? "session";
|
|
1484
|
-
const allowedScopes = config.tools?.allowedScopes ?? ["session", "user", "tool", "global"];
|
|
1485
|
-
if (!allowedScopes.includes(scope)) {
|
|
1486
|
-
throw this.fail(new RememberScopeNotAllowedError(scope, allowedScopes));
|
|
1487
|
-
}
|
|
1488
|
-
const entry = await remember.set(input.key, input.value, {
|
|
1489
|
-
scope,
|
|
1490
|
-
ttl: input.ttl,
|
|
1491
|
-
brand: input.brand
|
|
1492
|
-
});
|
|
1493
|
-
return {
|
|
1494
|
-
success: true,
|
|
1495
|
-
key: input.key,
|
|
1496
|
-
scope,
|
|
1497
|
-
expiresAt: entry.expiresAt
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
};
|
|
1501
|
-
RememberThisTool = __decorateClass([
|
|
1502
|
-
Tool4(rememberThisToolMetadata())
|
|
1503
|
-
], RememberThisTool);
|
|
1504
|
-
|
|
1505
|
-
// plugins/plugin-remember/src/tools/remember-tools.factory.ts
|
|
1506
|
-
function createRememberTools(prefix = "") {
|
|
1507
|
-
if (!prefix) return [RememberThisTool, RecallTool, ForgetTool, ListMemoriesTool];
|
|
1508
|
-
const names = rememberToolNames(prefix);
|
|
1509
|
-
let PrefixedRememberThisTool = class extends RememberThisTool {
|
|
1510
|
-
};
|
|
1511
|
-
PrefixedRememberThisTool = __decorateClass([
|
|
1512
|
-
Tool5(rememberThisToolMetadata(names))
|
|
1513
|
-
], PrefixedRememberThisTool);
|
|
1514
|
-
let PrefixedRecallTool = class extends RecallTool {
|
|
1515
|
-
};
|
|
1516
|
-
PrefixedRecallTool = __decorateClass([
|
|
1517
|
-
Tool5(recallToolMetadata(names))
|
|
1518
|
-
], PrefixedRecallTool);
|
|
1519
|
-
let PrefixedForgetTool = class extends ForgetTool {
|
|
1520
|
-
};
|
|
1521
|
-
PrefixedForgetTool = __decorateClass([
|
|
1522
|
-
Tool5(forgetToolMetadata(names))
|
|
1523
|
-
], PrefixedForgetTool);
|
|
1524
|
-
let PrefixedListMemoriesTool = class extends ListMemoriesTool {
|
|
1525
|
-
};
|
|
1526
|
-
PrefixedListMemoriesTool = __decorateClass([
|
|
1527
|
-
Tool5(listMemoriesToolMetadata(names))
|
|
1528
|
-
], PrefixedListMemoriesTool);
|
|
1529
|
-
return [PrefixedRememberThisTool, PrefixedRecallTool, PrefixedForgetTool, PrefixedListMemoriesTool];
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// plugins/plugin-remember/src/remember.plugin.ts
|
|
1533
|
-
function withCheckedOptions(options) {
|
|
1534
|
-
const customKey = options.encryption?.customKey;
|
|
1535
|
-
if (customKey !== void 0 && customKey.trim() === "") {
|
|
1536
|
-
throw new RememberConfigurationError(
|
|
1537
|
-
"RememberPlugin encryption.customKey must not be empty; omit it to derive keys from the server secret"
|
|
1538
|
-
);
|
|
1539
|
-
}
|
|
1540
|
-
const { defaultTTL } = options;
|
|
1541
|
-
if (defaultTTL === void 0) return options;
|
|
1542
|
-
if (defaultTTL === 0) return { ...options, defaultTTL: void 0 };
|
|
1543
|
-
if (!Number.isInteger(defaultTTL) || defaultTTL < 0) {
|
|
1544
|
-
throw new RememberConfigurationError(
|
|
1545
|
-
`RememberPlugin defaultTTL must be a whole number of seconds (0 for no default expiry), got ${defaultTTL}`
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
return options;
|
|
1549
|
-
}
|
|
1550
|
-
var RememberPlugin = class extends DynamicPlugin {
|
|
1551
|
-
options;
|
|
1552
|
-
constructor(options = {}) {
|
|
1553
|
-
super();
|
|
1554
|
-
this.options = {
|
|
1555
|
-
...RememberPlugin.defaultOptions,
|
|
1556
|
-
...withCheckedOptions(options)
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
__publicField(RememberPlugin, "defaultOptions", {
|
|
1561
|
-
type: "memory",
|
|
1562
|
-
keyPrefix: "remember:",
|
|
1563
|
-
encryption: { enabled: true }
|
|
1564
|
-
});
|
|
1565
|
-
/**
|
|
1566
|
-
* The memory tools, when `tools.enabled` is set: `remember_this`, `recall`, `forget` and
|
|
1567
|
-
* `list_memories`, under `tools.prefix`.
|
|
1568
|
-
*/
|
|
1569
|
-
__publicField(RememberPlugin, "dynamicTools", (options) => options.tools?.enabled ? createRememberTools(options.tools.prefix) : []);
|
|
1570
|
-
/**
|
|
1571
|
-
* Dynamic providers based on plugin options.
|
|
1572
|
-
*/
|
|
1573
|
-
__publicField(RememberPlugin, "dynamicProviders", (options) => {
|
|
1574
|
-
const providers = [];
|
|
1575
|
-
const config = {
|
|
1576
|
-
...RememberPlugin.defaultOptions,
|
|
1577
|
-
...withCheckedOptions(options)
|
|
1578
|
-
};
|
|
1579
|
-
switch (config.type) {
|
|
1580
|
-
case "global-store":
|
|
1581
|
-
providers.push({
|
|
1582
|
-
name: "remember:store:global",
|
|
1583
|
-
provide: RememberStoreToken,
|
|
1584
|
-
inject: () => [FrontMcpConfig],
|
|
1585
|
-
useFactory: (frontmcpConfig) => {
|
|
1586
|
-
const storeConfig = getGlobalStoreConfig("RememberPlugin", frontmcpConfig);
|
|
1587
|
-
if (isVercelKvProvider(storeConfig)) {
|
|
1588
|
-
return new RememberVercelKvProvider({
|
|
1589
|
-
url: storeConfig.url,
|
|
1590
|
-
token: storeConfig.token,
|
|
1591
|
-
keyPrefix: config.keyPrefix,
|
|
1592
|
-
defaultTTL: config.defaultTTL
|
|
1593
|
-
});
|
|
1594
|
-
}
|
|
1595
|
-
return new RememberRedisProvider({
|
|
1596
|
-
type: "redis",
|
|
1597
|
-
config: {
|
|
1598
|
-
host: storeConfig.host ?? "localhost",
|
|
1599
|
-
port: storeConfig.port ?? 6379,
|
|
1600
|
-
password: storeConfig.password,
|
|
1601
|
-
db: storeConfig.db
|
|
1602
|
-
},
|
|
1603
|
-
keyPrefix: config.keyPrefix,
|
|
1604
|
-
defaultTTL: config.defaultTTL
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
});
|
|
1608
|
-
break;
|
|
1609
|
-
case "redis":
|
|
1610
|
-
if ("config" in config && config.config) {
|
|
1611
|
-
providers.push({
|
|
1612
|
-
name: "remember:store:redis",
|
|
1613
|
-
provide: RememberStoreToken,
|
|
1614
|
-
useValue: new RememberRedisProvider({
|
|
1615
|
-
type: "redis",
|
|
1616
|
-
config: config.config,
|
|
1617
|
-
keyPrefix: config.keyPrefix,
|
|
1618
|
-
defaultTTL: config.defaultTTL
|
|
1619
|
-
})
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
break;
|
|
1623
|
-
case "redis-client":
|
|
1624
|
-
if ("client" in config && config.client) {
|
|
1625
|
-
providers.push({
|
|
1626
|
-
name: "remember:store:redis-client",
|
|
1627
|
-
provide: RememberStoreToken,
|
|
1628
|
-
useValue: new RememberRedisProvider({
|
|
1629
|
-
type: "redis-client",
|
|
1630
|
-
client: config.client,
|
|
1631
|
-
keyPrefix: config.keyPrefix,
|
|
1632
|
-
defaultTTL: config.defaultTTL
|
|
1633
|
-
})
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
break;
|
|
1637
|
-
case "vercel-kv":
|
|
1638
|
-
providers.push({
|
|
1639
|
-
name: "remember:store:vercel-kv",
|
|
1640
|
-
provide: RememberStoreToken,
|
|
1641
|
-
useValue: new RememberVercelKvProvider({
|
|
1642
|
-
url: "url" in config ? config.url : void 0,
|
|
1643
|
-
token: "token" in config ? config.token : void 0,
|
|
1644
|
-
keyPrefix: config.keyPrefix,
|
|
1645
|
-
defaultTTL: config.defaultTTL
|
|
1646
|
-
})
|
|
1647
|
-
});
|
|
1648
|
-
break;
|
|
1649
|
-
case "memory":
|
|
1650
|
-
default:
|
|
1651
|
-
providers.push({
|
|
1652
|
-
name: "remember:store:memory",
|
|
1653
|
-
provide: RememberStoreToken,
|
|
1654
|
-
// Built per server: `init()` runs once, so a value here would be shared by every server using it.
|
|
1655
|
-
inject: () => [],
|
|
1656
|
-
useFactory: () => new RememberMemoryProvider(void 0, config.defaultTTL)
|
|
1657
|
-
});
|
|
1658
|
-
break;
|
|
1659
|
-
}
|
|
1660
|
-
providers.push({
|
|
1661
|
-
name: "remember:config",
|
|
1662
|
-
provide: RememberConfigToken,
|
|
1663
|
-
useValue: config
|
|
1664
|
-
});
|
|
1665
|
-
providers.push({
|
|
1666
|
-
name: "remember:accessor",
|
|
1667
|
-
provide: RememberAccessorToken,
|
|
1668
|
-
scope: ProviderScope5.CONTEXT,
|
|
1669
|
-
inject: () => [RememberStoreToken, FRONTMCP_CONTEXT, RememberConfigToken, FrontMcpLogger],
|
|
1670
|
-
useFactory: (store, ctx, cfg, logger) => createRememberAccessor(store, ctx, cfg, logger)
|
|
1671
|
-
});
|
|
1672
|
-
return providers;
|
|
1673
|
-
});
|
|
1674
|
-
RememberPlugin = __decorateClass([
|
|
1675
|
-
Plugin({
|
|
1676
|
-
name: "remember",
|
|
1677
|
-
description: "Help your LLM remember things across sessions",
|
|
1678
|
-
providers: [
|
|
1679
|
-
// Default in-memory provider (overridden by dynamicProviders if configured). A factory, so each
|
|
1680
|
-
// server gets its own store: a value built here is one store for every server in the process.
|
|
1681
|
-
{
|
|
1682
|
-
name: "remember:store:memory",
|
|
1683
|
-
provide: RememberStoreToken,
|
|
1684
|
-
inject: () => [],
|
|
1685
|
-
useFactory: () => new RememberMemoryProvider()
|
|
1686
|
-
}
|
|
1687
|
-
],
|
|
1688
|
-
// Context extensions - SDK handles runtime installation
|
|
1689
|
-
// TypeScript types are declared in remember.context-extension.ts
|
|
1690
|
-
contextExtensions: [
|
|
1691
|
-
{
|
|
1692
|
-
property: "remember",
|
|
1693
|
-
token: RememberAccessorToken,
|
|
1694
|
-
errorMessage: "RememberPlugin is not installed. Add RememberPlugin.init() to your plugins array."
|
|
1695
|
-
}
|
|
1696
|
-
]
|
|
1697
|
-
})
|
|
1698
|
-
], RememberPlugin);
|
|
1699
|
-
|
|
1700
|
-
// plugins/plugin-remember/src/remember.types.ts
|
|
1701
|
-
import { z as z6 } from "@frontmcp/lazy-zod";
|
|
1702
|
-
var rememberScopeSchema = z6.enum(["session", "user", "tool", "global"]);
|
|
1703
|
-
function brand(data, _brand) {
|
|
1704
|
-
return data;
|
|
1705
|
-
}
|
|
1706
|
-
var rememberEntrySchema = z6.object({
|
|
1707
|
-
value: z6.unknown(),
|
|
1708
|
-
brand: z6.enum(["approval", "preference", "cache", "state", "conversation", "custom"]).optional(),
|
|
1709
|
-
createdAt: z6.number(),
|
|
1710
|
-
updatedAt: z6.number(),
|
|
1711
|
-
expiresAt: z6.number().optional(),
|
|
1712
|
-
metadata: z6.record(z6.string(), z6.unknown()).optional()
|
|
1713
|
-
});
|
|
1714
|
-
|
|
1715
|
-
// plugins/plugin-remember/src/remember.context-extension.ts
|
|
1716
|
-
function getRemember(ctx) {
|
|
1717
|
-
return ctx.get(RememberAccessorToken);
|
|
1718
|
-
}
|
|
1719
|
-
function tryGetRemember(ctx) {
|
|
1720
|
-
if (typeof ctx.tryGet === "function") {
|
|
1721
|
-
return ctx.tryGet(RememberAccessorToken);
|
|
1722
|
-
}
|
|
1723
|
-
return void 0;
|
|
1724
|
-
}
|
|
1725
|
-
export {
|
|
1726
|
-
ForgetTool,
|
|
1727
|
-
ListMemoriesTool,
|
|
1728
|
-
RecallTool,
|
|
1729
|
-
RememberAccessor,
|
|
1730
|
-
RememberAccessorToken,
|
|
1731
|
-
RememberConfigToken,
|
|
1732
|
-
RememberConfigurationError,
|
|
1733
|
-
RememberIdentityError,
|
|
1734
|
-
RememberMemoryProvider,
|
|
1735
|
-
RememberPlugin,
|
|
1736
|
-
RememberRedisProvider,
|
|
1737
|
-
RememberScopeNotAllowedError,
|
|
1738
|
-
RememberStoreToken,
|
|
1739
|
-
RememberThisTool,
|
|
1740
|
-
RememberVercelKvProvider,
|
|
1741
|
-
brand,
|
|
1742
|
-
RememberPlugin as default,
|
|
1743
|
-
getRemember,
|
|
1744
|
-
tryGetRemember
|
|
1745
|
-
};
|
|
5
|
+
`;var RememberRedisProvider=class{client;keyPrefix;defaultTTL;ownsClient;constructor(options){this.keyPrefix=options.keyPrefix??"";this.defaultTTL=options.defaultTTL;if(options.type==="redis-client"){this.client=options.client;this.ownsClient=false;return}this.ownsClient=true;this.client=new Redis({lazyConnect:false,maxRetriesPerRequest:3,...options.config});this.client.on("connect",()=>{if(process.env["DEBUG"]==="true"){console.log("[RememberPlugin:Redis] Connected")}});this.client.on("error",err=>{console.error("[RememberPlugin:Redis] Error:",err.message)})}async setValue(key,value,ttlSeconds){if(value===void 0){throw new Error("Cannot store undefined value. Use null or delete the key instead.")}if(ttlSeconds!==void 0){if(typeof ttlSeconds!=="number"||!Number.isFinite(ttlSeconds)){throw new Error(`Invalid TTL: expected a number, got ${typeof ttlSeconds}`)}if(ttlSeconds<=0){throw new Error(`Invalid TTL: must be positive, got ${ttlSeconds}`)}if(!Number.isInteger(ttlSeconds)){throw new Error(`Invalid TTL: must be an integer, got ${ttlSeconds}`)}}const fullKey=prefixedStoreKey(this.keyPrefix,key);const strValue=JSON.stringify(value);const ttl=ttlSeconds??this.defaultTTL;if(ttl!==void 0&&ttl>0){await this.client.set(fullKey,strValue,"EX",ttl)}else{await this.client.set(fullKey,strValue)}await this.deleteDoubledPrefixKey(key)}async setIfAbsent(key,value,ttlSeconds){const fullKey=prefixedStoreKey(this.keyPrefix,key);const strValue=JSON.stringify(value);const ttl=ttlSeconds??this.defaultTTL;const hasTtl=ttl!==void 0&&ttl>0;const doubledKey=doubledPrefixKey(this.keyPrefix,key);if(doubledKey){const ttlArgument=hasTtl?String(ttl):"";const created=await this.client.eval(SET_IF_NEITHER_KEY_EXISTS_SCRIPT,2,fullKey,doubledKey,strValue,ttlArgument);return created===1}const result=hasTtl?await this.client.set(fullKey,strValue,"EX",ttl,"NX"):await this.client.set(fullKey,strValue,"NX");return result==="OK"}async getValue(key,defaultValue){const raw=await this.client.get(prefixedStoreKey(this.keyPrefix,key))??await this.readDoubledPrefixValue(key);if(raw===null)return defaultValue;try{return JSON.parse(raw)}catch{return defaultValue}}async delete(key){await this.client.del(prefixedStoreKey(this.keyPrefix,key));await this.deleteDoubledPrefixKey(key)}async exists(key){if(await this.client.exists(prefixedStoreKey(this.keyPrefix,key))===1)return true;const doubledKey=doubledPrefixKey(this.keyPrefix,key);return doubledKey!==void 0&&await this.client.exists(doubledKey)===1}async keys(pattern="*"){const result=new Set;for(const key of await this.scan(prefixedStoreKey(this.keyPrefix,pattern))){result.add(callerKeyOf(this.keyPrefix,pattern,key))}const doubledPattern=doubledPrefixKey(this.keyPrefix,pattern);if(doubledPattern){for(const key of await this.scan(doubledPattern))result.add(key.slice(this.keyPrefix.length))}return[...result]}async scan(match){const found=[];let cursor="0";do{const[nextCursor,keys]=await this.client.scan(cursor,"MATCH",match,"COUNT",100);cursor=nextCursor;found.push(...keys)}while(cursor!=="0");return found}async readDoubledPrefixValue(key){const doubledKey=doubledPrefixKey(this.keyPrefix,key);return doubledKey?this.client.get(doubledKey):null}async deleteDoubledPrefixKey(key){const doubledKey=doubledPrefixKey(this.keyPrefix,key);if(doubledKey)await this.client.del(doubledKey)}async close(){if(this.ownsClient){await this.client.quit()}}};RememberRedisProvider=__decorateClass([Provider3({name:"provider:remember:redis",description:"Redis-based storage provider for RememberPlugin",scope:ProviderScope3.GLOBAL})],RememberRedisProvider);import{Provider as Provider4,ProviderScope as ProviderScope4}from"@frontmcp/sdk";import{createVercelKvClient}from"@frontmcp/utils";var RememberVercelKvProvider=class{connection;client;keyPrefix;defaultTTL;constructor(options={}){const hasUrl=options.url!==void 0;const hasToken=options.token!==void 0;if(hasUrl!==hasToken){throw new Error(`RememberVercelKvProvider: Both 'url' and 'token' must be provided together, or neither. Received: url=${hasUrl?"provided":"missing"}, token=${hasToken?"provided":"missing"}`)}this.connection={url:options.url,token:options.token};this.keyPrefix=options.keyPrefix??"remember:";this.defaultTTL=options.defaultTTL}kv(){this.client??=createVercelKvClient(this.connection).catch(error=>{this.client=void 0;throw error});return this.client}prefixKey(key){return prefixedStoreKey(this.keyPrefix,key)}async setValue(key,value,ttlSeconds){const fullKey=this.prefixKey(key);const strValue=JSON.stringify(value);const ttl=ttlSeconds??this.defaultTTL;const kv=await this.kv();if(ttl&&ttl>0){await kv.set(fullKey,strValue,{ex:ttl})}else{await kv.set(fullKey,strValue)}await this.deleteDoubledPrefixKey(key)}async setIfAbsent(key,value,ttlSeconds){const fullKey=this.prefixKey(key);const strValue=JSON.stringify(value);const ttl=ttlSeconds??this.defaultTTL;const doubledKey=doubledPrefixKey(this.keyPrefix,key);const kv=await this.kv();if(doubledKey){const ttlArgument=ttl&&ttl>0?String(ttl):"";const created=await kv.eval(SET_IF_NEITHER_KEY_EXISTS_SCRIPT,[fullKey,doubledKey],[strValue,ttlArgument]);return Number(created)===1}const result=ttl&&ttl>0?await kv.set(fullKey,strValue,{nx:true,ex:ttl}):await kv.set(fullKey,strValue,{nx:true});return result==="OK"}async getValue(key,defaultValue){const kv=await this.kv();const raw=await kv.get(this.prefixKey(key))??await this.readDoubledPrefixValue(key);if(raw===null)return defaultValue;try{return JSON.parse(raw)}catch{return raw}}async delete(key){const kv=await this.kv();await kv.del(this.prefixKey(key));await this.deleteDoubledPrefixKey(key)}async exists(key){const kv=await this.kv();if(await kv.exists(this.prefixKey(key))===1)return true;const doubledKey=doubledPrefixKey(this.keyPrefix,key);return doubledKey!==void 0&&await kv.exists(doubledKey)===1}async keys(pattern="*"){const result=new Set;for(const key of await this.scan(this.prefixKey(pattern))){result.add(callerKeyOf(this.keyPrefix,pattern,key))}const doubledPattern=doubledPrefixKey(this.keyPrefix,pattern);if(doubledPattern){for(const key of await this.scan(doubledPattern))result.add(key.slice(this.keyPrefix.length))}return[...result]}async scan(match){const found=[];const kv=await this.kv();try{let cursor=0;do{const[nextCursor,keys]=await kv.scan(cursor,{match,count:100});cursor=nextCursor;found.push(...keys)}while(String(cursor)!=="0")}catch{try{found.push(...await kv.keys(match))}catch{console.warn("[RememberPlugin:VercelKV] keys() operation not supported")}}return found}async readDoubledPrefixValue(key){const doubledKey=doubledPrefixKey(this.keyPrefix,key);if(!doubledKey)return null;const kv=await this.kv();return kv.get(doubledKey)}async deleteDoubledPrefixKey(key){const doubledKey=doubledPrefixKey(this.keyPrefix,key);if(!doubledKey)return;const kv=await this.kv();await kv.del(doubledKey)}async close(){}};RememberVercelKvProvider=__decorateClass([Provider4({name:"provider:remember:vercel-kv",description:"Vercel KV-based storage provider for RememberPlugin",scope:ProviderScope4.GLOBAL})],RememberVercelKvProvider);var RememberStoreToken=Symbol("plugin:remember:store");var RememberConfigToken=Symbol("plugin:remember:config");var RememberAccessorToken=Symbol("plugin:remember:accessor");import{Tool as Tool5}from"@frontmcp/sdk";import{z as z2}from"@frontmcp/lazy-zod";import{Tool,ToolContext}from"@frontmcp/sdk";var REMEMBER_SCOPE_DESCRIPTION="Whose memory to use (default: session). session: this session; without one (stateless HTTP, MCP 2026-07-28), the signed-in caller across its requests. user: the signed-in caller, across all of its sessions. tool: the tool running the call, for the same caller as session; each memory tool has its own, so the other memory tools do not see what one of them stores in tool scope. global: shared by every caller. An anonymous caller cannot use user scope, nor session or tool scope without a session.";function rememberToolNames(prefix=""){return{rememberThis:`${prefix}remember_this`,recall:`${prefix}recall`,forget:`${prefix}forget`,listMemories:`${prefix}list_memories`}}var forgetInputSchema={key:z2.string().min(1).describe("What memory to forget"),scope:z2.enum(["session","user","tool","global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION)};var forgetOutputSchema=z2.object({success:z2.boolean(),key:z2.string(),scope:z2.string(),existed:z2.boolean()});function forgetDescription(){return"Forget a previously remembered value. Use this when the user wants to delete stored preferences or information."}function forgetToolMetadata(names=rememberToolNames()){return{name:names.forget,description:forgetDescription(),inputSchema:forgetInputSchema,outputSchema:forgetOutputSchema,annotations:{readOnlyHint:false}}}var ForgetTool=class extends ToolContext{async execute(input){const remember=this.get(RememberAccessorToken);const config=this.get(RememberConfigToken);const scope=input.scope??"session";const allowedScopes=config.tools?.allowedScopes??["session","user","tool","global"];if(!allowedScopes.includes(scope)){throw this.fail(new RememberScopeNotAllowedError(scope,allowedScopes))}const existed=await remember.knows(input.key,{scope});await remember.forget(input.key,{scope});return{success:true,key:input.key,scope,existed}}};ForgetTool=__decorateClass([Tool(forgetToolMetadata())],ForgetTool);import{z as z3}from"@frontmcp/lazy-zod";import{Tool as Tool2,ToolContext as ToolContext2}from"@frontmcp/sdk";var listMemoriesInputSchema={scope:z3.enum(["session","user","tool","global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION),pattern:z3.string().optional().describe('Pattern to filter keys (e.g., "user_*")'),limit:z3.number().positive().max(100).optional().describe("Maximum number of keys to return (default: 50)")};var listMemoriesOutputSchema=z3.object({keys:z3.array(z3.string()),scope:z3.string(),count:z3.number(),truncated:z3.boolean()});function listMemoriesDescription(names=rememberToolNames()){return`List the remembered keys in one scope (default: session). Use this to see what memories are stored before you ${names.recall} or ${names.forget} one.`}function listMemoriesToolMetadata(names=rememberToolNames()){return{name:names.listMemories,description:listMemoriesDescription(names),inputSchema:listMemoriesInputSchema,outputSchema:listMemoriesOutputSchema,annotations:{readOnlyHint:true}}}var ListMemoriesTool=class extends ToolContext2{async execute(input){const remember=this.get(RememberAccessorToken);const config=this.get(RememberConfigToken);const scope=input.scope??"session";const allowedScopes=config.tools?.allowedScopes??["session","user","tool","global"];if(!allowedScopes.includes(scope)){throw this.fail(new RememberScopeNotAllowedError(scope,allowedScopes))}const limit=input.limit??50;let keys=await remember.list({scope,pattern:input.pattern});const truncated=keys.length>limit;if(truncated){keys=keys.slice(0,limit)}return{keys,scope,count:keys.length,truncated}}};ListMemoriesTool=__decorateClass([Tool2(listMemoriesToolMetadata())],ListMemoriesTool);import{z as z4}from"@frontmcp/lazy-zod";import{Tool as Tool3,ToolContext as ToolContext3}from"@frontmcp/sdk";var recallInputSchema={key:z4.string().min(1).describe("What memory to recall"),scope:z4.enum(["session","user","tool","global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION)};var recallOutputSchema=z4.object({found:z4.boolean(),key:z4.string(),value:z4.unknown().optional(),scope:z4.string(),createdAt:z4.number().optional(),expiresAt:z4.number().optional()});function recallDescription(names=rememberToolNames()){return`Recall something that was previously remembered. Use this to retrieve stored preferences, settings, or any information that was saved with ${names.rememberThis}.`}function recallToolMetadata(names=rememberToolNames()){return{name:names.recall,description:recallDescription(names),inputSchema:recallInputSchema,outputSchema:recallOutputSchema,annotations:{readOnlyHint:true}}}var RecallTool=class extends ToolContext3{async execute(input){const remember=this.get(RememberAccessorToken);const config=this.get(RememberConfigToken);const scope=input.scope??"session";const allowedScopes=config.tools?.allowedScopes??["session","user","tool","global"];if(!allowedScopes.includes(scope)){throw this.fail(new RememberScopeNotAllowedError(scope,allowedScopes))}const entry=await remember.getEntry(input.key,{scope});if(!entry){return{found:false,key:input.key,scope}}return{found:true,key:input.key,value:entry.value,scope,createdAt:entry.createdAt,expiresAt:entry.expiresAt}}};RecallTool=__decorateClass([Tool3(recallToolMetadata())],RecallTool);import{z as z5}from"@frontmcp/lazy-zod";import{Tool as Tool4,ToolContext as ToolContext4}from"@frontmcp/sdk";var rememberThisInputSchema={key:z5.string().min(1).describe('What to call this memory (e.g., "user_preference", "last_action")'),value:z5.unknown().describe("The value to remember (any JSON-serializable data)"),scope:z5.enum(["session","user","tool","global"]).optional().describe(REMEMBER_SCOPE_DESCRIPTION),ttl:z5.number().int().positive().optional().describe("Forget after this many whole seconds"),brand:z5.enum(["preference","cache","state","conversation","custom"]).optional().describe("Type hint for the stored value")};var rememberThisOutputSchema=z5.object({success:z5.boolean(),key:z5.string(),scope:z5.string(),expiresAt:z5.number().optional()});function rememberThisDescription(names=rememberToolNames()){return`Remember something for later: store a value under a key that ${names.recall} can read back, in the scope you choose (this session by default; user scope for the signed-in user across sessions). It lasts until it is forgotten or its ttl runs out. Use this when the user asks you to remember preferences, settings, or any information they want to persist.`}function rememberThisToolMetadata(names=rememberToolNames()){return{name:names.rememberThis,description:rememberThisDescription(names),inputSchema:rememberThisInputSchema,outputSchema:rememberThisOutputSchema,annotations:{readOnlyHint:false}}}var RememberThisTool=class extends ToolContext4{async execute(input){const remember=this.get(RememberAccessorToken);const config=this.get(RememberConfigToken);const scope=input.scope??"session";const allowedScopes=config.tools?.allowedScopes??["session","user","tool","global"];if(!allowedScopes.includes(scope)){throw this.fail(new RememberScopeNotAllowedError(scope,allowedScopes))}const entry=await remember.set(input.key,input.value,{scope,ttl:input.ttl,brand:input.brand});return{success:true,key:input.key,scope,expiresAt:entry.expiresAt}}};RememberThisTool=__decorateClass([Tool4(rememberThisToolMetadata())],RememberThisTool);function createRememberTools(prefix=""){if(!prefix)return[RememberThisTool,RecallTool,ForgetTool,ListMemoriesTool];const names=rememberToolNames(prefix);let PrefixedRememberThisTool=class extends RememberThisTool{};PrefixedRememberThisTool=__decorateClass([Tool5(rememberThisToolMetadata(names))],PrefixedRememberThisTool);let PrefixedRecallTool=class extends RecallTool{};PrefixedRecallTool=__decorateClass([Tool5(recallToolMetadata(names))],PrefixedRecallTool);let PrefixedForgetTool=class extends ForgetTool{};PrefixedForgetTool=__decorateClass([Tool5(forgetToolMetadata(names))],PrefixedForgetTool);let PrefixedListMemoriesTool=class extends ListMemoriesTool{};PrefixedListMemoriesTool=__decorateClass([Tool5(listMemoriesToolMetadata(names))],PrefixedListMemoriesTool);return[PrefixedRememberThisTool,PrefixedRecallTool,PrefixedForgetTool,PrefixedListMemoriesTool]}function withCheckedOptions(options){const customKey=options.encryption?.customKey;if(customKey!==void 0&&customKey.trim()===""){throw new RememberConfigurationError("RememberPlugin encryption.customKey must not be empty; omit it to derive keys from the server secret")}const{defaultTTL}=options;if(defaultTTL===void 0)return options;if(defaultTTL===0)return{...options,defaultTTL:void 0};if(!Number.isInteger(defaultTTL)||defaultTTL<0){throw new RememberConfigurationError(`RememberPlugin defaultTTL must be a whole number of seconds (0 for no default expiry), got ${defaultTTL}`)}return options}var RememberPlugin=class extends DynamicPlugin{options;constructor(options={}){super();this.options={...RememberPlugin.defaultOptions,...withCheckedOptions(options)}}};__publicField(RememberPlugin,"defaultOptions",{type:"memory",keyPrefix:"remember:",encryption:{enabled:true}});__publicField(RememberPlugin,"dynamicTools",options=>options.tools?.enabled?createRememberTools(options.tools.prefix):[]);__publicField(RememberPlugin,"dynamicProviders",options=>{const providers=[];const config={...RememberPlugin.defaultOptions,...withCheckedOptions(options)};switch(config.type){case"global-store":providers.push({name:"remember:store:global",provide:RememberStoreToken,inject:()=>[FrontMcpConfig],useFactory:frontmcpConfig=>{const storeConfig=getGlobalStoreConfig("RememberPlugin",frontmcpConfig);if(isVercelKvProvider(storeConfig)){return new RememberVercelKvProvider({url:storeConfig.url,token:storeConfig.token,keyPrefix:config.keyPrefix,defaultTTL:config.defaultTTL})}return new RememberRedisProvider({type:"redis",config:{host:storeConfig.host??"localhost",port:storeConfig.port??6379,password:storeConfig.password,db:storeConfig.db},keyPrefix:config.keyPrefix,defaultTTL:config.defaultTTL})}});break;case"redis":if("config"in config&&config.config){providers.push({name:"remember:store:redis",provide:RememberStoreToken,useValue:new RememberRedisProvider({type:"redis",config:config.config,keyPrefix:config.keyPrefix,defaultTTL:config.defaultTTL})})}break;case"redis-client":if("client"in config&&config.client){providers.push({name:"remember:store:redis-client",provide:RememberStoreToken,useValue:new RememberRedisProvider({type:"redis-client",client:config.client,keyPrefix:config.keyPrefix,defaultTTL:config.defaultTTL})})}break;case"vercel-kv":providers.push({name:"remember:store:vercel-kv",provide:RememberStoreToken,useValue:new RememberVercelKvProvider({url:"url"in config?config.url:void 0,token:"token"in config?config.token:void 0,keyPrefix:config.keyPrefix,defaultTTL:config.defaultTTL})});break;case"memory":default:providers.push({name:"remember:store:memory",provide:RememberStoreToken,inject:()=>[],useFactory:()=>new RememberMemoryProvider(void 0,config.defaultTTL)});break}providers.push({name:"remember:config",provide:RememberConfigToken,useValue:config});providers.push({name:"remember:accessor",provide:RememberAccessorToken,scope:ProviderScope5.CONTEXT,inject:()=>[RememberStoreToken,FRONTMCP_CONTEXT,RememberConfigToken,FrontMcpLogger],useFactory:(store,ctx,cfg,logger)=>createRememberAccessor(store,ctx,cfg,logger)});return providers});RememberPlugin=__decorateClass([Plugin({name:"remember",description:"Help your LLM remember things across sessions",providers:[{name:"remember:store:memory",provide:RememberStoreToken,inject:()=>[],useFactory:()=>new RememberMemoryProvider}],contextExtensions:[{property:"remember",token:RememberAccessorToken,errorMessage:"RememberPlugin is not installed. Add RememberPlugin.init() to your plugins array."}]})],RememberPlugin);import{z as z6}from"@frontmcp/lazy-zod";var rememberScopeSchema=z6.enum(["session","user","tool","global"]);function brand(data,_brand){return data}var rememberEntrySchema=z6.object({value:z6.unknown(),brand:z6.enum(["approval","preference","cache","state","conversation","custom"]).optional(),createdAt:z6.number(),updatedAt:z6.number(),expiresAt:z6.number().optional(),metadata:z6.record(z6.string(),z6.unknown()).optional()});function getRemember(ctx){return ctx.get(RememberAccessorToken)}function tryGetRemember(ctx){if(typeof ctx.tryGet==="function"){return ctx.tryGet(RememberAccessorToken)}return void 0}export{ForgetTool,ListMemoriesTool,RecallTool,RememberAccessor,RememberAccessorToken,RememberConfigToken,RememberConfigurationError,RememberIdentityError,RememberMemoryProvider,RememberPlugin,RememberRedisProvider,RememberScopeNotAllowedError,RememberStoreToken,RememberThisTool,RememberVercelKvProvider,brand,RememberPlugin as default,getRemember,tryGetRemember};
|