pptx-viewer-mcp 1.1.3 → 1.1.5
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/dist/codec/index.js +8 -2
- package/dist/codec/index.mjs +8 -2
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +45 -2
- package/dist/index.mjs +45 -2
- package/dist/mcp/index.js +75 -5
- package/dist/mcp/index.mjs +75 -5
- package/dist/schemas/index.d.mts +88 -519
- package/dist/schemas/index.d.ts +88 -519
- package/package.json +6 -6
package/dist/codec/index.js
CHANGED
|
@@ -195,7 +195,10 @@ var PptxCodec = class {
|
|
|
195
195
|
for (const [prefixed, original] of Object.entries(REVERSE_COMPLEX_SLIDE_MAP)) {
|
|
196
196
|
const value = slideMap.get(prefixed);
|
|
197
197
|
if (value !== void 0) {
|
|
198
|
-
|
|
198
|
+
try {
|
|
199
|
+
slide[original] = JSON.parse(value);
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
199
202
|
}
|
|
200
203
|
}
|
|
201
204
|
const elementsArray = slideMap.get("elements");
|
|
@@ -228,7 +231,10 @@ var PptxCodec = class {
|
|
|
228
231
|
const element = {};
|
|
229
232
|
elemMap.forEach((value, key) => {
|
|
230
233
|
if (REVERSE_COMPLEX_MAP[key]) {
|
|
231
|
-
|
|
234
|
+
try {
|
|
235
|
+
element[REVERSE_COMPLEX_MAP[key]] = JSON.parse(value);
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
232
238
|
} else {
|
|
233
239
|
element[key] = value;
|
|
234
240
|
}
|
package/dist/codec/index.mjs
CHANGED
|
@@ -193,7 +193,10 @@ var PptxCodec = class {
|
|
|
193
193
|
for (const [prefixed, original] of Object.entries(REVERSE_COMPLEX_SLIDE_MAP)) {
|
|
194
194
|
const value = slideMap.get(prefixed);
|
|
195
195
|
if (value !== void 0) {
|
|
196
|
-
|
|
196
|
+
try {
|
|
197
|
+
slide[original] = JSON.parse(value);
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
197
200
|
}
|
|
198
201
|
}
|
|
199
202
|
const elementsArray = slideMap.get("elements");
|
|
@@ -226,7 +229,10 @@ var PptxCodec = class {
|
|
|
226
229
|
const element = {};
|
|
227
230
|
elemMap.forEach((value, key) => {
|
|
228
231
|
if (REVERSE_COMPLEX_MAP[key]) {
|
|
229
|
-
|
|
232
|
+
try {
|
|
233
|
+
element[REVERSE_COMPLEX_MAP[key]] = JSON.parse(value);
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
230
236
|
} else {
|
|
231
237
|
element[key] = value;
|
|
232
238
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -468,6 +468,11 @@ interface ConvertToMarkdownParams {
|
|
|
468
468
|
includeSpeakerNotes?: boolean;
|
|
469
469
|
semanticMode?: boolean;
|
|
470
470
|
sourceName?: string;
|
|
471
|
+
/**
|
|
472
|
+
* Root directory under which `outputDir` must resolve. Defaults to
|
|
473
|
+
* `process.env.PPTX_TOOLS_ROOT` if set, else `process.cwd()`.
|
|
474
|
+
*/
|
|
475
|
+
rootDir?: string;
|
|
471
476
|
}
|
|
472
477
|
interface ConvertToMarkdownResult {
|
|
473
478
|
markdown: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -468,6 +468,11 @@ interface ConvertToMarkdownParams {
|
|
|
468
468
|
includeSpeakerNotes?: boolean;
|
|
469
469
|
semanticMode?: boolean;
|
|
470
470
|
sourceName?: string;
|
|
471
|
+
/**
|
|
472
|
+
* Root directory under which `outputDir` must resolve. Defaults to
|
|
473
|
+
* `process.env.PPTX_TOOLS_ROOT` if set, else `process.cwd()`.
|
|
474
|
+
*/
|
|
475
|
+
rootDir?: string;
|
|
471
476
|
}
|
|
472
477
|
interface ConvertToMarkdownResult {
|
|
473
478
|
markdown: string;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var pptxViewerCore = require('pptx-viewer-core');
|
|
4
|
+
var path = require('path');
|
|
4
5
|
|
|
5
6
|
// src/execution.ts
|
|
6
7
|
var PPTX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
@@ -1251,10 +1252,26 @@ function runAccessibilityCheck(ctx) {
|
|
|
1251
1252
|
}
|
|
1252
1253
|
};
|
|
1253
1254
|
}
|
|
1255
|
+
var MAX_REGEX_LEN = 200;
|
|
1256
|
+
function hasNestedUnboundedQuantifier(pattern) {
|
|
1257
|
+
return /\([^)]*[+*]\)[+*]/.test(pattern);
|
|
1258
|
+
}
|
|
1254
1259
|
function buildSearchRegex(query, useRegex, caseSensitive) {
|
|
1260
|
+
if (useRegex) {
|
|
1261
|
+
if (query.length > MAX_REGEX_LEN) {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
if (hasNestedUnboundedQuantifier(query)) {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1255
1268
|
const pattern = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1256
1269
|
const flags = caseSensitive ? "g" : "gi";
|
|
1257
|
-
|
|
1270
|
+
try {
|
|
1271
|
+
return new RegExp(pattern, flags);
|
|
1272
|
+
} catch {
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1258
1275
|
}
|
|
1259
1276
|
function findText(ctx, params) {
|
|
1260
1277
|
const regex = buildSearchRegex(
|
|
@@ -1262,6 +1279,13 @@ function findText(ctx, params) {
|
|
|
1262
1279
|
params.useRegex === true,
|
|
1263
1280
|
params.caseSensitive === true
|
|
1264
1281
|
);
|
|
1282
|
+
if (!regex) {
|
|
1283
|
+
return {
|
|
1284
|
+
pptxData: ctx.pptxData,
|
|
1285
|
+
dirty: false,
|
|
1286
|
+
result: { query: params.query, matchCount: 0, matches: [] }
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1265
1289
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
1266
1290
|
const matches = [];
|
|
1267
1291
|
for (const si of targetIndexes) {
|
|
@@ -1346,6 +1370,13 @@ function replaceText(ctx, params) {
|
|
|
1346
1370
|
params.useRegex === true,
|
|
1347
1371
|
params.caseSensitive === true
|
|
1348
1372
|
);
|
|
1373
|
+
if (!regex) {
|
|
1374
|
+
return {
|
|
1375
|
+
pptxData: ctx.pptxData,
|
|
1376
|
+
dirty: false,
|
|
1377
|
+
result: { query: params.query, replacement: params.replacement, replacementCount: 0 }
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1349
1380
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
1350
1381
|
let count = 0;
|
|
1351
1382
|
for (const si of targetIndexes) {
|
|
@@ -1523,6 +1554,17 @@ function manageComments(ctx, params) {
|
|
|
1523
1554
|
}
|
|
1524
1555
|
}
|
|
1525
1556
|
}
|
|
1557
|
+
function scopeOutputDir(outputDir, rootDir) {
|
|
1558
|
+
const root = path.resolve(rootDir ?? process.env["PPTX_TOOLS_ROOT"] ?? process.cwd());
|
|
1559
|
+
const resolved = path.isAbsolute(outputDir) ? path.resolve(outputDir) : path.resolve(root, outputDir);
|
|
1560
|
+
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
1561
|
+
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
1562
|
+
throw new Error(
|
|
1563
|
+
`convertToMarkdown: outputDir "${outputDir}" resolves outside the allowed root "${root}"`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
return resolved;
|
|
1567
|
+
}
|
|
1526
1568
|
async function convertToMarkdown(ctx, params) {
|
|
1527
1569
|
const options = {
|
|
1528
1570
|
mediaFolderName: params.mediaFolderName ?? "media",
|
|
@@ -1532,7 +1574,8 @@ async function convertToMarkdown(ctx, params) {
|
|
|
1532
1574
|
semanticMode: params.semanticMode === true,
|
|
1533
1575
|
sourceName: params.sourceName ?? "unknown"
|
|
1534
1576
|
};
|
|
1535
|
-
const
|
|
1577
|
+
const safeOutputDir = scopeOutputDir(params.outputDir ?? ".", params.rootDir);
|
|
1578
|
+
const converter = new pptxViewerCore.PptxMarkdownConverter(safeOutputDir, options);
|
|
1536
1579
|
const markdown = await converter.convert(ctx.pptxData);
|
|
1537
1580
|
return {
|
|
1538
1581
|
pptxData: ctx.pptxData,
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PptxHandler, hasTextProperties, PptxMarkdownConverter } from 'pptx-viewer-core';
|
|
2
|
+
import { resolve, isAbsolute, sep } from 'path';
|
|
2
3
|
|
|
3
4
|
// src/execution.ts
|
|
4
5
|
var PPTX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
@@ -1249,10 +1250,26 @@ function runAccessibilityCheck(ctx) {
|
|
|
1249
1250
|
}
|
|
1250
1251
|
};
|
|
1251
1252
|
}
|
|
1253
|
+
var MAX_REGEX_LEN = 200;
|
|
1254
|
+
function hasNestedUnboundedQuantifier(pattern) {
|
|
1255
|
+
return /\([^)]*[+*]\)[+*]/.test(pattern);
|
|
1256
|
+
}
|
|
1252
1257
|
function buildSearchRegex(query, useRegex, caseSensitive) {
|
|
1258
|
+
if (useRegex) {
|
|
1259
|
+
if (query.length > MAX_REGEX_LEN) {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
if (hasNestedUnboundedQuantifier(query)) {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1253
1266
|
const pattern = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1254
1267
|
const flags = caseSensitive ? "g" : "gi";
|
|
1255
|
-
|
|
1268
|
+
try {
|
|
1269
|
+
return new RegExp(pattern, flags);
|
|
1270
|
+
} catch {
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1256
1273
|
}
|
|
1257
1274
|
function findText(ctx, params) {
|
|
1258
1275
|
const regex = buildSearchRegex(
|
|
@@ -1260,6 +1277,13 @@ function findText(ctx, params) {
|
|
|
1260
1277
|
params.useRegex === true,
|
|
1261
1278
|
params.caseSensitive === true
|
|
1262
1279
|
);
|
|
1280
|
+
if (!regex) {
|
|
1281
|
+
return {
|
|
1282
|
+
pptxData: ctx.pptxData,
|
|
1283
|
+
dirty: false,
|
|
1284
|
+
result: { query: params.query, matchCount: 0, matches: [] }
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1263
1287
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
1264
1288
|
const matches = [];
|
|
1265
1289
|
for (const si of targetIndexes) {
|
|
@@ -1344,6 +1368,13 @@ function replaceText(ctx, params) {
|
|
|
1344
1368
|
params.useRegex === true,
|
|
1345
1369
|
params.caseSensitive === true
|
|
1346
1370
|
);
|
|
1371
|
+
if (!regex) {
|
|
1372
|
+
return {
|
|
1373
|
+
pptxData: ctx.pptxData,
|
|
1374
|
+
dirty: false,
|
|
1375
|
+
result: { query: params.query, replacement: params.replacement, replacementCount: 0 }
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1347
1378
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
1348
1379
|
let count = 0;
|
|
1349
1380
|
for (const si of targetIndexes) {
|
|
@@ -1521,6 +1552,17 @@ function manageComments(ctx, params) {
|
|
|
1521
1552
|
}
|
|
1522
1553
|
}
|
|
1523
1554
|
}
|
|
1555
|
+
function scopeOutputDir(outputDir, rootDir) {
|
|
1556
|
+
const root = resolve(rootDir ?? process.env["PPTX_TOOLS_ROOT"] ?? process.cwd());
|
|
1557
|
+
const resolved = isAbsolute(outputDir) ? resolve(outputDir) : resolve(root, outputDir);
|
|
1558
|
+
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
1559
|
+
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`convertToMarkdown: outputDir "${outputDir}" resolves outside the allowed root "${root}"`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
return resolved;
|
|
1565
|
+
}
|
|
1524
1566
|
async function convertToMarkdown(ctx, params) {
|
|
1525
1567
|
const options = {
|
|
1526
1568
|
mediaFolderName: params.mediaFolderName ?? "media",
|
|
@@ -1530,7 +1572,8 @@ async function convertToMarkdown(ctx, params) {
|
|
|
1530
1572
|
semanticMode: params.semanticMode === true,
|
|
1531
1573
|
sourceName: params.sourceName ?? "unknown"
|
|
1532
1574
|
};
|
|
1533
|
-
const
|
|
1575
|
+
const safeOutputDir = scopeOutputDir(params.outputDir ?? ".", params.rootDir);
|
|
1576
|
+
const converter = new PptxMarkdownConverter(safeOutputDir, options);
|
|
1534
1577
|
const markdown = await converter.convert(ctx.pptxData);
|
|
1535
1578
|
return {
|
|
1536
1579
|
pptxData: ctx.pptxData,
|
package/dist/mcp/index.js
CHANGED
|
@@ -5,6 +5,7 @@ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
|
5
5
|
var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
6
6
|
var zod = require('zod');
|
|
7
7
|
var pptxViewerCore = require('pptx-viewer-core');
|
|
8
|
+
var path = require('path');
|
|
8
9
|
var promises = require('fs/promises');
|
|
9
10
|
|
|
10
11
|
var GetSlideSchema = zod.z.object({
|
|
@@ -369,10 +370,26 @@ function validateSlideIndex(slideIndex, slideCount) {
|
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
// src/tools/content-tools.ts
|
|
373
|
+
var MAX_REGEX_LEN = 200;
|
|
374
|
+
function hasNestedUnboundedQuantifier(pattern) {
|
|
375
|
+
return /\([^)]*[+*]\)[+*]/.test(pattern);
|
|
376
|
+
}
|
|
372
377
|
function buildSearchRegex(query, useRegex, caseSensitive) {
|
|
378
|
+
if (useRegex) {
|
|
379
|
+
if (query.length > MAX_REGEX_LEN) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
if (hasNestedUnboundedQuantifier(query)) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
373
386
|
const pattern = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
374
387
|
const flags = caseSensitive ? "g" : "gi";
|
|
375
|
-
|
|
388
|
+
try {
|
|
389
|
+
return new RegExp(pattern, flags);
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
376
393
|
}
|
|
377
394
|
function findText(ctx, params) {
|
|
378
395
|
const regex = buildSearchRegex(
|
|
@@ -380,6 +397,13 @@ function findText(ctx, params) {
|
|
|
380
397
|
params.useRegex === true,
|
|
381
398
|
params.caseSensitive === true
|
|
382
399
|
);
|
|
400
|
+
if (!regex) {
|
|
401
|
+
return {
|
|
402
|
+
pptxData: ctx.pptxData,
|
|
403
|
+
dirty: false,
|
|
404
|
+
result: { query: params.query, matchCount: 0, matches: [] }
|
|
405
|
+
};
|
|
406
|
+
}
|
|
383
407
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
384
408
|
const matches = [];
|
|
385
409
|
for (const si of targetIndexes) {
|
|
@@ -464,6 +488,13 @@ function replaceText(ctx, params) {
|
|
|
464
488
|
params.useRegex === true,
|
|
465
489
|
params.caseSensitive === true
|
|
466
490
|
);
|
|
491
|
+
if (!regex) {
|
|
492
|
+
return {
|
|
493
|
+
pptxData: ctx.pptxData,
|
|
494
|
+
dirty: false,
|
|
495
|
+
result: { query: params.query, replacement: params.replacement, replacementCount: 0 }
|
|
496
|
+
};
|
|
497
|
+
}
|
|
467
498
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
468
499
|
let count = 0;
|
|
469
500
|
for (const si of targetIndexes) {
|
|
@@ -641,6 +672,17 @@ function manageComments(ctx, params) {
|
|
|
641
672
|
}
|
|
642
673
|
}
|
|
643
674
|
}
|
|
675
|
+
function scopeOutputDir(outputDir, rootDir) {
|
|
676
|
+
const root = path.resolve(rootDir ?? process.env["PPTX_TOOLS_ROOT"] ?? process.cwd());
|
|
677
|
+
const resolved = path.isAbsolute(outputDir) ? path.resolve(outputDir) : path.resolve(root, outputDir);
|
|
678
|
+
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
679
|
+
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`convertToMarkdown: outputDir "${outputDir}" resolves outside the allowed root "${root}"`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return resolved;
|
|
685
|
+
}
|
|
644
686
|
async function convertToMarkdown(ctx, params) {
|
|
645
687
|
const options = {
|
|
646
688
|
mediaFolderName: params.mediaFolderName ?? "media",
|
|
@@ -650,7 +692,8 @@ async function convertToMarkdown(ctx, params) {
|
|
|
650
692
|
semanticMode: params.semanticMode === true,
|
|
651
693
|
sourceName: params.sourceName ?? "unknown"
|
|
652
694
|
};
|
|
653
|
-
const
|
|
695
|
+
const safeOutputDir = scopeOutputDir(params.outputDir ?? ".", params.rootDir);
|
|
696
|
+
const converter = new pptxViewerCore.PptxMarkdownConverter(safeOutputDir, options);
|
|
654
697
|
const markdown = await converter.convert(ctx.pptxData);
|
|
655
698
|
return {
|
|
656
699
|
pptxData: ctx.pptxData,
|
|
@@ -1783,18 +1826,45 @@ async function executeToolWithContext(filePath, execCtx, toolFn) {
|
|
|
1783
1826
|
}
|
|
1784
1827
|
|
|
1785
1828
|
// src/mcp/handlers.ts
|
|
1829
|
+
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".pptx", ".ppt"]);
|
|
1830
|
+
function resolveScopedFilePath(filePath, rootDir) {
|
|
1831
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
1832
|
+
throw new Error("MCP tool: filePath must be a non-empty string");
|
|
1833
|
+
}
|
|
1834
|
+
const root = path.resolve(process.env["PPTX_TOOLS_ROOT"] ?? process.cwd());
|
|
1835
|
+
const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(root, filePath);
|
|
1836
|
+
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
1837
|
+
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
1838
|
+
throw new Error(`MCP tool: filePath "${filePath}" resolves outside the allowed root "${root}"`);
|
|
1839
|
+
}
|
|
1840
|
+
const lower = resolved.toLowerCase();
|
|
1841
|
+
let isAllowed = false;
|
|
1842
|
+
for (const ext of ALLOWED_EXTENSIONS) {
|
|
1843
|
+
if (lower.endsWith(ext)) {
|
|
1844
|
+
isAllowed = true;
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (!isAllowed) {
|
|
1849
|
+
throw new Error(
|
|
1850
|
+
`MCP tool: filePath "${filePath}" must end with one of: ${Array.from(ALLOWED_EXTENSIONS).join(", ")}`
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
return resolved;
|
|
1854
|
+
}
|
|
1786
1855
|
var nodeFs = {
|
|
1787
1856
|
readFile: async (path) => new Uint8Array(await promises.readFile(path)),
|
|
1788
1857
|
writeFile: async (path, data) => {
|
|
1789
1858
|
await promises.writeFile(path, data);
|
|
1790
1859
|
}
|
|
1791
1860
|
};
|
|
1792
|
-
function createMcpContext() {
|
|
1861
|
+
function createMcpContext(_options) {
|
|
1793
1862
|
return { filesystem: nodeFs };
|
|
1794
1863
|
}
|
|
1795
|
-
async function runMcpTool(filePath, fn) {
|
|
1864
|
+
async function runMcpTool(filePath, fn, options) {
|
|
1865
|
+
const safePath = resolveScopedFilePath(filePath);
|
|
1796
1866
|
const ctx = createMcpContext();
|
|
1797
|
-
return executeToolWithContext(
|
|
1867
|
+
return executeToolWithContext(safePath, ctx, fn);
|
|
1798
1868
|
}
|
|
1799
1869
|
|
|
1800
1870
|
// src/mcp/server.ts
|
package/dist/mcp/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { PptxHandler, hasTextProperties, PptxMarkdownConverter } from 'pptx-viewer-core';
|
|
6
|
+
import { resolve, isAbsolute, sep } from 'path';
|
|
6
7
|
import { writeFile, readFile } from 'fs/promises';
|
|
7
8
|
|
|
8
9
|
var GetSlideSchema = z.object({
|
|
@@ -367,10 +368,26 @@ function validateSlideIndex(slideIndex, slideCount) {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
// src/tools/content-tools.ts
|
|
371
|
+
var MAX_REGEX_LEN = 200;
|
|
372
|
+
function hasNestedUnboundedQuantifier(pattern) {
|
|
373
|
+
return /\([^)]*[+*]\)[+*]/.test(pattern);
|
|
374
|
+
}
|
|
370
375
|
function buildSearchRegex(query, useRegex, caseSensitive) {
|
|
376
|
+
if (useRegex) {
|
|
377
|
+
if (query.length > MAX_REGEX_LEN) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if (hasNestedUnboundedQuantifier(query)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
371
384
|
const pattern = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
372
385
|
const flags = caseSensitive ? "g" : "gi";
|
|
373
|
-
|
|
386
|
+
try {
|
|
387
|
+
return new RegExp(pattern, flags);
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
374
391
|
}
|
|
375
392
|
function findText(ctx, params) {
|
|
376
393
|
const regex = buildSearchRegex(
|
|
@@ -378,6 +395,13 @@ function findText(ctx, params) {
|
|
|
378
395
|
params.useRegex === true,
|
|
379
396
|
params.caseSensitive === true
|
|
380
397
|
);
|
|
398
|
+
if (!regex) {
|
|
399
|
+
return {
|
|
400
|
+
pptxData: ctx.pptxData,
|
|
401
|
+
dirty: false,
|
|
402
|
+
result: { query: params.query, matchCount: 0, matches: [] }
|
|
403
|
+
};
|
|
404
|
+
}
|
|
381
405
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
382
406
|
const matches = [];
|
|
383
407
|
for (const si of targetIndexes) {
|
|
@@ -462,6 +486,13 @@ function replaceText(ctx, params) {
|
|
|
462
486
|
params.useRegex === true,
|
|
463
487
|
params.caseSensitive === true
|
|
464
488
|
);
|
|
489
|
+
if (!regex) {
|
|
490
|
+
return {
|
|
491
|
+
pptxData: ctx.pptxData,
|
|
492
|
+
dirty: false,
|
|
493
|
+
result: { query: params.query, replacement: params.replacement, replacementCount: 0 }
|
|
494
|
+
};
|
|
495
|
+
}
|
|
465
496
|
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
466
497
|
let count = 0;
|
|
467
498
|
for (const si of targetIndexes) {
|
|
@@ -639,6 +670,17 @@ function manageComments(ctx, params) {
|
|
|
639
670
|
}
|
|
640
671
|
}
|
|
641
672
|
}
|
|
673
|
+
function scopeOutputDir(outputDir, rootDir) {
|
|
674
|
+
const root = resolve(rootDir ?? process.env["PPTX_TOOLS_ROOT"] ?? process.cwd());
|
|
675
|
+
const resolved = isAbsolute(outputDir) ? resolve(outputDir) : resolve(root, outputDir);
|
|
676
|
+
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
677
|
+
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`convertToMarkdown: outputDir "${outputDir}" resolves outside the allowed root "${root}"`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return resolved;
|
|
683
|
+
}
|
|
642
684
|
async function convertToMarkdown(ctx, params) {
|
|
643
685
|
const options = {
|
|
644
686
|
mediaFolderName: params.mediaFolderName ?? "media",
|
|
@@ -648,7 +690,8 @@ async function convertToMarkdown(ctx, params) {
|
|
|
648
690
|
semanticMode: params.semanticMode === true,
|
|
649
691
|
sourceName: params.sourceName ?? "unknown"
|
|
650
692
|
};
|
|
651
|
-
const
|
|
693
|
+
const safeOutputDir = scopeOutputDir(params.outputDir ?? ".", params.rootDir);
|
|
694
|
+
const converter = new PptxMarkdownConverter(safeOutputDir, options);
|
|
652
695
|
const markdown = await converter.convert(ctx.pptxData);
|
|
653
696
|
return {
|
|
654
697
|
pptxData: ctx.pptxData,
|
|
@@ -1781,18 +1824,45 @@ async function executeToolWithContext(filePath, execCtx, toolFn) {
|
|
|
1781
1824
|
}
|
|
1782
1825
|
|
|
1783
1826
|
// src/mcp/handlers.ts
|
|
1827
|
+
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".pptx", ".ppt"]);
|
|
1828
|
+
function resolveScopedFilePath(filePath, rootDir) {
|
|
1829
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
1830
|
+
throw new Error("MCP tool: filePath must be a non-empty string");
|
|
1831
|
+
}
|
|
1832
|
+
const root = resolve(process.env["PPTX_TOOLS_ROOT"] ?? process.cwd());
|
|
1833
|
+
const resolved = isAbsolute(filePath) ? resolve(filePath) : resolve(root, filePath);
|
|
1834
|
+
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
1835
|
+
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
1836
|
+
throw new Error(`MCP tool: filePath "${filePath}" resolves outside the allowed root "${root}"`);
|
|
1837
|
+
}
|
|
1838
|
+
const lower = resolved.toLowerCase();
|
|
1839
|
+
let isAllowed = false;
|
|
1840
|
+
for (const ext of ALLOWED_EXTENSIONS) {
|
|
1841
|
+
if (lower.endsWith(ext)) {
|
|
1842
|
+
isAllowed = true;
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (!isAllowed) {
|
|
1847
|
+
throw new Error(
|
|
1848
|
+
`MCP tool: filePath "${filePath}" must end with one of: ${Array.from(ALLOWED_EXTENSIONS).join(", ")}`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
return resolved;
|
|
1852
|
+
}
|
|
1784
1853
|
var nodeFs = {
|
|
1785
1854
|
readFile: async (path) => new Uint8Array(await readFile(path)),
|
|
1786
1855
|
writeFile: async (path, data) => {
|
|
1787
1856
|
await writeFile(path, data);
|
|
1788
1857
|
}
|
|
1789
1858
|
};
|
|
1790
|
-
function createMcpContext() {
|
|
1859
|
+
function createMcpContext(_options) {
|
|
1791
1860
|
return { filesystem: nodeFs };
|
|
1792
1861
|
}
|
|
1793
|
-
async function runMcpTool(filePath, fn) {
|
|
1862
|
+
async function runMcpTool(filePath, fn, options) {
|
|
1863
|
+
const safePath = resolveScopedFilePath(filePath);
|
|
1794
1864
|
const ctx = createMcpContext();
|
|
1795
|
-
return executeToolWithContext(
|
|
1865
|
+
return executeToolWithContext(safePath, ctx, fn);
|
|
1796
1866
|
}
|
|
1797
1867
|
|
|
1798
1868
|
// src/mcp/server.ts
|