reflect-mcp 1.0.14 → 1.0.16
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/dist/pkcehandler.d.ts +0 -2
- package/dist/pkcehandler.js +55 -82
- package/dist/server.js +1 -0
- package/dist/utils.js +11 -7
- package/package.json +2 -2
package/dist/pkcehandler.d.ts
CHANGED
|
@@ -28,8 +28,6 @@ export declare class PKCEOAuthProxy {
|
|
|
28
28
|
private tokens;
|
|
29
29
|
private recentlyExchangedCodes;
|
|
30
30
|
private cleanupInterval;
|
|
31
|
-
private tokenMutex;
|
|
32
|
-
private tokenMutexBusy;
|
|
33
31
|
private activeConnections;
|
|
34
32
|
constructor(options: PKCEOAuthProxyConfig);
|
|
35
33
|
private loadTokensFromDisk;
|
package/dist/pkcehandler.js
CHANGED
|
@@ -74,10 +74,6 @@ export class PKCEOAuthProxy {
|
|
|
74
74
|
// Track tokens that have been exchanged but allow brief retry window
|
|
75
75
|
recentlyExchangedCodes = new Map();
|
|
76
76
|
cleanupInterval = null;
|
|
77
|
-
// In-memory mutex to prevent race conditions during token operations
|
|
78
|
-
// This ensures only one client can exchange a code at a time
|
|
79
|
-
tokenMutex = new Map();
|
|
80
|
-
tokenMutexBusy = false;
|
|
81
77
|
// Active connections counter for debugging
|
|
82
78
|
activeConnections = 0;
|
|
83
79
|
constructor(options) {
|
|
@@ -135,7 +131,7 @@ export class PKCEOAuthProxy {
|
|
|
135
131
|
await globalWriteQueue.add(async () => {
|
|
136
132
|
await fs.promises.writeFile(this.config.tokenStoragePath, JSON.stringify(toStore, null, 2), "utf-8");
|
|
137
133
|
});
|
|
138
|
-
console.log(`[PKCEProxy] Saved ${toStore.length} tokens to disk`);
|
|
134
|
+
console.log(`[PKCEProxy] Saved ${Object.keys(toStore).length} tokens to disk`);
|
|
139
135
|
}
|
|
140
136
|
catch (error) {
|
|
141
137
|
console.error("[PKCEProxy] Failed to save tokens to disk:", error);
|
|
@@ -190,7 +186,7 @@ export class PKCEOAuthProxy {
|
|
|
190
186
|
await globalWriteQueue.add(async () => {
|
|
191
187
|
await fs.promises.writeFile(this.config.transactionStoragePath, JSON.stringify(toStore, null, 2), "utf-8");
|
|
192
188
|
});
|
|
193
|
-
console.log(`[PKCEProxy] Saved ${toStore.length} transactions to disk`);
|
|
189
|
+
console.log(`[PKCEProxy] Saved ${Object.keys(toStore).length} transactions to disk`);
|
|
194
190
|
}
|
|
195
191
|
catch (error) {
|
|
196
192
|
console.error("[PKCEProxy] Failed to save transactions to disk:", error);
|
|
@@ -232,6 +228,23 @@ export class PKCEOAuthProxy {
|
|
|
232
228
|
headers: { "Content-Type": "application/json" },
|
|
233
229
|
});
|
|
234
230
|
}
|
|
231
|
+
// If we already have a valid token, skip the full OAuth dance.
|
|
232
|
+
// Issue a proxy code immediately so subsequent clients never need a browser.
|
|
233
|
+
const existingToken = this.getFirstValidToken();
|
|
234
|
+
if (existingToken) {
|
|
235
|
+
console.log("[PKCEProxy] Valid token exists — issuing proxy code directly (skipping OAuth)");
|
|
236
|
+
const proxyCode = this.generateId();
|
|
237
|
+
this.tokens.set(proxyCode, { ...existingToken });
|
|
238
|
+
await this.saveTokensToDisk();
|
|
239
|
+
const clientRedirect = new URL(params.redirect_uri);
|
|
240
|
+
clientRedirect.searchParams.set("code", proxyCode);
|
|
241
|
+
clientRedirect.searchParams.set("state", params.state || "");
|
|
242
|
+
console.log("[PKCEProxy] Redirecting client directly to:", clientRedirect.toString());
|
|
243
|
+
return new Response(null, {
|
|
244
|
+
status: 302,
|
|
245
|
+
headers: { Location: clientRedirect.toString() },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
235
248
|
// Generate our own PKCE for upstream
|
|
236
249
|
const pkce = this.generatePKCE();
|
|
237
250
|
const transactionId = this.generateId();
|
|
@@ -369,83 +382,35 @@ export class PKCEOAuthProxy {
|
|
|
369
382
|
};
|
|
370
383
|
}
|
|
371
384
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// Wait for the current exchange to complete
|
|
379
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
380
|
-
}
|
|
381
|
-
// Create a pending promise to indicate we're acquiring the lock
|
|
382
|
-
const pending = new Promise(resolve => {
|
|
383
|
-
releaseMutex = resolve;
|
|
384
|
-
this.tokenMutex.set(codeKey, Promise.resolve());
|
|
385
|
-
});
|
|
386
|
-
// Check if we got the lock before another concurrent request took it
|
|
387
|
-
if (this.tokenMutex.get(codeKey) !== pending) {
|
|
388
|
-
this.tokenMutex.delete(codeKey);
|
|
389
|
-
return acquireMutex(); // Try again
|
|
390
|
-
}
|
|
391
|
-
await pending;
|
|
392
|
-
};
|
|
393
|
-
const releaseMutexForCode = (code) => {
|
|
394
|
-
const codeKey = `code_${code}`;
|
|
395
|
-
this.tokenMutex.delete(codeKey);
|
|
396
|
-
};
|
|
397
|
-
try {
|
|
398
|
-
await acquireMutex();
|
|
399
|
-
// Check again after acquiring mutex - another request might have already processed this code
|
|
400
|
-
const cachedExchange = this.recentlyExchangedCodes.get(params.code);
|
|
401
|
-
if (cachedExchange && cachedExchange.expiresAt > new Date()) {
|
|
402
|
-
const cachedTokenData = this.tokens.get(cachedExchange.accessToken);
|
|
403
|
-
if (cachedTokenData) {
|
|
404
|
-
const expiresIn = Math.floor((cachedTokenData.expiresAt.getTime() - Date.now()) / 1000);
|
|
405
|
-
return {
|
|
406
|
-
access_token: cachedExchange.accessToken,
|
|
407
|
-
token_type: "Bearer",
|
|
408
|
-
expires_in: expiresIn > 0 ? expiresIn : 3600,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
const tokenData = this.tokens.get(params.code);
|
|
413
|
-
if (!tokenData) {
|
|
414
|
-
console.error(`[PKCEProxy] Token not found for code: ${params.code}`);
|
|
415
|
-
console.error(`[PKCEProxy] Available tokens:`, Array.from(this.tokens.keys()).map(k => k.slice(0, 8) + "..."));
|
|
416
|
-
console.error(`[PKCEProxy] Recently exchanged codes:`, Array.from(this.recentlyExchangedCodes.keys()).map(k => k.slice(0, 8) + "..."));
|
|
417
|
-
throw new OAuthProxyError("invalid_grant", "Invalid or expired authorization code", 400);
|
|
418
|
-
}
|
|
419
|
-
// Remove the code but keep track of it for retry tolerance (30 second window)
|
|
420
|
-
// Mark as exchanged BEFORE saving to disk to prevent race conditions
|
|
421
|
-
this.tokens.delete(params.code);
|
|
422
|
-
// Generate a new access token for the client
|
|
423
|
-
const accessToken = this.generateId();
|
|
424
|
-
this.tokens.set(accessToken, tokenData);
|
|
425
|
-
// Store the exchange for retry tolerance (30 seconds) - mark as exchanged
|
|
426
|
-
this.recentlyExchangedCodes.set(params.code, {
|
|
427
|
-
accessToken,
|
|
428
|
-
expiresAt: new Date(Date.now() + 30 * 1000),
|
|
429
|
-
});
|
|
430
|
-
// Now save to disk - this is the only write operation during the critical section
|
|
431
|
-
await this.saveTokensToDisk();
|
|
432
|
-
const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
|
|
433
|
-
console.log(`[PKCEProxy] Issued access token (expires in ${expiresIn}s) for code: ${params.code.slice(0, 8)}...`);
|
|
434
|
-
return {
|
|
435
|
-
access_token: accessToken,
|
|
436
|
-
token_type: "Bearer",
|
|
437
|
-
expires_in: expiresIn > 0 ? expiresIn : 3600,
|
|
438
|
-
// Note: Not returning refresh_token since Reflect doesn't support refresh_token grant
|
|
439
|
-
// This tells the MCP client to re-authenticate via OAuth when the token expires
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
throw error;
|
|
444
|
-
}
|
|
445
|
-
finally {
|
|
446
|
-
// Release the mutex
|
|
447
|
-
releaseMutexForCode(params.code);
|
|
385
|
+
const tokenData = this.tokens.get(params.code);
|
|
386
|
+
if (!tokenData) {
|
|
387
|
+
console.error(`[PKCEProxy] Token not found for code: ${params.code}`);
|
|
388
|
+
console.error(`[PKCEProxy] Available tokens:`, Array.from(this.tokens.keys()).map(k => k.slice(0, 8) + "..."));
|
|
389
|
+
console.error(`[PKCEProxy] Recently exchanged codes:`, Array.from(this.recentlyExchangedCodes.keys()).map(k => k.slice(0, 8) + "..."));
|
|
390
|
+
throw new OAuthProxyError("invalid_grant", "Invalid or expired authorization code", 400);
|
|
448
391
|
}
|
|
392
|
+
// Remove the code but keep track of it for retry tolerance (30 second window)
|
|
393
|
+
// Mark as exchanged BEFORE saving to disk to prevent race conditions
|
|
394
|
+
this.tokens.delete(params.code);
|
|
395
|
+
// Generate a new access token for the client
|
|
396
|
+
const accessToken = this.generateId();
|
|
397
|
+
this.tokens.set(accessToken, tokenData);
|
|
398
|
+
// Store the exchange for retry tolerance (30 seconds) - mark as exchanged
|
|
399
|
+
this.recentlyExchangedCodes.set(params.code, {
|
|
400
|
+
accessToken,
|
|
401
|
+
expiresAt: new Date(Date.now() + 30 * 1000),
|
|
402
|
+
});
|
|
403
|
+
// Now save to disk
|
|
404
|
+
await this.saveTokensToDisk();
|
|
405
|
+
const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
|
|
406
|
+
console.log(`[PKCEProxy] Issued access token (expires in ${expiresIn}s) for code: ${params.code.slice(0, 8)}...`);
|
|
407
|
+
return {
|
|
408
|
+
access_token: accessToken,
|
|
409
|
+
token_type: "Bearer",
|
|
410
|
+
expires_in: expiresIn > 0 ? expiresIn : 3600,
|
|
411
|
+
// Note: Not returning refresh_token since Reflect doesn't support refresh_token grant
|
|
412
|
+
// This tells the MCP client to re-authenticate via OAuth when the token expires
|
|
413
|
+
};
|
|
449
414
|
}
|
|
450
415
|
// Handle refresh token exchange
|
|
451
416
|
// Note: Reflect's API doesn't support standard refresh_token grant
|
|
@@ -471,6 +436,14 @@ export class PKCEOAuthProxy {
|
|
|
471
436
|
async loadUpstreamTokens(proxyToken) {
|
|
472
437
|
const data = this.tokens.get(proxyToken);
|
|
473
438
|
if (!data) {
|
|
439
|
+
// Token not found — check if this is a stale/rotated token from a previous session.
|
|
440
|
+
// For a local server, all clients share the same Reflect credentials, so we can
|
|
441
|
+
// fall back to any currently-valid token rather than forcing a re-auth loop.
|
|
442
|
+
const validToken = this.getFirstValidToken();
|
|
443
|
+
if (validToken) {
|
|
444
|
+
console.warn("[PKCEProxy] Stale token presented, mapping to current valid token:", proxyToken.slice(0, 8) + "...");
|
|
445
|
+
return validToken;
|
|
446
|
+
}
|
|
474
447
|
console.warn("[PKCEProxy] Token not found:", proxyToken.slice(0, 8) + "...");
|
|
475
448
|
console.warn("[PKCEProxy] Total tokens in store:", this.tokens.size);
|
|
476
449
|
return null;
|
package/dist/server.js
CHANGED
package/dist/utils.js
CHANGED
|
@@ -32,31 +32,35 @@ export function findLocalDatabase() {
|
|
|
32
32
|
}
|
|
33
33
|
// Search for database files in the File System directory
|
|
34
34
|
// Structure is typically: File System/XXX/t/XX/XXXXXXXX
|
|
35
|
+
// Reflect may have multiple database partitions (000, 001, etc.)
|
|
36
|
+
// so we find all SQLite files and return the most recently modified one.
|
|
35
37
|
try {
|
|
36
38
|
const entries = fs.readdirSync(basePath);
|
|
39
|
+
let bestPath = null;
|
|
40
|
+
let bestMtime = 0;
|
|
37
41
|
for (const entry of entries) {
|
|
38
42
|
const entryPath = path.join(basePath, entry);
|
|
39
43
|
const tPath = path.join(entryPath, "t");
|
|
40
44
|
if (fs.existsSync(tPath) && fs.statSync(tPath).isDirectory()) {
|
|
41
|
-
// Look for subdirectories in t/
|
|
42
45
|
const tEntries = fs.readdirSync(tPath);
|
|
43
46
|
for (const tEntry of tEntries) {
|
|
44
47
|
const subPath = path.join(tPath, tEntry);
|
|
45
48
|
if (fs.statSync(subPath).isDirectory()) {
|
|
46
|
-
// Look for database files (8-char hex names)
|
|
47
49
|
const dbFiles = fs.readdirSync(subPath);
|
|
48
50
|
for (const dbFile of dbFiles) {
|
|
49
51
|
const dbPath = path.join(subPath, dbFile);
|
|
50
|
-
// Check if it's a valid SQLite database (starts with SQLite header)
|
|
51
52
|
if (fs.statSync(dbPath).isFile()) {
|
|
52
53
|
try {
|
|
53
54
|
const header = Buffer.alloc(16);
|
|
54
55
|
const fd = fs.openSync(dbPath, 'r');
|
|
55
56
|
fs.readSync(fd, header, 0, 16, 0);
|
|
56
57
|
fs.closeSync(fd);
|
|
57
|
-
// SQLite files start with "SQLite format 3"
|
|
58
58
|
if (header.toString('utf8', 0, 15) === 'SQLite format 3') {
|
|
59
|
-
|
|
59
|
+
const mtime = fs.statSync(dbPath).mtimeMs;
|
|
60
|
+
if (mtime > bestMtime) {
|
|
61
|
+
bestMtime = mtime;
|
|
62
|
+
bestPath = dbPath;
|
|
63
|
+
}
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
catch {
|
|
@@ -68,11 +72,11 @@ export function findLocalDatabase() {
|
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
}
|
|
75
|
+
return bestPath;
|
|
71
76
|
}
|
|
72
77
|
catch {
|
|
73
78
|
return null;
|
|
74
79
|
}
|
|
75
|
-
return null;
|
|
76
80
|
}
|
|
77
81
|
/**
|
|
78
82
|
* Gets the default database path, searching for it if not provided.
|
|
@@ -84,7 +88,7 @@ export function getDefaultDbPath() {
|
|
|
84
88
|
return found;
|
|
85
89
|
}
|
|
86
90
|
if (os.platform() === "darwin") {
|
|
87
|
-
return expandPath("~/Library/Application Support/Reflect/File System/
|
|
91
|
+
return expandPath("~/Library/Application Support/Reflect/File System/001/t/00/00000000");
|
|
88
92
|
}
|
|
89
93
|
return "";
|
|
90
94
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reflect-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "MCP server for Reflect Notes - connect your notes to Claude Desktop. Just run: npx reflect-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
40
40
|
"better-sqlite3": "^11.10.0",
|
|
41
41
|
"fastmcp": "^3.25.4",
|
|
42
|
-
"reflect-mcp": "^1.0.
|
|
42
|
+
"reflect-mcp": "^1.0.15",
|
|
43
43
|
"zod": "^4.1.13"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|