@myspec/mcp-server 0.4.0-next.92 → 0.4.0-next.94

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/index.js +56 -5
  2. 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
- await fs.writeFile(target, JSON.stringify(data, null, 2), { mode: 384 });
369
- if (opts.secret && process.platform !== "win32") {
370
- await fs.chmod(target, 384);
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 loaded = await this.store.load();
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.cached = loaded;
674
+ if (generation === this.loadGeneration) {
675
+ this.cached = loaded;
676
+ }
630
677
  return loaded;
631
678
  }
632
679
  async refresh(creds) {
@@ -1729,6 +1776,9 @@ function transformSpecFileResponse(raw) {
1729
1776
  deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
1730
1777
  };
1731
1778
  }
1779
+ function toRevisionAuthorKind(raw) {
1780
+ return raw === "ai" ? "ai" : "human";
1781
+ }
1732
1782
  function transformFileRevisionResponse(raw) {
1733
1783
  return {
1734
1784
  id: raw.id,
@@ -1739,6 +1789,7 @@ function transformFileRevisionResponse(raw) {
1739
1789
  fileSizeBytes: raw.file_size_bytes,
1740
1790
  checksum: raw.checksum,
1741
1791
  changedBy: raw.changed_by,
1792
+ changedByKind: toRevisionAuthorKind(raw.changed_by_kind),
1742
1793
  createdAt: new Date(raw.created_at)
1743
1794
  };
1744
1795
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.4.0-next.92",
3
+ "version": "0.4.0-next.94",
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": {