agent-reader 1.0.0 → 1.1.0

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/src/mcp/server.js CHANGED
@@ -2,12 +2,12 @@ import path from 'node:path';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import * as z from 'zod/v4';
6
5
  import { renderMarkdown } from '../core/renderer.js';
7
- import { exportDOCX, exportPDF } from '../core/exporter.js';
6
+ import { exportDOCX, exportPDF, resolveSandboxMode } from '../core/exporter.js';
8
7
  import { createSlideshow } from '../core/slideshow.js';
9
8
  import { openTarget } from '../core/opener.js';
10
9
  import { createOutputDir } from '../utils/output.js';
10
+ import { MCP_TOOL_SCHEMAS } from './toolSchemas.js';
11
11
  import {
12
12
  getPreferencesPath,
13
13
  loadPreferences,
@@ -16,6 +16,7 @@ import {
16
16
  } from '../utils/preferences.js';
17
17
 
18
18
  const MAX_CONTENT_BYTES = 50 * 1024 * 1024;
19
+ const MCP_SANDBOX_MODE = resolveSandboxMode(process.env.AGENT_READER_SANDBOX, process.env);
19
20
 
20
21
  function getBaseDirFromSourcePath(sourcePath) {
21
22
  if (!sourcePath) {
@@ -71,21 +72,12 @@ async function saveHtmlResult(html, outputDir, name = 'output') {
71
72
 
72
73
  const server = new McpServer({
73
74
  name: 'agent-reader',
74
- version: '0.2.0',
75
+ version: '1.1.0',
75
76
  });
76
77
 
77
78
  server.registerTool(
78
79
  'render_markdown',
79
- {
80
- description: 'Render markdown text into styled HTML',
81
- inputSchema: {
82
- content: z.string().describe('Markdown source content'),
83
- source_path: z.string().optional().describe('Source markdown path for relative images'),
84
- theme: z.string().optional().describe('Theme name'),
85
- auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
86
- return_content: z.boolean().optional().describe('Return inline HTML content directly'),
87
- },
88
- },
80
+ MCP_TOOL_SCHEMAS.render_markdown,
89
81
  async ({ content, source_path, theme, return_content }) => {
90
82
  try {
91
83
  const baseDir = getBaseDirFromSourcePath(source_path);
@@ -142,15 +134,7 @@ server.registerTool(
142
134
 
143
135
  server.registerTool(
144
136
  'export_document',
145
- {
146
- description: 'Export markdown text into PDF or DOCX',
147
- inputSchema: {
148
- content: z.string().describe('Markdown source content'),
149
- source_path: z.string().optional().describe('Source markdown path for relative images'),
150
- format: z.enum(['pdf', 'docx']).describe('Export format'),
151
- return_content: z.boolean().optional().describe('Return file bytes as base64'),
152
- },
153
- },
137
+ MCP_TOOL_SCHEMAS.export_document,
154
138
  async ({ content, source_path, format, return_content }) => {
155
139
  try {
156
140
  const baseDir = getBaseDirFromSourcePath(source_path);
@@ -174,6 +158,7 @@ server.registerTool(
174
158
  outDir: outputDir,
175
159
  fileName: 'export.pdf',
176
160
  htmlPath,
161
+ sandbox: MCP_SANDBOX_MODE,
177
162
  });
178
163
  filePath = pdf.pdfPath;
179
164
  warnings = [...warnings, ...rendered.warnings, ...pdf.warnings];
@@ -223,15 +208,7 @@ server.registerTool(
223
208
 
224
209
  server.registerTool(
225
210
  'create_slideshow',
226
- {
227
- description: 'Create slideshow HTML from an image directory',
228
- inputSchema: {
229
- image_dir: z.string().describe('Absolute or relative image directory path'),
230
- auto_play: z.number().optional().describe('Autoplay interval in seconds'),
231
- auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
232
- return_content: z.boolean().optional().describe('Return inline HTML content directly'),
233
- },
234
- },
211
+ MCP_TOOL_SCHEMAS.create_slideshow,
235
212
  async ({ image_dir, auto_play, return_content }) => {
236
213
  try {
237
214
  const outputDir = await createOutputDir('create-slideshow');
@@ -278,16 +255,7 @@ server.registerTool(
278
255
 
279
256
  server.registerTool(
280
257
  'open_file',
281
- {
282
- description: 'Open a local file/path using user preference or explicit mode: web/word/pdf/ppt',
283
- inputSchema: {
284
- file_path: z.string().describe('File path or image directory path'),
285
- open_as: z.string().optional().describe('auto|web|word|pdf|ppt'),
286
- theme: z.string().optional().describe('theme for web rendering'),
287
- auto_play: z.number().optional().describe('auto play seconds for ppt mode'),
288
- return_content: z.boolean().optional().describe('return generated content directly'),
289
- },
290
- },
258
+ MCP_TOOL_SCHEMAS.open_file,
291
259
  async ({ file_path, open_as, theme, auto_play, return_content }) => {
292
260
  try {
293
261
  const preferences = await loadPreferences();
@@ -299,6 +267,7 @@ server.registerTool(
299
267
  autoPlay: auto_play,
300
268
  returnContent: Boolean(return_content),
301
269
  maxContentBytes: MAX_CONTENT_BYTES,
270
+ sandbox: MCP_SANDBOX_MODE,
302
271
  });
303
272
 
304
273
  const payload = {
@@ -319,13 +288,7 @@ server.registerTool(
319
288
 
320
289
  server.registerTool(
321
290
  'configure_user_preferences',
322
- {
323
- description: 'Set default open behavior for novice users',
324
- inputSchema: {
325
- default_open_mode: z.string().optional().describe('web|word|pdf|ppt'),
326
- default_theme: z.string().optional().describe('default web theme'),
327
- },
328
- },
291
+ MCP_TOOL_SCHEMAS.configure_user_preferences,
329
292
  async ({ default_open_mode, default_theme }) => {
330
293
  try {
331
294
  const updates = {};
@@ -352,10 +315,7 @@ server.registerTool(
352
315
 
353
316
  server.registerTool(
354
317
  'get_user_preferences',
355
- {
356
- description: 'Read current user preferences for open behavior',
357
- inputSchema: {},
358
- },
318
+ MCP_TOOL_SCHEMAS.get_user_preferences,
359
319
  async () => {
360
320
  try {
361
321
  const preferences = await loadPreferences();
@@ -0,0 +1,53 @@
1
+ import * as z from 'zod/v4';
2
+
3
+ export const MCP_TOOL_SCHEMAS = {
4
+ render_markdown: {
5
+ description: 'Render markdown text into styled HTML',
6
+ inputSchema: {
7
+ content: z.string().describe('Markdown source content'),
8
+ source_path: z.string().optional().describe('Source markdown path for relative images'),
9
+ theme: z.string().optional().describe('Theme name'),
10
+ auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
11
+ return_content: z.boolean().optional().describe('Return inline HTML content directly'),
12
+ },
13
+ },
14
+ export_document: {
15
+ description: 'Export markdown text into PDF or DOCX',
16
+ inputSchema: {
17
+ content: z.string().describe('Markdown source content'),
18
+ source_path: z.string().optional().describe('Source markdown path for relative images'),
19
+ format: z.enum(['pdf', 'docx']).describe('Export format'),
20
+ return_content: z.boolean().optional().describe('Return file bytes as base64'),
21
+ },
22
+ },
23
+ create_slideshow: {
24
+ description: 'Create slideshow HTML from an image directory',
25
+ inputSchema: {
26
+ image_dir: z.string().describe('Absolute or relative image directory path'),
27
+ auto_play: z.number().optional().describe('Autoplay interval in seconds'),
28
+ auto_open: z.boolean().optional().describe('Open output automatically (ignored in MCP)'),
29
+ return_content: z.boolean().optional().describe('Return inline HTML content directly'),
30
+ },
31
+ },
32
+ open_file: {
33
+ description: 'Open a local file/path using user preference or explicit mode: web/word/pdf/ppt',
34
+ inputSchema: {
35
+ file_path: z.string().describe('File path or image directory path'),
36
+ open_as: z.string().optional().describe('auto|web|word|pdf|ppt'),
37
+ theme: z.string().optional().describe('theme for web rendering'),
38
+ auto_play: z.number().optional().describe('auto play seconds for ppt mode'),
39
+ return_content: z.boolean().optional().describe('return generated content directly'),
40
+ },
41
+ },
42
+ configure_user_preferences: {
43
+ description: 'Set default open behavior for novice users',
44
+ inputSchema: {
45
+ default_open_mode: z.string().optional().describe('web|word|pdf|ppt'),
46
+ default_theme: z.string().optional().describe('default web theme'),
47
+ },
48
+ },
49
+ get_user_preferences: {
50
+ description: 'Read current user preferences for open behavior',
51
+ inputSchema: {},
52
+ },
53
+ };
@@ -0,0 +1,62 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ async function resolveWithNearestParent(targetPath) {
5
+ const absoluteTarget = path.resolve(targetPath);
6
+ try {
7
+ const realTarget = await fs.realpath(absoluteTarget);
8
+ return { ok: true, realTarget };
9
+ } catch (error) {
10
+ if (error?.code !== 'ENOENT') {
11
+ return { ok: false };
12
+ }
13
+ }
14
+
15
+ const missingSegments = [];
16
+ let cursor = absoluteTarget;
17
+
18
+ while (true) {
19
+ const parent = path.dirname(cursor);
20
+ if (parent === cursor) {
21
+ return { ok: false };
22
+ }
23
+ missingSegments.push(path.basename(cursor));
24
+ cursor = parent;
25
+
26
+ try {
27
+ const realParent = await fs.realpath(cursor);
28
+ return {
29
+ ok: true,
30
+ realTarget: path.join(realParent, ...missingSegments.reverse()),
31
+ };
32
+ } catch (error) {
33
+ if (error?.code !== 'ENOENT') {
34
+ return { ok: false };
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ export async function assertWithinBase(targetPath, baseDir) {
41
+ if (!baseDir) {
42
+ return false;
43
+ }
44
+
45
+ let realBase;
46
+ try {
47
+ realBase = await fs.realpath(path.resolve(baseDir));
48
+ } catch {
49
+ return false;
50
+ }
51
+
52
+ const resolved = await resolveWithNearestParent(targetPath);
53
+ if (!resolved.ok) {
54
+ return false;
55
+ }
56
+
57
+ const relative = path.relative(realBase, resolved.realTarget);
58
+ if (!relative) {
59
+ return true;
60
+ }
61
+ return !relative.startsWith('..') && !path.isAbsolute(relative);
62
+ }
@@ -2,7 +2,7 @@ import http from 'node:http';
2
2
  import net from 'node:net';
3
3
  import { promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
- import { exportDOCXFromHTML, exportPDF } from '../core/exporter.js';
5
+ import { exportDOCXFromHTML, exportPDF, resolveSandboxMode } from '../core/exporter.js';
6
6
 
7
7
  const MIME_TYPES = {
8
8
  '.html': 'text/html; charset=utf-8',
@@ -94,7 +94,7 @@ async function resolveExportSourcePath(req, rootDir, sourceParam) {
94
94
  throw createHttpError(400, 'missing source path');
95
95
  }
96
96
 
97
- async function handleExportRequest(req, res, rootDir, urlObject) {
97
+ async function handleExportRequest(req, res, rootDir, urlObject, sandbox) {
98
98
  if (req.method !== 'GET') {
99
99
  sendJson(res, 405, { error: 'method not allowed' });
100
100
  return;
@@ -131,6 +131,7 @@ async function handleExportRequest(req, res, rootDir, urlObject) {
131
131
  fileName: `${sourceName}.pdf`,
132
132
  htmlPath: sourcePath,
133
133
  landscape: isLandscape,
134
+ sandbox,
134
135
  });
135
136
  outputPath = result.pdfPath;
136
137
  warnings = result.warnings || [];
@@ -140,6 +141,7 @@ async function handleExportRequest(req, res, rootDir, urlObject) {
140
141
  baseDir: sourceDir,
141
142
  outDir: sourceDir,
142
143
  fileName: `${sourceName}.docx`,
144
+ sandbox,
143
145
  });
144
146
  outputPath = result.docxPath;
145
147
  warnings = result.warnings || [];
@@ -190,14 +192,18 @@ async function isPortInUse(host, port) {
190
192
  });
191
193
  }
192
194
 
193
- export async function startStaticServer(rootDir, { host = '127.0.0.1', port = 3000 } = {}) {
195
+ export async function startStaticServer(
196
+ rootDir,
197
+ { host = '127.0.0.1', port = 3000, sandbox } = {},
198
+ ) {
194
199
  const absoluteRoot = path.resolve(rootDir);
200
+ const resolvedSandbox = resolveSandboxMode(sandbox, process.env);
195
201
 
196
202
  const server = http.createServer(async (req, res) => {
197
203
  const urlObject = new URL(req.url || '/', `http://${host}:${port}`);
198
204
  try {
199
205
  if (urlObject.pathname === '/api/export') {
200
- await handleExportRequest(req, res, absoluteRoot, urlObject);
206
+ await handleExportRequest(req, res, absoluteRoot, urlObject, resolvedSandbox);
201
207
  return;
202
208
  }
203
209
 
@@ -1,54 +0,0 @@
1
- :root {
2
- color-scheme: light;
3
- }
4
-
5
- html,
6
- body {
7
- margin: 0;
8
- padding: 0;
9
- color: #111;
10
- background: #fff;
11
- font-family: "Noto Serif SC", "Songti SC", "SimSun", Georgia, serif;
12
- }
13
-
14
- .page {
15
- max-width: 100%;
16
- margin: 0;
17
- padding: 0;
18
- }
19
-
20
- .markdown-body {
21
- border: none;
22
- border-radius: 0;
23
- padding: 0;
24
- box-shadow: none;
25
- }
26
-
27
- .markdown-body pre,
28
- .markdown-body code {
29
- background: #f5f5f5;
30
- color: #1f2937;
31
- }
32
-
33
- @media print {
34
- #sidebar,
35
- .doc-toolbar {
36
- display: none !important;
37
- }
38
-
39
- body {
40
- display: block !important;
41
- height: auto !important;
42
- overflow: visible !important;
43
- }
44
-
45
- #content-wrapper {
46
- padding: 0 !important;
47
- overflow: visible !important;
48
- }
49
-
50
- .doc-inner {
51
- max-width: none !important;
52
- padding: 0 !important;
53
- }
54
- }