reflect-mcp 1.0.6 → 1.0.7

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/cli.js CHANGED
File without changes
@@ -17,15 +17,19 @@ export interface PKCEOAuthProxyConfig {
17
17
  scopes: string[];
18
18
  redirectPath?: string;
19
19
  tokenStoragePath?: string;
20
+ transactionStoragePath?: string;
20
21
  }
21
22
  export declare class PKCEOAuthProxy {
22
23
  private config;
23
24
  private transactions;
24
25
  private tokens;
26
+ private recentlyExchangedCodes;
25
27
  private cleanupInterval;
26
28
  constructor(options: PKCEOAuthProxyConfig);
27
29
  private loadTokensFromDisk;
28
30
  private saveTokensToDisk;
31
+ private loadTransactionsFromDisk;
32
+ private saveTransactionsToDisk;
29
33
  private generatePKCE;
30
34
  private generateId;
31
35
  getAuthorizationServerMetadata(): {
@@ -14,10 +14,12 @@ import { OAuthProxyError } from "fastmcp/auth";
14
14
  // ============================================================================
15
15
  export class PKCEOAuthProxy {
16
16
  config;
17
- // In-memory storage for transactions (short-lived, don't need persistence)
17
+ // Transaction storage - now persisted to disk to survive restarts
18
18
  transactions = new Map();
19
19
  // Token storage - persisted to disk
20
20
  tokens = new Map();
21
+ // Track tokens that have been exchanged but allow brief retry window
22
+ recentlyExchangedCodes = new Map();
21
23
  cleanupInterval = null;
22
24
  constructor(options) {
23
25
  this.config = {
@@ -28,8 +30,10 @@ export class PKCEOAuthProxy {
28
30
  redirectPath: options.redirectPath || "/oauth/callback",
29
31
  scopes: options.scopes,
30
32
  tokenStoragePath: options.tokenStoragePath || path.join(os.homedir(), ".reflect-mcp-tokens.json"),
33
+ transactionStoragePath: options.transactionStoragePath || path.join(os.homedir(), ".reflect-mcp-transactions.json"),
31
34
  };
32
35
  this.loadTokensFromDisk();
36
+ this.loadTransactionsFromDisk();
33
37
  this.startCleanup();
34
38
  }
35
39
  // Load tokens from disk on startup
@@ -73,6 +77,57 @@ export class PKCEOAuthProxy {
73
77
  console.error("[PKCEProxy] Failed to save tokens to disk:", error);
74
78
  }
75
79
  }
80
+ // Load transactions from disk on startup (survives server restarts)
81
+ loadTransactionsFromDisk() {
82
+ try {
83
+ if (fs.existsSync(this.config.transactionStoragePath)) {
84
+ const data = fs.readFileSync(this.config.transactionStoragePath, "utf-8");
85
+ const stored = JSON.parse(data);
86
+ for (const [key, value] of Object.entries(stored)) {
87
+ const expiresAt = new Date(value.expiresAt);
88
+ // Only load non-expired transactions
89
+ if (expiresAt > new Date()) {
90
+ this.transactions.set(key, {
91
+ codeVerifier: value.codeVerifier,
92
+ codeChallenge: value.codeChallenge,
93
+ clientCallbackUrl: value.clientCallbackUrl,
94
+ clientId: value.clientId,
95
+ clientState: value.clientState,
96
+ scope: value.scope,
97
+ createdAt: new Date(value.createdAt),
98
+ expiresAt,
99
+ });
100
+ }
101
+ }
102
+ console.log(`[PKCEProxy] Loaded ${this.transactions.size} transactions from disk`);
103
+ }
104
+ }
105
+ catch (error) {
106
+ console.warn("[PKCEProxy] Failed to load transactions from disk:", error);
107
+ }
108
+ }
109
+ // Save transactions to disk (survives server restarts)
110
+ saveTransactionsToDisk() {
111
+ try {
112
+ const toStore = {};
113
+ for (const [key, value] of this.transactions) {
114
+ toStore[key] = {
115
+ codeVerifier: value.codeVerifier,
116
+ codeChallenge: value.codeChallenge,
117
+ clientCallbackUrl: value.clientCallbackUrl,
118
+ clientId: value.clientId,
119
+ clientState: value.clientState,
120
+ scope: value.scope,
121
+ createdAt: value.createdAt.toISOString(),
122
+ expiresAt: value.expiresAt.toISOString(),
123
+ };
124
+ }
125
+ fs.writeFileSync(this.config.transactionStoragePath, JSON.stringify(toStore, null, 2));
126
+ }
127
+ catch (error) {
128
+ console.error("[PKCEProxy] Failed to save transactions to disk:", error);
129
+ }
130
+ }
76
131
  // Generate PKCE code verifier and challenge
77
132
  generatePKCE() {
78
133
  // Generate a random code verifier (43-128 characters)
@@ -124,6 +179,7 @@ export class PKCEOAuthProxy {
124
179
  expiresAt: new Date(Date.now() + 600 * 1000), // 10 minutes
125
180
  };
126
181
  this.transactions.set(transactionId, transaction);
182
+ this.saveTransactionsToDisk(); // Persist to survive restarts
127
183
  console.log("[PKCEProxy] Created transaction:", transactionId);
128
184
  // Build upstream authorization URL
129
185
  const authUrl = new URL(this.config.authorizationEndpoint);
@@ -172,6 +228,8 @@ export class PKCEOAuthProxy {
172
228
  }
173
229
  if (transaction.expiresAt < new Date()) {
174
230
  this.transactions.delete(state);
231
+ this.saveTransactionsToDisk();
232
+ console.error("[PKCEProxy] Transaction expired, created:", transaction.createdAt, "expired:", transaction.expiresAt);
175
233
  return new Response(JSON.stringify({ error: "transaction_expired" }), {
176
234
  status: 400,
177
235
  headers: { "Content-Type": "application/json" },
@@ -214,6 +272,7 @@ export class PKCEOAuthProxy {
214
272
  clientRedirect.searchParams.set("state", transaction.clientState);
215
273
  // Clean up transaction
216
274
  this.transactions.delete(state);
275
+ this.saveTransactionsToDisk();
217
276
  console.log("[PKCEProxy] Redirecting to client:", clientRedirect.toString());
218
277
  return new Response(null, {
219
278
  status: 302,
@@ -226,18 +285,39 @@ export class PKCEOAuthProxy {
226
285
  if (!params.code) {
227
286
  throw new OAuthProxyError("invalid_request", "Missing authorization code", 400);
228
287
  }
288
+ // Check if this code was recently exchanged (retry tolerance)
289
+ // This allows mcp-remote to retry if the first request timed out but actually succeeded
290
+ const recentExchange = this.recentlyExchangedCodes.get(params.code);
291
+ if (recentExchange && recentExchange.expiresAt > new Date()) {
292
+ console.log("[PKCEProxy] Returning cached token for retry of code:", params.code.slice(0, 8) + "...");
293
+ const tokenData = this.tokens.get(recentExchange.accessToken);
294
+ if (tokenData) {
295
+ const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
296
+ return {
297
+ access_token: recentExchange.accessToken,
298
+ token_type: "Bearer",
299
+ expires_in: expiresIn > 0 ? expiresIn : 3600,
300
+ };
301
+ }
302
+ }
229
303
  const tokenData = this.tokens.get(params.code);
230
304
  if (!tokenData) {
231
305
  console.error("[PKCEProxy] Token not found for code:", params.code);
232
306
  console.error("[PKCEProxy] Available tokens:", Array.from(this.tokens.keys()).map(k => k.slice(0, 8) + "..."));
307
+ console.error("[PKCEProxy] Recently exchanged codes:", Array.from(this.recentlyExchangedCodes.keys()).map(k => k.slice(0, 8) + "..."));
233
308
  throw new OAuthProxyError("invalid_grant", "Invalid or expired authorization code", 400);
234
309
  }
235
- // Remove the code (single use)
310
+ // Remove the code but keep track of it for retry tolerance (30 second window)
236
311
  this.tokens.delete(params.code);
237
312
  // Generate a new access token for the client
238
313
  const accessToken = this.generateId();
239
314
  this.tokens.set(accessToken, tokenData);
240
315
  this.saveTokensToDisk(); // Persist to disk
316
+ // Store the exchange for retry tolerance (30 seconds)
317
+ this.recentlyExchangedCodes.set(params.code, {
318
+ accessToken,
319
+ expiresAt: new Date(Date.now() + 30 * 1000),
320
+ });
241
321
  const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
242
322
  console.log("[PKCEProxy] Issuing access token, expires in:", expiresIn, "seconds");
243
323
  return {
@@ -271,13 +351,22 @@ export class PKCEOAuthProxy {
271
351
  // Load upstream tokens for a given proxy token
272
352
  loadUpstreamTokens(proxyToken) {
273
353
  const data = this.tokens.get(proxyToken);
274
- if (!data)
354
+ if (!data) {
355
+ console.warn("[PKCEProxy] Token not found:", proxyToken.slice(0, 8) + "...");
356
+ console.warn("[PKCEProxy] Total tokens in store:", this.tokens.size);
275
357
  return null;
276
- if (data.expiresAt < new Date()) {
358
+ }
359
+ const now = new Date();
360
+ if (data.expiresAt < now) {
361
+ console.warn("[PKCEProxy] Token expired:", proxyToken.slice(0, 8) + "...", "expired at:", data.expiresAt, "now:", now);
277
362
  this.tokens.delete(proxyToken);
278
363
  this.saveTokensToDisk();
279
364
  return null;
280
365
  }
366
+ const timeRemaining = Math.floor((data.expiresAt.getTime() - now.getTime()) / 1000);
367
+ if (timeRemaining < 300) { // Less than 5 minutes remaining
368
+ console.warn("[PKCEProxy] Token expiring soon:", proxyToken.slice(0, 8) + "...", "remaining:", timeRemaining, "seconds");
369
+ }
281
370
  return data;
282
371
  }
283
372
  // Get first valid token (for stdio mode where we don't have specific token ID)
@@ -290,24 +379,37 @@ export class PKCEOAuthProxy {
290
379
  }
291
380
  return null;
292
381
  }
293
- // Cleanup expired transactions and tokens
382
+ // Cleanup expired transactions, tokens, and retry cache
294
383
  startCleanup() {
295
384
  this.cleanupInterval = setInterval(() => {
296
385
  const now = new Date();
297
386
  let tokensChanged = false;
387
+ let transactionsChanged = false;
298
388
  for (const [id, tx] of this.transactions) {
299
- if (tx.expiresAt < now)
389
+ if (tx.expiresAt < now) {
300
390
  this.transactions.delete(id);
391
+ transactionsChanged = true;
392
+ }
301
393
  }
302
394
  for (const [id, token] of this.tokens) {
303
395
  if (token.expiresAt < now) {
396
+ console.log("[PKCEProxy] Cleaning up expired token:", id.slice(0, 8) + "...");
304
397
  this.tokens.delete(id);
305
398
  tokensChanged = true;
306
399
  }
307
400
  }
401
+ // Clean up expired retry cache entries
402
+ for (const [code, data] of this.recentlyExchangedCodes) {
403
+ if (data.expiresAt < now) {
404
+ this.recentlyExchangedCodes.delete(code);
405
+ }
406
+ }
308
407
  if (tokensChanged) {
309
408
  this.saveTokensToDisk();
310
409
  }
410
+ if (transactionsChanged) {
411
+ this.saveTransactionsToDisk();
412
+ }
311
413
  }, 60000); // Every minute
312
414
  }
313
415
  destroy() {
package/dist/server.js CHANGED
@@ -35,15 +35,26 @@ export async function startReflectMCPServer(config) {
35
35
  authenticate: async (request) => {
36
36
  const authHeader = request.headers.authorization;
37
37
  if (!authHeader?.startsWith("Bearer ")) {
38
- return undefined;
38
+ console.warn("[Auth] Missing or invalid Authorization header - triggering 401");
39
+ // Throw Response to trigger re-authentication (per FastMCP docs)
40
+ throw new Response(null, {
41
+ status: 401,
42
+ statusText: "Unauthorized - Bearer token required",
43
+ });
39
44
  }
40
45
  const token = authHeader.slice(7);
41
46
  try {
42
47
  const tokenData = pkceProxy.loadUpstreamTokens(token);
43
48
  if (!tokenData) {
44
- return undefined;
49
+ console.warn("[Auth] Token validation failed for:", token.slice(0, 8) + "... - triggering 401");
50
+ // Throw Response to trigger re-authentication (per FastMCP docs)
51
+ throw new Response(null, {
52
+ status: 401,
53
+ statusText: "Unauthorized - Invalid or expired token",
54
+ });
45
55
  }
46
56
  const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
57
+ console.log("[Auth] Token validated, expires in:", expiresIn, "seconds");
47
58
  return {
48
59
  accessToken: tokenData.accessToken,
49
60
  refreshToken: tokenData.refreshToken,
@@ -51,8 +62,15 @@ export async function startReflectMCPServer(config) {
51
62
  };
52
63
  }
53
64
  catch (error) {
54
- console.error("[Auth] Error:", error);
55
- return undefined;
65
+ // Re-throw if it's already a Response (our auth failures above)
66
+ if (error instanceof Response) {
67
+ throw error;
68
+ }
69
+ console.error("[Auth] Error validating token:", error);
70
+ throw new Response(null, {
71
+ status: 401,
72
+ statusText: "Unauthorized - Token validation error",
73
+ });
56
74
  }
57
75
  },
58
76
  version: "1.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reflect-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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",