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

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 +12 -4
  2. package/dist/index.js +47 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -101,9 +101,17 @@ claude mcp add --scope user myspec \
101
101
  ```
102
102
 
103
103
  Nothing else is required — no `login`, no files. **For a non-production token,
104
- set `MYSPEC_USER_AUTH_URL` too** (e.g. `https://dev-auth.myspec.dev`); a dev
105
- token against the production default fails with a bare 401 that reads like a bad
106
- token.
104
+ set `MYSPEC_USER_AUTH_URL` too** (e.g. `https://dev-auth.myspec.dev`) unless
105
+ `~/.myspec/settings.json` already names that environment, which the server reads
106
+ even with no `oauth_creds.json` beside it; a dev token against the production
107
+ default fails with a 401 that reads like a bad token until you read the host it
108
+ names.
109
+
110
+ That cuts both ways: set it explicitly for a **production** token on a machine
111
+ that was ever signed in to dev. `login` writes `settings.json` and `logout` does
112
+ not remove it, so the leftover `userAuthUrl` decides the exchange either way.
113
+ The 401 names the host it asked, so the wrong environment is visible rather than
114
+ looking like a rejected token.
107
115
 
108
116
  The server writes `~/.myspec/settings.json` so restarts skip endpoint discovery.
109
117
  It never writes the token to disk, so unsetting the variable removes the
@@ -272,7 +280,7 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
272
280
  | Variable | Purpose |
273
281
  |---|---|
274
282
  | `MYSPEC_API_TOKEN` | Long-lived MySpec API token (`msp_pat_…`) for unattended use. Takes precedence over anything in `~/.myspec/oauth_creds.json` and is never written to disk. Needs 0.3.0+. |
275
- | `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. |
283
+ | `MYSPEC_USER_AUTH_URL` | user-auth base URL (`--user-auth-url` > this > `userAuthUrl` in `~/.myspec/settings.json` > `https://auth.myspec.dev`). The webapp URL is derived from it; the platform / ai-agent URLs are discovered via the webapp. |
276
284
  | `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_spec_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
277
285
  | `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL for `reverse`; skips the webapp discovery call |
278
286
 
package/dist/index.js CHANGED
@@ -360,6 +360,10 @@ async function loadSettings(target) {
360
360
  throw err;
361
361
  }
362
362
  }
