@myspec/mcp-server 0.1.0-next.4 → 0.1.0-next.6
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 +19 -6
- package/dist/index.js +127 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,28 +23,40 @@ 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:
|
|
46
58
|
|
|
47
|
-
- `settings.json` — non-secret config: the auth-server and
|
|
59
|
+
- `settings.json` — non-secret config: the auth-server, platform, and webapp URLs you logged in with.
|
|
48
60
|
- `oauth_creds.json` — the OAuth tokens (access/refresh/expiry) and your user identity, written mode `0600`.
|
|
49
61
|
|
|
50
62
|
`logout` removes `oauth_creds.json`; `settings.json` is left in place.
|
|
@@ -57,7 +69,8 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
57
69
|
| `MYSPEC_PLATFORM_URL` | platform API base URL (default `https://platform.myspec.dev`) |
|
|
58
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`. |
|
|
59
71
|
| `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
|
|
60
|
-
| `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 |
|
|
61
74
|
|
|
62
75
|
## Tools
|
|
63
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
|
}
|
|
@@ -313,13 +315,18 @@ function createFileCredentialsStore(paths = {}) {
|
|
|
313
315
|
expiresAt: oauth.expiresAt,
|
|
314
316
|
userAuthUrl: settings.userAuthUrl,
|
|
315
317
|
platformUrl: settings.platformUrl,
|
|
318
|
+
webappUrl: settings.webappUrl,
|
|
316
319
|
user: oauth.user
|
|
317
320
|
};
|
|
318
321
|
},
|
|
319
322
|
async save(creds) {
|
|
320
323
|
await writeJsonFile(
|
|
321
324
|
settingsPath,
|
|
322
|
-
{
|
|
325
|
+
{
|
|
326
|
+
userAuthUrl: creds.userAuthUrl,
|
|
327
|
+
platformUrl: creds.platformUrl,
|
|
328
|
+
webappUrl: creds.webappUrl
|
|
329
|
+
},
|
|
323
330
|
{ secret: false }
|
|
324
331
|
);
|
|
325
332
|
await writeJsonFile(
|
|
@@ -462,7 +469,8 @@ function parseSettings(value) {
|
|
|
462
469
|
const v = value;
|
|
463
470
|
const userAuthUrl = requireString(v, "userAuthUrl");
|
|
464
471
|
const platformUrl = typeof v.platformUrl === "string" ? v.platformUrl : void 0;
|
|
465
|
-
|
|
472
|
+
const webappUrl = typeof v.webappUrl === "string" ? v.webappUrl : void 0;
|
|
473
|
+
return { userAuthUrl, platformUrl, webappUrl };
|
|
466
474
|
}
|
|
467
475
|
function requireString(obj, key) {
|
|
468
476
|
const value = obj[key];
|
|
@@ -487,7 +495,7 @@ function resolveConfig(sources = {}) {
|
|
|
487
495
|
const env = sources.env ?? process.env;
|
|
488
496
|
const userAuthUrl = sources.cliUserAuthUrl ?? env.MYSPEC_USER_AUTH_URL ?? sources.storedUserAuthUrl ?? "https://auth.myspec.dev";
|
|
489
497
|
const platformUrl = sources.cliPlatformUrl ?? env.MYSPEC_PLATFORM_URL ?? sources.storedPlatformUrl ?? "https://platform.myspec.dev";
|
|
490
|
-
const webappUrl = sources.cliWebappUrl ?? env.MYSPEC_WEBAPP_URL ?? deriveWebappFromAuth(userAuthUrl);
|
|
498
|
+
const webappUrl = sources.cliWebappUrl ?? env.MYSPEC_WEBAPP_URL ?? sources.storedWebappUrl ?? deriveWebappFromAuth(userAuthUrl);
|
|
491
499
|
validateUrl(userAuthUrl, "user-auth URL");
|
|
492
500
|
validateUrl(platformUrl, "platform URL");
|
|
493
501
|
validateUrl(webappUrl, "webapp URL");
|
|
@@ -651,6 +659,7 @@ var TokenManager = class {
|
|
|
651
659
|
expiresAt: 0,
|
|
652
660
|
userAuthUrl: this.envFallback.userAuthUrl,
|
|
653
661
|
platformUrl: creds.platformUrl,
|
|
662
|
+
webappUrl: creds.webappUrl,
|
|
654
663
|
user: creds.user
|
|
655
664
|
};
|
|
656
665
|
try {
|
|
@@ -1141,7 +1150,9 @@ var HttpAttachmentClient = class {
|
|
|
1141
1150
|
const baseUrl = this.httpClient.getBaseUrl().replace(/\/$/, "");
|
|
1142
1151
|
const url = `${baseUrl}/project/v1/attachments/${attachmentId}/content`;
|
|
1143
1152
|
const attempt = async () => {
|
|
1144
|
-
const blob = new Blob([content], {
|
|
1153
|
+
const blob = new Blob([content], {
|
|
1154
|
+
type: "application/octet-stream"
|
|
1155
|
+
});
|
|
1145
1156
|
const response = await fetch(url, {
|
|
1146
1157
|
method: "PUT",
|
|
1147
1158
|
headers: {
|
|
@@ -1235,7 +1246,9 @@ var HttpFileClient = class {
|
|
|
1235
1246
|
async uploadContent(fileId, content, jwtToken) {
|
|
1236
1247
|
const baseUrl = this.httpClient.getBaseUrl().replace(/\/$/, "");
|
|
1237
1248
|
const url = `${baseUrl}/project/v1/files/${fileId}/content`;
|
|
1238
|
-
const blob = new Blob([content], {
|
|
1249
|
+
const blob = new Blob([content], {
|
|
1250
|
+
type: "application/octet-stream"
|
|
1251
|
+
});
|
|
1239
1252
|
const response = await fetch(url, {
|
|
1240
1253
|
method: "PUT",
|
|
1241
1254
|
headers: {
|
|
@@ -1262,7 +1275,9 @@ var HttpFileClient = class {
|
|
|
1262
1275
|
async saveManualRevision(fileId, content, jwtToken) {
|
|
1263
1276
|
const baseUrl = this.httpClient.getBaseUrl().replace(/\/$/, "");
|
|
1264
1277
|
const url = `${baseUrl}/project/v1/files/${fileId}/revisions`;
|
|
1265
|
-
const blob = new Blob([content], {
|
|
1278
|
+
const blob = new Blob([content], {
|
|
1279
|
+
type: "application/octet-stream"
|
|
1280
|
+
});
|
|
1266
1281
|
const response = await fetch(url, {
|
|
1267
1282
|
method: "POST",
|
|
1268
1283
|
headers: {
|
|
@@ -3446,6 +3461,64 @@ function sleep2(ms) {
|
|
|
3446
3461
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3447
3462
|
}
|
|
3448
3463
|
|
|
3464
|
+
// src/reverse/discover.ts
|
|
3465
|
+
async function discoverAgentUrl(opts) {
|
|
3466
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
3467
|
+
const endpoint = `${opts.webappUrl}/api/v1/config`;
|
|
3468
|
+
let response;
|
|
3469
|
+
try {
|
|
3470
|
+
const token = await opts.getAccessToken();
|
|
3471
|
+
response = await fetchConfig(endpoint, token, fetchImpl);
|
|
3472
|
+
if (response.status === 401 && opts.forceRefresh) {
|
|
3473
|
+
const refreshed = await opts.forceRefresh();
|
|
3474
|
+
response = await fetchConfig(endpoint, refreshed, fetchImpl);
|
|
3475
|
+
}
|
|
3476
|
+
} catch (err) {
|
|
3477
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3478
|
+
throw discoveryError(endpoint, message);
|
|
3479
|
+
}
|
|
3480
|
+
if (!response.ok) {
|
|
3481
|
+
throw discoveryError(endpoint, `HTTP ${String(response.status)}`);
|
|
3482
|
+
}
|
|
3483
|
+
let body;
|
|
3484
|
+
try {
|
|
3485
|
+
body = await response.json();
|
|
3486
|
+
} catch {
|
|
3487
|
+
throw discoveryError(endpoint, "response is not valid JSON");
|
|
3488
|
+
}
|
|
3489
|
+
const agentUrl = body.aiAgentMcpReverseUrl;
|
|
3490
|
+
if (typeof agentUrl !== "string" || agentUrl.length === 0) {
|
|
3491
|
+
throw discoveryError(endpoint, "response is missing aiAgentMcpReverseUrl");
|
|
3492
|
+
}
|
|
3493
|
+
assertWebSocketUrl(agentUrl, endpoint);
|
|
3494
|
+
return agentUrl;
|
|
3495
|
+
}
|
|
3496
|
+
function fetchConfig(endpoint, token, fetchImpl) {
|
|
3497
|
+
return fetchImpl(endpoint, {
|
|
3498
|
+
method: "GET",
|
|
3499
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
function assertWebSocketUrl(value, endpoint) {
|
|
3503
|
+
let parsed;
|
|
3504
|
+
try {
|
|
3505
|
+
parsed = new URL(value);
|
|
3506
|
+
} catch {
|
|
3507
|
+
throw discoveryError(endpoint, `returned an invalid URL: ${value}`);
|
|
3508
|
+
}
|
|
3509
|
+
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
3510
|
+
throw discoveryError(
|
|
3511
|
+
endpoint,
|
|
3512
|
+
`returned a non-WebSocket URL (${value}); expected ws:// or wss://`
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
function discoveryError(endpoint, reason) {
|
|
3517
|
+
return new ConfigError(
|
|
3518
|
+
`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.`
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3449
3522
|
// src/index.ts
|
|
3450
3523
|
function parseArgs(argv) {
|
|
3451
3524
|
const [first, ...rest] = argv;
|
|
@@ -3512,22 +3585,27 @@ function printHelp() {
|
|
|
3512
3585
|
" login Sign in via browser (chooser page on the webapp)",
|
|
3513
3586
|
" login --paste Sign in by pasting a one-time code",
|
|
3514
3587
|
" logout Revoke refresh token and clear local credentials",
|
|
3515
|
-
" reverse --root <dir> Connect to ai-agent and expose local_fs tools
|
|
3588
|
+
" reverse --root <dir> Connect to ai-agent and expose local_fs tools.",
|
|
3589
|
+
" The agent WebSocket URL is discovered from the webapp",
|
|
3590
|
+
" (GET /api/v1/config) using your saved credentials;",
|
|
3591
|
+
" override with --agent-url <url> or MYSPEC_AI_AGENT_WS_URL.",
|
|
3516
3592
|
" Uses the saved refresh_token from `npx @myspec/mcp-server login`",
|
|
3517
3593
|
" to obtain & auto-refresh access tokens. Override with",
|
|
3518
3594
|
" --access-token <jwt> or MYSPEC_ACCESS_TOKEN for local testing.",
|
|
3519
3595
|
" --version Print version",
|
|
3520
3596
|
" --help Print this help",
|
|
3521
3597
|
"",
|
|
3522
|
-
"Login flags:",
|
|
3598
|
+
"Login / reverse flags:",
|
|
3523
3599
|
" --user-auth-url <url> user-auth base URL (overrides env / defaults)",
|
|
3524
3600
|
" --platform-url <url> platform API base URL",
|
|
3525
|
-
" --webapp-url <url> webapp base URL
|
|
3601
|
+
" --webapp-url <url> webapp base URL (login: hosts the /auth/cli page;",
|
|
3602
|
+
" reverse: hosts the /api/v1/config discovery endpoint)",
|
|
3526
3603
|
"",
|
|
3527
3604
|
"Environment:",
|
|
3528
3605
|
" MYSPEC_USER_AUTH_URL user-auth base URL (default https://auth.myspec.dev)",
|
|
3529
3606
|
" MYSPEC_PLATFORM_URL platform API base URL (default https://platform.myspec.dev)",
|
|
3530
3607
|
" MYSPEC_WEBAPP_URL webapp base URL (default derived from user-auth host)",
|
|
3608
|
+
" MYSPEC_AI_AGENT_WS_URL ai-agent WebSocket URL for `reverse` (skips discovery)",
|
|
3531
3609
|
" MYSPEC_REFRESH_TOKEN Skip file-based credentials; the server mints a",
|
|
3532
3610
|
" fresh access token on first use via this refresh",
|
|
3533
3611
|
" token."
|
|
@@ -3578,16 +3656,35 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3578
3656
|
return;
|
|
3579
3657
|
}
|
|
3580
3658
|
}
|
|
3659
|
+
async function resolveReverseAgentUrl(sources) {
|
|
3660
|
+
const env = sources.env ?? process.env;
|
|
3661
|
+
const explicitAgentUrl = sources.flagAgentUrl ?? env.MYSPEC_AI_AGENT_WS_URL;
|
|
3662
|
+
if (explicitAgentUrl) {
|
|
3663
|
+
return { agentUrl: explicitAgentUrl };
|
|
3664
|
+
}
|
|
3665
|
+
const stored = await sources.loadStored().catch(() => null);
|
|
3666
|
+
const config = resolveConfig({
|
|
3667
|
+
env,
|
|
3668
|
+
cliWebappUrl: sources.cliWebappUrl,
|
|
3669
|
+
storedUserAuthUrl: stored?.userAuthUrl,
|
|
3670
|
+
storedWebappUrl: stored?.webappUrl
|
|
3671
|
+
});
|
|
3672
|
+
const agentUrl = await sources.discover(config.webappUrl);
|
|
3673
|
+
return { agentUrl, discoveredVia: `${config.webappUrl}/api/v1/config` };
|
|
3674
|
+
}
|
|
3581
3675
|
async function runReverseCommand(flags) {
|
|
3582
3676
|
const root = flagString(flags, "root") ?? process.cwd();
|
|
3583
|
-
const agentUrl = flagString(flags, "agent-url") ?? process.env.MYSPEC_AI_AGENT_WS_URL ?? "ws://localhost:3001/mcp/reverse";
|
|
3584
3677
|
const inlineToken = flagString(flags, "access-token") ?? process.env.MYSPEC_ACCESS_TOKEN;
|
|
3585
3678
|
let getAccessToken;
|
|
3586
3679
|
let onAuthFailed;
|
|
3680
|
+
let forceRefresh;
|
|
3681
|
+
let loadStored;
|
|
3587
3682
|
if (inlineToken) {
|
|
3588
3683
|
const staticToken = inlineToken;
|
|
3589
3684
|
getAccessToken = () => Promise.resolve(staticToken);
|
|
3590
3685
|
onAuthFailed = void 0;
|
|
3686
|
+
forceRefresh = void 0;
|
|
3687
|
+
loadStored = () => Promise.resolve(null);
|
|
3591
3688
|
} else {
|
|
3592
3689
|
const envConfig = readEnvRefreshConfig();
|
|
3593
3690
|
const store = createCompositeCredentialsStore({
|
|
@@ -3599,10 +3696,26 @@ async function runReverseCommand(flags) {
|
|
|
3599
3696
|
onAuthFailed = async () => {
|
|
3600
3697
|
await tokenManager.forceRefresh();
|
|
3601
3698
|
};
|
|
3699
|
+
forceRefresh = () => tokenManager.forceRefresh();
|
|
3700
|
+
loadStored = () => store.load();
|
|
3701
|
+
}
|
|
3702
|
+
const resolved = await resolveReverseAgentUrl({
|
|
3703
|
+
flagAgentUrl: flagString(flags, "agent-url"),
|
|
3704
|
+
cliWebappUrl: flagString(flags, "webapp-url"),
|
|
3705
|
+
loadStored,
|
|
3706
|
+
discover: (webappUrl) => {
|
|
3707
|
+
return discoverAgentUrl({ webappUrl, getAccessToken, forceRefresh });
|
|
3708
|
+
}
|
|
3709
|
+
});
|
|
3710
|
+
if (resolved.discoveredVia) {
|
|
3711
|
+
process.stderr.write(
|
|
3712
|
+
`myspec-mcp reverse: discovered agent URL via ${resolved.discoveredVia}
|
|
3713
|
+
`
|
|
3714
|
+
);
|
|
3602
3715
|
}
|
|
3603
3716
|
await runReverse({
|
|
3604
3717
|
root,
|
|
3605
|
-
agentUrl,
|
|
3718
|
+
agentUrl: resolved.agentUrl,
|
|
3606
3719
|
getAccessToken,
|
|
3607
3720
|
onAuthFailed,
|
|
3608
3721
|
version: readVersion()
|
|
@@ -3624,7 +3737,8 @@ async function runServe(flags) {
|
|
|
3624
3737
|
cliUserAuthUrl: flagString(flags, "user-auth-url"),
|
|
3625
3738
|
cliPlatformUrl: flagString(flags, "platform-url"),
|
|
3626
3739
|
storedUserAuthUrl: initial?.userAuthUrl,
|
|
3627
|
-
storedPlatformUrl: initial?.platformUrl
|
|
3740
|
+
storedPlatformUrl: initial?.platformUrl,
|
|
3741
|
+
storedWebappUrl: initial?.webappUrl
|
|
3628
3742
|
});
|
|
3629
3743
|
const tokenManager = new TokenManager({
|
|
3630
3744
|
store,
|
|
@@ -3662,5 +3776,6 @@ if (isCliEntry()) {
|
|
|
3662
3776
|
}
|
|
3663
3777
|
export {
|
|
3664
3778
|
main,
|
|
3779
|
+
resolveReverseAgentUrl,
|
|
3665
3780
|
runServe
|
|
3666
3781
|
};
|
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.6",
|
|
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": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@eslint/js": "^10.0.1",
|
|
51
51
|
"@myspec/platform-client": "file:../../packages/platform-client",
|
|
52
52
|
"@myspec/shared": "file:../../packages/shared",
|
|
53
|
-
"@types/node": "^
|
|
53
|
+
"@types/node": "^25.9.1",
|
|
54
54
|
"@types/ws": "^8.18.1",
|
|
55
55
|
"eslint": "^10.4.1",
|
|
56
56
|
"tsup": "^8.3.5",
|