@myspec/mcp-server 0.1.0-next.3 → 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 +6 -1
- package/dist/index.js +108 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,12 @@ npx @myspec/mcp-server@next reverse --root /path/to/project
|
|
|
42
42
|
| `--agent-url <url>` / `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL (default `ws://localhost:3001/mcp/reverse`). |
|
|
43
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
44
|
|
|
45
|
-
|
|
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.
|
|
46
51
|
|
|
47
52
|
## Environment variables
|
|
48
53
|
|
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];
|
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": {
|