@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.
Files changed (2) hide show
  1. package/dist/api-client.js +59 -1
  2. package/package.json +1 -1
@@ -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
- throw new OktaRefreshError(`Okta refresh failed (HTTP ${response.status}). Your refresh token may be revoked or idle-expired. Re-run \`npx @loopops/mcp-cli login\`. Detail: ${body.slice(0, 300)}`);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",