vplex-memory 2.4.1 → 2.4.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/README.md +5 -5
- package/package.json +1 -1
- package/vplex-mcp-server.mjs +149 -21
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Works with Claude Code, Cursor, VS Code Copilot, Windsurf, Codex, and any MCP-co
|
|
|
11
11
|
"mcpServers": {
|
|
12
12
|
"vplex-memory": {
|
|
13
13
|
"command": "npx",
|
|
14
|
-
"args": ["-y", "vplex-memory@2.4.
|
|
14
|
+
"args": ["-y", "vplex-memory@2.4.2"]
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -30,7 +30,7 @@ Add to `.mcp.json` in your project root or `~/.claude/settings.json` globally:
|
|
|
30
30
|
"mcpServers": {
|
|
31
31
|
"vplex-memory": {
|
|
32
32
|
"command": "npx",
|
|
33
|
-
"args": ["-y", "vplex-memory@2.4.
|
|
33
|
+
"args": ["-y", "vplex-memory@2.4.2"]
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -46,7 +46,7 @@ Add to `.cursor/mcp.json`:
|
|
|
46
46
|
"vplex-memory": {
|
|
47
47
|
"type": "stdio",
|
|
48
48
|
"command": "npx",
|
|
49
|
-
"args": ["-y", "vplex-memory@2.4.
|
|
49
|
+
"args": ["-y", "vplex-memory@2.4.2"]
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -62,7 +62,7 @@ Add to `.vscode/mcp.json`:
|
|
|
62
62
|
"vplex-memory": {
|
|
63
63
|
"type": "stdio",
|
|
64
64
|
"command": "npx",
|
|
65
|
-
"args": ["-y", "vplex-memory@2.4.
|
|
65
|
+
"args": ["-y", "vplex-memory@2.4.2"]
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -78,7 +78,7 @@ Add to `~/.windsurf/mcp.json`:
|
|
|
78
78
|
"vplex-memory": {
|
|
79
79
|
"type": "stdio",
|
|
80
80
|
"command": "npx",
|
|
81
|
-
"args": ["-y", "vplex-memory@2.4.
|
|
81
|
+
"args": ["-y", "vplex-memory@2.4.2"]
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}
|
package/package.json
CHANGED
package/vplex-mcp-server.mjs
CHANGED
|
@@ -209,7 +209,7 @@ const TOKEN_CACHE_MS = 30_000; // re-read from disk every 30s
|
|
|
209
209
|
const TOKEN_REFRESH_MARGIN_S = 120; // refresh if < 2 min until expiry
|
|
210
210
|
|
|
211
211
|
// CLI auth flow state
|
|
212
|
-
let cliAuthPending = null; // { sessionId, userCode,
|
|
212
|
+
let cliAuthPending = null; // { sessionId, userCode, autoApproveUrl, manualUrl, pollInterval, expiresAt }
|
|
213
213
|
|
|
214
214
|
function getSessionPath() {
|
|
215
215
|
return join(homedir(), ".vplex", "session.json");
|
|
@@ -373,44 +373,40 @@ function startCliAuthPolling(sessionId) {
|
|
|
373
373
|
|
|
374
374
|
/**
|
|
375
375
|
* Starts CLI auth flow if no token is available.
|
|
376
|
-
*
|
|
376
|
+
* Uses the auto-approve page — if the user is logged in via browser cookie,
|
|
377
|
+
* the session is approved automatically (no manual code entry needed).
|
|
378
|
+
* Falls back to manual code entry URL if auto-approve is not possible.
|
|
377
379
|
*/
|
|
378
380
|
async function startCliAuthFlow() {
|
|
379
381
|
// Don't start if already pending
|
|
380
382
|
if (cliAuthPending && Date.now() < cliAuthPending.expiresAt) {
|
|
381
|
-
return `Not authenticated. Open ${cliAuthPending.
|
|
383
|
+
return `Not authenticated. Open this URL to sign in:\n${cliAuthPending.autoApproveUrl}\n\nIf that doesn't work, open ${cliAuthPending.manualUrl} and enter code: ${cliAuthPending.userCode}`;
|
|
382
384
|
}
|
|
383
385
|
|
|
384
386
|
const result = await initCliAuth();
|
|
385
387
|
if (!result?.cli_session_id) {
|
|
386
|
-
return
|
|
388
|
+
return `Not authenticated. Auth service unavailable — please try again in a moment, or sign in at ${WEB_URL}/auth/signin manually.`;
|
|
387
389
|
}
|
|
388
390
|
|
|
389
391
|
const pollInterval = startCliAuthPolling(result.cli_session_id);
|
|
390
392
|
|
|
393
|
+
const autoApproveUrl = `${WEB_URL}/auth/device-approve?session_id=${result.cli_session_id}`;
|
|
394
|
+
const manualUrl = result.verification_url || `${WEB_URL}/auth/cli`;
|
|
395
|
+
|
|
391
396
|
cliAuthPending = {
|
|
392
397
|
sessionId: result.cli_session_id,
|
|
393
398
|
userCode: result.user_code,
|
|
394
|
-
|
|
399
|
+
autoApproveUrl,
|
|
400
|
+
manualUrl,
|
|
395
401
|
pollInterval,
|
|
396
402
|
expiresAt: Date.now() + (result.expires_in || 600) * 1000,
|
|
397
403
|
};
|
|
398
404
|
|
|
399
|
-
return `Not authenticated. Open ${
|
|
405
|
+
return `Not authenticated. Open this URL to sign in:\n${autoApproveUrl}\n\nIf that doesn't work, open ${manualUrl} and enter code: ${cliAuthPending.userCode}`;
|
|
400
406
|
}
|
|
401
407
|
|
|
402
408
|
// ── Project Identification ──────────────────────────────────────────
|
|
403
409
|
|
|
404
|
-
function hashString(str) {
|
|
405
|
-
let hash = 0;
|
|
406
|
-
for (let i = 0; i < str.length; i++) {
|
|
407
|
-
const char = str.charCodeAt(i);
|
|
408
|
-
hash = ((hash << 5) - hash) + char;
|
|
409
|
-
hash |= 0;
|
|
410
|
-
}
|
|
411
|
-
return Math.abs(hash).toString(36);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
410
|
// Detect project root: env override > .git traversal > cwd fallback
|
|
415
411
|
function detectProjectRoot() {
|
|
416
412
|
// Explicit override via env (set in MCP config)
|
|
@@ -435,10 +431,98 @@ function detectProjectRoot() {
|
|
|
435
431
|
return process.cwd();
|
|
436
432
|
}
|
|
437
433
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Read or create the project identifier from .vplex/project-id.
|
|
436
|
+
*
|
|
437
|
+
* This file is the single source of truth for project identity.
|
|
438
|
+
* It survives folder renames, git remote changes, moves, and re-clones
|
|
439
|
+
* (if committed to the repo).
|
|
440
|
+
*
|
|
441
|
+
* Priority:
|
|
442
|
+
* 1. .vplex/project-id file exists → read and use it
|
|
443
|
+
* 2. File doesn't exist → generate new ID, create .vplex/ dir + file
|
|
444
|
+
*/
|
|
445
|
+
function resolveProjectId(projectRoot) {
|
|
446
|
+
const vplexDir = join(projectRoot, ".vplex");
|
|
447
|
+
const idFile = join(vplexDir, "project-id");
|
|
448
|
+
|
|
449
|
+
// Try to read existing ID
|
|
450
|
+
try {
|
|
451
|
+
const id = readFileSync(idFile, "utf-8").trim();
|
|
452
|
+
if (id.length >= 4 && id.length <= 64) {
|
|
453
|
+
return id;
|
|
454
|
+
}
|
|
455
|
+
// Invalid content — regenerate
|
|
456
|
+
} catch {
|
|
457
|
+
// File doesn't exist — create it
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Generate new project ID
|
|
461
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
462
|
+
let id = "";
|
|
463
|
+
for (let i = 0; i < 12; i++) {
|
|
464
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
mkdirSync(vplexDir, { recursive: true });
|
|
469
|
+
writeFileSync(idFile, id + "\n", { encoding: "utf-8" });
|
|
470
|
+
process.stderr.write(`[vplex-mcp] Created .vplex/project-id: ${id}\n`);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
process.stderr.write(`[vplex-mcp] Warning: could not write .vplex/project-id: ${err.message}\n`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return id;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let detectedRoot = detectProjectRoot();
|
|
479
|
+
let projectHash = resolveProjectId(detectedRoot);
|
|
480
|
+
let projectName = detectedRoot.replace(/\\/g, "/").split("/").pop() || detectedRoot;
|
|
481
|
+
let projectPath = detectedRoot;
|
|
482
|
+
process.stderr.write(`[vplex-mcp] Project: ${projectName} | id: ${projectHash} | root: ${detectedRoot}\n`);
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Re-detect project from a new root path (e.g. from MCP client roots).
|
|
486
|
+
* Walks up from the given path looking for .git, then resolves project ID.
|
|
487
|
+
*/
|
|
488
|
+
function reinitProject(rootUri) {
|
|
489
|
+
let newRoot = rootUri;
|
|
490
|
+
// Convert file:// URI to path
|
|
491
|
+
if (newRoot.startsWith("file:///")) {
|
|
492
|
+
newRoot = newRoot.slice(8); // file:///C:/foo → C:/foo
|
|
493
|
+
// On Windows, file:///C:/foo → C:/foo (keep drive letter)
|
|
494
|
+
// On Unix, file:///home/user → /home/user
|
|
495
|
+
if (!/^\//.test(newRoot) && /^[a-zA-Z]:/.test(newRoot)) {
|
|
496
|
+
// Windows path — already correct
|
|
497
|
+
} else if (!newRoot.startsWith("/")) {
|
|
498
|
+
newRoot = "/" + newRoot;
|
|
499
|
+
}
|
|
500
|
+
} else if (newRoot.startsWith("file://")) {
|
|
501
|
+
newRoot = newRoot.slice(7);
|
|
502
|
+
}
|
|
503
|
+
// Decode URI components (spaces, special chars)
|
|
504
|
+
newRoot = decodeURIComponent(newRoot);
|
|
505
|
+
|
|
506
|
+
// Walk up from new root looking for .git
|
|
507
|
+
let dir = newRoot;
|
|
508
|
+
const rootDir = parse(dir).root;
|
|
509
|
+
while (dir !== rootDir) {
|
|
510
|
+
try {
|
|
511
|
+
statSync(join(dir, ".git"));
|
|
512
|
+
newRoot = dir;
|
|
513
|
+
break;
|
|
514
|
+
} catch { /* continue */ }
|
|
515
|
+
const parent = dirname(dir);
|
|
516
|
+
if (parent === dir) break;
|
|
517
|
+
dir = parent;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
detectedRoot = newRoot;
|
|
521
|
+
projectHash = resolveProjectId(detectedRoot);
|
|
522
|
+
projectName = detectedRoot.replace(/\\/g, "/").split("/").pop() || detectedRoot;
|
|
523
|
+
projectPath = detectedRoot;
|
|
524
|
+
process.stderr.write(`[vplex-mcp] Project updated: ${projectName} | id: ${projectHash} | root: ${detectedRoot}\n`);
|
|
525
|
+
}
|
|
442
526
|
|
|
443
527
|
// ── HTTP Helper ─────────────────────────────────────────────────────
|
|
444
528
|
|
|
@@ -682,6 +766,11 @@ const TOOLS = [
|
|
|
682
766
|
description: "Check authentication status: whether logged in, token expiry, and user info.",
|
|
683
767
|
inputSchema: { type: "object", properties: {} },
|
|
684
768
|
},
|
|
769
|
+
{
|
|
770
|
+
name: "memory_signin",
|
|
771
|
+
description: "Sign in to VPLEX Memory. Opens the browser-based auth flow — if already logged in via browser, approval is automatic (no code needed). Returns a URL to open.",
|
|
772
|
+
inputSchema: { type: "object", properties: {} },
|
|
773
|
+
},
|
|
685
774
|
{
|
|
686
775
|
name: "memory_logout",
|
|
687
776
|
description: "Log out by deleting the cached session. Next tool call will require re-authentication.",
|
|
@@ -1147,6 +1236,24 @@ async function handleToolCall(name, args) {
|
|
|
1147
1236
|
};
|
|
1148
1237
|
}
|
|
1149
1238
|
|
|
1239
|
+
case "memory_signin": {
|
|
1240
|
+
// Check if already authenticated
|
|
1241
|
+
const existingToken = await getToken();
|
|
1242
|
+
if (existingToken) {
|
|
1243
|
+
const session = readSession();
|
|
1244
|
+
const user = session?.user?.email || session?.user?.name || "unknown";
|
|
1245
|
+
return { content: [{ type: "text", text: `Already signed in as ${user}. Use memory_logout first to switch accounts.` }] };
|
|
1246
|
+
}
|
|
1247
|
+
// Start the CLI auth flow with auto-approve URL
|
|
1248
|
+
const authMessage = await startCliAuthFlow();
|
|
1249
|
+
return {
|
|
1250
|
+
content: [{
|
|
1251
|
+
type: "text",
|
|
1252
|
+
text: `${authMessage}\n\nOnce you sign in via the browser, this session will be automatically authenticated. The polling runs in the background — just try any memory tool again after signing in.`,
|
|
1253
|
+
}],
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1150
1257
|
case "memory_auth_status": {
|
|
1151
1258
|
const session = readSession();
|
|
1152
1259
|
if (!session?.token) {
|
|
@@ -1200,17 +1307,38 @@ async function handleRequest(request) {
|
|
|
1200
1307
|
|
|
1201
1308
|
// MCP notifications (no id, or notifications/* prefix) — never respond
|
|
1202
1309
|
if (id === undefined || id === null || method.startsWith("notifications/")) {
|
|
1310
|
+
// Handle roots/list_changed — client workspace changed
|
|
1311
|
+
if (method === "notifications/roots/list_changed" && params?.roots?.length > 0) {
|
|
1312
|
+
const rootUri = params.roots[0].uri || params.roots[0];
|
|
1313
|
+
if (typeof rootUri === "string" && rootUri.length > 0) {
|
|
1314
|
+
reinitProject(rootUri);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1203
1317
|
return null;
|
|
1204
1318
|
}
|
|
1205
1319
|
|
|
1206
1320
|
try {
|
|
1207
1321
|
switch (method) {
|
|
1208
1322
|
case "initialize":
|
|
1323
|
+
// Read workspace roots from client (e.g. VS Code workspace folders)
|
|
1324
|
+
if (params?.roots?.length > 0) {
|
|
1325
|
+
const rootUri = params.roots[0].uri || params.roots[0];
|
|
1326
|
+
process.stderr.write(`[vplex-mcp] Client root: ${typeof rootUri === "string" ? rootUri : JSON.stringify(rootUri)}\n`);
|
|
1327
|
+
if (typeof rootUri === "string" && rootUri.length > 0) {
|
|
1328
|
+
reinitProject(rootUri);
|
|
1329
|
+
}
|
|
1330
|
+
} else if (params?.workspaceFolders?.length > 0) {
|
|
1331
|
+
// Some clients send workspaceFolders instead of roots
|
|
1332
|
+
const folder = params.workspaceFolders[0].uri || params.workspaceFolders[0];
|
|
1333
|
+
if (typeof folder === "string" && folder.length > 0) {
|
|
1334
|
+
reinitProject(folder);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1209
1337
|
return {
|
|
1210
1338
|
jsonrpc: "2.0", id,
|
|
1211
1339
|
result: {
|
|
1212
1340
|
protocolVersion: "2024-11-05",
|
|
1213
|
-
capabilities: { tools: {} },
|
|
1341
|
+
capabilities: { tools: {}, roots: { listChanged: false } },
|
|
1214
1342
|
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
1215
1343
|
},
|
|
1216
1344
|
};
|