@myspec/mcp-server 0.2.0-next.79 → 0.2.0-next.80

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 (3) hide show
  1. package/README.md +86 -1
  2. package/dist/index.js +147 -97
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,6 +58,92 @@ value, including the dot** and paste it back into the terminal. Two traps:
58
58
  > keeping access until you run `logout`. The CLI prints the same warning with
59
59
  > the actual port, before the URL.
60
60
 
61
+ ### API tokens (unattended setup)
62
+
63
+ `login` is interactive — it opens a browser. For anything that has to start
64
+ without a person present (a server, a container, a CI job, a shared machine),
65
+ use an API token instead.
66
+
67
+ **1. Create the token.** In the MySpec webapp, open the avatar menu → **API
68
+ tokens** → **Create token**. Choose the organization it should act in, whether
69
+ it may write or only read, and an expiry. The token is shown **once**; it
70
+ cannot be retrieved afterwards.
71
+
72
+ **2. Configure it.** Credentials live in two files under `~/.myspec/`, both of
73
+ which the server needs:
74
+
75
+ `~/.myspec/oauth_creds.json` — the secret, **mode 0600** (the store refuses any
76
+ file readable by group or other):
77
+
78
+ ```json
79
+ { "apiToken": "msp_pat_…" }
80
+ ```
81
+
82
+ `~/.myspec/settings.json` — which auth server to talk to:
83
+
84
+ ```json
85
+ { "userAuthUrl": "https://auth.myspec.dev" }
86
+ ```
87
+
88
+ ```bash
89
+ chmod 600 ~/.myspec/oauth_creds.json
90
+ ```
91
+
92
+ `accessToken`, `expiresAt` and the discovered service URLs are filled in
93
+ automatically on first use, and an API-token credential needs no `user` block —
94
+ the owner is resolved server-side at exchange.
95
+
96
+ There is **no environment variable for an API token** — these files are the
97
+ only way to configure one.
98
+
99
+ **3. Verify it.** The token is exchanged for a short-lived access token on
100
+ every run, so a token that works here works in the server:
101
+
102
+ ```bash
103
+ curl -sS -X POST "https://auth.myspec.dev/api/auth/token/exchange" \
104
+ -H "x-api-key: msp_pat_…" -w '\n%{http_code}\n'
105
+ ```
106
+
107
+ `200` with an `accessToken` in the body means it is good. Otherwise:
108
+
109
+ | Status | Meaning |
110
+ |---|---|
111
+ | `401` | Unknown, revoked or expired token — deliberately indistinguishable. Create a new one. |
112
+ | `403` | The token's organization is gone, or its owner is no longer a member of it. Create a token for an organization you belong to. |
113
+ | `429` | Rate limited. `Retry-After` says how long to wait; the body's `scope` says whether the limit was per-address or per-token. |
114
+
115
+ #### How it differs from `login`
116
+
117
+ | | `login` | API token |
118
+ |---|---|---|
119
+ | Needs a browser | Yes | No |
120
+ | Credential rotates | Yes, on every refresh | No |
121
+ | Survives a missed write | No | Yes |
122
+ | Scope | Whatever the session can reach | One organization, fixed at creation |
123
+ | Access | Full | Read-only or read-write, fixed at creation |
124
+
125
+ An API token's organization and access mode cannot be changed after creation —
126
+ create a new token instead. Expiry *can* be extended, from the same page, and
127
+ doing so does not change the token value, so nothing needs reconfiguring.
128
+
129
+ #### Rolling back to a refresh token
130
+
131
+ If a token does not work and you need the deployment running again now:
132
+
133
+ 1. **Delete the `apiToken` field** from `~/.myspec/oauth_creds.json`. Emptying
134
+ it is not enough and neither is adding a refresh token alongside it: when an
135
+ `apiToken` is present it is used exclusively, and a rejected one fails the
136
+ run rather than falling back — silently downgrading to a different identity
137
+ would be worse than stopping.
138
+ 2. Restore the previous credential — restore a backup of the credentials file
139
+ if you took one, or run `login` again.
140
+
141
+ Do **not** use `logout` to roll back. It deletes the whole credentials file,
142
+ API token included.
143
+
144
+ Migrating an existing install off `MYSPEC_REFRESH_TOKEN`? See the
145
+ [migration runbook](../../docs/runbooks/mcp-api-token-migration.md).
146
+
61
147
  ### Active organization
