@myspec/mcp-server 0.1.0-next.2 → 0.1.0-next.4
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 -2
- package/dist/index.js +110 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,16 +23,38 @@ 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 (experimental)
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
### `reverse` (experimental)
|
|
30
|
+
|
|
31
|
+
`reverse` connects out to the ai-agent service over WebSocket and exposes a set
|
|
32
|
+
of `local_fs` tools scoped to a single local directory, so a cloud agent can
|
|
33
|
+
read files from your machine. This is an early spike.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @myspec/mcp-server@next reverse --root /path/to/project
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Flag / env | Purpose |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `--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 (default `ws://localhost:3001/mcp/reverse`). |
|
|
43
|
+
| `--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
|
+
|
|
45
|
+
Local state is stored under `~/.myspec/` (alongside the on-disk cache root), split into two files:
|
|
46
|
+
|
|
47
|
+
- `settings.json` — non-secret config: the auth-server and platform URLs you logged in with.
|
|
48
|
+
- `oauth_creds.json` — the OAuth tokens (access/refresh/expiry) and your user identity, written mode `0600`.
|
|
49
|
+
|
|
50
|
+
`logout` removes `oauth_creds.json`; `settings.json` is left in place.
|
|
29
51
|
|
|
30
52
|
## Environment variables
|
|
31
53
|
|
|
32
54
|
| Variable | Purpose |
|
|
33
55
|
|---|---|
|
|
34
56
|
| `MYSPEC_USER_AUTH_URL` | user-auth base URL (default `https://auth.myspec.dev`) |
|
|
35
|
-
| `MYSPEC_PLATFORM_URL` | platform API base URL (default `https://
|
|
57
|
+
| `MYSPEC_PLATFORM_URL` | platform API base URL (default `https://platform.myspec.dev`) |
|
|
36
58
|
| `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`. |
|
|
37
59
|
| `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
|
|
38
60
|
| `MYSPEC_WEBAPP_URL` | Webapp base URL used by `login --paste` to display the OAuth callback URL (default: derived from `MYSPEC_USER_AUTH_URL`) |
|
package/dist/index.js
CHANGED
|
@@ -282,57 +282,112 @@ async function defaultPrompt(question) {
|
|
|
282
282
|
import { promises as fs } from "fs";
|
|
283
283
|
import os from "os";
|
|
284
284
|
import path from "path";
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const xdg = env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
291
|
-
return path.join(xdg, "myspec", "mcp-server.json");
|
|
285
|
+
function myspecDir() {
|
|
286
|
+
return path.join(os.homedir(), ".myspec");
|
|
287
|
+
}
|
|
288
|
+
function defaultSettingsPath() {
|
|
289
|
+
return path.join(myspecDir(), "settings.json");
|
|
292
290
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
291
|
+
function defaultOauthCredsPath() {
|
|
292
|
+
return path.join(myspecDir(), "oauth_creds.json");
|
|
293
|
+
}
|
|
294
|
+
function createFileCredentialsStore(paths = {}) {
|
|
295
|
+
const settingsPath = paths.settingsPath ?? defaultSettingsPath();
|
|
296
|
+
const oauthCredsPath = paths.oauthCredsPath ?? defaultOauthCredsPath();
|
|
295
297
|
return {
|
|
296
|
-
|
|
298
|
+
// The secret bundle is the credential file users care about (`login`
|
|
299
|
+
// prints this); settings.json sits next to it.
|
|
300
|
+
path: () => oauthCredsPath,
|
|
297
301
|
async load() {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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;
|
|
302
|
+
const oauth = await loadOauthCreds(oauthCredsPath);
|
|
303
|
+
if (!oauth) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const settings = await loadSettings(settingsPath);
|
|
307
|
+
if (!settings) {
|
|
308
|
+
return null;
|
|
315
309
|
}
|
|
310
|
+
return {
|
|
311
|
+
accessToken: oauth.accessToken,
|
|
312
|
+
refreshToken: oauth.refreshToken,
|
|
313
|
+
expiresAt: oauth.expiresAt,
|
|
314
|
+
userAuthUrl: settings.userAuthUrl,
|
|
315
|
+
platformUrl: settings.platformUrl,
|
|
316
|
+
user: oauth.user
|
|
317
|
+
};
|
|
316
318
|
},
|
|
317
319
|
async save(creds) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
await writeJsonFile(
|
|
321
|
+
settingsPath,
|
|
322
|
+
{ userAuthUrl: creds.userAuthUrl, platformUrl: creds.platformUrl },
|
|
323
|
+
{ secret: false }
|
|
324
|
+
);
|
|
325
|
+
await writeJsonFile(
|
|
326
|
+
oauthCredsPath,
|
|
327
|
+
{
|
|
328
|
+
accessToken: creds.accessToken,
|
|
329
|
+
refreshToken: creds.refreshToken,
|
|
330
|
+
expiresAt: creds.expiresAt,
|
|
331
|
+
user: creds.user
|
|
332
|
+
},
|
|
333
|
+
{ secret: true }
|
|
334
|
+
);
|
|
324
335
|
},
|
|
325
336
|
async clear() {
|
|
326
|
-
|
|
327
|
-
await fs.unlink(target);
|
|
328
|
-
} catch (err) {
|
|
329
|
-
if (!isFsNotFound(err)) {
|
|
330
|
-
throw err;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
337
|
+
await unlinkIfExists(oauthCredsPath);
|
|
333
338
|
}
|
|
334
339
|
};
|
|
335
340
|
}
|
|
341
|
+
async function loadOauthCreds(target) {
|
|
342
|
+
try {
|
|
343
|
+
if (process.platform !== "win32") {
|
|
344
|
+
const stat = await fs.stat(target);
|
|
345
|
+
if ((stat.mode & 63) !== 0) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Refusing to load credentials from ${target}: file mode is too permissive (${(stat.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const raw = await fs.readFile(target, "utf-8");
|
|
352
|
+
return parseOauthCreds(JSON.parse(raw));
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (isFsNotFound(err)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function loadSettings(target) {
|
|
361
|
+
try {
|
|
362
|
+
const raw = await fs.readFile(target, "utf-8");
|
|
363
|
+
return parseSettings(JSON.parse(raw));
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (isFsNotFound(err)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function writeJsonFile(target, data, opts) {
|
|
372
|
+
const dir = path.dirname(target);
|
|
373
|
+
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
374
|
+
if (process.platform !== "win32") {
|
|
375
|
+
await fs.chmod(dir, 448);
|
|
376
|
+
}
|
|
377
|
+
await fs.writeFile(target, JSON.stringify(data, null, 2), { mode: 384 });
|
|
378
|
+
if (opts.secret && process.platform !== "win32") {
|
|
379
|
+
await fs.chmod(target, 384);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function unlinkIfExists(target) {
|
|
383
|
+
try {
|
|
384
|
+
await fs.unlink(target);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (!isFsNotFound(err)) {
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
336
391
|
var DEFAULT_USER_AUTH_URL = "https://auth.myspec.dev";
|
|
337
392
|
function readEnvRefreshConfig(env = process.env) {
|
|
338
393
|
if (env.MYSPEC_ACCESS_TOKEN || env.MYSPEC_ACCESS_TOKEN_EXPIRES_AT) {
|
|
@@ -380,19 +435,17 @@ function createCompositeCredentialsStore(opts) {
|
|
|
380
435
|
envUserAuthUrl: () => envUserAuthUrl
|
|
381
436
|
};
|
|
382
437
|
}
|
|
383
|
-
function
|
|
438
|
+
function parseOauthCreds(value) {
|
|
384
439
|
if (typeof value !== "object" || value === null) {
|
|
385
|
-
throw new Error("
|
|
440
|
+
throw new Error("oauth_creds.json is malformed (not an object)");
|
|
386
441
|
}
|
|
387
442
|
const v = value;
|
|
388
443
|
const accessToken = requireString(v, "accessToken");
|
|
389
444
|
const refreshToken = requireString(v, "refreshToken");
|
|
390
445
|
const expiresAt = requireNumber(v, "expiresAt");
|
|
391
|
-
const userAuthUrl = requireString(v, "userAuthUrl");
|
|
392
|
-
const platformUrl = typeof v.platformUrl === "string" ? v.platformUrl : void 0;
|
|
393
446
|
const userRaw = v.user;
|
|
394
447
|
if (typeof userRaw !== "object" || userRaw === null) {
|
|
395
|
-
throw new Error("
|
|
448
|
+
throw new Error("oauth_creds.json is malformed (missing user)");
|
|
396
449
|
}
|
|
397
450
|
const userObj = userRaw;
|
|
398
451
|
const user = {
|
|
@@ -400,7 +453,16 @@ function parseCredentials(value) {
|
|
|
400
453
|
email: requireString(userObj, "email"),
|
|
401
454
|
name: typeof userObj.name === "string" ? userObj.name : void 0
|
|
402
455
|
};
|
|
403
|
-
return { accessToken, refreshToken, expiresAt,
|
|
456
|
+
return { accessToken, refreshToken, expiresAt, user };
|
|
457
|
+
}
|
|
458
|
+
function parseSettings(value) {
|
|
459
|
+
if (typeof value !== "object" || value === null) {
|
|
460
|
+
throw new Error("settings.json is malformed (not an object)");
|
|
461
|
+
}
|
|
462
|
+
const v = value;
|
|
463
|
+
const userAuthUrl = requireString(v, "userAuthUrl");
|
|
464
|
+
const platformUrl = typeof v.platformUrl === "string" ? v.platformUrl : void 0;
|
|
465
|
+
return { userAuthUrl, platformUrl };
|
|
404
466
|
}
|
|
405
467
|
function requireString(obj, key) {
|
|
406
468
|
const value = obj[key];
|
|
@@ -424,7 +486,7 @@ function isFsNotFound(err) {
|
|
|
424
486
|
function resolveConfig(sources = {}) {
|
|
425
487
|
const env = sources.env ?? process.env;
|
|
426
488
|
const userAuthUrl = sources.cliUserAuthUrl ?? env.MYSPEC_USER_AUTH_URL ?? sources.storedUserAuthUrl ?? "https://auth.myspec.dev";
|
|
427
|
-
const platformUrl = sources.cliPlatformUrl ?? env.MYSPEC_PLATFORM_URL ?? sources.storedPlatformUrl ?? "https://
|
|
489
|
+
const platformUrl = sources.cliPlatformUrl ?? env.MYSPEC_PLATFORM_URL ?? sources.storedPlatformUrl ?? "https://platform.myspec.dev";
|
|
428
490
|
const webappUrl = sources.cliWebappUrl ?? env.MYSPEC_WEBAPP_URL ?? deriveWebappFromAuth(userAuthUrl);
|
|
429
491
|
validateUrl(userAuthUrl, "user-auth URL");
|
|
430
492
|
validateUrl(platformUrl, "platform URL");
|
|
@@ -3464,7 +3526,7 @@ function printHelp() {
|
|
|
3464
3526
|
"",
|
|
3465
3527
|
"Environment:",
|
|
3466
3528
|
" MYSPEC_USER_AUTH_URL user-auth base URL (default https://auth.myspec.dev)",
|
|
3467
|
-
" MYSPEC_PLATFORM_URL platform API base URL (default https://
|
|
3529
|
+
" MYSPEC_PLATFORM_URL platform API base URL (default https://platform.myspec.dev)",
|
|
3468
3530
|
" MYSPEC_WEBAPP_URL webapp base URL (default derived from user-auth host)",
|
|
3469
3531
|
" MYSPEC_REFRESH_TOKEN Skip file-based credentials; the server mints a",
|
|
3470
3532
|
" fresh access token on first use via this refresh",
|
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.4",
|
|
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": {
|