@tandryukha/aidemo 0.19.0 → 0.20.0
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 +3 -3
- package/package.json +8 -7
- package/src/mcp/jobs.ts +0 -4
- package/src/mcp/server.ts +66 -54
package/README.md
CHANGED
|
@@ -304,9 +304,9 @@ demos/<name>/ ← your working area (untracked; scaffold with `aidemo i
|
|
|
304
304
|
ffmpeg helper that encodes the take; off with `AIDEMO_NO_AUTO_INSTALL=1`), and
|
|
305
305
|
`github.com` (your own `gh`, for `aidemo feedback`). Recording/composing are
|
|
306
306
|
otherwise fully local. The MCP server is **stdio-only** — no listener.
|
|
307
|
-
- **Small, auditable surface:** ~50 source files, 7 runtime deps
|
|
308
|
-
immutable ref if you're wary of the moving
|
|
309
|
-
`npx -y github:tandryukha/aidemo#v0.
|
|
307
|
+
- **Small, auditable surface:** ~50 source files, 7 runtime deps (11 packages
|
|
308
|
+
installed in all), MIT. Pin an immutable ref if you're wary of the moving
|
|
309
|
+
`#stable` tag: `npx -y github:tandryukha/aidemo#v0.20.0` (any released tag).
|
|
310
310
|
- Full detail: [docs/LOCAL_MODELS.md](docs/LOCAL_MODELS.md#what-leaves-the-machine)
|
|
311
311
|
· report vulnerabilities privately per [SECURITY.md](SECURITY.md).
|
|
312
312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tandryukha/aidemo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "AI Demo Engine — turn a storyboard into a narrated, captioned product demo video",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"build": "tsc --noEmit",
|
|
32
32
|
"serve-example": "node examples/local-demo/serve.mjs",
|
|
33
33
|
"mcp-smoke": "node test/mcp-smoke.mjs",
|
|
34
|
-
"test": "tsx --test test/issue-64.mjs test/issue-89.mjs test/runtime-setup.mjs test/mcp-schemas.mjs",
|
|
34
|
+
"test": "tsx --test test/issue-64.mjs test/issue-89.mjs test/runtime-setup.mjs test/mcp-schemas.mjs test/mcp-stdio.mjs",
|
|
35
35
|
"test:blank-open": "tsx --test test/blank-open.mjs",
|
|
36
36
|
"mcp-env-test": "node test/mcp-env.mjs",
|
|
37
37
|
"guard-openai-key": "tsx scripts/guard-openai-key.ts",
|
|
@@ -39,16 +39,17 @@
|
|
|
39
39
|
"check:plugin-skill": "diff -q .claude/skills/record-demo/SKILL.md plugins/record-demo/skills/record-demo/SKILL.md"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@modelcontextprotocol/
|
|
42
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
43
43
|
"commander": "^15.0.0",
|
|
44
44
|
"openai": "^7.47.0",
|
|
45
|
-
"playwright": "^1.
|
|
45
|
+
"playwright": "^1.64.1",
|
|
46
46
|
"re2js": "2.9.1",
|
|
47
|
-
"tsx": "^4.23.
|
|
47
|
+
"tsx": "^4.23.15",
|
|
48
48
|
"zod": "^4.6.7"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@
|
|
52
|
-
"
|
|
51
|
+
"@modelcontextprotocol/client": "2.4.0",
|
|
52
|
+
"@types/node": "^26.6.6",
|
|
53
|
+
"typescript": "^7.0.2"
|
|
53
54
|
}
|
|
54
55
|
}
|
package/src/mcp/jobs.ts
CHANGED
|
@@ -87,9 +87,6 @@ export class JobManager {
|
|
|
87
87
|
private jobs = new Map<string, Job>();
|
|
88
88
|
private activeId: string | null = null;
|
|
89
89
|
|
|
90
|
-
/** Best-effort per-line forwarding (e.g. MCP logging notifications). */
|
|
91
|
-
onLine?: (job: Job, cleanChunk: string) => void;
|
|
92
|
-
|
|
93
90
|
/** Start a job or throw JobBusyError if one is already running. */
|
|
94
91
|
startJob(
|
|
95
92
|
kind: JobKind,
|
|
@@ -284,6 +281,5 @@ export class JobManager {
|
|
|
284
281
|
if (job.ring.length > RING_MAX) {
|
|
285
282
|
job.ring.splice(0, job.ring.length - RING_MAX);
|
|
286
283
|
}
|
|
287
|
-
this.onLine?.(job, chunk);
|
|
288
284
|
}
|
|
289
285
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { resolve } from "node:path";
|
|
@@ -83,10 +83,11 @@ function storyboardJsonSchema(): Record<string, unknown> {
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Tool input schemas advertise `additionalProperties: false` on every object,
|
|
86
|
-
* as they did before the zod 4 migration: the MCP SDK converts zod
|
|
87
|
-
* with io:"input", which leaves objects
|
|
88
|
-
* strips unknown arguments at runtime,
|
|
89
|
-
*
|
|
86
|
+
* as they did before the zod 4 migration: the MCP SDK converts zod schemas
|
|
87
|
+
* through zod's Standard JSON Schema with io:"input", which leaves objects
|
|
88
|
+
* open. Metadata only — zod still strips unknown arguments at runtime,
|
|
89
|
+
* exactly as before. (A tool without arguments registers no input schema;
|
|
90
|
+
* the SDK lists it as an empty object.)
|
|
90
91
|
*/
|
|
91
92
|
function closedInput<Shape extends z.ZodRawShape>(shape: Shape) {
|
|
92
93
|
for (const field of Object.values(shape)) closeObjects(field);
|
|
@@ -280,29 +281,20 @@ function throwIfAborted(signal: AbortSignal): void {
|
|
|
280
281
|
if (signal.aborted) throw new CanceledError("canceled between stages");
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
|
|
284
|
-
|
|
284
|
+
/**
|
|
285
|
+
* One MCP server instance over the process-wide job manager. Under stdio the
|
|
286
|
+
* SDK may build more than one instance per process (a discarded
|
|
287
|
+
* `server/discover` probe, then the connection's instance), so job state lives
|
|
288
|
+
* in the shared `jobs`, never in an instance. Progress reaches clients through
|
|
289
|
+
* job_status polling; engine logging goes to stderr and the job's log file
|
|
290
|
+
* (MCP logging notifications are deprecated as of protocol 2026-07-28).
|
|
291
|
+
*/
|
|
292
|
+
export function buildMcpServer(jobs: JobManager = new JobManager()): { server: McpServer; jobs: JobManager } {
|
|
285
293
|
const server = new McpServer(
|
|
286
294
|
{ name: "aidemo", version: engineVersion() },
|
|
287
|
-
{ instructions: INSTRUCTIONS
|
|
295
|
+
{ instructions: INSTRUCTIONS }
|
|
288
296
|
);
|
|
289
297
|
|
|
290
|
-
// Best-effort log forwarding (throttled); job_status polling is the source
|
|
291
|
-
// of truth — many clients don't surface logging notifications.
|
|
292
|
-
const lastSent = new Map<string, number>();
|
|
293
|
-
jobs.onLine = (job, chunk) => {
|
|
294
|
-
const now = Date.now();
|
|
295
|
-
if (now - (lastSent.get(job.jobId) ?? 0) < 1000) return;
|
|
296
|
-
lastSent.set(job.jobId, now);
|
|
297
|
-
void server
|
|
298
|
-
.sendLoggingMessage({
|
|
299
|
-
level: "info",
|
|
300
|
-
logger: "aidemo",
|
|
301
|
-
data: { jobId: job.jobId, stage: job.stage, line: chunk.trim() },
|
|
302
|
-
})
|
|
303
|
-
.catch(() => {});
|
|
304
|
-
};
|
|
305
|
-
|
|
306
298
|
const guidePath = guideFilePath();
|
|
307
299
|
|
|
308
300
|
server.registerTool(
|
|
@@ -324,13 +316,13 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
324
316
|
`Slice: ${GUIDE_TOPIC_NAMES.join(", ")} — or an H2 heading prefix. Omit for the full guide.`
|
|
325
317
|
),
|
|
326
318
|
}),
|
|
327
|
-
outputSchema: {
|
|
319
|
+
outputSchema: z.object({
|
|
328
320
|
guide: z.string(),
|
|
329
321
|
engineVersion: z.string(),
|
|
330
322
|
topic: z.string().optional(),
|
|
331
323
|
topics: z.array(z.string()),
|
|
332
324
|
sections: z.array(z.string()),
|
|
333
|
-
},
|
|
325
|
+
}),
|
|
334
326
|
annotations: { readOnlyHint: true },
|
|
335
327
|
},
|
|
336
328
|
async ({ topic }) => {
|
|
@@ -366,8 +358,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
366
358
|
description:
|
|
367
359
|
"JSON Schema for storyboard.json, generated from the engine's own " +
|
|
368
360
|
"zod schema — the exact contract the engine validates against.",
|
|
369
|
-
|
|
370
|
-
outputSchema: { schema: z.record(z.string(), z.unknown()) },
|
|
361
|
+
outputSchema: z.object({ schema: z.record(z.string(), z.unknown()) }),
|
|
371
362
|
annotations: { readOnlyHint: true },
|
|
372
363
|
},
|
|
373
364
|
async () =>
|
|
@@ -392,7 +383,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
392
383
|
relaxed: z.boolean().optional(),
|
|
393
384
|
params: PARAMS_INPUT,
|
|
394
385
|
}),
|
|
395
|
-
outputSchema: {
|
|
386
|
+
outputSchema: z.object({
|
|
396
387
|
valid: z.boolean(),
|
|
397
388
|
storyboardPath: z.string().optional(),
|
|
398
389
|
title: z.string().optional(),
|
|
@@ -402,7 +393,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
402
393
|
),
|
|
403
394
|
warnings: z.array(z.string()),
|
|
404
395
|
lint: z.array(LINT_ISSUE_SHAPE).optional(),
|
|
405
|
-
},
|
|
396
|
+
}),
|
|
406
397
|
annotations: { readOnlyHint: true },
|
|
407
398
|
},
|
|
408
399
|
async (args) => {
|
|
@@ -485,7 +476,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
485
476
|
lang: z.string().optional(),
|
|
486
477
|
params: PARAMS_INPUT,
|
|
487
478
|
}),
|
|
488
|
-
outputSchema: {
|
|
479
|
+
outputSchema: z.object({
|
|
489
480
|
issues: z.array(LINT_ISSUE_SHAPE),
|
|
490
481
|
estimate: z.array(
|
|
491
482
|
z.object({
|
|
@@ -499,7 +490,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
499
490
|
),
|
|
500
491
|
narrationTotalMs: z.number(),
|
|
501
492
|
wordsPerSec: z.number(),
|
|
502
|
-
},
|
|
493
|
+
}),
|
|
503
494
|
annotations: { readOnlyHint: true },
|
|
504
495
|
},
|
|
505
496
|
async (args) => {
|
|
@@ -557,11 +548,11 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
557
548
|
.optional()
|
|
558
549
|
.describe("fromUrl: viewport (default 1280x720)"),
|
|
559
550
|
}),
|
|
560
|
-
outputSchema: {
|
|
551
|
+
outputSchema: z.object({
|
|
561
552
|
demoDir: z.string(),
|
|
562
553
|
storyboardPath: z.string(),
|
|
563
554
|
briefPath: z.string(),
|
|
564
|
-
},
|
|
555
|
+
}),
|
|
565
556
|
},
|
|
566
557
|
async (args) => {
|
|
567
558
|
// fromUrl launches Chrome: honour the target repo's .env (#77).
|
|
@@ -599,7 +590,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
599
590
|
inputSchema: closedInput({
|
|
600
591
|
dir: z.string().optional().describe("repo to check for an installed skill"),
|
|
601
592
|
}),
|
|
602
|
-
outputSchema: {
|
|
593
|
+
outputSchema: z.object({
|
|
603
594
|
engineVersion: z.string(),
|
|
604
595
|
node: z.string(),
|
|
605
596
|
ffmpeg: z.string().nullable(),
|
|
@@ -647,7 +638,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
647
638
|
.nullable(),
|
|
648
639
|
}),
|
|
649
640
|
ok: z.boolean(),
|
|
650
|
-
},
|
|
641
|
+
}),
|
|
651
642
|
annotations: { readOnlyHint: true },
|
|
652
643
|
},
|
|
653
644
|
async (args) => {
|
|
@@ -681,7 +672,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
681
672
|
.optional()
|
|
682
673
|
.describe("still-frame basename under stills/ (default: poster)"),
|
|
683
674
|
}),
|
|
684
|
-
outputSchema: {
|
|
675
|
+
outputSchema: z.object({
|
|
685
676
|
owner: z.string(),
|
|
686
677
|
repo: z.string(),
|
|
687
678
|
demo: z.string(),
|
|
@@ -705,7 +696,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
705
696
|
present: z.boolean(),
|
|
706
697
|
template: z.string(),
|
|
707
698
|
}),
|
|
708
|
-
},
|
|
699
|
+
}),
|
|
709
700
|
annotations: { readOnlyHint: true },
|
|
710
701
|
},
|
|
711
702
|
async (args) => {
|
|
@@ -758,14 +749,14 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
758
749
|
.optional()
|
|
759
750
|
.describe("assemble the title/body but don't file anything — preview only"),
|
|
760
751
|
}),
|
|
761
|
-
outputSchema: {
|
|
752
|
+
outputSchema: z.object({
|
|
762
753
|
filed: z.boolean(),
|
|
763
754
|
title: z.string(),
|
|
764
755
|
body: z.string(),
|
|
765
756
|
url: z.string().optional(),
|
|
766
757
|
localPath: z.string().optional(),
|
|
767
758
|
message: z.string(),
|
|
768
|
-
},
|
|
759
|
+
}),
|
|
769
760
|
},
|
|
770
761
|
async (args) => {
|
|
771
762
|
const demoDir = args.dir ? resolve(args.dir) : null;
|
|
@@ -793,7 +784,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
793
784
|
jobId: z.string(),
|
|
794
785
|
tailLines: z.number().optional().describe("log lines to return (default 40)"),
|
|
795
786
|
}),
|
|
796
|
-
outputSchema: JOB_STATUS_SHAPE,
|
|
787
|
+
outputSchema: z.object(JOB_STATUS_SHAPE),
|
|
797
788
|
annotations: { readOnlyHint: true },
|
|
798
789
|
},
|
|
799
790
|
async (args) => {
|
|
@@ -814,8 +805,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
814
805
|
{
|
|
815
806
|
title: "List jobs",
|
|
816
807
|
description: "All jobs this server session, newest last.",
|
|
817
|
-
|
|
818
|
-
outputSchema: {
|
|
808
|
+
outputSchema: z.object({
|
|
819
809
|
jobs: z.array(
|
|
820
810
|
z.object({
|
|
821
811
|
jobId: z.string(),
|
|
@@ -827,7 +817,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
827
817
|
endedAt: z.string().nullable(),
|
|
828
818
|
})
|
|
829
819
|
),
|
|
830
|
-
},
|
|
820
|
+
}),
|
|
831
821
|
annotations: { readOnlyHint: true },
|
|
832
822
|
},
|
|
833
823
|
async () =>
|
|
@@ -853,7 +843,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
853
843
|
"mid-record cancel salvages the partial timeline + footage. The job " +
|
|
854
844
|
"settles asynchronously — poll job_status for the final state.",
|
|
855
845
|
inputSchema: closedInput({ jobId: z.string() }),
|
|
856
|
-
outputSchema: JOB_STATUS_SHAPE,
|
|
846
|
+
outputSchema: z.object(JOB_STATUS_SHAPE),
|
|
857
847
|
},
|
|
858
848
|
async (args) => {
|
|
859
849
|
const job = jobs.cancel(args.jobId);
|
|
@@ -879,7 +869,7 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
879
869
|
`${description} Returns a jobId immediately — poll job_status. ` +
|
|
880
870
|
`Rejects if another job is running.`,
|
|
881
871
|
inputSchema: closedInput(inputShape),
|
|
882
|
-
outputSchema: JOB_START_SHAPE,
|
|
872
|
+
outputSchema: z.object(JOB_START_SHAPE),
|
|
883
873
|
},
|
|
884
874
|
(async (args: z.infer<z.ZodObject<Shape>>) => {
|
|
885
875
|
const project = new Project((args as { dir: string }).dir);
|
|
@@ -1063,13 +1053,13 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
1063
1053
|
dir: z.string().optional().describe("repo to scaffold into (default: server cwd)"),
|
|
1064
1054
|
force: z.boolean().optional(),
|
|
1065
1055
|
}),
|
|
1066
|
-
outputSchema: {
|
|
1056
|
+
outputSchema: z.object({
|
|
1067
1057
|
demoDir: z.string(),
|
|
1068
1058
|
storyboardPath: z.string(),
|
|
1069
1059
|
steps: z.number(),
|
|
1070
1060
|
scenes: z.number(),
|
|
1071
1061
|
notes: z.array(z.string()),
|
|
1072
|
-
},
|
|
1062
|
+
}),
|
|
1073
1063
|
},
|
|
1074
1064
|
async (args) => {
|
|
1075
1065
|
try {
|
|
@@ -1526,9 +1516,30 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
1526
1516
|
return { server, jobs };
|
|
1527
1517
|
}
|
|
1528
1518
|
|
|
1529
|
-
/**
|
|
1519
|
+
/**
|
|
1520
|
+
* The process's stdio wire, reporting when the connection ends. serveStdio
|
|
1521
|
+
* owns the wire and closes it when the connection is over (stdin EOF, a
|
|
1522
|
+
* broken stdout, an oversized message, or the connection's server closing);
|
|
1523
|
+
* discarding a `server/discover` probe instance never closes it.
|
|
1524
|
+
*/
|
|
1525
|
+
class ProcessStdioTransport extends StdioServerTransport {
|
|
1526
|
+
constructor(private readonly onEnd: () => void) {
|
|
1527
|
+
super();
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
override async close(): Promise<void> {
|
|
1531
|
+
await super.close();
|
|
1532
|
+
this.onEnd();
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Run the stdio server until the client disconnects. Never resolves.
|
|
1538
|
+
* serveStdio answers both protocol eras: a 2025-era client (`initialize`
|
|
1539
|
+
* handshake, what deployed clients speak) and a 2026-07-28 client.
|
|
1540
|
+
*/
|
|
1530
1541
|
export async function runMcpServer(): Promise<void> {
|
|
1531
|
-
const
|
|
1542
|
+
const jobs = new JobManager();
|
|
1532
1543
|
let shuttingDown = false;
|
|
1533
1544
|
const shutdown = () => {
|
|
1534
1545
|
if (shuttingDown) return;
|
|
@@ -1551,8 +1562,9 @@ export async function runMcpServer(): Promise<void> {
|
|
|
1551
1562
|
};
|
|
1552
1563
|
process.stdin.on("end", shutdown);
|
|
1553
1564
|
process.stdin.on("close", shutdown);
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1565
|
+
serveStdio(() => buildMcpServer(jobs).server, {
|
|
1566
|
+
transport: new ProcessStdioTransport(shutdown),
|
|
1567
|
+
onerror: (err) => log(`mcp: ${err.message}`),
|
|
1568
|
+
});
|
|
1557
1569
|
await new Promise(() => {}); // lives until shutdown() exits the process
|
|
1558
1570
|
}
|