reflect-mcp 1.0.13 → 1.0.14

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
@@ -34,55 +34,18 @@ function isPortInUse(port) {
34
34
  });
35
35
  }
36
36
  /**
37
- * Kill any process using the specified port
37
+ * Find a free port starting from startPort.
38
+ * Returns the first available port within maxAttempts tries,
39
+ * or throws if none is found.
38
40
  */
39
- function killProcessOnPort(port) {
40
- try {
41
- let pids;
42
- if (platform === "darwin") {
43
- pids = execSync(`lsof -ti:${port}`, { encoding: "utf-8" }).trim();
44
- }
45
- else {
46
- // Linux: try lsof first, fall back to fuser
47
- try {
48
- pids = execSync(`lsof -ti:${port}`, { encoding: "utf-8" }).trim();
49
- }
50
- catch {
51
- pids = execSync(`fuser ${port}/tcp 2>/dev/null`, { encoding: "utf-8" }).trim();
52
- }
53
- }
54
- if (pids) {
55
- console.log(`⚠️ Port ${port} in use by PID(s): ${pids.replace(/\n/g, ", ")}`);
56
- for (const pid of pids.split(/\s+/)) {
57
- if (pid) {
58
- try {
59
- process.kill(parseInt(pid), "SIGKILL");
60
- }
61
- catch { /* already dead */ }
62
- }
63
- }
64
- console.log(`✅ Killed existing process(es) on port ${port}`);
65
- return true;
66
- }
67
- }
68
- catch {
69
- // No process found on port, or kill failed - that's fine
70
- }
71
- return false;
72
- }
73
- /**
74
- * Ensure the port is free, killing any existing process if needed
75
- */
76
- async function ensurePortFree(port) {
77
- if (await isPortInUse(port)) {
78
- killProcessOnPort(port);
79
- // Wait a moment for the port to be released
80
- await new Promise((resolve) => setTimeout(resolve, 500));
81
- // Check again
82
- if (await isPortInUse(port)) {
83
- throw new Error(`Port ${port} is still in use after attempting to free it`);
41
+ async function findFreePort(startPort, maxAttempts = 10) {
42
+ for (let i = 0; i < maxAttempts; i++) {
43
+ const candidate = startPort + i;
44
+ if (!(await isPortInUse(candidate))) {
45
+ return candidate;
84
46
  }
85
47
  }
48
+ throw new Error(`No free port found in range ${startPort}–${startPort + maxAttempts - 1}`);
86
49
  }
87
50
  const REFLECT_CLIENT_ID = "55798f25d5a24efb95e4174fff3d219e";
88
51
  const platform = os.platform();
@@ -180,9 +143,6 @@ async function install(installArgs) {
180
143
  const nodePath = process.execPath;
181
144
  const cliPath = process.argv[1];
182
145
  console.log("📦 Installing Reflect MCP Server as auto-start service...\n");
183
- // Kill any stale processes on the port
184
- killProcessOnPort(port);
185
- await new Promise((resolve) => setTimeout(resolve, 300));
186
146
  if (platform === "darwin") {
187
147
  installDarwin(nodePath, cliPath, expandedDbPath, port);
188
148
  }
@@ -203,7 +163,7 @@ async function install(installArgs) {
203
163
  "mcpServers": {
204
164
  "reflect": {
205
165
  "command": "npx",
206
- "args": ["-y", "mcp-remote", "--port", "4209", "http://localhost:${port}/mcp"]
166
+ "args": ["-y", "mcp-remote", "http://localhost:${port}/mcp", "--port", "4209"]
207
167
  }
208
168
  }
209
169
  }`);
@@ -401,10 +361,10 @@ function statusLinux() {
401
361
  }
402
362
  async function runServer(serverArgs) {
403
363
  let dbPath;
404
- let port = 3000;
364
+ let requestedPort = 3000;
405
365
  for (let i = 0; i < serverArgs.length; i++) {
406
366
  if (serverArgs[i] === "--port" && serverArgs[i + 1]) {
407
- port = parseInt(serverArgs[++i]);
367
+ requestedPort = parseInt(serverArgs[++i]);
408
368
  }
409
369
  else if (!serverArgs[i].startsWith("--")) {
410
370
  dbPath = serverArgs[i];
@@ -418,22 +378,27 @@ async function runServer(serverArgs) {
418
378
  }
419
379
  dbPath = DEFAULT_DB_PATH;
420
380
  }
421
- // Ensure port is free before starting
381
+ // Find the first free port at or above the requested port.
382
+ // This allows multiple MCP clients to each get their own HTTP server
383
+ // without killing other running instances.
384
+ let port;
422
385
  try {
423
- await ensurePortFree(port);
386
+ port = await findFreePort(requestedPort);
424
387
  }
425
388
  catch (err) {
426
- console.error(`❌ ${err}`);
389
+ console.error(`[reflect-mcp] ${err}`);
427
390
  process.exit(1);
428
391
  }
392
+ if (port !== requestedPort) {
393
+ console.error(`[reflect-mcp] Requested port ${requestedPort} is in use — using port ${port} instead`);
394
+ }
429
395
  try {
430
396
  await startReflectMCPServer({
431
397
  clientId: REFLECT_CLIENT_ID,
432
398
  port,
433
399
  dbPath,
434
400
  });
435
- console.log(`Reflect MCP Server running on http://localhost:${port}`);
436
- console.log(`Database: ${dbPath}`);
401
+ console.error(`[reflect-mcp] HTTP server running on http://localhost:${port}`);
437
402
  }
438
403
  catch (err) {
439
404
  console.error("Failed to start server:", err);
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * PKCE OAuth Proxy (No Client Secret Required)
3
3
  *
4
+ * Updated by Twice 🦸‍♂️
5
+ * Cloning capabilities enabled for multiple MCP clients
6
+ *
4
7
  * This module provides a custom OAuth proxy that uses PKCE for authentication
5
8
  * without requiring a client secret, suitable for public clients.
6
9
  */
@@ -25,6 +28,9 @@ export declare class PKCEOAuthProxy {
25
28
  private tokens;
26
29
  private recentlyExchangedCodes;
27
30
  private cleanupInterval;
31
+ private tokenMutex;
32
+ private tokenMutexBusy;
33
+ private activeConnections;
28
34
  constructor(options: PKCEOAuthProxyConfig);
29
35
  private loadTokensFromDisk;
30
36
  private saveTokensToDisk;
@@ -85,7 +91,7 @@ export declare class PKCEOAuthProxy {
85
91
  client_name?: string;
86
92
  redirect_uris?: string[];
87
93
  }>;
88
- loadUpstreamTokens(proxyToken: string): TokenData | null;
94
+ loadUpstreamTokens(proxyToken: string): Promise<TokenData | null>;
89
95
  getFirstValidToken(): TokenData | null;
90
96
  private startCleanup;
91
97
  destroy(): void;
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * PKCE OAuth Proxy (No Client Secret Required)
3
3
  *
4
+ * Updated by Twice 🦸‍♂️
5
+ * Cloning capabilities enabled for multiple MCP clients
6
+ *
4
7
  * This module provides a custom OAuth proxy that uses PKCE for authentication
5
8
  * without requiring a client secret, suitable for public clients.
6
9
  */
@@ -10,6 +13,56 @@ import * as path from "path";
10
13
  import * as os from "os";
11
14
  import { OAuthProxyError } from "fastmcp/auth";
12
15
  // ============================================================================
16
+ // Write Queue - Prevents concurrent file I/O operations
17
+ // ============================================================================
18
+ /**
19
+ * Simple in-memory write queue that batches and serializes file writes.
20
+ * This prevents race conditions when multiple clients trigger disk writes simultaneously.
21
+ */
22
+ class WriteQueue {
23
+ queue = [];
24
+ isProcessing = false;
25
+ /**
26
+ * Add a write operation to the queue and wait for it to complete
27
+ */
28
+ async add(writeOperation) {
29
+ return new Promise((resolve, reject) => {
30
+ const operation = async () => {
31
+ try {
32
+ await writeOperation();
33
+ resolve();
34
+ }
35
+ catch (error) {
36
+ reject(error);
37
+ }
38
+ };
39
+ this.queue.push(operation);
40
+ this.processQueue();
41
+ });
42
+ }
43
+ /**
44
+ * Process the queue one operation at a time
45
+ */
46
+ async processQueue() {
47
+ if (this.isProcessing || this.queue.length === 0) {
48
+ return;
49
+ }
50
+ this.isProcessing = true;
51
+ const operation = this.queue.shift();
52
+ try {
53
+ await operation();
54
+ }
55
+ finally {
56
+ this.isProcessing = false;
57
+ // Small delay to batch rapid writes together
58
+ await new Promise(resolve => setTimeout(resolve, 10));
59
+ this.processQueue();
60
+ }
61
+ }
62
+ }
63
+ // Global write queue instance shared across all PKCEProxy instances
64
+ const globalWriteQueue = new WriteQueue();
65
+ // ============================================================================
13
66
  // PKCEOAuthProxy Class
14
67
  // ============================================================================
15
68
  export class PKCEOAuthProxy {
@@ -21,6 +74,12 @@ export class PKCEOAuthProxy {
21
74
  // Track tokens that have been exchanged but allow brief retry window
22
75
  recentlyExchangedCodes = new Map();
23
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
+ // Active connections counter for debugging
82
+ activeConnections = 0;
24
83
  constructor(options) {
25
84
  this.config = {
26
85
  baseUrl: options.baseUrl,
@@ -60,8 +119,9 @@ export class PKCEOAuthProxy {
60
119
  console.warn("[PKCEProxy] Failed to load tokens from disk:", error);
61
120
  }
62
121
  }
63
- // Save tokens to disk
64
- saveTokensToDisk() {
122
+ // Save tokens to disk - ASYNC with write queue
123
+ // This prevents blocking the event loop and prevents race conditions
124
+ async saveTokensToDisk() {
65
125
  try {
66
126
  const toStore = {};
67
127
  for (const [key, value] of this.tokens) {
@@ -71,7 +131,11 @@ export class PKCEOAuthProxy {
71
131
  expiresAt: value.expiresAt.toISOString(),
72
132
  };
73
133
  }
74
- fs.writeFileSync(this.config.tokenStoragePath, JSON.stringify(toStore, null, 2));
134
+ // Use the global write queue to serialize this write operation
135
+ await globalWriteQueue.add(async () => {
136
+ await fs.promises.writeFile(this.config.tokenStoragePath, JSON.stringify(toStore, null, 2), "utf-8");
137
+ });
138
+ console.log(`[PKCEProxy] Saved ${toStore.length} tokens to disk`);
75
139
  }
76
140
  catch (error) {
77
141
  console.error("[PKCEProxy] Failed to save tokens to disk:", error);
@@ -106,8 +170,8 @@ export class PKCEOAuthProxy {
106
170
  console.warn("[PKCEProxy] Failed to load transactions from disk:", error);
107
171
  }
108
172
  }
109
- // Save transactions to disk (survives server restarts)
110
- saveTransactionsToDisk() {
173
+ // Save transactions to disk (survives server restarts) - ASYNC with write queue
174
+ async saveTransactionsToDisk() {
111
175
  try {
112
176
  const toStore = {};
113
177
  for (const [key, value] of this.transactions) {
@@ -122,7 +186,11 @@ export class PKCEOAuthProxy {
122
186
  expiresAt: value.expiresAt.toISOString(),
123
187
  };
124
188
  }
125
- fs.writeFileSync(this.config.transactionStoragePath, JSON.stringify(toStore, null, 2));
189
+ // Use the global write queue to serialize this write operation
190
+ await globalWriteQueue.add(async () => {
191
+ await fs.promises.writeFile(this.config.transactionStoragePath, JSON.stringify(toStore, null, 2), "utf-8");
192
+ });
193
+ console.log(`[PKCEProxy] Saved ${toStore.length} transactions to disk`);
126
194
  }
127
195
  catch (error) {
128
196
  console.error("[PKCEProxy] Failed to save transactions to disk:", error);
@@ -179,7 +247,7 @@ export class PKCEOAuthProxy {
179
247
  expiresAt: new Date(Date.now() + 600 * 1000), // 10 minutes
180
248
  };
181
249
  this.transactions.set(transactionId, transaction);
182
- this.saveTransactionsToDisk(); // Persist to survive restarts
250
+ await this.saveTransactionsToDisk(); // Persist to survive restarts (async now)
183
251
  console.log("[PKCEProxy] Created transaction:", transactionId);
184
252
  // Build upstream authorization URL
185
253
  const authUrl = new URL(this.config.authorizationEndpoint);
@@ -228,7 +296,7 @@ export class PKCEOAuthProxy {
228
296
  }
229
297
  if (transaction.expiresAt < new Date()) {
230
298
  this.transactions.delete(state);
231
- this.saveTransactionsToDisk();
299
+ await this.saveTransactionsToDisk();
232
300
  console.error("[PKCEProxy] Transaction expired, created:", transaction.createdAt, "expired:", transaction.expiresAt);
233
301
  return new Response(JSON.stringify({ error: "transaction_expired" }), {
234
302
  status: 400,
@@ -265,14 +333,14 @@ export class PKCEOAuthProxy {
265
333
  refreshToken: tokens.refresh_token,
266
334
  expiresAt: new Date(Date.now() + (tokens.expires_in || 3600) * 1000),
267
335
  });
268
- this.saveTokensToDisk(); // Persist to disk
336
+ await this.saveTokensToDisk(); // Persist to disk (async now)
269
337
  // Redirect back to client with our proxy token
270
338
  const clientRedirect = new URL(transaction.clientCallbackUrl);
271
339
  clientRedirect.searchParams.set("code", proxyToken);
272
340
  clientRedirect.searchParams.set("state", transaction.clientState);
273
341
  // Clean up transaction
274
342
  this.transactions.delete(state);
275
- this.saveTransactionsToDisk();
343
+ await this.saveTransactionsToDisk();
276
344
  console.log("[PKCEProxy] Redirecting to client:", clientRedirect.toString());
277
345
  return new Response(null, {
278
346
  status: 302,
@@ -285,11 +353,12 @@ export class PKCEOAuthProxy {
285
353
  if (!params.code) {
286
354
  throw new OAuthProxyError("invalid_request", "Missing authorization code", 400);
287
355
  }
356
+ console.log(`[PKCEProxy] Exchange requested for code: ${params.code.slice(0, 8)}... (connections: ${this.activeConnections})`);
288
357
  // Check if this code was recently exchanged (retry tolerance)
289
358
  // This allows mcp-remote to retry if the first request timed out but actually succeeded
290
359
  const recentExchange = this.recentlyExchangedCodes.get(params.code);
291
360
  if (recentExchange && recentExchange.expiresAt > new Date()) {
292
- console.log("[PKCEProxy] Returning cached token for retry of code:", params.code.slice(0, 8) + "...");
361
+ console.log(`[PKCEProxy] Returning cached token for retry of code: ${params.code.slice(0, 8)}...`);
293
362
  const tokenData = this.tokens.get(recentExchange.accessToken);
294
363
  if (tokenData) {
295
364
  const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
@@ -300,33 +369,83 @@ export class PKCEOAuthProxy {
300
369
  };
301
370
  }
302
371
  }
303
- const tokenData = this.tokens.get(params.code);
304
- if (!tokenData) {
305
- console.error("[PKCEProxy] Token not found for code:", params.code);
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) + "..."));
308
- throw new OAuthProxyError("invalid_grant", "Invalid or expired authorization code", 400);
309
- }
310
- // Remove the code but keep track of it for retry tolerance (30 second window)
311
- this.tokens.delete(params.code);
312
- // Generate a new access token for the client
313
- const accessToken = this.generateId();
314
- this.tokens.set(accessToken, tokenData);
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
- });
321
- const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
322
- console.log("[PKCEProxy] Issuing access token, expires in:", expiresIn, "seconds");
323
- return {
324
- access_token: accessToken,
325
- token_type: "Bearer",
326
- expires_in: expiresIn > 0 ? expiresIn : 3600,
327
- // Note: Not returning refresh_token since Reflect doesn't support refresh_token grant
328
- // This tells the MCP client to re-authenticate via OAuth when the token expires
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);
329
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);
448
+ }
330
449
  }
331
450
  // Handle refresh token exchange
332
451
  // Note: Reflect's API doesn't support standard refresh_token grant
@@ -349,7 +468,7 @@ export class PKCEOAuthProxy {
349
468
  };
350
469
  }
351
470
  // Load upstream tokens for a given proxy token
352
- loadUpstreamTokens(proxyToken) {
471
+ async loadUpstreamTokens(proxyToken) {
353
472
  const data = this.tokens.get(proxyToken);
354
473
  if (!data) {
355
474
  console.warn("[PKCEProxy] Token not found:", proxyToken.slice(0, 8) + "...");
@@ -360,7 +479,7 @@ export class PKCEOAuthProxy {
360
479
  if (data.expiresAt < now) {
361
480
  console.warn("[PKCEProxy] Token expired:", proxyToken.slice(0, 8) + "...", "expired at:", data.expiresAt, "now:", now);
362
481
  this.tokens.delete(proxyToken);
363
- this.saveTokensToDisk();
482
+ await this.saveTokensToDisk();
364
483
  return null;
365
484
  }
366
485
  const timeRemaining = Math.floor((data.expiresAt.getTime() - now.getTime()) / 1000);
@@ -380,8 +499,8 @@ export class PKCEOAuthProxy {
380
499
  return null;
381
500
  }
382
501
  // Cleanup expired transactions, tokens, and retry cache
383
- startCleanup() {
384
- this.cleanupInterval = setInterval(() => {
502
+ async startCleanup() {
503
+ const cleanup = async () => {
385
504
  const now = new Date();
386
505
  let tokensChanged = false;
387
506
  let transactionsChanged = false;
@@ -405,12 +524,15 @@ export class PKCEOAuthProxy {
405
524
  }
406
525
  }
407
526
  if (tokensChanged) {
408
- this.saveTokensToDisk();
527
+ await this.saveTokensToDisk();
409
528
  }
410
529
  if (transactionsChanged) {
411
- this.saveTransactionsToDisk();
530
+ await this.saveTransactionsToDisk();
412
531
  }
413
- }, 60000); // Every minute
532
+ };
533
+ this.cleanupInterval = setInterval(cleanup, 60000); // Every minute
534
+ // Run cleanup immediately on startup
535
+ await cleanup();
414
536
  }
415
537
  destroy() {
416
538
  if (this.cleanupInterval) {
package/dist/server.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Reflect MCP Server Factory
3
3
  *
4
+ * Updated by Twice 🦸‍♂️
5
+ * Now handling multiple concurrent clients!
6
+ *
4
7
  * Creates and configures the FastMCP server with PKCE OAuth
5
8
  */
6
9
  export interface ServerConfig {
@@ -9,5 +12,11 @@ export interface ServerConfig {
9
12
  dbPath?: string;
10
13
  }
11
14
  export declare function startReflectMCPServer(config: ServerConfig): Promise<void>;
15
+ /**
16
+ * Start the Reflect MCP server in stdio mode.
17
+ * Used when an HTTP server is already running on the port (e.g. a second MCP client).
18
+ * Reads the cached OAuth token from disk instead of running the full OAuth flow.
19
+ */
20
+ export declare function startReflectMCPServerStdio(config: ServerConfig): Promise<void>;
12
21
  export { PKCEOAuthProxy } from "./pkcehandler.js";
13
22
  export * from "./utils.js";
package/dist/server.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Reflect MCP Server Factory
3
3
  *
4
+ * Updated by Twice 🦸‍♂️
5
+ * Now handling multiple concurrent clients!
6
+ *
4
7
  * Creates and configures the FastMCP server with PKCE OAuth
5
8
  */
6
9
  import { FastMCP } from "fastmcp";
@@ -44,7 +47,7 @@ export async function startReflectMCPServer(config) {
44
47
  }
45
48
  const token = authHeader.slice(7);
46
49
  try {
47
- const tokenData = pkceProxy.loadUpstreamTokens(token);
50
+ const tokenData = await pkceProxy.loadUpstreamTokens(token);
48
51
  if (!tokenData) {
49
52
  console.warn("[Auth] Token validation failed for:", token.slice(0, 8) + "... - triggering 401");
50
53
  // Throw Response to trigger re-authentication (per FastMCP docs)
@@ -85,6 +88,43 @@ export async function startReflectMCPServer(config) {
85
88
  transportType: "httpStream",
86
89
  });
87
90
  }
91
+ /**
92
+ * Start the Reflect MCP server in stdio mode.
93
+ * Used when an HTTP server is already running on the port (e.g. a second MCP client).
94
+ * Reads the cached OAuth token from disk instead of running the full OAuth flow.
95
+ */
96
+ export async function startReflectMCPServerStdio(config) {
97
+ const port = config.port || 3000;
98
+ const baseUrl = `http://localhost:${port}`;
99
+ // Instantiate proxy only to read tokens from disk — no HTTP server needed
100
+ const pkceProxy = new PKCEOAuthProxy({
101
+ baseUrl,
102
+ clientId: config.clientId,
103
+ authorizationEndpoint: "https://reflect.app/oauth",
104
+ tokenEndpoint: "https://reflect.app/api/oauth/token",
105
+ scopes: ["read:graph", "write:graph"],
106
+ });
107
+ const server = new FastMCP({
108
+ name: "Reflect MCP Server",
109
+ // For stdio, FastMCP calls authenticate(undefined). We load the token from disk.
110
+ authenticate: async (_request) => {
111
+ const tokenData = pkceProxy.getFirstValidToken();
112
+ if (!tokenData) {
113
+ console.error("[Auth] No valid token on disk. Connect via HTTP mode first to complete OAuth.");
114
+ throw new Error("No valid token. Please authenticate via HTTP mode first.");
115
+ }
116
+ const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
117
+ console.error("[Auth] Stdio mode: token loaded from disk, expires in:", expiresIn, "seconds");
118
+ return {
119
+ accessToken: tokenData.accessToken,
120
+ expiresIn,
121
+ };
122
+ },
123
+ version: "1.0.0",
124
+ });
125
+ registerTools(server, config.dbPath);
126
+ await server.start({ transportType: "stdio" });
127
+ }
88
128
  // Also export for programmatic use
89
129
  export { PKCEOAuthProxy } from "./pkcehandler.js";
90
130
  export * from "./utils.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reflect-mcp",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
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",