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

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 +49 -7
  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) {
@@ -4930,7 +4934,7 @@ function registerLocalFsTools(server, opts) {
4930
4934
  },
4931
4935
  {
4932
4936
  name: "local_pack_codebase",
4933
- description: "Pack the entire local codebase (under the granted --root) into a single XML bundle using `npx repomix@latest --style xml --compress`. Prefer this over many `local_fs_read_file` calls when you need broad codebase context. Output is paginated at ~2500 lines per page; a `<file>` block is never split across a page boundary, but the `<files>` root wrapper spans pages (its opener is on the first files page and its closer on the last) \u2014 always keep reading while `hasMore` is true rather than stopping at a `</files>`. The response includes `outputId`, `page`, `totalPages`, `totalLines`, `pageLines`, `hasMore`, and a `nextPage` hint. Use `local_pack_codebase_read_page` to fetch subsequent pages by `outputId`. Repeat calls with the same `subpath`/`includePatterns`/`ignorePatterns` return the same `outputId` and are served instantly from the 30-minute cache without re-running repomix.",
4937
+ description: "Pack the local codebase (under the granted --root) into a single compressed XML bundle (embedded repomix, `--style xml --compress`; file bodies use `\u22EE----` delimiters). Prefer this over many `local_fs_read_file` calls when you need broad codebase context. Output is paginated at ~2500 lines per page; a `<file>` block is never split across a page boundary, but the `<files>` root wrapper spans pages (its opener is on the first files page and its closer on the last) \u2014 never treat a missing `</files>` as truncation. The response includes `outputId`, `page`, `totalPages`, `totalLines`, `pageLines`, `hasMore`, and a `nextPage` hint; page 1 opens with the full directory structure. Fetch further pages with `local_pack_codebase_read_page` while `totalPages` is small; for a large pack (roughly 10+ pages) do NOT page through everything \u2014 use page 1 as the repo map, then re-pack narrowed with `subpath`/`includePatterns` or switch to `local_fs_grep`/`local_fs_read_file` for targeted reads. Repeat calls with the same `subpath`/`includePatterns`/`ignorePatterns` return the same `outputId` and are served instantly from the 30-minute cache without re-running repomix.",
4934
4938
  inputSchema: jsonSchemaFromZod(packCodebaseInput),
4935
4939
  handler: async (args) => {
4936
4940
  try {
@@ -4943,7 +4947,7 @@ function registerLocalFsTools(server, opts) {
4943
4947
  },
4944
4948
  {
4945
4949
  name: "local_pack_codebase_read_page",
4946
- description: "Read a specific page of a previously packed codebase output. Provide the `outputId` returned by `local_pack_codebase` and the 1-indexed `page` to fetch. Each page is up to ~2500 lines of XML; no `<file>` block is split across pages, but the `<files>` root wrapper spans pages \u2014 keep fetching while `hasMore` is true, do not stop at a `</files>`. Cache entries expire after 30 minutes; on cache miss, re-run `local_pack_codebase` (same args return the same outputId \u2014 re-pack is cheap, served from cache).",
4950
+ description: "Read a specific page of a previously packed codebase output. Provide the `outputId` returned by `local_pack_codebase` and the 1-indexed `page` to fetch. Each page is up to ~2500 lines of the compressed XML bundle; no `<file>` block is split across pages, but the `<files>` root wrapper spans pages \u2014 never treat a missing `</files>` as truncation. Cache entries expire after 30 minutes; on cache miss, re-run `local_pack_codebase` (same args return the same outputId \u2014 re-pack is cheap, served from cache).",
4947
4951
  inputSchema: jsonSchemaFromZod(packCodebaseReadPageInput),
4948
4952
  handler: (args) => {
4949
4953
  try {
@@ -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.96",
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": {