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 CHANGED
@@ -1,15 +1,15 @@
1
1
  # Slide Studio MCP
2
2
 
3
- Local-first MCP companion for the hosted [Slide Studio test editor](https://slides-mcp-poc-0821.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.
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@beta setup
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@beta setup --client=codex --yes
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@beta call get_design_guidance
23
- npx -y slides-studio-mcp@beta call list_editors
24
- npx -y slides-studio-mcp@beta call begin_edit_session --json '{"editorId":"EDITOR_ID","purpose":"Build my deck"}'
25
- npx -y slides-studio-mcp@beta call create_project --json '{"editSessionId":"SESSION_ID","name":"My presentation"}'
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@beta serve
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@beta setup --dry-run
48
- npx slides-studio-mcp@beta doctor
49
- npx slides-studio-mcp@beta restart
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 test 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.
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.
@@ -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"` and do not size the box tightly around the letters; rounded pills need breathing room.
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-beta.9",
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-mcp-poc-0821.pages.dev",
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@beta 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`.
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@beta call get_design_guidance
25
- npx -y slides-studio-mcp@beta call list_editors
26
- npx -y slides-studio-mcp@beta call list_tools --json '{"names":["add_slide","add_text"]}'
27
- npx -y slides-studio-mcp@beta call create_project --json '{"name":"My presentation"}'
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, then call `fit_text_boxes` after any copy or size change so the rectangle hugs the rendered text. Use `mode: "height"` when its width should stay fixed.
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, TEST_EDITOR_URL } from "./config.mjs";
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
- await serveMcp();
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: TEST_EDITOR_URL, daemon: health }, null, 2)}\n`);
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@beta restart\`, then reload the editor.`);
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 || clientName;
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 TEST_EDITOR_URL = "https://slides-mcp-poc-0821.pages.dev";
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
- TEST_EDITOR_URL,
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) || 5 * 60_000;
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) editor.pollTimer = setTimeout(() => {
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
- }, EVENT_POLL_TIMEOUT_MS);
571
+ }, waitMs);
565
572
  editor.pollTimer?.unref();
566
573
  return;
567
574
  }
@@ -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, TEST_EDITOR_URL } from "./config.mjs";
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 ${TEST_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.`,
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. 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, 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 });
165
- register("fit_text_boxes", "Resize one or more text boxes to their rendered content while preserving alignment. Use after changing text or size, and always for full-box backgrounds; mode=height keeps the current 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 });
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, TEST_EDITOR_URL } from "./config.mjs";
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 specifier = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
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: ${TEST_EDITOR_URL}\n\n`);
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 ${TEST_EDITOR_URL} in your normal local browser and click Connect AI. Do not use a sandboxed agent browser.\n`);
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");
@@ -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`),