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.
@@ -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;
@@ -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
- // Acquire mutex for this specific code to prevent race conditions
373
- // This ensures only ONE client can exchange a given code at a time
374
- let releaseMutex;
375
- const acquireMutex = async () => {
376
- const codeKey = `code_${params.code}`;
377
- while (this.tokenMutex.has(codeKey)) {
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
@@ -84,6 +84,7 @@ export async function startReflectMCPServer(config) {
84
84
  await server.start({
85
85
  httpStream: {
86
86
  port,
87
+ stateless: true,
87
88
  },
88
89
  transportType: "httpStream",
89
90
  });
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
- return dbPath;
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/000/t/00/00000000");
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.14",
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.12",
42
+ "reflect-mcp": "^1.0.15",
43
43
  "zod": "^4.1.13"
44
44
  },
45
45
  "devDependencies": {