html2pptx-local-mcp 1.1.20 → 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.
Files changed (32) hide show
  1. package/app/docs/content.js +50 -16
  2. package/cli/dist/commands/edit.d.ts +1 -1
  3. package/cli/dist/commands/edit.js +30 -13
  4. package/cli/dist/index.js +0 -0
  5. package/lib/local-editor-server.js +316 -0
  6. package/lib/local-editor-state.js +45 -0
  7. package/lib/local-slide-editor-launcher.js +19 -18
  8. package/lib/pptx-studio-mcp-core.js +15 -9
  9. package/local-editor-app/app/api/edit-slide/local-health/route.js +16 -0
  10. package/local-editor-app/app/edit-slide/edit-slide-client.jsx +13153 -0
  11. package/local-editor-app/app/edit-slide/page.jsx +13 -0
  12. package/local-editor-app/app/globals.css +4 -0
  13. package/local-editor-app/app/layout.jsx +14 -0
  14. package/local-editor-app/components/studio/edit-property-panel.jsx +1061 -0
  15. package/local-editor-app/lib/edit-panel-value-normalizer.js +97 -0
  16. package/local-editor-app/lib/edit-slide-editor-helpers.js +120 -0
  17. package/local-editor-app/lib/edit-slide-url-security.js +247 -0
  18. package/local-editor-app/next.config.mjs +31 -0
  19. package/local-editor-app/package.json +7 -0
  20. package/mcp/pptx-studio-mcp-server.mjs +1 -1
  21. package/package.json +13 -2
  22. package/public/skills/html2pptx/SKILL.md +635 -0
  23. package/public/skills/html2pptx/references/automation-contract.md +68 -0
  24. package/public/skills/html2pptx/references/input-contract.md +107 -0
  25. package/public/skills/html2pptx/references/japanese-slide-design.md +273 -0
  26. package/public/skills/html2pptx/references/rewrite-patterns.md +218 -0
  27. package/public/skills/icon-generator/SKILL.md +133 -0
  28. package/public/skills/open-slide/SKILL.md +160 -0
  29. package/public/skills/publish-template/SKILL.md +215 -0
  30. package/public/skills/register-template/SKILL.md +142 -0
  31. package/scripts/install-mcp.mjs +28 -2
  32. package/scripts/install-skills.mjs +82 -0
@@ -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, readFile, realpath, stat } from 'node:fs/promises';
5
- import { dirname, extname, join, relative, resolve, sep } from 'node:path';
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 || process.env.HTML2PPTX_EDITOR_BASE_URL || await readRegisteredEditorBaseUrl(file.root),
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 registered. Start it with `node scripts/dev-studio.mjs`, then retry, or pass baseUrl as a loopback editor URL.',
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 async function readRegisteredEditorBaseUrl(root = process.cwd()) {
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. Start the html2pptx CLI local edit 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.',
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 launcher uses the local editor URL registered by node scripts/dev-studio.mjs. Hosted editor URLs are not allowed for local file editing.',
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. Open only a loopback editor UI. First ensure the local editor app is running with node scripts/dev-studio.mjs; it will choose an available port and register the editor URL under .html2pptx/edit-slide/editor-server.json in the current project.',
601
- '5. Call html2pptx_open_local_slide_editor with { filePath: <path> }. Pass baseUrl only when the user supplied a specific loopback editor URL. Do not use https://html2pptx.app/edit-slide for local file editing.',
602
- '6. 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>. If the CLI reports that the editor is not registered, start node scripts/dev-studio.mjs first.',
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 CLI localhost bridge and does not publish or upload the HTML file.',
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. Start node scripts/dev-studio.mjs first; it chooses an available local port and registers the editor URL under .html2pptx/edit-slide/ in the current project. Then call html2pptx_open_local_slide_editor with the project-relative file path. Hosted https://html2pptx.app/edit-slide is not allowed for local file editing.',
719
- 'If the local editor app is not running, ask the user to start it with node scripts/dev-studio.mjs rather than falling back to a hosted editor URL.',
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
+ }