@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.
- package/README.md +33 -1
- package/dist/index.js +36 -56
- 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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
|
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.
|
|
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": {
|