@loopops/mcp-server 2.8.0 → 2.9.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/dist/api-client.js +59 -1
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -256,6 +256,63 @@ function persistRotatedRefreshToken(newToken) {
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Build a useful error message when Okta rejects our refresh token. The
|
|
261
|
+
* generic answer ("token may be revoked or idle-expired") is the last
|
|
262
|
+
* resort — first we check whether settings.json is pointing at the wrong
|
|
263
|
+
* tenant or client, which is a much more common cause and one the user
|
|
264
|
+
* cannot diagnose from the bare 400.
|
|
265
|
+
*
|
|
266
|
+
* The Loop API exposes the tenant it actually authenticates against at
|
|
267
|
+
* `GET /api/mcp/tenant-info` (added 2026-04-29 alongside the
|
|
268
|
+
* trial-2194356 → integrator-4680962 migration). If that endpoint
|
|
269
|
+
* disagrees with our env vars, we report exactly which key is wrong and
|
|
270
|
+
* the one-shot fix command. Older webapp deployments will 404 the
|
|
271
|
+
* endpoint — we silently fall through to the generic message.
|
|
272
|
+
*/
|
|
273
|
+
async function diagnoseRefreshFailure(status, body) {
|
|
274
|
+
const head = `Okta refresh failed (HTTP ${status}). Detail: ${body.slice(0, 300)}`;
|
|
275
|
+
const generic = "Your refresh token may be revoked or idle-expired. Re-run " +
|
|
276
|
+
"`npx @loopops/mcp-cli@latest login` (the `@latest` pin bypasses the " +
|
|
277
|
+
"npx cache, which can serve a stale cli with old tenant defaults for " +
|
|
278
|
+
"up to 24h after a publish).";
|
|
279
|
+
if (!apiUrl)
|
|
280
|
+
return `${head}\n\n${generic}`;
|
|
281
|
+
try {
|
|
282
|
+
const apiBase = new URL(apiUrl).origin;
|
|
283
|
+
const tenantInfoUrl = `${apiBase}/api/mcp/tenant-info`;
|
|
284
|
+
const probe = await doFetch(tenantInfoUrl, { method: "GET", headers: { accept: "application/json" } }, 5_000);
|
|
285
|
+
if (!probe.ok)
|
|
286
|
+
return `${head}\n\n${generic}`;
|
|
287
|
+
const live = (await probe.json());
|
|
288
|
+
const issuerMismatch = !!live.issuer && live.issuer !== oktaIssuer;
|
|
289
|
+
const clientMismatch = !!live.clientId && live.clientId !== oktaClientId;
|
|
290
|
+
if (!issuerMismatch && !clientMismatch)
|
|
291
|
+
return `${head}\n\n${generic}`;
|
|
292
|
+
const lines = [
|
|
293
|
+
head,
|
|
294
|
+
"",
|
|
295
|
+
"DIAGNOSIS: your MCP config points at a tenant the Loop API does not recognise.",
|
|
296
|
+
];
|
|
297
|
+
if (issuerMismatch) {
|
|
298
|
+
lines.push(` OKTA_ISSUER on disk: ${oktaIssuer}`);
|
|
299
|
+
lines.push(` OKTA_ISSUER expected: ${live.issuer}`);
|
|
300
|
+
}
|
|
301
|
+
if (clientMismatch) {
|
|
302
|
+
lines.push(` OKTA_CLIENT_ID on disk: ${oktaClientId}`);
|
|
303
|
+
lines.push(` OKTA_CLIENT_ID expected: ${live.clientId}`);
|
|
304
|
+
}
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push("Fix: `npx @loopops/mcp-cli@latest login`. The `@latest` pin is " +
|
|
307
|
+
"important — without it, npx may serve a cached older cli that " +
|
|
308
|
+
"writes the same wrong defaults right back to disk.");
|
|
309
|
+
return lines.join("\n");
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Network blip, DNS failure, or older webapp without the endpoint.
|
|
313
|
+
return `${head}\n\n${generic}`;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
259
316
|
/**
|
|
260
317
|
* Mint a fresh access token via Okta's /token endpoint. Updates the
|
|
261
318
|
* in-memory cache AND (if Okta rotated the refresh token) persists the
|
|
@@ -298,7 +355,8 @@ async function refreshAccessTokenOnce() {
|
|
|
298
355
|
}, DEFAULT_TIMEOUT_MS);
|
|
299
356
|
if (!response.ok) {
|
|
300
357
|
const body = await response.text().catch(() => "<unreadable>");
|
|
301
|
-
|
|
358
|
+
const diagnosis = await diagnoseRefreshFailure(response.status, body);
|
|
359
|
+
throw new OktaRefreshError(diagnosis);
|
|
302
360
|
}
|
|
303
361
|
const tokens = (await response.json());
|
|
304
362
|
cachedAccessToken = tokens.access_token;
|