@myspec/mcp-server 0.1.0-next.3 → 0.1.0-next.5
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 +24 -6
- package/dist/index.js +224 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,26 +23,43 @@ 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
|
|
26
|
+
reverse --root <dir> Connect to ai-agent and expose local_fs tools
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
### `reverse`
|
|
29
|
+
### `reverse`
|
|
30
30
|
|
|
31
31
|
`reverse` connects out to the ai-agent service over WebSocket and exposes a set
|
|
32
32
|
of `local_fs` tools scoped to a single local directory, so a cloud agent can
|
|
33
|
-
read files from your machine.
|
|
33
|
+
read files from your machine.
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
npx @myspec/mcp-server@next reverse --root /path/to/project
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
The ai-agent WebSocket URL is **discovered at startup** from the webapp's
|
|
40
|
+
authenticated `GET /api/v1/config` endpoint using your saved credentials, so
|
|
41
|
+
the agent domain is never hardcoded in the CLI and can change server-side.
|
|
42
|
+
If discovery fails, `reverse` exits with an actionable error instead of
|
|
43
|
+
guessing — pass `--agent-url` or set `MYSPEC_AI_AGENT_WS_URL` to skip
|
|
44
|
+
discovery (e.g. against a local ai-agent):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @myspec/mcp-server@next reverse --root . --agent-url ws://localhost:3001/mcp/reverse
|
|
48
|
+
```
|
|
49
|
+
|
|
39
50
|
| Flag / env | Purpose |
|
|
40
51
|
|---|---|
|
|
41
52
|
| `--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
|
|
53
|
+
| `--agent-url <url>` / `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL override; skips webapp discovery entirely. |
|
|
54
|
+
| `--webapp-url <url>` | Webapp base URL used for discovery (default: the URL stored at login, else derived from the auth host). |
|
|
43
55
|
| `--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
56
|
|
|
45
|
-
|
|
57
|
+
Local state is stored under `~/.myspec/` (alongside the on-disk cache root), split into two files:
|
|
58
|
+
|
|
59
|
+
- `settings.json` — non-secret config: the auth-server, platform, and webapp URLs you logged in with.
|
|
60
|
+
- `oauth_creds.json` — the OAuth tokens (access/refresh/expiry) and your user identity, written mode `0600`.
|
|
61
|
+
|
|
62
|
+
`logout` removes `oauth_creds.json`; `settings.json` is left in place.
|
|
46
63
|
|
|
47
64
|
## Environment variables
|
|
48
65
|
|
|
@@ -52,7 +69,8 @@ Credentials are persisted at `$XDG_CONFIG_HOME/myspec/mcp-server.json` (POSIX) o
|
|
|
52
69
|
| `MYSPEC_PLATFORM_URL` | platform API base URL (default `https://platform.myspec.dev`) |
|
|
53
70
|
| `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`. |
|
|
54
71
|
| `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
|
|
55
|
-
| `MYSPEC_WEBAPP_URL` | Webapp base URL used by `login
|
|
72
|
+
| `MYSPEC_WEBAPP_URL` | Webapp base URL used by `login` (the `/auth/cli` page) and by `reverse` discovery (default: the URL stored at login, else derived from `MYSPEC_USER_AUTH_URL`) |
|
|
73
|
+
| `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL for `reverse`; skips the webapp discovery call |
|
|
56
74
|
|
|
57
75
|
## Tools
|
|
58
76
|
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,7 @@ async function loopbackLogin(opts) {
|
|
|
90
90
|
expiresAt: now() + exchanged.expiresIn * 1e3,
|
|
91
91
|
userAuthUrl: opts.userAuthUrl,
|
|
92
92
|
platformUrl: opts.platformUrl,
|
|
93
|
+
webappUrl: opts.webappUrl,
|
|
93
94
|
user: exchanged.user
|
|
94
95
|
};
|
|
95
96
|
}
|
|
@@ -266,6 +267,7 @@ async function pasteLogin(opts) {
|
|
|
266
267
|
expiresAt: now() + exchanged.expiresIn * 1e3,
|
|
267
268
|
userAuthUrl: opts.userAuthUrl,
|
|
268
269
|
platformUrl: opts.platformUrl,
|
|
270
|
+
webappUrl: opts.webappUrl,
|
|
269
271
|
user: exchanged.user
|
|
270
272
|
};
|
|
271
273
|
}
|
|
@@ -282,57 +284,117 @@ async function defaultPrompt(question) {
|
|
|
282
284
|
import { promises as fs } from "fs";
|
|
283
285
|
import os from "os";
|
|
284
286
|
import path from "path";
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
function myspecDir() {
|
|
288
|
+
return path.join(os.homedir(), ".myspec");
|
|
289
|
+
}
|
|
290
|
+
function defaultSettingsPath() {
|
|
291
|
+
return path.join(myspecDir(), "settings.json");
|
|
292
|
+
}
|
|
293
|
+
function defaultOauthCredsPath() {
|
|
294
|
+
return path.join(myspecDir(), "oauth_creds.json");
|
|
292
295
|
}
|
|
293
|
-
function createFileCredentialsStore(
|
|
294
|
-
const
|
|
296
|
+
function createFileCredentialsStore(paths = {}) {
|
|
297
|
+
const settingsPath = paths.settingsPath ?? defaultSettingsPath();
|
|
298
|
+
const oauthCredsPath = paths.oauthCredsPath ?? defaultOauthCredsPath();
|
|
295
299
|
return {
|
|
296
|
-
|
|
300
|
+
// The secret bundle is the credential file users care about (`login`
|
|
301
|
+
// prints this); settings.json sits next to it.
|
|
302
|
+
path: () => oauthCredsPath,
|
|
297
303
|
async load() {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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;
|
|
304
|
+
const oauth = await loadOauthCreds(oauthCredsPath);
|
|
305
|
+
if (!oauth) {
|
|
306
|
+
return null;
|
|
315
307
|
}
|
|
308
|
+
const settings = await loadSettings(settingsPath);
|
|
309
|
+
if (!settings) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
accessToken: oauth.accessToken,
|
|
314
|
+
refreshToken: oauth.refreshToken,
|
|
315
|
+
expiresAt: oauth.expiresAt,
|
|
316
|
+
userAuthUrl: settings.userAuthUrl,
|
|
317
|
+
platformUrl: settings.platformUrl,
|
|
318
|
+
webappUrl: settings.webappUrl,
|
|
319
|
+
user: oauth.user
|
|
320
|
+
};
|
|
316
321
|
},
|
|
317
322
|
async save(creds) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
await writeJsonFile(
|
|
324
|
+
settingsPath,
|
|
325
|
+
{
|
|
326
|
+
userAuthUrl: creds.userAuthUrl,
|
|
327
|
+
platformUrl: creds.platformUrl,
|
|
328
|
+
webappUrl: creds.webappUrl
|
|
329
|
+
},
|
|
330
|
+
{ secret: false }
|
|
331
|
+
);
|
|
332
|
+
await writeJsonFile(
|
|
333
|
+
oauthCredsPath,
|
|
334
|
+
{
|
|
335
|
+
accessToken: creds.accessToken,
|
|
336
|
+
refreshToken: creds.refreshToken,
|
|
337
|
+
expiresAt: creds.expiresAt,
|
|
338
|
+
user: creds.user
|
|
339
|
+
},
|
|
340
|
+
{ secret: true }
|
|
341
|
+
);
|
|
324
342
|
},
|
|
325
343
|
async clear() {
|
|
326
|
-
|
|
327
|
-
await fs.unlink(target);
|
|
328
|
-
} catch (err) {
|
|
329
|
-
if (!isFsNotFound(err)) {
|
|
330
|
-
throw err;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
344
|
+
await unlinkIfExists(oauthCredsPath);
|
|
333
345
|
}
|
|
334
346
|
};
|
|
335
347
|
}
|
|
348
|
+
async function loadOauthCreds(target) {
|
|
349
|
+
try {
|
|
350
|
+
if (process.platform !== "win32") {
|
|
351
|
+
const stat = await fs.stat(target);
|
|
352
|
+
if ((stat.mode & 63) !== 0) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Refusing to load credentials from ${target}: file mode is too permissive (${(stat.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const raw = await fs.readFile(target, "utf-8");
|
|
359
|
+
return parseOauthCreds(JSON.parse(raw));
|
|
360
|
+
} catch (err) {
|
|
361
|
+
if (isFsNotFound(err)) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
throw err;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function loadSettings(target) {
|
|
368
|
+
try {
|
|
369
|
+
const raw = await fs.readFile(target, "utf-8");
|
|
370
|
+
return parseSettings(JSON.parse(raw));
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (isFsNotFound(err)) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function writeJsonFile(target, data, opts) {
|
|
379
|
+
const dir = path.dirname(target);
|
|
380
|
+
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
381
|
+
if (process.platform !== "win32") {
|
|
382
|
+
await fs.chmod(dir, 448);
|
|
383
|
+
}
|
|
384
|
+
await fs.writeFile(target, JSON.stringify(data, null, 2), { mode: 384 });
|
|
385
|
+
if (opts.secret && process.platform !== "win32") {
|
|
386
|
+
await fs.chmod(target, 384);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function unlinkIfExists(target) {
|
|
390
|
+
try {
|
|
391
|
+
await fs.unlink(target);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (!isFsNotFound(err)) {
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
336
398
|
var DEFAULT_USER_AUTH_URL = "https://auth.myspec.dev";
|
|
337
399
|
function readEnvRefreshConfig(env = process.env) {
|
|
338
400
|
if (env.MYSPEC_ACCESS_TOKEN || env.MYSPEC_ACCESS_TOKEN_EXPIRES_AT) {
|
|
@@ -380,19 +442,17 @@ function createCompositeCredentialsStore(opts) {
|
|
|
380
442
|
envUserAuthUrl: () => envUserAuthUrl
|
|
381
443
|
};
|
|
382
444
|
}
|
|
383
|
-
function
|
|
445
|
+
function parseOauthCreds(value) {
|
|
384
446
|
if (typeof value !== "object" || value === null) {
|
|
385
|
-
throw new Error("
|
|
447
|
+
throw new Error("oauth_creds.json is malformed (not an object)");
|
|
386
448
|
}
|
|
387
449
|
const v = value;
|
|
388
450
|
const accessToken = requireString(v, "accessToken");
|
|
389
451
|
const refreshToken = requireString(v, "refreshToken");
|
|
390
452
|
const expiresAt = requireNumber(v, "expiresAt");
|
|
391
|
-
const userAuthUrl = requireString(v, "userAuthUrl");
|
|
392
|
-
const platformUrl = typeof v.platformUrl === "string" ? v.platformUrl : void 0;
|
|
393
453
|
const userRaw = v.user;
|
|
394
454
|
if (typeof userRaw !== "object" || userRaw === null) {
|
|
395
|
-
throw new Error("
|
|
455
|
+
throw new Error("oauth_creds.json is malformed (missing user)");
|
|
396
456
|
}
|
|
397
457
|
const userObj = userRaw;
|
|
398
458
|
const user = {
|
|
@@ -400,7 +460,17 @@ function parseCredentials(value) {
|
|
|
400
460
|
email: requireString(userObj, "email"),
|
|
401
461
|
name: typeof userObj.name === "string" ? userObj.name : void 0
|
|
402
462
|
};
|
|
403
|
-
return { accessToken, refreshToken, expiresAt,
|
|
463
|
+
return { accessToken, refreshToken, expiresAt, user };
|
|
464
|
+
}
|
|
465
|
+
function parseSettings(value) {
|
|
466
|
+
if (typeof value !== "object" || value === null) {
|
|
467
|
+
throw new Error("settings.json is malformed (not an object)");
|
|
468
|
+
}
|
|
469
|
+
const v = value;
|
|
470
|
+
const userAuthUrl = requireString(v, "userAuthUrl");
|
|
471
|
+
const platformUrl = typeof v.platformUrl === "string" ? v.platformUrl : void 0;
|
|
472
|
+
const webappUrl = typeof v.webappUrl === "string" ? v.webappUrl : void 0;
|
|
473
|
+
return { userAuthUrl, platformUrl, webappUrl };
|
|
404
474
|
}
|
|
405
475
|
function requireString(obj, key) {
|
|
406
476
|
const value = obj[key];
|
|
@@ -425,7 +495,7 @@ function resolveConfig(sources = {}) {
|
|
|
425
495
|
const env = sources.env ?? process.env;
|
|
426
496
|
const userAuthUrl = sources.cliUserAuthUrl ?? env.MYSPEC_USER_AUTH_URL ?? sources.storedUserAuthUrl ?? "https://auth.myspec.dev";
|
|
427
497
|
const platformUrl = sources.cliPlatformUrl ?? env.MYSPEC_PLATFORM_URL ?? sources.storedPlatformUrl ?? "https://platform.myspec.dev";
|
|
428
|
-
const webappUrl = sources.cliWebappUrl ?? env.MYSPEC_WEBAPP_URL ?? deriveWebappFromAuth(userAuthUrl);
|
|
498
|
+
const webappUrl = sources.cliWebappUrl ?? env.MYSPEC_WEBAPP_URL ?? sources.storedWebappUrl ?? deriveWebappFromAuth(userAuthUrl);
|
|
429
499
|
validateUrl(userAuthUrl, "user-auth URL");
|
|
430
500
|
validateUrl(platformUrl, "platform URL");
|
|
431
501
|
validateUrl(webappUrl, "webapp URL");
|
|
@@ -589,6 +659,7 @@ var TokenManager = class {
|
|
|
589
659
|
expiresAt: 0,
|
|
590
660
|
userAuthUrl: this.envFallback.userAuthUrl,
|
|
591
661
|
platformUrl: creds.platformUrl,
|
|
662
|
+
webappUrl: creds.webappUrl,
|
|
592
663
|
user: creds.user
|
|
593
664
|
};
|
|
594
665
|
try {
|
|
@@ -3384,6 +3455,64 @@ function sleep2(ms) {
|
|
|
3384
3455
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3385
3456
|
}
|
|
3386
3457
|
|
|
3458
|
+
// src/reverse/discover.ts
|
|
3459
|
+
async function discoverAgentUrl(opts) {
|
|
3460
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
3461
|
+
const endpoint = `${opts.webappUrl}/api/v1/config`;
|
|
3462
|
+
let response;
|
|
3463
|
+
try {
|
|
3464
|
+
const token = await opts.getAccessToken();
|
|
3465
|
+
response = await fetchConfig(endpoint, token, fetchImpl);
|
|
3466
|
+
if (response.status === 401 && opts.forceRefresh) {
|
|
3467
|
+
const refreshed = await opts.forceRefresh();
|
|
3468
|
+
response = await fetchConfig(endpoint, refreshed, fetchImpl);
|
|
3469
|
+
}
|
|
3470
|
+
} catch (err) {
|
|
3471
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3472
|
+
throw discoveryError(endpoint, message);
|
|
3473
|
+
}
|
|
3474
|
+
if (!response.ok) {
|
|
3475
|
+
throw discoveryError(endpoint, `HTTP ${String(response.status)}`);
|
|
3476
|
+
}
|
|
3477
|
+
let body;
|
|
3478
|
+
try {
|
|
3479
|
+
body = await response.json();
|
|
3480
|
+
} catch {
|
|
3481
|
+
throw discoveryError(endpoint, "response is not valid JSON");
|
|
3482
|
+
}
|
|
3483
|
+
const agentUrl = body.aiAgentMcpReverseUrl;
|
|
3484
|
+
if (typeof agentUrl !== "string" || agentUrl.length === 0) {
|
|
3485
|
+
throw discoveryError(endpoint, "response is missing aiAgentMcpReverseUrl");
|
|
3486
|
+
}
|
|
3487
|
+
assertWebSocketUrl(agentUrl, endpoint);
|
|
3488
|
+
return agentUrl;
|
|
3489
|
+
}
|
|
3490
|
+
function fetchConfig(endpoint, token, fetchImpl) {
|
|
3491
|
+
return fetchImpl(endpoint, {
|
|
3492
|
+
method: "GET",
|
|
3493
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
function assertWebSocketUrl(value, endpoint) {
|
|
3497
|
+
let parsed;
|
|
3498
|
+
try {
|
|
3499
|
+
parsed = new URL(value);
|
|
3500
|
+
} catch {
|
|
3501
|
+
throw discoveryError(endpoint, `returned an invalid URL: ${value}`);
|
|
3502
|
+
}
|
|
3503
|
+
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
3504
|
+
throw discoveryError(
|
|
3505
|
+
endpoint,
|
|
3506
|
+
`returned a non-WebSocket URL (${value}); expected ws:// or wss://`
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
function discoveryError(endpoint, reason) {
|
|
3511
|
+
return new ConfigError(
|
|
3512
|
+
`Could not discover the ai-agent URL from ${endpoint} (${reason}). Pass --agent-url <url>, set MYSPEC_AI_AGENT_WS_URL, or run \`npx @myspec/mcp-server login\` and retry.`
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3387
3516
|
// src/index.ts
|
|
3388
3517
|
function parseArgs(argv) {
|
|
3389
3518
|
const [first, ...rest] = argv;
|
|
@@ -3450,22 +3579,27 @@ function printHelp() {
|
|
|
3450
3579
|
" login Sign in via browser (chooser page on the webapp)",
|
|
3451
3580
|
" login --paste Sign in by pasting a one-time code",
|
|
3452
3581
|
" logout Revoke refresh token and clear local credentials",
|
|
3453
|
-
" reverse --root <dir> Connect to ai-agent and expose local_fs tools
|
|
3582
|
+
" reverse --root <dir> Connect to ai-agent and expose local_fs tools.",
|
|
3583
|
+
" The agent WebSocket URL is discovered from the webapp",
|
|
3584
|
+
" (GET /api/v1/config) using your saved credentials;",
|
|
3585
|
+
" override with --agent-url <url> or MYSPEC_AI_AGENT_WS_URL.",
|
|
3454
3586
|
" Uses the saved refresh_token from `npx @myspec/mcp-server login`",
|
|
3455
3587
|
" to obtain & auto-refresh access tokens. Override with",
|
|
3456
3588
|
" --access-token <jwt> or MYSPEC_ACCESS_TOKEN for local testing.",
|
|
3457
3589
|
" --version Print version",
|
|
3458
3590
|
" --help Print this help",
|
|
3459
3591
|
"",
|
|
3460
|
-
"Login flags:",
|
|
3592
|
+
"Login / reverse flags:",
|
|
3461
3593
|
" --user-auth-url <url> user-auth base URL (overrides env / defaults)",
|
|
3462
3594
|
" --platform-url <url> platform API base URL",
|
|
3463
|
-
" --webapp-url <url> webapp base URL
|
|
3595
|
+
" --webapp-url <url> webapp base URL (login: hosts the /auth/cli page;",
|
|
3596
|
+
" reverse: hosts the /api/v1/config discovery endpoint)",
|
|
3464
3597
|
"",
|
|
3465
3598
|
"Environment:",
|
|
3466
3599
|
" MYSPEC_USER_AUTH_URL user-auth base URL (default https://auth.myspec.dev)",
|
|
3467
3600
|
" MYSPEC_PLATFORM_URL platform API base URL (default https://platform.myspec.dev)",
|
|
3468
3601
|
" MYSPEC_WEBAPP_URL webapp base URL (default derived from user-auth host)",
|
|
3602
|
+
" MYSPEC_AI_AGENT_WS_URL ai-agent WebSocket URL for `reverse` (skips discovery)",
|
|
3469
3603
|
" MYSPEC_REFRESH_TOKEN Skip file-based credentials; the server mints a",
|
|
3470
3604
|
" fresh access token on first use via this refresh",
|
|
3471
3605
|
" token."
|
|
@@ -3516,16 +3650,35 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3516
3650
|
return;
|
|
3517
3651
|
}
|
|
3518
3652
|
}
|
|
3653
|
+
async function resolveReverseAgentUrl(sources) {
|
|
3654
|
+
const env = sources.env ?? process.env;
|
|
3655
|
+
const explicitAgentUrl = sources.flagAgentUrl ?? env.MYSPEC_AI_AGENT_WS_URL;
|
|
3656
|
+
if (explicitAgentUrl) {
|
|
3657
|
+
return { agentUrl: explicitAgentUrl };
|
|
3658
|
+
}
|
|
3659
|
+
const stored = await sources.loadStored().catch(() => null);
|
|
3660
|
+
const config = resolveConfig({
|
|
3661
|
+
env,
|
|
3662
|
+
cliWebappUrl: sources.cliWebappUrl,
|
|
3663
|
+
storedUserAuthUrl: stored?.userAuthUrl,
|
|
3664
|
+
storedWebappUrl: stored?.webappUrl
|
|
3665
|
+
});
|
|
3666
|
+
const agentUrl = await sources.discover(config.webappUrl);
|
|
3667
|
+
return { agentUrl, discoveredVia: `${config.webappUrl}/api/v1/config` };
|
|
3668
|
+
}
|
|
3519
3669
|
async function runReverseCommand(flags) {
|
|
3520
3670
|
const root = flagString(flags, "root") ?? process.cwd();
|
|
3521
|
-
const agentUrl = flagString(flags, "agent-url") ?? process.env.MYSPEC_AI_AGENT_WS_URL ?? "ws://localhost:3001/mcp/reverse";
|
|
3522
3671
|
const inlineToken = flagString(flags, "access-token") ?? process.env.MYSPEC_ACCESS_TOKEN;
|
|
3523
3672
|
let getAccessToken;
|
|
3524
3673
|
let onAuthFailed;
|
|
3674
|
+
let forceRefresh;
|
|
3675
|
+
let loadStored;
|
|
3525
3676
|
if (inlineToken) {
|
|
3526
3677
|
const staticToken = inlineToken;
|
|
3527
3678
|
getAccessToken = () => Promise.resolve(staticToken);
|
|
3528
3679
|
onAuthFailed = void 0;
|
|
3680
|
+
forceRefresh = void 0;
|
|
3681
|
+
loadStored = () => Promise.resolve(null);
|
|
3529
3682
|
} else {
|
|
3530
3683
|
const envConfig = readEnvRefreshConfig();
|
|
3531
3684
|
const store = createCompositeCredentialsStore({
|
|
@@ -3537,10 +3690,26 @@ async function runReverseCommand(flags) {
|
|
|
3537
3690
|
onAuthFailed = async () => {
|
|
3538
3691
|
await tokenManager.forceRefresh();
|
|
3539
3692
|
};
|
|
3693
|
+
forceRefresh = () => tokenManager.forceRefresh();
|
|
3694
|
+
loadStored = () => store.load();
|
|
3695
|
+
}
|
|
3696
|
+
const resolved = await resolveReverseAgentUrl({
|
|
3697
|
+
flagAgentUrl: flagString(flags, "agent-url"),
|
|
3698
|
+
cliWebappUrl: flagString(flags, "webapp-url"),
|
|
3699
|
+
loadStored,
|
|
3700
|
+
discover: (webappUrl) => {
|
|
3701
|
+
return discoverAgentUrl({ webappUrl, getAccessToken, forceRefresh });
|
|
3702
|
+
}
|
|
3703
|
+
});
|
|
3704
|
+
if (resolved.discoveredVia) {
|
|
3705
|
+
process.stderr.write(
|
|
3706
|
+
`myspec-mcp reverse: discovered agent URL via ${resolved.discoveredVia}
|
|
3707
|
+
`
|
|
3708
|
+
);
|
|
3540
3709
|
}
|
|
3541
3710
|
await runReverse({
|
|
3542
3711
|
root,
|
|
3543
|
-
agentUrl,
|
|
3712
|
+
agentUrl: resolved.agentUrl,
|
|
3544
3713
|
getAccessToken,
|
|
3545
3714
|
onAuthFailed,
|
|
3546
3715
|
version: readVersion()
|
|
@@ -3562,7 +3731,8 @@ async function runServe(flags) {
|
|
|
3562
3731
|
cliUserAuthUrl: flagString(flags, "user-auth-url"),
|
|
3563
3732
|
cliPlatformUrl: flagString(flags, "platform-url"),
|
|
3564
3733
|
storedUserAuthUrl: initial?.userAuthUrl,
|
|
3565
|
-
storedPlatformUrl: initial?.platformUrl
|
|
3734
|
+
storedPlatformUrl: initial?.platformUrl,
|
|
3735
|
+
storedWebappUrl: initial?.webappUrl
|
|
3566
3736
|
});
|
|
3567
3737
|
const tokenManager = new TokenManager({
|
|
3568
3738
|
store,
|
|
@@ -3600,5 +3770,6 @@ if (isCliEntry()) {
|
|
|
3600
3770
|
}
|
|
3601
3771
|
export {
|
|
3602
3772
|
main,
|
|
3773
|
+
resolveReverseAgentUrl,
|
|
3603
3774
|
runServe
|
|
3604
3775
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.5",
|
|
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": {
|