@lalalic/markcut 3.2.12 → 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 +1 -1
- package/src/mcp/bridge.mjs +4 -3
- package/src/mcp/preview.mjs +77 -38
- package/src/mcp/server.mjs +7 -7
- package/src/player/server.mjs +12 -0
- package/tests/mcp-preview.test.ts +25 -18
package/package.json
CHANGED
package/src/mcp/bridge.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
export function buildPreviewBridgeScript(submitToolName) {
|
|
2
2
|
return `
|
|
3
|
-
const feedback=document.getElementById('feedback'),status=document.getElementById('status'),
|
|
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
|
|
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.
|
|
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');
|
package/src/mcp/preview.mjs
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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/
|
|
10
|
+
export const RESOURCE_URI = "ui://markcut/video-preview-v3.html";
|
|
8
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;
|
|
9
20
|
|
|
10
21
|
export function buildPreviewResourceResult() {
|
|
11
22
|
return {
|
|
@@ -14,11 +25,15 @@ export function buildPreviewResourceResult() {
|
|
|
14
25
|
contents: [{
|
|
15
26
|
uri: RESOURCE_URI,
|
|
16
27
|
mimeType: RESOURCE_MIME_TYPE,
|
|
17
|
-
text: buildPreviewHtml(
|
|
28
|
+
text: buildPreviewHtml(),
|
|
18
29
|
_meta: {
|
|
19
30
|
ui: {
|
|
20
31
|
prefersBorder: true,
|
|
21
|
-
csp: {
|
|
32
|
+
csp: {
|
|
33
|
+
connectDomains: [PLAYER_ORIGIN],
|
|
34
|
+
resourceDomains: [],
|
|
35
|
+
frameDomains: [PLAYER_ORIGIN],
|
|
36
|
+
},
|
|
22
37
|
},
|
|
23
38
|
},
|
|
24
39
|
}],
|
|
@@ -35,13 +50,59 @@ export function validateMarkdownPath(value) {
|
|
|
35
50
|
return value;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
export async function
|
|
53
|
+
export async function ensureVideoPreview(path, options = {}) {
|
|
39
54
|
const validatedPath = validateMarkdownPath(path);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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");
|
|
44
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;
|
|
45
106
|
}
|
|
46
107
|
|
|
47
108
|
export function reviewResult(decision, feedback = "") {
|
|
@@ -59,38 +120,16 @@ export function reviewMessage(result) {
|
|
|
59
120
|
return {
|
|
60
121
|
jsonrpc: "2.0",
|
|
61
122
|
method: "ui/message",
|
|
62
|
-
params: {
|
|
63
|
-
role: "user",
|
|
64
|
-
content: [{ type: "text", text: JSON.stringify(normalized) }],
|
|
65
|
-
},
|
|
123
|
+
params: { role: "user", content: [{ type: "text", text: JSON.stringify(normalized) }] },
|
|
66
124
|
};
|
|
67
125
|
}
|
|
68
126
|
|
|
69
|
-
function
|
|
70
|
-
return value.replace(/[&<>"']/g, (character) => ({
|
|
71
|
-
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
72
|
-
})[character]);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function renderMarkdown(markdown) {
|
|
76
|
-
return markdown.split(/\r?\n/).map((line) => {
|
|
77
|
-
const escaped = escapeHtml(line);
|
|
78
|
-
if (line.startsWith("### ")) return `<h3>${escaped.slice(4)}</h3>`;
|
|
79
|
-
if (line.startsWith("## ")) return `<h2>${escaped.slice(3)}</h2>`;
|
|
80
|
-
if (line.startsWith("# ")) return `<h1>${escaped.slice(2)}</h1>`;
|
|
81
|
-
if (line.startsWith("- ")) return `<li>${escaped.slice(2)}</li>`;
|
|
82
|
-
return line.trim() ? `<p>${escaped}</p>` : "";
|
|
83
|
-
}).join("\n");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function buildPreviewHtml({ path = "", markdown = "" }) {
|
|
87
|
-
const safePath = escapeHtml(path);
|
|
88
|
-
const rendered = renderMarkdown(markdown);
|
|
127
|
+
export function buildPreviewHtml() {
|
|
89
128
|
return `<!doctype html>
|
|
90
|
-
<html><head><meta charset="utf-8"><title>
|
|
91
|
-
<style>body{font:15px system-ui,sans-serif;margin:0;
|
|
92
|
-
<body><main><h1>Review
|
|
93
|
-
<label for="feedback">Feedback (required for requested changes)</label><textarea id="feedback" placeholder="What should be changed?"></textarea><div id="status" role="status"
|
|
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>
|
|
94
133
|
<button id="approve">Approve</button><button id="changes">Request changes</button></main>
|
|
95
134
|
<script>${buildPreviewBridgeScript(SUBMIT_TOOL_NAME)}</script></body></html>`;
|
|
96
135
|
}
|
package/src/mcp/server.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
|
4
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, buildPreviewResourceResult,
|
|
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: "
|
|
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
|
|
26
|
+
const preview = await ensureVideoPreview(path);
|
|
27
27
|
return {
|
|
28
|
-
content: [{ type: "text", text: `
|
|
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
|
|
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,13 +72,13 @@ registerAppTool(
|
|
|
72
72
|
|
|
73
73
|
registerAppResource(
|
|
74
74
|
server,
|
|
75
|
-
"
|
|
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
|
},
|
package/src/player/server.mjs
CHANGED
|
@@ -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 {
|
|
@@ -6,7 +6,7 @@ import vm from "node:vm";
|
|
|
6
6
|
import {
|
|
7
7
|
buildPreviewHtml,
|
|
8
8
|
buildPreviewResourceResult,
|
|
9
|
-
|
|
9
|
+
PLAYER_ORIGIN,
|
|
10
10
|
RESOURCE_MIME_TYPE,
|
|
11
11
|
RESOURCE_URI,
|
|
12
12
|
reviewMessage,
|
|
@@ -21,21 +21,15 @@ describe("MarkCut MCP Markdown preview", () => {
|
|
|
21
21
|
expect(() => validateMarkdownPath("/tmp/notes.txt")).toThrow(/Markdown/);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it("
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const preview = await readMarkdownPreview(path);
|
|
29
|
-
const html = buildPreviewHtml(preview);
|
|
30
|
-
expect(preview.markdown).toContain("# Hello");
|
|
31
|
-
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");
|
|
32
28
|
expect(html).toContain("Approve");
|
|
33
29
|
expect(html).toContain("Request changes");
|
|
34
|
-
expect(html).toContain("ui/initialize");
|
|
35
30
|
expect(html).toContain("ui/notifications/tool-result");
|
|
36
|
-
expect(html).toContain("
|
|
37
|
-
expect(html).toContain("
|
|
38
|
-
expect(html).toContain("review.md");
|
|
31
|
+
expect(html).toContain("previewUrl");
|
|
32
|
+
expect(html).not.toContain("<article>");
|
|
39
33
|
});
|
|
40
34
|
|
|
41
35
|
it("returns Codex-compatible private no-cache resource metadata", () => {
|
|
@@ -49,6 +43,7 @@ describe("MarkCut MCP Markdown preview", () => {
|
|
|
49
43
|
});
|
|
50
44
|
expect(result.contents[0].text).toContain("ui/initialize");
|
|
51
45
|
expect(result.contents[0].text).toContain("ui/notifications/initialized");
|
|
46
|
+
expect(result.contents[0]._meta.ui.csp.frameDomains).toEqual([PLAYER_ORIGIN]);
|
|
52
47
|
});
|
|
53
48
|
|
|
54
49
|
it("returns approved", () => {
|
|
@@ -80,7 +75,7 @@ describe("MarkCut MCP Markdown preview", () => {
|
|
|
80
75
|
id: 1,
|
|
81
76
|
method: "ui/initialize",
|
|
82
77
|
params: {
|
|
83
|
-
appInfo: { name: "Markcut Preview", version: "1.
|
|
78
|
+
appInfo: { name: "Markcut Preview", version: "1.1.0" },
|
|
84
79
|
appCapabilities: {},
|
|
85
80
|
protocolVersion: "2026-01-26",
|
|
86
81
|
},
|
|
@@ -98,6 +93,11 @@ describe("MarkCut MCP Markdown preview", () => {
|
|
|
98
93
|
await tick();
|
|
99
94
|
expect(harness.messages[1]).toMatchObject({ method: "ui/notifications/initialized" });
|
|
100
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
|
+
|
|
101
101
|
harness.elements.approve.onclick();
|
|
102
102
|
expect(harness.messages[2]).toMatchObject({ id: 2, method: "tools/call" });
|
|
103
103
|
expect(harness.messages).not.toContainEqual(expect.objectContaining({ method: "ui/message" }));
|
|
@@ -136,23 +136,30 @@ function createBridgeHarness() {
|
|
|
136
136
|
const elements = {
|
|
137
137
|
feedback: element(),
|
|
138
138
|
status: element(),
|
|
139
|
-
|
|
139
|
+
player: element(),
|
|
140
140
|
pathLabel: element(),
|
|
141
141
|
approve: element(),
|
|
142
142
|
changes: element(),
|
|
143
143
|
};
|
|
144
144
|
const parent = { postMessage(message: any) { messages.push(message); } };
|
|
145
145
|
const document = {
|
|
146
|
-
getElementById(id: string) {
|
|
147
|
-
|
|
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
|
+
},
|
|
148
154
|
};
|
|
149
155
|
const window = { parent, addEventListener(_type: string, listener: (event: any) => void) { listeners.push(listener); } };
|
|
150
|
-
const context = vm.createContext({ window, document });
|
|
156
|
+
const context = vm.createContext({ window, document, setTimeout, fetch: async () => ({ ok: true, json: async () => ({ ready: true }) }) });
|
|
151
157
|
return {
|
|
152
158
|
messages,
|
|
153
159
|
elements,
|
|
154
160
|
run() { vm.runInContext(buildPreviewBridgeScript("markcut.preview.submit"), context); },
|
|
155
161
|
dispatch(event: any) { listeners.forEach((listener) => listener(event)); },
|
|
156
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 } } } }); },
|
|
157
164
|
};
|
|
158
165
|
}
|