@lalalic/markcut 3.2.9 → 3.2.11
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 +3 -2
- package/src/mcp/bridge.mjs +1 -1
- package/src/mcp/preview.mjs +1 -1
- package/src/mcp/server.mjs +85 -56
- package/tests/mcp-preview.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lalalic/markcut",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.11",
|
|
4
4
|
"description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"markcut": "bin/markcut"
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@babel/standalone": "^8.0.3",
|
|
41
|
-
"@modelcontextprotocol/
|
|
41
|
+
"@modelcontextprotocol/ext-apps": "^2.0.3",
|
|
42
|
+
"@modelcontextprotocol/server": "^2.3.1",
|
|
42
43
|
"@remotion/cli": "4.0.469",
|
|
43
44
|
"@remotion/media-parser": "4.0.469",
|
|
44
45
|
"@remotion/media-utils": "4.0.469",
|
package/src/mcp/bridge.mjs
CHANGED
|
@@ -8,7 +8,7 @@ function rpcRequest(method,params){const id=++nextRequestId;send({jsonrpc:'2.0',
|
|
|
8
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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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');}}
|
|
9
9
|
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
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 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});
|
|
11
|
+
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
12
|
initializeBridge();
|
|
13
13
|
document.getElementById('approve').onclick=()=>submit('approved');document.getElementById('changes').onclick=()=>submit('changes_requested');
|
|
14
14
|
`;
|
package/src/mcp/preview.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { buildPreviewBridgeScript } from "./bridge.mjs";
|
|
|
4
4
|
|
|
5
5
|
export const TOOL_NAME = "markcut.preview";
|
|
6
6
|
export const SUBMIT_TOOL_NAME = "markcut.preview.submit";
|
|
7
|
-
export const RESOURCE_URI = "ui://markcut/markdown-preview.html";
|
|
7
|
+
export const RESOURCE_URI = "ui://markcut/markdown-preview-v2.html";
|
|
8
8
|
|
|
9
9
|
export function validateMarkdownPath(value) {
|
|
10
10
|
if (typeof value !== "string" || !isAbsolute(value)) {
|
package/src/mcp/server.mjs
CHANGED
|
@@ -1,71 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
ReadResourceRequestSchema,
|
|
7
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
4
|
+
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
|
5
|
+
import { z } from "zod";
|
|
8
6
|
import {
|
|
9
7
|
TOOL_NAME, SUBMIT_TOOL_NAME, RESOURCE_URI, buildPreviewHtml, readMarkdownPreview, reviewResult,
|
|
10
8
|
} from "./preview.mjs";
|
|
11
9
|
|
|
12
|
-
const server = new
|
|
13
|
-
{ name: "markcut", version: "3.2.0" },
|
|
14
|
-
{ capabilities: { tools: {}, resources: {} } },
|
|
15
|
-
);
|
|
10
|
+
const server = new McpServer({ name: "markcut", version: "3.2.0" });
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
registerAppTool(
|
|
13
|
+
server,
|
|
14
|
+
TOOL_NAME,
|
|
15
|
+
{
|
|
20
16
|
description: "Ask a human to review a Markdown file and return an approval or requested changes.",
|
|
21
|
-
inputSchema: {
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
inputSchema: z.object({
|
|
18
|
+
path: z.string().describe("Absolute path to a .md file"),
|
|
19
|
+
}),
|
|
20
|
+
_meta: {
|
|
21
|
+
ui: { resourceUri: RESOURCE_URI },
|
|
24
22
|
},
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
23
|
+
},
|
|
24
|
+
async ({ path }) => {
|
|
25
|
+
try {
|
|
26
|
+
const preview = await readMarkdownPreview(path);
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: `Markdown ready for review: ${preview.path}` }],
|
|
29
|
+
structuredContent: { ...preview, resourceUri: RESOURCE_URI },
|
|
30
|
+
_meta: {
|
|
31
|
+
ui: { resourceUri: RESOURCE_URI, props: preview },
|
|
32
|
+
"ui/resourceUri": RESOURCE_URI,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
isError: true,
|
|
38
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
registerAppTool(
|
|
45
|
+
server,
|
|
46
|
+
SUBMIT_TOOL_NAME,
|
|
47
|
+
{
|
|
28
48
|
description: "Submit the review decision from the Markdown preview UI.",
|
|
29
|
-
inputSchema: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
required: ["decision"], additionalProperties: false,
|
|
49
|
+
inputSchema: z.object({
|
|
50
|
+
decision: z.enum(["approved", "changes_requested"]),
|
|
51
|
+
feedback: z.string().optional(),
|
|
52
|
+
}),
|
|
53
|
+
_meta: {
|
|
54
|
+
ui: { visibility: ["app"] },
|
|
36
55
|
},
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const input = request.params.arguments ?? {};
|
|
52
|
-
if (request.params.name === SUBMIT_TOOL_NAME) {
|
|
53
|
-
const decision = reviewResult(input.decision, input.feedback);
|
|
54
|
-
return { content: [{ type: "text", text: JSON.stringify(decision) }], structuredContent: decision };
|
|
56
|
+
},
|
|
57
|
+
async ({ decision, feedback }) => {
|
|
58
|
+
try {
|
|
59
|
+
const result = reviewResult(decision, feedback);
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
62
|
+
structuredContent: result,
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
isError: true,
|
|
67
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
68
|
+
};
|
|
55
69
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
registerAppResource(
|
|
74
|
+
server,
|
|
75
|
+
"MarkCut Markdown review",
|
|
76
|
+
RESOURCE_URI,
|
|
77
|
+
{
|
|
78
|
+
_meta: {
|
|
79
|
+
ui: {
|
|
80
|
+
prefersBorder: true,
|
|
81
|
+
csp: { connectDomains: [], resourceDomains: [] },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
async () => ({
|
|
86
|
+
contents: [{
|
|
87
|
+
uri: RESOURCE_URI,
|
|
88
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
89
|
+
text: buildPreviewHtml({ path: "", markdown: "" }),
|
|
61
90
|
_meta: {
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
ui: {
|
|
92
|
+
prefersBorder: true,
|
|
93
|
+
csp: { connectDomains: [], resourceDomains: [] },
|
|
94
|
+
},
|
|
64
95
|
},
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
});
|
|
96
|
+
}],
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
70
99
|
|
|
71
100
|
await server.connect(new StdioServerTransport());
|
|
@@ -82,7 +82,7 @@ describe("MarkCut MCP Markdown preview", () => {
|
|
|
82
82
|
harness.respond({ id: 2, result: { structuredContent: { decision: "approved" } } });
|
|
83
83
|
await tick();
|
|
84
84
|
expect(harness.messages[3]).toMatchObject({ method: "ui/message" });
|
|
85
|
-
expect(new Set(harness.messages.filter((message) => "id" in message).map((message) => message.id)).size).toBe(
|
|
85
|
+
expect(new Set(harness.messages.filter((message) => "id" in message).map((message) => message.id)).size).toBe(3);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it("surfaces submit errors without sending a decision message and rejects invalid bridge messages", async () => {
|