@maka/maka-cli 5.142.0 → 5.143.1
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/bundle/typescript/package.json +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.js +173 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/service-account-auth.js +68 -0
- package/bundle/typescript/src/commands/game/sideQuest-backlog-mcp.sub.cmd.js +175 -0
- package/bundle/typescript/src/commands/game/sideQuest-backlog.sub.cmd.js +5 -1
- package/package.json +4 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.143.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@inquirer/prompts": "^8.5.2",
|
|
50
50
|
"@maka/meteor-sdk": "^0.2.13",
|
|
51
51
|
"@maka/tar": "^6.2.2",
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
52
53
|
"blessed": "^0.1.81",
|
|
53
54
|
"chalk": "^4.1.2",
|
|
54
55
|
"change-case": "^5.4.4",
|
|
@@ -70,7 +71,8 @@
|
|
|
70
71
|
"tar": "^7.4.3",
|
|
71
72
|
"uuid": "^14.0.1",
|
|
72
73
|
"ws": "^8.21.3",
|
|
73
|
-
"xtend": "^4.0.2"
|
|
74
|
+
"xtend": "^4.0.2",
|
|
75
|
+
"zod": "^4.5.4"
|
|
74
76
|
},
|
|
75
77
|
"devDependencies": {
|
|
76
78
|
"@aws-sdk/types": "^3.974.4",
|
|
@@ -115,8 +115,15 @@ export async function fetchReviewQueue() {
|
|
|
115
115
|
}
|
|
116
116
|
/** Answer, ask, or annotate one item. `kind` defaults to a plain note;
|
|
117
117
|
* a play-tester replying to a question posts an 'answer'. */
|
|
118
|
-
export async function postBacklogComment(itemId, text, opts = {}
|
|
119
|
-
|
|
118
|
+
export async function postBacklogComment(itemId, text, opts = {},
|
|
119
|
+
// ADMIN OVERRIDE (2026-09-08). The MCP admin server (mcp.sub.cmd.ts)
|
|
120
|
+
// reuses this for notes/questions posted as the service account,
|
|
121
|
+
// which holds no entry in ~/.maka/global-config.json for authToken()
|
|
122
|
+
// to find. Optional and trailing so every existing human-CLI call
|
|
123
|
+
// site is untouched -- omitted, this is exactly the function it always
|
|
124
|
+
// was.
|
|
125
|
+
tokenOverride) {
|
|
126
|
+
const token = tokenOverride ?? authToken();
|
|
120
127
|
if (!token)
|
|
121
128
|
return 'no-login';
|
|
122
129
|
try {
|
|
@@ -375,8 +382,12 @@ export async function setRole(action, email, role) {
|
|
|
375
382
|
* in-review first, then open. `all` asks for everything anyway; a
|
|
376
383
|
* specific `status` is always honoured exactly.
|
|
377
384
|
*/
|
|
378
|
-
export async function fetchBacklog(status, opts = {}
|
|
379
|
-
|
|
385
|
+
export async function fetchBacklog(status, opts = {},
|
|
386
|
+
// ADMIN OVERRIDE, same reasoning as postBacklogComment's: the MCP
|
|
387
|
+
// admin server has no human login for authToken() to find. Optional
|
|
388
|
+
// and trailing so every existing call site is untouched.
|
|
389
|
+
tokenOverride) {
|
|
390
|
+
const token = tokenOverride ?? authToken();
|
|
380
391
|
if (!token)
|
|
381
392
|
return { outcome: 'no-login' };
|
|
382
393
|
try {
|
|
@@ -395,4 +406,162 @@ export async function fetchBacklog(status, opts = {}) {
|
|
|
395
406
|
return { outcome: 'offline' };
|
|
396
407
|
}
|
|
397
408
|
}
|
|
409
|
+
// ---------------------------------------------------------------------
|
|
410
|
+
// ADMIN CALLS (2026-09-08), added for the MCP admin server
|
|
411
|
+
// (sideQuest-backlog-mcp.sub.cmd.ts). Every function below takes its
|
|
412
|
+
// token EXPLICITLY rather than falling back to authToken() -- these are
|
|
413
|
+
// only ever called with the M2M service account's session, never a
|
|
414
|
+
// human's, and a silent fallback to whichever human happens to be
|
|
415
|
+
// logged in on this machine would be exactly the wrong default for an
|
|
416
|
+
// admin action. All of them are the same house shape as everything
|
|
417
|
+
// above: swallow errors, return an outcome, let the caller turn it into
|
|
418
|
+
// prose (or, here, into an MCP tool error).
|
|
419
|
+
// ---------------------------------------------------------------------
|
|
420
|
+
/** One item, in full -- including fields /list withholds from a caller
|
|
421
|
+
* who is not its reporter or an admin. `GET item/:id` is gated the same
|
|
422
|
+
* as /list (mayUseBacklog), so this never needs a special admin path
|
|
423
|
+
* server-side; the token being an admin's is what gets it the fuller
|
|
424
|
+
* read. */
|
|
425
|
+
export async function fetchBacklogItemById(itemId, token) {
|
|
426
|
+
try {
|
|
427
|
+
const res = await request('GET', `/${API_BASE}/item/${encodeURIComponent(itemId)}`, token);
|
|
428
|
+
if (res.statusCode === 401)
|
|
429
|
+
return { outcome: 'expired' };
|
|
430
|
+
if (res.statusCode === 403)
|
|
431
|
+
return { outcome: 'forbidden' };
|
|
432
|
+
if (res.statusCode === 404)
|
|
433
|
+
return { outcome: 'rejected' };
|
|
434
|
+
if (res.statusCode !== 200)
|
|
435
|
+
return { outcome: 'offline' };
|
|
436
|
+
const parsed = parseData(res.data);
|
|
437
|
+
return { outcome: 'ok', item: parsed?.item };
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return { outcome: 'offline' };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** THE ACTUAL RESOLUTION MOVE: attach evidence and/or advance status.
|
|
444
|
+
* Admin-only server-side (`PATCH item/:id`), and gated there exactly as
|
|
445
|
+
* documented in game-backlog-v1-rest-api.ts -- entering `in-review` or
|
|
446
|
+
* `complete` without commits is refused with a `message` this function
|
|
447
|
+
* passes straight through rather than flattening into a generic
|
|
448
|
+
* "rejected", because that message is the instruction for what to do
|
|
449
|
+
* differently (the MCP tool surfaces it verbatim to whoever is holding
|
|
450
|
+
* the AI's hand at that point). */
|
|
451
|
+
export async function patchBacklogItem(itemId, body, token) {
|
|
452
|
+
try {
|
|
453
|
+
const res = await request('PATCH', `/${API_BASE}/item/${encodeURIComponent(itemId)}`, token, body);
|
|
454
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
455
|
+
const parsed = parseData(res.data);
|
|
456
|
+
return { outcome: 'ok', status: parsed?.status, commits: parsed?.commits };
|
|
457
|
+
}
|
|
458
|
+
if (res.statusCode === 401)
|
|
459
|
+
return { outcome: 'expired' };
|
|
460
|
+
if (res.statusCode === 403)
|
|
461
|
+
return { outcome: 'forbidden' };
|
|
462
|
+
if (res.statusCode === 400 || res.statusCode === 404) {
|
|
463
|
+
const parsed = parseData(res.data);
|
|
464
|
+
return { outcome: 'rejected', message: parsed?.message };
|
|
465
|
+
}
|
|
466
|
+
return { outcome: 'offline' };
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return { outcome: 'offline' };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/** ASK THE SERVER TO DRAFT A BENCH for one item. Lands in `reproDraft`,
|
|
473
|
+
* never `repro` -- promoting a draft to a real bench is a separate,
|
|
474
|
+
* deliberate PATCH (patchBacklogItem with a `repro` body), same as the
|
|
475
|
+
* web board and the REST route's own doc comment describe. Admin-only,
|
|
476
|
+
* costs a model call server-side. */
|
|
477
|
+
export async function draftBacklogBench(itemId, token, verbs) {
|
|
478
|
+
try {
|
|
479
|
+
const res = await request('POST', `/${API_BASE}/item/${encodeURIComponent(itemId)}/bench-draft`, token, verbs ? { verbs } : undefined);
|
|
480
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
481
|
+
const parsed = parseData(res.data);
|
|
482
|
+
return { outcome: 'ok', reproDraft: parsed?.reproDraft };
|
|
483
|
+
}
|
|
484
|
+
if (res.statusCode === 401)
|
|
485
|
+
return { outcome: 'expired' };
|
|
486
|
+
if (res.statusCode === 403)
|
|
487
|
+
return { outcome: 'forbidden' };
|
|
488
|
+
if (res.statusCode === 400 || res.statusCode === 404) {
|
|
489
|
+
const parsed = parseData(res.data);
|
|
490
|
+
return { outcome: 'rejected', message: parsed?.message, code: parsed?.code };
|
|
491
|
+
}
|
|
492
|
+
return { outcome: 'offline' };
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return { outcome: 'offline' };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** THE BATCH FORM -- named ids only, never a selector (see the route's
|
|
499
|
+
* own doc comment on why: a sweep over a spend path is exactly the
|
|
500
|
+
* failure mode the per-item rule exists to prevent). Capped at 20 by
|
|
501
|
+
* the server; this does not re-enforce that limit, so a caller sees the
|
|
502
|
+
* server's own refusal if it is exceeded. */
|
|
503
|
+
export async function draftBacklogBenches(ids, token) {
|
|
504
|
+
try {
|
|
505
|
+
const res = await request('POST', `/${API_BASE}/bench-drafts`, token, { ids });
|
|
506
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
507
|
+
const parsed = parseData(res.data);
|
|
508
|
+
return { outcome: 'ok', drafted: parsed?.drafted, attempted: parsed?.attempted, results: parsed?.results };
|
|
509
|
+
}
|
|
510
|
+
if (res.statusCode === 401)
|
|
511
|
+
return { outcome: 'expired' };
|
|
512
|
+
if (res.statusCode === 403)
|
|
513
|
+
return { outcome: 'forbidden' };
|
|
514
|
+
if (res.statusCode === 400) {
|
|
515
|
+
const parsed = parseData(res.data);
|
|
516
|
+
return { outcome: 'rejected', message: parsed?.message };
|
|
517
|
+
}
|
|
518
|
+
return { outcome: 'offline' };
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return { outcome: 'offline' };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** RE-JUDGE ITEMS AGAINST THE RULEBOOKS. `all: true` re-grades the whole
|
|
525
|
+
* board (a model call per item -- see the route's own doc comment on
|
|
526
|
+
* why that is opt-in); the default only catches items whose first pass
|
|
527
|
+
* ran without session context and now has one. */
|
|
528
|
+
export async function revetBacklog(token, opts = {}) {
|
|
529
|
+
try {
|
|
530
|
+
const res = await request('POST', `/${API_BASE}/revet`, token, opts);
|
|
531
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
532
|
+
const parsed = parseData(res.data);
|
|
533
|
+
return { outcome: 'ok', ...parsed };
|
|
534
|
+
}
|
|
535
|
+
if (res.statusCode === 401)
|
|
536
|
+
return { outcome: 'expired' };
|
|
537
|
+
if (res.statusCode === 403)
|
|
538
|
+
return { outcome: 'forbidden' };
|
|
539
|
+
return { outcome: 'offline' };
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return { outcome: 'offline' };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/** THE M2M TOKEN EXCHANGE. Not gated behind authToken() at all -- there
|
|
546
|
+
* is no Meteor session yet at the point this is called, that is
|
|
547
|
+
* precisely what it establishes. Hits `/api/v1/auth`, not
|
|
548
|
+
* `/api/v1/game-backlog`; `request()` takes a raw path, so this is the
|
|
549
|
+
* one caller in the file that does not go through API_BASE. See
|
|
550
|
+
* utilities/service-account-auth.ts for what obtains the Auth0 token
|
|
551
|
+
* this expects and what caches the result. */
|
|
552
|
+
export async function mintServiceAccountToken(auth0Token) {
|
|
553
|
+
try {
|
|
554
|
+
const res = await request('POST', '/api/v1/auth/m2m-login', '', { auth0Token });
|
|
555
|
+
if (res.statusCode === 200) {
|
|
556
|
+
const parsed = parseData(res.data);
|
|
557
|
+
return { outcome: 'ok', authToken: parsed?.authToken, userId: parsed?.userId, when: parsed?.when };
|
|
558
|
+
}
|
|
559
|
+
if (res.statusCode === 401)
|
|
560
|
+
return { outcome: 'expired' };
|
|
561
|
+
return { outcome: 'offline' };
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return { outcome: 'offline' };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
398
567
|
//# sourceMappingURL=backlog.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import { mintServiceAccountToken } from './backlog.js';
|
|
3
|
+
/** Client-credentials grant, direct to Auth0. Throws on any failure --
|
|
4
|
+
* there is no partial-success shape worth returning here, and the
|
|
5
|
+
* caller (ensureServiceSession below) is the one place that decides
|
|
6
|
+
* what a failure means for the MCP server's own state. */
|
|
7
|
+
async function getAuth0AccessToken(creds) {
|
|
8
|
+
const body = new URLSearchParams({
|
|
9
|
+
grant_type: 'client_credentials',
|
|
10
|
+
client_id: creds.clientId,
|
|
11
|
+
client_secret: creds.clientSecret,
|
|
12
|
+
audience: creds.audience,
|
|
13
|
+
}).toString();
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const req = https.request({
|
|
16
|
+
hostname: creds.domain,
|
|
17
|
+
port: 443,
|
|
18
|
+
path: '/oauth/token',
|
|
19
|
+
method: 'POST',
|
|
20
|
+
timeout: 10000,
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
23
|
+
'Content-Length': Buffer.byteLength(body),
|
|
24
|
+
},
|
|
25
|
+
}, (res) => {
|
|
26
|
+
let data = '';
|
|
27
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
28
|
+
res.on('end', () => {
|
|
29
|
+
if (res.statusCode !== 200) {
|
|
30
|
+
reject(new Error(`Auth0 token request failed (${res.statusCode}): ${data.slice(0, 300)}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(data);
|
|
35
|
+
if (!parsed.access_token)
|
|
36
|
+
throw new Error('no access_token in Auth0 response');
|
|
37
|
+
resolve(parsed.access_token);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
req.on('timeout', () => req.destroy(new Error('Auth0 token request timeout')));
|
|
45
|
+
req.on('error', reject);
|
|
46
|
+
req.write(body);
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
let cachedToken;
|
|
51
|
+
/** THE ONE ENTRY POINT mcp.sub.cmd.ts calls. Returns a Meteor
|
|
52
|
+
* x-auth-token for the service account, minting one on first use and
|
|
53
|
+
* reusing it after that. `force: true` is what a 401 from any admin
|
|
54
|
+
* call should trigger -- the cached token may have been revoked
|
|
55
|
+
* (logoutAll, expiry) and one retry with a freshly minted token is
|
|
56
|
+
* cheap insurance against surfacing a spurious failure. */
|
|
57
|
+
export async function ensureServiceSession(creds, opts = {}) {
|
|
58
|
+
if (cachedToken && !opts.force)
|
|
59
|
+
return cachedToken;
|
|
60
|
+
const auth0Token = await getAuth0AccessToken(creds);
|
|
61
|
+
const minted = await mintServiceAccountToken(auth0Token);
|
|
62
|
+
if (minted.outcome !== 'ok' || !minted.authToken) {
|
|
63
|
+
throw new Error(`Could not establish a service-account session (${minted.outcome}).`);
|
|
64
|
+
}
|
|
65
|
+
cachedToken = minted.authToken;
|
|
66
|
+
return cachedToken;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=service-account-auth.js.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Command } from '../../command.js';
|
|
4
|
+
import { UsageError } from '../../error.js';
|
|
5
|
+
export function registerBacklogMcp(parent) {
|
|
6
|
+
return Command.create({
|
|
7
|
+
name: 'mcp',
|
|
8
|
+
usage: 'maka play:backlog:mcp [--env <env>] [--site <path>]',
|
|
9
|
+
// OFF, DELIBERATELY -- see the file header. The framework's own
|
|
10
|
+
// check fires before this handler runs at all and only knows how
|
|
11
|
+
// to look at process.cwd(); --site is this command's own way to
|
|
12
|
+
// satisfy the same requirement, so it needs the chance to try that
|
|
13
|
+
// first rather than being refused upstream of ever seeing it.
|
|
14
|
+
mustBeInMakaProject: false,
|
|
15
|
+
// NO maxArgLength HERE, DELIBERATELY. Command's own arg-length check
|
|
16
|
+
// is `args.length >= maxArgLength` (command.ts), so `maxArgLength: 0`
|
|
17
|
+
// refuses EVERY invocation regardless of what was typed -- an
|
|
18
|
+
// off-by-one in the shared framework, not something to work around
|
|
19
|
+
// per-command. No command in this codebase sets it to 0 for exactly
|
|
20
|
+
// that reason. This command takes no positionals; any stray one is
|
|
21
|
+
// simply ignored rather than rejected.
|
|
22
|
+
validOpts: [
|
|
23
|
+
{
|
|
24
|
+
name: 'env',
|
|
25
|
+
description: 'Which maka-cli.com config/<env>/process.env supplies the Auth0 M2M credentials (default: production).',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'site',
|
|
29
|
+
description: 'Path to a maka-cli.com checkout. Defaults to resolving one from the current working directory if omitted -- pass this explicitly when the launching MCP client does not set cwd reliably.',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
shortDesc: 'Run an MCP server exposing game-backlog admin tools, authenticated as the M2M service account.',
|
|
33
|
+
description: `Starts a local MCP server (stdio transport) with tools for listing and reading
|
|
34
|
+
backlog items, attaching evidence and advancing status, drafting an AI bench, and posting
|
|
35
|
+
admin notes -- all against maka-cli.com's existing game-backlog REST API, authenticated as
|
|
36
|
+
the claude-agent@maka-cli.com service account via the Auth0 M2M token exchange
|
|
37
|
+
(api/v1/auth/m2m-login). Needs a maka-cli.com checkout to read AUTHZERO_DOMAIN /
|
|
38
|
+
AUTHZERO_CLIENT_ID / AUTHZERO_SECRET / AUTHZERO_AUDIENCE from config/<env>/process.env --
|
|
39
|
+
pass --site explicitly, or run with cwd already set to one.`,
|
|
40
|
+
examples: [
|
|
41
|
+
'maka play:backlog:mcp --site C:\\path\\to\\maka-cli.com',
|
|
42
|
+
'maka play:backlog:mcp --env local --site C:\\path\\to\\maka-cli.com',
|
|
43
|
+
],
|
|
44
|
+
}, async function (_args, opts) {
|
|
45
|
+
const env = opts.env ?? 'production';
|
|
46
|
+
let processEnvPath;
|
|
47
|
+
if (opts.site) {
|
|
48
|
+
const siteConfigDir = path.join(path.resolve(opts.site), 'config', env);
|
|
49
|
+
processEnvPath = path.join(siteConfigDir, 'process.env');
|
|
50
|
+
if (!fs.existsSync(processEnvPath)) {
|
|
51
|
+
throw new UsageError(`No config/${env}/process.env under --site ${opts.site} (looked for ${processEnvPath}).`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (!this.cfg.checkConfigExists(env)) {
|
|
56
|
+
// Pre-handshake failure -- nothing has touched stdout as MCP
|
|
57
|
+
// protocol yet, so a plain thrown Error (which Command's own
|
|
58
|
+
// wrapper reports via Log.error) is fine here.
|
|
59
|
+
throw new UsageError(`Not in a maka-cli.com checkout and no --site given (looked for config/${env} under ${process.cwd()}).`);
|
|
60
|
+
}
|
|
61
|
+
const configPath = this.cfg.getAppConfigPath(env);
|
|
62
|
+
processEnvPath = path.join(configPath, 'process.env');
|
|
63
|
+
}
|
|
64
|
+
const [domain, clientId, clientSecret, audience] = await Promise.all([
|
|
65
|
+
this.dotenvx.get('AUTHZERO_DOMAIN', processEnvPath),
|
|
66
|
+
this.dotenvx.get('AUTHZERO_CLIENT_ID', processEnvPath),
|
|
67
|
+
this.dotenvx.get('AUTHZERO_SECRET', processEnvPath),
|
|
68
|
+
this.dotenvx.get('AUTHZERO_AUDIENCE', processEnvPath),
|
|
69
|
+
]);
|
|
70
|
+
if (!domain || !clientId || !clientSecret || !audience) {
|
|
71
|
+
throw new Error(`AUTHZERO_DOMAIN/CLIENT_ID/SECRET/AUDIENCE must all be set in config/${env}/process.env`);
|
|
72
|
+
}
|
|
73
|
+
const creds = { domain, clientId, clientSecret, audience };
|
|
74
|
+
// WHICH HOSTNAME THE HTTP TRANSPORT HITS is a SEPARATE switch from
|
|
75
|
+
// which config supplied these credentials -- utilities/backlog.ts's
|
|
76
|
+
// request() picks localhost vs www.maka-cli.com off `MAKA_DEV`
|
|
77
|
+
// (read once, at that module's first import), which is this CLI's
|
|
78
|
+
// own long-standing convention and has nothing to do with `--env`
|
|
79
|
+
// by default. Without this, `--env local` would mint a token
|
|
80
|
+
// against maka-cli.com's real local Auth0 credentials and then
|
|
81
|
+
// still call PRODUCTION with it. Only `local` flips it -- there is
|
|
82
|
+
// no separately-served "development" host in this cluster today
|
|
83
|
+
// (nothing deploys that environment; see maka-cli.com's CLAUDE.md),
|
|
84
|
+
// so that one still targets production same as the default.
|
|
85
|
+
if (env === 'local' && !process.env.MAKA_DEV) {
|
|
86
|
+
process.env.MAKA_DEV = 'True';
|
|
87
|
+
}
|
|
88
|
+
// Heavy/optional imports, lazily loaded inside the handler -- the
|
|
89
|
+
// same discipline sideQuest-backlog.sub.cmd.ts documents for the
|
|
90
|
+
// game graph, so `maka --help` and unrelated commands never pay for
|
|
91
|
+
// these.
|
|
92
|
+
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
93
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
94
|
+
const { z } = await import('zod');
|
|
95
|
+
const { ensureServiceSession } = await import('./sideQuest/utilities/service-account-auth.js');
|
|
96
|
+
const { fetchBacklog, fetchBacklogItemById, patchBacklogItem, draftBacklogBench, postBacklogComment, BACKLOG_STATUSES, } = await import('./sideQuest/utilities/backlog.js');
|
|
97
|
+
const statusEnum = BACKLOG_STATUSES;
|
|
98
|
+
/** Every tool call goes through this: reuse the cached session, and
|
|
99
|
+
* on the first `expired` outcome, mint a fresh one and retry
|
|
100
|
+
* exactly once. A second failure is surfaced rather than looping
|
|
101
|
+
* forever. */
|
|
102
|
+
async function withSession(call) {
|
|
103
|
+
const token = await ensureServiceSession(creds);
|
|
104
|
+
const result = await call(token);
|
|
105
|
+
if (result.outcome !== 'expired')
|
|
106
|
+
return result;
|
|
107
|
+
const fresh = await ensureServiceSession(creds, { force: true });
|
|
108
|
+
return call(fresh);
|
|
109
|
+
}
|
|
110
|
+
/** House shape for a tool result: JSON in, MCP text-content out. A
|
|
111
|
+
* non-'ok' outcome is reported as `isError` so the calling model
|
|
112
|
+
* sees a refusal it can act on (or explain to the human) rather
|
|
113
|
+
* than a raw protocol exception. */
|
|
114
|
+
function toolResult(value) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
117
|
+
isError: value.outcome !== 'ok',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const server = new McpServer({ name: 'maka-play-backlog', version: '1.0.0' });
|
|
121
|
+
server.registerTool('backlog_list', {
|
|
122
|
+
title: 'List backlog items',
|
|
123
|
+
description: 'List game-backlog items, optionally filtered by status. Closed (complete) items are excluded unless a status is given explicitly.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
status: z.enum(statusEnum).optional()
|
|
126
|
+
.describe('Filter to one status. Omit to see everything not yet complete.'),
|
|
127
|
+
},
|
|
128
|
+
}, async ({ status }) => toolResult(await withSession((token) => fetchBacklog(status, { all: false }, token))));
|
|
129
|
+
server.registerTool('backlog_get', {
|
|
130
|
+
title: 'Read one backlog item',
|
|
131
|
+
description: 'Read a single game-backlog item in full, by id.',
|
|
132
|
+
inputSchema: { itemId: z.string().describe('The backlog item id.') },
|
|
133
|
+
}, async ({ itemId }) => toolResult(await withSession((token) => fetchBacklogItemById(itemId, token))));
|
|
134
|
+
server.registerTool('backlog_comment', {
|
|
135
|
+
title: 'Comment on a backlog item',
|
|
136
|
+
description: 'Post an admin note or a question to the reporter on a backlog item. A "question" with awaitingReporter puts it in the reporter\'s review queue and tomorrow\'s digest; a "note" does neither.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
itemId: z.string(),
|
|
139
|
+
text: z.string().min(1),
|
|
140
|
+
kind: z.enum(['question', 'note']).default('note'),
|
|
141
|
+
awaitingReporter: z.boolean().optional()
|
|
142
|
+
.describe('Only meaningful with kind "question" -- puts this item in the reporter\'s queue.'),
|
|
143
|
+
},
|
|
144
|
+
}, async ({ itemId, text, kind, awaitingReporter }) => toolResult(await withSession(async (token) => ({
|
|
145
|
+
outcome: await postBacklogComment(itemId, text, { kind, awaitingReporter }, token),
|
|
146
|
+
}))));
|
|
147
|
+
server.registerTool('backlog_advance', {
|
|
148
|
+
title: 'Advance a backlog item (attach evidence / change status)',
|
|
149
|
+
description: `Move a backlog item's status and/or attach fix-commit evidence. This is the
|
|
150
|
+
actual resolution action -- it will be refused by the server (with a message explaining
|
|
151
|
+
why) if you try to enter "in-review" or "complete" without at least one commit attached,
|
|
152
|
+
or "complete" without the reporter's sign-off already on the item. This tool never signs
|
|
153
|
+
off or rejects a fix on the reporter's behalf -- only the person who filed the item can
|
|
154
|
+
do that, in-game.`,
|
|
155
|
+
inputSchema: {
|
|
156
|
+
itemId: z.string(),
|
|
157
|
+
status: z.enum(statusEnum),
|
|
158
|
+
commits: z.array(z.string()).optional()
|
|
159
|
+
.describe('Fix-commit SHAs to attach (merged with whatever is already there).'),
|
|
160
|
+
},
|
|
161
|
+
}, async ({ itemId, status, commits }) => toolResult(await withSession((token) => patchBacklogItem(itemId, { status, commits }, token))));
|
|
162
|
+
server.registerTool('backlog_draft_bench', {
|
|
163
|
+
title: 'Draft an AI bench for a backlog item',
|
|
164
|
+
description: 'Ask the server to AI-draft a two-room repro bench for one item (lands in reproDraft, not repro -- a human has to dry-run and promote it separately via backlog_advance with a repro body). Costs a model call server-side.',
|
|
165
|
+
inputSchema: {
|
|
166
|
+
itemId: z.string(),
|
|
167
|
+
verbs: z.array(z.string()).optional().describe('The verbs available to reference in the bench steps.'),
|
|
168
|
+
},
|
|
169
|
+
}, async ({ itemId, verbs }) => toolResult(await withSession((token) => draftBacklogBench(itemId, token, verbs))));
|
|
170
|
+
const transport = new StdioServerTransport();
|
|
171
|
+
await server.connect(transport);
|
|
172
|
+
console.error('[play:backlog:mcp] ready');
|
|
173
|
+
}, parent);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=sideQuest-backlog-mcp.sub.cmd.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ShadowrunCommand, { ShadowrunCompat } from './sideQuest.sub.cmd.js';
|
|
2
2
|
import { Command } from '../../command.js';
|
|
3
|
+
import { registerBacklogMcp } from './sideQuest-backlog-mcp.sub.cmd.js';
|
|
3
4
|
// Registered under BOTH the new spelling (play:backlog) and the compat
|
|
4
5
|
// sub (play:shadowrun:backlog) from ONE definition -- two commands, one
|
|
5
6
|
// options object, one handler, so the pair cannot drift.
|
|
@@ -62,6 +63,9 @@ const registerBacklog = (parent) => Command.create({
|
|
|
62
63
|
detachAsked,
|
|
63
64
|
});
|
|
64
65
|
}, parent);
|
|
65
|
-
registerBacklog(ShadowrunCommand);
|
|
66
|
+
const backlogCmd = registerBacklog(ShadowrunCommand);
|
|
66
67
|
registerBacklog(ShadowrunCompat);
|
|
68
|
+
// mcp: only under the live `play:` spelling -- a brand-new command has
|
|
69
|
+
// no compat surface to preserve under `play:shadowrun:backlog:mcp`.
|
|
70
|
+
registerBacklogMcp(backlogCmd);
|
|
67
71
|
//# sourceMappingURL=sideQuest-backlog.sub.cmd.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.143.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@inquirer/prompts": "^8.5.2",
|
|
50
50
|
"@maka/meteor-sdk": "^0.2.13",
|
|
51
51
|
"@maka/tar": "^6.2.2",
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
52
53
|
"blessed": "^0.1.81",
|
|
53
54
|
"chalk": "^4.1.2",
|
|
54
55
|
"change-case": "^5.4.4",
|
|
@@ -70,7 +71,8 @@
|
|
|
70
71
|
"tar": "^7.4.3",
|
|
71
72
|
"uuid": "^14.0.1",
|
|
72
73
|
"ws": "^8.21.3",
|
|
73
|
-
"xtend": "^4.0.2"
|
|
74
|
+
"xtend": "^4.0.2",
|
|
75
|
+
"zod": "^4.5.4"
|
|
74
76
|
},
|
|
75
77
|
"devDependencies": {
|
|
76
78
|
"@aws-sdk/types": "^3.974.4",
|