slides-studio-mcp 0.1.0-beta.9 → 0.1.0
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 +12 -12
- package/guidance/design.md +3 -1
- package/package.json +2 -2
- package/skill/slide-studio/SKILL.md +6 -6
- package/src/agent-identity.mjs +48 -0
- package/src/cli.mjs +5 -4
- package/src/companion.mjs +3 -2
- package/src/config.mjs +2 -2
- package/src/daemon.mjs +10 -3
- package/src/mcp-server.mjs +6 -6
- package/src/setup.mjs +10 -9
- package/src/stdio-server.mjs +3 -2
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Slide Studio MCP
|
|
2
2
|
|
|
3
|
-
Local-first MCP companion for the hosted [Slide Studio
|
|
3
|
+
Local-first MCP companion for the hosted [Slide Studio editor](https://slides-editor.pages.dev). It exposes complete project, slide, text, image, layer, history, rendering, and export controls to any stdio MCP client while the editor remains in the browser.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx slides-studio-mcp@
|
|
6
|
+
npx slides-studio-mcp@latest setup
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
If an agent is doing the setup itself, it should select its current client and run non-interactively:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx slides-studio-mcp@
|
|
12
|
+
npx slides-studio-mcp@latest setup --client=codex --yes
|
|
13
13
|
```
|
|
14
14
|
|
|
15
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.
|
|
@@ -19,10 +19,10 @@ Replace `codex` with `claude`, `hermes`, `opencode`, or `openclaw` for the agent
|
|
|
19
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
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx -y slides-studio-mcp@
|
|
23
|
-
npx -y slides-studio-mcp@
|
|
24
|
-
npx -y slides-studio-mcp@
|
|
25
|
-
npx -y slides-studio-mcp@
|
|
22
|
+
npx -y slides-studio-mcp@latest call get_design_guidance
|
|
23
|
+
npx -y slides-studio-mcp@latest call list_editors
|
|
24
|
+
npx -y slides-studio-mcp@latest call begin_edit_session --json '{"editorId":"EDITOR_ID","purpose":"Build my deck"}'
|
|
25
|
+
npx -y slides-studio-mcp@latest call create_project --json '{"editSessionId":"SESSION_ID","name":"My presentation"}'
|
|
26
26
|
```
|
|
27
27
|
|
|
28
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.
|
|
@@ -38,15 +38,15 @@ The companion binds only to `127.0.0.1`. There is no hosted relay: projects rema
|
|
|
38
38
|
Any MCP client can launch it with:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
npx -y slides-studio-mcp@
|
|
41
|
+
npx -y slides-studio-mcp@latest serve
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Useful commands:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
npx slides-studio-mcp@
|
|
48
|
-
npx slides-studio-mcp@
|
|
49
|
-
npx slides-studio-mcp@
|
|
47
|
+
npx slides-studio-mcp@latest setup --dry-run
|
|
48
|
+
npx slides-studio-mcp@latest doctor
|
|
49
|
+
npx slides-studio-mcp@latest restart
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
`restart` gracefully replaces an outdated local companion. Reload the real browser editor afterward; current MCP clients automatically recover their daemon registration.
|
|
@@ -59,4 +59,4 @@ For parallel editing, the parent agent must reserve a different connected editor
|
|
|
59
59
|
|
|
60
60
|
Browser writes use revision-checked IndexedDB transactions and cross-tab synchronization. A stale tab cannot replace a newer project snapshot; it reloads the canonical copy and returns `STALE_PROJECT` so the agent can inspect and retry.
|
|
61
61
|
|
|
62
|
-
Open the
|
|
62
|
+
Open the editor in the user's normal local browser, click **Connect AI**, and call `get_design_guidance` before editing. The server provides `render_slide` so agents can inspect actual pixels without permanently storing previews.
|
package/guidance/design.md
CHANGED
|
@@ -6,6 +6,8 @@ Read this before creating or editing slides. Use it as a compact quality bar, th
|
|
|
6
6
|
|
|
7
7
|
- Build for a 9:16 phone canvas and keep one clear idea per slide.
|
|
8
8
|
- Prefer `boxed` text with `backgroundShape: "lines"` for highlighted copy. Treat per-line boxes as the default; use `full` only for a deliberate card or label.
|
|
9
|
+
- `add_text` and `update_text` automatically preserve width and fit height around all wrapped lines with safety padding. Do not render just to discover clipping or call `fit_text_boxes` after ordinary copy edits.
|
|
10
|
+
- Use `fit_text_boxes` with `mode: "both"` only when you intentionally want width to shrink as well. If automatic fitting rejects copy that cannot fit on one slide, shorten it or split it across slides.
|
|
9
11
|
- After creating or changing any full-box text, call `fit_text_boxes`. A full box must hug its rendered content instead of leaving a large empty rectangle.
|
|
10
12
|
- Use plain or outlined text for supporting copy. Use no more than two text treatments on one slide.
|
|
11
13
|
- Choose size by role rather than one universal value: titles `92–124`, subtitles `68–84`, body copy `54–68`, captions `44–52`. These are ranges, not fixed presets; render and adjust within them.
|
|
@@ -34,7 +36,7 @@ Read this before creating or editing slides. Use it as a compact quality bar, th
|
|
|
34
36
|
- Increase height when multiline text or per-line backgrounds approach the top or bottom edge.
|
|
35
37
|
- Use `fit_text_boxes` after changing full-box copy or font size. Use `mode: "height"` when the chosen width must remain fixed.
|
|
36
38
|
- Keep `x + width` and `y + height` within the canvas unless an off-canvas effect is intentional.
|
|
37
|
-
- With boxed text, keep `backgroundShape: "lines"
|
|
39
|
+
- With boxed text, keep `backgroundShape: "lines"`; automatic height fitting includes minimum breathing room so rounded pills stay inside the text container.
|
|
38
40
|
- If the result is uncertain, render it. Numeric state is not a visual review.
|
|
39
41
|
|
|
40
42
|
## Agent behavior
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slides-studio-mcp",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Local-first MCP companion for the hosted Slide Studio editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/alexgusevski/tiktokslideeditor.git",
|
|
10
10
|
"directory": "packages/mcp"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://slides-
|
|
12
|
+
"homepage": "https://slides-editor.pages.dev",
|
|
13
13
|
"bugs": "https://github.com/alexgusevski/tiktokslideeditor/issues",
|
|
14
14
|
"bin": {
|
|
15
15
|
"slides-studio-mcp": "src/cli.mjs"
|
|
@@ -16,15 +16,15 @@ Before using any browser or making edits, call `list_editors`. A registered edit
|
|
|
16
16
|
|
|
17
17
|
Browser reconnection is automatic. After `EDITOR_DISCONNECTED`, `EDITOR_RELOADED`, a dropped browser request, or an empty `list_editors` result that follows a working connection, wait briefly and retry `list_editors` several times. Do not restart the companion for a transient browser disconnect: restarting invalidates every browser session and makes recovery slower. If the editor does not return, ask the user to keep or reload the real editor tab; preserve completed project work and begin a new edit session after it reconnects.
|
|
18
18
|
|
|
19
|
-
Run `npx -y slides-studio-mcp@
|
|
19
|
+
Run `npx -y slides-studio-mcp@latest restart` only when the MCP explicitly reports an outdated companion protocol or `doctor` reports that the daemon itself is unhealthy. After a necessary restart, ask the user to reload their real editor tab and retry `list_editors`.
|
|
20
20
|
|
|
21
21
|
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:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx -y slides-studio-mcp@
|
|
25
|
-
npx -y slides-studio-mcp@
|
|
26
|
-
npx -y slides-studio-mcp@
|
|
27
|
-
npx -y slides-studio-mcp@
|
|
24
|
+
npx -y slides-studio-mcp@latest call get_design_guidance
|
|
25
|
+
npx -y slides-studio-mcp@latest call list_editors
|
|
26
|
+
npx -y slides-studio-mcp@latest call list_tools --json '{"names":["add_slide","add_text"]}'
|
|
27
|
+
npx -y slides-studio-mcp@latest call create_project --json '{"name":"My presentation"}'
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
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`.
|
|
@@ -46,7 +46,7 @@ With only one agent and one editor, the server can create an implicit session fo
|
|
|
46
46
|
|
|
47
47
|
Inspect the assigned editor before editing and keep the returned IDs and revision. Work on the assigned project and slide. Pass `expectedRevision` for sensitive mutations. If `STALE_PROJECT` is returned, the browser has reloaded the canonical IndexedDB copy; inspect again and retry with current IDs. Prefer `apply_operations` for compact related changes while preserving logical order.
|
|
48
48
|
|
|
49
|
-
Use readable role-based type ranges: title `92–124`, subtitle `68–84`, body `54–68`, caption `44–52`. Do not solve dense copy by dropping below the body range; shorten it or split it across slides. For highlighted text, prefer `style: "boxed"` with `backgroundShape: "lines"`. Use `backgroundShape: "full"` only for a deliberate card
|
|
49
|
+
Use readable role-based type ranges: title `92–124`, subtitle `68–84`, body `54–68`, caption `44–52`. Do not solve dense copy by dropping below the body range; shorten it or split it across slides. `add_text` and `update_text` automatically preserve width and fit height around every wrapped line with safe padding, so do not waste calls on render-fit-render loops. For highlighted text, prefer `style: "boxed"` with `backgroundShape: "lines"`. Use `backgroundShape: "full"` only for a deliberate card. Call `fit_text_boxes` with `mode: "both"` only when you intentionally want the width to shrink too.
|
|
50
50
|
|
|
51
51
|
After each meaningful composition or after a short batch, call `render_slide` and inspect the returned image. Fix clipping, spacing, contrast, unsafe TikTok-overlay placement, and weak hierarchy before claiming the slide is finished. Use `export_slide` or `export_project` only when local files are requested; do not overwrite existing files unless authorized.
|
|
52
52
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const KNOWN_AGENTS = [
|
|
2
|
+
{ label: "Hermes", matches: ["hermes"] },
|
|
3
|
+
{ label: "Claude", matches: ["claude", "anthropic"] },
|
|
4
|
+
{ label: "Codex", matches: ["codex", "openai"] },
|
|
5
|
+
{ label: "OpenCode", matches: ["opencode"] },
|
|
6
|
+
{ label: "OpenClaw", matches: ["openclaw"] },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const GENERIC_CLIENT_NAMES = new Set([
|
|
10
|
+
"ai agent",
|
|
11
|
+
"agent",
|
|
12
|
+
"client",
|
|
13
|
+
"mcp",
|
|
14
|
+
"mcp agent",
|
|
15
|
+
"mcp client",
|
|
16
|
+
"slide studio cli fallback",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function canonicalAgentName(value) {
|
|
20
|
+
const name = String(value || "").trim();
|
|
21
|
+
const normalized = name.toLowerCase();
|
|
22
|
+
return KNOWN_AGENTS.find(({ matches }) => matches.some((match) => normalized.includes(match)))?.label || name || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isGenericAgentName(value) {
|
|
26
|
+
return GENERIC_CLIENT_NAMES.has(String(value || "").trim().toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function detectHostAgent(explicitName = null, environment = process.env, runtime = process) {
|
|
30
|
+
const configured = canonicalAgentName(explicitName || environment.SLIDE_STUDIO_AGENT);
|
|
31
|
+
if (configured) return configured;
|
|
32
|
+
const environmentKeys = Object.keys(environment).filter((key) => environment[key]).join(" ");
|
|
33
|
+
const evidence = [
|
|
34
|
+
runtime.execPath,
|
|
35
|
+
...(runtime.argv || []),
|
|
36
|
+
environment.PATH,
|
|
37
|
+
environment.TERM_PROGRAM,
|
|
38
|
+
environment.npm_execpath,
|
|
39
|
+
environmentKeys,
|
|
40
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
41
|
+
return KNOWN_AGENTS.find(({ matches }) => matches.some((match) => evidence.includes(match)))?.label || "MCP agent";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function preferHostAgent(reportedName, hostName) {
|
|
45
|
+
const reported = canonicalAgentName(reportedName);
|
|
46
|
+
const host = canonicalAgentName(hostName) || "MCP agent";
|
|
47
|
+
return !reported || isGenericAgentName(reported) ? host : reported;
|
|
48
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { companionDoctor, companionRestart } from "./companion.mjs";
|
|
3
|
-
import { PACKAGE_NAME, PACKAGE_VERSION
|
|
3
|
+
import { EDITOR_URL, PACKAGE_NAME, PACKAGE_VERSION } from "./config.mjs";
|
|
4
4
|
import { runSetup } from "./setup.mjs";
|
|
5
5
|
import { serveMcp } from "./stdio-server.mjs";
|
|
6
6
|
|
|
@@ -8,7 +8,8 @@ const [command = "serve", ...arguments_] = process.argv.slice(2);
|
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
10
|
if (command === "serve") {
|
|
11
|
-
|
|
11
|
+
const agentName = arguments_.find((value) => value.startsWith("--agent="))?.slice("--agent=".length) || null;
|
|
12
|
+
await serveMcp({ agentName });
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
15
|
if (command === "setup") {
|
|
@@ -22,7 +23,7 @@ async function main() {
|
|
|
22
23
|
}
|
|
23
24
|
if (command === "doctor") {
|
|
24
25
|
const health = await companionDoctor();
|
|
25
|
-
process.stdout.write(`${JSON.stringify({ package: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, editor:
|
|
26
|
+
process.stdout.write(`${JSON.stringify({ package: PACKAGE_NAME, packageVersion: PACKAGE_VERSION, editor: EDITOR_URL, daemon: health }, null, 2)}\n`);
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
if (command === "restart") {
|
|
@@ -35,7 +36,7 @@ async function main() {
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
38
|
-
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 restart\n slides-studio-mcp version\n`);
|
|
39
|
+
process.stdout.write(`Slide Studio MCP ${PACKAGE_VERSION}\n\nUsage:\n slides-studio-mcp serve [--agent=claude|codex|hermes|opencode|openclaw]\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 restart\n slides-studio-mcp version\n`);
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
42
|
throw new Error(`Unknown command: ${command}`);
|
package/src/companion.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { BRIDGE_URL, DAEMON_STATE_PATH, PACKAGE_VERSION, PROTOCOL_VERSION } from "./config.mjs";
|
|
6
|
+
import { preferHostAgent } from "./agent-identity.mjs";
|
|
6
7
|
|
|
7
8
|
const DAEMON_ENTRY = fileURLToPath(new URL("daemon.mjs", import.meta.url));
|
|
8
9
|
|
|
@@ -33,7 +34,7 @@ async function healthyState() {
|
|
|
33
34
|
try {
|
|
34
35
|
const result = await daemonRequest(state, "/internal/health");
|
|
35
36
|
if (result.protocolVersion !== PROTOCOL_VERSION) {
|
|
36
|
-
const error = new Error(`Local companion ${result.version || "unknown"} uses protocol ${result.protocolVersion}; ${PACKAGE_VERSION} requires protocol ${PROTOCOL_VERSION}. Run \`npx -y slides-studio-mcp@
|
|
37
|
+
const error = new Error(`Local companion ${result.version || "unknown"} uses protocol ${result.protocolVersion}; ${PACKAGE_VERSION} requires protocol ${PROTOCOL_VERSION}. Run \`npx -y slides-studio-mcp@latest restart\`, then reload the editor.`);
|
|
37
38
|
error.code = "EPROTOCOL";
|
|
38
39
|
throw error;
|
|
39
40
|
}
|
|
@@ -91,7 +92,7 @@ export async function createCompanion(initialName = "MCP agent", initialVersion
|
|
|
91
92
|
clientId,
|
|
92
93
|
get daemon() { return { pid: state.pid, url: BRIDGE_URL, version: state.version, packageVersion: PACKAGE_VERSION }; },
|
|
93
94
|
async identify(name, version) {
|
|
94
|
-
clientName = name
|
|
95
|
+
clientName = preferHostAgent(name, clientName);
|
|
95
96
|
clientVersion = version || clientVersion;
|
|
96
97
|
await register();
|
|
97
98
|
},
|
package/src/config.mjs
CHANGED
|
@@ -11,9 +11,9 @@ export const PROTOCOL_VERSION = 3;
|
|
|
11
11
|
export const BRIDGE_HOST = "127.0.0.1";
|
|
12
12
|
export const BRIDGE_PORT = Number(process.env.SLIDE_STUDIO_BRIDGE_PORT) || 43117;
|
|
13
13
|
export const BRIDGE_URL = `http://${BRIDGE_HOST}:${BRIDGE_PORT}`;
|
|
14
|
-
export const
|
|
14
|
+
export const EDITOR_URL = "https://slides-editor.pages.dev";
|
|
15
15
|
export const ALLOWED_ORIGINS = new Set((process.env.SLIDE_STUDIO_ALLOWED_ORIGINS || [
|
|
16
|
-
|
|
16
|
+
EDITOR_URL,
|
|
17
17
|
"http://127.0.0.1:4173",
|
|
18
18
|
"http://localhost:4173",
|
|
19
19
|
].join(",")).split(",").map((value) => value.trim()).filter(Boolean));
|
package/src/daemon.mjs
CHANGED
|
@@ -17,7 +17,8 @@ const COMMAND_TIMEOUT_MS = 90_000;
|
|
|
17
17
|
const EDIT_SESSION_TTL_MS = Number(process.env.SLIDE_STUDIO_EDIT_SESSION_TTL_MS) || 5 * 60_000;
|
|
18
18
|
const MAX_AUDIT_EVENTS = 500;
|
|
19
19
|
const MAX_AUDIT_BYTES = 2 * 1024 * 1024;
|
|
20
|
-
const EVENT_POLL_TIMEOUT_MS = Number(process.env.SLIDE_STUDIO_EVENT_POLL_TIMEOUT_MS) ||
|
|
20
|
+
const EVENT_POLL_TIMEOUT_MS = Number(process.env.SLIDE_STUDIO_EVENT_POLL_TIMEOUT_MS) || 500;
|
|
21
|
+
const MAX_EVENT_POLL_TIMEOUT_MS = 5_000;
|
|
21
22
|
const daemonSecret = randomBytes(32).toString("base64url");
|
|
22
23
|
const editors = new Map();
|
|
23
24
|
const clients = new Map();
|
|
@@ -547,6 +548,10 @@ const server = createServer(async (request, response) => {
|
|
|
547
548
|
if (url.pathname === "/events" && request.method === "GET") {
|
|
548
549
|
const editor = requireEditor(request, response, url.searchParams.get("editorId"), cors);
|
|
549
550
|
if (!editor) return;
|
|
551
|
+
const requestedWait = Number(url.searchParams.get("wait"));
|
|
552
|
+
const waitMs = url.searchParams.has("wait") && Number.isFinite(requestedWait)
|
|
553
|
+
? Math.min(MAX_EVENT_POLL_TIMEOUT_MS, Math.max(0, requestedWait))
|
|
554
|
+
: EVENT_POLL_TIMEOUT_MS;
|
|
550
555
|
editor.cors = cors;
|
|
551
556
|
if (editor.poll) endEditorPoll(editor);
|
|
552
557
|
editor.poll = response;
|
|
@@ -558,10 +563,12 @@ const server = createServer(async (request, response) => {
|
|
|
558
563
|
editor.lastSeen = Date.now();
|
|
559
564
|
});
|
|
560
565
|
deliverNext(editor);
|
|
561
|
-
if (editor.poll
|
|
566
|
+
if (editor.poll && waitMs === 0) {
|
|
567
|
+
endEditorPoll(editor);
|
|
568
|
+
} else if (editor.poll) editor.pollTimer = setTimeout(() => {
|
|
562
569
|
if (editor.poll !== response) return;
|
|
563
570
|
endEditorPoll(editor);
|
|
564
|
-
},
|
|
571
|
+
}, waitMs);
|
|
565
572
|
editor.pollTimer?.unref();
|
|
566
573
|
return;
|
|
567
574
|
}
|
package/src/mcp-server.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
|
-
import { GUIDANCE_PATH, PACKAGE_NAME, PACKAGE_VERSION
|
|
5
|
+
import { EDITOR_URL, GUIDANCE_PATH, PACKAGE_NAME, PACKAGE_VERSION } from "./config.mjs";
|
|
6
6
|
|
|
7
7
|
const id = z.string().min(1).max(160);
|
|
8
8
|
const optionalId = id.optional();
|
|
@@ -35,7 +35,7 @@ function textResult(value, summary = value) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function compactMutation(value) {
|
|
38
|
-
const keys = ["id", "editSessionId", "editorId", "projectId", "slideId", "revision", "leaseExpiresAt", "purpose", "released", "opened", "createdSlideId", "createdTextId", "createdImageId", "createdLayers", "assetId", "deletedAssetId", "deletedProjectId", "deletedSlideId", "deletedLayerIds", "updatedTextIds", "fittedTextBoxes", "updatedImageIds", "applied", "path", "bytes"];
|
|
38
|
+
const keys = ["id", "editSessionId", "editorId", "projectId", "slideId", "revision", "leaseExpiresAt", "purpose", "released", "opened", "createdSlideId", "createdTextId", "fittedTextBox", "createdImageId", "createdLayers", "assetId", "deletedAssetId", "deletedProjectId", "deletedSlideId", "deletedLayerIds", "updatedTextIds", "fittedTextBoxes", "updatedImageIds", "applied", "path", "bytes"];
|
|
39
39
|
return Object.fromEntries(keys.flatMap((key) => value?.[key] == null ? [] : [[key, value[key]]]));
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -99,7 +99,7 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
99
99
|
let guidanceRead = false;
|
|
100
100
|
let identifiedAs = null;
|
|
101
101
|
const server = new McpServer({ name: PACKAGE_NAME, version: PACKAGE_VERSION }, {
|
|
102
|
-
instructions: `First call list_editors and use the registered local browser tab. Never open or connect Slide Studio through a sandboxed agent browser. If no editor is listed, retry briefly because browser reconnection is automatic, then ask the user to open ${
|
|
102
|
+
instructions: `First call list_editors and use the registered local browser tab. Never open or connect Slide Studio through a sandboxed agent browser. If no editor is listed, retry briefly because browser reconnection is automatic, then ask the user to open ${EDITOR_URL} in their normal browser and click Connect AI. Never restart a healthy companion for a transient editor disconnect; restart only for an explicit protocol mismatch or failed daemon health check. Before edits call get_design_guidance, then begin_edit_session; pass editSessionId to every edit and end it in cleanup. Parallel editing workers require distinct editor sessions. Use render_slide to inspect actual pixels.`,
|
|
103
103
|
capabilities: { tools: {}, resources: {} },
|
|
104
104
|
});
|
|
105
105
|
|
|
@@ -160,9 +160,9 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
160
160
|
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 });
|
|
161
161
|
register("delete_slide", "Delete one slide.", z.object({ ...targetSlide, slideId: id }).strict(), (args) => browserOperation(companion, "delete_slide", args), { destructiveHint: true });
|
|
162
162
|
|
|
163
|
-
register("add_text", "Add a text layer. Choose a semantic role and a size within its readable range.
|
|
164
|
-
register("update_text", "Update one or more text layers
|
|
165
|
-
register("fit_text_boxes", "
|
|
163
|
+
register("add_text", "Add a text layer. Choose a semantic role and a size within its readable range. Width is preserved while height is fitted automatically with safe padding; boxed text defaults to the preferred per-line background.", z.object({ ...targetSlide, ...textFields, text: z.string().min(1).max(4000) }).strict(), (args) => browserOperation(companion, "add_text", args), { destructiveHint: false });
|
|
164
|
+
register("update_text", "Update one or more text layers. Every updated layer automatically keeps its width and refits its height with safe padding, so a render-fit-render loop is unnecessary.", z.object({ ...targetSlide, updates: z.array(z.object({ id, ...textFields }).strict()).min(1).max(100) }).strict(), (args) => browserOperation(companion, "update_text", args), { destructiveHint: true });
|
|
165
|
+
register("fit_text_boxes", "Explicitly resize text boxes to their rendered content. add_text and update_text already fit height automatically; use mode=both only when you also want to shrink width.", z.object({ ...targetSlide, textIds: z.array(id).min(1).max(100), mode: z.enum(["height", "both"]).default("both") }).strict(), (args) => browserOperation(companion, "fit_text_boxes", args), { destructiveHint: true });
|
|
166
166
|
|
|
167
167
|
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 });
|
|
168
168
|
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 });
|
package/src/setup.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { cp, mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { PACKAGE_NAME, PACKAGE_ROOT, PACKAGE_VERSION
|
|
6
|
+
import { EDITOR_URL, PACKAGE_NAME, PACKAGE_ROOT, PACKAGE_VERSION } from "./config.mjs";
|
|
7
7
|
|
|
8
8
|
const supported = ["claude", "codex", "hermes", "opencode", "openclaw"];
|
|
9
9
|
|
|
@@ -17,10 +17,10 @@ function commandVersion(command) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function shellCommand(client, specifier) {
|
|
20
|
-
if (client === "claude") return ["claude", "mcp", "add", "--scope", "user", "--transport", "stdio", "slide-studio", "--", "npx", "-y", specifier, "serve"];
|
|
21
|
-
if (client === "codex") return ["codex", "mcp", "add", "slide-studio", "--", "npx", "-y", specifier, "serve"];
|
|
22
|
-
if (client === "hermes") return ["hermes", "mcp", "add", "slide-studio", "--command", "npx", "--args", "-y", specifier, "serve"];
|
|
23
|
-
if (client === "openclaw") return ["openclaw", "mcp", "add", "slide-studio", "--command", "npx", "--arg", "-y", "--arg", specifier, "--arg", "serve"];
|
|
20
|
+
if (client === "claude") return ["claude", "mcp", "add", "--scope", "user", "--transport", "stdio", "slide-studio", "--", "npx", "-y", specifier, "serve", "--agent=claude"];
|
|
21
|
+
if (client === "codex") return ["codex", "mcp", "add", "slide-studio", "--", "npx", "-y", specifier, "serve", "--agent=codex"];
|
|
22
|
+
if (client === "hermes") return ["hermes", "mcp", "add", "slide-studio", "--command", "npx", "--args", "-y", specifier, "serve", "--agent=hermes"];
|
|
23
|
+
if (client === "openclaw") return ["openclaw", "mcp", "add", "slide-studio", "--command", "npx", "--arg", "-y", "--arg", specifier, "--arg", "serve", "--arg", "--agent=openclaw"];
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -35,7 +35,7 @@ function quote(value) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function openCodeSnippet(specifier, version = commandVersion("opencode")) {
|
|
38
|
-
const server = { type: "local", command: ["npx", "-y", specifier, "serve"] };
|
|
38
|
+
const server = { type: "local", command: ["npx", "-y", specifier, "serve", "--agent=opencode"] };
|
|
39
39
|
return JSON.stringify(Number(version?.split(".")[0]) >= 2
|
|
40
40
|
? { mcp: { servers: { "slide-studio": server } } }
|
|
41
41
|
: { mcp: { "slide-studio": { ...server, enabled: true } } }, null, 2);
|
|
@@ -60,12 +60,13 @@ export async function runSetup(arguments_) {
|
|
|
60
60
|
const clientArgument = arguments_.find((value) => value.startsWith("--client="))?.slice("--client=".length);
|
|
61
61
|
const requested = clientArgument ? clientArgument.split(",").map((value) => value.trim().toLowerCase()) : supported.filter(commandExists);
|
|
62
62
|
const clients = [...new Set(requested)].filter((client) => supported.includes(client));
|
|
63
|
-
const
|
|
63
|
+
const releaseTag = PACKAGE_VERSION.includes("-beta.") ? "beta" : "latest";
|
|
64
|
+
const specifier = `${PACKAGE_NAME}@${releaseTag}`;
|
|
64
65
|
const dryRun = flags.has("--dry-run");
|
|
65
66
|
const assumeYes = flags.has("--yes") || flags.has("-y");
|
|
66
67
|
if (!clients.length) throw new Error("No supported agent CLI was detected. Use --client=claude,codex,hermes,opencode,openclaw or copy the generic stdio config below.");
|
|
67
68
|
|
|
68
|
-
process.stdout.write(`Slide Studio MCP ${PACKAGE_VERSION}\nDetected: ${clients.join(", ")}\nEditor: ${
|
|
69
|
+
process.stdout.write(`Slide Studio MCP ${PACKAGE_VERSION}\nDetected: ${clients.join(", ")}\nEditor: ${EDITOR_URL}\n\n`);
|
|
69
70
|
for (const client of clients) {
|
|
70
71
|
const command = shellCommand(client, specifier);
|
|
71
72
|
if (command) process.stdout.write(`${client}: ${command.map(quote).join(" ")}\n`);
|
|
@@ -97,7 +98,7 @@ export async function runSetup(arguments_) {
|
|
|
97
98
|
else process.stderr.write(`Could not configure ${client}; its command is printed above for manual setup.\n`);
|
|
98
99
|
}
|
|
99
100
|
const skillTargets = await installSkill();
|
|
100
|
-
process.stdout.write(`\nConfigured: ${configured.join(", ") || "none automatically"}\nSkill installed in:\n${skillTargets.map((value) => ` ${value}`).join("\n")}\n\nOpen ${
|
|
101
|
+
process.stdout.write(`\nConfigured: ${configured.join(", ") || "none automatically"}\nSkill installed in:\n${skillTargets.map((value) => ` ${value}`).join("\n")}\n\nOpen ${EDITOR_URL} in your normal local browser and click Connect AI. Do not use a sandboxed agent browser.\n`);
|
|
101
102
|
process.stdout.write(`First connection check (no browser automation): npx -y ${specifier} call list_editors\n`);
|
|
102
103
|
if (clients.includes("hermes")) process.stdout.write("Hermes can also refresh native tools in place with /reload-mcp and /reload-skills.\n");
|
|
103
104
|
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");
|
package/src/stdio-server.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
2
|
+
import { detectHostAgent } from "./agent-identity.mjs";
|
|
2
3
|
import { createCompanion } from "./companion.mjs";
|
|
3
4
|
import { createSlideStudioMcpServer } from "./mcp-server.mjs";
|
|
4
5
|
|
|
5
|
-
export async function serveMcp() {
|
|
6
|
-
const companion = await createCompanion();
|
|
6
|
+
export async function serveMcp({ agentName = null } = {}) {
|
|
7
|
+
const companion = await createCompanion(detectHostAgent(agentName));
|
|
7
8
|
const handle = serveStdio(() => createSlideStudioMcpServer(companion), {
|
|
8
9
|
legacy: "serve",
|
|
9
10
|
onerror: (error) => process.stderr.write(`[slides-studio-mcp] ${error.message}\n`),
|