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 +0 -0
- package/dist/pkcehandler.d.ts +4 -0
- package/dist/pkcehandler.js +108 -6
- package/dist/server.js +22 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/pkcehandler.d.ts
CHANGED
|
@@ -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(): {
|
package/dist/pkcehandler.js
CHANGED
|
@@ -14,10 +14,12 @@ import { OAuthProxyError } from "fastmcp/auth";
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
export class PKCEOAuthProxy {
|
|
16
16
|
config;
|
|
17
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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",
|