slides-studio-mcp 0.1.0-beta.3 → 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 +2 -2
- 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
|
|
|
@@ -141,7 +141,7 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
141
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 });
|
|
142
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 });
|
|
143
143
|
|
|
144
|
-
register("create_project", "Create
|
|
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
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
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 });
|
|
147
147
|
register("delete_project", "Delete a project from browser storage.", z.object({ ...targetProject, projectId: id }).strict(), (args) => browserOperation(companion, "delete_project", args), { destructiveHint: true });
|
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
|
}
|