ppt-template-reuse 0.4.0-rc.2

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +8 -0
  3. package/README.md +233 -0
  4. package/adapters/README.md +13 -0
  5. package/adapters/claude/.claude-plugin/plugin.json +16 -0
  6. package/adapters/claude/.mcp.json +8 -0
  7. package/adapters/claude/skills/learn-ppt-template/SKILL.md +105 -0
  8. package/adapters/kimi/kimi.plugin.json +12 -0
  9. package/adapters/kimi/skills/learn-ppt-template/SKILL.md +105 -0
  10. package/adapters/kimi-work/mcp.json +8 -0
  11. package/adapters/kimi-work/skills/learn-ppt-template/SKILL.md +105 -0
  12. package/adapters/workbuddy/mcp.json +8 -0
  13. package/adapters/workbuddy/skills/learn-ppt-template/SKILL.md +105 -0
  14. package/package.json +53 -0
  15. package/plugins/ppt-template-reuse/.codex-plugin/plugin.json +38 -0
  16. package/plugins/ppt-template-reuse/.mcp.json +9 -0
  17. package/plugins/ppt-template-reuse/skills/learn-ppt-template/SKILL.md +105 -0
  18. package/plugins/ppt-template-reuse/skills/learn-ppt-template/agents/openai.yaml +6 -0
  19. package/skills/ppt-template-reuse/SKILL.md +105 -0
  20. package/skills/ppt-template-reuse/agents/openai.yaml +6 -0
  21. package/src/cli/ppt-reuse.mjs +203 -0
  22. package/src/core/adaptive-planner.mjs +1192 -0
  23. package/src/core/content-audit.mjs +573 -0
  24. package/src/core/frame-map.mjs +91 -0
  25. package/src/core/index.mjs +38 -0
  26. package/src/core/io.mjs +81 -0
  27. package/src/core/legacy-capsule-import.mjs +382 -0
  28. package/src/core/ooxml-editor.mjs +753 -0
  29. package/src/core/paths.mjs +60 -0
  30. package/src/core/planner-lib.mjs +59 -0
  31. package/src/core/pptx-package.mjs +227 -0
  32. package/src/core/renderer.mjs +402 -0
  33. package/src/core/source-extractor.mjs +412 -0
  34. package/src/core/template-inspector.mjs +489 -0
  35. package/src/core/template-intelligence.mjs +675 -0
  36. package/src/core/universal-engine.mjs +1075 -0
  37. package/src/index.mjs +2 -0
  38. package/src/install/installer.mjs +293 -0
  39. package/src/mcp/server.mjs +377 -0
