taskover-mcp 1.1.0 → 1.2.0
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/auth-flow.js +6 -53
- package/auth-gate.js +253 -0
- package/cloud-adapter.js +38 -15
- package/crypto.js +386 -0
- package/data-store.js +9352 -0
- package/data-store.json-backup.js +1264 -0
- package/db.js +2292 -0
- package/image-moderator.js +491 -0
- package/image-processor.js +160 -0
- package/image-upload-service.js +398 -0
- package/index.js +2294 -1433
- package/migrate-json-to-sqlite.js +256 -0
- package/package.json +29 -21
- package/publish/auth-flow.js +275 -0
- package/publish/cloud-adapter.js +246 -0
- package/publish/credential-store.js +93 -0
- package/publish/index.js +1433 -0
- package/publish/package.json +21 -0
- package/publish/tool-map.js +1146 -0
- package/scripts/build-publish.sh +95 -0
- package/scripts/test-auth-failure.js +68 -0
- package/scripts/test-success.js +232 -0
- package/scripts/test-validation.js +105 -0
- package/tool-map.js +58 -0
- /package/{README.md → publish/README.md} +0 -0
package/auth-flow.js
CHANGED
|
@@ -3,7 +3,7 @@ const http = require("http");
|
|
|
3
3
|
const credentialStore = require("./credential-store.js");
|
|
4
4
|
|
|
5
5
|
const API_BASE = "https://api.taskover.gg";
|
|
6
|
-
const AUTH_PAGE_BASE = "https://taskover.
|
|
6
|
+
const AUTH_PAGE_BASE = "https://taskover.com";
|
|
7
7
|
|
|
8
8
|
function generatePKCE() {
|
|
9
9
|
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
@@ -95,7 +95,7 @@ function startCallbackServer(expectedState) {
|
|
|
95
95
|
if (!callbackUsed) {
|
|
96
96
|
callbackUsed = true;
|
|
97
97
|
server.close();
|
|
98
|
-
rejectCode(new Error("Auth timed out (5 min).
|
|
98
|
+
rejectCode(new Error("Auth timed out (5 min). Try your request again to re-launch authorization."));
|
|
99
99
|
}
|
|
100
100
|
}, 300000);
|
|
101
101
|
});
|
|
@@ -163,41 +163,9 @@ async function refreshAccessToken(refreshToken) {
|
|
|
163
163
|
return res.json();
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (cached.expires_at && Date.now() < cached.expires_at - 30000) {
|
|
170
|
-
return { accessToken: cached.access_token, refreshToken: cached.refresh_token };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
const res = await fetch(`${API_BASE}/api/auth/validate`, {
|
|
175
|
-
method: "POST",
|
|
176
|
-
headers: { "Authorization": `Bearer ${cached.access_token}` },
|
|
177
|
-
signal: AbortSignal.timeout(5000),
|
|
178
|
-
});
|
|
179
|
-
if (res.ok) {
|
|
180
|
-
const data = await res.json();
|
|
181
|
-
return { accessToken: cached.access_token, refreshToken: cached.refresh_token, displayName: data.displayName || "user" };
|
|
182
|
-
}
|
|
183
|
-
} catch (_) {}
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
console.error("[AUTH] Access token expired, refreshing...");
|
|
187
|
-
const tokens = await refreshAccessToken(cached.refresh_token);
|
|
188
|
-
await credentialStore.write({
|
|
189
|
-
access_token: tokens.access_token,
|
|
190
|
-
refresh_token: tokens.refresh_token,
|
|
191
|
-
expires_at: Date.now() + (tokens.expires_in * 1000),
|
|
192
|
-
host_type: hostType,
|
|
193
|
-
});
|
|
194
|
-
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token };
|
|
195
|
-
} catch (err) {
|
|
196
|
-
console.error(`[AUTH] Refresh failed: ${err.message}. Starting browser login...`);
|
|
197
|
-
await credentialStore.clear();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
166
|
+
// Full browser PKCE auth flow. Does NOT check cached credentials.
|
|
167
|
+
// Called by auth-gate when no valid tokens exist.
|
|
168
|
+
async function authenticateFull(hostType) {
|
|
201
169
|
console.error("[AUTH] Opening browser for login...");
|
|
202
170
|
|
|
203
171
|
const pkce = generatePKCE();
|
|
@@ -252,24 +220,9 @@ async function authenticate(hostType) {
|
|
|
252
220
|
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token };
|
|
253
221
|
}
|
|
254
222
|
|
|
255
|
-
async function reauthenticate(currentRefreshToken) {
|
|
256
|
-
try {
|
|
257
|
-
const tokens = await refreshAccessToken(currentRefreshToken);
|
|
258
|
-
await credentialStore.write({
|
|
259
|
-
access_token: tokens.access_token,
|
|
260
|
-
refresh_token: tokens.refresh_token,
|
|
261
|
-
expires_at: Date.now() + (tokens.expires_in * 1000),
|
|
262
|
-
});
|
|
263
|
-
return tokens;
|
|
264
|
-
} catch (err) {
|
|
265
|
-
await credentialStore.clear();
|
|
266
|
-
throw new Error("Session expired. Restart the MCP server to re-authenticate via browser.");
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
223
|
async function logout() {
|
|
271
224
|
await credentialStore.clear();
|
|
272
225
|
console.error("[AUTH] Logged out. Credentials cleared.");
|
|
273
226
|
}
|
|
274
227
|
|
|
275
|
-
module.exports = {
|
|
228
|
+
module.exports = { authenticateFull, refreshAccessToken, logout };
|
package/auth-gate.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// mcp-server/auth-gate.js
|
|
2
|
+
// Singleton just-in-time auth coordinator.
|
|
3
|
+
// All tool calls that need auth go through ensureAuth().
|
|
4
|
+
// Guarantees: single browser popup, request hold+resume, bounded timeout.
|
|
5
|
+
//
|
|
6
|
+
// WRITE SAFETY GUARANTEE:
|
|
7
|
+
// The JIT auth flow guarantees at-most-once execution for mutating requests:
|
|
8
|
+
// 1. ensureAuth() blocks until auth succeeds — the write has not been attempted yet.
|
|
9
|
+
// 2. After auth, callRpc() executes the write exactly once.
|
|
10
|
+
// 3. On 401 during an RPC call, the server rejected the request (nothing was written).
|
|
11
|
+
// handleAuthFailure() re-authenticates, then callRpc() retries — this is the first real execution.
|
|
12
|
+
// 4. No write is ever sent to the server more than once for a single tool invocation.
|
|
13
|
+
// 5. Network-level failures (timeout, connection reset) are NOT retried — they surface
|
|
14
|
+
// as errors to the user. This ensures at-most-once even for partial server processing.
|
|
15
|
+
|
|
16
|
+
const credentialStore = require("./credential-store.js");
|
|
17
|
+
|
|
18
|
+
const COOLDOWN_MS = 10000; // 10s min gap between auth attempts after failure
|
|
19
|
+
const MAX_REAUTH_ATTEMPTS = 2; // max consecutive re-auth attempts before giving up
|
|
20
|
+
const BOOT_GRACE_MS = 8000; // 8s after boot — suppress browser auth from auto-discovery calls
|
|
21
|
+
|
|
22
|
+
// Structured lifecycle logging — concise, non-sensitive, never prints tokens or user data.
|
|
23
|
+
function _log(event, detail) {
|
|
24
|
+
console.error(`[AUTH] ${event}${detail ? " — " + detail : ""}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// States
|
|
28
|
+
const IDLE = "IDLE";
|
|
29
|
+
const AUTHENTICATED = "AUTHENTICATED";
|
|
30
|
+
const AUTH_IN_PROGRESS = "AUTH_IN_PROGRESS";
|
|
31
|
+
const FAILED = "FAILED";
|
|
32
|
+
|
|
33
|
+
let _state = IDLE;
|
|
34
|
+
let _accessToken = null;
|
|
35
|
+
let _refreshToken = null;
|
|
36
|
+
let _pendingAuthPromise = null; // dedup guard for concurrent ensureAuth() calls
|
|
37
|
+
let _pendingRefreshPromise = null; // dedup guard for concurrent 401 handling
|
|
38
|
+
let _lastFailureTime = 0;
|
|
39
|
+
let _reAuthAttempts = 0;
|
|
40
|
+
let _bootTime = Date.now(); // tracks server start for boot grace period
|
|
41
|
+
let _authFlow = null; // lazy-loaded to avoid circular deps
|
|
42
|
+
let _cloudAdapter = null;
|
|
43
|
+
|
|
44
|
+
function _getAuthFlow() {
|
|
45
|
+
if (!_authFlow) _authFlow = require("./auth-flow.js");
|
|
46
|
+
return _authFlow;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _getCloudAdapter() {
|
|
50
|
+
if (!_cloudAdapter) _cloudAdapter = require("./cloud-adapter.js");
|
|
51
|
+
return _cloudAdapter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Called at boot to try loading cached credentials without browser.
|
|
55
|
+
// Does NOT launch browser auth if missing — just loads what's there.
|
|
56
|
+
// NOTE: Does NOT validate token against the server (unlike old authenticate()).
|
|
57
|
+
// If the token was revoked server-side, the first tool call will get 401
|
|
58
|
+
// and handleAuthFailure() will handle it gracefully.
|
|
59
|
+
async function tryLoadCachedTokens() {
|
|
60
|
+
try {
|
|
61
|
+
const cached = await credentialStore.read();
|
|
62
|
+
if (cached && cached.access_token && cached.refresh_token) {
|
|
63
|
+
// Check if access token is still valid (with 30s margin)
|
|
64
|
+
if (cached.expires_at && Date.now() < cached.expires_at - 30000) {
|
|
65
|
+
_accessToken = cached.access_token;
|
|
66
|
+
_refreshToken = cached.refresh_token;
|
|
67
|
+
_state = AUTHENTICATED;
|
|
68
|
+
_getCloudAdapter().setTokens(_accessToken, _refreshToken);
|
|
69
|
+
_log("cached_auth_loaded");
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
// Try refresh silently
|
|
73
|
+
try {
|
|
74
|
+
const authFlow = _getAuthFlow();
|
|
75
|
+
const tokens = await authFlow.refreshAccessToken(cached.refresh_token);
|
|
76
|
+
await credentialStore.write({
|
|
77
|
+
access_token: tokens.access_token,
|
|
78
|
+
refresh_token: tokens.refresh_token,
|
|
79
|
+
expires_at: Date.now() + (tokens.expires_in * 1000),
|
|
80
|
+
host_type: cached.host_type,
|
|
81
|
+
});
|
|
82
|
+
_accessToken = tokens.access_token;
|
|
83
|
+
_refreshToken = tokens.refresh_token;
|
|
84
|
+
_state = AUTHENTICATED;
|
|
85
|
+
_getCloudAdapter().setTokens(_accessToken, _refreshToken);
|
|
86
|
+
_log("token_refresh_success", "silent startup refresh");
|
|
87
|
+
return true;
|
|
88
|
+
} catch (_) {
|
|
89
|
+
// Refresh failed — credentials are stale, clear them.
|
|
90
|
+
// Do NOT launch browser. Just go to IDLE.
|
|
91
|
+
await credentialStore.clear();
|
|
92
|
+
_log("token_refresh_failed", "cached credentials expired, will authenticate on first tool use");
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// If credential store or cloud adapter fails to load, boot unauthenticated
|
|
99
|
+
_log("cached_auth_error", err.message);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The main gate. Every protected tool call awaits this.
|
|
105
|
+
// Returns { accessToken, refreshToken } or throws.
|
|
106
|
+
// When auth is in progress, all concurrent callers share the same pending promise
|
|
107
|
+
// so only ONE browser popup ever opens and all held requests resume together.
|
|
108
|
+
//
|
|
109
|
+
// callerMethod: optional RPC method name for logging which tool triggered auth.
|
|
110
|
+
async function ensureAuth(callerMethod) {
|
|
111
|
+
// Fast path: already authenticated
|
|
112
|
+
if (_state === AUTHENTICATED && _accessToken) {
|
|
113
|
+
return { accessToken: _accessToken, refreshToken: _refreshToken };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Dedup: if auth is already in progress, all callers share the same promise
|
|
117
|
+
if (_state === AUTH_IN_PROGRESS && _pendingAuthPromise) {
|
|
118
|
+
_log("ensureAuth_dedup", `method=${callerMethod || "unknown"}, sharing pending auth`);
|
|
119
|
+
return _pendingAuthPromise;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Boot grace period: suppress browser popups from MCP host auto-discovery calls
|
|
123
|
+
// (e.g. Claude calling taskovergg_dashboard immediately on connect).
|
|
124
|
+
// During the grace window, return a passive error instead of opening the browser.
|
|
125
|
+
// After the grace period, real user-initiated calls trigger auth normally.
|
|
126
|
+
if (_state === IDLE) {
|
|
127
|
+
const sinceBoot = Date.now() - _bootTime;
|
|
128
|
+
if (sinceBoot < BOOT_GRACE_MS) {
|
|
129
|
+
_log("boot_grace_suppressed", `method=${callerMethod || "unknown"}, ${Math.ceil((BOOT_GRACE_MS - sinceBoot) / 1000)}s remaining`);
|
|
130
|
+
throw new Error("AUTH_NOT_READY:TaskOver is not connected yet. Use any TaskOver tool to start authorization.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Cooldown after failure to prevent popup loops
|
|
135
|
+
if (_state === FAILED) {
|
|
136
|
+
const elapsed = Date.now() - _lastFailureTime;
|
|
137
|
+
if (elapsed < COOLDOWN_MS) {
|
|
138
|
+
_log("reauth_cooldown_active", `${Math.ceil((COOLDOWN_MS - elapsed) / 1000)}s remaining`);
|
|
139
|
+
throw new Error("AUTH_FAILED_RETRY:Authorization was recently declined or timed out. Please try again in a moment.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Launch browser auth — transitions to AUTH_IN_PROGRESS
|
|
144
|
+
_log("ensureAuth_trigger", `method=${callerMethod || "unknown"}`);
|
|
145
|
+
_state = AUTH_IN_PROGRESS;
|
|
146
|
+
_pendingAuthPromise = _doJitAuth();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await _pendingAuthPromise;
|
|
150
|
+
return result;
|
|
151
|
+
} finally {
|
|
152
|
+
_pendingAuthPromise = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function _doJitAuth() {
|
|
157
|
+
const hostType = process.env.TASKOVER_HOST_TYPE || "MCP";
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
_log("jit_auth_start", `host=${hostType}`);
|
|
161
|
+
const authFlow = _getAuthFlow();
|
|
162
|
+
const auth = await authFlow.authenticateFull(hostType);
|
|
163
|
+
|
|
164
|
+
_accessToken = auth.accessToken;
|
|
165
|
+
_refreshToken = auth.refreshToken;
|
|
166
|
+
_state = AUTHENTICATED;
|
|
167
|
+
_lastFailureTime = 0;
|
|
168
|
+
_reAuthAttempts = 0;
|
|
169
|
+
_getCloudAdapter().setTokens(_accessToken, _refreshToken);
|
|
170
|
+
|
|
171
|
+
_log("jit_auth_success");
|
|
172
|
+
return { accessToken: _accessToken, refreshToken: _refreshToken };
|
|
173
|
+
} catch (err) {
|
|
174
|
+
_state = FAILED;
|
|
175
|
+
_lastFailureTime = Date.now();
|
|
176
|
+
_log("jit_auth_failed", err.message);
|
|
177
|
+
throw new Error(`AUTH_FAILED_RETRY:${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Called by cloud-adapter when a 401 is received during an RPC call.
|
|
182
|
+
// Tries refresh first, falls back to full browser re-auth.
|
|
183
|
+
// DEDUP: If another caller already refreshed successfully (state is AUTHENTICATED),
|
|
184
|
+
// return current tokens. If a refresh is already in progress, share the pending promise.
|
|
185
|
+
async function handleAuthFailure() {
|
|
186
|
+
// Dedup: if another concurrent 401 handler already refreshed, use those tokens
|
|
187
|
+
if (_state === AUTHENTICATED && _accessToken) {
|
|
188
|
+
return { accessToken: _accessToken, refreshToken: _refreshToken };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Dedup: if refresh is already in progress, share the pending promise
|
|
192
|
+
if (_pendingRefreshPromise) {
|
|
193
|
+
return _pendingRefreshPromise;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Max-retry guard to prevent infinite re-auth loops
|
|
197
|
+
_reAuthAttempts++;
|
|
198
|
+
if (_reAuthAttempts > MAX_REAUTH_ATTEMPTS) {
|
|
199
|
+
_log("reauth_max_attempts", `limit=${MAX_REAUTH_ATTEMPTS}`);
|
|
200
|
+
_reAuthAttempts = 0;
|
|
201
|
+
throw new Error("AUTH_FAILED_RETRY:Too many re-authentication attempts. Please try again later.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
_pendingRefreshPromise = _doHandleAuthFailure();
|
|
205
|
+
try {
|
|
206
|
+
return await _pendingRefreshPromise;
|
|
207
|
+
} finally {
|
|
208
|
+
_pendingRefreshPromise = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function _doHandleAuthFailure() {
|
|
213
|
+
// Try refresh first
|
|
214
|
+
if (_refreshToken) {
|
|
215
|
+
try {
|
|
216
|
+
_log("token_refresh_start");
|
|
217
|
+
const authFlow = _getAuthFlow();
|
|
218
|
+
const tokens = await authFlow.refreshAccessToken(_refreshToken);
|
|
219
|
+
await credentialStore.write({
|
|
220
|
+
access_token: tokens.access_token,
|
|
221
|
+
refresh_token: tokens.refresh_token,
|
|
222
|
+
expires_at: Date.now() + (tokens.expires_in * 1000),
|
|
223
|
+
});
|
|
224
|
+
_accessToken = tokens.access_token;
|
|
225
|
+
_refreshToken = tokens.refresh_token;
|
|
226
|
+
_state = AUTHENTICATED;
|
|
227
|
+
_reAuthAttempts = 0;
|
|
228
|
+
_getCloudAdapter().setTokens(_accessToken, _refreshToken);
|
|
229
|
+
_log("token_refresh_success");
|
|
230
|
+
return { accessToken: _accessToken, refreshToken: _refreshToken };
|
|
231
|
+
} catch (_) {
|
|
232
|
+
_log("token_refresh_failed", "clearing credentials, will re-authenticate");
|
|
233
|
+
await credentialStore.clear();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Refresh failed — need full re-auth
|
|
238
|
+
_state = IDLE;
|
|
239
|
+
_accessToken = null;
|
|
240
|
+
_refreshToken = null;
|
|
241
|
+
return ensureAuth();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isAuthenticated() {
|
|
245
|
+
return _state === AUTHENTICATED && !!_accessToken;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
tryLoadCachedTokens,
|
|
250
|
+
ensureAuth,
|
|
251
|
+
handleAuthFailure,
|
|
252
|
+
isAuthenticated,
|
|
253
|
+
};
|
package/cloud-adapter.js
CHANGED
|
@@ -44,6 +44,14 @@ const MCP_ALLOWED_READS = new Set([
|
|
|
44
44
|
"getStories", "getScenes", "getSceneContent", "getStoryBible",
|
|
45
45
|
"search",
|
|
46
46
|
"dashboard", "contextExport",
|
|
47
|
+
// Blueprint Intelligence reads
|
|
48
|
+
"getDiscoveredBpDetail", "getSyncDebugInfo",
|
|
49
|
+
"computeBlueprintComplexity", "computeBlueprintHealth",
|
|
50
|
+
"getBlueprintTimeline", "generateBlueprintSummary",
|
|
51
|
+
"computeBlueprintReview", "computeProjectBlueprintReview",
|
|
52
|
+
"getBlueprintStandards", "getBlueprintViolations",
|
|
53
|
+
"buildBlueprintDependencyGraph", "computeImpactAnalysis",
|
|
54
|
+
"getBlueprintUsageMap", "getBlueprintIntelligenceDashboard",
|
|
47
55
|
]);
|
|
48
56
|
|
|
49
57
|
// REVISED per M0 matrix (52 write methods). Removed: updateBug (no MCP tool),
|
|
@@ -80,6 +88,9 @@ const MCP_ALLOWED_WRITES = new Set([
|
|
|
80
88
|
"addScene", "updateScene",
|
|
81
89
|
"updateStoryBible",
|
|
82
90
|
"addStoryCharacter",
|
|
91
|
+
// Blueprint Intelligence writes
|
|
92
|
+
"syncDiscoveredBps", "createBlueprintSnapshot",
|
|
93
|
+
"toggleBlueprintStandard", "evaluateBlueprintStandards", "resolveBlueprintViolation",
|
|
83
94
|
]);
|
|
84
95
|
|
|
85
96
|
// SECURITY: Methods that are NEVER allowed through MCP, regardless of future changes.
|
|
@@ -102,7 +113,7 @@ const MCP_ALLOWED_METHODS = new Set([...MCP_ALLOWED_READS, ...MCP_ALLOWED_WRITES
|
|
|
102
113
|
|
|
103
114
|
let _apiKey = null;
|
|
104
115
|
|
|
105
|
-
// Token-based auth state (
|
|
116
|
+
// Token-based auth state (browser auth — set via setTokens, managed by auth-gate)
|
|
106
117
|
let _accessToken = null;
|
|
107
118
|
let _refreshToken = null;
|
|
108
119
|
|
|
@@ -110,11 +121,16 @@ function init(apiKey) {
|
|
|
110
121
|
_apiKey = apiKey;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
|
|
124
|
+
// Called by auth-gate after successful auth or refresh. Can be called multiple times.
|
|
125
|
+
function setTokens(accessToken, refreshToken) {
|
|
114
126
|
_accessToken = accessToken;
|
|
115
127
|
_refreshToken = refreshToken;
|
|
116
128
|
}
|
|
117
129
|
|
|
130
|
+
function isInitialized() {
|
|
131
|
+
return !!(_apiKey || _accessToken);
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
function isAllowed(method) {
|
|
119
135
|
return MCP_ALLOWED_METHODS.has(method);
|
|
120
136
|
}
|
|
@@ -186,20 +202,26 @@ async function _doRpcCall(method, args, token) {
|
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
async function callRpc(method, args) {
|
|
189
|
-
|
|
205
|
+
// In API-key mode, just use the key directly — no auth gate involvement
|
|
206
|
+
if (_apiKey) {
|
|
207
|
+
return _doRpcCall(method, args, _apiKey);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Browser-auth mode: ensure we have tokens via auth gate.
|
|
211
|
+
// Use the returned token directly (not the module-level _accessToken)
|
|
212
|
+
// to avoid stale-read issues if setTokens hasn't been called yet.
|
|
213
|
+
const authGate = require("./auth-gate.js");
|
|
214
|
+
const { accessToken } = await authGate.ensureAuth(method);
|
|
215
|
+
if (!accessToken) throw new Error("AUTH_FAILED_RETRY:No access token available");
|
|
216
|
+
|
|
190
217
|
try {
|
|
191
|
-
return await _doRpcCall(method, args,
|
|
218
|
+
return await _doRpcCall(method, args, accessToken);
|
|
192
219
|
} catch (err) {
|
|
193
|
-
if (err.message === "AUTH_FAILED"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
_refreshToken = tokens.refresh_token;
|
|
199
|
-
return await _doRpcCall(method, args, _accessToken);
|
|
200
|
-
} catch (refreshErr) {
|
|
201
|
-
throw new Error("Session expired. Restart the MCP server to re-authenticate.");
|
|
202
|
-
}
|
|
220
|
+
if (err.message === "AUTH_FAILED") {
|
|
221
|
+
// Token expired/revoked mid-session — let auth gate handle refresh or re-auth
|
|
222
|
+
const refreshed = await authGate.handleAuthFailure();
|
|
223
|
+
if (!refreshed.accessToken) throw new Error("AUTH_FAILED_RETRY:Re-authentication failed");
|
|
224
|
+
return await _doRpcCall(method, args, refreshed.accessToken);
|
|
203
225
|
}
|
|
204
226
|
throw err;
|
|
205
227
|
}
|
|
@@ -235,11 +257,12 @@ async function validateKey() {
|
|
|
235
257
|
|
|
236
258
|
module.exports = {
|
|
237
259
|
init,
|
|
238
|
-
|
|
260
|
+
setTokens,
|
|
239
261
|
isAllowed,
|
|
240
262
|
isWrite,
|
|
241
263
|
callRpc,
|
|
242
264
|
validateKey,
|
|
265
|
+
isInitialized,
|
|
243
266
|
MCP_ALLOWED_METHODS,
|
|
244
267
|
MCP_ALLOWED_READS,
|
|
245
268
|
MCP_ALLOWED_WRITES,
|