@myspec/mcp-server 1.1.1-next.155 → 1.1.1-next.157
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 +49 -4
- package/dist/index.js +266 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,13 +19,19 @@ Run with `npx @myspec/mcp-server [command]` (or `myspec-mcp [command]` when inst
|
|
|
19
19
|
```text
|
|
20
20
|
(default) Start MCP server over stdio
|
|
21
21
|
serve Start MCP server over stdio
|
|
22
|
-
login
|
|
23
|
-
login --paste
|
|
24
|
-
login --org <slug>
|
|
25
|
-
logout
|
|
22
|
+
auth login Sign in via browser loopback OAuth
|
|
23
|
+
auth login --paste Same as auth login, but never auto-opens a browser
|
|
24
|
+
auth login --org <slug> Sign in and pin that organization
|
|
25
|
+
auth logout Revoke refresh token and clear local credentials
|
|
26
|
+
auth status Show the sign-in: account email and active organization
|
|
27
|
+
login, logout Aliases for `auth login` and `auth logout`
|
|
26
28
|
reverse --root <dir> Connect to ai-agent and expose local_fs tools
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
`login` and `logout` keep working exactly as before; they run `auth login` and
|
|
32
|
+
`auth logout`. Add `--help` after any auth command to print the usage instead
|
|
33
|
+
of running it.
|
|
34
|
+
|
|
29
35
|
### Signing in from another device (`--paste`)
|
|
30
36
|
|
|
31
37
|
`login` opens a browser on this machine and catches the callback on a local
|
|
@@ -239,6 +245,45 @@ dev environment:
|
|
|
239
245
|
npx -y @myspec/mcp-server login --user-auth-url https://dev-auth.myspec.dev
|
|
240
246
|
```
|
|
241
247
|
|
|
248
|
+
### Checking your sign-in (`auth status`)
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npx -y @myspec/mcp-server auth status
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
Signed in to https://auth.myspec.dev
|
|
256
|
+
Account: jane@example.com
|
|
257
|
+
Organization: Acme Inc (acme)
|
|
258
|
+
Credential: login session (/home/jane/.myspec/oauth_creds.json)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
It reports the credential `serve` would use, in the same precedence order
|
|
262
|
+
(`MYSPEC_API_TOKEN` > file `apiToken` > the `login` session), and honours
|
|
263
|
+
`--user-auth-url` the same way. The first line names the auth server, which is
|
|
264
|
+
how you tell a dev sign-in from a production one.
|
|
265
|
+
|
|
266
|
+
- **What is checked online.** The email and organization come from the saved
|
|
267
|
+
access token while it is still valid (they last 15 minutes); once it has
|
|
268
|
+
expired, `auth status` renews it first, like any other command. For a login
|
|
269
|
+
session it then looks the organization name up from user-auth, which also
|
|
270
|
+
confirms the session: one that was ended on the server is reported as signed
|
|
271
|
+
out. `MYSPEC_API_TOKEN` is exchanged on every run. A file `apiToken` whose
|
|
272
|
+
saved access token is still valid is not checked with the server, so a token
|
|
273
|
+
revoked in the webapp can still show as signed in for up to 15 minutes.
|
|
274
|
+
- **API tokens show the organization slug only**, e.g.
|
|
275
|
+
`acme (API tokens do not include the organization name)`, plus the token's
|
|
276
|
+
access mode (`read-only` or `read-write`). user-auth has no endpoint that
|
|
277
|
+
returns the name for an API token.
|
|
278
|
+
- **No active organization** prints `Organization: none` and how to pin one.
|
|
279
|
+
- **If the name lookup fails** (the network, or a server error), the slug is
|
|
280
|
+
shown with the reason and the command still succeeds.
|
|
281
|
+
- **Exit codes:** `0` when signed in; `2` when not signed in, when the
|
|
282
|
+
credential is rejected, or when an expired token could not be renewed — so
|
|
283
|
+
`auth status || auth login` works in a script. A problem with the
|
|
284
|
+
credentials file itself (wrong file mode, bad JSON) fails the same way
|
|
285
|
+
`serve` does. A failure never deletes the saved credentials.
|
|
286
|
+
|
|
242
287
|
### `reverse`
|
|
243
288
|
|
|
244
289
|
`reverse` connects out to the ai-agent service over WebSocket and exposes a set
|
package/dist/index.js
CHANGED
|
@@ -319,6 +319,14 @@ function createFileCredentialsStore(paths = {}) {
|
|
|
319
319
|
}
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
|
+
function keepCredentialsOnFailure(store) {
|
|
323
|
+
return {
|
|
324
|
+
load: () => store.load(),
|
|
325
|
+
save: (creds) => store.save(creds),
|
|
326
|
+
clear: () => Promise.resolve(),
|
|
327
|
+
path: () => store.path()
|
|
328
|
+
};
|
|
329
|
+
}
|
|
322
330
|
async function writeSettingsFile(target, creds) {
|
|
323
331
|
await writeJsonFile(
|
|
324
332
|
target,
|
|
@@ -809,19 +817,36 @@ var ApiTokenRejectedError = class extends ConfigError {
|
|
|
809
817
|
|
|
810
818
|
// src/auth/organization.ts
|
|
811
819
|
import readline2 from "readline";
|
|
812
|
-
|
|
820
|
+
|
|
821
|
+
// src/auth/token-claims.ts
|
|
822
|
+
function readTokenClaims(accessToken) {
|
|
813
823
|
const payload = accessToken.split(".")[1];
|
|
814
824
|
if (payload === void 0) {
|
|
815
|
-
return
|
|
825
|
+
return {};
|
|
816
826
|
}
|
|
827
|
+
let claims;
|
|
817
828
|
try {
|
|
818
|
-
|
|
819
|
-
const claims = JSON.parse(json);
|
|
820
|
-
const org = claims.org;
|
|
821
|
-
return typeof org === "string" && org.length > 0 ? org : void 0;
|
|
829
|
+
claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
|
|
822
830
|
} catch {
|
|
823
|
-
return
|
|
831
|
+
return {};
|
|
832
|
+
}
|
|
833
|
+
if (typeof claims !== "object" || claims === null) {
|
|
834
|
+
return {};
|
|
824
835
|
}
|
|
836
|
+
const record = claims;
|
|
837
|
+
return {
|
|
838
|
+
email: nonEmptyString(record.email),
|
|
839
|
+
org: nonEmptyString(record.org),
|
|
840
|
+
access: nonEmptyString(record.access)
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function nonEmptyString(value) {
|
|
844
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/auth/organization.ts
|
|
848
|
+
function readOrgClaim(accessToken) {
|
|
849
|
+
return readTokenClaims(accessToken).org;
|
|
825
850
|
}
|
|
826
851
|
var NO_ACTIVE_ORG_HINT = "Your CLI session has no active organization, so the access token is missing the `org` claim that the platform API and ai-agent require. Run `npx @myspec/mcp-server login --org <slug>` to pin one.";
|
|
827
852
|
async function listOrganizations(opts) {
|
|
@@ -1199,14 +1224,6 @@ function defaultRefreshTokens(store) {
|
|
|
1199
1224
|
const tokenManager = new TokenManager({ store: keepCredentialsOnFailure(store) });
|
|
1200
1225
|
return tokenManager.forceRefresh();
|
|
1201
1226
|
}
|
|
1202
|
-
function keepCredentialsOnFailure(store) {
|
|
1203
|
-
return {
|
|
1204
|
-
load: () => store.load(),
|
|
1205
|
-
save: (creds) => store.save(creds),
|
|
1206
|
-
clear: () => Promise.resolve(),
|
|
1207
|
-
path: () => store.path()
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
1227
|
function sameSlug(a, b) {
|
|
1211
1228
|
return a.toLowerCase() === b.toLowerCase();
|
|
1212
1229
|
}
|
|
@@ -1289,6 +1306,153 @@ async function runLogout(env = process.env) {
|
|
|
1289
1306
|
}
|
|
1290
1307
|
}
|
|
1291
1308
|
|
|
1309
|
+
// src/cli/auth-status.ts
|
|
1310
|
+
var NOT_SIGNED_IN = "Not signed in. Run `npx @myspec/mcp-server auth login` to sign in, or set MYSPEC_API_TOKEN to a MySpec API token.";
|
|
1311
|
+
var SESSION_REVOKED = "Your saved sign-in is no longer valid: the session was ended on the server. Run `npx @myspec/mcp-server auth login` to sign in again.";
|
|
1312
|
+
async function resolveAuthStatus(deps) {
|
|
1313
|
+
const stored = await deps.store.load();
|
|
1314
|
+
if (!stored) {
|
|
1315
|
+
throw new NeedsLoginError(NOT_SIGNED_IN);
|
|
1316
|
+
}
|
|
1317
|
+
let accessToken;
|
|
1318
|
+
try {
|
|
1319
|
+
accessToken = await deps.getAccessToken();
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
throw describeTokenFailure(err, stored.userAuthUrl);
|
|
1322
|
+
}
|
|
1323
|
+
const claims = readTokenClaims(accessToken);
|
|
1324
|
+
const kind = credentialKind(stored, deps.envApiTokenActive);
|
|
1325
|
+
const status = {
|
|
1326
|
+
userAuthUrl: stored.userAuthUrl,
|
|
1327
|
+
credential: { kind, location: deps.store.path() },
|
|
1328
|
+
// The saved user is the fallback only: an API-token record has none.
|
|
1329
|
+
email: claims.email ?? stored.user?.email,
|
|
1330
|
+
access: claims.access
|
|
1331
|
+
};
|
|
1332
|
+
const slug = claims.org;
|
|
1333
|
+
if (slug === void 0) {
|
|
1334
|
+
return status;
|
|
1335
|
+
}
|
|
1336
|
+
if (kind !== "login-session") {
|
|
1337
|
+
return {
|
|
1338
|
+
...status,
|
|
1339
|
+
organization: { slug },
|
|
1340
|
+
orgNameNote: "API tokens do not include the organization name"
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
let organizations;
|
|
1344
|
+
try {
|
|
1345
|
+
organizations = await deps.listOrgs({
|
|
1346
|
+
userAuthUrl: stored.userAuthUrl,
|
|
1347
|
+
accessToken,
|
|
1348
|
+
webappUrl: stored.webappUrl ?? deps.webappUrl
|
|
1349
|
+
});
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
if (err instanceof HttpStatusError && err.status === 401) {
|
|
1352
|
+
throw new NeedsLoginError(SESSION_REVOKED);
|
|
1353
|
+
}
|
|
1354
|
+
return {
|
|
1355
|
+
...status,
|
|
1356
|
+
organization: { slug },
|
|
1357
|
+
orgNameNote: `could not look up its name: ${errorMessage2(err)}`
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
const match = organizations.find((org) => org.slug.toLowerCase() === slug.toLowerCase());
|
|
1361
|
+
if (!match) {
|
|
1362
|
+
return {
|
|
1363
|
+
...status,
|
|
1364
|
+
organization: { slug },
|
|
1365
|
+
orgNameNote: "not among your organizations; it may have been deleted, or you were removed"
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
return { ...status, organization: { slug, name: match.name } };
|
|
1369
|
+
}
|
|
1370
|
+
function describeTokenFailure(err, userAuthUrl) {
|
|
1371
|
+
if (err instanceof TypeError) {
|
|
1372
|
+
return new ConfigError(
|
|
1373
|
+
`Could not reach ${userAuthUrl} to check the sign-in: ${errorMessage2(err)}`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
if (err instanceof HttpStatusError) {
|
|
1377
|
+
return new ConfigError(
|
|
1378
|
+
`Could not check the sign-in against ${userAuthUrl}: ${errorMessage2(err)}`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
return err;
|
|
1382
|
+
}
|
|
1383
|
+
function credentialKind(stored, envApiTokenActive) {
|
|
1384
|
+
if (stored.apiToken === void 0) {
|
|
1385
|
+
return "login-session";
|
|
1386
|
+
}
|
|
1387
|
+
return envApiTokenActive ? "env-api-token" : "file-api-token";
|
|
1388
|
+
}
|
|
1389
|
+
function formatAuthStatus(status) {
|
|
1390
|
+
const lines = [
|
|
1391
|
+
`Signed in to ${status.userAuthUrl}`,
|
|
1392
|
+
` Account: ${status.email ?? "unknown"}`,
|
|
1393
|
+
` Organization: ${formatOrganization(status)}`,
|
|
1394
|
+
` Credential: ${formatCredential(status)}`
|
|
1395
|
+
];
|
|
1396
|
+
if (status.organization === void 0) {
|
|
1397
|
+
lines.push("", `Warning: ${NO_ACTIVE_ORG_HINT}`);
|
|
1398
|
+
}
|
|
1399
|
+
return lines;
|
|
1400
|
+
}
|
|
1401
|
+
function formatOrganization(status) {
|
|
1402
|
+
const org = status.organization;
|
|
1403
|
+
if (org === void 0) {
|
|
1404
|
+
return "none";
|
|
1405
|
+
}
|
|
1406
|
+
if (org.name !== void 0) {
|
|
1407
|
+
return `${org.name} (${org.slug})`;
|
|
1408
|
+
}
|
|
1409
|
+
return status.orgNameNote === void 0 ? org.slug : `${org.slug} (${status.orgNameNote})`;
|
|
1410
|
+
}
|
|
1411
|
+
function formatCredential(status) {
|
|
1412
|
+
const access = status.access === void 0 ? "" : ` (${formatAccess(status.access)})`;
|
|
1413
|
+
switch (status.credential.kind) {
|
|
1414
|
+
case "env-api-token":
|
|
1415
|
+
return `API token from MYSPEC_API_TOKEN${access}`;
|
|
1416
|
+
case "file-api-token":
|
|
1417
|
+
return `API token in ${status.credential.location}${access}`;
|
|
1418
|
+
case "login-session":
|
|
1419
|
+
return `login session (${status.credential.location})`;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
function formatAccess(access) {
|
|
1423
|
+
if (access === "read") {
|
|
1424
|
+
return "read-only";
|
|
1425
|
+
}
|
|
1426
|
+
if (access === "write") {
|
|
1427
|
+
return "read-write";
|
|
1428
|
+
}
|
|
1429
|
+
return access;
|
|
1430
|
+
}
|
|
1431
|
+
function errorMessage2(err) {
|
|
1432
|
+
return redactSensitive(err instanceof Error ? err.message : String(err));
|
|
1433
|
+
}
|
|
1434
|
+
async function runAuthStatus(options, deps = {}) {
|
|
1435
|
+
const log = deps.log ?? ((message) => {
|
|
1436
|
+
process.stderr.write(message + "\n");
|
|
1437
|
+
});
|
|
1438
|
+
const tokenManager = new TokenManager({
|
|
1439
|
+
store: keepCredentialsOnFailure(options.store),
|
|
1440
|
+
fetchImpl: deps.fetchImpl
|
|
1441
|
+
});
|
|
1442
|
+
const listOrgs = deps.listOrgs ?? ((opts) => listOrganizations({ ...opts, fetchImpl: deps.fetchImpl }));
|
|
1443
|
+
const status = await resolveAuthStatus({
|
|
1444
|
+
store: options.store,
|
|
1445
|
+
getAccessToken: () => tokenManager.getValidAccessToken(),
|
|
1446
|
+
listOrgs,
|
|
1447
|
+
webappUrl: options.webappUrl,
|
|
1448
|
+
envApiTokenActive: options.envApiTokenActive
|
|
1449
|
+
});
|
|
1450
|
+
for (const line of formatAuthStatus(status)) {
|
|
1451
|
+
log(line);
|
|
1452
|
+
}
|
|
1453
|
+
return status;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1292
1456
|
// src/platform/client.ts
|
|
1293
1457
|
import { createHash } from "crypto";
|
|
1294
1458
|
|
|
@@ -7328,7 +7492,7 @@ async function handleList(args, opts) {
|
|
|
7328
7492
|
try {
|
|
7329
7493
|
await walk(realPath, "");
|
|
7330
7494
|
} catch (err) {
|
|
7331
|
-
return structuredError("list_dir_failed",
|
|
7495
|
+
return structuredError("list_dir_failed", errorMessage3(err));
|
|
7332
7496
|
}
|
|
7333
7497
|
return structuredOk({ entries, truncated });
|
|
7334
7498
|
}
|
|
@@ -7349,7 +7513,7 @@ async function handleRead(args, opts) {
|
|
|
7349
7513
|
try {
|
|
7350
7514
|
stat2 = await fs4.stat(realPath);
|
|
7351
7515
|
} catch (err) {
|
|
7352
|
-
return structuredError("stat_failed",
|
|
7516
|
+
return structuredError("stat_failed", errorMessage3(err));
|
|
7353
7517
|
}
|
|
7354
7518
|
if (!stat2.isFile()) {
|
|
7355
7519
|
return structuredError("not_a_file", `Not a regular file: ${args.path}`);
|
|
@@ -7365,7 +7529,7 @@ async function handleRead(args, opts) {
|
|
|
7365
7529
|
try {
|
|
7366
7530
|
raw = await fs4.readFile(realPath);
|
|
7367
7531
|
} catch (err) {
|
|
7368
|
-
return structuredError("read_failed",
|
|
7532
|
+
return structuredError("read_failed", errorMessage3(err));
|
|
7369
7533
|
}
|
|
7370
7534
|
const probe = raw.subarray(0, Math.min(READ_FILE_BINARY_PROBE_BYTES, raw.length));
|
|
7371
7535
|
if (probe.includes(0)) {
|
|
@@ -7572,7 +7736,7 @@ async function nodeGrep(searchRoot, realRoot, pattern, opts) {
|
|
|
7572
7736
|
try {
|
|
7573
7737
|
regex = opts.isRegex ? new RegExp(pattern, opts.caseSensitive ? "" : "i") : new RegExp(escapeRegExp(pattern), opts.caseSensitive ? "" : "i");
|
|
7574
7738
|
} catch (err) {
|
|
7575
|
-
throw new InvalidRegexError(
|
|
7739
|
+
throw new InvalidRegexError(errorMessage3(err));
|
|
7576
7740
|
}
|
|
7577
7741
|
const matches = [];
|
|
7578
7742
|
let truncated = false;
|
|
@@ -7646,7 +7810,7 @@ async function nodeGrep(searchRoot, realRoot, pattern, opts) {
|
|
|
7646
7810
|
function escapeRegExp(s) {
|
|
7647
7811
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7648
7812
|
}
|
|
7649
|
-
function
|
|
7813
|
+
function errorMessage3(err) {
|
|
7650
7814
|
return err instanceof Error ? err.message : String(err);
|
|
7651
7815
|
}
|
|
7652
7816
|
function mapPathError(err) {
|
|
@@ -7659,7 +7823,7 @@ function mapPathError(err) {
|
|
|
7659
7823
|
if (err.code === "ENOENT") {
|
|
7660
7824
|
return structuredError("not_found", "Path does not exist");
|
|
7661
7825
|
}
|
|
7662
|
-
return structuredError("resolve_failed",
|
|
7826
|
+
return structuredError("resolve_failed", errorMessage3(err));
|
|
7663
7827
|
}
|
|
7664
7828
|
function mapPackError(err) {
|
|
7665
7829
|
if (err instanceof PackCacheMissError) {
|
|
@@ -7674,7 +7838,7 @@ function mapPackError(err) {
|
|
|
7674
7838
|
if (err instanceof AbsolutePathError || err instanceof PathOutsideRootError) {
|
|
7675
7839
|
return mapPathError(err);
|
|
7676
7840
|
}
|
|
7677
|
-
return structuredError("pack_failed",
|
|
7841
|
+
return structuredError("pack_failed", errorMessage3(err));
|
|
7678
7842
|
}
|
|
7679
7843
|
function registerLocalFsTools(server, opts) {
|
|
7680
7844
|
const packCache = new PackCache();
|
|
@@ -7753,7 +7917,7 @@ function registerLocalFsTools(server, opts) {
|
|
|
7753
7917
|
`);
|
|
7754
7918
|
return result;
|
|
7755
7919
|
} catch (err) {
|
|
7756
|
-
const msg =
|
|
7920
|
+
const msg = errorMessage3(err);
|
|
7757
7921
|
const elapsedMs = Math.round(performance.now() - started);
|
|
7758
7922
|
process.stdout.write(`[tool] ${name} -> threw ${String(elapsedMs)}ms ${msg}
|
|
7759
7923
|
`);
|
|
@@ -8114,20 +8278,49 @@ function createReverseMcpPresenceProbe(deps) {
|
|
|
8114
8278
|
}
|
|
8115
8279
|
|
|
8116
8280
|
// src/index.ts
|
|
8281
|
+
var AUTH_COMMANDS = ["login", "logout", "status"];
|
|
8282
|
+
function isAuthCommand(value) {
|
|
8283
|
+
return AUTH_COMMANDS.includes(value);
|
|
8284
|
+
}
|
|
8117
8285
|
function parseArgs(argv) {
|
|
8118
8286
|
const [first, ...rest] = argv;
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8287
|
+
if (first === "auth") {
|
|
8288
|
+
const [sub, ...subRest] = rest;
|
|
8289
|
+
if (sub === void 0 || sub === "help" || sub === "--help" || sub === "-h") {
|
|
8290
|
+
return { command: "help", flags: /* @__PURE__ */ new Map() };
|
|
8291
|
+
}
|
|
8292
|
+
if (!isAuthCommand(sub)) {
|
|
8293
|
+
throw new ConfigError(
|
|
8294
|
+
`Unknown auth command: ${sub}. Expected one of: ${AUTH_COMMANDS.join(", ")}.`
|
|
8295
|
+
);
|
|
8296
|
+
}
|
|
8297
|
+
return authArgs(sub, subRest);
|
|
8298
|
+
}
|
|
8299
|
+
if (first === "login" || first === "logout") {
|
|
8300
|
+
return authArgs(first, rest);
|
|
8301
|
+
}
|
|
8302
|
+
if (first === "serve" || first === "reverse" || first === "help" || first === "version") {
|
|
8303
|
+
return { command: first, flags: parseFlags(rest) };
|
|
8304
|
+
}
|
|
8305
|
+
if (first === "--help" || first === "-h") {
|
|
8306
|
+
return { command: "help", flags: parseFlags(rest) };
|
|
8307
|
+
}
|
|
8308
|
+
if (first === "--version" || first === "-v") {
|
|
8309
|
+
return { command: "version", flags: parseFlags(rest) };
|
|
8310
|
+
}
|
|
8311
|
+
return { command: "serve", flags: parseFlags(argv) };
|
|
8312
|
+
}
|
|
8313
|
+
function authArgs(authCommand, args) {
|
|
8314
|
+
if (args.includes("-h")) {
|
|
8315
|
+
return { command: "help", flags: /* @__PURE__ */ new Map() };
|
|
8130
8316
|
}
|
|
8317
|
+
const flags = parseFlags(args);
|
|
8318
|
+
if (flags.has("help")) {
|
|
8319
|
+
return { command: "help", flags };
|
|
8320
|
+
}
|
|
8321
|
+
return { command: "auth", authCommand, flags };
|
|
8322
|
+
}
|
|
8323
|
+
function parseFlags(flagArgs) {
|
|
8131
8324
|
const flags = /* @__PURE__ */ new Map();
|
|
8132
8325
|
const unknownPositional = [];
|
|
8133
8326
|
for (let i = 0; i < flagArgs.length; i += 1) {
|
|
@@ -8157,7 +8350,7 @@ function parseArgs(argv) {
|
|
|
8157
8350
|
`Unknown positional arguments: ${unknownPositional.join(", ")}. Run \`npx @myspec/mcp-server --help\` for usage.`
|
|
8158
8351
|
);
|
|
8159
8352
|
}
|
|
8160
|
-
return
|
|
8353
|
+
return flags;
|
|
8161
8354
|
}
|
|
8162
8355
|
function flagString(flags, key) {
|
|
8163
8356
|
const value = flags.get(key);
|
|
@@ -8176,16 +8369,21 @@ function printHelp() {
|
|
|
8176
8369
|
"Commands:",
|
|
8177
8370
|
" (default) Start MCP server over stdio",
|
|
8178
8371
|
" serve Start MCP server over stdio",
|
|
8179
|
-
" login
|
|
8180
|
-
" login --paste
|
|
8181
|
-
" the printed URL yourself; if the page cannot reach
|
|
8182
|
-
" CLI it shows a `<code>.<state>` token to paste back.",
|
|
8183
|
-
" login --org <slug>
|
|
8372
|
+
" auth login Sign in via browser (chooser page on the webapp)",
|
|
8373
|
+
" auth login --paste Same as auth login, but never auto-opens a browser.",
|
|
8374
|
+
" Open the printed URL yourself; if the page cannot reach",
|
|
8375
|
+
" this CLI it shows a `<code>.<state>` token to paste back.",
|
|
8376
|
+
" auth login --org <slug> Sign in and pin that organization. Tokens only work",
|
|
8184
8377
|
" when an organization is active; the CLI asks when you",
|
|
8185
8378
|
" belong to several and this flag is not given.",
|
|
8186
|
-
" logout
|
|
8379
|
+
" auth logout Sign out and clear the session credentials. A",
|
|
8187
8380
|
" configured apiToken is KEPT, so an unattended",
|
|
8188
8381
|
" install keeps working.",
|
|
8382
|
+
" auth status Show whether you are signed in, the account email and",
|
|
8383
|
+
" the active organization. Renews an expired access",
|
|
8384
|
+
" token first, as any command does. Exits 2 when not",
|
|
8385
|
+
" signed in.",
|
|
8386
|
+
" login, logout Aliases for `auth login` and `auth logout`.",
|
|
8189
8387
|
" reverse --root <dir> Connect to ai-agent and expose local_fs tools.",
|
|
8190
8388
|
" The agent WebSocket URL is discovered from the webapp",
|
|
8191
8389
|
" (GET /api/v1/config) using your saved credentials;",
|
|
@@ -8238,14 +8436,28 @@ function assertOrgFlagHasValue(flags) {
|
|
|
8238
8436
|
}
|
|
8239
8437
|
}
|
|
8240
8438
|
async function main(argv = process.argv.slice(2)) {
|
|
8241
|
-
const
|
|
8242
|
-
|
|
8439
|
+
const parsed = parseArgs(argv);
|
|
8440
|
+
const { flags } = parsed;
|
|
8441
|
+
switch (parsed.command) {
|
|
8243
8442
|
case "help":
|
|
8244
8443
|
printHelp();
|
|
8245
8444
|
return;
|
|
8246
8445
|
case "version":
|
|
8247
8446
|
process.stderr.write(readVersion() + "\n");
|
|
8248
8447
|
return;
|
|
8448
|
+
case "auth":
|
|
8449
|
+
await runAuthCommand(parsed.authCommand, flags);
|
|
8450
|
+
return;
|
|
8451
|
+
case "reverse":
|
|
8452
|
+
await runReverseCommand(flags);
|
|
8453
|
+
return;
|
|
8454
|
+
case "serve":
|
|
8455
|
+
await runServe(flags);
|
|
8456
|
+
return;
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
async function runAuthCommand(authCommand, flags) {
|
|
8460
|
+
switch (authCommand) {
|
|
8249
8461
|
case "login":
|
|
8250
8462
|
assertOrgFlagHasValue(flags);
|
|
8251
8463
|
await runLogin({
|
|
@@ -8257,13 +8469,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
8257
8469
|
case "logout":
|
|
8258
8470
|
await runLogout();
|
|
8259
8471
|
return;
|
|
8260
|
-
case "
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
await
|
|
8472
|
+
case "status": {
|
|
8473
|
+
assertNoEnvAccessToken();
|
|
8474
|
+
const { store, config, envApiTokenActive } = await buildCredentialsStore({
|
|
8475
|
+
cliUserAuthUrl: flagString(flags, "user-auth-url")
|
|
8476
|
+
});
|
|
8477
|
+
await runAuthStatus({ store, webappUrl: config.webappUrl, envApiTokenActive });
|
|
8266
8478
|
return;
|
|
8479
|
+
}
|
|
8267
8480
|
}
|
|
8268
8481
|
}
|
|
8269
8482
|
var REVERSE_DISCOVERY_HINT = "Pass --agent-url <url>, set MYSPEC_AI_AGENT_WS_URL, or run `npx @myspec/mcp-server login` and retry.";
|
|
@@ -8353,17 +8566,12 @@ async function buildCredentialsStore(opts = {}) {
|
|
|
8353
8566
|
// ConfigError exit — and one that never names the file it came from.
|
|
8354
8567
|
storedUserAuthUrl: stored?.userAuthUrl ?? usableAuthUrl(fromSettings, settingsPath, warn)
|
|
8355
8568
|
});
|
|
8569
|
+
const envConfig = readEnvApiToken(config.userAuthUrl, env, warn);
|
|
8356
8570
|
return {
|
|
8357
|
-
store: createEnvApiTokenStore({
|
|
8358
|
-
fileStore,
|
|
8359
|
-
// `warn` rather than the default sink, so the "cannot be sent as a
|
|
8360
|
-
// header" warning lands wherever the caller's other warnings do.
|
|
8361
|
-
envConfig: readEnvApiToken(config.userAuthUrl, env, warn),
|
|
8362
|
-
settingsPath,
|
|
8363
|
-
warn
|
|
8364
|
-
}),
|
|
8571
|
+
store: createEnvApiTokenStore({ fileStore, envConfig, settingsPath, warn }),
|
|
8365
8572
|
config,
|
|
8366
|
-
usingEnvApiToken
|
|
8573
|
+
usingEnvApiToken,
|
|
8574
|
+
envApiTokenActive: envConfig !== null
|
|
8367
8575
|
};
|
|
8368
8576
|
}
|
|
8369
8577
|
function warnIfStaticTokenShadowsApiToken(inlineToken, env = process.env, warn = (message) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "1.1.1-next.
|
|
3
|
+
"version": "1.1.1-next.157",
|
|
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": {
|