pptx-viewer-mcp 1.1.4 → 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.
@@ -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
- slide[original] = JSON.parse(value);
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
- element[REVERSE_COMPLEX_MAP[key]] = JSON.parse(value);
234
+ try {
235
+ element[REVERSE_COMPLEX_MAP[key]] = JSON.parse(value);
236
+ } catch {
237
+ }
232
238
  } else {
233
239
  element[key] = value;
234
240
  }
@@ -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
- slide[original] = JSON.parse(value);
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
- element[REVERSE_COMPLEX_MAP[key]] = JSON.parse(value);
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
- return new RegExp(pattern, flags);
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 converter = new pptxViewerCore.PptxMarkdownConverter(params.outputDir ?? ".", options);
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
- return new RegExp(pattern, flags);
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 converter = new PptxMarkdownConverter(params.outputDir ?? ".", options);
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
- return new RegExp(pattern, flags);
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 converter = new pptxViewerCore.PptxMarkdownConverter(params.outputDir ?? ".", options);
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(filePath, ctx, fn);
1867
+ return executeToolWithContext(safePath, ctx, fn);
1798
1868
  }
1799
1869
 
1800
1870
  // src/mcp/server.ts
@@ -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
- return new RegExp(pattern, flags);
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 converter = new PptxMarkdownConverter(params.outputDir ?? ".", options);
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(filePath, ctx, fn);
1865
+ return executeToolWithContext(safePath, ctx, fn);
1796
1866
  }
1797
1867
 
1798
1868
  // src/mcp/server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptx-viewer-mcp",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "PPTX manipulation tools, MCP server, and collaboration codec for pptx-viewer-core.",
5
5
  "keywords": [
6
6
  "ai",