@lalalic/markcut 3.2.11 → 3.2.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalalic/markcut",
3
- "version": "3.2.11",
3
+ "version": "3.2.13",
4
4
  "description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
5
5
  "bin": {
6
6
  "markcut": "bin/markcut"
@@ -1,13 +1,14 @@
1
1
  export function buildPreviewBridgeScript(submitToolName) {
2
2
  return `
3
- const feedback=document.getElementById('feedback'),status=document.getElementById('status'),article=document.querySelector('article'),pathLabel=document.querySelector('main>p code');
3
+ const feedback=document.getElementById('feedback'),status=document.getElementById('status'),player=document.getElementById('video-player'),pathLabel=document.getElementById('path-label');
4
4
  let nextRequestId=0;
5
5
  const pendingRequests=new Map();
6
6
  function send(message){window.parent.postMessage(message,'*');}
7
7
  function rpcRequest(method,params){const id=++nextRequestId;send({jsonrpc:'2.0',id,method,params});return new Promise((resolve,reject)=>pendingRequests.set(id,{resolve,reject}));}
8
- function applyToolResult(params){const result=params?.result||params||{},data=result.structuredContent||result.structured_content||{};if(typeof data.path==='string')pathLabel.textContent=data.path;if(typeof data.markdown==='string'){article.innerHTML=data.markdown.split(/\\r?\\n/).map(line=>{const escaped=line.replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));if(line.startsWith('### '))return '<h3>'+escaped.slice(4)+'</h3>';if(line.startsWith('## '))return '<h2>'+escaped.slice(3)+'</h2>';if(line.startsWith('# '))return '<h1>'+escaped.slice(2)+'</h1>';if(line.startsWith('- '))return '<li>'+escaped.slice(2)+'</li>';return line.trim()?'<p>'+escaped+'</p>':''}).join('\\n');}}
8
+ async function waitForVideo(url){status.textContent='Preparing video…';for(;;){try{const response=await fetch(url+'api/ready',{cache:'no-store'});if(response.ok){const ready=await response.json();if(ready.ready){player.src=url;status.textContent='Video ready for review.';return;}}}catch{}await new Promise(resolve=>setTimeout(resolve,1500));}}
9
+ function applyToolResult(params){const result=params?.result||params||{},data=result.structuredContent||result.structured_content||{};if(typeof data.path==='string')pathLabel.textContent=data.path;if(typeof data.previewUrl==='string')waitForVideo(data.previewUrl);}
9
10
  window.addEventListener('message',event=>{if(event.source!==window.parent)return;const message=event.data;if(!message||typeof message!=='object'||message.jsonrpc!=='2.0')return;if(Object.prototype.hasOwnProperty.call(message,'id')&&pendingRequests.has(message.id)){const pending=pendingRequests.get(message.id);pendingRequests.delete(message.id);if(message.error)pending.reject(new Error(message.error.message||'MCP request failed'));else pending.resolve(message.result);return;}if(message.method==='ui/notifications/tool-result')applyToolResult(message.params);});
10
- async function initializeBridge(){try{await rpcRequest('ui/initialize',{appInfo:{name:'Markcut Preview',version:'1.0.0'},appCapabilities:{},protocolVersion:'2026-01-26'});send({jsonrpc:'2.0',method:'ui/notifications/initialized'});status.textContent='Ready for review.';}catch(error){status.textContent=error.message||'MCP initialization failed.';}}
11
+ async function initializeBridge(){try{await rpcRequest('ui/initialize',{appInfo:{name:'Markcut Preview',version:'1.1.0'},appCapabilities:{},protocolVersion:'2026-01-26'});send({jsonrpc:'2.0',method:'ui/notifications/initialized'});if(!player.src)status.textContent='Waiting for video preview…';}catch(error){status.textContent=error.message||'MCP initialization failed.';}}
11
12
  async function submit(decision){const value=feedback.value.trim();if(decision==='changes_requested'&&!value){status.textContent='Feedback is required.';feedback.focus();return;}const result={decision};if(value&&decision==='changes_requested')result.feedback=value;status.textContent='Submitting...';try{await rpcRequest('tools/call',{name:'${submitToolName}',arguments:result});await rpcRequest('ui/message',{role:'user',content:[{type:'text',text:JSON.stringify(result)}]});status.textContent=decision==='approved'?'Approved.':'Changes requested.';}catch(error){status.textContent=error.message||'Unable to submit review.';}}
12
13
  initializeBridge();
13
14
  document.getElementById('approve').onclick=()=>submit('approved');document.getElementById('changes').onclick=()=>submit('changes_requested');
@@ -1,10 +1,44 @@
1
- import { readFile } from "node:fs/promises";
2
- import { isAbsolute, extname } from "node:path";
1
+ import { spawn } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { constants as fsConstants } from "node:fs";
4
+ import { isAbsolute, extname, dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { buildPreviewBridgeScript } from "./bridge.mjs";
4
7
 
5
8
  export const TOOL_NAME = "markcut.preview";
6
9
  export const SUBMIT_TOOL_NAME = "markcut.preview.submit";
7
- export const RESOURCE_URI = "ui://markcut/markdown-preview-v2.html";
10
+ export const RESOURCE_URI = "ui://markcut/video-preview-v3.html";
11
+ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
12
+ export const PLAYER_PORT = 4571;
13
+ export const PLAYER_ORIGIN = `http://127.0.0.1:${PLAYER_PORT}`;
14
+
15
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
16
+ const playerServer = resolve(moduleDir, "../player/server.mjs");
17
+ let managedPlayer = null;
18
+ let managedPath = null;
19
+ let startPromise = null;
20
+
21
+ export function buildPreviewResourceResult() {
22
+ return {
23
+ ttlMs: 0,
24
+ cacheScope: "private",
25
+ contents: [{
26
+ uri: RESOURCE_URI,
27
+ mimeType: RESOURCE_MIME_TYPE,
28
+ text: buildPreviewHtml(),
29
+ _meta: {
30
+ ui: {
31
+ prefersBorder: true,
32
+ csp: {
33
+ connectDomains: [PLAYER_ORIGIN],
34
+ resourceDomains: [],
35
+ frameDomains: [PLAYER_ORIGIN],
36
+ },
37
+ },
38
+ },
39
+ }],
40
+ };
41
+ }
8
42
 
9
43
  export function validateMarkdownPath(value) {
10
44
  if (typeof value !== "string" || !isAbsolute(value)) {
@@ -16,13 +50,59 @@ export function validateMarkdownPath(value) {
16
50
  return value;
17
51
  }
18
52
 
19
- export async function readMarkdownPreview(path) {
53
+ export async function ensureVideoPreview(path, options = {}) {
20
54
  const validatedPath = validateMarkdownPath(path);
21
- try {
22
- return { path: validatedPath, markdown: await readFile(validatedPath, "utf8") };
23
- } catch (error) {
24
- throw new Error(`unable to read Markdown file: ${error instanceof Error ? error.message : String(error)}`);
55
+ await access(validatedPath, fsConstants.R_OK);
56
+ if (managedPlayer && managedPath === validatedPath && managedPlayer.exitCode == null && !managedPlayer.killed) {
57
+ return { path: validatedPath, previewUrl: `${PLAYER_ORIGIN}/` };
58
+ }
59
+ if (startPromise && managedPath === validatedPath) return startPromise;
60
+
61
+ if (managedPlayer && managedPlayer.exitCode == null && !managedPlayer.killed) {
62
+ managedPlayer.kill("SIGTERM");
25
63
  }
64
+ managedPlayer = null;
65
+ managedPath = validatedPath;
66
+
67
+ const spawnImpl = options.spawnImpl || spawn;
68
+ const settleMs = options.settleMs ?? 250;
69
+ startPromise = new Promise((resolvePromise, rejectPromise) => {
70
+ const child = spawnImpl(process.execPath, [playerServer, validatedPath, `--port=${PLAYER_PORT}`], {
71
+ cwd: resolve(moduleDir, "../.."),
72
+ stdio: ["ignore", "pipe", "pipe"],
73
+ });
74
+ managedPlayer = child;
75
+ let stderr = "";
76
+ let settled = false;
77
+ const finish = (error) => {
78
+ if (settled) return;
79
+ settled = true;
80
+ startPromise = null;
81
+ if (error) {
82
+ if (managedPlayer === child) managedPlayer = null;
83
+ rejectPromise(error);
84
+ } else {
85
+ resolvePromise({ path: validatedPath, previewUrl: `${PLAYER_ORIGIN}/` });
86
+ }
87
+ };
88
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
89
+ child.once("error", (error) => finish(new Error(`unable to start Markcut player: ${error.message}`)));
90
+ child.once("exit", (code, signal) => {
91
+ if (managedPlayer === child) managedPlayer = null;
92
+ if (!settled) finish(new Error(`Markcut player exited during startup (${signal || code || "unknown"})${stderr ? `: ${stderr.trim()}` : ""}`));
93
+ });
94
+ setTimeout(() => {
95
+ if (!settled) finish();
96
+ }, settleMs);
97
+ });
98
+ return startPromise;
99
+ }
100
+
101
+ export function stopManagedVideoPreview() {
102
+ if (managedPlayer && managedPlayer.exitCode == null && !managedPlayer.killed) managedPlayer.kill("SIGTERM");
103
+ managedPlayer = null;
104
+ managedPath = null;
105
+ startPromise = null;
26
106
  }
27
107
 
28
108
  export function reviewResult(decision, feedback = "") {
@@ -40,38 +120,16 @@ export function reviewMessage(result) {
40
120
  return {
41
121
  jsonrpc: "2.0",
42
122
  method: "ui/message",
43
- params: {
44
- role: "user",
45
- content: [{ type: "text", text: JSON.stringify(normalized) }],
46
- },
123
+ params: { role: "user", content: [{ type: "text", text: JSON.stringify(normalized) }] },
47
124
  };
48
125
  }
49
126
 
50
- function escapeHtml(value) {
51
- return value.replace(/[&<>"']/g, (character) => ({
52
- "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
53
- })[character]);
54
- }
55
-
56
- function renderMarkdown(markdown) {
57
- return markdown.split(/\r?\n/).map((line) => {
58
- const escaped = escapeHtml(line);
59
- if (line.startsWith("### ")) return `<h3>${escaped.slice(4)}</h3>`;
60
- if (line.startsWith("## ")) return `<h2>${escaped.slice(3)}</h2>`;
61
- if (line.startsWith("# ")) return `<h1>${escaped.slice(2)}</h1>`;
62
- if (line.startsWith("- ")) return `<li>${escaped.slice(2)}</li>`;
63
- return line.trim() ? `<p>${escaped}</p>` : "";
64
- }).join("\n");
65
- }
66
-
67
- export function buildPreviewHtml({ path = "", markdown = "" }) {
68
- const safePath = escapeHtml(path);
69
- const rendered = renderMarkdown(markdown);
127
+ export function buildPreviewHtml() {
70
128
  return `<!doctype html>
71
- <html><head><meta charset="utf-8"><title>MarkCut Markdown review</title>
72
- <style>body{font:15px system-ui,sans-serif;margin:0;padding:24px;color:#17202a;background:#fff}main{max-width:860px;margin:auto}article{border:1px solid #d8dee4;border-radius:8px;padding:20px;overflow:auto}h1,h2,h3{margin-top:0}p{line-height:1.5}textarea{box-sizing:border-box;width:100%;min-height:100px;margin:16px 0 10px;padding:10px;border:1px solid #8c959f;border-radius:6px;font:inherit}button{border:0;border-radius:6px;padding:9px 14px;margin-right:8px;font:inherit;cursor:pointer}#approve{background:#1f883d;color:#fff}#changes{background:#cf222e;color:#fff}#status{min-height:1.4em;color:#cf222e}</style></head>
73
- <body><main><h1>Review Markdown</h1><p><code>${safePath}</code></p><article>${rendered}</article>
74
- <label for="feedback">Feedback (required for requested changes)</label><textarea id="feedback" placeholder="What should be changed?"></textarea><div id="status" role="status"></div>
129
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Markcut video review</title>
130
+ <style>body{font:15px system-ui,sans-serif;margin:0;color:#17202a;background:#fff}main{max-width:980px;margin:auto;padding:16px}.meta{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.meta h1{font-size:18px;margin:0}.path{font-size:12px;color:#656d76;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.player{width:100%;aspect-ratio:9/16;max-height:72vh;border:1px solid #d8dee4;border-radius:10px;background:#0d1117;overflow:hidden}.player iframe{width:100%;height:100%;border:0;display:block}textarea{box-sizing:border-box;width:100%;min-height:82px;margin:12px 0 8px;padding:10px;border:1px solid #8c959f;border-radius:6px;font:inherit}button{border:0;border-radius:6px;padding:9px 14px;margin-right:8px;font:inherit;cursor:pointer}#approve{background:#1f883d;color:#fff}#changes{background:#cf222e;color:#fff}#status{min-height:1.4em;margin:8px 0;color:#656d76}</style></head>
131
+ <body><main><div class="meta"><h1>Review video</h1><div class="path" id="path-label"></div></div><div class="player"><iframe id="video-player" title="Markcut video preview" allow="autoplay; fullscreen"></iframe></div>
132
+ <label for="feedback">Feedback (required for requested changes)</label><textarea id="feedback" placeholder="What should be changed?"></textarea><div id="status" role="status">Starting preview…</div>
75
133
  <button id="approve">Approve</button><button id="changes">Request changes</button></main>
76
134
  <script>${buildPreviewBridgeScript(SUBMIT_TOOL_NAME)}</script></body></html>`;
77
135
  }
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/server";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
4
- import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
4
+ import { registerAppResource, registerAppTool } from "@modelcontextprotocol/ext-apps/server";
5
5
  import { z } from "zod";
6
6
  import {
7
- TOOL_NAME, SUBMIT_TOOL_NAME, RESOURCE_URI, buildPreviewHtml, readMarkdownPreview, reviewResult,
7
+ TOOL_NAME, SUBMIT_TOOL_NAME, RESOURCE_URI, PLAYER_ORIGIN, buildPreviewResourceResult, ensureVideoPreview, reviewResult,
8
8
  } from "./preview.mjs";
9
9
 
10
10
  const server = new McpServer({ name: "markcut", version: "3.2.0" });
@@ -13,7 +13,7 @@ registerAppTool(
13
13
  server,
14
14
  TOOL_NAME,
15
15
  {
16
- description: "Ask a human to review a Markdown file and return an approval or requested changes.",
16
+ description: "Show the Markcut video player for a Markdown video definition and ask a human to approve it or request changes.",
17
17
  inputSchema: z.object({
18
18
  path: z.string().describe("Absolute path to a .md file"),
19
19
  }),
@@ -23,9 +23,9 @@ registerAppTool(
23
23
  },
24
24
  async ({ path }) => {
25
25
  try {
26
- const preview = await readMarkdownPreview(path);
26
+ const preview = await ensureVideoPreview(path);
27
27
  return {
28
- content: [{ type: "text", text: `Markdown ready for review: ${preview.path}` }],
28
+ content: [{ type: "text", text: `Video ready for review: ${preview.path}` }],
29
29
  structuredContent: { ...preview, resourceUri: RESOURCE_URI },
30
30
  _meta: {
31
31
  ui: { resourceUri: RESOURCE_URI, props: preview },
@@ -45,7 +45,7 @@ registerAppTool(
45
45
  server,
46
46
  SUBMIT_TOOL_NAME,
47
47
  {
48
- description: "Submit the review decision from the Markdown preview UI.",
48
+ description: "Submit the review decision from the Markcut video preview UI.",
49
49
  inputSchema: z.object({
50
50
  decision: z.enum(["approved", "changes_requested"]),
51
51
  feedback: z.string().optional(),
@@ -72,29 +72,17 @@ registerAppTool(
72
72
 
73
73
  registerAppResource(
74
74
  server,
75
- "MarkCut Markdown review",
75
+ "Markcut video review",
76
76
  RESOURCE_URI,
77
77
  {
78
78
  _meta: {
79
79
  ui: {
80
80
  prefersBorder: true,
81
- csp: { connectDomains: [], resourceDomains: [] },
81
+ csp: { connectDomains: [PLAYER_ORIGIN], resourceDomains: [], frameDomains: [PLAYER_ORIGIN] },
82
82
  },
83
83
  },
84
84
  },
85
- async () => ({
86
- contents: [{
87
- uri: RESOURCE_URI,
88
- mimeType: RESOURCE_MIME_TYPE,
89
- text: buildPreviewHtml({ path: "", markdown: "" }),
90
- _meta: {
91
- ui: {
92
- prefersBorder: true,
93
- csp: { connectDomains: [], resourceDomains: [] },
94
- },
95
- },
96
- }],
97
- }),
85
+ async () => buildPreviewResourceResult(),
98
86
  );
99
87
 
100
88
  await server.connect(new StdioServerTransport());
@@ -1158,6 +1158,18 @@ Edit request: ${text}`;
1158
1158
  return;
1159
1159
  }
1160
1160
 
1161
+ // API: lightweight readiness probe for embedded MCP App previews.
1162
+ if (path === "/api/ready" && req.method === "GET") {
1163
+ const ready = compiledRootCache.has(variantLabel);
1164
+ res.writeHead(200, {
1165
+ "Content-Type": "application/json",
1166
+ "Cache-Control": "no-store",
1167
+ "Access-Control-Allow-Origin": "*",
1168
+ });
1169
+ res.end(JSON.stringify({ ready }));
1170
+ return;
1171
+ }
1172
+
1161
1173
  // API: Get video.json data for a specific variant
1162
1174
  if (path === "/api/video-data") {
1163
1175
  try {
@@ -3,7 +3,16 @@ import { mkdtempSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import vm from "node:vm";
6
- import { buildPreviewHtml, readMarkdownPreview, reviewMessage, reviewResult, validateMarkdownPath } from "../src/mcp/preview.mjs";
6
+ import {
7
+ buildPreviewHtml,
8
+ buildPreviewResourceResult,
9
+ PLAYER_ORIGIN,
10
+ RESOURCE_MIME_TYPE,
11
+ RESOURCE_URI,
12
+ reviewMessage,
13
+ reviewResult,
14
+ validateMarkdownPath,
15
+ } from "../src/mcp/preview.mjs";
7
16
  import { buildPreviewBridgeScript } from "../src/mcp/bridge.mjs";
8
17
 
9
18
  describe("MarkCut MCP Markdown preview", () => {
@@ -12,21 +21,29 @@ describe("MarkCut MCP Markdown preview", () => {
12
21
  expect(() => validateMarkdownPath("/tmp/notes.txt")).toThrow(/Markdown/);
13
22
  });
14
23
 
15
- it("reads Markdown and builds a reviewable UI resource", async () => {
16
- const directory = mkdtempSync(join(tmpdir(), "markcut-mcp-"));
17
- const path = join(directory, "review.md");
18
- writeFileSync(path, "# Hello\n\nThis is a review.");
19
- const preview = await readMarkdownPreview(path);
20
- const html = buildPreviewHtml(preview);
21
- expect(preview.markdown).toContain("# Hello");
22
- expect(html).toContain("This is a review.");
24
+ it("builds a video-player review UI instead of rendering Markdown", () => {
25
+ const html = buildPreviewHtml();
26
+ expect(html).toContain('id="video-player"');
27
+ expect(html).toContain("Review video");
23
28
  expect(html).toContain("Approve");
24
29
  expect(html).toContain("Request changes");
25
- expect(html).toContain("ui/initialize");
26
30
  expect(html).toContain("ui/notifications/tool-result");
27
- expect(html).toContain("tools/call");
28
- expect(html).toContain("ui/message");
29
- expect(html).toContain("review.md");
31
+ expect(html).toContain("previewUrl");
32
+ expect(html).not.toContain("<article>");
33
+ });
34
+
35
+ it("returns Codex-compatible private no-cache resource metadata", () => {
36
+ const result = buildPreviewResourceResult();
37
+ expect(result.ttlMs).toBe(0);
38
+ expect(result.cacheScope).toBe("private");
39
+ expect(result.contents).toHaveLength(1);
40
+ expect(result.contents[0]).toMatchObject({
41
+ uri: RESOURCE_URI,
42
+ mimeType: RESOURCE_MIME_TYPE,
43
+ });
44
+ expect(result.contents[0].text).toContain("ui/initialize");
45
+ expect(result.contents[0].text).toContain("ui/notifications/initialized");
46
+ expect(result.contents[0]._meta.ui.csp.frameDomains).toEqual([PLAYER_ORIGIN]);
30
47
  });
31
48
 
32
49
  it("returns approved", () => {
@@ -58,7 +75,7 @@ describe("MarkCut MCP Markdown preview", () => {
58
75
  id: 1,
59
76
  method: "ui/initialize",
60
77
  params: {
61
- appInfo: { name: "Markcut Preview", version: "1.0.0" },
78
+ appInfo: { name: "Markcut Preview", version: "1.1.0" },
62
79
  appCapabilities: {},
63
80
  protocolVersion: "2026-01-26",
64
81
  },
@@ -76,6 +93,11 @@ describe("MarkCut MCP Markdown preview", () => {
76
93
  await tick();
77
94
  expect(harness.messages[1]).toMatchObject({ method: "ui/notifications/initialized" });
78
95
 
96
+ harness.toolResult({ path: "/tmp/video.md", previewUrl: PLAYER_ORIGIN + "/" });
97
+ await tick();
98
+ expect(harness.elements.player.src).toBe(PLAYER_ORIGIN + "/");
99
+ expect(harness.elements.pathLabel.textContent).toBe("/tmp/video.md");
100
+
79
101
  harness.elements.approve.onclick();
80
102
  expect(harness.messages[2]).toMatchObject({ id: 2, method: "tools/call" });
81
103
  expect(harness.messages).not.toContainEqual(expect.objectContaining({ method: "ui/message" }));
@@ -114,23 +136,30 @@ function createBridgeHarness() {
114
136
  const elements = {
115
137
  feedback: element(),
116
138
  status: element(),
117
- article: element(),
139
+ player: element(),
118
140
  pathLabel: element(),
119
141
  approve: element(),
120
142
  changes: element(),
121
143
  };
122
144
  const parent = { postMessage(message: any) { messages.push(message); } };
123
145
  const document = {
124
- getElementById(id: string) { return id === "feedback" ? elements.feedback : id === "status" ? elements.status : id === "approve" ? elements.approve : elements.changes; },
125
- querySelector(selector: string) { return selector === "article" ? elements.article : elements.pathLabel; },
146
+ getElementById(id: string) {
147
+ if (id === "feedback") return elements.feedback;
148
+ if (id === "status") return elements.status;
149
+ if (id === "video-player") return elements.player;
150
+ if (id === "path-label") return elements.pathLabel;
151
+ if (id === "approve") return elements.approve;
152
+ return elements.changes;
153
+ },
126
154
  };
127
155
  const window = { parent, addEventListener(_type: string, listener: (event: any) => void) { listeners.push(listener); } };
128
- const context = vm.createContext({ window, document });
156
+ const context = vm.createContext({ window, document, setTimeout, fetch: async () => ({ ok: true, json: async () => ({ ready: true }) }) });
129
157
  return {
130
158
  messages,
131
159
  elements,
132
160
  run() { vm.runInContext(buildPreviewBridgeScript("markcut.preview.submit"), context); },
133
161
  dispatch(event: any) { listeners.forEach((listener) => listener(event)); },
134
162
  respond(message: any) { this.dispatch({ source: parent, data: { jsonrpc: "2.0", ...message } }); },
163
+ toolResult(structuredContent: any) { this.dispatch({ source: parent, data: { jsonrpc: "2.0", method: "ui/notifications/tool-result", params: { result: { structuredContent } } } }); },
135
164
  };
136
165
  }