@myspec/mcp-server 0.1.0-next.3 → 0.1.0-next.4

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 +6 -1
  2. package/dist/index.js +108 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,7 +42,12 @@ npx @myspec/mcp-server@next reverse --root /path/to/project
42
42
  | `--agent-url <url>` / `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL (default `ws://localhost:3001/mcp/reverse`). |
43
43
  | `--access-token <jwt>` / `MYSPEC_ACCESS_TOKEN` | Use a static access token for local testing instead of the saved credentials. Otherwise `reverse` uses the refresh token from `npx @myspec/mcp-server login` and auto-refreshes. |
44
44
 
45
- Credentials are persisted at `$XDG_CONFIG_HOME/myspec/mcp-server.json` (POSIX) or `%APPDATA%\myspec\mcp-server.json` (Windows), mode `0600`.
45
+ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), split into two files:
46
+
47
+ - `settings.json` — non-secret config: the auth-server and platform URLs you logged in with.
48
+ - `oauth_creds.json` — the OAuth tokens (access/refresh/expiry) and your user identity, written mode `0600`.
49
+
50
+ `logout` removes `oauth_creds.json`; `settings.json` is left in place.
46
51
 
47
52
  ## Environment variables
48
53
 
package/dist/index.js CHANGED
@@ -282,57 +282,112 @@ async function defaultPrompt(question) {
282
282
  import { promises as fs } from "fs";
283
283
  import os from "os";
284
284
  import path from "path";
285
- function defaultCredentialsPath(env = process.env) {
286
- if (process.platform === "win32") {
287
- const appData = env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
288
- return path.join(appData, "myspec", "mcp-server.json");
289
- }
290
- const xdg = env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
291
- return path.join(xdg, "myspec", "mcp-server.json");
285
+ function myspecDir() {
286
+ return path.join(os.homedir(), ".myspec");
287
+ }
288
+ function defaultSettingsPath() {
289
+ return path.join(myspecDir(), "settings.json");
292
290
  }
293
- function createFileCredentialsStore(filePath) {
294
- const target = filePath ?? defaultCredentialsPath();
291
+ function defaultOauthCredsPath() {
292
+ return path.join(myspecDir(), "oauth_creds.json");
293
+ }
294
+ function createFileCredentialsStore(paths = {}) {
295
+ const settingsPath = paths.settingsPath ?? defaultSettingsPath();
296
+ const oauthCredsPath = paths.oauthCredsPath ?? defaultOauthCredsPath();
295
297
  return {
296
- path: () => target,
298
+ // The secret bundle is the credential file users care about (`login`
299
+ // prints this); settings.json sits next to it.
300
+ path: () => oauthCredsPath,
297
301
  async load() {
298
- try {
299
- if (process.platform !== "win32") {
300
- const stat = await fs.stat(target);
301
- if ((stat.mode & 63) !== 0) {
302
- throw new Error(
303
- `Refusing to load credentials from ${target}: file mode is too permissive (${(stat.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
304
- );
305
- }
306
- }
307
- const raw = await fs.readFile(target, "utf-8");
308
- const parsed = JSON.parse(raw);
309
- return parseCredentials(parsed);
310
- } catch (err) {
311
- if (isFsNotFound(err)) {
312
- return null;
313
- }
314
- throw err;
302
+ const oauth = await loadOauthCreds(oauthCredsPath);
303
+ if (!oauth) {
304
+ return null;
305
+ }
306
+ const settings = await loadSettings(settingsPath);
307
+ if (!settings) {
308
+ return null;
315
309
  }
310
+ return {
311
+ accessToken: oauth.accessToken,
312
+ refreshToken: oauth.refreshToken,
313
+ expiresAt: oauth.expiresAt,
314
+ userAuthUrl: settings.userAuthUrl,
315
+ platformUrl: settings.platformUrl,
316
+ user: oauth.user
317
+ };
316
318
  },
317
319
  async save(creds) {
318
- const dir = path.dirname(target);
319
- await fs.mkdir(dir, { recursive: true, mode: 448 });
320
- await fs.writeFile(target, JSON.stringify(creds, null, 2), { mode: 384 });
321
- if (process.platform !== "win32") {
322
- await fs.chmod(target, 384);
323
- }
320
+ await writeJsonFile(
321
+ settingsPath,
322
+ { userAuthUrl: creds.userAuthUrl, platformUrl: creds.platformUrl },
323
+ { secret: false }
324
+ );
325
+ await writeJsonFile(
326
+ oauthCredsPath,
327
+ {
328
+ accessToken: creds.accessToken,
329
+ refreshToken: creds.refreshToken,
330
+ expiresAt: creds.expiresAt,
331
+ user: creds.user
332
+ },
333
+ { secret: true }
334
+ );
324
335
  },
325
336
  async clear() {
326
- try {
327
- await fs.unlink(target);
328
- } catch (err) {
329
- if (!isFsNotFound(err)) {
330
- throw err;
331
- }
332
- }
337
+ await unlinkIfExists(oauthCredsPath);
333
338
  }
334
339
  };
335
340
  }
341
+ async function loadOauthCreds(target) {
342
+ try {
343
+ if (process.platform !== "win32") {
344
+ const stat = await fs.stat(target);
345
+ if ((stat.mode & 63) !== 0) {
346
+ throw new Error(
347
+ `Refusing to load credentials from ${target}: file mode is too permissive (${(stat.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
348
+ );
349
+ }
350
+ }
351
+ const raw = await fs.readFile(target, "utf-8");
352
+ return parseOauthCreds(JSON.parse(raw));
353
+ } catch (err) {
354
+ if (isFsNotFound(err)) {
355
+ return null;
356
+ }
357
+ throw err;
358
+ }
359
+ }
360
+ async function loadSettings(target) {
361
+ try {
362
+ const raw = await fs.readFile(target, "utf-8");
363
+ return parseSettings(JSON.parse(raw));
364
+ } catch (err) {
365
+ if (isFsNotFound(err)) {
366
+ return null;
367
+ }
368
+ throw err;
369
+ }
370
+ }
371
+ async function writeJsonFile(target, data, opts) {
372
+ const dir = path.dirname(target);
373
+ await fs.mkdir(dir, { recursive: true, mode: 448 });
374
+ if (process.platform !== "win32") {
375
+ await fs.chmod(dir, 448);
376
+ }
377
+ await fs.writeFile(target, JSON.stringify(data, null, 2), { mode: 384 });
378
+ if (opts.secret && process.platform !== "win32") {
379
+ await fs.chmod(target, 384);
380
+ }
381
+ }
382
+ async function unlinkIfExists(target) {
383
+ try {
384
+ await fs.unlink(target);
385
+ } catch (err) {
386
+ if (!isFsNotFound(err)) {
387
+ throw err;
388
+ }
389
+ }
390
+ }
336
391
  var DEFAULT_USER_AUTH_URL = "https://auth.myspec.dev";
337
392
  function readEnvRefreshConfig(env = process.env) {
338
393
  if (env.MYSPEC_ACCESS_TOKEN || env.MYSPEC_ACCESS_TOKEN_EXPIRES_AT) {
@@ -380,19 +435,17 @@ function createCompositeCredentialsStore(opts) {
380
435
  envUserAuthUrl: () => envUserAuthUrl
381
436
  };
382
437
  }
383
- function parseCredentials(value) {
438
+ function parseOauthCreds(value) {
384
439
  if (typeof value !== "object" || value === null) {
385
- throw new Error("Credentials file is malformed (not an object)");
440
+ throw new Error("oauth_creds.json is malformed (not an object)");
386
441
  }
387
442
  const v = value;
388
443
  const accessToken = requireString(v, "accessToken");
389
444
  const refreshToken = requireString(v, "refreshToken");
390
445
  const expiresAt = requireNumber(v, "expiresAt");
391
- const userAuthUrl = requireString(v, "userAuthUrl");
392
- const platformUrl = typeof v.platformUrl === "string" ? v.platformUrl : void 0;
393
446
  const userRaw = v.user;
394
447
  if (typeof userRaw !== "object" || userRaw === null) {
395
- throw new Error("Credentials file is malformed (missing user)");
448
+ throw new Error("oauth_creds.json is malformed (missing user)");
396
449
  }
397
450
  const userObj = userRaw;
398
451
  const user = {
@@ -400,7 +453,16 @@ function parseCredentials(value) {
400
453
  email: requireString(userObj, "email"),
401
454
  name: typeof userObj.name === "string" ? userObj.name : void 0
402
455
  };
403
- return { accessToken, refreshToken, expiresAt, userAuthUrl, platformUrl, user };
456
+ return { accessToken, refreshToken, expiresAt, user };
457
+ }
458
+ function parseSettings(value) {
459
+ if (typeof value !== "object" || value === null) {
460
+ throw new Error("settings.json is malformed (not an object)");
461
+ }
462
+ const v = value;
463
+ const userAuthUrl = requireString(v, "userAuthUrl");
464
+ const platformUrl = typeof v.platformUrl === "string" ? v.platformUrl : void 0;
465
+ return { userAuthUrl, platformUrl };
404
466
  }
405
467
  function requireString(obj, key) {
406
468
  const value = obj[key];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.1.0-next.3",
3
+ "version": "0.1.0-next.4",
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": {