@myspec/mcp-server 0.2.1-next.86 → 0.3.0-next.88
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 +57 -10
- package/dist/index.js +216 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,8 +64,9 @@ value, including the dot** and paste it back into the terminal. Two traps:
|
|
|
64
64
|
without a person present (a server, a container, a CI job, a shared machine),
|
|
65
65
|
use an API token instead.
|
|
66
66
|
|
|
67
|
-
>
|
|
68
|
-
>
|
|
67
|
+
> `MYSPEC_API_TOKEN` needs **`@myspec/mcp-server` 0.3.0 or newer**; the
|
|
68
|
+
> `apiToken` file field needs **0.2.0 or newer** (`npm view @myspec/mcp-server
|
|
69
|
+
> version`). Earlier builds ignore the setting entirely and report that they are
|
|
69
70
|
> not signed in, without mentioning the version.
|
|
70
71
|
|
|
71
72
|
**1. Create the token.** In the MySpec webapp, open the avatar menu → **API
|
|
@@ -73,8 +74,48 @@ tokens** → **Create token**. Choose the organization it should act in, whether
|
|
|
73
74
|
it may write or only read, and an expiry. The token is shown **once**; it
|
|
74
75
|
cannot be retrieved afterwards.
|
|
75
76
|
|
|
76
|
-
**2. Configure it.**
|
|
77
|
-
|
|
77
|
+
**2. Configure it.** Either way works; the environment variable needs no files.
|
|
78
|
+
|
|
79
|
+
#### Option A — `MYSPEC_API_TOKEN` (recommended)
|
|
80
|
+
|
|
81
|
+
Put it in your MCP client's `env` block:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"myspec": {
|
|
87
|
+
"command": "npx",
|
|
88
|
+
"args": ["-y", "@myspec/mcp-server"],
|
|
89
|
+
"env": { "MYSPEC_API_TOKEN": "msp_pat_…" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
or register it in one command:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
claude mcp add --scope user myspec \
|
|
99
|
+
--env MYSPEC_API_TOKEN=msp_pat_… \
|
|
100
|
+
-- npx -y @myspec/mcp-server
|
|
101
|
+
```
|
|
102
|
+
|
|
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.
|
|
107
|
+
|
|
108
|
+
The server writes `~/.myspec/settings.json` so restarts skip endpoint discovery.
|
|
109
|
+
It never writes the token to disk, so unsetting the variable removes the
|
|
110
|
+
credential and leaves any saved sign-in exactly as it was.
|
|
111
|
+
|
|
112
|
+
> One trade-off: an environment variable is visible in process listings and is
|
|
113
|
+
> inherited by child processes. On a shared machine, Option B's `0600` file is
|
|
114
|
+
> the tighter choice.
|
|
115
|
+
|
|
116
|
+
#### Option B — credentials files
|
|
117
|
+
|
|
118
|
+
Two files under `~/.myspec/`, both of which the server needs:
|
|
78
119
|
|
|
79
120
|
`~/.myspec/oauth_creds.json` — the secret, **mode 0600** (the store refuses any
|
|
80
121
|
file readable by group or other):
|
|
@@ -97,8 +138,11 @@ chmod 600 ~/.myspec/oauth_creds.json
|
|
|
97
138
|
automatically on first use, and an API-token credential needs no `user` block —
|
|
98
139
|
the owner is resolved server-side at exchange.
|
|
99
140
|
|
|
100
|
-
|
|
101
|
-
|
|
141
|
+
#### Precedence
|
|
142
|
+
|
|
143
|
+
`MYSPEC_API_TOKEN` > `apiToken` in `oauth_creds.json` > the refresh token saved
|
|
144
|
+
by `login`. Whichever API token wins is used **exclusively**: if it is rejected
|
|
145
|
+
the run fails rather than quietly falling back to another identity.
|
|
102
146
|
|
|
103
147
|
**3. Verify it.** The token is exchanged for a short-lived access token on
|
|
104
148
|
every run, so a token that works here works in the server:
|
|
@@ -134,12 +178,14 @@ doing so does not change the token value, so nothing needs reconfiguring.
|
|
|
134
178
|
|
|
135
179
|
If a token does not work and you need the deployment running again now:
|
|
136
180
|
|
|
137
|
-
1. **
|
|
181
|
+
1. **Unset `MYSPEC_API_TOKEN`** if it is set — it wins over the file, so
|
|
182
|
+
editing the file changes nothing while the variable is in place.
|
|
183
|
+
2. **Delete the `apiToken` field** from `~/.myspec/oauth_creds.json`. Emptying
|
|
138
184
|
it is not enough and neither is adding a refresh token alongside it: when an
|
|
139
185
|
`apiToken` is present it is used exclusively, and a rejected one fails the
|
|
140
186
|
run rather than falling back — silently downgrading to a different identity
|
|
141
187
|
would be worse than stopping.
|
|
142
|
-
|
|
188
|
+
3. Restore the previous credential — restore a backup of the credentials file
|
|
143
189
|
if you took one, or run `login` again.
|
|
144
190
|
|
|
145
191
|
Do **not** use `logout` to roll back. It deletes the whole credentials file,
|
|
@@ -212,12 +258,12 @@ npx @myspec/mcp-server reverse --root . --agent-url ws://localhost:3001/mcp/reve
|
|
|
212
258
|
|---|---|
|
|
213
259
|
| `--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. |
|
|
214
260
|
| `--agent-url <url>` / `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL override; skips webapp discovery entirely. |
|
|
215
|
-
| `--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
|
|
261
|
+
| `--access-token <jwt>` / `MYSPEC_ACCESS_TOKEN` | Use a static access token for local testing instead of the saved credentials. It overrides `MYSPEC_API_TOKEN` too (with a warning) and cannot be refreshed. Otherwise `reverse` uses `MYSPEC_API_TOKEN`, else the saved `apiToken`, else the refresh token from `npx @myspec/mcp-server login`, and auto-refreshes. |
|
|
216
262
|
|
|
217
263
|
Local state is stored under `~/.myspec/` (alongside the on-disk cache root), split into two files:
|
|
218
264
|
|
|
219
265
|
- `settings.json` — non-secret config: the auth-server URL you logged in with, plus the derived webapp URL and the discovered platform / ai-agent URLs (refreshed on every successful discovery).
|
|
220
|
-
- `oauth_creds.json` — the OAuth tokens (access/refresh/expiry) and your user identity, written mode `0600`.
|
|
266
|
+
- `oauth_creds.json` — the OAuth tokens (access/refresh/expiry) and your user identity, written mode `0600`. A session configured through `MYSPEC_API_TOKEN` never writes this file — it writes `settings.json` only.
|
|
221
267
|
|
|
222
268
|
`logout` removes `oauth_creds.json`; `settings.json` is left in place.
|
|
223
269
|
|
|
@@ -225,6 +271,7 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
225
271
|
|
|
226
272
|
| Variable | Purpose |
|
|
227
273
|
|---|---|
|
|
274
|
+
| `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+. |
|
|
228
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. |
|
|
229
276
|
| `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_spec_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
|
|
230
277
|
| `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL for `reverse`; skips the webapp discovery call |
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import readline from "readline";
|
|
|
11
11
|
|
|
12
12
|
// src/errors.ts
|
|
13
13
|
var NeedsLoginError = class extends Error {
|
|
14
|
-
constructor(message = "Not authenticated.
|
|
14
|
+
constructor(message = "Not authenticated. Set MYSPEC_API_TOKEN to a MySpec API token, or run `npx @myspec/mcp-server login` to sign in.") {
|
|
15
15
|
super(message);
|
|
16
16
|
this.name = "NeedsLoginError";
|
|
17
17
|
}
|
|
@@ -296,16 +296,7 @@ function createFileCredentialsStore(paths = {}) {
|
|
|
296
296
|
};
|
|
297
297
|
},
|
|
298
298
|
async save(creds) {
|
|
299
|
-
await
|
|
300
|
-
settingsPath,
|
|
301
|
-
{
|
|
302
|
-
userAuthUrl: creds.userAuthUrl,
|
|
303
|
-
platformUrl: creds.platformUrl,
|
|
304
|
-
webappUrl: creds.webappUrl,
|
|
305
|
-
aiAgentMcpReverseUrl: creds.aiAgentMcpReverseUrl
|
|
306
|
-
},
|
|
307
|
-
{ secret: false }
|
|
308
|
-
);
|
|
299
|
+
await writeSettingsFile(settingsPath, creds);
|
|
309
300
|
await writeJsonFile(
|
|
310
301
|
oauthCredsPath,
|
|
311
302
|
{
|
|
@@ -326,6 +317,18 @@ function createFileCredentialsStore(paths = {}) {
|
|
|
326
317
|
}
|
|
327
318
|
};
|
|
328
319
|
}
|
|
320
|
+
async function writeSettingsFile(target, creds) {
|
|
321
|
+
await writeJsonFile(
|
|
322
|
+
target,
|
|
323
|
+
{
|
|
324
|
+
userAuthUrl: creds.userAuthUrl,
|
|
325
|
+
platformUrl: creds.platformUrl,
|
|
326
|
+
webappUrl: creds.webappUrl,
|
|
327
|
+
aiAgentMcpReverseUrl: creds.aiAgentMcpReverseUrl
|
|
328
|
+
},
|
|
329
|
+
{ secret: false }
|
|
330
|
+
);
|
|
331
|
+
}
|
|
329
332
|
async function loadOauthCreds(target) {
|
|
330
333
|
try {
|
|
331
334
|
if (process.platform !== "win32") {
|
|
@@ -376,17 +379,133 @@ async function unlinkIfExists(target) {
|
|
|
376
379
|
}
|
|
377
380
|
}
|
|
378
381
|
}
|
|
382
|
+
var API_TOKEN_PREFIX = "msp_pat_";
|
|
383
|
+
function isUnusableAsHeaderValue(value) {
|
|
384
|
+
for (const char of value) {
|
|
385
|
+
const code = char.codePointAt(0) ?? 0;
|
|
386
|
+
if (code <= 32 || code === 127) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
function defaultWarn(message) {
|
|
393
|
+
process.stderr.write(message + "\n");
|
|
394
|
+
}
|
|
395
|
+
function readEnvApiToken(userAuthUrl, env = process.env, warn = defaultWarn) {
|
|
396
|
+
const raw = env.MYSPEC_API_TOKEN;
|
|
397
|
+
if (!raw) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const apiToken = raw.trim();
|
|
401
|
+
if (!apiToken) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
if (isUnusableAsHeaderValue(apiToken)) {
|
|
405
|
+
warn(
|
|
406
|
+
"myspec-mcp: MYSPEC_API_TOKEN contains whitespace or control characters and cannot be sent as a header \u2014 it is being ignored. Check for a line break in the value (a wrapped paste into .mcp.json is the usual cause)."
|
|
407
|
+
);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return { apiToken, userAuthUrl };
|
|
411
|
+
}
|
|
412
|
+
function hasEnvApiToken(env = process.env) {
|
|
413
|
+
return Boolean(env.MYSPEC_API_TOKEN);
|
|
414
|
+
}
|
|
415
|
+
function createEnvApiTokenStore(opts) {
|
|
416
|
+
const { fileStore, envConfig } = opts;
|
|
417
|
+
if (!envConfig) {
|
|
418
|
+
return fileStore;
|
|
419
|
+
}
|
|
420
|
+
const settingsPath = opts.settingsPath ?? defaultSettingsPath();
|
|
421
|
+
const warn = opts.warn ?? defaultWarn;
|
|
422
|
+
const warned = /* @__PURE__ */ new Set();
|
|
423
|
+
const warnOnce = (key, message) => {
|
|
424
|
+
if (warned.has(key)) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
warned.add(key);
|
|
428
|
+
warn(message);
|
|
429
|
+
};
|
|
430
|
+
if (!envConfig.apiToken.startsWith(API_TOKEN_PREFIX)) {
|
|
431
|
+
warnOnce(
|
|
432
|
+
"prefix",
|
|
433
|
+
`myspec-mcp: MYSPEC_API_TOKEN does not start with \`${API_TOKEN_PREFIX}\`, so it is probably not a MySpec API token. Create one in the MySpec webapp under the avatar menu, API tokens.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
// Not a filesystem path — the credential has no file. Only ever printed.
|
|
438
|
+
path: () => "MYSPEC_API_TOKEN (environment)",
|
|
439
|
+
async load() {
|
|
440
|
+
const fromFile = await fileStore.load().catch((err) => {
|
|
441
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
442
|
+
warnOnce(
|
|
443
|
+
"file-unreadable",
|
|
444
|
+
`myspec-mcp: using MYSPEC_API_TOKEN; the existing credentials file could not be read (${message})`
|
|
445
|
+
);
|
|
446
|
+
return null;
|
|
447
|
+
});
|
|
448
|
+
if (fromFile && (fromFile.apiToken !== void 0 || fromFile.refreshToken !== void 0)) {
|
|
449
|
+
warnOnce(
|
|
450
|
+
"shadowed",
|
|
451
|
+
`myspec-mcp: MYSPEC_API_TOKEN is set and takes precedence over the credentials in ${fileStore.path()}.`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const cached = fromFile ?? await loadSettings(settingsPath).catch(() => null);
|
|
455
|
+
const sameEnvironment = cached?.userAuthUrl === envConfig.userAuthUrl;
|
|
456
|
+
return {
|
|
457
|
+
accessToken: "",
|
|
458
|
+
apiToken: envConfig.apiToken,
|
|
459
|
+
expiresAt: 0,
|
|
460
|
+
userAuthUrl: envConfig.userAuthUrl,
|
|
461
|
+
// `sameEnvironment` implies `cached` is non-null: a null cached record
|
|
462
|
+
// compares undefined against a string.
|
|
463
|
+
...sameEnvironment && cached.platformUrl ? { platformUrl: cached.platformUrl } : {},
|
|
464
|
+
...sameEnvironment && cached.webappUrl ? { webappUrl: cached.webappUrl } : {},
|
|
465
|
+
...sameEnvironment && cached.aiAgentMcpReverseUrl ? { aiAgentMcpReverseUrl: cached.aiAgentMcpReverseUrl } : {}
|
|
466
|
+
};
|
|
467
|
+
},
|
|
468
|
+
async save(creds) {
|
|
469
|
+
let existing;
|
|
470
|
+
try {
|
|
471
|
+
existing = await loadSettings(settingsPath);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
474
|
+
warnOnce(
|
|
475
|
+
"settings-unreadable",
|
|
476
|
+
`myspec-mcp: leaving ${settingsPath} alone \u2014 it could not be read (${message}).`
|
|
477
|
+
);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (existing && existing.userAuthUrl !== envConfig.userAuthUrl) {
|
|
481
|
+
warnOnce(
|
|
482
|
+
"settings-mismatch",
|
|
483
|
+
`myspec-mcp: leaving ${settingsPath} alone \u2014 it names ${existing.userAuthUrl}, but MYSPEC_API_TOKEN is being used against ${envConfig.userAuthUrl}. Overwriting it would break the sign-in saved for that environment.`
|
|
484
|
+
);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
await writeSettingsFile(settingsPath, creds);
|
|
488
|
+
},
|
|
489
|
+
clear() {
|
|
490
|
+
warnOnce(
|
|
491
|
+
"clear",
|
|
492
|
+
"myspec-mcp: credentials come from MYSPEC_API_TOKEN; nothing was cleared. Unset the variable to stop using it."
|
|
493
|
+
);
|
|
494
|
+
return Promise.resolve();
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
379
498
|
function assertNoEnvAccessToken(env = process.env) {
|
|
380
499
|
if (env.MYSPEC_REFRESH_TOKEN) {
|
|
381
500
|
process.stderr.write(
|
|
382
|
-
"myspec-mcp: MYSPEC_REFRESH_TOKEN is set but no longer supported and is being ignored. Set an `apiToken` in ~/.myspec/oauth_creds.json, or run `npx @myspec/mcp-server login`. See docs/runbooks/mcp-api-token-migration.md\n"
|
|
501
|
+
"myspec-mcp: MYSPEC_REFRESH_TOKEN is set but no longer supported and is being ignored. Set MYSPEC_API_TOKEN to a MySpec API token instead (or an `apiToken` in ~/.myspec/oauth_creds.json), or run `npx @myspec/mcp-server login`. See docs/runbooks/mcp-api-token-migration.md\n"
|
|
383
502
|
);
|
|
384
503
|
}
|
|
385
504
|
if (!env.MYSPEC_ACCESS_TOKEN && !env.MYSPEC_ACCESS_TOKEN_EXPIRES_AT) {
|
|
386
505
|
return;
|
|
387
506
|
}
|
|
388
507
|
throw new ConfigError(
|
|
389
|
-
"MYSPEC_ACCESS_TOKEN / MYSPEC_ACCESS_TOKEN_EXPIRES_AT are not supported.
|
|
508
|
+
"MYSPEC_ACCESS_TOKEN / MYSPEC_ACCESS_TOKEN_EXPIRES_AT are not supported. Set MYSPEC_API_TOKEN to a long-lived MySpec API token instead (or put one in the `apiToken` field of your credentials file), or run `npx @myspec/mcp-server login`, and unset these variables."
|
|
390
509
|
);
|
|
391
510
|
}
|
|
392
511
|
function parseOauthCreds(value) {
|
|
@@ -528,7 +647,7 @@ var TokenManager = class {
|
|
|
528
647
|
}
|
|
529
648
|
if (!creds.refreshToken) {
|
|
530
649
|
throw new NeedsLoginError(
|
|
531
|
-
"No credential is configured. Either
|
|
650
|
+
"No credential is configured. Either set MYSPEC_API_TOKEN to a long-lived API token (or put one in the `apiToken` field of your credentials file) \u2014 create one in the MySpec webapp under the avatar menu, API tokens \u2014 or run `npx @myspec/mcp-server login` to sign in interactively."
|
|
532
651
|
);
|
|
533
652
|
}
|
|
534
653
|
try {
|
|
@@ -560,7 +679,7 @@ var TokenManager = class {
|
|
|
560
679
|
if (response.status === 401 || response.status === 403) {
|
|
561
680
|
const reason = response.status === 403 ? "the token owner no longer has access to its organization" : "the token is invalid, disabled, or expired";
|
|
562
681
|
const rejection = new ApiTokenRejectedError(
|
|
563
|
-
`API token exchange failed: ${reason}. Create a new API token in MySpec and write it to the apiToken field of your credentials file.`
|
|
682
|
+
`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).`
|
|
564
683
|
);
|
|
565
684
|
this.apiTokenRejection = rejection;
|
|
566
685
|
throw rejection;
|
|
@@ -570,7 +689,7 @@ var TokenManager = class {
|
|
|
570
689
|
throw new HttpStatusError(
|
|
571
690
|
response.status,
|
|
572
691
|
body,
|
|
573
|
-
`Token exchange failed: ${url} returned 404, so no exchange endpoint is there.
|
|
692
|
+
`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.`
|
|
574
693
|
);
|
|
575
694
|
}
|
|
576
695
|
if (!response.ok) {
|
|
@@ -920,6 +1039,11 @@ async function runLogin(options, deps = {}) {
|
|
|
920
1039
|
const log = deps.log ?? ((message) => {
|
|
921
1040
|
process.stderr.write(message + "\n");
|
|
922
1041
|
});
|
|
1042
|
+
if (hasEnvApiToken(deps.env)) {
|
|
1043
|
+
log(
|
|
1044
|
+
"myspec-mcp: MYSPEC_API_TOKEN is set and takes precedence \u2014 this login will not take effect until you unset it."
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
923
1047
|
const openBrowser = async (url) => {
|
|
924
1048
|
const mod = await import("open");
|
|
925
1049
|
await mod.default(url);
|
|
@@ -1059,11 +1183,15 @@ async function discoverAfterLogin(credentials, webappUrl, discover = discoverCon
|
|
|
1059
1183
|
}
|
|
1060
1184
|
|
|
1061
1185
|
// src/cli/logout.ts
|
|
1062
|
-
|
|
1186
|
+
var ENV_TOKEN_STILL_SET = "MYSPEC_API_TOKEN is still set in this environment and will keep being used \u2014 remove it from your MCP client config (or shell) to stop.\n";
|
|
1187
|
+
async function runLogout(env = process.env) {
|
|
1063
1188
|
const store = createFileCredentialsStore();
|
|
1189
|
+
const usingEnvToken = hasEnvApiToken(env);
|
|
1064
1190
|
const existing = await store.load();
|
|
1065
1191
|
if (!existing) {
|
|
1066
|
-
process.stderr.write(
|
|
1192
|
+
process.stderr.write(
|
|
1193
|
+
usingEnvToken ? `No credentials file to clear. ${ENV_TOKEN_STILL_SET}` : "No credentials to clear.\n"
|
|
1194
|
+
);
|
|
1067
1195
|
return;
|
|
1068
1196
|
}
|
|
1069
1197
|
const config = resolveConfig({ storedUserAuthUrl: existing.userAuthUrl });
|
|
@@ -1097,10 +1225,16 @@ async function runLogout() {
|
|
|
1097
1225
|
process.stderr.write(
|
|
1098
1226
|
"Signed out. The configured API token was kept \u2014 delete the `apiToken` field in ~/.myspec/oauth_creds.json to remove it.\n"
|
|
1099
1227
|
);
|
|
1228
|
+
if (usingEnvToken) {
|
|
1229
|
+
process.stderr.write(ENV_TOKEN_STILL_SET);
|
|
1230
|
+
}
|
|
1100
1231
|
return;
|
|
1101
1232
|
}
|
|
1102
1233
|
await store.clear();
|
|
1103
1234
|
process.stderr.write("Signed out and cleared local credentials.\n");
|
|
1235
|
+
if (usingEnvToken) {
|
|
1236
|
+
process.stderr.write(ENV_TOKEN_STILL_SET);
|
|
1237
|
+
}
|
|
1104
1238
|
}
|
|
1105
1239
|
|
|
1106
1240
|
// src/platform/client.ts
|
|
@@ -5155,10 +5289,11 @@ function printHelp() {
|
|
|
5155
5289
|
" The agent WebSocket URL is discovered from the webapp",
|
|
5156
5290
|
" (GET /api/v1/config) using your saved credentials;",
|
|
5157
5291
|
" override with --agent-url <url> or MYSPEC_AI_AGENT_WS_URL.",
|
|
5158
|
-
" Uses the apiToken from
|
|
5159
|
-
"
|
|
5160
|
-
" obtain & auto-refresh access tokens.
|
|
5161
|
-
" --access-token <jwt> or MYSPEC_ACCESS_TOKEN
|
|
5292
|
+
" Uses MYSPEC_API_TOKEN if set, else the apiToken from",
|
|
5293
|
+
" ~/.myspec/oauth_creds.json, else the refresh_token saved",
|
|
5294
|
+
" by `login`, to obtain & auto-refresh access tokens.",
|
|
5295
|
+
" Override with --access-token <jwt> or MYSPEC_ACCESS_TOKEN",
|
|
5296
|
+
" for local testing.",
|
|
5162
5297
|
" --version Print version",
|
|
5163
5298
|
" --help Print this help",
|
|
5164
5299
|
"",
|
|
@@ -5170,6 +5305,11 @@ function printHelp() {
|
|
|
5170
5305
|
" via the webapp and saved to ~/.myspec/settings.json.",
|
|
5171
5306
|
"",
|
|
5172
5307
|
"Environment:",
|
|
5308
|
+
" MYSPEC_API_TOKEN Long-lived MySpec API token (msp_pat_...). Set it in your",
|
|
5309
|
+
" MCP client's `env` block to configure the server with no",
|
|
5310
|
+
" credential files. Takes precedence over anything saved by",
|
|
5311
|
+
" `login`; create one in the webapp under avatar menu > API",
|
|
5312
|
+
" tokens.",
|
|
5173
5313
|
" MYSPEC_USER_AUTH_URL user-auth base URL (default https://auth.myspec.dev)",
|
|
5174
5314
|
" MYSPEC_AI_AGENT_WS_URL ai-agent WebSocket URL for `reverse` (skips discovery)"
|
|
5175
5315
|
];
|
|
@@ -5236,7 +5376,11 @@ async function resolveReverseAgentUrl(sources) {
|
|
|
5236
5376
|
return { agentUrl: explicitAgentUrl };
|
|
5237
5377
|
}
|
|
5238
5378
|
const stored = await sources.loadStored().catch(() => null);
|
|
5239
|
-
const config = resolveConfig({
|
|
5379
|
+
const config = resolveConfig({
|
|
5380
|
+
env,
|
|
5381
|
+
cliUserAuthUrl: sources.cliUserAuthUrl,
|
|
5382
|
+
storedUserAuthUrl: stored?.userAuthUrl
|
|
5383
|
+
});
|
|
5240
5384
|
const endpoint = configEndpoint(config.webappUrl);
|
|
5241
5385
|
try {
|
|
5242
5386
|
const discovered = await sources.discover(config.webappUrl);
|
|
@@ -5263,9 +5407,41 @@ async function resolveReverseAgentUrl(sources) {
|
|
|
5263
5407
|
throw err;
|
|
5264
5408
|
}
|
|
5265
5409
|
}
|
|
5410
|
+
async function buildCredentialsStore(opts = {}) {
|
|
5411
|
+
const env = opts.env ?? process.env;
|
|
5412
|
+
const fileStore = opts.fileStore ?? createFileCredentialsStore();
|
|
5413
|
+
const usingEnvApiToken = hasEnvApiToken(env);
|
|
5414
|
+
const tolerate = opts.toleratesUnreadableFile ?? usingEnvApiToken;
|
|
5415
|
+
const stored = tolerate ? await fileStore.load().catch(() => null) : await fileStore.load();
|
|
5416
|
+
const config = resolveConfig({
|
|
5417
|
+
env,
|
|
5418
|
+
cliUserAuthUrl: opts.cliUserAuthUrl,
|
|
5419
|
+
storedUserAuthUrl: stored?.userAuthUrl
|
|
5420
|
+
});
|
|
5421
|
+
return {
|
|
5422
|
+
store: createEnvApiTokenStore({
|
|
5423
|
+
fileStore,
|
|
5424
|
+
envConfig: readEnvApiToken(config.userAuthUrl, env)
|
|
5425
|
+
}),
|
|
5426
|
+
config,
|
|
5427
|
+
usingEnvApiToken
|
|
5428
|
+
};
|
|
5429
|
+
}
|
|
5430
|
+
function warnIfStaticTokenShadowsApiToken(inlineToken, env = process.env, warn = (message) => {
|
|
5431
|
+
process.stderr.write(message + "\n");
|
|
5432
|
+
}) {
|
|
5433
|
+
if (!inlineToken || !hasEnvApiToken(env)) {
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
5436
|
+
warn(
|
|
5437
|
+
"myspec-mcp reverse: MYSPEC_ACCESS_TOKEN (or --access-token) overrides MYSPEC_API_TOKEN; using the static access token, which cannot be refreshed."
|
|
5438
|
+
);
|
|
5439
|
+
}
|
|
5266
5440
|
async function runReverseCommand(flags) {
|
|
5267
5441
|
const root = flagString(flags, "root") ?? process.cwd();
|
|
5442
|
+
const cliUserAuthUrl = flagString(flags, "user-auth-url");
|
|
5268
5443
|
const inlineToken = flagString(flags, "access-token") ?? process.env.MYSPEC_ACCESS_TOKEN;
|
|
5444
|
+
warnIfStaticTokenShadowsApiToken(inlineToken);
|
|
5269
5445
|
let getAccessToken;
|
|
5270
5446
|
let onAuthFailed;
|
|
5271
5447
|
let forceRefresh;
|
|
@@ -5280,7 +5456,11 @@ async function runReverseCommand(flags) {
|
|
|
5280
5456
|
persist = void 0;
|
|
5281
5457
|
} else {
|
|
5282
5458
|
assertNoEnvAccessToken();
|
|
5283
|
-
const
|
|
5459
|
+
const built = await buildCredentialsStore({
|
|
5460
|
+
cliUserAuthUrl,
|
|
5461
|
+
toleratesUnreadableFile: true
|
|
5462
|
+
});
|
|
5463
|
+
const store = built.store;
|
|
5284
5464
|
const tokenManager = new TokenManager({ store });
|
|
5285
5465
|
getAccessToken = () => tokenManager.getValidAccessToken();
|
|
5286
5466
|
onAuthFailed = async () => {
|
|
@@ -5292,6 +5472,10 @@ async function runReverseCommand(flags) {
|
|
|
5292
5472
|
}
|
|
5293
5473
|
const resolved = await resolveReverseAgentUrl({
|
|
5294
5474
|
flagAgentUrl: flagString(flags, "agent-url"),
|
|
5475
|
+
// Passed through so --user-auth-url means the same thing on both paths.
|
|
5476
|
+
// buildCredentialsStore validates it above; without this, discovery would
|
|
5477
|
+
// keep using settings.json and the flag would be validated but ignored.
|
|
5478
|
+
cliUserAuthUrl,
|
|
5295
5479
|
loadStored,
|
|
5296
5480
|
persist,
|
|
5297
5481
|
discover: (webappUrl) => {
|
|
@@ -5349,17 +5533,15 @@ function createPlatformUrlResolver(deps) {
|
|
|
5349
5533
|
}
|
|
5350
5534
|
async function runServe(flags) {
|
|
5351
5535
|
assertNoEnvAccessToken();
|
|
5352
|
-
const store =
|
|
5536
|
+
const { store, config } = await buildCredentialsStore({
|
|
5537
|
+
cliUserAuthUrl: flagString(flags, "user-auth-url")
|
|
5538
|
+
});
|
|
5353
5539
|
const initial = await store.load();
|
|
5354
5540
|
if (!initial) {
|
|
5355
5541
|
process.stderr.write(
|
|
5356
|
-
"myspec-mcp: starting unauthenticated.
|
|
5542
|
+
"myspec-mcp: starting unauthenticated. Set MYSPEC_API_TOKEN to a MySpec API token, or run `npx @myspec/mcp-server login`, to enable tools.\n"
|
|
5357
5543
|
);
|
|
5358
5544
|
}
|
|
5359
|
-
const config = resolveConfig({
|
|
5360
|
-
cliUserAuthUrl: flagString(flags, "user-auth-url"),
|
|
5361
|
-
storedUserAuthUrl: initial?.userAuthUrl
|
|
5362
|
-
});
|
|
5363
5545
|
const tokenManager = new TokenManager({ store });
|
|
5364
5546
|
const client = new PlatformClient({
|
|
5365
5547
|
resolveBaseUrl: createPlatformUrlResolver({
|
|
@@ -5395,14 +5577,16 @@ if (isCliEntry()) {
|
|
|
5395
5577
|
process.exit(2);
|
|
5396
5578
|
}
|
|
5397
5579
|
const message = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
5398
|
-
process.stderr.write(message + "\n");
|
|
5580
|
+
process.stderr.write(redactSensitive(message) + "\n");
|
|
5399
5581
|
process.exit(1);
|
|
5400
5582
|
});
|
|
5401
5583
|
}
|
|
5402
5584
|
export {
|
|
5403
5585
|
REVERSE_DISCOVERY_HINT,
|
|
5586
|
+
buildCredentialsStore,
|
|
5404
5587
|
createPlatformUrlResolver,
|
|
5405
5588
|
main,
|
|
5406
5589
|
resolveReverseAgentUrl,
|
|
5407
|
-
runServe
|
|
5590
|
+
runServe,
|
|
5591
|
+
warnIfStaticTokenShadowsApiToken
|
|
5408
5592
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-next.88",
|
|
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": {
|