@myspec/mcp-server 1.1.1-next.157 → 1.1.1-next.158
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 +270 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -316,15 +316,88 @@ function createFileCredentialsStore(paths = {}) {
|
|
|
316
316
|
},
|
|
317
317
|
async clear() {
|
|
318
318
|
await unlinkIfExists(oauthCredsPath);
|
|
319
|
-
}
|
|
319
|
+
},
|
|
320
|
+
withLock: (fn) => withFileLock(`${oauthCredsPath}.lock`, fn, paths.lockTimings),
|
|
321
|
+
saveSettings: (creds) => writeSettingsFile(settingsPath, creds)
|
|
320
322
|
};
|
|
321
323
|
}
|
|
324
|
+
function saveDiscoveredUrls(store, creds) {
|
|
325
|
+
return store.saveSettings ? store.saveSettings(creds) : store.save(creds);
|
|
326
|
+
}
|
|
327
|
+
var DEFAULT_LOCK_TIMINGS = { staleMs: 3e4, waitMs: 2e4, pollMs: 50 };
|
|
328
|
+
async function withFileLock(lockPath, fn, timings = DEFAULT_LOCK_TIMINGS) {
|
|
329
|
+
const owner = await acquireFileLock(lockPath, timings);
|
|
330
|
+
try {
|
|
331
|
+
return await fn();
|
|
332
|
+
} finally {
|
|
333
|
+
if (owner !== null) {
|
|
334
|
+
await releaseFileLock(lockPath, owner);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function acquireFileLock(lockPath, timings) {
|
|
339
|
+
const owner = randomUUID();
|
|
340
|
+
const deadline = Date.now() + timings.waitMs;
|
|
341
|
+
try {
|
|
342
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 448 });
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
for (; ; ) {
|
|
347
|
+
try {
|
|
348
|
+
await fs.writeFile(lockPath, owner, { flag: "wx", mode: 384 });
|
|
349
|
+
return owner;
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (!isFsExists(err)) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (Date.now() >= deadline) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const held = await readLock(lockPath);
|
|
359
|
+
if (held === null) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (held.ageMs > timings.staleMs) {
|
|
363
|
+
const again = await readLock(lockPath);
|
|
364
|
+
if (again?.owner !== held.owner) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
await fs.unlink(lockPath);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
if (!isFsNotFound(err)) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
await new Promise((resolve3) => setTimeout(resolve3, timings.pollMs));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async function readLock(lockPath) {
|
|
380
|
+
try {
|
|
381
|
+
const [stat2, owner] = await Promise.all([fs.stat(lockPath), fs.readFile(lockPath, "utf-8")]);
|
|
382
|
+
return { owner, ageMs: Date.now() - stat2.mtimeMs };
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function releaseFileLock(lockPath, owner) {
|
|
388
|
+
const current = await fs.readFile(lockPath, "utf-8").catch(() => null);
|
|
389
|
+
if (current === owner) {
|
|
390
|
+
await unlinkIfExists(lockPath).catch(() => void 0);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
322
393
|
function keepCredentialsOnFailure(store) {
|
|
323
394
|
return {
|
|
324
395
|
load: () => store.load(),
|
|
325
396
|
save: (creds) => store.save(creds),
|
|
326
397
|
clear: () => Promise.resolve(),
|
|
327
|
-
path: () => store.path()
|
|
398
|
+
path: () => store.path(),
|
|
399
|
+
...store.withLock ? { withLock: store.withLock } : {},
|
|
400
|
+
...store.saveSettings ? { saveSettings: store.saveSettings } : {}
|
|
328
401
|
};
|
|
329
402
|
}
|
|
330
403
|
async function writeSettingsFile(target, creds) {
|
|
@@ -596,15 +669,30 @@ function requireNumber(obj, key) {
|
|
|
596
669
|
function isFsNotFound(err) {
|
|
597
670
|
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
598
671
|
}
|
|
672
|
+
function isFsExists(err) {
|
|
673
|
+
return typeof err === "object" && err !== null && err.code === "EEXIST";
|
|
674
|
+
}
|
|
599
675
|
|
|
600
676
|
// src/auth/token-manager.ts
|
|
601
677
|
var EXPIRY_SKEW_MS = 3e4;
|
|
678
|
+
var REFRESH_TIMEOUT_MS = 8e3;
|
|
602
679
|
var TokenManager = class {
|
|
603
680
|
store;
|
|
604
681
|
fetchImpl;
|
|
605
682
|
now;
|
|
606
683
|
cached = null;
|
|
607
684
|
refreshInFlight = null;
|
|
685
|
+
/**
|
|
686
|
+
* Whether the refresh in flight is guaranteed to mint. An unforced one may
|
|
687
|
+
* settle for a token another process saved, which a forced caller must not
|
|
688
|
+
* be handed — see refresh().
|
|
689
|
+
*/
|
|
690
|
+
refreshInFlightForced = false;
|
|
691
|
+
/**
|
|
692
|
+
* The access token this instance last minted, so a forced caller that
|
|
693
|
+
* waited on an unforced refresh can tell whether it got a minted token.
|
|
694
|
+
*/
|
|
695
|
+
lastMintedAccessToken = null;
|
|
608
696
|
/**
|
|
609
697
|
* The in-flight disk read, shared by every concurrent caller.
|
|
610
698
|
*
|
|
@@ -652,18 +740,25 @@ var TokenManager = class {
|
|
|
652
740
|
}
|
|
653
741
|
async getValidAccessToken() {
|
|
654
742
|
const creds = await this.ensureLoaded();
|
|
655
|
-
if (
|
|
743
|
+
if (this.isFresh(creds)) {
|
|
656
744
|
return creds.accessToken;
|
|
657
745
|
}
|
|
658
|
-
const refreshed = await this.refresh(creds);
|
|
746
|
+
const refreshed = await this.refresh(creds, { forced: false });
|
|
659
747
|
return refreshed.accessToken;
|
|
660
748
|
}
|
|
661
749
|
async getCredentials() {
|
|
662
750
|
return this.ensureLoaded();
|
|
663
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Mints a new access token even if the current one has not expired — for
|
|
754
|
+
* callers whose token was just refused, or that need claims stamped after a
|
|
755
|
+
* server-side change (login re-minting after an organization switch). So,
|
|
756
|
+
* unlike getValidAccessToken, it never settles for a token another process
|
|
757
|
+
* already saved.
|
|
758
|
+
*/
|
|
664
759
|
async forceRefresh() {
|
|
665
760
|
const creds = await this.ensureLoaded();
|
|
666
|
-
const refreshed = await this.refresh(creds);
|
|
761
|
+
const refreshed = await this.refresh(creds, { forced: true });
|
|
667
762
|
return refreshed.accessToken;
|
|
668
763
|
}
|
|
669
764
|
async clear() {
|
|
@@ -689,16 +784,27 @@ var TokenManager = class {
|
|
|
689
784
|
}
|
|
690
785
|
return loaded;
|
|
691
786
|
}
|
|
692
|
-
|
|
787
|
+
isFresh(creds) {
|
|
788
|
+
return creds.expiresAt > this.now() + EXPIRY_SKEW_MS;
|
|
789
|
+
}
|
|
790
|
+
async refresh(creds, opts) {
|
|
693
791
|
if (this.refreshInFlight) {
|
|
694
|
-
|
|
792
|
+
if (this.refreshInFlightForced || !opts.forced) {
|
|
793
|
+
return this.refreshInFlight;
|
|
794
|
+
}
|
|
795
|
+
const settled = await this.refreshInFlight;
|
|
796
|
+
if (settled.accessToken === this.lastMintedAccessToken) {
|
|
797
|
+
return settled;
|
|
798
|
+
}
|
|
799
|
+
return this.refresh(this.cached ?? creds, opts);
|
|
695
800
|
}
|
|
696
|
-
this.
|
|
801
|
+
this.refreshInFlightForced = opts.forced;
|
|
802
|
+
this.refreshInFlight = this.doRefresh(creds, opts).finally(() => {
|
|
697
803
|
this.refreshInFlight = null;
|
|
698
804
|
});
|
|
699
805
|
return this.refreshInFlight;
|
|
700
806
|
}
|
|
701
|
-
async doRefresh(creds) {
|
|
807
|
+
async doRefresh(creds, opts) {
|
|
702
808
|
if (creds.apiToken) {
|
|
703
809
|
if (this.apiTokenRejection) {
|
|
704
810
|
throw this.apiTokenRejection;
|
|
@@ -710,18 +816,100 @@ var TokenManager = class {
|
|
|
710
816
|
"No credential is configured. Either set MYSPEC_API_TOKEN to a long-lived API token (or put one in the `apiToken` field of your credentials file) \u2014 create one in the MySpec webapp under the avatar menu, API tokens \u2014 or run `npx @myspec/mcp-server login` to sign in interactively."
|
|
711
817
|
);
|
|
712
818
|
}
|
|
819
|
+
return this.refreshSession(creds, opts);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Refreshes a login session that other processes may be refreshing too.
|
|
823
|
+
*
|
|
824
|
+
* Refresh tokens are single-use, and user-auth honours the previous one for
|
|
825
|
+
* only 60 seconds after a rotation. Every MCP client spawns its own `serve`,
|
|
826
|
+
* and `reverse` and `auth status` run beside them, all on the same
|
|
827
|
+
* ~/.myspec. This instance caches the credentials once, so when another
|
|
828
|
+
* process rotated the token, the cached one is already retired: presenting
|
|
829
|
+
* it gets a 401, and treating that as "signed out" used to delete the file —
|
|
830
|
+
* along with the valid token the other process had just saved.
|
|
831
|
+
*
|
|
832
|
+
* So the whole read → refresh → save runs under the store's lock, and the
|
|
833
|
+
* store is re-read first: a session rotated elsewhere is adopted (see
|
|
834
|
+
* newerSession), used as it is while its access token is fresh, otherwise
|
|
835
|
+
* refreshed with its token. While every process refreshing this file holds
|
|
836
|
+
* the lock, none can rotate in between. The lock is best-effort, though — a
|
|
837
|
+
* store without one, a lock not acquired in time or taken over as stale, an
|
|
838
|
+
* older CLI version — so a refusal is checked the same way, since a rotation
|
|
839
|
+
* can land while our request is in flight, and gets one retry with the newer
|
|
840
|
+
* token. Only then is the store cleared — and only if it still holds a token
|
|
841
|
+
* that was refused (see clearUnlessReplaced).
|
|
842
|
+
*/
|
|
843
|
+
refreshSession(cachedCreds, opts) {
|
|
844
|
+
const run = () => this.refreshSessionLocked(cachedCreds, opts);
|
|
845
|
+
return this.store.withLock ? this.store.withLock(run) : run();
|
|
846
|
+
}
|
|
847
|
+
async refreshSessionLocked(cachedCreds, opts) {
|
|
848
|
+
const refused = /* @__PURE__ */ new Set();
|
|
849
|
+
let creds = cachedCreds;
|
|
850
|
+
let onDisk = await this.readStore();
|
|
851
|
+
const newer = newerSession(onDisk, creds);
|
|
852
|
+
if (newer) {
|
|
853
|
+
this.cached = newer;
|
|
854
|
+
if (!opts.forced && this.isFresh(newer)) {
|
|
855
|
+
return newer;
|
|
856
|
+
}
|
|
857
|
+
creds = newer;
|
|
858
|
+
}
|
|
713
859
|
try {
|
|
714
|
-
return await this.performRefresh(creds);
|
|
860
|
+
return await this.performRefresh(creds, onDisk?.apiToken);
|
|
715
861
|
} catch (err) {
|
|
716
|
-
|
|
717
|
-
|
|
862
|
+
if (!(err instanceof NeedsLoginError)) {
|
|
863
|
+
throw err;
|
|
864
|
+
}
|
|
865
|
+
refused.add(creds.refreshToken ?? "");
|
|
866
|
+
onDisk = await this.readStore();
|
|
867
|
+
const raced = newerSession(onDisk, creds);
|
|
868
|
+
if (!raced) {
|
|
869
|
+
await this.clearUnlessReplaced(onDisk, refused);
|
|
870
|
+
throw err;
|
|
871
|
+
}
|
|
872
|
+
this.cached = raced;
|
|
873
|
+
if (!opts.forced && this.isFresh(raced)) {
|
|
874
|
+
return raced;
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
return await this.performRefresh(raced, onDisk?.apiToken);
|
|
878
|
+
} catch (retryErr) {
|
|
879
|
+
if (retryErr instanceof NeedsLoginError) {
|
|
880
|
+
refused.add(raced.refreshToken ?? "");
|
|
881
|
+
await this.clearUnlessReplaced(await this.readStore(), refused);
|
|
882
|
+
}
|
|
883
|
+
throw retryErr;
|
|
884
|
+
}
|
|
718
885
|
}
|
|
719
886
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
887
|
+
/**
|
|
888
|
+
* A fresh read of the store; null when it is empty or cannot be read, so a
|
|
889
|
+
* refresh that would work is not failed by it.
|
|
890
|
+
*/
|
|
891
|
+
readStore() {
|
|
892
|
+
return this.store.load().catch(() => null);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Signs out after a refusal: drops the cache and clears the store — unless
|
|
896
|
+
* the store no longer holds a refused token. An API-token record is shown
|
|
897
|
+
* exactly once and is never ours to delete, and a session we have not
|
|
898
|
+
* presented belongs to another process. A store that could not be read is
|
|
899
|
+
* left alone too: it may hold either (a hand edit with a typo, a bad file
|
|
900
|
+
* mode), and its own error is what the user needs to see. Network errors,
|
|
901
|
+
* 5xx and other transient faults never reach here: they must not delete the
|
|
902
|
+
* stored refresh token.
|
|
903
|
+
*/
|
|
904
|
+
async clearUnlessReplaced(onDisk, refused) {
|
|
905
|
+
this.cached = null;
|
|
906
|
+
if (!onDisk || onDisk.apiToken !== void 0) {
|
|
907
|
+
return;
|
|
724
908
|
}
|
|
909
|
+
if (onDisk.refreshToken !== void 0 && !refused.has(onDisk.refreshToken)) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
await this.store.clear();
|
|
725
913
|
}
|
|
726
914
|
/**
|
|
727
915
|
* Trades the long-lived API token for a short-lived access token.
|
|
@@ -774,15 +962,26 @@ var TokenManager = class {
|
|
|
774
962
|
expiresAt: this.now() + payload.expiresIn * 1e3
|
|
775
963
|
};
|
|
776
964
|
this.cached = next;
|
|
777
|
-
|
|
965
|
+
this.lastMintedAccessToken = next.accessToken;
|
|
966
|
+
const onDisk = await this.readStore();
|
|
967
|
+
if (onDisk?.apiToken === apiToken) {
|
|
968
|
+
await this.store.save({ ...onDisk, accessToken: next.accessToken, expiresAt: next.expiresAt });
|
|
969
|
+
}
|
|
778
970
|
return next;
|
|
779
971
|
}
|
|
780
|
-
|
|
972
|
+
/**
|
|
973
|
+
* @param keepApiToken An API token the store holds beside this session —
|
|
974
|
+
* added by hand while this process ran. Written back with the rotated pair,
|
|
975
|
+
* since save() rewrites the whole file and a PAT is shown exactly once; the
|
|
976
|
+
* cache stays on the session, so this process does not switch identity.
|
|
977
|
+
*/
|
|
978
|
+
async performRefresh(creds, keepApiToken) {
|
|
781
979
|
const url = `${creds.userAuthUrl}/api/auth/token/refresh`;
|
|
782
980
|
const response = await this.fetchImpl(url, {
|
|
783
981
|
method: "POST",
|
|
784
982
|
headers: { "Content-Type": "application/json" },
|
|
785
|
-
body: JSON.stringify({ refreshToken: creds.refreshToken })
|
|
983
|
+
body: JSON.stringify({ refreshToken: creds.refreshToken }),
|
|
984
|
+
signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS)
|
|
786
985
|
});
|
|
787
986
|
if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) {
|
|
788
987
|
throw new RefreshRejectedError(
|
|
@@ -793,7 +992,8 @@ var TokenManager = class {
|
|
|
793
992
|
const body = await response.text().catch(() => "");
|
|
794
993
|
throw new HttpStatusError(response.status, body, `Token refresh failed: HTTP ${String(response.status)}`);
|
|
795
994
|
}
|
|
796
|
-
const
|
|
995
|
+
const text = await response.text();
|
|
996
|
+
const payload = parseJson(text);
|
|
797
997
|
if (!payload || typeof payload.accessToken !== "string" || typeof payload.refreshToken !== "string" || typeof payload.expiresIn !== "number") {
|
|
798
998
|
throw new NeedsLoginError(
|
|
799
999
|
"Token refresh response was malformed. Run `npx @myspec/mcp-server login` to sign in again."
|
|
@@ -806,10 +1006,24 @@ var TokenManager = class {
|
|
|
806
1006
|
expiresAt: this.now() + payload.expiresIn * 1e3
|
|
807
1007
|
};
|
|
808
1008
|
this.cached = next;
|
|
809
|
-
|
|
1009
|
+
this.lastMintedAccessToken = next.accessToken;
|
|
1010
|
+
await this.store.save(keepApiToken === void 0 ? next : { ...next, apiToken: keepApiToken });
|
|
810
1011
|
return next;
|
|
811
1012
|
}
|
|
812
1013
|
};
|
|
1014
|
+
function parseJson(text) {
|
|
1015
|
+
try {
|
|
1016
|
+
return JSON.parse(text);
|
|
1017
|
+
} catch {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function newerSession(onDisk, creds) {
|
|
1022
|
+
if (onDisk?.refreshToken === void 0 || onDisk.refreshToken === creds.refreshToken || onDisk.userAuthUrl !== creds.userAuthUrl || onDisk.expiresAt <= creds.expiresAt) {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
return { ...onDisk, apiToken: void 0 };
|
|
1026
|
+
}
|
|
813
1027
|
var RefreshRejectedError = class extends NeedsLoginError {
|
|
814
1028
|
};
|
|
815
1029
|
var ApiTokenRejectedError = class extends ConfigError {
|
|
@@ -1136,9 +1350,13 @@ async function runLogin(options, deps = {}) {
|
|
|
1136
1350
|
webappUrl: config.webappUrl
|
|
1137
1351
|
});
|
|
1138
1352
|
const enriched = await discoverAfterLogin(credentials, config.webappUrl, deps.discover, log);
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1353
|
+
const persist = async () => {
|
|
1354
|
+
const prior = await store.load().catch(() => null);
|
|
1355
|
+
const merged = prior?.apiToken === void 0 ? enriched : { ...enriched, apiToken: prior.apiToken };
|
|
1356
|
+
await store.save(merged);
|
|
1357
|
+
return merged;
|
|
1358
|
+
};
|
|
1359
|
+
const preserved = await (store.withLock ? store.withLock(persist) : persist());
|
|
1142
1360
|
log(`Signed in as ${enriched.user?.email ?? "unknown"}.`);
|
|
1143
1361
|
log(`Credentials saved to ${store.path()}`);
|
|
1144
1362
|
await ensureActiveOrganization({
|
|
@@ -1221,9 +1439,25 @@ async function ensureActiveOrganization(args) {
|
|
|
1221
1439
|
log(`Active organization: ${mintedOrg}`);
|
|
1222
1440
|
}
|
|
1223
1441
|
function defaultRefreshTokens(store) {
|
|
1224
|
-
const tokenManager = new TokenManager({ store:
|
|
1442
|
+
const tokenManager = new TokenManager({ store: sessionOnly(store) });
|
|
1225
1443
|
return tokenManager.forceRefresh();
|
|
1226
1444
|
}
|
|
1445
|
+
function sessionOnly(store) {
|
|
1446
|
+
return {
|
|
1447
|
+
load: async () => {
|
|
1448
|
+
const creds = await store.load();
|
|
1449
|
+
return creds === null ? null : { ...creds, apiToken: void 0 };
|
|
1450
|
+
},
|
|
1451
|
+
save: async (creds) => {
|
|
1452
|
+
const current = await store.load();
|
|
1453
|
+
const apiToken = creds.apiToken ?? current?.apiToken;
|
|
1454
|
+
await store.save(apiToken === void 0 ? creds : { ...creds, apiToken });
|
|
1455
|
+
},
|
|
1456
|
+
clear: () => Promise.resolve(),
|
|
1457
|
+
path: () => store.path(),
|
|
1458
|
+
...store.withLock ? { withLock: store.withLock } : {}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1227
1461
|
function sameSlug(a, b) {
|
|
1228
1462
|
return a.toLowerCase() === b.toLowerCase();
|
|
1229
1463
|
}
|
|
@@ -1368,7 +1602,7 @@ async function resolveAuthStatus(deps) {
|
|
|
1368
1602
|
return { ...status, organization: { slug, name: match.name } };
|
|
1369
1603
|
}
|
|
1370
1604
|
function describeTokenFailure(err, userAuthUrl) {
|
|
1371
|
-
if (err instanceof TypeError) {
|
|
1605
|
+
if (err instanceof TypeError || err instanceof Error && err.name === "TimeoutError") {
|
|
1372
1606
|
return new ConfigError(
|
|
1373
1607
|
`Could not reach ${userAuthUrl} to check the sign-in: ${errorMessage2(err)}`
|
|
1374
1608
|
);
|
|
@@ -8504,10 +8738,13 @@ async function resolveReverseAgentUrl(sources) {
|
|
|
8504
8738
|
);
|
|
8505
8739
|
}
|
|
8506
8740
|
if (stored && sources.persist) {
|
|
8507
|
-
await sources.
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8741
|
+
const latest = await sources.loadStored().catch(() => null);
|
|
8742
|
+
if (latest) {
|
|
8743
|
+
await sources.persist(applyDiscoveredUrls(latest, config.webappUrl, discovered)).catch((err) => {
|
|
8744
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8745
|
+
warn(`myspec-mcp reverse: could not save discovered URLs (${message})`);
|
|
8746
|
+
});
|
|
8747
|
+
}
|
|
8511
8748
|
}
|
|
8512
8749
|
return { agentUrl: discovered.aiAgentMcpReverseUrl, discoveredVia: endpoint };
|
|
8513
8750
|
} catch (err) {
|
|
@@ -8615,7 +8852,7 @@ async function runReverseCommand(flags) {
|
|
|
8615
8852
|
};
|
|
8616
8853
|
forceRefresh = () => tokenManager.forceRefresh();
|
|
8617
8854
|
loadStored = () => store.load();
|
|
8618
|
-
persist = (creds) => store
|
|
8855
|
+
persist = (creds) => saveDiscoveredUrls(store, creds);
|
|
8619
8856
|
}
|
|
8620
8857
|
const resolved = await resolveReverseAgentUrl({
|
|
8621
8858
|
flagAgentUrl: flagString(flags, "agent-url"),
|
|
@@ -8668,9 +8905,9 @@ function createPlatformUrlResolver(deps) {
|
|
|
8668
8905
|
`Could not discover endpoints from ${configEndpoint(deps.webappUrl)} (response is missing platformUrl). Run \`npx @myspec/mcp-server login\` and retry.`
|
|
8669
8906
|
);
|
|
8670
8907
|
}
|
|
8671
|
-
const creds = await deps.
|
|
8908
|
+
const creds = await deps.store.load().catch(() => null);
|
|
8672
8909
|
if (creds) {
|
|
8673
|
-
await deps.store
|
|
8910
|
+
await saveDiscoveredUrls(deps.store, applyDiscoveredUrls(creds, deps.webappUrl, discovered)).catch((err) => {
|
|
8674
8911
|
const message = err instanceof Error ? err.message : String(err);
|
|
8675
8912
|
warn(`myspec-mcp: could not save discovered URLs (${message})`);
|
|
8676
8913
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "1.1.1-next.
|
|
3
|
+
"version": "1.1.1-next.158",
|
|
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": {
|