html2pptx-local-mcp 1.1.19 → 1.1.21
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/app/docs/content.js +57 -23
- package/cli/dist/commands/edit.d.ts +1 -1
- package/cli/dist/commands/edit.js +231 -3
- package/cli/dist/index.js +0 -0
- package/lib/local-editor-server.js +316 -0
- package/lib/local-editor-state.js +45 -0
- package/lib/local-slide-editor-launcher.js +19 -18
- package/lib/pptx-studio-mcp-core.js +15 -9
- package/local-editor-app/app/api/edit-slide/local-health/route.js +16 -0
- package/local-editor-app/app/edit-slide/edit-slide-client.jsx +13153 -0
- package/local-editor-app/app/edit-slide/page.jsx +13 -0
- package/local-editor-app/app/globals.css +4 -0
- package/local-editor-app/app/layout.jsx +14 -0
- package/local-editor-app/components/studio/edit-property-panel.jsx +1061 -0
- package/local-editor-app/lib/edit-panel-value-normalizer.js +97 -0
- package/local-editor-app/lib/edit-slide-editor-helpers.js +120 -0
- package/local-editor-app/lib/edit-slide-url-security.js +247 -0
- package/local-editor-app/next.config.mjs +31 -0
- package/local-editor-app/package.json +7 -0
- package/mcp/pptx-studio-mcp-server.mjs +1 -1
- package/package.json +16 -3
- package/public/skills/html2pptx/SKILL.md +635 -0
- package/public/skills/html2pptx/references/automation-contract.md +68 -0
- package/public/skills/html2pptx/references/input-contract.md +107 -0
- package/public/skills/html2pptx/references/japanese-slide-design.md +273 -0
- package/public/skills/html2pptx/references/rewrite-patterns.md +218 -0
- package/public/skills/icon-generator/SKILL.md +133 -0
- package/public/skills/open-slide/SKILL.md +160 -0
- package/public/skills/publish-template/SKILL.md +215 -0
- package/public/skills/register-template/SKILL.md +142 -0
- package/scripts/extract-html2pptx-comments.mjs +172 -0
- package/scripts/install-mcp.mjs +58 -13
- package/scripts/install-skills.mjs +82 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { access } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import {
|
|
8
|
+
readEditorServerState,
|
|
9
|
+
writeEditorServerState,
|
|
10
|
+
} from './local-editor-state.js';
|
|
11
|
+
|
|
12
|
+
const AUTO_PORT = 0;
|
|
13
|
+
const DEFAULT_READY_TIMEOUT_MS = 30000;
|
|
14
|
+
|
|
15
|
+
export function createLocalEditorServerManager(options = {}) {
|
|
16
|
+
const sessions = new Map();
|
|
17
|
+
const launchTimeoutMs = Number.isFinite(options.launchTimeoutMs)
|
|
18
|
+
? Math.max(1000, Math.floor(options.launchTimeoutMs))
|
|
19
|
+
: DEFAULT_READY_TIMEOUT_MS;
|
|
20
|
+
|
|
21
|
+
async function ensure(root = process.cwd(), input = {}) {
|
|
22
|
+
const projectRoot = resolve(root);
|
|
23
|
+
const requestedPort = normalizePort(input.editorPort ?? process.env.HTML2PPTX_STUDIO_PORT ?? process.env.PORT, AUTO_PORT);
|
|
24
|
+
|
|
25
|
+
const registered = await readEditorServerState(projectRoot);
|
|
26
|
+
if (registered && await isEditorServer(registered.port)) {
|
|
27
|
+
return {
|
|
28
|
+
baseUrl: registered.baseUrl,
|
|
29
|
+
port: registered.port,
|
|
30
|
+
reused: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const existing = sessions.get(projectRoot);
|
|
35
|
+
if (existing && !existing.child.killed && existing.child.exitCode == null && await isEditorServer(existing.port)) {
|
|
36
|
+
await writeEditorServerState(projectRoot, {
|
|
37
|
+
baseUrl: existing.baseUrl,
|
|
38
|
+
port: existing.port,
|
|
39
|
+
pid: existing.child.pid,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
baseUrl: existing.baseUrl,
|
|
43
|
+
port: existing.port,
|
|
44
|
+
reused: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (requestedPort !== AUTO_PORT && await isEditorServer(requestedPort)) {
|
|
49
|
+
const baseUrl = `http://localhost:${requestedPort}`;
|
|
50
|
+
await writeEditorServerState(projectRoot, {
|
|
51
|
+
baseUrl,
|
|
52
|
+
port: requestedPort,
|
|
53
|
+
pid: null,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
baseUrl,
|
|
57
|
+
port: requestedPort,
|
|
58
|
+
reused: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const appRoot = await resolveEditorAppRoot(options);
|
|
63
|
+
const port = requestedPort === AUTO_PORT ? await getEphemeralPort() : await pickOpenPort(requestedPort);
|
|
64
|
+
const baseUrl = `http://localhost:${port}`;
|
|
65
|
+
const invocation = await resolveNextInvocation(appRoot, options);
|
|
66
|
+
|
|
67
|
+
const child = spawn(invocation.command, [
|
|
68
|
+
...invocation.baseArgs,
|
|
69
|
+
'dev',
|
|
70
|
+
'--webpack',
|
|
71
|
+
'-p',
|
|
72
|
+
String(port),
|
|
73
|
+
], {
|
|
74
|
+
cwd: appRoot,
|
|
75
|
+
env: {
|
|
76
|
+
...process.env,
|
|
77
|
+
HTML2PPTX_EDITOR_BASE_URL: baseUrl,
|
|
78
|
+
NEXT_TELEMETRY_DISABLED: '1',
|
|
79
|
+
PORT: String(port),
|
|
80
|
+
},
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const session = {
|
|
85
|
+
child,
|
|
86
|
+
appRoot,
|
|
87
|
+
baseUrl,
|
|
88
|
+
port,
|
|
89
|
+
stdout: '',
|
|
90
|
+
stderr: '',
|
|
91
|
+
startedAt: new Date().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
sessions.set(projectRoot, session);
|
|
94
|
+
|
|
95
|
+
child.stdout.setEncoding('utf8');
|
|
96
|
+
child.stderr.setEncoding('utf8');
|
|
97
|
+
child.stdout.on('data', (chunk) => {
|
|
98
|
+
session.stdout += chunk;
|
|
99
|
+
});
|
|
100
|
+
child.stderr.on('data', (chunk) => {
|
|
101
|
+
session.stderr += chunk;
|
|
102
|
+
});
|
|
103
|
+
child.once('exit', () => {
|
|
104
|
+
if (sessions.get(projectRoot) === session) sessions.delete(projectRoot);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await waitForEditorServer(session, launchTimeoutMs);
|
|
108
|
+
await writeEditorServerState(projectRoot, {
|
|
109
|
+
baseUrl,
|
|
110
|
+
port,
|
|
111
|
+
pid: child.pid,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
baseUrl,
|
|
116
|
+
port,
|
|
117
|
+
pid: child.pid,
|
|
118
|
+
reused: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stopAll() {
|
|
123
|
+
for (const session of sessions.values()) {
|
|
124
|
+
session.child.kill('SIGTERM');
|
|
125
|
+
}
|
|
126
|
+
sessions.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ensure,
|
|
131
|
+
stopAll,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const localEditorServerManager = createLocalEditorServerManager();
|
|
136
|
+
|
|
137
|
+
export async function ensureLocalEditorServer(root = process.cwd(), options = {}) {
|
|
138
|
+
return localEditorServerManager.ensure(root, options);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function resolveEditorAppRoot(options = {}) {
|
|
142
|
+
if (options.appRoot) return resolve(options.appRoot);
|
|
143
|
+
if (process.env.HTML2PPTX_EDITOR_APP_ROOT) return resolve(process.env.HTML2PPTX_EDITOR_APP_ROOT);
|
|
144
|
+
|
|
145
|
+
const packageRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
146
|
+
const packagedAppRoot = join(packageRoot, 'local-editor-app');
|
|
147
|
+
if (existsSync(join(packagedAppRoot, 'app', 'edit-slide', 'page.jsx'))) {
|
|
148
|
+
return packagedAppRoot;
|
|
149
|
+
}
|
|
150
|
+
if (existsSync(join(packageRoot, 'app', 'edit-slide', 'page.jsx'))) {
|
|
151
|
+
return packageRoot;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(
|
|
155
|
+
'html2pptx local editor app is not bundled. Reinstall html2pptx-local-mcp or pass HTML2PPTX_EDITOR_APP_ROOT.',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function resolveNextInvocation(appRoot, options = {}) {
|
|
160
|
+
if (options.command) {
|
|
161
|
+
return {
|
|
162
|
+
command: options.command,
|
|
163
|
+
baseArgs: Array.isArray(options.baseArgs) ? options.baseArgs : [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (process.env.HTML2PPTX_NEXT_BIN) {
|
|
167
|
+
return {
|
|
168
|
+
command: process.execPath,
|
|
169
|
+
baseArgs: [process.env.HTML2PPTX_NEXT_BIN],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const nextBin = await findUp(join('node_modules', 'next', 'dist', 'bin', 'next'), appRoot);
|
|
174
|
+
if (nextBin) {
|
|
175
|
+
return {
|
|
176
|
+
command: process.execPath,
|
|
177
|
+
baseArgs: [nextBin],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
command: 'npx',
|
|
183
|
+
baseArgs: ['--yes', 'next'],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function isEditorServer(port) {
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
190
|
+
try {
|
|
191
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/edit-slide/local-health`, {
|
|
192
|
+
headers: { accept: 'application/json' },
|
|
193
|
+
signal: controller.signal,
|
|
194
|
+
});
|
|
195
|
+
if (!response.ok) return false;
|
|
196
|
+
const payload = await response.json();
|
|
197
|
+
return payload?.app === 'html2pptx-local-editor' && payload?.ok === true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function waitForEditorServer(session, timeoutMs) {
|
|
206
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
207
|
+
const startedAt = Date.now();
|
|
208
|
+
const tick = async () => {
|
|
209
|
+
if (await isEditorServer(session.port)) {
|
|
210
|
+
resolvePromise();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (session.child.exitCode != null || session.child.killed) {
|
|
214
|
+
rejectPromise(new Error([
|
|
215
|
+
`html2pptx editor server exited before it was ready on ${session.baseUrl}.`,
|
|
216
|
+
session.stderr.trim() || session.stdout.trim(),
|
|
217
|
+
].filter(Boolean).join(' ')));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
221
|
+
session.child.kill('SIGTERM');
|
|
222
|
+
rejectPromise(new Error([
|
|
223
|
+
`Timed out after ${timeoutMs}ms while starting html2pptx editor server on ${session.baseUrl}.`,
|
|
224
|
+
session.stderr.trim(),
|
|
225
|
+
].filter(Boolean).join(' ')));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setTimeout(tick, 350);
|
|
229
|
+
};
|
|
230
|
+
tick();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function findUp(relativePath, startDir) {
|
|
235
|
+
let dir = resolve(startDir);
|
|
236
|
+
while (true) {
|
|
237
|
+
const candidate = join(dir, relativePath);
|
|
238
|
+
if (await exists(candidate)) return candidate;
|
|
239
|
+
const parent = dirname(dir);
|
|
240
|
+
if (parent === dir) return null;
|
|
241
|
+
dir = parent;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function exists(path) {
|
|
246
|
+
try {
|
|
247
|
+
await access(path);
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function pickOpenPort(preferredPort) {
|
|
255
|
+
if (!(await isPortOpen(preferredPort))) return preferredPort;
|
|
256
|
+
|
|
257
|
+
for (let port = preferredPort + 1; port < preferredPort + 100; port += 1) {
|
|
258
|
+
if (!(await isPortOpen(port))) return port;
|
|
259
|
+
}
|
|
260
|
+
return getEphemeralPort();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getEphemeralPort() {
|
|
264
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
265
|
+
const server = net.createServer();
|
|
266
|
+
server.once('error', rejectPromise);
|
|
267
|
+
server.listen(0, '127.0.0.1', () => {
|
|
268
|
+
const address = server.address();
|
|
269
|
+
server.close(() => {
|
|
270
|
+
if (!address || typeof address === 'string') {
|
|
271
|
+
rejectPromise(new Error('Failed to allocate an editor port.'));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
resolvePromise(address.port);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isPortOpen(port) {
|
|
281
|
+
return new Promise((resolvePromise) => {
|
|
282
|
+
const socket = net.createConnection({ host: '127.0.0.1', port });
|
|
283
|
+
socket.once('connect', () => {
|
|
284
|
+
socket.end();
|
|
285
|
+
resolvePromise(true);
|
|
286
|
+
});
|
|
287
|
+
socket.once('error', () => {
|
|
288
|
+
socket.destroy();
|
|
289
|
+
resolvePromise(false);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function normalizePort(value, fallback) {
|
|
295
|
+
if (value == null || value === '') return fallback;
|
|
296
|
+
const port = Number.parseInt(String(value), 10);
|
|
297
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
298
|
+
throw new Error('editorPort must be an integer from 0 to 65535.');
|
|
299
|
+
}
|
|
300
|
+
return port;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function installExitHandlers() {
|
|
304
|
+
const stop = () => localEditorServerManager.stopAll();
|
|
305
|
+
process.once('exit', stop);
|
|
306
|
+
process.once('SIGINT', () => {
|
|
307
|
+
stop();
|
|
308
|
+
process.exit(130);
|
|
309
|
+
});
|
|
310
|
+
process.once('SIGTERM', () => {
|
|
311
|
+
stop();
|
|
312
|
+
process.exit(143);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
installExitHandlers();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const EDITOR_SERVER_STATE_FILE = '.html2pptx/edit-slide/editor-server.json';
|
|
5
|
+
export const LEGACY_EDITOR_SERVER_STATE_FILE = '.open-slide/editor-server.json';
|
|
6
|
+
|
|
7
|
+
export async function readEditorServerState(root = process.cwd()) {
|
|
8
|
+
for (const stateFile of [EDITOR_SERVER_STATE_FILE, LEGACY_EDITOR_SERVER_STATE_FILE]) {
|
|
9
|
+
try {
|
|
10
|
+
const raw = await readFile(join(root, stateFile), 'utf8');
|
|
11
|
+
const state = JSON.parse(raw);
|
|
12
|
+
const port = Number.parseInt(state?.port, 10);
|
|
13
|
+
const baseUrl = typeof state?.baseUrl === 'string' ? state.baseUrl : '';
|
|
14
|
+
if (!Number.isInteger(port) || port <= 0 || !baseUrl.startsWith('http://')) continue;
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
port,
|
|
18
|
+
baseUrl,
|
|
19
|
+
};
|
|
20
|
+
} catch {
|
|
21
|
+
// Try the next known state location.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readRegisteredEditorBaseUrl(root = process.cwd()) {
|
|
28
|
+
const state = await readEditorServerState(root);
|
|
29
|
+
return state?.baseUrl || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function writeEditorServerState(root, { baseUrl, port, pid, managedBy = 'html2pptx-local-mcp' }) {
|
|
33
|
+
await mkdir(join(root, '.html2pptx', 'edit-slide'), { recursive: true });
|
|
34
|
+
await writeFile(
|
|
35
|
+
join(root, EDITOR_SERVER_STATE_FILE),
|
|
36
|
+
`${JSON.stringify({
|
|
37
|
+
baseUrl,
|
|
38
|
+
port,
|
|
39
|
+
pid,
|
|
40
|
+
managedBy,
|
|
41
|
+
updatedAt: new Date().toISOString(),
|
|
42
|
+
}, null, 2)}\n`,
|
|
43
|
+
'utf8',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { access,
|
|
5
|
-
import { dirname, extname,
|
|
4
|
+
import { access, realpath, stat } from 'node:fs/promises';
|
|
5
|
+
import { dirname, extname, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { ensureLocalEditorServer } from './local-editor-server.js';
|
|
7
8
|
|
|
8
9
|
const ALLOWED_EXTENSIONS = new Set(['.html', '.htm']);
|
|
9
|
-
const EDITOR_SERVER_STATE_FILE = '.html2pptx/edit-slide/editor-server.json';
|
|
10
|
-
const LEGACY_EDITOR_SERVER_STATE_FILE = '.open-slide/editor-server.json';
|
|
11
10
|
const DEFAULT_LAUNCH_TIMEOUT_MS = 10000;
|
|
12
11
|
|
|
13
12
|
export function createLocalSlideEditorManager(options = {}) {
|
|
@@ -33,7 +32,12 @@ export function createLocalSlideEditorManager(options = {}) {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const baseUrl = normalizeEditorBaseUrl(
|
|
36
|
-
input.baseUrl ||
|
|
35
|
+
input.baseUrl ||
|
|
36
|
+
process.env.HTML2PPTX_EDITOR_BASE_URL ||
|
|
37
|
+
await resolveEditorBaseUrl(file.root, {
|
|
38
|
+
...input,
|
|
39
|
+
ensureEditorServer: options.ensureEditorServer,
|
|
40
|
+
}),
|
|
37
41
|
);
|
|
38
42
|
const port = normalizePort(input.port, 0);
|
|
39
43
|
const openBrowser = input.openBrowser === true;
|
|
@@ -148,6 +152,14 @@ export function createLocalSlideEditorManager(options = {}) {
|
|
|
148
152
|
|
|
149
153
|
export const localSlideEditorManager = createLocalSlideEditorManager();
|
|
150
154
|
|
|
155
|
+
async function resolveEditorBaseUrl(root, input = {}) {
|
|
156
|
+
const ensureEditorServer = typeof input.ensureEditorServer === 'function'
|
|
157
|
+
? input.ensureEditorServer
|
|
158
|
+
: ensureLocalEditorServer;
|
|
159
|
+
const ensured = await ensureEditorServer(root, input);
|
|
160
|
+
return ensured.baseUrl;
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
export async function resolveEditableFile(filePath, cwd = process.cwd()) {
|
|
152
164
|
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
153
165
|
throw new Error('filePath must be a non-empty .html or .htm path.');
|
|
@@ -305,7 +317,7 @@ export function normalizeEditorBaseUrl(raw) {
|
|
|
305
317
|
try {
|
|
306
318
|
if (!raw) {
|
|
307
319
|
throw new Error(
|
|
308
|
-
'Local editor UI is not
|
|
320
|
+
'Local editor UI is not available. Reinstall html2pptx-local-mcp or pass baseUrl as a loopback editor URL.',
|
|
309
321
|
);
|
|
310
322
|
}
|
|
311
323
|
const url = new URL(raw);
|
|
@@ -330,18 +342,7 @@ function isAllowedEditorBaseUrl(url) {
|
|
|
330
342
|
return isLoopbackHostname(url.hostname);
|
|
331
343
|
}
|
|
332
344
|
|
|
333
|
-
export
|
|
334
|
-
for (const stateFile of [EDITOR_SERVER_STATE_FILE, LEGACY_EDITOR_SERVER_STATE_FILE]) {
|
|
335
|
-
try {
|
|
336
|
-
const raw = await readFile(join(root, stateFile), 'utf8');
|
|
337
|
-
const state = JSON.parse(raw);
|
|
338
|
-
if (typeof state?.baseUrl === 'string') return state.baseUrl;
|
|
339
|
-
} catch {
|
|
340
|
-
// Try the next known state location.
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return null;
|
|
344
|
-
}
|
|
345
|
+
export { readRegisteredEditorBaseUrl } from './local-editor-state.js';
|
|
345
346
|
|
|
346
347
|
function isLoopbackHostname(hostname) {
|
|
347
348
|
const host = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
|
|
@@ -416,7 +416,7 @@ export const LOCAL_TOOL_DEFINITIONS = [
|
|
|
416
416
|
name: 'html2pptx_open_local_slide_editor',
|
|
417
417
|
title: 'Open Local Slide Editor',
|
|
418
418
|
description:
|
|
419
|
-
'Local stdio MCP only.
|
|
419
|
+
'Local stdio MCP only. Ensure the loopback html2pptx editor UI is running, start the local file bridge for a .html/.htm slide file inside the MCP server working directory, then return the tokenized editor URL. This does not publish or upload the HTML file.',
|
|
420
420
|
inputSchema: {
|
|
421
421
|
type: 'object',
|
|
422
422
|
properties: {
|
|
@@ -427,7 +427,13 @@ export const LOCAL_TOOL_DEFINITIONS = [
|
|
|
427
427
|
},
|
|
428
428
|
baseUrl: {
|
|
429
429
|
type: 'string',
|
|
430
|
-
description: 'Optional editor base URL. Must be a loopback http(s) origin such as http://localhost:<port>. When omitted, the
|
|
430
|
+
description: 'Optional editor base URL. Must be a loopback http(s) origin such as http://localhost:<port>. When omitted, the local MCP reuses or starts its bundled local editor UI. Hosted editor URLs are not allowed for local file editing.',
|
|
431
|
+
},
|
|
432
|
+
editorPort: {
|
|
433
|
+
type: 'integer',
|
|
434
|
+
minimum: 0,
|
|
435
|
+
maximum: 65535,
|
|
436
|
+
description: 'Optional editor UI port. Defaults to 0 so the OS picks an available loopback port when the local MCP starts the editor UI.',
|
|
431
437
|
},
|
|
432
438
|
port: {
|
|
433
439
|
type: 'integer',
|
|
@@ -597,9 +603,9 @@ function buildPromptMessages(name, args) {
|
|
|
597
603
|
'1. Read html2pptx_get_docs with section="html-contract" to confirm the slide HTML contract.',
|
|
598
604
|
'2. Generate slide-safe HTML. Each slide must be a <section class="slide"> element with explicit dimensions; 1600x900 is the default example unless the request needs a different size.',
|
|
599
605
|
`3. Save the complete HTML document to a project-local .html file, preferably "${fileName}" or another clear path under html2pptx/.`,
|
|
600
|
-
'4.
|
|
601
|
-
'5.
|
|
602
|
-
'6. If that
|
|
606
|
+
'4. Call html2pptx_open_local_slide_editor with { filePath: <path> }. The local MCP will reuse or start the loopback editor UI and the localhost file bridge. Pass baseUrl only when the user supplied a specific loopback editor URL. Do not use https://html2pptx.app/edit-slide for local file editing.',
|
|
607
|
+
'5. If that local editor tool is not available, do not pretend the editor was opened. Return the saved HTML path and the local-UI CLI fallback command: npx --yes https://html2pptx.app/downloads/html2pptx-cli-0.4.0.tgz edit <path>.',
|
|
608
|
+
'6. If the CLI fallback reports that the editor is not available, use the local MCP package or run the command from an html2pptx source checkout.',
|
|
603
609
|
'7. Do not export PPTX from this prompt. In the editor, the export button should only show a prompt telling the user to ask Claude Code or another agent to use the html2pptx skills for PowerPoint export.',
|
|
604
610
|
].join('\n'),
|
|
605
611
|
},
|
|
@@ -679,7 +685,7 @@ function buildServerInstructions(client = {}, { localOnly = false } = {}) {
|
|
|
679
685
|
'Use this stdio server only for local visual editing. Use the remote html2pptx MCP server for PPTX export, docs, usage, templates, and publishing workflows.',
|
|
680
686
|
'When the user asks to open, preview, no-code edit, visually edit, or launch an editing screen for generated slides, save the deck as a local .html/.htm file first, usually under html2pptx/<name>.html.',
|
|
681
687
|
'Then call html2pptx_open_local_slide_editor with the project-relative file path.',
|
|
682
|
-
'The tool starts the html2pptx
|
|
688
|
+
'The tool starts or reuses the loopback editor UI, starts the html2pptx localhost file bridge, and does not publish or upload the HTML file.',
|
|
683
689
|
].join('\n');
|
|
684
690
|
}
|
|
685
691
|
|
|
@@ -715,9 +721,8 @@ function buildServerInstructions(client = {}, { localOnly = false } = {}) {
|
|
|
715
721
|
'',
|
|
716
722
|
'Local visual editing is available in this stdio MCP server.',
|
|
717
723
|
'When the user asks to "open", "preview", "no-code edit", "visually edit", or "launch an editing screen" for generated slides, save the deck as a local .html/.htm file first, usually under html2pptx/<name>.html.',
|
|
718
|
-
'Use only loopback editor UI for local files.
|
|
719
|
-
'
|
|
720
|
-
'The tool starts the existing html2pptx CLI localhost bridge and does not publish or upload the HTML file.',
|
|
724
|
+
'Use only loopback editor UI for local files. Call html2pptx_open_local_slide_editor with the project-relative file path; the local MCP will reuse or start the local editor UI and localhost bridge. Hosted https://html2pptx.app/edit-slide is not allowed for local file editing.',
|
|
725
|
+
'The tool does not publish or upload the HTML file.',
|
|
721
726
|
);
|
|
722
727
|
} else {
|
|
723
728
|
lines.push(
|
|
@@ -911,6 +916,7 @@ export async function executeTool(name, args, client, { sendNotification, progre
|
|
|
911
916
|
const data = await client.openLocalSlideEditor({
|
|
912
917
|
filePath: args.filePath,
|
|
913
918
|
baseUrl: typeof args.baseUrl === 'string' ? args.baseUrl : undefined,
|
|
919
|
+
editorPort: Number.isFinite(args.editorPort) ? args.editorPort : undefined,
|
|
914
920
|
port: Number.isFinite(args.port) ? args.port : undefined,
|
|
915
921
|
openBrowser: args.openBrowser === true,
|
|
916
922
|
reuseExisting: args.reuseExisting !== false,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { isLoopbackRequest } from '../../../../lib/edit-slide-url-security.js';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
export function GET(request) {
|
|
7
|
+
if (!isLoopbackRequest(request.headers)) {
|
|
8
|
+
return NextResponse.json({ ok: false }, { status: 404 });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return NextResponse.json({
|
|
12
|
+
ok: true,
|
|
13
|
+
app: 'html2pptx-local-editor',
|
|
14
|
+
version: 1,
|
|
15
|
+
});
|
|
16
|
+
}
|