package/src/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./core/index.mjs";
2
+ export { createMcpServer } from "./mcp/server.mjs";
@@ -0,0 +1,293 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { exists, writeJsonAtomic } from "../core/io.mjs";
7
+ import {
8
+ migrateLegacyCapsules,
9
+ resolveEngineHome,
10
+ } from "../core/paths.mjs";
11
+
12
+ const PACKAGE_ROOT = path.resolve(
13
+ path.dirname(fileURLToPath(import.meta.url)),
14
+ "../..",
15
+ );
16
+ const SKILL_SOURCE = path.join(
17
+ PACKAGE_ROOT,
18
+ "skills",
19
+ "ppt-template-reuse",
20
+ "SKILL.md",
21
+ );
22
+ const SERVER_COMMAND =
23
+ process.platform === "win32"
24
+ ? "ppt-template-reuse-mcp.cmd"
25
+ : "ppt-template-reuse-mcp";
26
+
27
+ function stripJsonComments(value) {
28
+ return value
29
+ .replace(/^\uFEFF/, "")
30
+ .replace(/\/\*[\s\S]*?\*\//g, "")
31
+ .replace(/(^|[^:\\])\/\/.*$/gm, "$1")
32
+ .replace(/,\s*([}\]])/g, "$1");
33
+ }
34
+
35
+ async function readJsonc(candidate, fallback = {}) {
36
+ if (!(await exists(candidate))) return fallback;
37
+ return JSON.parse(stripJsonComments(await fs.readFile(candidate, "utf8")));
38
+ }
39
+
40
+ async function backup(candidate) {
41
+ if (!(await exists(candidate))) return null;
42
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
43
+ const output = `${candidate}.backup-${stamp}`;
44
+ await fs.copyFile(candidate, output);
45
+ return output;
46
+ }
47
+
48
+ async function writeIfChanged(candidate, content, { dryRun = false } = {}) {
49
+ const prior = (await exists(candidate))
50
+ ? await fs.readFile(candidate, "utf8")
51
+ : null;
52
+ if (prior === content) return { path: candidate, changed: false, backup: null };
53
+ if (dryRun) return { path: candidate, changed: true, dryRun: true, backup: null };
54
+ await fs.mkdir(path.dirname(candidate), { recursive: true });
55
+ const saved = await backup(candidate);
56
+ const temporary = `${candidate}.${process.pid}.tmp`;
57
+ await fs.writeFile(temporary, content, "utf8");
58
+ await fs.rename(temporary, candidate);
59
+ return { path: candidate, changed: true, backup: saved };
60
+ }
61
+
62
+ async function mergeJsonConfig(candidate, mutator, options) {
63
+ const current = await readJsonc(candidate, {});
64
+ const next = structuredClone(current);
65
+ mutator(next);
66
+ return writeIfChanged(candidate, `${JSON.stringify(next, null, 2)}\n`, options);
67
+ }
68
+
69
+ async function mergeTomlMcp(candidate, options) {
70
+ const block = [
71
+ "[mcp_servers.ppt-template-reuse]",
72
+ `command = ${JSON.stringify(SERVER_COMMAND)}`,
73
+ "args = []",
74
+ ].join("\n");
75
+ const current = (await exists(candidate))
76
+ ? await fs.readFile(candidate, "utf8")
77
+ : "";
78
+ const pattern =
79
+ /(?:^|\n)\[mcp_servers\.ppt-template-reuse\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/;
80
+ const without = current.replace(pattern, "\n").trimEnd();
81
+ const next = `${without}${without ? "\n\n" : ""}${block}\n`;
82
+ return writeIfChanged(candidate, next, options);
83
+ }
84
+
85
+ async function copySkill(target, options) {
86
+ const source = await fs.readFile(SKILL_SOURCE, "utf8");
87
+ return writeIfChanged(target, source, options);
88
+ }
89
+
90
+ function hostCandidates(homeRoot) {
91
+ const kimiHome =
92
+ path.resolve(homeRoot) === path.resolve(os.homedir()) &&
93
+ process.env.KIMI_CODE_HOME
94
+ ? path.resolve(process.env.KIMI_CODE_HOME)
95
+ : path.join(homeRoot, ".kimi-code");
96
+ return {
97
+ codex: {
98
+ detected: [path.join(homeRoot, ".codex")],
99
+ skill: path.join(homeRoot, ".codex", "skills", "ppt-template-reuse", "SKILL.md"),
100
+ config: path.join(homeRoot, ".codex", "config.toml"),
101
+ configType: "toml",
102
+ },
103
+ claude: {
104
+ detected: [path.join(homeRoot, ".claude"), path.join(homeRoot, ".claude.json")],
105
+ skill: path.join(homeRoot, ".claude", "skills", "ppt-template-reuse", "SKILL.md"),
106
+ config: path.join(homeRoot, ".claude.json"),
107
+ configType: "json",
108
+ },
109
+ kimi: {
110
+ detected: [kimiHome],
111
+ skill: path.join(kimiHome, "skills", "ppt-template-reuse", "SKILL.md"),
112
+ config: path.join(kimiHome, "mcp.json"),
113
+ configType: "json",
114
+ },
115
+ workbuddy: {
116
+ detected: [path.join(homeRoot, ".workbuddy")],
117
+ skill: path.join(
118
+ resolveEngineHome(path.join(homeRoot, ".ppt-template-reuse")),
119
+ "imports",
120
+ "workbuddy",
121
+ "ppt-template-reuse",
122
+ "SKILL.md",
123
+ ),
124
+ config: path.join(
125
+ resolveEngineHome(path.join(homeRoot, ".ppt-template-reuse")),
126
+ "imports",
127
+ "workbuddy",
128
+ "mcp.json",
129
+ ),
130
+ configType: "json",
131
+ manualTrust: true,
132
+ },
133
+ "kimi-work": {
134
+ detected: [
135
+ path.join(
136
+ homeRoot,
137
+ "Library",
138
+ "Application Support",
139
+ "Kimi",
140
+ ),
141
+ ],
142
+ skill: path.join(
143
+ resolveEngineHome(path.join(homeRoot, ".ppt-template-reuse")),
144
+ "imports",
145
+ "kimi-work",
146
+ "ppt-template-reuse",
147
+ "SKILL.md",
148
+ ),
149
+ config: path.join(
150
+ resolveEngineHome(path.join(homeRoot, ".ppt-template-reuse")),
151
+ "imports",
152
+ "kimi-work",
153
+ "mcp.json",
154
+ ),
155
+ configType: "json",
156
+ manualTrust: true,
157
+ },
158
+ };
159
+ }
160
+
161
+ async function isDetected(record) {
162
+ for (const candidate of record.detected) {
163
+ if (await exists(candidate)) return true;
164
+ }
165
+ return false;
166
+ }
167
+
168
+ function mergeMcpJson(config) {
169
+ config.mcpServers ||= {};
170
+ config.mcpServers["ppt-template-reuse"] = {
171
+ command: SERVER_COMMAND,
172
+ args: [],
173
+ };
174
+ }
175
+
176
+ export async function installHosts({
177
+ host = "auto",
178
+ homeRoot = os.homedir(),
179
+ dryRun = false,
180
+ } = {}) {
181
+ const legacyImport =
182
+ !dryRun && path.resolve(homeRoot) === path.resolve(os.homedir())
183
+ ? await migrateLegacyCapsules()
184
+ : {
185
+ imported: 0,
186
+ skipped: dryRun ? "dry-run" : "test-or-alternate-home",
187
+ };
188
+ const candidates = hostCandidates(path.resolve(homeRoot));
189
+ const requested =
190
+ host === "all"
191
+ ? Object.keys(candidates)
192
+ : host === "auto"
193
+ ? (
194
+ await Promise.all(
195
+ Object.entries(candidates).map(async ([name, value]) => [
196
+ name,
197
+ await isDetected(value),
198
+ ]),
199
+ )
200
+ )
201
+ .filter(([, detected]) => detected)
202
+ .map(([name]) => name)
203
+ : String(host)
204
+ .split(",")
205
+ .map((item) => item.trim().toLowerCase())
206
+ .filter(Boolean);
207
+ const unknown = requested.filter((name) => !candidates[name]);
208
+ if (unknown.length) throw new Error(`Unknown hosts: ${unknown.join(", ")}`);
209
+ const results = [];
210
+ for (const name of requested) {
211
+ const record = candidates[name];
212
+ const changes = [];
213
+ changes.push(await copySkill(record.skill, { dryRun }));
214
+ changes.push(
215
+ record.configType === "toml"
216
+ ? await mergeTomlMcp(record.config, { dryRun })
217
+ : await mergeJsonConfig(record.config, mergeMcpJson, { dryRun }),
218
+ );
219
+ results.push({
220
+ host: name,
221
+ detected: await isDetected(record),
222
+ manualTrustRequired: record.manualTrust === true,
223
+ changes,
224
+ });
225
+ }
226
+ return {
227
+ packageRoot: PACKAGE_ROOT,
228
+ serverCommand: SERVER_COMMAND,
229
+ homeRoot: path.resolve(homeRoot),
230
+ dryRun,
231
+ hosts: results,
232
+ actionRequired: results
233
+ .filter((item) => item.manualTrustRequired)
234
+ .map((item) => ({
235
+ host: item.host,
236
+ instruction:
237
+ "Import the prepared Skill folder and MCP JSON in the host UI, then approve the one-time local command trust prompt.",
238
+ })),
239
+ legacyImport,
240
+ };
241
+ }
242
+
243
+ export async function uninstallHost({
244
+ host,
245
+ homeRoot = os.homedir(),
246
+ dryRun = false,
247
+ } = {}) {
248
+ const candidates = hostCandidates(path.resolve(homeRoot));
249
+ const record = candidates[host];
250
+ if (!record) throw new Error(`Unknown host: ${host}`);
251
+ const result = { host, removedSkill: false, configChanged: false, backups: [] };
252
+ if (await exists(record.skill)) {
253
+ if (!dryRun) {
254
+ const saved = await backup(record.skill);
255
+ if (saved) result.backups.push(saved);
256
+ await fs.unlink(record.skill);
257
+ }
258
+ result.removedSkill = true;
259
+ }
260
+ if (record.configType === "json") {
261
+ const config = await readJsonc(record.config, {});
262
+ if (config.mcpServers?.["ppt-template-reuse"]) {
263
+ delete config.mcpServers["ppt-template-reuse"];
264
+ if (!dryRun) {
265
+ const saved = await backup(record.config);
266
+ if (saved) result.backups.push(saved);
267
+ await writeJsonAtomic(record.config, config);
268
+ }
269
+ result.configChanged = true;
270
+ }
271
+ } else if (await exists(record.config)) {
272
+ const current = await fs.readFile(record.config, "utf8");
273
+ const next = current
274
+ .replace(
275
+ /(?:^|\n)\[mcp_servers\.ppt-template-reuse\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/,
276
+ "\n",
277
+ )
278
+ .trimEnd();
279
+ if (`${next}\n` !== current) {
280
+ if (!dryRun) {
281
+ const saved = await backup(record.config);
282
+ if (saved) result.backups.push(saved);
283
+ await fs.writeFile(record.config, `${next}\n`, "utf8");
284
+ }
285
+ result.configChanged = true;
286
+ }
287
+ }
288
+ return result;
289
+ }
290
+
291
+ export function installerPaths() {
292
+ return { packageRoot: PACKAGE_ROOT, skillSource: SKILL_SOURCE };
293
+ }
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
11
+ import * as z from "zod/v4";
12
+
13
+ import {
14
+ cancelSession,
15
+ createSession,
16
+ engineVersion,
17
+ getSessionStatus,
18
+ inspectSession,
19
+ planSession,
20
+ startGeneration,
21
+ submitSemantics,
22
+ supplyAssets,
23
+ validateSession,
24
+ } from "../core/universal-engine.mjs";
25
+
26
+ function textResult(value) {
27
+ return {
28
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
29
+ structuredContent: value,
30
+ };
31
+ }
32
+
33
+ function toolError(error) {
34
+ return {
35
+ isError: true,
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text: JSON.stringify(
40
+ {
41
+ error: {
42
+ code: error.code || "PPT_REUSE_ERROR",
43
+ message: error.message || String(error),
44
+ details: error.details,
45
+ },
46
+ },
47
+ null,
48
+ 2,
49
+ ),
50
+ },
51
+ ],
52
+ };
53
+ }
54
+
55
+ function guarded(handler) {
56
+ return async (input) => {
57
+ try {
58
+ return textResult(await handler(input));
59
+ } catch (error) {
60
+ return toolError(error);
61
+ }
62
+ };
63
+ }
64
+
65
+ function resourceLinks(session) {
66
+ const links = [];
67
+ for (const [name, value] of Object.entries(session.artifacts || {})) {
68
+ if (Array.isArray(value)) {
69
+ value.forEach((_, index) =>
70
+ links.push({
71
+ type: "resource_link",
72
+ uri: `ppt://session/${session.id}/${name}/${index}`,
73
+ name: `${name}-${index + 1}`,
74
+ }),
75
+ );
76
+ } else if (typeof value === "string") {
77
+ links.push({
78
+ type: "resource_link",
79
+ uri: `ppt://session/${session.id}/${name}`,
80
+ name,
81
+ });
82
+ }
83
+ }
84
+ return links;
85
+ }
86
+
87
+ function statusResult(status) {
88
+ return {
89
+ content: [
90
+ { type: "text", text: JSON.stringify(status, null, 2) },
91
+ ...resourceLinks(status),
92
+ ],
93
+ structuredContent: status,
94
+ };
95
+ }
96
+
97
+ function registerTools(server, options = {}) {
98
+ const sessionIdSchema = {
99
+ sessionId: z.string().min(1).describe("Persistent PPT reuse session id"),
100
+ };
101
+ server.registerTool(
102
+ "ppt_create_session",
103
+ {
104
+ description:
105
+ "Register a PPTX/POTX template, source document, output path, allowed roots, and presentation requirements. This never uploads files.",
106
+ inputSchema: {
107
+ template: z.string(),
108
+ source: z.string(),
109
+ output: z.string(),
110
+ roots: z.array(z.string()).default([]),
111
+ requirements: z.record(z.string(), z.any()).default({}),
112
+ },
113
+ },
114
+ guarded((input) => createSession(input, options)),
115
+ );
116
+ server.registerTool(
117
+ "ppt_inspect",
118
+ {
119
+ description:
120
+ "Deterministically inspect template structure and source anchors, returning semantic packages the host AI must complete without deleting units.",
121
+ inputSchema: sessionIdSchema,
122
+ },
123
+ guarded(({ sessionId }) => inspectSession(sessionId, options)),
124
+ );
125
+ server.registerTool(
126
+ "ppt_submit_semantics",
127
+ {
128
+ description:
129
+ "Submit host-AI template and source semantics. Every extracted source id and anchor must be preserved exactly.",
130
+ inputSchema: {
131
+ ...sessionIdSchema,
132
+ templateProfile: z.record(z.string(), z.any()).optional(),
133
+ sourceSemantics: z.union([
134
+ z.array(z.record(z.string(), z.any())),
135
+ z.record(z.string(), z.any()),
136
+ ]),
137
+ },
138
+ },
139
+ guarded(({ sessionId, ...payload }) =>
140
+ submitSemantics(sessionId, payload, options),
141
+ ),
142
+ );
143
+ server.registerTool(
144
+ "ppt_plan",
145
+ {
146
+ description:
147
+ "Create and freeze an adaptive content plan, coverage audit, deterministic template-page sequence, and native edit map.",
148
+ inputSchema: {
149
+ ...sessionIdSchema,
150
+ density: z.string().optional(),
151
+ scope: z.string().optional(),
152
+ templateMode: z.string().optional(),
153
+ allowLowFit: z.boolean().default(false),
154
+ durationSeconds: z
155
+ .object({ min: z.number().optional(), max: z.number().optional() })
156
+ .optional(),
157
+ },
158
+ },
159
+ guarded(({ sessionId, ...planning }) =>
160
+ planSession(sessionId, planning, options),
161
+ ),
162
+ );
163
+ server.registerTool(
164
+ "ppt_supply_assets",
165
+ {
166
+ description:
167
+ "Register host-generated or user-supplied visual assets. Missing required visuals block generation instead of becoming gray placeholders.",
168
+ inputSchema: {
169
+ ...sessionIdSchema,
170
+ assets: z.array(
171
+ z.object({
172
+ sourceUnitId: z.string(),
173
+ type: z.string().default("image"),
174
+ path: z.string().optional(),
175
+ value: z.any().optional(),
176
+ alt: z.string().optional(),
177
+ }),
178
+ ),
179
+ },
180
+ },
181
+ guarded(({ sessionId, assets }) =>
182
+ supplyAssets(sessionId, assets, options),
183
+ ),
184
+ );
185
+ server.registerTool(
186
+ "ppt_generate",
187
+ {
188
+ description:
189
+ "Start native OOXML generation as a recoverable background job. Poll ppt_status for page events and previews.",
190
+ inputSchema: {
191
+ ...sessionIdSchema,
192
+ validate: z.boolean().default(true),
193
+ livePreview: z.boolean().default(true),
194
+ },
195
+ },
196
+ guarded(({ sessionId, validate, livePreview }) =>
197
+ Promise.resolve(
198
+ startGeneration(sessionId, { ...options, validate, livePreview }),
199
+ ),
200
+ ),
201
+ );
202
+ server.registerTool(
203
+ "ppt_status",
204
+ {
205
+ description:
206
+ "Read persistent status, warnings, action-required blocks, new page events, preview resources, and QA artifacts.",
207
+ inputSchema: {
208
+ ...sessionIdSchema,
209
+ afterEvent: z.number().int().min(0).default(0),
210
+ },
211
+ },
212
+ async ({ sessionId, afterEvent }) => {
213
+ try {
214
+ return statusResult(
215
+ await getSessionStatus(sessionId, { ...options, afterEvent }),
216
+ );
217
+ } catch (error) {
218
+ return toolError(error);
219
+ }
220
+ },
221
+ );
222
+ server.registerTool(
223
+ "ppt_validate",
224
+ {
225
+ description:
226
+ "Run structural, content, placeholder, notes, chart, page-count, LibreOffice rendering, rendered-text fidelity, and protected-region validation.",
227
+ inputSchema: sessionIdSchema,
228
+ },
229
+ guarded(({ sessionId }) => validateSession(sessionId, options)),
230
+ );
231
+ server.registerTool(
232
+ "ppt_cancel",
233
+ {
234
+ description:
235
+ "Safely mark a session cancelled while retaining its frozen plans and artifacts for later recovery.",
236
+ inputSchema: sessionIdSchema,
237
+ },
238
+ guarded(({ sessionId }) => cancelSession(sessionId, options)),
239
+ );
240
+ }
241
+
242
+ function mimeFor(candidate) {
243
+ const extension = path.extname(candidate).toLowerCase();
244
+ return (
245
+ {
246
+ ".json": "application/json",
247
+ ".png": "image/png",
248
+ ".pdf": "application/pdf",
249
+ ".pptx":
250
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
251
+ }[extension] || "application/octet-stream"
252
+ );
253
+ }
254
+
255
+ function registerResources(server, options = {}) {
256
+ server.registerResource(
257
+ "ppt-session-artifact",
258
+ new ResourceTemplate("ppt://session/{sessionId}/{artifact}", {
259
+ list: undefined,
260
+ }),
261
+ {
262
+ description:
263
+ "Frozen semantic packages, plans, QA reports, output PPTX files, and contact sheets for a PPT reuse session.",
264
+ },
265
+ async (uri, variables) => {
266
+ const status = await getSessionStatus(String(variables.sessionId), options);
267
+ const candidate = status.artifacts?.[String(variables.artifact)];
268
+ if (!candidate || Array.isArray(candidate)) {
269
+ throw new Error("Unknown or indexed session artifact");
270
+ }
271
+ const bytes = await fs.readFile(candidate);
272
+ const mimeType = mimeFor(candidate);
273
+ return {
274
+ contents: [
275
+ mimeType.startsWith("text/") || mimeType === "application/json"
276
+ ? { uri: uri.href, mimeType, text: bytes.toString("utf8") }
277
+ : { uri: uri.href, mimeType, blob: bytes.toString("base64") },
278
+ ],
279
+ };
280
+ },
281
+ );
282
+ server.registerResource(
283
+ "ppt-session-indexed-artifact",
284
+ new ResourceTemplate("ppt://session/{sessionId}/{artifact}/{index}", {
285
+ list: undefined,
286
+ }),
287
+ { description: "A numbered generated slide preview." },
288
+ async (uri, variables) => {
289
+ const status = await getSessionStatus(String(variables.sessionId), options);
290
+ const values = status.artifacts?.[String(variables.artifact)];
291
+ const candidate = Array.isArray(values)
292
+ ? values[Number(variables.index)]
293
+ : undefined;
294
+ if (!candidate) throw new Error("Unknown indexed session artifact");
295
+ const bytes = await fs.readFile(candidate);
296
+ return {
297
+ contents: [
298
+ {
299
+ uri: uri.href,
300
+ mimeType: mimeFor(candidate),
301
+ blob: bytes.toString("base64"),
302
+ },
303
+ ],
304
+ };
305
+ },
306
+ );
307
+ }
308
+
309
+ export function createMcpServer(options = {}) {
310
+ const server = new McpServer(
311
+ { name: "ppt-template-reuse", version: engineVersion },
312
+ { capabilities: { logging: {}, resources: {} } },
313
+ );
314
+ registerTools(server, options);
315
+ registerResources(server, options);
316
+ return server;
317
+ }
318
+
319
+ export async function startStdio(options = {}) {
320
+ const server = createMcpServer(options);
321
+ await server.connect(new StdioServerTransport());
322
+ return server;
323
+ }
324
+
325
+ export async function startHttp({
326
+ host = "127.0.0.1",
327
+ port = 8765,
328
+ ...options
329
+ } = {}) {
330
+ if (!["127.0.0.1", "localhost", "::1"].includes(host)) {
331
+ throw new Error("HTTP MCP binds to loopback only");
332
+ }
333
+ const app = createMcpExpressApp({ host });
334
+ app.post("/mcp", async (request, response) => {
335
+ const server = createMcpServer(options);
336
+ const transport = new StreamableHTTPServerTransport({
337
+ sessionIdGenerator: undefined,
338
+ });
339
+ try {
340
+ await server.connect(transport);
341
+ await transport.handleRequest(request, response, request.body);
342
+ response.on("close", async () => {
343
+ await transport.close();
344
+ await server.close();
345
+ });
346
+ } catch (error) {
347
+ if (!response.headersSent) {
348
+ response.status(500).json({
349
+ jsonrpc: "2.0",
350
+ error: { code: -32603, message: error.message },
351
+ id: null,
352
+ });
353
+ }
354
+ }
355
+ });
356
+ app.get("/health", (_request, response) =>
357
+ response.json({ ok: true, engineVersion }),
358
+ );
359
+ app.get("/mcp", (_request, response) => response.status(405).end());
360
+ app.delete("/mcp", (_request, response) => response.status(405).end());
361
+ return new Promise((resolve, reject) => {
362
+ const listener = app.listen(Number(port), host, (error) => {
363
+ if (error) reject(error);
364
+ else resolve({ app, listener, host, port: Number(port) });
365
+ });
366
+ });
367
+ }
368
+
369
+ const isMain =
370
+ process.argv[1] &&
371
+ pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
372
+ if (isMain) {
373
+ startStdio().catch((error) => {
374
+ process.stderr.write(`${error.stack || error.message}\n`);
375
+ process.exit(1);
376
+ });
377
+ }