vplex-memory 2.4.2 → 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 +134 -24
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
|
|
|
@@ -684,7 +768,7 @@ const TOOLS = [
|
|
|
684
768
|
},
|
|
685
769
|
{
|
|
686
770
|
name: "memory_signin",
|
|
687
|
-
description: "Sign in to VPLEX Memory.
|
|
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.",
|
|
688
772
|
inputSchema: { type: "object", properties: {} },
|
|
689
773
|
},
|
|
690
774
|
{
|
|
@@ -1160,9 +1244,14 @@ async function handleToolCall(name, args) {
|
|
|
1160
1244
|
const user = session?.user?.email || session?.user?.name || "unknown";
|
|
1161
1245
|
return { content: [{ type: "text", text: `Already signed in as ${user}. Use memory_logout first to switch accounts.` }] };
|
|
1162
1246
|
}
|
|
1163
|
-
// Start the CLI auth flow
|
|
1247
|
+
// Start the CLI auth flow with auto-approve URL
|
|
1164
1248
|
const authMessage = await startCliAuthFlow();
|
|
1165
|
-
return {
|
|
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
|
+
};
|
|
1166
1255
|
}
|
|
1167
1256
|
|
|
1168
1257
|
case "memory_auth_status": {
|
|
@@ -1218,17 +1307,38 @@ async function handleRequest(request) {
|
|
|
1218
1307
|
|
|
1219
1308
|
// MCP notifications (no id, or notifications/* prefix) — never respond
|
|
1220
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
|
+
}
|
|
1221
1317
|
return null;
|
|
1222
1318
|
}
|
|
1223
1319
|
|
|
1224
1320
|
try {
|
|
1225
1321
|
switch (method) {
|
|
1226
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
|
+
}
|
|
1227
1337
|
return {
|
|
1228
1338
|
jsonrpc: "2.0", id,
|
|
1229
1339
|
result: {
|
|
1230
1340
|
protocolVersion: "2024-11-05",
|
|
1231
|
-
capabilities: { tools: {} },
|
|
1341
|
+
capabilities: { tools: {}, roots: { listChanged: false } },
|
|
1232
1342
|
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
1233
1343
|
},
|
|
1234
1344
|
};
|