@myspec/mcp-server 0.4.0-next.91 → 0.4.0-next.93
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/index.js +87 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -255,6 +255,7 @@ async function exchangeCode(userAuthUrl, code, fetchImpl) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// src/auth/credentials-store.ts
|
|
258
|
+
import { randomUUID } from "crypto";
|
|
258
259
|
import { promises as fs } from "fs";
|
|
259
260
|
import os from "os";
|
|
260
261
|
import path from "path";
|
|
@@ -365,9 +366,16 @@ async function writeJsonFile(target, data, opts) {
|
|
|
365
366
|
if (process.platform !== "win32") {
|
|
366
367
|
await fs.chmod(dir, 448);
|
|
367
368
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
await fs.
|
|
369
|
+
const tmp = `${target}.${randomUUID()}.tmp`;
|
|
370
|
+
try {
|
|
371
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
372
|
+
if (opts.secret && process.platform !== "win32") {
|
|
373
|
+
await fs.chmod(tmp, 384);
|
|
374
|
+
}
|
|
375
|
+
await fs.rename(tmp, target);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
await unlinkIfExists(tmp);
|
|
378
|
+
throw err;
|
|
371
379
|
}
|
|
372
380
|
}
|
|
373
381
|
async function unlinkIfExists(target) {
|
|
@@ -584,6 +592,37 @@ var TokenManager = class {
|
|
|
584
592
|
now;
|
|
585
593
|
cached = null;
|
|
586
594
|
refreshInFlight = null;
|
|
595
|
+
/**
|
|
596
|
+
* The in-flight disk read, shared by every concurrent caller.
|
|
597
|
+
*
|
|
598
|
+
* `refresh` below has always deduped; this did not, so N concurrent platform
|
|
599
|
+
* calls each issued their own `store.load()` before any of them populated
|
|
600
|
+
* `cached`. That is N redundant reads on every cold call, and it is what made
|
|
601
|
+
* a torn read reachable at all: one caller could be refreshing (and so
|
|
602
|
+
* rewriting the credential files) while the others were still reading them.
|
|
603
|
+
*
|
|
604
|
+
* The atomic write in credentials-store.ts is the real guarantee. This is the
|
|
605
|
+
* other half — it removes the redundant reads rather than merely surviving
|
|
606
|
+
* them.
|
|
607
|
+
*/
|
|
608
|
+
loadInFlight = null;
|
|
609
|
+
/**
|
|
610
|
+
* Bumped by clear(), so a read that started before it cannot publish its
|
|
611
|
+
* result to `cached` afterwards.
|
|
612
|
+
*
|
|
613
|
+
* Scope, precisely: the caller already awaiting that read still receives the
|
|
614
|
+
* pre-clear credentials as its own return value and proceeds with them —
|
|
615
|
+
* nothing can retract a value from a call already in flight. What the guard
|
|
616
|
+
* buys is that the value stops there instead of being cached, so no
|
|
617
|
+
* SUBSEQUENT caller is handed credentials the user asked us to forget.
|
|
618
|
+
*
|
|
619
|
+
* Dropping `loadInFlight` alone would not achieve even that: the awaiting
|
|
620
|
+
* caller resumes after the clear and would assign `cached` itself.
|
|
621
|
+
* Pre-existing — the old code assigned `cached` straight after an un-deduped
|
|
622
|
+
* `await` — but the fix belongs with the dedupe rather than left as a claim
|
|
623
|
+
* the comment cannot back up.
|
|
624
|
+
*/
|
|
625
|
+
loadGeneration = 0;
|
|
587
626
|
/**
|
|
588
627
|
* Set once the exchange endpoint rejects the API token outright.
|
|
589
628
|
*
|
|
@@ -616,17 +655,25 @@ var TokenManager = class {
|
|
|
616
655
|
}
|
|
617
656
|
async clear() {
|
|
618
657
|
this.cached = null;
|
|
658
|
+
this.loadInFlight = null;
|
|
659
|
+
this.loadGeneration += 1;
|
|
619
660
|
await this.store.clear();
|
|
620
661
|
}
|
|
621
662
|
async ensureLoaded() {
|
|
622
663
|
if (this.cached) {
|
|
623
664
|
return this.cached;
|
|
624
665
|
}
|
|
625
|
-
const
|
|
666
|
+
const generation = this.loadGeneration;
|
|
667
|
+
this.loadInFlight ??= this.store.load().finally(() => {
|
|
668
|
+
this.loadInFlight = null;
|
|
669
|
+
});
|
|
670
|
+
const loaded = await this.loadInFlight;
|
|
626
671
|
if (!loaded) {
|
|
627
672
|
throw new NeedsLoginError();
|
|
628
673
|
}
|
|
629
|
-
this.
|
|
674
|
+
if (generation === this.loadGeneration) {
|
|
675
|
+
this.cached = loaded;
|
|
676
|
+
}
|
|
630
677
|
return loaded;
|
|
631
678
|
}
|
|
632
679
|
async refresh(creds) {
|
|
@@ -1333,6 +1380,26 @@ var PlatformPreconditionRequiredError = class extends PlatformHttpError {
|
|
|
1333
1380
|
this.name = "PlatformPreconditionRequiredError";
|
|
1334
1381
|
}
|
|
1335
1382
|
};
|
|
1383
|
+
var QUOTA_EXCEEDED_CODE = "quota_exceeded";
|
|
1384
|
+
var PlatformQuotaError = class extends PlatformHttpError {
|
|
1385
|
+
/** The platform's machine-readable code, e.g. `quota_exceeded`. */
|
|
1386
|
+
code;
|
|
1387
|
+
constructor(message, status, details, code) {
|
|
1388
|
+
super(message, status, details);
|
|
1389
|
+
this.name = "PlatformQuotaError";
|
|
1390
|
+
this.code = code;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
function isPlatformQuotaError(err) {
|
|
1394
|
+
if (statusOf(err) !== 429) {
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
if (typeof err !== "object" || err === null) {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
const code = err.code;
|
|
1401
|
+
return typeof code === "string" && code.toLowerCase() === QUOTA_EXCEEDED_CODE;
|
|
1402
|
+
}
|
|
1336
1403
|
function isPlatformHttpError(err) {
|
|
1337
1404
|
return typeof err === "object" && err !== null && err[PLATFORM_ERROR_BRAND] === true;
|
|
1338
1405
|
}
|
|
@@ -1409,13 +1476,16 @@ function toConflictDetails(details) {
|
|
|
1409
1476
|
changedAt: stringField(details, "last_modified_at")
|
|
1410
1477
|
};
|
|
1411
1478
|
}
|
|
1412
|
-
function createPlatformError(message, status, details) {
|
|
1479
|
+
function createPlatformError(message, status, details, code) {
|
|
1413
1480
|
if (status === 409) {
|
|
1414
1481
|
return new PlatformConflictError(message, status, details, toConflictDetails(details));
|
|
1415
1482
|
}
|
|
1416
1483
|
if (status === 428) {
|
|
1417
1484
|
return new PlatformPreconditionRequiredError(message, details);
|
|
1418
1485
|
}
|
|
1486
|
+
if (status === 429 && typeof code === "string" && code.toLowerCase() === QUOTA_EXCEEDED_CODE) {
|
|
1487
|
+
return new PlatformQuotaError(message, status, details, code.toLowerCase());
|
|
1488
|
+
}
|
|
1419
1489
|
return new PlatformHttpError(message, status, details);
|
|
1420
1490
|
}
|
|
1421
1491
|
function truncate(text) {
|
|
@@ -1426,17 +1496,19 @@ async function throwFromResponse(response, fallbackMessage) {
|
|
|
1426
1496
|
const rawBody = await response.text().catch(() => "");
|
|
1427
1497
|
let message = fallbackMessage;
|
|
1428
1498
|
let details;
|
|
1499
|
+
let code;
|
|
1429
1500
|
if (rawBody) {
|
|
1430
1501
|
try {
|
|
1431
1502
|
const parsed = JSON.parse(rawBody);
|
|
1432
1503
|
details = parsed.details;
|
|
1504
|
+
code = typeof parsed.error === "string" ? parsed.error : void 0;
|
|
1433
1505
|
const suffix = details ? ` [details: ${JSON.stringify(details)}]` : "";
|
|
1434
1506
|
message = truncate(parsed.message ?? rawBody) + suffix;
|
|
1435
1507
|
} catch {
|
|
1436
1508
|
message = truncate(rawBody);
|
|
1437
1509
|
}
|
|
1438
1510
|
}
|
|
1439
|
-
throw createPlatformError(`HTTP ${response.status}: ${message}`, response.status, details);
|
|
1511
|
+
throw createPlatformError(`HTTP ${response.status}: ${message}`, response.status, details, code);
|
|
1440
1512
|
}
|
|
1441
1513
|
|
|
1442
1514
|
// ../../packages/shared/logger.ts
|
|
@@ -1644,6 +1716,10 @@ var BaseHttpClient = class {
|
|
|
1644
1716
|
this.logger.debug("Retrying after abort/timeout", { error: error.message });
|
|
1645
1717
|
return true;
|
|
1646
1718
|
}
|
|
1719
|
+
if (isPlatformQuotaError(error)) {
|
|
1720
|
+
this.logger.debug("Not retrying quota rejection", { status: 429 });
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1647
1723
|
const httpError = error;
|
|
1648
1724
|
if (httpError.status !== void 0) {
|
|
1649
1725
|
const shouldRetry = RETRYABLE_STATUS_CODES.includes(httpError.status);
|
|
@@ -1700,6 +1776,9 @@ function transformSpecFileResponse(raw) {
|
|
|
1700
1776
|
deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
|
|
1701
1777
|
};
|
|
1702
1778
|
}
|
|
1779
|
+
function toRevisionAuthorKind(raw) {
|
|
1780
|
+
return raw === "ai" ? "ai" : "human";
|
|
1781
|
+
}
|
|
1703
1782
|
function transformFileRevisionResponse(raw) {
|
|
1704
1783
|
return {
|
|
1705
1784
|
id: raw.id,
|
|
@@ -1710,6 +1789,7 @@ function transformFileRevisionResponse(raw) {
|
|
|
1710
1789
|
fileSizeBytes: raw.file_size_bytes,
|
|
1711
1790
|
checksum: raw.checksum,
|
|
1712
1791
|
changedBy: raw.changed_by,
|
|
1792
|
+
changedByKind: toRevisionAuthorKind(raw.changed_by_kind),
|
|
1713
1793
|
createdAt: new Date(raw.created_at)
|
|
1714
1794
|
};
|
|
1715
1795
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "0.4.0-next.
|
|
3
|
+
"version": "0.4.0-next.93",
|
|
4
4
|
"description": "MySpec MCP server — exposes MySpec platform projects, files and attachments to MCP-aware clients via OAuth-authenticated access tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|