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

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 +33 -1
  2. package/dist/index.js +36 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,12 +20,44 @@ Run with `npx @myspec/mcp-server [command]` (or `myspec-mcp [command]` when inst
20
20
  (default) Start MCP server over stdio
21
21
  serve Start MCP server over stdio
22
22
  login Sign in via browser loopback OAuth
23
- login --paste Sign in by pasting a one-time code
23
+ login --paste Same as login, but never auto-opens a browser
24
24
  login --org <slug> Sign in and pin that organization
25
25
  logout Revoke refresh token and clear local credentials
26
26
  reverse --root <dir> Connect to ai-agent and expose local_fs tools
27
27
  ```
28
28
 
29
+ ### Signing in from another device (`--paste`)
30
+
31
+ `login` opens a browser on this machine and catches the callback on a local
32
+ loopback port. When the browser lives somewhere else — a remote dev box, a
33
+ container, a phone — use `--paste`: the same flow, minus the auto-open.
34
+
35
+ ```bash
36
+ npx -y @myspec/mcp-server login --paste
37
+ ```
38
+
39
+ Open the printed URL wherever you can. If the page cannot reach this CLI, it
40
+ shows a token in a box that looks like `<code>.<state>`. Copy **the whole
41
+ value, including the dot** and paste it back into the terminal. Two traps:
42
+
43
+ - The page's copy button is labelled "Copy code" and the page URL contains a
44
+ bare `?code=`. Neither of those bare values works — the CLI rejects them with
45
+ `Pasted token is missing a state suffix`.
46
+ - The code expires **60 seconds after you finish signing in** — not 60 seconds
47
+ after the token appears. user-auth starts that clock before redirecting to
48
+ the page, and the page then spends part of it trying to reach this CLI
49
+ (instant if the port refuses the connection, but up to the browser's connect
50
+ timeout if it is filtered). So paste it the moment it appears; a stale one
51
+ fails with `OAuth code exchange failed: HTTP 401`. The CLI's own deadline is
52
+ minutes long, which is time to *reach* the token, not to use it.
53
+
54
+ > **Do not open the sign-in URL on a machine where you do not trust everything
55
+ > else running on it.** The page attempts a callback to `127.0.0.1:<port>` on
56
+ > whichever machine opens the URL, and anything listening on that port can take
57
+ > the one-time code and exchange it for an access **and** refresh token —
58
+ > keeping access until you run `logout`. The CLI prints the same warning with
59
+ > the actual port, before the URL.
60
+
29
61
  ### Active organization
30
62
 
31
63
  Every MySpec access token carries the slug of one **active organization**, and
package/dist/index.js CHANGED
@@ -44,6 +44,13 @@ var ConfigError = class extends Error {
44
44
 
45
45
  // src/auth/oauth-loopback.ts
46
46
  var DEFAULT_TIMEOUT_MS = 5 * 6e4;
47
+ function callbackWarning(callbackBase) {
48
+ return `Note: the sign-in page will try to call back to ${callbackBase}
49
+ on whichever machine opens that URL. Do not open it on a machine where you
50
+ do not trust everything else running on it \u2014 anything listening on that port
51
+ can take the one-time code and exchange it for an access *and* refresh token,
52
+ keeping access until you run \`myspec-mcp logout\`.`;
53
+ }
47
54
  var SUCCESS_HTML = "<!doctype html><html><body><h1>Sign-in complete</h1><p>You may close this tab and return to your terminal.</p></body></html>";
48
55
  var ERROR_HTML = "<!doctype html><html><body><h1>Sign-in failed</h1><p>Return to your terminal for details.</p></body></html>";