363
+ async function readSettingsUserAuthUrl(settingsPath = defaultSettingsPath()) {
364
+ const settings = await loadSettings(settingsPath);
365
+ return settings?.userAuthUrl;
366
+ }
363
367
  async function writeJsonFile(target, data, opts) {
364
368
  const dir = path.dirname(target);
365
369
  await fs.mkdir(dir, { recursive: true, mode: 448 });
@@ -726,7 +730,7 @@ var TokenManager = class {
726
730
  if (response.status === 401 || response.status === 403) {
727
731
  const reason = response.status === 403 ? "the token owner no longer has access to its organization" : "the token is invalid, disabled, or expired";
728
732
  const rejection = new ApiTokenRejectedError(
729
- `API token exchange failed: ${reason}. Create a new API token in MySpec and set it as MYSPEC_API_TOKEN (or write it to the apiToken field of your credentials file).`
733
+ `API token exchange failed at ${url}: ${reason}. Check that URL names the environment the token was created in. With MYSPEC_API_TOKEN it comes from MYSPEC_USER_AUTH_URL (or --user-auth-url), then userAuthUrl in ~/.myspec/settings.json, defaulting to https://auth.myspec.dev; with an apiToken in oauth_creds.json it comes from that settings.json value alone, which only \`npx @myspec/mcp-server login --user-auth-url <url>\` rewrites. If the host is right, create a new API token in MySpec and set it as MYSPEC_API_TOKEN (or write it to the apiToken field of your credentials file).`
730
734
  );
731
735
  this.apiTokenRejection = rejection;
732
736
  throw rejection;
@@ -736,7 +740,7 @@ var TokenManager = class {
736
740
  throw new HttpStatusError(
737
741
  response.status,
738
742
  body,
739
- `Token exchange failed: ${url} returned 404, so no exchange endpoint is there. Check that URL names the environment the token was created in. With MYSPEC_API_TOKEN it comes from MYSPEC_USER_AUTH_URL (or --user-auth-url), defaulting to https://auth.myspec.dev. Otherwise it comes from userAuthUrl in ~/.myspec/settings.json, which only \`npx @myspec/mcp-server login --user-auth-url <url>\` rewrites \u2014 the flag and the variable do not redirect a file-configured exchange on their own, and userAuthUrl in oauth_creds.json is ignored entirely.`
743
+ `Token exchange failed: ${url} returned 404, so no exchange endpoint is there. Check that URL names the environment the token was created in. With MYSPEC_API_TOKEN it comes from MYSPEC_USER_AUTH_URL (or --user-auth-url), then userAuthUrl in ~/.myspec/settings.json, defaulting to https://auth.myspec.dev. Otherwise it comes from that same settings.json value, which only \`npx @myspec/mcp-server login --user-auth-url <url>\` rewrites \u2014 the flag and the variable do not redirect a file-configured exchange on their own, and userAuthUrl in oauth_creds.json is ignored entirely.`
740
744
  );
741
745
  }
742
746
  if (!response.ok) {
@@ -5487,21 +5491,59 @@ async function resolveReverseAgentUrl(sources) {
5487
5491
  throw err;
5488
5492
  }
5489
5493
  }
5494
+ function ignoringSettings(settingsPath, reason) {
5495
+ return `myspec-mcp: ignoring the userAuthUrl in ${settingsPath} for the MYSPEC_API_TOKEN exchange URL \u2014 ${reason}.`;
5496
+ }
5497
+ function usableAuthUrl(candidate, settingsPath, warn) {
5498
+ if (candidate === void 0) {
5499
+ return void 0;
5500
+ }
5501
+ try {
5502
+ resolveConfig({ env: {}, storedUserAuthUrl: candidate });
5503
+ return candidate;
5504
+ } catch (err) {
5505
+ if (!(err instanceof ConfigError)) {
5506
+ throw err;
5507
+ }
5508
+ warn(ignoringSettings(settingsPath, err.message));
5509
+ return void 0;
5510
+ }
5511
+ }
5490
5512
  async function buildCredentialsStore(opts = {}) {
5491
5513
  const env = opts.env ?? process.env;
5492
- const fileStore = opts.fileStore ?? createFileCredentialsStore();
5514
+ const settingsPath = opts.settingsPath ?? defaultSettingsPath();
5515
+ const fileStore = opts.fileStore ?? createFileCredentialsStore({ settingsPath });
5516
+ const warn = opts.warn ?? ((message) => {
5517
+ process.stderr.write(message + "\n");
5518
+ });
5493
5519
  const usingEnvApiToken = hasEnvApiToken(env);
5494
5520
  const tolerate = opts.toleratesUnreadableFile ?? usingEnvApiToken;
5495
5521
  const stored = tolerate ? await fileStore.load().catch(() => null) : await fileStore.load();
5522
+ const overridden = (opts.cliUserAuthUrl ?? env.MYSPEC_USER_AUTH_URL) !== void 0;
5523
+ const fromSettings = usingEnvApiToken && !overridden && stored?.userAuthUrl === void 0 ? await readSettingsUserAuthUrl(settingsPath).catch((err) => {
5524
+ const message = err instanceof Error ? err.message : String(err);
5525
+ warn(ignoringSettings(settingsPath, `it could not be read (${message})`));
5526
+ return void 0;
5527
+ }) : void 0;
5496
5528
  const config = resolveConfig({
5497
5529
  env,
5498
5530
  cliUserAuthUrl: opts.cliUserAuthUrl,
5499
- storedUserAuthUrl: stored?.userAuthUrl
5531
+ // Validated on its own before it joins the chain, for the same reason the
5532
+ // unreadable file above is forgiven: parseSettings only requires a
5533
+ // non-empty string, so a hand-written `htps://…` or a bare host in a
5534
+ // settings.json that is not even the credential would otherwise turn a
5535
+ // pure-env install that used to start on the prod default into a
5536
+ // ConfigError exit — and one that never names the file it came from.
5537
+ storedUserAuthUrl: stored?.userAuthUrl ?? usableAuthUrl(fromSettings, settingsPath, warn)
5500
5538
  });
5501
5539
  return {
5502
5540
  store: createEnvApiTokenStore({
5503
5541
  fileStore,
5504
- envConfig: readEnvApiToken(config.userAuthUrl, env)
5542
+ // `warn` rather than the default sink, so the "cannot be sent as a
5543
+ // header" warning lands wherever the caller's other warnings do.
5544
+ envConfig: readEnvApiToken(config.userAuthUrl, env, warn),
5545
+ settingsPath,
5546
+ warn
5505
5547
  }),
5506
5548
  config,
5507
5549
  usingEnvApiToken
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.4.0-next.94",
3
+ "version": "0.4.0-next.95",
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": {