62
148
 
63
149
  Every MySpec access token carries the slug of one **active organization**, and
@@ -136,7 +222,6 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
136
222
  | Variable | Purpose |
137
223
  |---|---|
138
224
  | `MYSPEC_USER_AUTH_URL` | user-auth base URL (default `https://auth.myspec.dev`). The webapp URL is derived from it; the platform / ai-agent URLs are discovered via the webapp. |
139
- | `MYSPEC_REFRESH_TOKEN` | Skip file-based credentials; the server mints a fresh access token on first use via this refresh token. The supported way to wire the MCP server up without running `npx @myspec/mcp-server login`. |
140
225
  | `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_spec_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
141
226
  | `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL for `reverse`; skips the webapp discovery call |
142
227
 
package/dist/index.js CHANGED
@@ -29,11 +29,13 @@ var HttpStatusError = class extends Error {
29
29
  };
30
30
  var BEARER_PATTERN = /Bearer\s+[A-Za-z0-9._\-+/=]+/g;
31
31
  var REFRESH_TOKEN_PATTERN = /"refreshToken"\s*:\s*"[^"]+"/g;
32
+ var API_TOKEN_PATTERN = /"apiToken"\s*:\s*"[^"]+"/g;
33
+ var API_TOKEN_VALUE_PATTERN = /\bmsp_pat_[A-Za-z0-9]+/g;
32
34
  var ACCESS_TOKEN_PATTERN = /"accessToken"\s*:\s*"[^"]+"/g;
33
35
  var SNAKE_TOKEN_PATTERN = /"(access_token|refresh_token|id_token)"\s*:\s*"[^"]+"/g;
34
36
  var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
35
37
  function redactSensitive(value) {
36
- return value.replace(BEARER_PATTERN, "Bearer [REDACTED]").replace(REFRESH_TOKEN_PATTERN, '"refreshToken":"[REDACTED]"').replace(ACCESS_TOKEN_PATTERN, '"accessToken":"[REDACTED]"').replace(SNAKE_TOKEN_PATTERN, (_match, key) => `"${key}":"[REDACTED]"`).replace(JWT_PATTERN, "[REDACTED_JWT]");
38
+ return value.replace(BEARER_PATTERN, "Bearer [REDACTED]").replace(REFRESH_TOKEN_PATTERN, '"refreshToken":"[REDACTED]"').replace(API_TOKEN_PATTERN, '"apiToken":"[REDACTED]"').replace(API_TOKEN_VALUE_PATTERN, "[REDACTED_API_TOKEN]").replace(ACCESS_TOKEN_PATTERN, '"accessToken":"[REDACTED]"').replace(SNAKE_TOKEN_PATTERN, (_match, key) => `"${key}":"[REDACTED]"`).replace(JWT_PATTERN, "[REDACTED_JWT]");
37
39
  }