49
56
  async function loopbackLogin(opts) {
@@ -67,17 +74,29 @@ async function loopbackLogin(opts) {
67
74
  const webappLogin = new URL(`${opts.webappUrl.replace(/\/$/, "")}/auth/cli`);
68
75
  webappLogin.searchParams.set("target", loopbackTarget);
69
76
  const signInUrl = webappLogin.toString();
77
+ const remoteBrowser = opts.remoteBrowser ?? !opts.openBrowser;
78
+ if (remoteBrowser) {
79
+ logger(callbackWarning(callbackBase));
80
+ }
70
81
  logger(`Open this URL in your browser to sign in:
71
82
  ${signInUrl}`);
72
83
  if (!opts.disablePasteFallback) {
84
+ const lead = remoteBrowser ? "Sign in there. If the page shows a token box, copy the whole value" : "If the browser cannot reach this CLI directly, copy the whole token";
73
85
  logger(
74
- "If the browser cannot reach this CLI directly, paste the code shown on the\n/auth/cli page here and press Enter."
86
+ `${lead}
87
+ shown in the box \u2014 it looks like \`<code>.<state>\` \u2014 and paste it here,
88
+ then press Enter. (Not the \`code=\` value from the URL.) The code expires
89
+ 60 seconds after you finish signing in, and the page spends part of that
90
+ trying to reach this CLI, so paste it the moment it appears.`
75
91
  );
76
92
  }
77
93
  if (opts.openBrowser) {
78
94
  opts.openBrowser(signInUrl).catch((err) => {
79
95
  const message = err instanceof Error ? err.message : String(err);
80
96
  logger(`Failed to open browser automatically: ${message}`);
97
+ if (!remoteBrowser) {
98
+ logger(callbackWarning(callbackBase));
99
+ }
81
100
  });
82
101
  }
83
102
  }
@@ -233,51 +252,6 @@ async function exchangeCode(userAuthUrl, code, fetchImpl) {
233
252
  return await response.json();
234
253
  }
235
254
 
236
- // src/auth/oauth-paste.ts
237
- import readline2 from "readline/promises";
238
- async function pasteLogin(opts) {
239
- const fetchImpl = opts.fetchImpl ?? fetch;
240
- const now = opts.now ?? Date.now;
241
- const logger = opts.logger ?? ((m) => {
242
- process.stderr.write(m + "\n");
243
- });
244
- const signInUrl = `${opts.webappUrl.replace(/\/$/, "")}/auth/cli`;
245
- logger("Open this URL in your browser, sign in, and copy the one-time code shown:");
246
- logger(` ${signInUrl}`);
247
- const ask = opts.prompt ?? defaultPrompt;
248
- const raw = await ask("Paste the one-time code: ");
249
- const code = raw.trim();
250
- if (!code) {
251
- throw new Error("No code provided.");
252
- }
253
- const response = await fetchImpl(`${opts.userAuthUrl}/api/auth/oauth/exchange`, {
254
- method: "POST",
255
- headers: { "Content-Type": "application/json" },
256
- body: JSON.stringify({ code })
257
- });
258
- if (!response.ok) {
259
- const body = await response.text().catch(() => "");
260
- throw new HttpStatusError(response.status, body, `OAuth code exchange failed: HTTP ${String(response.status)}`);
261
- }
262
- const exchanged = await response.json();
263
- return {
264
- accessToken: exchanged.accessToken,
265
- refreshToken: exchanged.refreshToken,
266
- expiresAt: now() + exchanged.expiresIn * 1e3,
267
- userAuthUrl: opts.userAuthUrl,
268
- webappUrl: opts.webappUrl,
269
- user: exchanged.user
270
- };
271
- }
272
- async function defaultPrompt(question) {
273
- const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
274
- try {
275
- return await rl.question(question);
276
- } finally {
277
- rl.close();
278
- }
279
- }
280
-
281
255
  // src/auth/credentials-store.ts
282
256
  import { promises as fs } from "fs";
283
257
  import os from "os";
@@ -615,7 +589,7 @@ var RefreshRejectedError = class extends NeedsLoginError {
615
589
  };
616
590
 
617
591
  // src/auth/organization.ts
618
- import readline3 from "readline";
592
+ import readline2 from "readline";
619
593
  function readOrgClaim(accessToken) {
620
594
  const payload = accessToken.split(".")[1];
621
595
  if (payload === void 0) {
@@ -716,7 +690,7 @@ async function promptForOrganization(organizations, deps = {}) {
716
690
  output.write(` ${String(index + 1)}) ${org.name} (${org.slug})
717
691
  `);
718
692
  });
719
- const rl = readline3.createInterface({ input, output });
693
+ const rl = readline2.createInterface({ input, output });
720
694
  try {
721
695
  for (; ; ) {
722
696
  const answer = await new Promise((resolve3) => {
@@ -891,19 +865,23 @@ function discoveryError(endpoint, reason, hint) {
891
865
  }
892
866
 
893
867
  // src/cli/login.ts
868
+ var PASTE_TIMEOUT_MS = 15 * 6e4;
894
869
  async function runLogin(options, deps = {}) {
895
870
  const config = resolveConfig({ cliUserAuthUrl: options.userAuthUrl });
896
871
  const store = deps.store ?? createFileCredentialsStore();
897
872
  const log = deps.log ?? ((message) => {
898
873
  process.stderr.write(message + "\n");
899
874
  });
900
- const doLogin = deps.doLogin ?? ((cfg) => options.paste ? pasteLogin(cfg) : loopbackLogin({
901
- ...cfg,
902
- openBrowser: async (url) => {
903
- const mod = await import("open");
904
- await mod.default(url);
905
- }
906
- }));
875
+ const openBrowser = async (url) => {
876
+ const mod = await import("open");
877
+ await mod.default(url);
878
+ };
879
+ const loopback = deps.loopback ?? loopbackLogin;
880
+ const doLogin = deps.doLogin ?? ((cfg) => {
881
+ return loopback(
882
+ options.paste ? { ...cfg, remoteBrowser: true, timeoutMs: PASTE_TIMEOUT_MS } : { ...cfg, openBrowser }
883
+ );
884
+ });
907
885
  const credentials = await doLogin({
908
886
  userAuthUrl: config.userAuthUrl,
909
887
  webappUrl: config.webappUrl
@@ -5011,7 +4989,9 @@ function printHelp() {
5011
4989
  " (default) Start MCP server over stdio",
5012
4990
  " serve Start MCP server over stdio",
5013
4991
  " login Sign in via browser (chooser page on the webapp)",
5014
- " login --paste Sign in by pasting a one-time code",
4992
+ " login --paste Same as login, but never auto-opens a browser. Open",
4993
+ " the printed URL yourself; if the page cannot reach this",
4994
+ " CLI it shows a `<code>.<state>` token to paste back.",
5015
4995
  " login --org <slug> Sign in and pin that organization. Tokens only work",
5016
4996
  " when an organization is active; the CLI asks when you",
5017
4997
  " belong to several and this flag is not given.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.2.0-next.78",
3
+ "version": "0.2.0-next.79",
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": {