slides-studio-mcp 0.1.0-beta.2 → 0.1.0-beta.4
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 +23 -0
- package/package.json +1 -1
- package/skill/slide-studio/SKILL.md +11 -0
- package/src/call.mjs +124 -0
- package/src/cli.mjs +6 -1
- package/src/mcp-server.mjs +27 -26
- package/src/setup.mjs +4 -1
- package/src/stdio-server.mjs +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,29 @@ Local-first MCP companion for the hosted [Slide Studio test editor](https://slid
|
|
|
6
6
|
npx slides-studio-mcp@beta setup
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
If an agent is doing the setup itself, it should select its current client and run non-interactively:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx slides-studio-mcp@beta setup --client=codex --yes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Replace `codex` with `claude`, `hermes`, `opencode`, or `openclaw` for the agent doing the setup. OpenCode prints the version-appropriate JSON to merge into its config. Other detected clients are configured through their official CLI.
|
|
16
|
+
|
|
17
|
+
## Use it immediately without restarting
|
|
18
|
+
|
|
19
|
+
Some clients do not add a newly configured MCP server to the tool catalog of the already-running session. The same validated tool surface is available through the package CLI, so the current agent can continue immediately:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y slides-studio-mcp@beta call get_design_guidance
|
|
23
|
+
npx -y slides-studio-mcp@beta call list_editors
|
|
24
|
+
npx -y slides-studio-mcp@beta call list_tools --json '{"names":["add_slide","add_text"]}'
|
|
25
|
+
npx -y slides-studio-mcp@beta call create_project --json '{"name":"My presentation"}'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every MCP tool name and JSON argument shape works with `call`. The CLI-only `list_tools` helper lists all names compactly, or returns schemas for the requested names. The CLI automatically applies the guidance gate for each invocation and prints compact JSON. `render_slide` writes image output to a temporary `previewPath` instead of dumping base64 into the terminal.
|
|
29
|
+
|
|
30
|
+
Hermes can refresh native tools without restarting by running `/reload-mcp`, then `/reload-skills`. Claude may still need a new session to register a newly added plain stdio server, but its current session can use the CLI fallback instead of stopping.
|
|
31
|
+
|
|
9
32
|
The companion binds only to `127.0.0.1`. There is no hosted relay: projects remain in browser IndexedDB and local images remain on the user's computer.
|
|
10
33
|
|
|
11
34
|
Any MCP client can launch it with:
|
package/package.json
CHANGED
|
@@ -7,6 +7,17 @@ description: Create, edit, visually inspect, and export Slide Studio carousel sl
|
|
|
7
7
|
|
|
8
8
|
Use the Slide Studio MCP tools for all presentation changes. The hosted browser editor is the live visual surface; local files and rendered images pass only through the local companion.
|
|
9
9
|
|
|
10
|
+
If the native MCP tools are not registered in the current session, do not stop or ask for a restart. Use the same validated tools through the local CLI fallback:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx -y slides-studio-mcp@beta call get_design_guidance
|
|
14
|
+
npx -y slides-studio-mcp@beta call list_editors
|
|
15
|
+
npx -y slides-studio-mcp@beta call list_tools --json '{"names":["add_slide","add_text"]}'
|
|
16
|
+
npx -y slides-studio-mcp@beta call create_project --json '{"name":"My presentation"}'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Every tool accepts the same JSON arguments as MCP. Use `list_tools` without arguments for compact discovery or pass `{"names":[...]}` to retrieve selected schemas. Prefer `apply_operations` for batches. `render_slide` writes its returned image to a temporary local `previewPath`; inspect that file and remove the temporary directory after the review. Hermes can load the native tools in place with `/reload-mcp` and refresh this skill with `/reload-skills`.
|
|
20
|
+
|
|
10
21
|
Before the first mutation in a task, call `get_design_guidance`. The server intentionally rejects mutations until this guidance has been read.
|
|
11
22
|
|
|
12
23
|
Inspect the editor before editing and keep the returned IDs. Work on the requested project and slide; do not infer targets when multiple editors or projects are ambiguous. Prefer `apply_operations` for compact related changes, while preserving logical edit order.
|
package/src/call.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const CLI_PATH = fileURLToPath(new URL("cli.mjs", import.meta.url));
|
|
8
|
+
|
|
9
|
+
async function stdinText() {
|
|
10
|
+
if (process.stdin.isTTY) return "";
|
|
11
|
+
let value = "";
|
|
12
|
+
process.stdin.setEncoding("utf8");
|
|
13
|
+
for await (const chunk of process.stdin) value += chunk;
|
|
14
|
+
return value.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function parseArguments(arguments_) {
|
|
18
|
+
const [toolName, ...rest] = arguments_;
|
|
19
|
+
if (!toolName) throw new Error("Usage: slides-studio-mcp call <tool> [--json '{\"key\":\"value\"}']");
|
|
20
|
+
const inline = rest.find((value) => value.startsWith("--json="));
|
|
21
|
+
const flagIndex = rest.indexOf("--json");
|
|
22
|
+
const raw = inline?.slice("--json=".length)
|
|
23
|
+
|| (flagIndex >= 0 ? rest[flagIndex + 1] : rest.find((value) => !value.startsWith("--")))
|
|
24
|
+
|| (rest.includes("--stdin") ? await stdinText() : "")
|
|
25
|
+
|| "{}";
|
|
26
|
+
let args;
|
|
27
|
+
try { args = JSON.parse(raw); }
|
|
28
|
+
catch { throw new Error("--json must be a valid JSON object."); }
|
|
29
|
+
if (!args || Array.isArray(args) || typeof args !== "object") throw new Error("--json must be a JSON object.");
|
|
30
|
+
return { toolName, args };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function printableResult(toolName, result) {
|
|
34
|
+
const error = result.isError && result.content?.find((item) => item.type === "text")?.text;
|
|
35
|
+
if (error) throw new Error(error);
|
|
36
|
+
if (toolName === "get_design_guidance") return { guidance: result.content?.find((item) => item.type === "text")?.text || "" };
|
|
37
|
+
const image = result.content?.find((item) => item.type === "image" && item.data);
|
|
38
|
+
if (!image) return result.structuredContent ?? { content: result.content || [] };
|
|
39
|
+
const extension = image.mimeType === "image/jpeg" ? "jpg" : "png";
|
|
40
|
+
const directory = await mkdtemp(join(tmpdir(), "slide-studio-preview-"));
|
|
41
|
+
const previewPath = join(directory, `slide.${extension}`);
|
|
42
|
+
await writeFile(previewPath, Buffer.from(image.data, "base64"));
|
|
43
|
+
return { ...(result.structuredContent || {}), previewPath, temporary: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createRpc(child) {
|
|
47
|
+
let input = "";
|
|
48
|
+
let id = 0;
|
|
49
|
+
const pending = new Map();
|
|
50
|
+
child.stdout.setEncoding("utf8");
|
|
51
|
+
child.stdout.on("data", (chunk) => {
|
|
52
|
+
input += chunk;
|
|
53
|
+
let newline;
|
|
54
|
+
while ((newline = input.indexOf("\n")) >= 0) {
|
|
55
|
+
const line = input.slice(0, newline).trim();
|
|
56
|
+
input = input.slice(newline + 1);
|
|
57
|
+
if (!line) continue;
|
|
58
|
+
const message = JSON.parse(line);
|
|
59
|
+
const waiter = pending.get(message.id);
|
|
60
|
+
if (!waiter) continue;
|
|
61
|
+
pending.delete(message.id);
|
|
62
|
+
clearTimeout(waiter.timer);
|
|
63
|
+
message.error ? waiter.reject(new Error(message.error.message)) : waiter.resolve(message.result);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
child.once("exit", (code) => {
|
|
67
|
+
for (const waiter of pending.values()) {
|
|
68
|
+
clearTimeout(waiter.timer);
|
|
69
|
+
waiter.reject(new Error(`Local MCP process exited before replying${code == null ? "" : ` (${code})`}.`));
|
|
70
|
+
}
|
|
71
|
+
pending.clear();
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
notify(method, params = {}) { child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`); },
|
|
75
|
+
request(method, params = {}) {
|
|
76
|
+
const requestId = ++id;
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const timer = setTimeout(() => { pending.delete(requestId); reject(new Error(`Timed out waiting for ${method}.`)); }, 120_000);
|
|
79
|
+
pending.set(requestId, { resolve, reject, timer });
|
|
80
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: requestId, method, params })}\n`);
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function stopChild(child) {
|
|
87
|
+
if (child.exitCode != null) return;
|
|
88
|
+
child.stdin.end();
|
|
89
|
+
const exited = new Promise((resolve) => child.once("exit", resolve));
|
|
90
|
+
const timeout = new Promise((resolve) => { const timer = setTimeout(resolve, 2000); timer.unref(); });
|
|
91
|
+
await Promise.race([exited, timeout]);
|
|
92
|
+
if (child.exitCode == null) child.kill("SIGTERM");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function runCall(arguments_) {
|
|
96
|
+
const { toolName, args } = await parseArguments(arguments_);
|
|
97
|
+
const child = spawn(process.execPath, [CLI_PATH, "serve"], { cwd: process.cwd(), env: process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
98
|
+
let errors = "";
|
|
99
|
+
child.stderr.setEncoding("utf8");
|
|
100
|
+
child.stderr.on("data", (chunk) => { errors = `${errors}${chunk}`.slice(-4000); });
|
|
101
|
+
const rpc = createRpc(child);
|
|
102
|
+
try {
|
|
103
|
+
await rpc.request("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "Slide Studio CLI fallback", version: "1" } });
|
|
104
|
+
rpc.notify("notifications/initialized");
|
|
105
|
+
if (toolName === "list_tools") {
|
|
106
|
+
const listed = await rpc.request("tools/list");
|
|
107
|
+
const requested = Array.isArray(args.names) ? new Set(args.names.map(String)) : null;
|
|
108
|
+
const tools = listed.tools.filter((tool) => !requested || requested.has(tool.name)).map((tool) => ({
|
|
109
|
+
name: tool.name,
|
|
110
|
+
description: tool.description,
|
|
111
|
+
...(requested ? { inputSchema: tool.inputSchema } : {}),
|
|
112
|
+
}));
|
|
113
|
+
process.stdout.write(`${JSON.stringify({ tools }, null, 2)}\n`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (toolName !== "get_design_guidance") await rpc.request("tools/call", { name: "get_design_guidance", arguments: {} });
|
|
117
|
+
const result = await rpc.request("tools/call", { name: toolName, arguments: args });
|
|
118
|
+
process.stdout.write(`${JSON.stringify(await printableResult(toolName, result), null, 2)}\n`);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`${error.message}${errors ? `\n${errors.trim()}` : ""}`);
|
|
121
|
+
} finally {
|
|
122
|
+
await stopChild(child);
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -15,6 +15,11 @@ async function main() {
|
|
|
15
15
|
await runSetup(arguments_);
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
+
if (command === "call") {
|
|
19
|
+
const { runCall } = await import("./call.mjs");
|
|
20
|
+
await runCall(arguments_);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
18
23
|
if (command === "doctor") {
|
|
19
24
|
const health = await companionDoctor();
|
|
20
25
|
process.stdout.write(`${JSON.stringify({ package: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, editor: TEST_EDITOR_URL, daemon: health }, null, 2)}\n`);
|
|
@@ -25,7 +30,7 @@ async function main() {
|
|
|
25
30
|
return;
|
|
26
31
|
}
|
|
27
32
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
28
|
-
process.stdout.write(`Slide Studio MCP ${PACKAGE_VERSION}\n\nUsage:\n slides-studio-mcp serve\n slides-studio-mcp setup [--client=claude,codex,hermes,opencode,openclaw] [--yes] [--dry-run]\n slides-studio-mcp doctor\n slides-studio-mcp version\n`);
|
|
33
|
+
process.stdout.write(`Slide Studio MCP ${PACKAGE_VERSION}\n\nUsage:\n slides-studio-mcp serve\n slides-studio-mcp setup [--client=claude,codex,hermes,opencode,openclaw] [--yes] [--dry-run]\n slides-studio-mcp call <tool> [--json '{"key":"value"}'] [--stdin]\n slides-studio-mcp doctor\n slides-studio-mcp version\n`);
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
31
36
|
throw new Error(`Unknown command: ${command}`);
|
package/src/mcp-server.mjs
CHANGED
|
@@ -33,7 +33,7 @@ function textResult(value, summary = value) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function compactMutation(value) {
|
|
36
|
-
const keys = ["projectId", "slideId", "revision", "createdSlideId", "createdTextId", "createdImageId", "assetId", "deletedProjectId", "deletedSlideId", "deletedLayerIds", "updatedTextIds", "updatedImageIds", "applied", "path", "bytes"];
|
|
36
|
+
const keys = ["projectId", "slideId", "revision", "opened", "createdSlideId", "createdTextId", "createdImageId", "assetId", "deletedProjectId", "deletedSlideId", "deletedLayerIds", "updatedTextIds", "updatedImageIds", "applied", "path", "bytes"];
|
|
37
37
|
return Object.fromEntries(keys.flatMap((key) => value?.[key] == null ? [] : [[key, value[key]]]));
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -109,9 +109,10 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function register(name, description, inputSchema, handler, annotations = {}) {
|
|
112
|
+
const normalizedAnnotations = { openWorldHint: false, ...annotations };
|
|
112
113
|
const guidanceExempt = new Set(["select_editor", "open_project", "set_view", "show_notification"]);
|
|
113
|
-
definitions.set(name, { inputSchema, handler, mutating: !
|
|
114
|
-
server.registerTool(name, { title: name.split("_").map((part) => part[0].toUpperCase() + part.slice(1)).join(" "), description, inputSchema, annotations }, async (args, context) => {
|
|
114
|
+
definitions.set(name, { inputSchema, handler, mutating: !normalizedAnnotations.readOnlyHint && !guidanceExempt.has(name) });
|
|
115
|
+
server.registerTool(name, { title: name.split("_").map((part) => part[0].toUpperCase() + part.slice(1)).join(" "), description, inputSchema, annotations: normalizedAnnotations }, async (args, context) => {
|
|
115
116
|
await identify(context);
|
|
116
117
|
if (definitions.get(name).mutating && !guidanceRead) throw new Error("Call get_design_guidance before changing slides. This one-time step prevents avoidable clipping and unattractive defaults.");
|
|
117
118
|
const value = await handler(args, context);
|
|
@@ -136,36 +137,36 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
136
137
|
}, { readOnlyHint: true, idempotentHint: true });
|
|
137
138
|
|
|
138
139
|
register("list_editors", "List connected browser tabs and show which tab this MCP session targets.", z.object({}).strict(), () => companion.call("list_editors"), { readOnlyHint: true });
|
|
139
|
-
register("select_editor", "Select a connected browser tab for this MCP session.", z.object({ editorId: id }).strict(), ({ editorId }) => companion.call("select_editor", { editorId }), { idempotentHint: true });
|
|
140
|
+
register("select_editor", "Select a connected browser tab for this MCP session.", z.object({ editorId: id }).strict(), ({ editorId }) => companion.call("select_editor", { editorId }), { destructiveHint: false, idempotentHint: true });
|
|
140
141
|
register("inspect_editor", "Inspect projects, slides, assets, and every text/image layer without returning image bytes.", z.object({ ...targetSlide, includeAllProjects: z.boolean().default(true) }).strict(), (args) => browserOperation(companion, "inspect_editor", args), { readOnlyHint: true });
|
|
141
|
-
register("show_notification", "Show a short visual notification in the connected editor for status or marketing demos.", z.object({ message: z.string().min(1).max(240), tone: z.enum(["agent", "success", "info", "error"]).default("agent") }).strict(), (args) => companion.call("notify", args), { idempotentHint: false });
|
|
142
|
+
register("show_notification", "Show a short visual notification in the connected editor for status or marketing demos.", z.object({ message: z.string().min(1).max(240), tone: z.enum(["agent", "success", "info", "error"]).default("agent") }).strict(), (args) => companion.call("notify", args), { destructiveHint: false, idempotentHint: false });
|
|
142
143
|
|
|
143
|
-
register("create_project", "Create
|
|
144
|
-
register("open_project", "Open a project and optionally a specific slide without changing content.", z.object({ projectId: id, slideId: optionalId }).strict(), (args) => browserOperation(companion, "open_project", args), { idempotentHint: true });
|
|
145
|
-
register("update_project", "Rename a project.", z.object({ ...targetProject, name: z.string().min(1).max(160) }).strict(), (args) => browserOperation(companion, "update_project", args));
|
|
144
|
+
register("create_project", "Create an empty project. If the dashboard is visible, its card appears live; adding the first slide opens the editor.", z.object({ name: z.string().min(1).max(160) }).strict(), (args) => browserOperation(companion, "create_project", args), { destructiveHint: false });
|
|
145
|
+
register("open_project", "Open a project and optionally a specific slide without changing content.", z.object({ projectId: id, slideId: optionalId }).strict(), (args) => browserOperation(companion, "open_project", args), { destructiveHint: false, idempotentHint: true });
|
|
146
|
+
register("update_project", "Rename a project.", z.object({ ...targetProject, name: z.string().min(1).max(160) }).strict(), (args) => browserOperation(companion, "update_project", args), { destructiveHint: true });
|
|
146
147
|
register("delete_project", "Delete a project from browser storage.", z.object({ ...targetProject, projectId: id }).strict(), (args) => browserOperation(companion, "delete_project", args), { destructiveHint: true });
|
|
147
148
|
|
|
148
|
-
register("add_slide", "Add and open a slide using a solid color or local background image path.", z.object({ ...targetProject, name: z.string().max(160).optional(), index: z.number().int().min(0).optional(), backgroundColor: color.optional(), backgroundPath: z.string().min(1).optional() }).strict(), (args) => browserOperation(companion, "add_slide", args));
|
|
149
|
-
register("update_slide", "Rename a slide, replace its background, or change background pan/zoom. The browser opens this slide.", z.object({ ...targetSlide, name: z.string().max(160).optional(), backgroundColor: color.optional(), backgroundPath: z.string().min(1).optional(), imageScale: z.number().min(1).max(3).optional(), imageX: unit.optional(), imageY: unit.optional() }).strict(), (args) => browserOperation(companion, "update_slide", args));
|
|
150
|
-
register("duplicate_slide", "Duplicate a slide with all layers and open the copy.", z.object({ ...targetSlide, name: z.string().max(160).optional() }).strict(), (args) => browserOperation(companion, "duplicate_slide", args));
|
|
151
|
-
register("reorder_slides", "Set the complete slide order using every slide ID exactly once.", z.object({ ...targetProject, slideIds: z.array(id).min(1) }).strict(), (args) => browserOperation(companion, "reorder_slides", args));
|
|
149
|
+
register("add_slide", "Add and open a slide using a solid color or local background image path.", z.object({ ...targetProject, name: z.string().max(160).optional(), index: z.number().int().min(0).optional(), backgroundColor: color.optional(), backgroundPath: z.string().min(1).optional() }).strict(), (args) => browserOperation(companion, "add_slide", args), { destructiveHint: false });
|
|
150
|
+
register("update_slide", "Rename a slide, replace its background, or change background pan/zoom. The browser opens this slide.", z.object({ ...targetSlide, name: z.string().max(160).optional(), backgroundColor: color.optional(), backgroundPath: z.string().min(1).optional(), imageScale: z.number().min(1).max(3).optional(), imageX: unit.optional(), imageY: unit.optional() }).strict(), (args) => browserOperation(companion, "update_slide", args), { destructiveHint: true });
|
|
151
|
+
register("duplicate_slide", "Duplicate a slide with all layers and open the copy.", z.object({ ...targetSlide, name: z.string().max(160).optional() }).strict(), (args) => browserOperation(companion, "duplicate_slide", args), { destructiveHint: false });
|
|
152
|
+
register("reorder_slides", "Set the complete slide order using every slide ID exactly once.", z.object({ ...targetProject, slideIds: z.array(id).min(1) }).strict(), (args) => browserOperation(companion, "reorder_slides", args), { destructiveHint: true });
|
|
152
153
|
register("delete_slide", "Delete one slide.", z.object({ ...targetSlide, slideId: id }).strict(), (args) => browserOperation(companion, "delete_slide", args), { destructiveHint: true });
|
|
153
154
|
|
|
154
|
-
register("add_text", "Add a text layer. Coordinates and dimensions are normalized to the 9:16 canvas; attractive defaults use generous bounds and per-line boxes.", z.object({ ...targetSlide, ...textFields, text: z.string().min(1).max(4000) }).strict(), (args) => browserOperation(companion, "add_text", args));
|
|
155
|
-
register("update_text", "Update one or more text layers, including content, geometry, color, style, alignment, rotation, and stacking.", z.object({ ...targetSlide, updates: z.array(z.object({ id, ...textFields }).strict()).min(1).max(100) }).strict(), (args) => browserOperation(companion, "update_text", args));
|
|
155
|
+
register("add_text", "Add a text layer. Coordinates and dimensions are normalized to the 9:16 canvas; attractive defaults use generous bounds and per-line boxes.", z.object({ ...targetSlide, ...textFields, text: z.string().min(1).max(4000) }).strict(), (args) => browserOperation(companion, "add_text", args), { destructiveHint: false });
|
|
156
|
+
register("update_text", "Update one or more text layers, including content, geometry, color, style, alignment, rotation, and stacking.", z.object({ ...targetSlide, updates: z.array(z.object({ id, ...textFields }).strict()).min(1).max(100) }).strict(), (args) => browserOperation(companion, "update_text", args), { destructiveHint: true });
|
|
156
157
|
|
|
157
|
-
register("import_asset", "Import a local image file into the active project's reusable asset library. Image bytes stay local.", z.object({ ...targetSlide, path: z.string().min(1), name: z.string().max(160).optional() }).strict(), (args) => browserOperation(companion, "import_asset", args));
|
|
158
|
-
register("update_asset", "Rename a reusable image asset.", z.object({ ...targetProject, assetId: id, name: z.string().min(1).max(160) }).strict(), (args) => browserOperation(companion, "update_asset", args));
|
|
158
|
+
register("import_asset", "Import a local image file into the active project's reusable asset library. Image bytes stay local.", z.object({ ...targetSlide, path: z.string().min(1), name: z.string().max(160).optional() }).strict(), (args) => browserOperation(companion, "import_asset", args), { destructiveHint: false });
|
|
159
|
+
register("update_asset", "Rename a reusable image asset.", z.object({ ...targetProject, assetId: id, name: z.string().min(1).max(160) }).strict(), (args) => browserOperation(companion, "update_asset", args), { destructiveHint: true });
|
|
159
160
|
register("delete_asset", "Delete an asset and every placed instance that references it.", z.object({ ...targetProject, assetId: id }).strict(), (args) => browserOperation(companion, "delete_asset", args), { destructiveHint: true });
|
|
160
|
-
register("add_image", "Place an imported asset as an image layer and optionally set geometry, crop, rotation, and stacking.", z.object({ ...targetSlide, assetId: id, ...imageFields }).strict(), (args) => browserOperation(companion, "add_image", args));
|
|
161
|
-
register("update_image", "Update one or more placed image layers, including geometry, crop, rotation, and stacking.", z.object({ ...targetSlide, updates: z.array(z.object({ id, ...imageFields }).strict()).min(1).max(100) }).strict(), (args) => browserOperation(companion, "update_image", args));
|
|
161
|
+
register("add_image", "Place an imported asset as an image layer and optionally set geometry, crop, rotation, and stacking.", z.object({ ...targetSlide, assetId: id, ...imageFields }).strict(), (args) => browserOperation(companion, "add_image", args), { destructiveHint: false });
|
|
162
|
+
register("update_image", "Update one or more placed image layers, including geometry, crop, rotation, and stacking.", z.object({ ...targetSlide, updates: z.array(z.object({ id, ...imageFields }).strict()).min(1).max(100) }).strict(), (args) => browserOperation(companion, "update_image", args), { destructiveHint: true });
|
|
162
163
|
|
|
163
164
|
register("delete_layers", "Delete text and/or image layers by ID.", z.object({ ...targetSlide, layerIds: z.array(id).min(1).max(200) }).strict(), (args) => browserOperation(companion, "delete_layers", args), { destructiveHint: true });
|
|
164
|
-
register("duplicate_layers", "Duplicate text and/or image layers with an optional normalized offset.", z.object({ ...targetSlide, layerIds: z.array(id).min(1).max(100), offsetX: z.number().min(-1).max(1).optional(), offsetY: z.number().min(-1).max(1).optional() }).strict(), (args) => browserOperation(companion, "duplicate_layers", args));
|
|
165
|
-
register("reorder_layers", "Set the complete back-to-front layer order using every layer ID exactly once.", z.object({ ...targetSlide, layerIds: z.array(id).min(1).max(300) }).strict(), (args) => browserOperation(companion, "reorder_layers", args));
|
|
166
|
-
register("undo", "Undo the latest project edit.", z.object(targetSlide).strict(), (args) => browserOperation(companion, "undo", args));
|
|
167
|
-
register("redo", "Redo the latest undone project edit.", z.object(targetSlide).strict(), (args) => browserOperation(companion, "redo", args));
|
|
168
|
-
register("set_view", "Open a project/slide and control editor-only canvas zoom or TikTok safe-area overlay.", z.object({ ...targetSlide, canvasZoom: z.number().min(0.2).max(3).optional(), showTikTokOverlay: z.boolean().optional() }).strict(), (args) => browserOperation(companion, "set_view", args), { idempotentHint: true });
|
|
165
|
+
register("duplicate_layers", "Duplicate text and/or image layers with an optional normalized offset.", z.object({ ...targetSlide, layerIds: z.array(id).min(1).max(100), offsetX: z.number().min(-1).max(1).optional(), offsetY: z.number().min(-1).max(1).optional() }).strict(), (args) => browserOperation(companion, "duplicate_layers", args), { destructiveHint: false });
|
|
166
|
+
register("reorder_layers", "Set the complete back-to-front layer order using every layer ID exactly once.", z.object({ ...targetSlide, layerIds: z.array(id).min(1).max(300) }).strict(), (args) => browserOperation(companion, "reorder_layers", args), { destructiveHint: true });
|
|
167
|
+
register("undo", "Undo the latest project edit.", z.object(targetSlide).strict(), (args) => browserOperation(companion, "undo", args), { destructiveHint: true });
|
|
168
|
+
register("redo", "Redo the latest undone project edit.", z.object(targetSlide).strict(), (args) => browserOperation(companion, "redo", args), { destructiveHint: true });
|
|
169
|
+
register("set_view", "Open a project/slide and control editor-only canvas zoom or TikTok safe-area overlay.", z.object({ ...targetSlide, canvasZoom: z.number().min(0.2).max(3).optional(), showTikTokOverlay: z.boolean().optional() }).strict(), (args) => browserOperation(companion, "set_view", args), { destructiveHint: false, idempotentHint: true });
|
|
169
170
|
|
|
170
171
|
register("render_slide", "Render and return the actual slide image for visual inspection. This does not persist the rendered file.", z.object({ ...targetSlide, width: z.number().int().min(180).max(1080).default(540), format: z.enum(["png", "jpeg"]).default("png"), quality: z.number().min(0.4).max(1).default(0.9) }).strict(), async (args) => {
|
|
171
172
|
const rendered = await browserOperation(companion, "render_slide", args);
|
|
@@ -181,7 +182,7 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
181
182
|
const path = absolutePath(outputPath);
|
|
182
183
|
await mkdir(dirname(path), { recursive: true });
|
|
183
184
|
return companion.call("write_export", { path, data: rendered.data, overwrite });
|
|
184
|
-
});
|
|
185
|
+
}, { destructiveHint: true });
|
|
185
186
|
|
|
186
187
|
register("export_project", "Render every slide at full resolution into a local directory. Existing files are protected unless overwrite=true.", z.object({ ...targetProject, outputDirectory: z.string().min(1), overwrite: z.boolean().default(false) }).strict(), async ({ outputDirectory, overwrite, ...target }) => {
|
|
187
188
|
const inspected = await browserOperation(companion, "inspect_editor", { ...target, includeAllProjects: false });
|
|
@@ -196,7 +197,7 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
196
197
|
files.push(await companion.call("write_export", { path, data: rendered.data, overwrite }));
|
|
197
198
|
}
|
|
198
199
|
return { projectId: inspected.project.id, outputDirectory: directory, fileCount: files.length, files };
|
|
199
|
-
});
|
|
200
|
+
}, { destructiveHint: true });
|
|
200
201
|
|
|
201
202
|
const batchTools = [...definitions.entries()].filter(([, definition]) => definition.mutating).map(([name]) => name).filter((name) => !["export_slide", "export_project"].includes(name));
|
|
202
203
|
register("apply_operations", "Apply many ordered editing operations in one compact tool call. Each edit still appears live in the browser.", z.object({ operations: z.array(z.object({ tool: z.enum(batchTools), arguments: z.record(z.string(), z.unknown()).default({}) }).strict()).min(1).max(100) }).strict(), async ({ operations }) => {
|
|
@@ -208,7 +209,7 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
208
209
|
items.push({ operation: await prepareOperation(companion, item.tool, args), label: operationLabel(item.tool) });
|
|
209
210
|
}
|
|
210
211
|
return companion.call("batch", { items });
|
|
211
|
-
});
|
|
212
|
+
}, { destructiveHint: true });
|
|
212
213
|
|
|
213
214
|
return server;
|
|
214
215
|
}
|
package/src/setup.mjs
CHANGED
|
@@ -89,7 +89,10 @@ export async function runSetup(arguments_) {
|
|
|
89
89
|
else process.stderr.write(`Could not configure ${client}; its command is printed above for manual setup.\n`);
|
|
90
90
|
}
|
|
91
91
|
const skillTargets = await installSkill();
|
|
92
|
-
process.stdout.write(`\nConfigured: ${configured.join(", ") || "none automatically"}\nSkill installed in:\n${skillTargets.map((value) => ` ${value}`).join("\n")}\n\nOpen ${TEST_EDITOR_URL}
|
|
92
|
+
process.stdout.write(`\nConfigured: ${configured.join(", ") || "none automatically"}\nSkill installed in:\n${skillTargets.map((value) => ` ${value}`).join("\n")}\n\nOpen ${TEST_EDITOR_URL} and click Connect AI.\n`);
|
|
93
|
+
process.stdout.write(`Current-session fallback (no restart): npx -y ${specifier} call list_editors\n`);
|
|
94
|
+
if (clients.includes("hermes")) process.stdout.write("Hermes can also refresh native tools in place with /reload-mcp and /reload-skills.\n");
|
|
95
|
+
if (clients.includes("claude")) process.stdout.write("Claude may require a new session for native MCP registration; use the CLI fallback immediately instead of stopping.\n");
|
|
93
96
|
if (clients.includes("opencode")) process.stdout.write("OpenCode currently uses its JSON config; merge the snippet printed above into opencode.json.\n");
|
|
94
97
|
return { clients, configured, skillTargets };
|
|
95
98
|
}
|
package/src/stdio-server.mjs
CHANGED
|
@@ -18,6 +18,6 @@ export async function serveMcp() {
|
|
|
18
18
|
process.once("SIGINT", () => void close().finally(() => process.exit()));
|
|
19
19
|
process.once("SIGTERM", () => void close().finally(() => process.exit()));
|
|
20
20
|
process.once("SIGHUP", () => void close().finally(() => process.exit()));
|
|
21
|
-
process.stdin.once("end", () => void close());
|
|
21
|
+
process.stdin.once("end", () => void close().finally(() => process.exit()));
|
|
22
22
|
return { handle, companion, close };
|
|
23
23
|
}
|