38
40
  var ConfigError = class extends Error {
39
41
  constructor(message) {
@@ -284,6 +286,7 @@ function createFileCredentialsStore(paths = {}) {
284
286
  return {
285
287
  accessToken: oauth.accessToken,
286
288
  refreshToken: oauth.refreshToken,
289
+ apiToken: oauth.apiToken,
287
290
  expiresAt: oauth.expiresAt,
288
291
  userAuthUrl: settings.userAuthUrl,
289
292
  platformUrl: settings.platformUrl,
@@ -308,6 +311,10 @@ function createFileCredentialsStore(paths = {}) {
308
311
  {
309
312
  accessToken: creds.accessToken,
310
313
  refreshToken: creds.refreshToken,
314
+ // Carried through every save. Omitting it would delete a configured
315
+ // API token on the first successful exchange, since save rewrites
316
+ // the whole file.
317
+ apiToken: creds.apiToken,
311
318
  expiresAt: creds.expiresAt,
312
319
  user: creds.user
313
320
  },
@@ -369,64 +376,39 @@ async function unlinkIfExists(target) {
369
376
  }
370
377
  }
371
378
  }
372
- var DEFAULT_USER_AUTH_URL = "https://auth.myspec.dev";
373
- function readEnvRefreshConfig(env = process.env) {
374
- if (env.MYSPEC_ACCESS_TOKEN || env.MYSPEC_ACCESS_TOKEN_EXPIRES_AT) {
375
- throw new ConfigError(
376
- "MYSPEC_ACCESS_TOKEN / MYSPEC_ACCESS_TOKEN_EXPIRES_AT are not supported. The MCP server mints a fresh access token via MYSPEC_REFRESH_TOKEN; unset MYSPEC_ACCESS_TOKEN (and MYSPEC_ACCESS_TOKEN_EXPIRES_AT if set) and provide only MYSPEC_REFRESH_TOKEN."
379
+ function assertNoEnvAccessToken(env = process.env) {
380
+ if (env.MYSPEC_REFRESH_TOKEN) {
381
+ process.stderr.write(
382
+ "myspec-mcp: MYSPEC_REFRESH_TOKEN is set but no longer supported and is being ignored. Set an `apiToken` in ~/.myspec/oauth_creds.json, or run `npx @myspec/mcp-server login`. See docs/runbooks/mcp-api-token-migration.md\n"
377
383
  );
378
384
  }
379
- if (!env.MYSPEC_REFRESH_TOKEN) {
380
- return null;
385
+ if (!env.MYSPEC_ACCESS_TOKEN && !env.MYSPEC_ACCESS_TOKEN_EXPIRES_AT) {
386
+ return;
381
387
  }
382
- return {
383
- refreshToken: env.MYSPEC_REFRESH_TOKEN,
384
- userAuthUrl: env.MYSPEC_USER_AUTH_URL ?? DEFAULT_USER_AUTH_URL
385
- };
386
- }
387
- function createCompositeCredentialsStore(opts) {
388
- const { fileStore, envConfig } = opts;
389
- const envRefreshToken = envConfig?.refreshToken;
390
- const envUserAuthUrl = envConfig?.userAuthUrl ?? DEFAULT_USER_AUTH_URL;
391
- return {
392
- path: () => fileStore.path(),
393
- async load() {
394
- const fromFile = await fileStore.load();
395
- if (fromFile) {
396
- return fromFile;
397
- }
398
- if (!envRefreshToken) {
399
- return null;
400
- }
401
- return {
402
- accessToken: "",
403
- refreshToken: envRefreshToken,
404
- expiresAt: 0,
405
- userAuthUrl: envUserAuthUrl,
406
- user: { id: "env", email: "env@mcp" }
407
- };
408
- },
409
- save(creds) {
410
- return fileStore.save(creds);
411
- },
412
- clear() {
413
- return fileStore.clear();
414
- },
415
- envRefreshToken: () => envRefreshToken,
416
- envUserAuthUrl: () => envUserAuthUrl
417
- };
388
+ throw new ConfigError(
389
+ "MYSPEC_ACCESS_TOKEN / MYSPEC_ACCESS_TOKEN_EXPIRES_AT are not supported. Run `npx @myspec/mcp-server login`, or put a long-lived API token in the `apiToken` field of your credentials file, and unset these variables."
390
+ );
418
391
  }
419
392
  function parseOauthCreds(value) {
420
393
  if (typeof value !== "object" || value === null) {
421
394
  throw new Error("oauth_creds.json is malformed (not an object)");
422
395
  }
423
396
  const v = value;
424
- const accessToken = requireString(v, "accessToken");
425
- const refreshToken = requireString(v, "refreshToken");
426
- const expiresAt = requireNumber(v, "expiresAt");
397
+ const refreshToken = optionalString(v, "refreshToken");
398
+ const apiToken = optionalString(v, "apiToken");
399
+ if (!refreshToken && !apiToken) {
400
+ throw new NeedsLoginError(
401
+ "oauth_creds.json has no apiToken or refreshToken. Run `npx @myspec/mcp-server login` to sign in, or provision an API token."
402
+ );
403
+ }
404
+ const accessToken = optionalString(v, "accessToken") ?? "";
405
+ const expiresAt = v.expiresAt === void 0 ? 0 : requireNumber(v, "expiresAt");
427
406
  const userRaw = v.user;
428
407
  if (typeof userRaw !== "object" || userRaw === null) {
429
- throw new Error("oauth_creds.json is malformed (missing user)");
408
+ if (!apiToken) {
409
+ throw new Error("oauth_creds.json is malformed (missing user)");
410
+ }
411
+ return { accessToken, refreshToken, apiToken, expiresAt };
430
412
  }
431
413
  const userObj = userRaw;
432
414
  const user = {
@@ -434,7 +416,17 @@ function parseOauthCreds(value) {
434
416
  email: requireString(userObj, "email"),
435
417
  name: typeof userObj.name === "string" ? userObj.name : void 0
436
418
  };
437
- return { accessToken, refreshToken, expiresAt, user };
419
+ return { accessToken, refreshToken, apiToken, expiresAt, user };
420
+ }
421
+ function optionalString(v, key) {
422
+ const raw = v[key];
423
+ if (raw === void 0 || raw === null) {
424
+ return void 0;
425
+ }
426
+ if (typeof raw !== "string") {
427
+ throw new Error(`Credentials file is malformed (invalid ${key})`);
428
+ }
429
+ return raw.length === 0 ? void 0 : raw;
438
430
  }
439
431
  function parseSettings(value) {
440
432
  if (typeof value !== "object" || value === null) {
@@ -471,14 +463,21 @@ var TokenManager = class {
471
463
  store;
472
464
  fetchImpl;
473
465
  now;
474
- envFallback;
475
466
  cached = null;
476
467
  refreshInFlight = null;
468
+ /**
469
+ * Set once the exchange endpoint rejects the API token outright.
470
+ *
471
+ * The rejection deliberately does not wipe the credential, so without this
472
+ * every subsequent tool call would re-POST a known-bad token to the endpoint
473
+ * the server rate-limits as its brute-force control. Latching turns a loop
474
+ * into one failed request.
475
+ */
476
+ apiTokenRejection = null;
477
477
  constructor(deps) {
478
478
  this.store = deps.store;
479
479
  this.fetchImpl = deps.fetchImpl ?? fetch;
480
480
  this.now = deps.now ?? Date.now;
481
- this.envFallback = deps.envFallback ?? null;
482
481
  }
483
482
  async getValidAccessToken() {
484
483
  const creds = await this.ensureLoaded();
@@ -521,27 +520,20 @@ var TokenManager = class {
521
520
  return this.refreshInFlight;
522
521
  }
523
522
  async doRefresh(creds) {
523
+ if (creds.apiToken) {
524
+ if (this.apiTokenRejection) {
525
+ throw this.apiTokenRejection;
526
+ }
527
+ return this.performExchange(creds, creds.apiToken);
528
+ }
529
+ if (!creds.refreshToken) {
530
+ throw new NeedsLoginError(
531
+ "No credential is configured. Either run `npx @myspec/mcp-server login` to sign in interactively, or add a long-lived API token to the `apiToken` field of your credentials file \u2014 create one in the MySpec webapp under the avatar menu, API tokens."
532
+ );
533
+ }
524
534
  try {
525
535
  return await this.performRefresh(creds);
526
536
  } catch (err) {
527
- if (err instanceof RefreshRejectedError && this.envFallback && this.envFallback.refreshToken !== creds.refreshToken) {
528
- const bootstrap = {
529
- accessToken: "",
530
- refreshToken: this.envFallback.refreshToken,
531
- expiresAt: 0,
532
- userAuthUrl: this.envFallback.userAuthUrl,
533
- platformUrl: creds.platformUrl,
534
- webappUrl: creds.webappUrl,
535
- aiAgentMcpReverseUrl: creds.aiAgentMcpReverseUrl,
536
- user: creds.user
537
- };
538
- try {
539
- return await this.performRefresh(bootstrap);
540
- } catch (retryErr) {
541
- await this.invalidateOnAuthFailure(retryErr);
542
- throw retryErr;
543
- }
544
- }
545
537
  await this.invalidateOnAuthFailure(err);
546
538
  throw err;
547
539
  }
@@ -552,6 +544,52 @@ var TokenManager = class {
552
544
  await this.store.clear();
553
545
  }
554
546
  }
547
+ /**
548
+ * Trades the long-lived API token for a short-lived access token.
549
+ *
550
+ * Unlike refresh, nothing here is persisted except the new access token and
551
+ * its expiry — the API token is unchanged by the exchange, so re-saving it
552
+ * would be a no-op and expecting a rotated value back would be wrong.
553
+ */
554
+ async performExchange(creds, apiToken) {
555
+ const url = `${creds.userAuthUrl}/api/auth/token/exchange`;
556
+ const response = await this.fetchImpl(url, {
557
+ method: "POST",
558
+ headers: { "x-api-key": apiToken }
559
+ });
560
+ if (response.status === 401 || response.status === 403) {
561
+ const reason = response.status === 403 ? "the token owner no longer has access to its organization" : "the token is invalid, disabled, or expired";
562
+ const rejection = new ApiTokenRejectedError(
563
+ `API token exchange failed: ${reason}. Create a new API token in MySpec and write it to the apiToken field of your credentials file.`
564
+ );
565
+ this.apiTokenRejection = rejection;
566
+ throw rejection;
567
+ }
568
+ if (!response.ok) {
569
+ const body = await response.text().catch(() => "");
570
+ throw new HttpStatusError(
571
+ response.status,
572
+ body,
573
+ `Token exchange failed: HTTP ${String(response.status)}`
574
+ );
575
+ }
576
+ const payload = await response.json().catch(() => null);
577
+ if (!payload || typeof payload.accessToken !== "string" || typeof payload.expiresIn !== "number") {
578
+ throw new HttpStatusError(
579
+ response.status,
580
+ "",
581
+ "Token exchange response was malformed."
582
+ );
583
+ }
584
+ const next = {
585
+ ...creds,
586
+ accessToken: payload.accessToken,
587
+ expiresAt: this.now() + payload.expiresIn * 1e3
588
+ };
589
+ this.cached = next;
590
+ await this.store.save(next);
591
+ return next;
592
+ }
555
593
  async performRefresh(creds) {
556
594
  const url = `${creds.userAuthUrl}/api/auth/token/refresh`;
557
595
  const response = await this.fetchImpl(url, {
@@ -587,6 +625,8 @@ var TokenManager = class {
587
625
  };
588
626
  var RefreshRejectedError = class extends NeedsLoginError {
589
627
  };
628
+ var ApiTokenRejectedError = class extends ConfigError {
629
+ };
590
630
 
591
631
  // src/auth/organization.ts
592
632
  import readline2 from "readline";
@@ -797,11 +837,11 @@ async function discoverConfig(opts) {
797
837
  } catch {
798
838
  throw discoveryError(endpoint, "response is not valid JSON", hint);
799
839
  }
800
- const platformUrl = optionalString(body.platformUrl);
840
+ const platformUrl = optionalString2(body.platformUrl);
801
841
  if (platformUrl !== void 0) {
802
842
  assertHttpUrl(platformUrl, endpoint, hint);
803
843
  }
804
- const aiAgentMcpReverseUrl = optionalString(body.aiAgentMcpReverseUrl);
844
+ const aiAgentMcpReverseUrl = optionalString2(body.aiAgentMcpReverseUrl);
805
845
  if (aiAgentMcpReverseUrl !== void 0) {
806
846
  assertWebSocketUrl(aiAgentMcpReverseUrl, endpoint, hint);
807
847
  }
@@ -827,7 +867,7 @@ function fetchConfig(endpoint, token, fetchImpl) {
827
867
  headers: { Authorization: `Bearer ${token}` }
828
868
  });
829
869
  }
830
- function optionalString(value) {
870
+ function optionalString2(value) {
831
871
  return typeof value === "string" && value.length > 0 ? value : void 0;
832
872
  }
833
873
  function assertHttpUrl(value, endpoint, hint) {
@@ -887,11 +927,13 @@ async function runLogin(options, deps = {}) {
887
927
  webappUrl: config.webappUrl
888
928
  });
889
929
  const enriched = await discoverAfterLogin(credentials, config.webappUrl, deps.discover, log);
890
- await store.save(enriched);
891
- log(`Signed in as ${enriched.user.email}.`);
930
+ const prior = await store.load().catch(() => null);
931
+ const preserved = prior?.apiToken === void 0 ? enriched : { ...enriched, apiToken: prior.apiToken };
932
+ await store.save(preserved);
933
+ log(`Signed in as ${enriched.user?.email ?? "unknown"}.`);
892
934
  log(`Credentials saved to ${store.path()}`);
893
935
  await ensureActiveOrganization({
894
- credentials: enriched,
936
+ credentials: preserved,
895
937
  webappUrl: config.webappUrl,
896
938
  requestedSlug: options.org,
897
939
  store,
@@ -1024,11 +1066,31 @@ async function runLogout() {
1024
1066
  "Content-Type": "application/json",
1025
1067
  Authorization: `Bearer ${existing.accessToken}`
1026
1068
  },
1027
- body: JSON.stringify({ refreshToken: existing.refreshToken }),
1069
+ // Only session-derived credentials have a server-side session to end.
1070
+ // An API-token credential has none, so there is nothing to revoke here —
1071
+ // revoking the token itself is done from the MySpec web UI.
1072
+ body: JSON.stringify(
1073
+ existing.refreshToken ? { refreshToken: existing.refreshToken } : {}
1074
+ ),
1028
1075
  signal: AbortSignal.timeout(5e3)
1029
1076
  });
1030
1077
  } catch {
1031
1078
  }
1079
+ if (existing.apiToken) {
1080
+ await store.save({
1081
+ apiToken: existing.apiToken,
1082
+ accessToken: "",
1083
+ expiresAt: 0,
1084
+ userAuthUrl: existing.userAuthUrl,
1085
+ ...existing.platformUrl ? { platformUrl: existing.platformUrl } : {},
1086
+ ...existing.webappUrl ? { webappUrl: existing.webappUrl } : {},
1087
+ ...existing.aiAgentMcpReverseUrl ? { aiAgentMcpReverseUrl: existing.aiAgentMcpReverseUrl } : {}
1088
+ });
1089
+ process.stderr.write(
1090
+ "Signed out. The configured API token was kept \u2014 delete the `apiToken` field in ~/.myspec/oauth_creds.json to remove it.\n"
1091
+ );
1092
+ return;
1093
+ }
1032
1094
  await store.clear();
1033
1095
  process.stderr.write("Signed out and cleared local credentials.\n");
1034
1096
  }
@@ -4713,7 +4775,7 @@ Press Ctrl-C to stop. Auto-reconnect enabled.
4713
4775
  const message = err instanceof Error ? err.message : String(err);
4714
4776
  process.stderr.write(
4715
4777
  `myspec-mcp reverse: cannot obtain access token: ${message}
4716
- Run \`npx @myspec/mcp-server login\` (or set MYSPEC_REFRESH_TOKEN) and try again.
4778
+ Run \`npx @myspec/mcp-server login\`, or configure an API token, and try again.
4717
4779
  `
4718
4780
  );
4719
4781
  return;
@@ -5015,10 +5077,7 @@ function printHelp() {
5015
5077
  "",
5016
5078
  "Environment:",
5017
5079
  " MYSPEC_USER_AUTH_URL user-auth base URL (default https://auth.myspec.dev)",
5018
- " MYSPEC_AI_AGENT_WS_URL ai-agent WebSocket URL for `reverse` (skips discovery)",
5019
- " MYSPEC_REFRESH_TOKEN Skip file-based credentials; the server mints a",
5020
- " fresh access token on first use via this refresh",
5021
- " token."
5080
+ " MYSPEC_AI_AGENT_WS_URL ai-agent WebSocket URL for `reverse` (skips discovery)"
5022
5081
  ];
5023
5082
  process.stderr.write(lines.join("\n") + "\n");
5024
5083
  }
@@ -5126,12 +5185,9 @@ async function runReverseCommand(flags) {
5126
5185
  loadStored = () => Promise.resolve(null);
5127
5186
  persist = void 0;
5128
5187
  } else {
5129
- const envConfig = readEnvRefreshConfig();
5130
- const store = createCompositeCredentialsStore({
5131
- fileStore: createFileCredentialsStore(),
5132
- envConfig
5133
- });
5134
- const tokenManager = new TokenManager({ store, envFallback: envConfig });
5188
+ assertNoEnvAccessToken();
5189
+ const store = createFileCredentialsStore();
5190
+ const tokenManager = new TokenManager({ store });
5135
5191
  getAccessToken = () => tokenManager.getValidAccessToken();
5136
5192
  onAuthFailed = async () => {
5137
5193
  await tokenManager.forceRefresh();
@@ -5198,25 +5254,19 @@ function createPlatformUrlResolver(deps) {
5198
5254
  };
5199
5255
  }
5200
5256
  async function runServe(flags) {
5201
- const envConfig = readEnvRefreshConfig();
5202
- const store = createCompositeCredentialsStore({
5203
- fileStore: createFileCredentialsStore(),
5204
- envConfig
5205
- });
5257
+ assertNoEnvAccessToken();
5258
+ const store = createFileCredentialsStore();
5206
5259
  const initial = await store.load();
5207
5260
  if (!initial) {
5208
5261
  process.stderr.write(
5209
- "myspec-mcp: starting unauthenticated. Run `npx @myspec/mcp-server login` (or set MYSPEC_REFRESH_TOKEN) to enable tools.\n"
5262
+ "myspec-mcp: starting unauthenticated. Run `npx @myspec/mcp-server login`, or configure an API token, to enable tools.\n"
5210
5263
  );
5211
5264
  }
5212
5265
  const config = resolveConfig({
5213
5266
  cliUserAuthUrl: flagString(flags, "user-auth-url"),
5214
5267
  storedUserAuthUrl: initial?.userAuthUrl
5215
5268
  });
5216
- const tokenManager = new TokenManager({
5217
- store,
5218
- envFallback: envConfig
5219
- });
5269
+ const tokenManager = new TokenManager({ store });
5220
5270
  const client = new PlatformClient({
5221
5271
  resolveBaseUrl: createPlatformUrlResolver({
5222
5272
  store,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.2.0-next.79",
3
+ "version": "0.2.0-next.80",
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": {