slides-studio-mcp 0.1.0-beta.7 → 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 +14 -12
- package/guidance/design.md +11 -6
- package/package.json +2 -2
- package/skill/slide-studio/SKILL.md +10 -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 +28 -5
- package/src/mcp-server.mjs +14 -12
- 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,16 +19,18 @@ 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.
|
|
29
29
|
|
|
30
30
|
Always use `list_editors` to check the browser connection. Do not open Slide Studio or click **Connect AI** through a sandboxed, remote, or agent-controlled browser: that is a different browser session and may not have access to the user's local companion. If no editor is listed, ask the user to open the editor in their normal browser on the same computer and connect there, then retry `list_editors`.
|
|
31
31
|
|
|
32
|
+
Browser reconnection is automatic. For a transient `EDITOR_DISCONNECTED`, `EDITOR_RELOADED`, dropped browser request, or newly empty editor list, wait briefly and retry `list_editors`; do not restart a healthy daemon. Use `restart` only for an explicit companion protocol mismatch or when `doctor` reports that the daemon itself is unhealthy. Restarting invalidates current browser session tokens and should not be used as generic connection recovery.
|
|
33
|
+
|
|
32
34
|
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.
|
|
33
35
|
|
|
34
36
|
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.
|
|
@@ -36,15 +38,15 @@ The companion binds only to `127.0.0.1`. There is no hosted relay: projects rema
|
|
|
36
38
|
Any MCP client can launch it with:
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
npx -y slides-studio-mcp@
|
|
41
|
+
npx -y slides-studio-mcp@latest serve
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
Useful commands:
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
|
-
npx slides-studio-mcp@
|
|
46
|
-
npx slides-studio-mcp@
|
|
47
|
-
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
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
`restart` gracefully replaces an outdated local companion. Reload the real browser editor afterward; current MCP clients automatically recover their daemon registration.
|
|
@@ -57,4 +59,4 @@ For parallel editing, the parent agent must reserve a different connected editor
|
|
|
57
59
|
|
|
58
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.
|
|
59
61
|
|
|
60
|
-
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
|
@@ -5,12 +5,15 @@ Read this before creating or editing slides. Use it as a compact quality bar, th
|
|
|
5
5
|
## Defaults that usually look good
|
|
6
6
|
|
|
7
7
|
- Build for a 9:16 phone canvas and keep one clear idea per slide.
|
|
8
|
-
- Prefer `boxed` text with `backgroundShape: "lines"` for highlighted copy.
|
|
9
|
-
-
|
|
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.
|
|
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.
|
|
14
|
+
- Do not shrink dense copy below the body range to make it fit. Shorten it or split it across slides. Aim for roughly 3–7 body lines on a slide.
|
|
12
15
|
- Keep important content inside roughly `x: 0.06..0.86` and `y: 0.08..0.78` when the TikTok overlay matters. The right and bottom edges are occupied by interface controls and captions.
|
|
13
|
-
- Give text boxes generous width
|
|
16
|
+
- Give text boxes generous width while composing. Per-line backgrounds include protected edge padding; if content or size changes, render again and use `fit_text_boxes` when the box itself should hug the content.
|
|
14
17
|
- Use short lines. Two to four lines for a headline is usually stronger than one dense paragraph.
|
|
15
18
|
- Keep strong contrast between copy and the image. Use black boxed backgrounds with white text or white boxed backgrounds with near-black text.
|
|
16
19
|
- Preserve an obvious focal image. Do not cover faces or the main subject unless the composition intentionally calls for it.
|
|
@@ -21,7 +24,7 @@ Read this before creating or editing slides. Use it as a compact quality bar, th
|
|
|
21
24
|
## Working method
|
|
22
25
|
|
|
23
26
|
1. Inspect the editor and use the returned project, slide, asset, and layer IDs.
|
|
24
|
-
2. Create or update one slide at a time.
|
|
27
|
+
2. Create or update one slide at a time. If that project is already visible, the editor follows its most recently changed slide. Work on another project never takes over the user's current view.
|
|
25
28
|
3. Use `apply_operations` when several related edits can be expressed compactly; the browser still shows each operation live.
|
|
26
29
|
4. Call `render_slide` after a meaningful composition change and look at the returned image.
|
|
27
30
|
5. Correct clipping, collisions, weak contrast, unsafe placement, inconsistent spacing, and visual imbalance before continuing.
|
|
@@ -31,8 +34,9 @@ Read this before creating or editing slides. Use it as a compact quality bar, th
|
|
|
31
34
|
|
|
32
35
|
- Increase width before shrinking type when a line almost fits.
|
|
33
36
|
- Increase height when multiline text or per-line backgrounds approach the top or bottom edge.
|
|
37
|
+
- Use `fit_text_boxes` after changing full-box copy or font size. Use `mode: "height"` when the chosen width must remain fixed.
|
|
34
38
|
- Keep `x + width` and `y + height` within the canvas unless an off-canvas effect is intentional.
|
|
35
|
-
- 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.
|
|
36
40
|
- If the result is uncertain, render it. Numeric state is not a visual review.
|
|
37
41
|
|
|
38
42
|
## Agent behavior
|
|
@@ -42,3 +46,4 @@ Read this before creating or editing slides. Use it as a compact quality bar, th
|
|
|
42
46
|
- Keep tool responses and progress messages concise.
|
|
43
47
|
- Prefer IDs returned by tools over guessed names or array positions.
|
|
44
48
|
- Do not claim a slide looks good until you have inspected a rendered image.
|
|
49
|
+
- Do not call `open_project` merely to edit or render another project. It intentionally changes the user's browser view; use it only when the user asks to see that project.
|
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"
|
|
@@ -14,15 +14,17 @@ Before using any browser or making edits, call `list_editors`. A registered edit
|
|
|
14
14
|
- If no editor is listed, ask the user to open the editor in their normal browser on the same computer, click **Connect AI**, and accept the browser permission. Then retry `list_editors`.
|
|
15
15
|
- A loaded MCP server and a connected browser editor are different states. Determine browser connectivity only from `list_editors`, not from a sandbox browser or the agent's tool catalog.
|
|
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
|
+
|
|
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`.
|
|
18
20
|
|
|
19
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:
|
|
20
22
|
|
|
21
23
|
```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@
|
|
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"}'
|
|
26
28
|
```
|
|
27
29
|
|
|
28
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`.
|
|
@@ -44,6 +46,8 @@ With only one agent and one editor, the server can create an implicit session fo
|
|
|
44
46
|
|
|
45
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.
|
|
46
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. `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
|
+
|
|
47
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.
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
Edits follow the latest changed slide only when their project is already visible. Creating or editing another project must not take over the user's current browser view. `open_project` and `set_view` intentionally navigate, so call them only when the user asks to see that project. Other tabs synchronize project cards and project state through local browser storage. Use `show_notification` only for short, useful status messages.
|
|
@@ -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
|
@@ -10,14 +10,15 @@ import {
|
|
|
10
10
|
|
|
11
11
|
const MAX_JSON_BYTES = 40 * 1024 * 1024;
|
|
12
12
|
const MAX_MEDIA_BYTES = 25 * 1024 * 1024;
|
|
13
|
-
const EDITOR_TTL_MS = Number(process.env.SLIDE_STUDIO_EDITOR_TTL_MS) ||
|
|
13
|
+
const EDITOR_TTL_MS = Number(process.env.SLIDE_STUDIO_EDITOR_TTL_MS) || 60_000;
|
|
14
14
|
const CLIENT_TTL_MS = 45_000;
|
|
15
15
|
const MEDIA_TTL_MS = 5 * 60_000;
|
|
16
16
|
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();
|
|
@@ -34,11 +35,17 @@ function log(message) {
|
|
|
34
35
|
process.stderr.write(`[slide-studio-daemon] ${message}\n`);
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
function editorHasInflightCommand(editorId) {
|
|
39
|
+
for (const pending of inflight.values()) if (pending.editorId === editorId) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
function activeEditors() {
|
|
38
44
|
const cutoff = Date.now() - EDITOR_TTL_MS;
|
|
39
45
|
return [...editors.values()].filter((editor) => (
|
|
40
46
|
editor.lastSeen >= cutoff
|
|
41
47
|
|| Boolean(editor.poll && !editor.poll.destroyed && !editor.poll.writableEnded)
|
|
48
|
+
|| editorHasInflightCommand(editor.id)
|
|
42
49
|
));
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -525,6 +532,12 @@ const server = createServer(async (request, response) => {
|
|
|
525
532
|
focusedEditorId = editor.id;
|
|
526
533
|
return sendJson(response, 200, { ok: true, editorId: editor.id }, cors);
|
|
527
534
|
}
|
|
535
|
+
if (url.pathname === "/heartbeat" && request.method === "POST") {
|
|
536
|
+
const body = await readJson(request);
|
|
537
|
+
const editor = requireEditor(request, response, body.editorId, cors);
|
|
538
|
+
if (!editor) return;
|
|
539
|
+
return sendJson(response, 200, { ok: true, editorId: editor.id }, cors);
|
|
540
|
+
}
|
|
528
541
|
if (url.pathname === "/disconnect" && request.method === "POST") {
|
|
529
542
|
const body = await readJson(request);
|
|
530
543
|
const editor = requireEditor(request, response, body.editorId, cors);
|
|
@@ -535,6 +548,10 @@ const server = createServer(async (request, response) => {
|
|
|
535
548
|
if (url.pathname === "/events" && request.method === "GET") {
|
|
536
549
|
const editor = requireEditor(request, response, url.searchParams.get("editorId"), cors);
|
|
537
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;
|
|
538
555
|
editor.cors = cors;
|
|
539
556
|
if (editor.poll) endEditorPoll(editor);
|
|
540
557
|
editor.poll = response;
|
|
@@ -546,10 +563,12 @@ const server = createServer(async (request, response) => {
|
|
|
546
563
|
editor.lastSeen = Date.now();
|
|
547
564
|
});
|
|
548
565
|
deliverNext(editor);
|
|
549
|
-
if (editor.poll
|
|
566
|
+
if (editor.poll && waitMs === 0) {
|
|
567
|
+
endEditorPoll(editor);
|
|
568
|
+
} else if (editor.poll) editor.pollTimer = setTimeout(() => {
|
|
550
569
|
if (editor.poll !== response) return;
|
|
551
570
|
endEditorPoll(editor);
|
|
552
|
-
},
|
|
571
|
+
}, waitMs);
|
|
553
572
|
editor.pollTimer?.unref();
|
|
554
573
|
return;
|
|
555
574
|
}
|
|
@@ -650,7 +669,11 @@ setInterval(() => {
|
|
|
650
669
|
clientsChanged = true;
|
|
651
670
|
}
|
|
652
671
|
for (const [id, editor] of editors) {
|
|
653
|
-
if (
|
|
672
|
+
if (
|
|
673
|
+
editor.lastSeen >= now - EDITOR_TTL_MS
|
|
674
|
+
|| (editor.poll && !editor.poll.destroyed && !editor.poll.writableEnded)
|
|
675
|
+
|| editorHasInflightCommand(id)
|
|
676
|
+
) continue;
|
|
654
677
|
disconnectEditor(id, "Browser editor connection expired.");
|
|
655
678
|
}
|
|
656
679
|
if (clientsChanged) broadcastAgents();
|
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();
|
|
@@ -15,6 +15,7 @@ const targetProject = { editSessionId, projectId: optionalId, expectedRevision }
|
|
|
15
15
|
const targetSlide = { editSessionId, projectId: optionalId, slideId: optionalId, expectedRevision };
|
|
16
16
|
const textFields = {
|
|
17
17
|
text: z.string().max(4000).optional(), x: unit.optional(), y: unit.optional(), width: positiveUnit.optional(), height: positiveUnit.optional(),
|
|
18
|
+
role: z.enum(["title", "subtitle", "body", "caption"]).optional().describe("Semantic size role. Recommended ranges: title 92-124, subtitle 68-84, body 54-68, caption 44-52."),
|
|
18
19
|
size: z.number().min(20).max(180).optional(), style: z.enum(["plain", "outline", "boxed"]).optional(),
|
|
19
20
|
outlineWidth: z.number().min(0).max(40).optional(), color: color.optional(), background: z.enum(["white", "black"]).optional(),
|
|
20
21
|
backgroundShape: z.enum(["lines", "full"]).optional(), align: z.enum(["left", "center", "right"]).optional(),
|
|
@@ -34,7 +35,7 @@ function textResult(value, summary = value) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function compactMutation(value) {
|
|
37
|
-
const keys = ["id", "editSessionId", "editorId", "projectId", "slideId", "revision", "leaseExpiresAt", "purpose", "released", "opened", "createdSlideId", "createdTextId", "createdImageId", "createdLayers", "assetId", "deletedAssetId", "deletedProjectId", "deletedSlideId", "deletedLayerIds", "updatedTextIds", "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"];
|
|
38
39
|
return Object.fromEntries(keys.flatMap((key) => value?.[key] == null ? [] : [[key, value[key]]]));
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -55,7 +56,7 @@ function operationLabel(toolName) {
|
|
|
55
56
|
create_project: "Creating a project…", update_project: "Updating the project…", delete_project: "Deleting a project…",
|
|
56
57
|
open_project: "Opening a project…", add_slide: "Adding a slide…", update_slide: "Updating a slide…",
|
|
57
58
|
duplicate_slide: "Duplicating a slide…", reorder_slides: "Reordering slides…", delete_slide: "Deleting a slide…",
|
|
58
|
-
add_text: "Adding text…", update_text: "Updating text…", import_asset: "Importing a local image…",
|
|
59
|
+
add_text: "Adding text…", update_text: "Updating text…", fit_text_boxes: "Fitting text boxes…", import_asset: "Importing a local image…",
|
|
59
60
|
update_asset: "Updating an image asset…", delete_asset: "Deleting an image asset…", add_image: "Placing an image…",
|
|
60
61
|
update_image: "Updating an image…", delete_layers: "Deleting layers…", duplicate_layers: "Duplicating layers…",
|
|
61
62
|
reorder_layers: "Reordering layers…", undo: "Undoing the last edit…", redo: "Redoing the last edit…",
|
|
@@ -85,7 +86,7 @@ async function prepareOperation(companion, toolName, args) {
|
|
|
85
86
|
const type = ({
|
|
86
87
|
create_project: "project.create", open_project: "project.open", update_project: "project.update", delete_project: "project.delete",
|
|
87
88
|
add_slide: "slide.add", update_slide: "slide.update", duplicate_slide: "slide.duplicate", reorder_slides: "slide.reorder", delete_slide: "slide.delete",
|
|
88
|
-
add_text: "text.add", update_text: "text.update", import_asset: "asset.import", update_asset: "asset.update", delete_asset: "asset.delete",
|
|
89
|
+
add_text: "text.add", update_text: "text.update", fit_text_boxes: "text.fit", import_asset: "asset.import", update_asset: "asset.update", delete_asset: "asset.delete",
|
|
89
90
|
add_image: "image.add", update_image: "image.update", delete_layers: "layer.delete", duplicate_layers: "layer.duplicate", reorder_layers: "layer.reorder",
|
|
90
91
|
undo: "history.undo", redo: "history.redo", set_view: "view.update", render_slide: "slide.render", inspect_editor: "editor.inspect",
|
|
91
92
|
})[toolName];
|
|
@@ -98,7 +99,7 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
98
99
|
let guidanceRead = false;
|
|
99
100
|
let identifiedAs = null;
|
|
100
101
|
const server = new McpServer({ name: PACKAGE_NAME, version: PACKAGE_VERSION }, {
|
|
101
|
-
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, 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.`,
|
|
102
103
|
capabilities: { tools: {}, resources: {} },
|
|
103
104
|
});
|
|
104
105
|
|
|
@@ -148,19 +149,20 @@ export async function createSlideStudioMcpServer(companion) {
|
|
|
148
149
|
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 });
|
|
149
150
|
register("show_notification", "Show a short visual notification in a connected editor for status or marketing demos.", z.object({ editSessionId, message: z.string().min(1).max(240), tone: z.enum(["agent", "success", "info", "error"]).default("agent") }).strict(), ({ editSessionId, ...args }) => companion.call("notify", { ...args, editSessionId }), { destructiveHint: false, idempotentHint: false });
|
|
150
151
|
|
|
151
|
-
register("create_project", "Create an empty project
|
|
152
|
-
register("open_project", "
|
|
152
|
+
register("create_project", "Create an empty project without changing the user's current browser view. Its dashboard card appears live when the dashboard is open.", z.object({ editSessionId, name: z.string().min(1).max(160) }).strict(), (args) => browserOperation(companion, "create_project", args), { destructiveHint: false });
|
|
153
|
+
register("open_project", "Explicitly navigate the browser to a project and optionally a specific slide without changing content. Use only when the user asks to show it.", z.object({ editSessionId, projectId: id, slideId: optionalId }).strict(), (args) => browserOperation(companion, "open_project", args), { destructiveHint: false, idempotentHint: true });
|
|
153
154
|
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 });
|
|
154
155
|
register("delete_project", "Delete a project from browser storage.", z.object({ ...targetProject, projectId: id }).strict(), (args) => browserOperation(companion, "delete_project", args), { destructiveHint: true });
|
|
155
156
|
|
|
156
|
-
register("add_slide", "Add
|
|
157
|
-
register("update_slide", "Rename a slide, replace its background, or change background pan/zoom. The browser
|
|
158
|
-
register("duplicate_slide", "Duplicate a slide with all layers
|
|
157
|
+
register("add_slide", "Add a slide using a solid color or local background image path. The browser follows it only when that project is already visible.", 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 });
|
|
158
|
+
register("update_slide", "Rename a slide, replace its background, or change background pan/zoom. The browser follows it only when that project is already visible.", 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 });
|
|
159
|
+
register("duplicate_slide", "Duplicate a slide with all layers. The browser follows the copy only when that project is already visible.", z.object({ ...targetSlide, name: z.string().max(160).optional() }).strict(), (args) => browserOperation(companion, "duplicate_slide", args), { destructiveHint: false });
|
|
159
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 });
|
|
160
161
|
register("delete_slide", "Delete one slide.", z.object({ ...targetSlide, slideId: id }).strict(), (args) => browserOperation(companion, "delete_slide", args), { destructiveHint: true });
|
|
161
162
|
|
|
162
|
-
register("add_text", "Add a text layer.
|
|
163
|
-
register("update_text", "Update one or more text layers
|
|
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 });
|
|
164
166
|
|
|
165
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 });
|
|
166
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`),
|