@myspec/mcp-server 0.1.0-next.2 → 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 +24 -2
  2. package/dist/index.js +110 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,16 +23,38 @@ login Sign in via browser loopback OAuth
23
23
  login --paste Sign in by pasting a one-time code
24
24
  login --provider <google|github|gitlab|linear|atlassian>
25
25
  logout Revoke refresh token and clear local credentials
26
+ reverse --root <dir> Connect to ai-agent and expose local_fs tools (experimental)
26
27
  ```
27
28
 
28
- Credentials are persisted at `$XDG_CONFIG_HOME/myspec/mcp-server.json` (POSIX) or `%APPDATA%\myspec\mcp-server.json` (Windows), mode `0600`.
29
+ ### `reverse` (experimental)
30
+
31
+ `reverse` connects out to the ai-agent service over WebSocket and exposes a set
32
+ of `local_fs` tools scoped to a single local directory, so a cloud agent can
33
+ read files from your machine. This is an early spike.
34
+
35
+ ```bash
36
+ npx @myspec/mcp-server@next reverse --root /path/to/project
37
+ ```
38
+
39
+ | Flag / env | Purpose |
40
+ |---|---|
41
+ | `--root <dir>` | Directory to grant read access to (default: current working directory). All tool paths resolve relative to this root; nothing outside it is reachable. |
42
+ | `--agent-url <url>` / `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL (default `ws://localhost:3001/mcp/reverse`). |
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
+
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.
29
51
 
30
52
  ## Environment variables
31
53
 
32
54
  | Variable | Purpose |
33
55
  |---|---|
34
56
  | `MYSPEC_USER_AUTH_URL` | user-auth base URL (default `https://auth.myspec.dev`) |
35
- | `MYSPEC_PLATFORM_URL` | platform API base URL (default `https://api.myspec.dev`) |
57
+ | `MYSPEC_PLATFORM_URL` | platform API base URL (default `https://platform.myspec.dev`) |
36
58
  | `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`. |
37
59
  | `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
38
60
  | `MYSPEC_WEBAPP_URL` | Webapp base URL used by `login --paste` to display the OAuth callback URL (default: derived from `MYSPEC_USER_AUTH_URL`) |
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];
@@ -424,7 +486,7 @@ function isFsNotFound(err) {
424
486
  function resolveConfig(sources = {}) {
425
487
  const env = sources.env ?? process.env;
426
488
  const userAuthUrl = sources.cliUserAuthUrl ?? env.MYSPEC_USER_AUTH_URL ?? sources.storedUserAuthUrl ?? "https://auth.myspec.dev";
427
- const platformUrl = sources.cliPlatformUrl ?? env.MYSPEC_PLATFORM_URL ?? sources.storedPlatformUrl ?? "https://api.myspec.dev";
489
+ const platformUrl = sources.cliPlatformUrl ?? env.MYSPEC_PLATFORM_URL ?? sources.storedPlatformUrl ?? "https://platform.myspec.dev";
428
490
  const webappUrl = sources.cliWebappUrl ?? env.MYSPEC_WEBAPP_URL ?? deriveWebappFromAuth(userAuthUrl);
429
491
  validateUrl(userAuthUrl, "user-auth URL");
430
492
  validateUrl(platformUrl, "platform URL");
@@ -3464,7 +3526,7 @@ function printHelp() {
3464
3526
  "",
3465
3527
  "Environment:",
3466
3528
  " MYSPEC_USER_AUTH_URL user-auth base URL (default https://auth.myspec.dev)",
3467
- " MYSPEC_PLATFORM_URL platform API base URL (default https://api.myspec.dev)",
3529
+ " MYSPEC_PLATFORM_URL platform API base URL (default https://platform.myspec.dev)",
3468
3530
  " MYSPEC_WEBAPP_URL webapp base URL (default derived from user-auth host)",
3469
3531
  " MYSPEC_REFRESH_TOKEN Skip file-based credentials; the server mints a",
3470
3532
  " fresh access token on first use via this refresh",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.1.0-next.2",
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": {