@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 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, MIT. Pin an
308
- immutable ref if you're wary of the moving `#stable` tag:
309
- `npx -y github:tandryukha/aidemo#v0.18.0` (any released tag).
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.19.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/sdk": "1.32.2",
42
+ "@modelcontextprotocol/server": "2.4.0",
43
43
  "commander": "^15.0.0",
44
44
  "openai": "^7.47.0",
45
- "playwright": "^1.55.1",
45
+ "playwright": "^1.64.1",
46
46
  "re2js": "2.9.1",
47
- "tsx": "^4.23.1",
47
+ "tsx": "^4.23.15",
48
48
  "zod": "^4.6.7"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^22.20.1",
52
- "typescript": "^5.6.0"
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/sdk/server/mcp.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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 4 schemas
87
- * with io:"input", which leaves objects open. Metadata only — zod still
88
- * strips unknown arguments at runtime, exactly as before. (An empty input
89
- * stays a raw `{}`, which the SDK lists as it always has.)
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
- export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
284
- const jobs = new JobManager();
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, capabilities: { logging: {} } }
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
- inputSchema: {},
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
- inputSchema: {},
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
- /** Run the stdio server until the client disconnects. Never resolves. */
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 { server, jobs } = buildMcpServer();
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
- server.server.onclose = shutdown;
1555
- const transport = new StdioServerTransport();
1556
- await server.connect(transport);
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
  }